-
Notifications
You must be signed in to change notification settings - Fork 398
/
Copy pathTunnelControlPage.swift
234 lines (195 loc) · 9.31 KB
/
TunnelControlPage.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
//
// TunnelControlPage.swift
// MullvadVPNUITests
//
// Created by Niklas Berglund on 2024-01-11.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//
import Foundation
import XCTest
class TunnelControlPage: Page {
private struct ConnectionAttempt: Hashable {
let ipAddress: String
let port: String
let protocolName: String
}
var connectionIsSecured: Bool {
app.staticTexts[AccessibilityIdentifier.connectionStatusConnectedLabel].exists
}
/// 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: return the attemps found so far after this many seconds if `attemptsCount` haven't been reached yet
private func waitForConnectionAttempts(_ attemptsCount: Int, timeout: TimeInterval) -> [ConnectionAttempt] {
var connectionAttempts: [ConnectionAttempt] = []
var lastConnectionAttempt: 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 connectionAttempt != lastConnectionAttempt {
connectionAttempts.append(connectionAttempt)
lastConnectionAttempt = connectionAttempt
if connectionAttempts.count == attemptsCount {
break
}
}
}
}
return connectionAttempts
}
@discardableResult override init(_ app: XCUIApplication) {
super.init(app)
self.pageElement = app.otherElements[.tunnelControlView]
waitForPageToBeShown()
}
@discardableResult func tapSelectLocationButton() -> Self {
app.buttons[AccessibilityIdentifier.selectLocationButton].tap()
return self
}
@discardableResult func tapSecureConnectionButton() -> Self {
app.buttons[AccessibilityIdentifier.secureConnectionButton].tap()
return self
}
@discardableResult func tapDisconnectButton() -> Self {
app.buttons[AccessibilityIdentifier.disconnectButton].tap()
return self
}
@discardableResult func tapCancelButton() -> Self {
app.buttons[AccessibilityIdentifier.cancelButton].tap()
return self
}
/// Tap either cancel or disconnect button, depending on the current connection state. Use this function sparingly when it's irrelevant whether the app is currently connecting to a relay or already connected.
@discardableResult func tapCancelOrDisconnectButton() -> Self {
let cancelButton = app.buttons[.cancelButton]
let disconnectButton = app.buttons[.disconnectButton]
if disconnectButton.exists && disconnectButton.isHittable {
disconnectButton.tap()
} else {
cancelButton.tap()
}
return self
}
@discardableResult func waitForSecureConnectionLabel() -> Self {
let labelFound = app.staticTexts[.connectionStatusConnectedLabel]
.waitForExistence(timeout: BaseUITestCase.extremelyLongTimeout)
XCTAssertTrue(labelFound, "Secure connection label presented")
return self
}
@discardableResult func tapRelayStatusExpandCollapseButton() -> Self {
app.otherElements[AccessibilityIdentifier.relayStatusCollapseButton].press(forDuration: .leastNonzeroMagnitude)
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
}
/// Verify that connection attempts are made in the correct order
@discardableResult func verifyConnectionAttemptsOrder() -> Self {
let connectionAttempts = waitForConnectionAttempts(4, timeout: 50)
XCTAssertEqual(connectionAttempts.count, 4)
if connectionAttempts.last?.protocolName == "UDP" {
// If last attempt is over UDP it means we have encountered the UI bug where only one UDP attempt is shown and then the two TCP attempts
for (attemptIndex, attempt) in connectionAttempts.enumerated() {
if attemptIndex == 0 {
XCTAssertEqual(attempt.protocolName, "UDP")
} else if attemptIndex == 1 {
XCTAssertEqual(attempt.protocolName, "TCP")
XCTAssertEqual(attempt.port, "80")
} else if attemptIndex == 2 {
XCTAssertEqual(attempt.protocolName, "TCP")
XCTAssertEqual(attempt.port, "5001")
} // Ignore the 4th attempt which is the first attempt of new attempt cycle
}
} else {
for (attemptIndex, attempt) in connectionAttempts.enumerated() {
if attemptIndex == 0 {
XCTAssertEqual(attempt.protocolName, "UDP")
} else if attemptIndex == 1 {
XCTAssertEqual(attempt.protocolName, "UDP")
} else if attemptIndex == 2 {
XCTAssertEqual(attempt.protocolName, "TCP")
XCTAssertEqual(attempt.port, "80")
} else if attemptIndex == 3 {
XCTAssertEqual(attempt.protocolName, "TCP")
XCTAssertEqual(attempt.port, "5001")
}
}
}
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
}
/// Verify that the app attempts to connect over Multihop.
@discardableResult func verifyConnectingOverMultihop() -> Self {
let relayName = getCurrentRelayName().lowercased()
XCTAssertTrue(relayName.contains("via"))
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()
}
}
func getCurrentRelayName() -> String {
let relayExpandButton = app.otherElements[.relayStatusCollapseButton]
guard let relayName = relayExpandButton.value as? String else {
XCTFail("Failed to read relay name from tunnel control page")
return String()
}
return relayName
}
}