summaryrefslogtreecommitdiffhomepage
path: root/android/test
diff options
context:
space:
mode:
authorDavid Göransson <david.goransson@mullvad.net>2025-04-14 15:58:38 +0200
committerDavid Göransson <david.goransson@mullvad.net>2025-04-16 14:33:50 +0200
commit7a5346a49ab661b8abfed89024dddadbbdacb62e (patch)
tree3c63a26550bf1d01c1f0a87419b95e6634403c35 /android/test
parent9babff88e7180d4b363afb48f513654e6b8126a3 (diff)
downloadmullvadvpn-7a5346a49ab661b8abfed89024dddadbbdacb62e.tar.xz
mullvadvpn-7a5346a49ab661b8abfed89024dddadbbdacb62e.zip
Replace Volley with ktor
Diffstat (limited to 'android/test')
-rw-r--r--android/test/e2e/build.gradle.kts4
-rw-r--r--android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/ConnectionTest.kt133
-rw-r--r--android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/LeakTest.kt251
-rw-r--r--android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/api/connectioncheck/ConnCheckResult.kt15
-rw-r--r--android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/api/connectioncheck/ConnectionCheckApi.kt50
-rw-r--r--android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/api/connectioncheck/ConnectionCheckApiTest.kt19
-rw-r--r--android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/api/mullvad/MullvadApi.kt102
-rw-r--r--android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/api/mullvad/MullvadApiTest.kt28
-rw-r--r--android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/api/partner/PartnerApi.kt73
-rw-r--r--android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/api/partner/PartnerApiTest.kt27
-rw-r--r--android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/constant/UrlConstants.kt17
-rw-r--r--android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/interactor/MullvadAccountInteractor.kt12
-rw-r--r--android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/AccountProvider.kt36
-rw-r--r--android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/AccountTestRule.kt31
-rw-r--r--android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/CleanupAccountTestRule.kt9
-rw-r--r--android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/ConnCheckState.kt3
-rw-r--r--android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/KermitLogger.kt23
-rw-r--r--android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/SimpleMullvadHttpClient.kt221
-rw-r--r--android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/router/firewall/FirewallClient.kt26
-rw-r--r--android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/TooManyDevicesMockApiTest.kt20
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
}