diff options
| author | David Göransson <david.goransson@mullvad.net> | 2025-04-14 15:58:38 +0200 |
|---|---|---|
| committer | David Göransson <david.goransson@mullvad.net> | 2025-04-16 14:33:50 +0200 |
| commit | 7a5346a49ab661b8abfed89024dddadbbdacb62e (patch) | |
| tree | 3c63a26550bf1d01c1f0a87419b95e6634403c35 /android/test | |
| parent | 9babff88e7180d4b363afb48f513654e6b8126a3 (diff) | |
| download | mullvadvpn-7a5346a49ab661b8abfed89024dddadbbdacb62e.tar.xz mullvadvpn-7a5346a49ab661b8abfed89024dddadbbdacb62e.zip | |
Replace Volley with ktor
Diffstat (limited to 'android/test')
20 files changed, 582 insertions, 518 deletions
diff --git a/android/test/e2e/build.gradle.kts b/android/test/e2e/build.gradle.kts index 8bcc45ab77..0fdc7448b0 100644 --- a/android/test/e2e/build.gradle.kts +++ b/android/test/e2e/build.gradle.kts @@ -145,7 +145,6 @@ dependencies { implementation(libs.androidx.test.runner) implementation(libs.androidx.test.rules) implementation(libs.androidx.test.uiautomator) - implementation(libs.android.volley) implementation(libs.kermit) implementation(Dependencies.junitJupiterApi) implementation(Dependencies.junit5AndroidTestExtensions) @@ -153,8 +152,11 @@ dependencies { implementation(libs.kotlin.stdlib) implementation(libs.ktor.client.core) implementation(libs.ktor.client.cio) + implementation(libs.ktor.client.auth) + implementation(libs.ktor.client.logging) implementation(libs.ktor.serialization.kotlinx.json) implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.client.resources) androidTestUtil(libs.androidx.test.orchestrator) 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 index 688b83c875..e7761b163f 100644 --- 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 @@ -2,7 +2,7 @@ package net.mullvad.mullvadvpn.test.e2e import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest import net.mullvad.mullvadvpn.BuildConfig import net.mullvad.mullvadvpn.test.common.constant.EXTREMELY_LONG_TIMEOUT import net.mullvad.mullvadvpn.test.common.page.ConnectPage @@ -16,10 +16,9 @@ import net.mullvad.mullvadvpn.test.common.page.enableShadowsocksStory import net.mullvad.mullvadvpn.test.common.page.on import net.mullvad.mullvadvpn.test.common.rule.ForgetAllVpnAppsInSettingsTestRule import net.mullvad.mullvadvpn.test.e2e.annotations.HasDependencyOnLocalAPI +import net.mullvad.mullvadvpn.test.e2e.api.connectioncheck.ConnectionCheckApi import net.mullvad.mullvadvpn.test.e2e.misc.AccountTestRule import net.mullvad.mullvadvpn.test.e2e.misc.ClearFirewallRules -import net.mullvad.mullvadvpn.test.e2e.misc.ConnCheckState -import net.mullvad.mullvadvpn.test.e2e.misc.SimpleMullvadHttpClient import net.mullvad.mullvadvpn.test.e2e.router.firewall.DropRule import net.mullvad.mullvadvpn.test.e2e.router.firewall.FirewallClient import org.junit.jupiter.api.Assertions.assertEquals @@ -34,6 +33,7 @@ class ConnectionTest : EndToEndTest(BuildConfig.FLAVOR_infrastructure) { @JvmField val forgetAllVpnAppsInSettingsTestRule = ForgetAllVpnAppsInSettingsTestRule() + private val connCheckClient = ConnectionCheckApi() private val firewallClient = FirewallClient() @Test @@ -49,7 +49,7 @@ class ConnectionTest : EndToEndTest(BuildConfig.FLAVOR_infrastructure) { } @Test - fun testConnectAndVerifyWithConnectionCheck() { + fun testConnectAndVerifyWithConnectionCheck() = runTest { // Given app.launchAndEnsureLoggedIn(accountTestRule.validAccountNumber) @@ -57,22 +57,23 @@ class ConnectionTest : EndToEndTest(BuildConfig.FLAVOR_infrastructure) { on<SystemVpnConfigurationAlert> { clickOk() } - var expectedConnectionState: ConnCheckState? = null + var outIpv4Address = "" on<ConnectPage> { waitForConnectedLabel() - expectedConnectionState = ConnCheckState(true, extractOutIpv4Address()) + outIpv4Address = extractOutIpv4Address() } // Then - val result = SimpleMullvadHttpClient(targetContext).runConnectionCheck() - assertEquals(expectedConnectionState, result) + val result = connCheckClient.connectionCheck() + + assertEquals(result.ip, outIpv4Address) } @Test @HasDependencyOnLocalAPI @ClearFirewallRules - fun testWireGuardObfuscationAutomatic() = runBlocking { + fun testWireGuardObfuscationAutomatic() = runTest { app.launchAndEnsureLoggedIn(accountTestRule.validAccountNumber) on<ConnectPage> { enableLocalNetworkSharingStory() } @@ -109,7 +110,7 @@ class ConnectionTest : EndToEndTest(BuildConfig.FLAVOR_infrastructure) { @Test @HasDependencyOnLocalAPI @ClearFirewallRules - fun testWireGuardObfuscationOff() = runBlocking { + fun testWireGuardObfuscationOff() = runTest { app.launchAndEnsureLoggedIn(accountTestRule.validAccountNumber) on<ConnectPage> { enableLocalNetworkSharingStory() } @@ -161,87 +162,85 @@ class ConnectionTest : EndToEndTest(BuildConfig.FLAVOR_infrastructure) { @Test @HasDependencyOnLocalAPI @ClearFirewallRules - fun testUDPOverTCP() = - runBlocking<Unit> { - app.launchAndEnsureLoggedIn(accountTestRule.validAccountNumber) - on<ConnectPage> { enableLocalNetworkSharingStory() } + fun testUDPOverTCP() = runTest { + app.launchAndEnsureLoggedIn(accountTestRule.validAccountNumber) + on<ConnectPage> { enableLocalNetworkSharingStory() } - on<ConnectPage> { clickSelectLocation() } + on<ConnectPage> { clickSelectLocation() } - on<SelectLocationPage> { - clickLocationExpandButton(DEFAULT_COUNTRY) - clickLocationExpandButton(DEFAULT_CITY) - clickLocationCell(DEFAULT_RELAY) - } + on<SelectLocationPage> { + clickLocationExpandButton(DEFAULT_COUNTRY) + clickLocationExpandButton(DEFAULT_CITY) + clickLocationCell(DEFAULT_RELAY) + } - on<SystemVpnConfigurationAlert> { clickOk() } + on<SystemVpnConfigurationAlert> { clickOk() } - var relayIpAddress: String? = null + var relayIpAddress: String? = null - on<ConnectPage> { - waitForConnectedLabel() - relayIpAddress = extractInIpv4Address() - clickDisconnect() - } + on<ConnectPage> { + waitForConnectedLabel() + relayIpAddress = extractInIpv4Address() + clickDisconnect() + } - // Block UDP traffic to the relay - val firewallRule = DropRule.blockUDPTrafficRule(relayIpAddress!!) - firewallClient.createRule(firewallRule) + // Block UDP traffic to the relay + val firewallRule = DropRule.blockUDPTrafficRule(relayIpAddress!!) + firewallClient.createRule(firewallRule) - // Enable UDP-over-TCP - on<ConnectPage> { clickSettings() } + // Enable UDP-over-TCP + on<ConnectPage> { clickSettings() } - on<SettingsPage> { clickVpnSettings() } + on<SettingsPage> { clickVpnSettings() } - on<VpnSettingsPage> { - scrollUntilWireGuardObfuscationUdpOverTcpCell() - clickWireguardObfuscationUdpOverTcpCell() - } + on<VpnSettingsPage> { + scrollUntilWireGuardObfuscationUdpOverTcpCell() + clickWireguardObfuscationUdpOverTcpCell() + } - device.pressBack() - device.pressBack() + device.pressBack() + device.pressBack() - on<ConnectPage> { - clickConnect() - waitForConnectedLabel(timeout = EXTREMELY_LONG_TIMEOUT) - clickDisconnect() - } + on<ConnectPage> { + clickConnect() + waitForConnectedLabel(timeout = EXTREMELY_LONG_TIMEOUT) + clickDisconnect() } + } @Test @HasDependencyOnLocalAPI @ClearFirewallRules - fun testShadowsocks() = - runBlocking<Unit> { - app.launchAndEnsureLoggedIn(accountTestRule.validAccountNumber) - on<ConnectPage> { enableLocalNetworkSharingStory() } + fun testShadowsocks() = runTest { + app.launchAndEnsureLoggedIn(accountTestRule.validAccountNumber) + on<ConnectPage> { enableLocalNetworkSharingStory() } - on<ConnectPage> { disableObfuscationStory() } + on<ConnectPage> { disableObfuscationStory() } - // Block all WireGuard traffic - val firewallRule = DropRule.blockWireGuardTrafficRule(ANY_IP_ADDRESS) - firewallClient.createRule(firewallRule) + // Block all WireGuard traffic + val firewallRule = DropRule.blockWireGuardTrafficRule(ANY_IP_ADDRESS) + firewallClient.createRule(firewallRule) - on<ConnectPage> { clickConnect() } + on<ConnectPage> { clickConnect() } - on<SystemVpnConfigurationAlert> { clickOk() } + on<SystemVpnConfigurationAlert> { clickOk() } - // Ensure it is not possible to connect to relay - on<ConnectPage> { - delay(UNSUCCESSFUL_CONNECTION_TIMEOUT.milliseconds) - waitForConnectingLabel() - clickCancel() - } + // Ensure it is not possible to connect to relay + on<ConnectPage> { + delay(UNSUCCESSFUL_CONNECTION_TIMEOUT.milliseconds) + waitForConnectingLabel() + clickCancel() + } - on<ConnectPage> { enableShadowsocksStory() } + on<ConnectPage> { enableShadowsocksStory() } - // Ensure we can now connect with Shadowsocks enabled - on<ConnectPage> { - clickConnect() - waitForConnectedLabel(timeout = EXTREMELY_LONG_TIMEOUT) - clickDisconnect() - } + // Ensure we can now connect with Shadowsocks enabled + on<ConnectPage> { + clickConnect() + waitForConnectedLabel(timeout = EXTREMELY_LONG_TIMEOUT) + clickDisconnect() } + } companion object { const val VERY_FORGIVING_WIREGUARD_OFF_CONNECTION_TIMEOUT = 60000L diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/LeakTest.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/LeakTest.kt index 79471a2363..9157e10bdc 100644 --- a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/LeakTest.kt +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/LeakTest.kt @@ -2,7 +2,7 @@ package net.mullvad.mullvadvpn.test.e2e import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest import net.mullvad.mullvadvpn.test.common.misc.Attachment import net.mullvad.mullvadvpn.test.common.page.ConnectPage import net.mullvad.mullvadvpn.test.common.page.SelectLocationPage @@ -64,173 +64,164 @@ class LeakTest : EndToEndTest(BuildConfig.FLAVOR_infrastructure) { @Test @HasDependencyOnLocalAPI - fun testEnsureNoLeaksToSpecificHost() = - runBlocking<Unit> { - app.launch() + fun testEnsureNoLeaksToSpecificHost() = runTest { + app.launch() - on<ConnectPage> { - waitForDisconnectedLabel() + on<ConnectPage> { + waitForDisconnectedLabel() - clickSelectLocation() - } + clickSelectLocation() + } - on<SelectLocationPage> { - clickLocationExpandButton(DEFAULT_COUNTRY) - clickLocationExpandButton(DEFAULT_CITY) - clickLocationCell(DEFAULT_RELAY) - } + on<SelectLocationPage> { + clickLocationExpandButton(DEFAULT_COUNTRY) + clickLocationExpandButton(DEFAULT_CITY) + clickLocationCell(DEFAULT_RELAY) + } - on<SystemVpnConfigurationAlert> { clickOk() } + on<SystemVpnConfigurationAlert> { clickOk() } - on<ConnectPage> { waitForConnectedLabel() } + on<ConnectPage> { waitForConnectedLabel() } - // Capture generated traffic to a specific host - val targetIpAddress = BuildConfig.TRAFFIC_GENERATION_IP_ADDRESS - val targetPort = 80 - val captureResult = - PacketCapture().capturePackets { - TrafficGenerator(targetIpAddress, targetPort).generateTraffic(10.milliseconds) { - // Give it some time for generating traffic - delay(3000) - } + // Capture generated traffic to a specific host + val targetIpAddress = BuildConfig.TRAFFIC_GENERATION_IP_ADDRESS + val targetPort = 80 + val captureResult = + PacketCapture().capturePackets { + TrafficGenerator(targetIpAddress, targetPort).generateTraffic(10.milliseconds) { + // Give it some time for generating traffic + delay(3000) } + } - on<ConnectPage> { clickDisconnect() } + on<ConnectPage> { clickDisconnect() } - val capturedStreams = captureResult.streams - val capturedPcap = captureResult.pcap - val timestamp = System.currentTimeMillis() - Attachment.saveAttachment( - "capture-${javaClass.enclosingMethod}-$timestamp.pcap", - capturedPcap, - ) + val capturedStreams = captureResult.streams + val capturedPcap = captureResult.pcap + val timestamp = System.currentTimeMillis() + Attachment.saveAttachment( + "capture-${javaClass.enclosingMethod}-$timestamp.pcap", + capturedPcap, + ) - NetworkTrafficChecker.checkTrafficStreamsAgainstRules( - capturedStreams, - NoTrafficToHostRule(targetIpAddress), - ) - } + NetworkTrafficChecker.checkTrafficStreamsAgainstRules( + capturedStreams, + NoTrafficToHostRule(targetIpAddress), + ) + } @Test @HasDependencyOnLocalAPI - fun testEnsureLeaksToSpecificHost() = - runBlocking<Unit> { - app.launch() - - on<ConnectPage> { - waitForDisconnectedLabel() + fun testEnsureLeaksToSpecificHost() = runTest { + app.launch() - clickSelectLocation() - } + on<ConnectPage> { + waitForDisconnectedLabel() - on<SelectLocationPage> { - clickLocationExpandButton(DEFAULT_COUNTRY) - clickLocationExpandButton(DEFAULT_CITY) - clickLocationCell(DEFAULT_RELAY) - } + clickSelectLocation() + } - on<SystemVpnConfigurationAlert> { clickOk() } + on<SelectLocationPage> { + clickLocationExpandButton(DEFAULT_COUNTRY) + clickLocationExpandButton(DEFAULT_CITY) + clickLocationCell(DEFAULT_RELAY) + } - on<ConnectPage> { waitForConnectedLabel() } + on<SystemVpnConfigurationAlert> { clickOk() } - // Capture generated traffic to a specific host - val targetIpAddress = BuildConfig.TRAFFIC_GENERATION_IP_ADDRESS - val targetPort = 80 - val captureResult: PacketCaptureResult = - PacketCapture().capturePackets { - TrafficGenerator(targetIpAddress, targetPort).generateTraffic(10.milliseconds) { - delay( - 3000.milliseconds - ) // Give it some time for generating traffic in tunnel + on<ConnectPage> { waitForConnectedLabel() } - on<ConnectPage> { clickDisconnect() } + // Capture generated traffic to a specific host + val targetIpAddress = BuildConfig.TRAFFIC_GENERATION_IP_ADDRESS + val targetPort = 80 + val captureResult: PacketCaptureResult = + PacketCapture().capturePackets { + TrafficGenerator(targetIpAddress, targetPort).generateTraffic(10.milliseconds) { + delay(3000.milliseconds) // Give it some time for generating traffic in tunnel - delay( - 2000.milliseconds - ) // Give it some time to leak traffic outside of tunnel + on<ConnectPage> { clickDisconnect() } - on<ConnectPage> { - clickConnect() - waitForConnectedLabel() - } + delay(2000.milliseconds) // Give it some time to leak traffic outside of tunnel - delay( - 3000.milliseconds - ) // Give it some time for generating traffic in tunnel + on<ConnectPage> { + clickConnect() + waitForConnectedLabel() } + + delay(3000.milliseconds) // Give it some time for generating traffic in tunnel } + } - on<ConnectPage> { clickDisconnect() } + on<ConnectPage> { clickDisconnect() } - val capturedStreams = captureResult.streams - val capturedPcap = captureResult.pcap - val timestamp = System.currentTimeMillis() - Attachment.saveAttachment( - "capture-${javaClass.enclosingMethod}-$timestamp.pcap", - capturedPcap, - ) + val capturedStreams = captureResult.streams + val capturedPcap = captureResult.pcap + val timestamp = System.currentTimeMillis() + Attachment.saveAttachment( + "capture-${javaClass.enclosingMethod}-$timestamp.pcap", + capturedPcap, + ) - NetworkTrafficChecker.checkTrafficStreamsAgainstRules( - capturedStreams, - SomeTrafficToHostRule(targetIpAddress), - SomeTrafficToOtherHostsRule(targetIpAddress), - ) - } + NetworkTrafficChecker.checkTrafficStreamsAgainstRules( + capturedStreams, + SomeTrafficToHostRule(targetIpAddress), + SomeTrafficToOtherHostsRule(targetIpAddress), + ) + } @Test @HasDependencyOnLocalAPI @Disabled("Disabled due to problems finding: lazy_list_vpn_settings_test_tag") - fun testEnsureNoLeaksToSpecificHostWhenSwitchingBetweenVariousVpnSettings() = - runBlocking<Unit> { - app.launch() - // Obfuscation and Post-Quantum are by default set to automatic. Explicitly set to off. - on<ConnectPage> { disableObfuscationStory() } - on<ConnectPage> { disablePostQuantumStory() } - on<ConnectPage> { clickSelectLocation() } + fun testEnsureNoLeaksToSpecificHostWhenSwitchingBetweenVariousVpnSettings() = runTest { + app.launch() + // Obfuscation and Post-Quantum are by default set to automatic. Explicitly set to off. + on<ConnectPage> { disableObfuscationStory() } + on<ConnectPage> { disablePostQuantumStory() } + on<ConnectPage> { clickSelectLocation() } - on<SelectLocationPage> { - clickLocationExpandButton(DAITA_COMPATIBLE_COUNTRY) - clickLocationExpandButton(DAITA_COMPATIBLE_CITY) - clickLocationCell(DAITA_COMPATIBLE_RELAY) - } + on<SelectLocationPage> { + clickLocationExpandButton(DAITA_COMPATIBLE_COUNTRY) + clickLocationExpandButton(DAITA_COMPATIBLE_CITY) + clickLocationCell(DAITA_COMPATIBLE_RELAY) + } - on<SystemVpnConfigurationAlert> { clickOk() } + on<SystemVpnConfigurationAlert> { clickOk() } - on<ConnectPage> { waitForConnectedLabel() } + on<ConnectPage> { waitForConnectedLabel() } - // Capture generated traffic to a specific host - val targetIpAddress = BuildConfig.TRAFFIC_GENERATION_IP_ADDRESS - val targetPort = 80 - val captureResult: PacketCaptureResult = - PacketCapture().capturePackets { - TrafficGenerator(targetIpAddress, targetPort).generateTraffic(10.milliseconds) { - delay( - 1000.milliseconds - ) // Give it some time for generating traffic in tunnel before changing - // settings + // Capture generated traffic to a specific host + val targetIpAddress = BuildConfig.TRAFFIC_GENERATION_IP_ADDRESS + val targetPort = 80 + val captureResult: PacketCaptureResult = + PacketCapture().capturePackets { + TrafficGenerator(targetIpAddress, targetPort).generateTraffic(10.milliseconds) { + delay( + 1000.milliseconds + ) // Give it some time for generating traffic in tunnel before changing + // settings - on<ConnectPage> { enableDAITAStory() } - on<ConnectPage> { enableShadowsocksStory() } - on<ConnectPage> { waitForConnectedLabel() } + on<ConnectPage> { enableDAITAStory() } + on<ConnectPage> { enableShadowsocksStory() } + on<ConnectPage> { waitForConnectedLabel() } - delay( - 1000.milliseconds - ) // Give it some time for generating traffic in tunnel after enabling - // settings - } + delay( + 1000.milliseconds + ) // Give it some time for generating traffic in tunnel after enabling + // settings } + } - val capturedStreams = captureResult.streams - val capturedPcap = captureResult.pcap - val timestamp = System.currentTimeMillis() - Attachment.saveAttachment( - "capture-${javaClass.enclosingMethod}-$timestamp.pcap", - capturedPcap, - ) + val capturedStreams = captureResult.streams + val capturedPcap = captureResult.pcap + val timestamp = System.currentTimeMillis() + Attachment.saveAttachment( + "capture-${javaClass.enclosingMethod}-$timestamp.pcap", + capturedPcap, + ) - NetworkTrafficChecker.checkTrafficStreamsAgainstRules( - capturedStreams, - NoTrafficToHostRule(targetIpAddress), - ) - } + NetworkTrafficChecker.checkTrafficStreamsAgainstRules( + capturedStreams, + NoTrafficToHostRule(targetIpAddress), + ) + } } diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/api/connectioncheck/ConnCheckResult.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/api/connectioncheck/ConnCheckResult.kt new file mode 100644 index 0000000000..f47dc5fd1e --- /dev/null +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/api/connectioncheck/ConnCheckResult.kt @@ -0,0 +1,15 @@ +package net.mullvad.mullvadvpn.test.e2e.api.connectioncheck + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ConnCheckResult( + @SerialName("mullvad_exit_ip") val mullvadExitIp: Boolean, + val ip: String, + val organization: String, + val country: String, + val city: String, + val longitude: Double, + val latitude: Double, +) diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/api/connectioncheck/ConnectionCheckApi.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/api/connectioncheck/ConnectionCheckApi.kt new file mode 100644 index 0000000000..f9da6beb2f --- /dev/null +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/api/connectioncheck/ConnectionCheckApi.kt @@ -0,0 +1,50 @@ +package net.mullvad.mullvadvpn.test.e2e.api.connectioncheck + +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.defaultRequest +import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logging +import io.ktor.client.request.get +import io.ktor.http.ContentType +import io.ktor.http.URLProtocol.Companion.HTTPS +import io.ktor.http.contentType +import io.ktor.http.path +import io.ktor.serialization.kotlinx.json.json +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import net.mullvad.mullvadvpn.test.e2e.BuildConfig +import net.mullvad.mullvadvpn.test.e2e.misc.KermitLogger + +class ConnectionCheckApi { + private val client: HttpClient = + HttpClient(CIO) { + install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) } + install(Logging) { + logger = KermitLogger() + level = LogLevel.INFO + } + defaultRequest { + url { + protocol = HTTPS + host = BASE_URL + } + contentType(ContentType.Application.Json) + } + expectSuccess = true + } + + suspend fun connectionCheck(): ConnCheckResult = + withContext(Dispatchers.IO) { + client.get { url { path(JSON_PATH) } }.body<ConnCheckResult>() + } + + companion object { + // Connection check + private const val BASE_URL = "am.i.${BuildConfig.INFRASTRUCTURE_BASE_DOMAIN}" + private const val JSON_PATH = "json" + } +} diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/api/connectioncheck/ConnectionCheckApiTest.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/api/connectioncheck/ConnectionCheckApiTest.kt new file mode 100644 index 0000000000..91d2b09ee7 --- /dev/null +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/api/connectioncheck/ConnectionCheckApiTest.kt @@ -0,0 +1,19 @@ +package net.mullvad.mullvadvpn.test.e2e.api.connectioncheck + +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertNotNull + +@Disabled("Only used developing the ConnectionCheckApi") +class ConnectionCheckApiTest { + private val connCheckApi = ConnectionCheckApi() + + @Test + fun testConnCheck() = runTest { + val result = connCheckApi.connectionCheck() + assertNotNull(result) + assertFalse(result.mullvadExitIp) + } +} diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/api/mullvad/MullvadApi.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/api/mullvad/MullvadApi.kt new file mode 100644 index 0000000000..d24a6d3c92 --- /dev/null +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/api/mullvad/MullvadApi.kt @@ -0,0 +1,102 @@ +package net.mullvad.mullvadvpn.test.e2e.api.mullvad + +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.defaultRequest +import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logging +import io.ktor.client.plugins.resources.Resources +import io.ktor.client.request.bearerAuth +import io.ktor.client.request.delete +import io.ktor.client.request.get +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.URLProtocol.Companion.HTTPS +import io.ktor.http.contentType +import io.ktor.http.path +import io.ktor.serialization.kotlinx.json.json +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.withContext +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import net.mullvad.mullvadvpn.test.e2e.BuildConfig +import net.mullvad.mullvadvpn.test.e2e.misc.KermitLogger + +class MullvadApi { + private val client: HttpClient = + HttpClient(CIO) { + install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) } + install(Resources) + install(Logging) { + logger = KermitLogger() + level = LogLevel.INFO + sanitizeHeader { header -> header == HttpHeaders.Authorization } + } + + defaultRequest { + url { + protocol = HTTPS + host = BASE_URL + } + contentType(ContentType.Application.Json) + } + expectSuccess = true + } + + suspend fun login(accountNumber: String): String = + withContext(Dispatchers.IO) { + client + .post { + url { path(AUTH_PATH) } + setBody(LoginRequest(accountNumber)) + } + .body<LoginResponse>() + .accessToken + } + + @Serializable data class Device(val name: String, val id: String) + + suspend fun getDeviceList(accessToken: String): List<String> = + withContext(Dispatchers.IO) { + client + .get { + url { path(DEVICES_PATH) } + bearerAuth(accessToken) + } + .body<List<Device>>() + .map { it.id } + } + + suspend fun removeDevice(accessToken: String, deviceId: String) = + withContext(Dispatchers.IO) { + client.delete { + url { path("$DEVICES_PATH/$deviceId") } + bearerAuth(accessToken) + } + } + + companion object { + private const val BASE_URL = "api.${BuildConfig.INFRASTRUCTURE_BASE_DOMAIN}" + private const val AUTH_PATH = "auth/${BuildConfig.API_VERSION}/token" + private const val DEVICES_PATH = "accounts/${BuildConfig.API_VERSION}/devices" + } +} + +@Serializable data class LoginRequest(@SerialName("account_number") val accountNumber: String) + +@Serializable data class LoginResponse(@SerialName("access_token") val accessToken: String) + +suspend fun MullvadApi.removeAllDevices(accessToken: String) = + withContext(Dispatchers.IO) { + val token = login(accessToken) + val devices = getDeviceList(token) + + devices.map { async { removeDevice(token, it) } }.awaitAll() + } diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/api/mullvad/MullvadApiTest.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/api/mullvad/MullvadApiTest.kt new file mode 100644 index 0000000000..734a371041 --- /dev/null +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/api/mullvad/MullvadApiTest.kt @@ -0,0 +1,28 @@ +package net.mullvad.mullvadvpn.test.e2e.api.mullvad + +import kotlinx.coroutines.test.runTest +import net.mullvad.mullvadvpn.test.e2e.misc.AccountProvider +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow + +@Disabled("Only used developing the MullvadApi") +class MullvadApiTest { + private val mullvadApi = MullvadApi() + + @Test + fun testLogin() = runTest { + val validAccountNumber = AccountProvider.getValidAccountNumber() + val accessToken = assertDoesNotThrow { mullvadApi.login(validAccountNumber) } + assertTrue(accessToken.isNotBlank()) + } + + @Test + fun testGetDeviceList() = runTest { + val validAccountNumber = AccountProvider.getValidAccountNumber() + val accessToken = assertDoesNotThrow { mullvadApi.login(validAccountNumber) } + assertTrue(accessToken.isNotBlank()) + assertDoesNotThrow { mullvadApi.getDeviceList(accessToken) } + } +} diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/api/partner/PartnerApi.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/api/partner/PartnerApi.kt new file mode 100644 index 0000000000..647bb49b20 --- /dev/null +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/api/partner/PartnerApi.kt @@ -0,0 +1,73 @@ +package net.mullvad.mullvadvpn.test.e2e.api.partner + +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.defaultRequest +import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logging +import io.ktor.client.request.headers +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.client.statement.bodyAsText +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.URLProtocol.Companion.HTTPS +import io.ktor.http.contentType +import io.ktor.http.path +import io.ktor.serialization.kotlinx.json.json +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import net.mullvad.mullvadvpn.test.e2e.BuildConfig +import net.mullvad.mullvadvpn.test.e2e.misc.KermitLogger + +class PartnerApi(base64AuthCredentials: String) { + private val client: HttpClient = + HttpClient(CIO) { + install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) } + install(Logging) { + logger = KermitLogger() + level = LogLevel.INFO + sanitizeHeader { header -> header == HttpHeaders.Authorization } + } + + defaultRequest { + url { + protocol = HTTPS + host = BASE_URL + } + contentType(ContentType.Application.Json) + + headers { append("Authorization", "Basic $base64AuthCredentials") } + } + expectSuccess = true + } + + suspend fun createAccount(): String = + withContext(Dispatchers.IO) { + client.post { url { path(ACCOUNT_PATH) } }.body<CreateAccountResponse>().id + } + + suspend fun addTime(accountNumber: String, daysToAdd: Int) = + withContext(Dispatchers.IO) { + val request = AddTimeRequest(daysToAdd) + client + .post { + url { path("$ACCOUNT_PATH/$accountNumber/extend") } + setBody(request) + } + .bodyAsText() + } + + companion object { + private const val BASE_URL = "partner.${BuildConfig.INFRASTRUCTURE_BASE_DOMAIN}" + private const val ACCOUNT_PATH = "${BuildConfig.API_VERSION}/accounts" + } +} + +@Serializable data class CreateAccountResponse(val id: String) + +@Serializable data class AddTimeRequest(val days: Int) diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/api/partner/PartnerApiTest.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/api/partner/PartnerApiTest.kt new file mode 100644 index 0000000000..d6f91bb749 --- /dev/null +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/api/partner/PartnerApiTest.kt @@ -0,0 +1,27 @@ +package net.mullvad.mullvadvpn.test.e2e.api.partner + +import androidx.test.platform.app.InstrumentationRegistry +import kotlinx.coroutines.test.runTest +import net.mullvad.mullvadvpn.test.e2e.constant.PARTNER_AUTH +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow + +@Disabled("Only used developing the PartnerApi") +class PartnerApiTest { + private val partnerApi = + PartnerApi(InstrumentationRegistry.getArguments().getString(PARTNER_AUTH, null)) + + @Test + fun testCreateAccount() = runTest { + val accessToken = assertDoesNotThrow { partnerApi.createAccount() } + assertTrue(accessToken.isNotBlank()) + } + + @Test + fun testAddTime() = runTest { + val accessToken = assertDoesNotThrow { partnerApi.createAccount() } + assertDoesNotThrow { partnerApi.addTime(accessToken, 1) } + } +} diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/constant/UrlConstants.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/constant/UrlConstants.kt deleted file mode 100644 index 70b06f9697..0000000000 --- a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/constant/UrlConstants.kt +++ /dev/null @@ -1,17 +0,0 @@ -package net.mullvad.mullvadvpn.test.e2e.constant - -import net.mullvad.mullvadvpn.test.e2e.BuildConfig - -// API URLs -const val API_BASE_URL = "https://api.${BuildConfig.INFRASTRUCTURE_BASE_DOMAIN}" -const val AUTH_URL = "$API_BASE_URL/auth/${BuildConfig.API_VERSION}/token" -const val ACCOUNT_URL = "$API_BASE_URL/accounts/${BuildConfig.API_VERSION}/accounts" -const val DEVICE_LIST_URL = "$API_BASE_URL/accounts/${BuildConfig.API_VERSION}/devices" - -// Partner URLs -const val PARTNER_BASE_URL = - "https://partner.${BuildConfig.INFRASTRUCTURE_BASE_DOMAIN}/${BuildConfig.API_VERSION}" -const val PARTNER_ACCOUNT_URL = "$PARTNER_BASE_URL/accounts" - -// Connection check -const val CONN_CHECK_URL = "https://am.i.${BuildConfig.INFRASTRUCTURE_BASE_DOMAIN}/json" 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 deleted file mode 100644 index b4b3f19e05..0000000000 --- a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/interactor/MullvadAccountInteractor.kt +++ /dev/null @@ -1,12 +0,0 @@ -package net.mullvad.mullvadvpn.test.e2e.interactor - -import net.mullvad.mullvadvpn.test.e2e.misc.SimpleMullvadHttpClient - -class MullvadAccountInteractor( - private val httpClient: SimpleMullvadHttpClient, - private val testAccountNumber: String, -) { - fun cleanupAccount() { - httpClient.removeAllDevices(testAccountNumber) - } -} diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/AccountProvider.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/AccountProvider.kt new file mode 100644 index 0000000000..61fc023ade --- /dev/null +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/AccountProvider.kt @@ -0,0 +1,36 @@ +package net.mullvad.mullvadvpn.test.e2e.misc + +import androidx.test.platform.app.InstrumentationRegistry +import net.mullvad.mullvadvpn.test.e2e.api.mullvad.MullvadApi +import net.mullvad.mullvadvpn.test.e2e.api.mullvad.removeAllDevices +import net.mullvad.mullvadvpn.test.e2e.api.partner.PartnerApi +import net.mullvad.mullvadvpn.test.e2e.constant.INVALID_TEST_ACCOUNT_NUMBER_ARGUMENT_KEY +import net.mullvad.mullvadvpn.test.e2e.constant.PARTNER_AUTH +import net.mullvad.mullvadvpn.test.e2e.constant.VALID_TEST_ACCOUNT_NUMBER_ARGUMENT_KEY +import net.mullvad.mullvadvpn.test.e2e.extension.getRequiredArgument + +object AccountProvider { + private val mullvadClient = MullvadApi() + private val partnerAuth: String? = + InstrumentationRegistry.getArguments().getString(PARTNER_AUTH, null) + private val partnerClient: PartnerApi by lazy { PartnerApi(partnerAuth!!) } + + suspend fun getValidAccountNumber() = + // If partner auth is provided, create a new account using the partner API. Otherwise we + // expect and account with time to be provided. + if (partnerAuth != null) { + val accountNumber = partnerClient.createAccount() + partnerClient.addTime(accountNumber = accountNumber, daysToAdd = 1) + accountNumber + } else { + val validAccountNumber = + InstrumentationRegistry.getArguments() + .getRequiredArgument(VALID_TEST_ACCOUNT_NUMBER_ARGUMENT_KEY) + mullvadClient.removeAllDevices(validAccountNumber) + validAccountNumber + } + + fun getInvalidAccountNumber() = + InstrumentationRegistry.getArguments() + .getRequiredArgument(INVALID_TEST_ACCOUNT_NUMBER_ARGUMENT_KEY) +} diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/AccountTestRule.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/AccountTestRule.kt index 1a414584e3..1f455af5c1 100644 --- a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/AccountTestRule.kt +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/AccountTestRule.kt @@ -1,38 +1,15 @@ package net.mullvad.mullvadvpn.test.e2e.misc -import androidx.test.platform.app.InstrumentationRegistry -import net.mullvad.mullvadvpn.test.e2e.constant.INVALID_TEST_ACCOUNT_NUMBER_ARGUMENT_KEY -import net.mullvad.mullvadvpn.test.e2e.constant.PARTNER_AUTH -import net.mullvad.mullvadvpn.test.e2e.constant.VALID_TEST_ACCOUNT_NUMBER_ARGUMENT_KEY -import net.mullvad.mullvadvpn.test.e2e.extension.getRequiredArgument +import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.extension.BeforeEachCallback import org.junit.jupiter.api.extension.ExtensionContext class AccountTestRule : BeforeEachCallback { - private val client = - SimpleMullvadHttpClient(InstrumentationRegistry.getInstrumentation().targetContext) - private val partnerAuth: String? = - InstrumentationRegistry.getArguments().getString(PARTNER_AUTH, null) lateinit var validAccountNumber: String lateinit var invalidAccountNumber: String - override fun beforeEach(context: ExtensionContext) { - InstrumentationRegistry.getArguments().also { bundle -> - if (partnerAuth != null) { - validAccountNumber = client.createAccountUsingPartnerApi(partnerAuth) - client.addTimeToAccountUsingPartnerAuth( - accountNumber = validAccountNumber, - daysToAdd = 1, - partnerAuth = partnerAuth, - ) - } else { - validAccountNumber = - bundle.getRequiredArgument(VALID_TEST_ACCOUNT_NUMBER_ARGUMENT_KEY) - client.removeAllDevices(validAccountNumber) - } - - invalidAccountNumber = - bundle.getRequiredArgument(INVALID_TEST_ACCOUNT_NUMBER_ARGUMENT_KEY) - } + override fun beforeEach(context: ExtensionContext): Unit = runBlocking { + validAccountNumber = AccountProvider.getValidAccountNumber() + invalidAccountNumber = AccountProvider.getInvalidAccountNumber() } } 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 index 6bacf15a3a..3cf1f602b4 100644 --- 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 @@ -2,21 +2,22 @@ package net.mullvad.mullvadvpn.test.e2e.misc import androidx.test.platform.app.InstrumentationRegistry import co.touchlab.kermit.Logger +import kotlinx.coroutines.runBlocking +import net.mullvad.mullvadvpn.test.e2e.api.mullvad.MullvadApi +import net.mullvad.mullvadvpn.test.e2e.api.mullvad.removeAllDevices import net.mullvad.mullvadvpn.test.e2e.constant.VALID_TEST_ACCOUNT_NUMBER_ARGUMENT_KEY import net.mullvad.mullvadvpn.test.e2e.extension.getRequiredArgument -import net.mullvad.mullvadvpn.test.e2e.interactor.MullvadAccountInteractor import org.junit.jupiter.api.extension.BeforeEachCallback import org.junit.jupiter.api.extension.ExtensionContext class CleanupAccountTestRule : BeforeEachCallback { + private val mullvadApi = MullvadApi() override fun beforeEach(context: ExtensionContext) { Logger.d("Cleaning up account before test: ${context.requiredTestMethod.name}") - val targetContext = InstrumentationRegistry.getInstrumentation().targetContext val validTestAccountNumber = InstrumentationRegistry.getArguments() .getRequiredArgument(VALID_TEST_ACCOUNT_NUMBER_ARGUMENT_KEY) - MullvadAccountInteractor(SimpleMullvadHttpClient(targetContext), validTestAccountNumber) - .cleanupAccount() + runBlocking { mullvadApi.removeAllDevices(validTestAccountNumber) } } } diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/ConnCheckState.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/ConnCheckState.kt deleted file mode 100644 index 75004270a4..0000000000 --- a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/ConnCheckState.kt +++ /dev/null @@ -1,3 +0,0 @@ -package net.mullvad.mullvadvpn.test.e2e.misc - -data class ConnCheckState(val isConnected: Boolean, val ipAddress: String) diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/KermitLogger.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/KermitLogger.kt new file mode 100644 index 0000000000..f503e1c12b --- /dev/null +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/KermitLogger.kt @@ -0,0 +1,23 @@ +package net.mullvad.mullvadvpn.test.e2e.misc + +import co.touchlab.kermit.Logger as KLogger +import io.ktor.client.plugins.logging.Logger + +class KermitLogger : Logger { + private val logger = KLogger.withTag("HttpClient") + + private val uuidRegex = + Regex( + "^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$" + ) + private val accountNumberRegex = Regex("\\d{16}") + + override fun log(message: String) { + + val redactedMessage = + message + .replace(accountNumberRegex, "<REDACTED_ACCOUNT_NUMBER>") + .replace(uuidRegex, "<REDACTED_UUID>") + logger.d(redactedMessage) + } +} 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 deleted file mode 100644 index fa4dd88613..0000000000 --- a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/SimpleMullvadHttpClient.kt +++ /dev/null @@ -1,221 +0,0 @@ -package net.mullvad.mullvadvpn.test.e2e.misc - -import android.content.Context -import androidx.test.services.events.TestEventException -import co.touchlab.kermit.Logger -import com.android.volley.Request -import com.android.volley.VolleyError -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.constant.AUTH_URL -import net.mullvad.mullvadvpn.test.e2e.constant.CONN_CHECK_URL -import net.mullvad.mullvadvpn.test.e2e.constant.DEVICE_LIST_URL -import net.mullvad.mullvadvpn.test.e2e.constant.PARTNER_ACCOUNT_URL -import org.json.JSONArray -import org.json.JSONObject -import org.junit.jupiter.api.fail - -class SimpleMullvadHttpClient(context: Context) { - - private val queue = Volley.newRequestQueue(context) - - fun removeAllDevices(accountNumber: String) { - Logger.v("Remove all devices") - val token = login(accountNumber) - val devices = getDeviceList(token) - devices.forEach { removeDevice(token, it) } - Logger.v("All devices removed") - } - - fun login(accountNumber: String): String { - Logger.v("Attempt login with account number: $accountNumber") - val json = JSONObject().apply { put("account_number", accountNumber) } - return sendSimpleSynchronousRequest(Request.Method.POST, AUTH_URL, json)!!.let { response -> - response.getString("access_token").also { accessToken -> - Logger.v("Successfully logged in and received access token: $accessToken") - } - } - } - - fun createAccountUsingPartnerApi(partnerAuth: String): String { - return sendSimpleSynchronousRequest( - method = Request.Method.POST, - url = PARTNER_ACCOUNT_URL, - authorizationHeader = "Basic $partnerAuth", - )!! - .getString("id") - } - - fun addTimeToAccountUsingPartnerAuth( - accountNumber: String, - daysToAdd: Int, - partnerAuth: String, - ) { - sendSimpleSynchronousRequest( - method = Request.Method.POST, - url = "$PARTNER_ACCOUNT_URL/$accountNumber/extend", - body = JSONObject().apply { put("days", "$daysToAdd") }, - authorizationHeader = "Basic $partnerAuth", - ) - } - - fun getDeviceList(accessToken: String): List<String> { - Logger.v("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 -> Logger.v("Devices received: $deviceNames") } - } - .map { it.getString("id") } - .toList() - } - - fun removeDevice(token: String, deviceId: String) { - Logger.v("Remove device: $deviceId") - sendSimpleSynchronousRequestString( - method = Request.Method.DELETE, - url = "$DEVICE_LIST_URL/$deviceId", - authorizationHeader = "Bearer $token", - ) - } - - fun runConnectionCheck(): ConnCheckState? { - return sendSimpleSynchronousRequestString(Request.Method.GET, CONN_CHECK_URL) - ?.let { respose -> JSONObject(respose) } - ?.let { json -> - ConnCheckState( - isConnected = json.getBoolean("mullvad_exit_ip"), - ipAddress = json.getString("ip"), - ) - } - } - - private fun sendSimpleSynchronousRequest( - method: Int, - url: String, - body: JSONObject? = null, - authorizationHeader: String? = null, - ): JSONObject? { - val future = RequestFuture.newFuture<JSONObject>() - - val request = - object : JsonObjectRequest(method, url, body, future, onErrorResponse) { - override fun getHeaders(): MutableMap<String, String> { - val headers = HashMap<String, String>() - if (body != null) { - headers.put("Content-Type", "application/json") - } - if (authorizationHeader != null) { - headers.put("Authorization", authorizationHeader) - } - return headers - } - } - queue.add(request) - return try { - future.get().also { response -> Logger.v("Json object request response: $response") } - } catch (e: Exception) { - Logger.v("Json object request error: ${e.message}") - throw TestEventException(REQUEST_ERROR_MESSAGE) - } - } - - private fun sendSimpleSynchronousRequestString( - method: Int, - url: String, - body: String? = null, - authorizationHeader: String? = null, - ): String? { - val future = RequestFuture.newFuture<String>() - val request = - object : StringRequest(method, url, future, onErrorResponse) { - override fun getHeaders(): MutableMap<String, String> { - val headers = HashMap<String, String>() - if (body != null) { - headers.put("Content-Type", "application/json") - } - if (authorizationHeader != null) { - headers.put("Authorization", authorizationHeader) - } - return headers - } - } - queue.add(request) - return try { - future.get().also { response -> Logger.v("String request response: $response") } - } catch (e: Exception) { - Logger.v("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, body, future, onErrorResponse) { - 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 -> Logger.v("Json array request response: $response") } - } catch (e: Exception) { - Logger.v("Json array request error: ${e.message}") - throw TestEventException(REQUEST_ERROR_MESSAGE) - } - } - - operator fun <T> JSONArray.iterator(): Iterator<T> = - (0 until length()) - .asSequence() - .map { - @Suppress("UNCHECKED_CAST") - get(it) as T - } - .iterator() - - companion object { - private const val REQUEST_ERROR_MESSAGE = - "Unable to verify account due to invalid account or connectivity issues." - - private val onErrorResponse = { error: VolleyError -> - if (error.networkResponse != null) { - if (error.networkResponse.statusCode == 429) { - fail("Request failed with response status code 429: Too many requests") - } - - Logger.e( - "Response returned error message: ${error.message} " + - "status code: ${error.networkResponse.statusCode}" - ) - } else { - Logger.e("Response returned error: ${error.message}") - } - } - } -} diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/router/firewall/FirewallClient.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/router/firewall/FirewallClient.kt index fabfee0132..3e2a35971a 100644 --- a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/router/firewall/FirewallClient.kt +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/router/firewall/FirewallClient.kt @@ -2,15 +2,14 @@ package net.mullvad.mullvadvpn.test.e2e.router.firewall import co.touchlab.kermit.Logger import io.ktor.client.HttpClient -import io.ktor.client.call.body import io.ktor.client.engine.cio.CIO -import io.ktor.client.plugins.HttpResponseValidator import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.defaultRequest import io.ktor.client.request.delete import io.ktor.client.request.post import io.ktor.client.request.setBody import io.ktor.http.ContentType +import io.ktor.http.URLProtocol import io.ktor.http.contentType import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json @@ -18,7 +17,6 @@ import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.contextual import net.mullvad.mullvadvpn.test.e2e.BuildConfig import net.mullvad.mullvadvpn.test.e2e.serializer.NanoSecondsTimestampSerializer -import org.junit.jupiter.api.fail class FirewallClient(private val httpClient: HttpClient = defaultHttpClient()) { suspend fun createRule(rule: DropRule) { @@ -42,7 +40,12 @@ class FirewallClient(private val httpClient: HttpClient = defaultHttpClient()) { private fun defaultHttpClient(): HttpClient = HttpClient(CIO) { - defaultRequest { url("http://${BuildConfig.TEST_ROUTER_API_HOST}") } + defaultRequest { + url { + protocol = URLProtocol.HTTP + host = BuildConfig.TEST_ROUTER_API_HOST + } + } install(ContentNegotiation) { json( @@ -56,18 +59,5 @@ private fun defaultHttpClient(): HttpClient = } ) } - - HttpResponseValidator { - validateResponse { response -> - val statusCode = response.status.value - if (statusCode >= 400) { - fail( - "Request failed with response status code $statusCode: ${response.body<String>()}" - ) - } - } - handleResponseExceptionWithRequest { exception, _ -> - fail("Request failed to be sent with exception: ${exception.message}") - } - } + expectSuccess = true } diff --git a/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/TooManyDevicesMockApiTest.kt b/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/TooManyDevicesMockApiTest.kt index 54af6057aa..2a5d91feaa 100644 --- a/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/TooManyDevicesMockApiTest.kt +++ b/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/TooManyDevicesMockApiTest.kt @@ -6,18 +6,9 @@ import net.mullvad.mullvadvpn.test.common.extension.clickAgreeOnPrivacyDisclaime import net.mullvad.mullvadvpn.test.common.extension.clickAllowOnNotificationPermissionPromptIfApiLevel33AndAbove import net.mullvad.mullvadvpn.test.common.extension.dismissChangelogDialogIfShown import net.mullvad.mullvadvpn.test.common.extension.findObjectWithTimeout -import net.mullvad.mullvadvpn.test.mockapi.constant.DUMMY_DEVICE_NAME_1 -import net.mullvad.mullvadvpn.test.mockapi.constant.DUMMY_DEVICE_NAME_2 -import net.mullvad.mullvadvpn.test.mockapi.constant.DUMMY_DEVICE_NAME_3 -import net.mullvad.mullvadvpn.test.mockapi.constant.DUMMY_DEVICE_NAME_4 -import net.mullvad.mullvadvpn.test.mockapi.constant.DUMMY_DEVICE_NAME_5 import net.mullvad.mullvadvpn.test.mockapi.constant.DUMMY_DEVICE_NAME_6 -import net.mullvad.mullvadvpn.test.mockapi.constant.DUMMY_ID_1 -import net.mullvad.mullvadvpn.test.mockapi.constant.DUMMY_ID_2 -import net.mullvad.mullvadvpn.test.mockapi.constant.DUMMY_ID_3 -import net.mullvad.mullvadvpn.test.mockapi.constant.DUMMY_ID_4 -import net.mullvad.mullvadvpn.test.mockapi.constant.DUMMY_ID_5 import net.mullvad.mullvadvpn.test.mockapi.constant.DUMMY_ID_6 +import net.mullvad.mullvadvpn.test.mockapi.constant.FULL_DEVICE_LIST import org.junit.jupiter.api.Test class TooManyDevicesMockApiTest : MockApiTest() { @@ -28,14 +19,7 @@ class TooManyDevicesMockApiTest : MockApiTest() { apiDispatcher.apply { expectedAccountNumber = validAccountNumber accountExpiry = ZonedDateTime.now().plusMonths(1) - devices = - mutableMapOf( - DUMMY_ID_1 to DUMMY_DEVICE_NAME_1, - DUMMY_ID_2 to DUMMY_DEVICE_NAME_2, - DUMMY_ID_3 to DUMMY_DEVICE_NAME_3, - DUMMY_ID_4 to DUMMY_DEVICE_NAME_4, - DUMMY_ID_5 to DUMMY_DEVICE_NAME_5, - ) + devices = FULL_DEVICE_LIST.toMutableMap() devicePendingToGetCreated = DUMMY_ID_6 to DUMMY_DEVICE_NAME_6 } |
