summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorBug Magnet <marco.nikic@mullvad.net>2024-03-25 14:24:14 +0100
committerBug Magnet <marco.nikic@mullvad.net>2024-03-25 14:24:14 +0100
commit030d55d7568318010090c6be01e30f5cbbb4e1bb (patch)
tree180d8109b9d8fae60dfc6af1c7d6ee02aad45ee9
parent8bc5c1372516d0e84b19c950f20583be58b6a8f7 (diff)
parent9d7b898a1e5d5629b7e538f5fc390607c32b054d (diff)
downloadmullvadvpn-030d55d7568318010090c6be01e30f5cbbb4e1bb.tar.xz
mullvadvpn-030d55d7568318010090c6be01e30f5cbbb4e1bb.zip
Merge branch 'test-wireguard-over-tcp-manually-ios-431'
-rw-r--r--ios/Configurations/UITests.xcconfig.template3
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj4
-rw-r--r--ios/MullvadVPN/Classes/AccessbilityIdentifier.swift10
-rw-r--r--ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift1
-rw-r--r--ios/MullvadVPN/View controllers/Settings/SettingsViewController.swift5
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/ConnectionPanelView.swift2
-rw-r--r--ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift1
-rw-r--r--ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift4
-rw-r--r--ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift1
-rw-r--r--ios/MullvadVPNUITests/Info.plist2
-rw-r--r--ios/MullvadVPNUITests/Networking/FirewallAPIClient.swift2
-rw-r--r--ios/MullvadVPNUITests/Networking/FirewallRule.swift16
-rw-r--r--ios/MullvadVPNUITests/Networking/Networking.swift32
-rw-r--r--ios/MullvadVPNUITests/Pages/Page.swift5
-rw-r--r--ios/MullvadVPNUITests/Pages/SelectLocationPage.swift2
-rw-r--r--ios/MullvadVPNUITests/Pages/SettingsPage.swift9
-rw-r--r--ios/MullvadVPNUITests/Pages/TunnelControlPage.swift117
-rw-r--r--ios/MullvadVPNUITests/Pages/VPNSettingsPage.swift79
-rw-r--r--ios/MullvadVPNUITests/RelayTests.swift128
-rw-r--r--ios/MullvadVPNUITests/Test base classes/BaseUITestCase.swift24
20 files changed, 429 insertions, 18 deletions
diff --git a/ios/Configurations/UITests.xcconfig.template b/ios/Configurations/UITests.xcconfig.template
index b55222dac2..c2a0fe821b 100644
--- a/ios/Configurations/UITests.xcconfig.template
+++ b/ios/Configurations/UITests.xcconfig.template
@@ -15,6 +15,9 @@ FIVE_WIREGUARD_KEYS_ACCOUNT_NUMBER =
// Ad serving domain used when testing ad blocking. Note that we are assuming there's an HTTP server running on the host.
AD_SERVING_DOMAIN = vpnlist.to
+
+// A domain which should be reachable. Used to verify Internet connectivity. Must be running a server on port 80.
+SHOULD_BE_REACHABLE_DOMAIN = mullvad.net
// Base URL for the firewall API, Note that // will be treated as a comment, therefor you need to insert a ${} between the slashes for example http:/${}/8.8.8.8
FIREWALL_API_BASE_URL = http:/${}/8.8.8.8
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 4047f906ac..f1ee25f1a1 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -618,6 +618,7 @@
8529693A2B4F0238007EAD4C /* TermsOfServicePage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852969392B4F0238007EAD4C /* TermsOfServicePage.swift */; };
8529693C2B4F0257007EAD4C /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8529693B2B4F0257007EAD4C /* Alert.swift */; };
8532E6872B8CCED600ACECD1 /* ProblemReportSubmittedPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8532E6862B8CCED600ACECD1 /* ProblemReportSubmittedPage.swift */; };
+ 8542CE242B95F7B9006FCA14 /* VPNSettingsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8542CE232B95F7B9006FCA14 /* VPNSettingsPage.swift */; };
85557B0E2B591B2600795FE1 /* FirewallAPIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85557B0D2B591B2600795FE1 /* FirewallAPIClient.swift */; };
85557B102B59215F00795FE1 /* FirewallRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85557B0F2B59215F00795FE1 /* FirewallRule.swift */; };
85557B122B594FC900795FE1 /* ConnectivityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85557B112B594FC900795FE1 /* ConnectivityTests.swift */; };
@@ -1861,6 +1862,7 @@
852969392B4F0238007EAD4C /* TermsOfServicePage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsOfServicePage.swift; sourceTree = "<group>"; };
8529693B2B4F0257007EAD4C /* Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alert.swift; sourceTree = "<group>"; };
8532E6862B8CCED600ACECD1 /* ProblemReportSubmittedPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemReportSubmittedPage.swift; sourceTree = "<group>"; };
+ 8542CE232B95F7B9006FCA14 /* VPNSettingsPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNSettingsPage.swift; sourceTree = "<group>"; };
85557B0D2B591B2600795FE1 /* FirewallAPIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirewallAPIClient.swift; sourceTree = "<group>"; };
85557B0F2B59215F00795FE1 /* FirewallRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirewallRule.swift; sourceTree = "<group>"; };
85557B112B594FC900795FE1 /* ConnectivityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectivityTests.swift; sourceTree = "<group>"; };
@@ -3609,6 +3611,7 @@
852969392B4F0238007EAD4C /* TermsOfServicePage.swift */,
850201DE2B5040A500EF8C96 /* TunnelControlPage.swift */,
85FB5A0B2B6903990015DCED /* WelcomePage.swift */,
+ 8542CE232B95F7B9006FCA14 /* VPNSettingsPage.swift */,
);
path = Pages;
sourceTree = "<group>";
@@ -5598,6 +5601,7 @@
8532E6872B8CCED600ACECD1 /* ProblemReportSubmittedPage.swift in Sources */,
85FB5A0C2B6903990015DCED /* WelcomePage.swift in Sources */,
850201DF2B5040A500EF8C96 /* TunnelControlPage.swift in Sources */,
+ 8542CE242B95F7B9006FCA14 /* VPNSettingsPage.swift in Sources */,
85557B1E2B5FB8C700795FE1 /* HeaderBar.swift in Sources */,
85557B122B594FC900795FE1 /* ConnectivityTests.swift in Sources */,
852969332B4E9232007EAD4C /* Page.swift in Sources */,
diff --git a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift
index 48651c39f3..9ee844b906 100644
--- a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift
+++ b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift
@@ -35,6 +35,8 @@ public enum AccessibilityIdentifier: String {
case startUsingTheAppButton
case problemReportAppLogsButton
case problemReportSendButton
+ case relayStatusCollapseButton
+ case settingsDoneButton
// Cells
case vpnSettingsCell
@@ -44,11 +46,16 @@ public enum AccessibilityIdentifier: String {
case apiAccessCell
case relayFilterOwnershipCell
case relayFilterProviderCell
+ case wireGuardPortsCell
+ case wireGuardObfuscationCell
+ case udpOverTCPPortCell
+ case quantumResistantTunnelCell
// Labels
case headerDeviceNameLabel
case connectionStatusLabel
case welcomeAccountNumberLabel
+ case connectionPanelDetailLabel
// Views
case accountView
@@ -62,17 +69,20 @@ public enum AccessibilityIdentifier: String {
case selectLocationView
case selectLocationTableView
case settingsTableView
+ case vpnSettingsTableView
case tunnelControlView
case problemReportView
case problemReportSubmittedView
case revokedDeviceView
case welcomeView
case deleteAccountView
+ case settingsContainerView
// Other UI elements
case connectionPanelInAddressRow
case connectionPanelOutAddressRow
case customSwitch
+ case customWireGuardPortTextField
case dnsContentBlockersHeaderView
case loginTextField
case selectLocationSearchTextField
diff --git a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
index ff113f5803..627183b1f7 100644
--- a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
@@ -777,6 +777,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
)
let navigationController = CustomNavigationController()
+ navigationController.view.accessibilityIdentifier = .settingsContainerView
let configurationTester = ProxyConfigurationTester(transportProvider: configuredTransportProvider)
diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsViewController.swift b/ios/MullvadVPN/View controllers/Settings/SettingsViewController.swift
index ed964a8dbe..27e89a883b 100644
--- a/ios/MullvadVPN/View controllers/Settings/SettingsViewController.swift
+++ b/ios/MullvadVPN/View controllers/Settings/SettingsViewController.swift
@@ -45,7 +45,8 @@ class SettingsViewController: UITableViewController, SettingsDataSourceDelegate
value: "Settings",
comment: ""
)
- navigationItem.rightBarButtonItem = UIBarButtonItem(
+
+ let doneButton = UIBarButtonItem(
systemItem: .done,
primaryAction: UIAction(handler: { [weak self] _ in
guard let self else { return }
@@ -53,6 +54,8 @@ class SettingsViewController: UITableViewController, SettingsDataSourceDelegate
delegate?.settingsViewControllerDidFinish(self)
})
)
+ doneButton.accessibilityIdentifier = .settingsDoneButton
+ navigationItem.rightBarButtonItem = doneButton
tableView.accessibilityIdentifier = .settingsTableView
tableView.backgroundColor = .secondaryColor
diff --git a/ios/MullvadVPN/View controllers/Tunnel/ConnectionPanelView.swift b/ios/MullvadVPN/View controllers/Tunnel/ConnectionPanelView.swift
index 2e975c9c38..5389d5dee4 100644
--- a/ios/MullvadVPN/View controllers/Tunnel/ConnectionPanelView.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/ConnectionPanelView.swift
@@ -29,6 +29,7 @@ class ConnectionPanelView: UIView {
var connectedRelayName = "" {
didSet {
+ collapseButton.accessibilityIdentifier = .relayStatusCollapseButton
collapseButton.setTitle(connectedRelayName, for: .normal)
collapseButton.accessibilityLabel = NSLocalizedString(
"RELAY_ACCESSIBILITY_LABEL",
@@ -185,6 +186,7 @@ class ConnectionPanelAddressRow: UIView {
private let detailTextLabel: UILabel = {
let detailTextLabel = UILabel()
+ detailTextLabel.accessibilityIdentifier = .connectionPanelDetailLabel
detailTextLabel.font = .systemFont(ofSize: 17)
detailTextLabel.textColor = .white
detailTextLabel.translatesAutoresizingMaskIntoConstraints = false
diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift
index 8e6ed59018..198aca720c 100644
--- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift
+++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift
@@ -97,6 +97,7 @@ final class VPNSettingsCellFactory: CellFactoryProtocol {
comment: ""
)
+ cell.textField.accessibilityIdentifier = .customWireGuardPortTextField
cell.accessibilityIdentifier = item.accessibilityIdentifier
cell.applySubCellStyling()
diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift
index f42d0d343b..a87f2ebd96 100644
--- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift
+++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift
@@ -446,6 +446,7 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource<
comment: ""
)
+ header.accessibilityIdentifier = .wireGuardPortsCell
header.titleLabel.text = title
header.accessibilityCustomActionName = title
header.infoButtonHandler = { [weak self] in
@@ -488,6 +489,7 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource<
comment: ""
)
+ header.accessibilityIdentifier = .wireGuardObfuscationCell
header.titleLabel.text = title
header.accessibilityCustomActionName = title
header.didCollapseHandler = { [weak self] header in
@@ -516,6 +518,7 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource<
comment: ""
)
+ header.accessibilityIdentifier = .udpOverTCPPortCell
header.titleLabel.text = title
header.accessibilityCustomActionName = title
header.didCollapseHandler = { [weak self] header in
@@ -545,6 +548,7 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource<
comment: ""
)
+ header.accessibilityIdentifier = .quantumResistantTunnelCell
header.titleLabel.text = title
header.accessibilityCustomActionName = title
header.didCollapseHandler = { [weak self] header in
diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift
index 3be523ef2a..3d55f6fd0e 100644
--- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift
+++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift
@@ -38,6 +38,7 @@ class VPNSettingsViewController: UITableViewController, VPNSettingsDataSourceDel
override func viewDidLoad() {
super.viewDidLoad()
+ tableView.accessibilityIdentifier = .vpnSettingsTableView
tableView.backgroundColor = .secondaryColor
tableView.separatorColor = .secondaryColor
tableView.rowHeight = UITableView.automaticDimension
diff --git a/ios/MullvadVPNUITests/Info.plist b/ios/MullvadVPNUITests/Info.plist
index dd7e65bae2..0bed909d3f 100644
--- a/ios/MullvadVPNUITests/Info.plist
+++ b/ios/MullvadVPNUITests/Info.plist
@@ -20,6 +20,8 @@
<string>$(IOS_DEVICE_PIN_CODE)</string>
<key>NoTimeAccountNumber</key>
<string>$(NO_TIME_ACCOUNT_NUMBER)</string>
+ <key>ShouldBeReachableDomain</key>
+ <string>$(SHOULD_BE_REACHABLE_DOMAIN)</string>
<key>TestDeviceIdentifier</key>
<string>$(TEST_DEVICE_IDENTIFIER_UUID</string>
</dict>
diff --git a/ios/MullvadVPNUITests/Networking/FirewallAPIClient.swift b/ios/MullvadVPNUITests/Networking/FirewallAPIClient.swift
index 49496f3d2e..0577e58e4e 100644
--- a/ios/MullvadVPNUITests/Networking/FirewallAPIClient.swift
+++ b/ios/MullvadVPNUITests/Networking/FirewallAPIClient.swift
@@ -34,6 +34,7 @@ class FirewallAPIClient {
"label": sessionIdentifier,
"from": firewallRule.fromIPAddress,
"to": firewallRule.toIPAddress,
+ "protocols": firewallRule.protocolsAsStringArray(),
]
var requestError: Error?
@@ -80,7 +81,6 @@ class FirewallAPIClient {
var request = URLRequest(url: removeRulesURL)
request.httpMethod = "DELETE"
- request.setValue("application/json", forHTTPHeaderField: "Content-Type")
var requestResponse: URLResponse?
var requestError: Error?
diff --git a/ios/MullvadVPNUITests/Networking/FirewallRule.swift b/ios/MullvadVPNUITests/Networking/FirewallRule.swift
index d89dbc6853..1b18baa91c 100644
--- a/ios/MullvadVPNUITests/Networking/FirewallRule.swift
+++ b/ios/MullvadVPNUITests/Networking/FirewallRule.swift
@@ -30,6 +30,10 @@ struct FirewallRule {
self.protocols = protocols
}
+ public func protocolsAsStringArray() -> [String] {
+ return protocols.map { $0.rawValue }
+ }
+
/// Make a firewall rule blocking API access for the current device under test
public static func makeBlockAPIAccessFirewallRule() throws -> FirewallRule {
let deviceIPAddress = try Networking.getIPAddress()
@@ -37,7 +41,17 @@ struct FirewallRule {
return FirewallRule(
fromIPAddress: deviceIPAddress,
toIPAddress: apiIPAddress,
- protocols: [NetworkingProtocol.TCP]
+ protocols: [.TCP]
+ )
+ }
+
+ public static func makeBlockUDPTrafficRule(toIPAddress: String) throws -> FirewallRule {
+ let deviceIPAddress = try Networking.getIPAddress()
+
+ return FirewallRule(
+ fromIPAddress: deviceIPAddress,
+ toIPAddress: toIPAddress,
+ protocols: [.UDP]
)
}
}
diff --git a/ios/MullvadVPNUITests/Networking/Networking.swift b/ios/MullvadVPNUITests/Networking/Networking.swift
index e8a038b4a2..67e2156603 100644
--- a/ios/MullvadVPNUITests/Networking/Networking.swift
+++ b/ios/MullvadVPNUITests/Networking/Networking.swift
@@ -63,24 +63,24 @@ class Networking {
throw NetworkingError.internalError(reason: "Failed to determine device's IP address")
}
- private static func getAdServingDomainURL() -> URL? {
- guard let adServingDomain = Bundle(for: BaseUITestCase.self)
- .infoDictionary?["AdServingDomain"] as? String,
- let adServingDomainURL = URL(string: adServingDomain) else {
- XCTFail("Ad serving domain not configured")
- return nil
+ /// Get configured ad serving domain
+ private static func getAdServingDomain() throws -> String {
+ guard let adServingDomain = Bundle(for: Networking.self)
+ .infoDictionary?["AdServingDomain"] as? String else {
+ throw NetworkingError.notConfiguredError
}
- return adServingDomainURL
+ return adServingDomain
}
- private static func getAdServingDomain() throws -> String {
- guard let adServingDomain = Bundle(for: BaseUITestCase.self)
- .infoDictionary?["AdServingDomain"] as? String else {
+ /// Get configured domain to use for Internet connectivity checks
+ private static func getAlwaysReachableDomain() throws -> String {
+ guard let shouldBeReachableDomain = Bundle(for: Networking.self)
+ .infoDictionary?["ShouldBeReachableDomain"] as? String else {
throw NetworkingError.notConfiguredError
}
- return adServingDomain
+ return shouldBeReachableDomain
}
/// Check whether host and port is reachable by attempting to connect a socket
@@ -134,6 +134,16 @@ class Networking {
XCTAssertFalse(try canConnectSocket(host: apiIPAddress, port: apiPort))
}
+ /// Verify that the device has Internet connectivity
+ public static func verifyCanAccessInternet() throws {
+ XCTAssertTrue(try canConnectSocket(host: getAlwaysReachableDomain(), port: "80"))
+ }
+
+ /// Verify that the device does not have Internet connectivity
+ public static func verifyCannotAccessInternet() throws {
+ XCTAssertFalse(try canConnectSocket(host: getAlwaysReachableDomain(), port: "80"))
+ }
+
/// Verify that an ad serving domain is reachable by making sure a connection can be established on port 80
public static func verifyCanReachAdServingDomain() throws {
XCTAssertTrue(try Self.canConnectSocket(host: try Self.getAdServingDomain(), port: "80"))
diff --git a/ios/MullvadVPNUITests/Pages/Page.swift b/ios/MullvadVPNUITests/Pages/Page.swift
index 453bd57375..77fbebadae 100644
--- a/ios/MullvadVPNUITests/Pages/Page.swift
+++ b/ios/MullvadVPNUITests/Pages/Page.swift
@@ -31,6 +31,11 @@ class Page {
return self
}
+ @discardableResult func dismissKeyboard() -> Self {
+ self.enterText("\n")
+ return self
+ }
+
/// Fast swipe down action to dismiss a modal view. Will swipe on the middle of the screen.
@discardableResult func swipeDownToDismissModal() -> Self {
app.swipeDown(velocity: .fast)
diff --git a/ios/MullvadVPNUITests/Pages/SelectLocationPage.swift b/ios/MullvadVPNUITests/Pages/SelectLocationPage.swift
index c5c54325ad..ba15a7323f 100644
--- a/ios/MullvadVPNUITests/Pages/SelectLocationPage.swift
+++ b/ios/MullvadVPNUITests/Pages/SelectLocationPage.swift
@@ -22,7 +22,7 @@ class SelectLocationPage: Page {
return self
}
- @discardableResult func tapLocationCellExpandButton(withName name: String) -> Self {
+ @discardableResult func tapLocationCellExpandCollapseButton(withName name: String) -> Self {
let table = app.tables[AccessibilityIdentifier.selectLocationTableView]
let matchingCells = table.cells.containing(.any, identifier: name)
let buttons = matchingCells.buttons
diff --git a/ios/MullvadVPNUITests/Pages/SettingsPage.swift b/ios/MullvadVPNUITests/Pages/SettingsPage.swift
index 43ad3dedd5..b65ca02f81 100644
--- a/ios/MullvadVPNUITests/Pages/SettingsPage.swift
+++ b/ios/MullvadVPNUITests/Pages/SettingsPage.swift
@@ -13,10 +13,17 @@ class SettingsPage: Page {
@discardableResult override init(_ app: XCUIApplication) {
super.init(app)
- self.pageAccessibilityIdentifier = .settingsTableView
+ self.pageAccessibilityIdentifier = .settingsContainerView
waitForPageToBeShown()
}
+ @discardableResult func tapDoneButton() -> Self {
+ app.buttons[AccessibilityIdentifier.settingsDoneButton]
+ .tap()
+
+ return self
+ }
+
@discardableResult func tapVPNSettingsCell() -> Self {
app.tables[AccessibilityIdentifier.settingsTableView]
.cells[AccessibilityIdentifier.vpnSettingsCell]
diff --git a/ios/MullvadVPNUITests/Pages/TunnelControlPage.swift b/ios/MullvadVPNUITests/Pages/TunnelControlPage.swift
index 37813b36c4..daef1a30e7 100644
--- a/ios/MullvadVPNUITests/Pages/TunnelControlPage.swift
+++ b/ios/MullvadVPNUITests/Pages/TunnelControlPage.swift
@@ -10,6 +10,61 @@ import Foundation
import XCTest
class TunnelControlPage: Page {
+ private struct ConnectionAttempt: Hashable {
+ let ipAddress: String
+ let port: String
+ let protocolName: String
+ }
+
+ /// Poll the "in address row" label for its updated values and output an array of ConnectionAttempt objects representing the connection attempts that have been communicated through the UI.
+ /// - Parameters:
+ /// - attemptsCount: number of connection attempts to look for
+ /// - timeout: timeout after this many seconds if attemptsCount haven't been reached yet
+ private func waitForConnectionAttempts(_ attemptsCount: Int, timeout: TimeInterval) -> [ConnectionAttempt] {
+ var connectionAttempts: [ConnectionAttempt] = []
+ let startTime = Date()
+ let pollingInterval = TimeInterval(0.5) // How often to check for changes
+
+ let inAddressRow = app.otherElements[AccessibilityIdentifier.connectionPanelInAddressRow]
+
+ while Date().timeIntervalSince(startTime) < timeout {
+ let expectation = XCTestExpectation(description: "Wait for connection attempts")
+
+ DispatchQueue.global().asyncAfter(deadline: .now() + pollingInterval) {
+ expectation.fulfill()
+ }
+
+ _ = XCTWaiter.wait(for: [expectation], timeout: pollingInterval + 0.5)
+
+ if let currentText = inAddressRow.value as? String {
+ // Skip initial label value with IP address only - no port or protocol
+ guard currentText.contains(" ") == true else {
+ continue
+ }
+
+ let addressPortComponent = currentText.components(separatedBy: " ")[0]
+ let ipAddress = addressPortComponent.components(separatedBy: ":")[0]
+ let port = addressPortComponent.components(separatedBy: ":")[1]
+ let protocolName = currentText.components(separatedBy: " ")[1]
+ let connectionAttempt = ConnectionAttempt(
+ ipAddress: ipAddress,
+ port: port,
+ protocolName: protocolName
+ )
+
+ if connectionAttempts.contains(connectionAttempt) == false {
+ connectionAttempts.append(connectionAttempt)
+
+ if connectionAttempts.count == attemptsCount {
+ break
+ }
+ }
+ }
+ }
+
+ return connectionAttempts
+ }
+
@discardableResult override init(_ app: XCUIApplication) {
super.init(app)
@@ -27,9 +82,71 @@ class TunnelControlPage: Page {
return self
}
+ @discardableResult func tapDisconnectButton() -> Self {
+ app.buttons[AccessibilityIdentifier.disconnectButton].tap()
+ return self
+ }
+
@discardableResult func waitForSecureConnectionLabel() -> Self {
_ = app.staticTexts[AccessibilityIdentifier.connectionStatusLabel]
.waitForExistence(timeout: BaseUITestCase.defaultTimeout)
return self
}
+
+ @discardableResult func tapRelayStatusExpandCollapseButton() -> Self {
+ app.buttons[AccessibilityIdentifier.relayStatusCollapseButton].tap()
+ return self
+ }
+
+ /// Verify that the app attempts to connect over UDP before switching to TCP. For testing blocked UDP traffic.
+ @discardableResult func verifyConnectingOverTCPAfterUDPAttempts() -> Self {
+ let connectionAttempts = waitForConnectionAttempts(3, timeout: 15)
+
+ // Should do three connection attempts but due to UI bug sometimes only two are displayed, sometimes all three
+ if connectionAttempts.count == 3 { // Expected retries flow
+ for (attemptIndex, attempt) in connectionAttempts.enumerated() {
+ if attemptIndex == 0 || attemptIndex == 1 {
+ XCTAssertEqual(attempt.protocolName, "UDP")
+ } else if attemptIndex == 2 {
+ XCTAssertEqual(attempt.protocolName, "TCP")
+ } else {
+ XCTFail("Unexpected connection attempt")
+ }
+ }
+ } else if connectionAttempts.count == 2 { // Most of the times this incorrect flow is shown
+ for (attemptIndex, attempt) in connectionAttempts.enumerated() {
+ if attemptIndex == 0 {
+ XCTAssertEqual(attempt.protocolName, "UDP")
+ } else if attemptIndex == 1 {
+ XCTAssertEqual(attempt.protocolName, "TCP")
+ } else {
+ XCTFail("Unexpected connection attempt")
+ }
+ }
+ } else {
+ XCTFail("Unexpected number of connection attempts")
+ }
+
+ return self
+ }
+
+ @discardableResult func verifyConnectingToPort(_ port: String) -> Self {
+ let connectionAttempts = waitForConnectionAttempts(1, timeout: 10)
+ XCTAssertEqual(connectionAttempts.count, 1)
+ XCTAssertEqual(connectionAttempts.first!.port, port)
+
+ return self
+ }
+
+ func getInIPAddressFromConnectionStatus() -> String {
+ let inAddressRow = app.otherElements[AccessibilityIdentifier.connectionPanelInAddressRow]
+
+ if let textValue = inAddressRow.value as? String {
+ let ipAddress = textValue.components(separatedBy: ":")[0]
+ return ipAddress
+ } else {
+ XCTFail("Failed to read relay IP address from status label")
+ return String()
+ }
+ }
}
diff --git a/ios/MullvadVPNUITests/Pages/VPNSettingsPage.swift b/ios/MullvadVPNUITests/Pages/VPNSettingsPage.swift
new file mode 100644
index 0000000000..22ad20540b
--- /dev/null
+++ b/ios/MullvadVPNUITests/Pages/VPNSettingsPage.swift
@@ -0,0 +1,79 @@
+//
+// VPNSettingsPage.swift
+// MullvadVPNUITests
+//
+// Created by Niklas Berglund on 2024-03-04.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import XCTest
+
+class VPNSettingsPage: Page {
+ @discardableResult override init(_ app: XCUIApplication) {
+ super.init(app)
+ }
+
+ private func cellExpandCollapseButton(_ cellAccessiblityIdentifier: AccessibilityIdentifier) -> XCUIElement {
+ let table = app.tables[AccessibilityIdentifier.vpnSettingsTableView]
+ let matchingCells = table.otherElements.containing(.any, identifier: cellAccessiblityIdentifier.rawValue)
+ let expandButton = matchingCells.buttons[AccessibilityIdentifier.collapseButton]
+
+ return expandButton
+ }
+
+ @discardableResult func tapBackButton() -> Self {
+ // Workaround for setting accessibility identifier on navigation bar button being non-trivial
+ app.buttons.matching(identifier: "Settings").allElementsBoundByIndex.last?.tap()
+ return self
+ }
+
+ @discardableResult func tapWireGuardPortsExpandButton() -> Self {
+ cellExpandCollapseButton(AccessibilityIdentifier.wireGuardPortsCell).tap()
+
+ return self
+ }
+
+ @discardableResult func tapWireGuardObfuscationExpandButton() -> Self {
+ cellExpandCollapseButton(AccessibilityIdentifier.wireGuardObfuscationCell).tap()
+
+ return self
+ }
+
+ @discardableResult func tapUDPOverTCPPortExpandButton() -> Self {
+ cellExpandCollapseButton(AccessibilityIdentifier.udpOverTCPPortCell).tap()
+
+ return self
+ }
+
+ @discardableResult func tapQuantumResistantTunnelExpandButton() -> Self {
+ cellExpandCollapseButton(AccessibilityIdentifier.quantumResistantTunnelCell).tap()
+
+ return self
+ }
+
+ @discardableResult func tapWireGuardObfuscationAutomaticCell() -> Self {
+ app.cells[AccessibilityIdentifier.wireGuardObfuscationAutomatic]
+ .tap()
+
+ return self
+ }
+
+ @discardableResult func tapWireGuardObfuscationOnCell() -> Self {
+ app.cells[AccessibilityIdentifier.wireGuardObfuscationOn].tap()
+
+ return self
+ }
+
+ @discardableResult func tapWireGuardObfuscationOffCell() -> Self {
+ app.cells[AccessibilityIdentifier.wireGuardObfuscationOff].tap()
+
+ return self
+ }
+
+ @discardableResult func tapCustomWireGuardPortTextField() -> Self {
+ app.textFields[AccessibilityIdentifier.customWireGuardPortTextField].tap()
+
+ return self
+ }
+}
diff --git a/ios/MullvadVPNUITests/RelayTests.swift b/ios/MullvadVPNUITests/RelayTests.swift
index a186bd7548..b90d158ba7 100644
--- a/ios/MullvadVPNUITests/RelayTests.swift
+++ b/ios/MullvadVPNUITests/RelayTests.swift
@@ -10,6 +10,22 @@ import Foundation
import XCTest
class RelayTests: LoggedInWithTimeUITestCase {
+ var removeFirewallRulesInTearDown = false
+
+ override func setUp() {
+ super.setUp()
+
+ removeFirewallRulesInTearDown = false
+ }
+
+ override func tearDown() {
+ super.tearDown()
+
+ if removeFirewallRulesInTearDown {
+ FirewallAPIClient().removeRules()
+ }
+ }
+
func testAdBlockingViaDNS() throws {
HeaderBar(app)
.tapSettingsButton()
@@ -30,5 +46,117 @@ class RelayTests: LoggedInWithTimeUITestCase {
.waitForSecureConnectionLabel()
try Networking.verifyCannotReachAdServingDomain()
+
+ TunnelControlPage(app)
+ .tapDisconnectButton()
+ }
+
+ func testWireGuardOverTCPManually() throws {
+ HeaderBar(app)
+ .tapSettingsButton()
+
+ SettingsPage(app)
+ .tapVPNSettingsCell()
+
+ VPNSettingsPage(app)
+ .tapWireGuardObfuscationExpandButton()
+ .tapWireGuardObfuscationOnCell()
+ .tapBackButton()
+
+ SettingsPage(app)
+ .tapDoneButton()
+
+ TunnelControlPage(app)
+ .tapSecureConnectionButton()
+
+ allowAddVPNConfigurationsIfAsked()
+
+ TunnelControlPage(app)
+ .waitForSecureConnectionLabel()
+
+ try Networking.verifyCanAccessInternet()
+
+ TunnelControlPage(app)
+ .tapDisconnectButton()
+ }
+
+ /// Test automatic switching to TCP is functioning when UDP traffic to relay is blocked. This test first connects to a realy to get the IP address of it, in order to block UDP traffic to this relay.
+ func testWireGuardOverTCPAutomatically() throws {
+ let wireGuardGot001RelayName = "se-got-wg-001"
+
+ FirewallAPIClient().removeRules()
+ removeFirewallRulesInTearDown = true
+
+ // First get relay IP address
+ TunnelControlPage(app)
+ .tapSelectLocationButton()
+
+ SelectLocationPage(app)
+ .tapLocationCellExpandCollapseButton(withName: "Sweden")
+ .tapLocationCellExpandCollapseButton(withName: "Gothenburg")
+ .tapLocationCell(withName: wireGuardGot001RelayName)
+
+ allowAddVPNConfigurationsIfAsked()
+
+ let relayIPAddress = TunnelControlPage(app)
+ .waitForSecureConnectionLabel()
+ .tapRelayStatusExpandCollapseButton()
+ .getInIPAddressFromConnectionStatus()
+
+ TunnelControlPage(app)
+ .tapDisconnectButton()
+
+ // Run actual test
+ try FirewallAPIClient().createRule(
+ FirewallRule.makeBlockUDPTrafficRule(toIPAddress: relayIPAddress)
+ )
+
+ HeaderBar(app)
+ .tapSettingsButton()
+
+ SettingsPage(app)
+ .tapVPNSettingsCell()
+
+ VPNSettingsPage(app)
+ .tapWireGuardObfuscationExpandButton()
+ .tapWireGuardObfuscationAutomaticCell()
+ .tapBackButton()
+
+ SettingsPage(app)
+ .tapDoneButton()
+
+ TunnelControlPage(app)
+ .tapSecureConnectionButton()
+
+ // Should be two UDP connection attempts but sometimes only one is shown in the UI
+ TunnelControlPage(app)
+ .verifyConnectingOverTCPAfterUDPAttempts()
+ .waitForSecureConnectionLabel()
+ .tapDisconnectButton()
+ }
+
+ func testWireGuardPortSettings() throws {
+ HeaderBar(app)
+ .tapSettingsButton()
+
+ SettingsPage(app)
+ .tapVPNSettingsCell()
+
+ VPNSettingsPage(app)
+ .tapWireGuardPortsExpandButton()
+ .tapCustomWireGuardPortTextField()
+ .enterText("4001")
+ .dismissKeyboard()
+ .swipeDownToDismissModal()
+
+ TunnelControlPage(app)
+ .tapSecureConnectionButton()
+
+ allowAddVPNConfigurationsIfAsked()
+
+ TunnelControlPage(app)
+ .tapRelayStatusExpandCollapseButton()
+ .verifyConnectingToPort("4001")
+ .tapDisconnectButton()
}
}
diff --git a/ios/MullvadVPNUITests/Test base classes/BaseUITestCase.swift b/ios/MullvadVPNUITests/Test base classes/BaseUITestCase.swift
index 2e4a252c76..be7d517d54 100644
--- a/ios/MullvadVPNUITests/Test base classes/BaseUITestCase.swift
+++ b/ios/MullvadVPNUITests/Test base classes/BaseUITestCase.swift
@@ -12,6 +12,7 @@ import XCTest
class BaseUITestCase: XCTestCase {
let app = XCUIApplication()
static let defaultTimeout = 5.0
+ static let shortTimeout = 1.0
// swiftlint:disable force_cast
let displayName = Bundle(for: BaseUITestCase.self)
@@ -41,6 +42,15 @@ class BaseUITestCase: XCTestCase {
}
}
+ /// Handle iOS add VPN configuration permission alert if presented, otherwise ignore
+ func allowAddVPNConfigurationsIfAsked() {
+ let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
+
+ if springboard.buttons["Allow"].waitForExistence(timeout: Self.shortTimeout) {
+ allowAddVPNConfigurations()
+ }
+ }
+
// MARK: - Setup & teardown
/// Suite level teardown ran after test have executed
@@ -62,10 +72,16 @@ class BaseUITestCase: XCTestCase {
/// Check if currently logged on to an account. Note that it is assumed that we are logged in if login view isn't currently shown.
func isLoggedIn() -> Bool {
return !app
- .otherElements[AccessibilityIdentifier.loginView.rawValue]
+ .otherElements[AccessibilityIdentifier.loginView]
.waitForExistence(timeout: 1.0)
}
+ func isPresentingSettings() -> Bool {
+ return app
+ .otherElements[AccessibilityIdentifier.settingsContainerView]
+ .exists
+ }
+
func agreeToTermsOfServiceIfShown() {
let termsOfServiceIsShown = app.otherElements[
AccessibilityIdentifier
@@ -101,10 +117,14 @@ class BaseUITestCase: XCTestCase {
func logoutIfLoggedIn() {
if isLoggedIn() {
+ if isPresentingSettings() {
+ SettingsPage(app)
+ .swipeDownToDismissModal()
+ }
+
if app.buttons[AccessibilityIdentifier.accountButton].exists {
HeaderBar(app)
.tapAccountButton()
-
AccountPage(app)
.tapLogOutButton()
} else {