From 4c4d8f287724117c5c41b7f2b49c87443a06cd9c Mon Sep 17 00:00:00 2001 From: Niklas Berglund Date: Wed, 17 Apr 2024 11:26:22 +0200 Subject: [PATCH] Add iOS test verifying API can be reached from blocked state --- ios/MullvadVPN.xcodeproj/project.pbxproj | 4 ++ .../Classes/AccessbilityIdentifier.swift | 12 +++- .../Account/AccountDeviceRow.swift | 1 + .../RelayFilter/RelayFilterCellFactory.swift | 4 +- .../RelayFilter/RelayFilterDataSource.swift | 4 ++ .../SelectLocation/LocationCell.swift | 14 +++- .../LocationViewController.swift | 1 + .../Settings/SettingsHeaderView.swift | 3 +- .../MullvadVPNScreenshots.swift | 6 +- ios/MullvadVPNUITests/ConnectivityTests.swift | 70 ++++++++++++++++++- ios/MullvadVPNUITests/CustomListsTests.swift | 2 +- ios/MullvadVPNUITests/Pages/AccountPage.swift | 5 ++ .../Pages/SelectLocationFilterPage.swift | 48 +++++++++++++ .../Pages/SelectLocationPage.swift | 39 +++++++++-- 14 files changed, 197 insertions(+), 16 deletions(-) create mode 100644 ios/MullvadVPNUITests/Pages/SelectLocationFilterPage.swift diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 2a5f7feac07f..27fc3db0b0da 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -632,6 +632,7 @@ 852BC6732BAB450B00A47558 /* MullvadVPNUITestsChangeDNSSettings.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = 852BC6722BAB450B00A47558 /* MullvadVPNUITestsChangeDNSSettings.xctestplan */; }; 8532E6872B8CCED600ACECD1 /* ProblemReportSubmittedPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8532E6862B8CCED600ACECD1 /* ProblemReportSubmittedPage.swift */; }; 8542CE242B95F7B9006FCA14 /* VPNSettingsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8542CE232B95F7B9006FCA14 /* VPNSettingsPage.swift */; }; + 8542F7532BCFBD050035C042 /* SelectLocationFilterPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8542F7522BCFBD050035C042 /* SelectLocationFilterPage.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 */; }; @@ -1897,6 +1898,7 @@ 852BC6722BAB450B00A47558 /* MullvadVPNUITestsChangeDNSSettings.xctestplan */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = MullvadVPNUITestsChangeDNSSettings.xctestplan; sourceTree = ""; }; 8532E6862B8CCED600ACECD1 /* ProblemReportSubmittedPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemReportSubmittedPage.swift; sourceTree = ""; }; 8542CE232B95F7B9006FCA14 /* VPNSettingsPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNSettingsPage.swift; sourceTree = ""; }; + 8542F7522BCFBD050035C042 /* SelectLocationFilterPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationFilterPage.swift; sourceTree = ""; }; 85557B0D2B591B2600795FE1 /* FirewallAPIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirewallAPIClient.swift; sourceTree = ""; }; 85557B0F2B59215F00795FE1 /* FirewallRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirewallRule.swift; sourceTree = ""; }; 85557B112B594FC900795FE1 /* ConnectivityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectivityTests.swift; sourceTree = ""; }; @@ -3812,6 +3814,7 @@ 850201DE2B5040A500EF8C96 /* TunnelControlPage.swift */, 8542CE232B95F7B9006FCA14 /* VPNSettingsPage.swift */, 85FB5A0B2B6903990015DCED /* WelcomePage.swift */, + 8542F7522BCFBD050035C042 /* SelectLocationFilterPage.swift */, ); path = Pages; sourceTree = ""; @@ -5797,6 +5800,7 @@ A9DF789D2B7D1E8B0094E4AD /* LoggedInWithTimeUITestCase.swift in Sources */, 85D2B0B12B6BD32400DF9DA7 /* BaseUITestCase.swift in Sources */, 8529693C2B4F0257007EAD4C /* Alert.swift in Sources */, + 8542F7532BCFBD050035C042 /* SelectLocationFilterPage.swift in Sources */, 850201DD2B503D8C00EF8C96 /* SelectLocationPage.swift in Sources */, 85D039982BA4711800940E7F /* SettingsMigrationTests.swift in Sources */, 850201DB2B503D7700EF8C96 /* RelayTests.swift in Sources */, diff --git a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift index 9be9248b7d2b..9f738fabcc2b 100644 --- a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift +++ b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift @@ -52,6 +52,7 @@ public enum AccessibilityIdentifier: String { case cancelDeleteCustomListButton case customListLocationCheckmarkButton case listCustomListDoneButton + case selectLocationFilterButton // Cells case deviceCell @@ -63,7 +64,6 @@ public enum AccessibilityIdentifier: String { case problemReportCell case faqCell case apiAccessCell - case relayFilterOwnershipCell case relayFilterProviderCell case wireGuardPortsCell case wireGuardObfuscationCell @@ -72,9 +72,19 @@ public enum AccessibilityIdentifier: String { case customListEditNameFieldCell case customListEditAddOrEditLocationCell case customListEditDeleteListCell + case locationFilterOwnershipHeaderCell + case locationFilterProvidersHeaderCell + case ownershipMullvadOwnedCell + case ownershipRentedCell + case ownershipAnyCell + case countryLocationCell + case cityLocationCell + case relayLocationCell + case customListLocationCell // Labels case accountPagePaidUntilLabel + case accountPageDeviceNameLabel case headerDeviceNameLabel case connectionStatusConnectedLabel case connectionStatusNotConnectedLabel diff --git a/ios/MullvadVPN/View controllers/Account/AccountDeviceRow.swift b/ios/MullvadVPN/View controllers/Account/AccountDeviceRow.swift index 5a38dd908a7a..ea71f4036e72 100644 --- a/ios/MullvadVPN/View controllers/Account/AccountDeviceRow.swift +++ b/ios/MullvadVPN/View controllers/Account/AccountDeviceRow.swift @@ -13,6 +13,7 @@ class AccountDeviceRow: UIView { didSet { deviceLabel.text = deviceName?.capitalized ?? "" accessibilityValue = deviceName + accessibilityIdentifier = .accountPageDeviceNameLabel } } diff --git a/ios/MullvadVPN/View controllers/RelayFilter/RelayFilterCellFactory.swift b/ios/MullvadVPN/View controllers/RelayFilter/RelayFilterCellFactory.swift index 99c341863ea2..25af0653696c 100644 --- a/ios/MullvadVPN/View controllers/RelayFilter/RelayFilterCellFactory.swift +++ b/ios/MullvadVPN/View controllers/RelayFilter/RelayFilterCellFactory.swift @@ -34,10 +34,13 @@ struct RelayFilterCellFactory: CellFactoryProtocol { switch item { case .ownershipAny: title = "Any" + cell.accessibilityIdentifier = .ownershipAnyCell case .ownershipOwned: title = "Mullvad owned only" + cell.accessibilityIdentifier = .ownershipMullvadOwnedCell case .ownershipRented: title = "Rented only" + cell.accessibilityIdentifier = .ownershipRentedCell default: assertionFailure("Item mismatch. Got: \(item)") } @@ -50,7 +53,6 @@ struct RelayFilterCellFactory: CellFactoryProtocol { ) cell.applySubCellStyling() - cell.accessibilityIdentifier = .relayFilterOwnershipCell } private func configureProviderCell(_ cell: UITableViewCell, item: RelayFilterDataSource.Item) { diff --git a/ios/MullvadVPN/View controllers/RelayFilter/RelayFilterDataSource.swift b/ios/MullvadVPN/View controllers/RelayFilter/RelayFilterDataSource.swift index 3d0653f569f3..3b4fadeac9b6 100644 --- a/ios/MullvadVPN/View controllers/RelayFilter/RelayFilterDataSource.swift +++ b/ios/MullvadVPN/View controllers/RelayFilter/RelayFilterDataSource.swift @@ -243,14 +243,18 @@ extension RelayFilterDataSource: UITableViewDelegate { let sectionId = snapshot().sectionIdentifiers[section] let title: String + let accessibilityIdentifier: AccessibilityIdentifier switch sectionId { case .ownership: + accessibilityIdentifier = .locationFilterOwnershipHeaderCell title = "Ownership" case .providers: + accessibilityIdentifier = .locationFilterProvidersHeaderCell title = "Providers" } + view.accessibilityIdentifier = accessibilityIdentifier view.titleLabel.text = NSLocalizedString( "RELAY_FILTER_HEADER_LABEL", tableName: "Relay filter header", diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationCell.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationCell.swift index ba4de11c17ed..e777ca5e21d4 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/LocationCell.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationCell.swift @@ -309,7 +309,6 @@ extension LocationCell { } func configure(item: LocationCellViewModel, behavior: LocationCellBehavior) { - accessibilityIdentifier = item.node.code isDisabled = !item.node.isActive locationLabel.text = item.node.name showsCollapseControl = !item.node.children.isEmpty @@ -318,6 +317,19 @@ extension LocationCell { checkboxButton.isSelected = item.isSelected checkboxButton.tintColor = item.isSelected ? .successColor : .white + if item.node is CountryLocationNode { + accessibilityIdentifier = .countryLocationCell + accessibilityValue = item.node.code + } else if item.node is CityLocationNode { + accessibilityIdentifier = .cityLocationCell + accessibilityValue = item.node.code + } else if item.node is HostLocationNode { + accessibilityIdentifier = .relayLocationCell + accessibilityValue = item.node.code + } else if item.node is CustomListLocationNode { + accessibilityIdentifier = .customListLocationCell + } + setBehavior(behavior) } diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift index a8bd63f1c2ca..fa787f86c683 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift @@ -76,6 +76,7 @@ final class LocationViewController: UIViewController { self?.navigateToFilter?() }) ) + navigationItem.leftBarButtonItem?.accessibilityIdentifier = .selectLocationFilterButton navigationItem.rightBarButtonItem = UIBarButtonItem( systemItem: .done, diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsHeaderView.swift b/ios/MullvadVPN/View controllers/Settings/SettingsHeaderView.swift index e6735ea2027e..7b51fe37ffa7 100644 --- a/ios/MullvadVPN/View controllers/Settings/SettingsHeaderView.swift +++ b/ios/MullvadVPN/View controllers/Settings/SettingsHeaderView.swift @@ -31,7 +31,7 @@ class SettingsHeaderView: UITableViewHeaderFooterView { let collapseButton: UIButton = { let button = UIButton(type: .custom) - button.accessibilityIdentifier = .collapseButton + button.accessibilityIdentifier = .expandButton button.tintColor = .white return button }() @@ -123,6 +123,7 @@ class SettingsHeaderView: UITableViewHeaderFooterView { let image = isExpanded ? chevronUp : chevronDown collapseButton.setImage(image, for: .normal) + collapseButton.accessibilityIdentifier = isExpanded ? .collapseButton : .expandButton } private func updateAccessibilityCustomActions() { diff --git a/ios/MullvadVPNScreenshots/MullvadVPNScreenshots.swift b/ios/MullvadVPNScreenshots/MullvadVPNScreenshots.swift index 18b53f563249..4aacb5f72898 100644 --- a/ios/MullvadVPNScreenshots/MullvadVPNScreenshots.swift +++ b/ios/MullvadVPNScreenshots/MullvadVPNScreenshots.swift @@ -92,8 +92,10 @@ class MullvadVPNScreenshots: XCTestCase { // Tap the "Filter" button and expand each relay filter app.navigationBars.buttons["Filter"].tap() - app.otherElements["Ownership"].buttons[AccessibilityIdentifier.collapseButton.rawValue].tap() - app.otherElements["Providers"].buttons[AccessibilityIdentifier.collapseButton.rawValue].tap() + app.otherElements[AccessibilityIdentifier.locationFilterOwnershipHeaderCell.rawValue] + .buttons[AccessibilityIdentifier.expandButton.rawValue].tap() + app.otherElements[AccessibilityIdentifier.locationFilterProvidersHeaderCell.rawValue] + .buttons[AccessibilityIdentifier.expandButton.rawValue].tap() snapshot("RelayFilter") app.navigationBars.buttons["Cancel"].tap() diff --git a/ios/MullvadVPNUITests/ConnectivityTests.swift b/ios/MullvadVPNUITests/ConnectivityTests.swift index 9288f84f5d5a..3caa77dd84ce 100644 --- a/ios/MullvadVPNUITests/ConnectivityTests.swift +++ b/ios/MullvadVPNUITests/ConnectivityTests.swift @@ -15,13 +15,13 @@ class ConnectivityTests: LoggedOutUITestCase { override func tearDownWithError() throws { super.tearDown() - firewallAPIClient.removeRules() } /// Verifies that the app still functions when API has been blocked func testAPIConnectionViaBridges() throws { - let app = XCUIApplication() - app.launch() + addTeardownBlock { + self.firewallAPIClient.removeRules() + } try Networking.verifyCanAccessAPI() // Just to make sure there's no old firewall rule still active firewallAPIClient.createRule(try FirewallRule.makeBlockAPIAccessFirewallRule()) @@ -45,4 +45,68 @@ class ConnectivityTests: LoggedOutUITestCase { .verifyDeviceLabelShown() } } + + /// Get the app into a blocked state by connecting to a relay then applying a filter which don't find this relay, then verify that app can still communicate by logging out and verifying that the device was successfully removed + func testAPIReachableWhenBlocked() throws { + // Setup. Enter blocked state by connecting to relay and applying filter which relay isn't part of. + login(accountNumber: hasTimeAccountNumber) + + TunnelControlPage(app) + .tapSelectLocationButton() + + SelectLocationPage(app) + .tapFilterButton() + + SelectLocationFilterPage(app) + .tapOwnershipCellExpandButton() + .tapMullvadOwnershipCell() + .tapApplyButton() + + // Select the first country, its first city and its first relay + SelectLocationPage(app) + .tapCountryLocationCellExpandButton(withIndex: 0) + .tapCityLocationCellExpandButton(withIndex: 0) + .tapRelayLocationCell(withIndex: 0) + + allowAddVPNConfigurationsIfAsked() + + TunnelControlPage(app) + .tapSelectLocationButton() + + SelectLocationPage(app) + .tapFilterButton() + + SelectLocationFilterPage(app) + .tapOwnershipCellExpandButton() + .tapRentedOwnershipCell() + .tapApplyButton() + + SelectLocationPage(app) + .tapDoneButton() + + // Get device name, log out and make sure device was removed as a a way of verifying that the API can be reached + HeaderBar(app) + .tapAccountButton() + + let deviceName = try AccountPage(app).getDeviceName() + + AccountPage(app) + .tapLogOutButton() + + LoginPage(app) + + verifyDeviceHasBeenRemoved(deviceName: deviceName, accountNumber: hasTimeAccountNumber) + } +} + +private func verifyDeviceHasBeenRemoved(deviceName: String, accountNumber: String) { + do { + let devices = try MullvadAPIWrapper().getDevices(accountNumber) + + for device in devices where device.name == deviceName { + XCTFail("Device has not been removed which tells us that the logout was not successful") + } + } catch { + XCTFail("Failed to get devices from app API") + } } diff --git a/ios/MullvadVPNUITests/CustomListsTests.swift b/ios/MullvadVPNUITests/CustomListsTests.swift index eb579b1b5546..6a3f9f1db725 100644 --- a/ios/MullvadVPNUITests/CustomListsTests.swift +++ b/ios/MullvadVPNUITests/CustomListsTests.swift @@ -128,7 +128,7 @@ class CustomListsTests: LoggedInWithTimeUITestCase { func workaroundOpenCustomListMenuBug() { // In order to avoid a bug where the open custom list button cannot be found, the location view is closed and then reopened SelectLocationPage(app) - .closeSelectLocationPage() + .tapDoneButton() TunnelControlPage(app) .tapSelectLocationButton() } diff --git a/ios/MullvadVPNUITests/Pages/AccountPage.swift b/ios/MullvadVPNUITests/Pages/AccountPage.swift index b076c09b99d8..811f3ef68dbb 100644 --- a/ios/MullvadVPNUITests/Pages/AccountPage.swift +++ b/ios/MullvadVPNUITests/Pages/AccountPage.swift @@ -42,6 +42,11 @@ class AccountPage: Page { return self } + func getDeviceName() throws -> String { + let deviceNameLabel = app.otherElements[AccessibilityIdentifier.accountPageDeviceNameLabel] + return try XCTUnwrap(deviceNameLabel.value as? String, "Failed to read device name from label") + } + @discardableResult func verifyPaidUntil(_ date: Date) -> Self { // Strip seconds from date, since the app don't display seconds let calendar = Calendar.current diff --git a/ios/MullvadVPNUITests/Pages/SelectLocationFilterPage.swift b/ios/MullvadVPNUITests/Pages/SelectLocationFilterPage.swift new file mode 100644 index 000000000000..62ad54982051 --- /dev/null +++ b/ios/MullvadVPNUITests/Pages/SelectLocationFilterPage.swift @@ -0,0 +1,48 @@ +// +// SelectLocationFilterPage.swift +// MullvadVPNUITests +// +// Created by Niklas Berglund on 2024-04-17. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import XCTest + +class SelectLocationFilterPage: Page { + override init(_ app: XCUIApplication) { + super.init(app) + } + + @discardableResult func tapOwnershipCellExpandButton() -> Self { + app.otherElements[AccessibilityIdentifier.locationFilterOwnershipHeaderCell] + .buttons[AccessibilityIdentifier.expandButton].tap() + return self + } + + @discardableResult func tapProvidersCellExpandButton() -> Self { + app.otherElements[AccessibilityIdentifier.locationFilterProvidersHeaderCell] + .buttons[AccessibilityIdentifier.expandButton].tap() + return self + } + + @discardableResult func tapAnyOwnershipCell() -> Self { + app.cells[AccessibilityIdentifier.ownershipAnyCell].tap() + return self + } + + @discardableResult func tapMullvadOwnershipCell() -> Self { + app.cells[AccessibilityIdentifier.ownershipMullvadOwnedCell].tap() + return self + } + + @discardableResult func tapRentedOwnershipCell() -> Self { + app.cells[AccessibilityIdentifier.ownershipRentedCell].tap() + return self + } + + @discardableResult func tapApplyButton() -> Self { + app.buttons[AccessibilityIdentifier.applyButton].tap() + return self + } +} diff --git a/ios/MullvadVPNUITests/Pages/SelectLocationPage.swift b/ios/MullvadVPNUITests/Pages/SelectLocationPage.swift index a1f46b7ea875..d4126083ca08 100644 --- a/ios/MullvadVPNUITests/Pages/SelectLocationPage.swift +++ b/ios/MullvadVPNUITests/Pages/SelectLocationPage.swift @@ -22,6 +22,29 @@ class SelectLocationPage: Page { return self } + @discardableResult func tapCountryLocationCellExpandButton(withIndex: Int) -> Self { + let cell = app.cells.containing(.any, identifier: AccessibilityIdentifier.countryLocationCell.rawValue) + .element(boundBy: withIndex) + let expandButton = cell.buttons[AccessibilityIdentifier.expandButton] + expandButton.tap() + return self + } + + @discardableResult func tapCityLocationCellExpandButton(withIndex: Int) -> Self { + let cell = app.cells.containing(.any, identifier: AccessibilityIdentifier.cityLocationCell.rawValue) + .element(boundBy: withIndex) + let expandButton = cell.buttons[AccessibilityIdentifier.expandButton] + expandButton.tap() + return self + } + + @discardableResult func tapRelayLocationCell(withIndex: Int) -> Self { + let cell = app.cells.containing(.any, identifier: AccessibilityIdentifier.relayLocationCell.rawValue) + .element(boundBy: withIndex) + cell.tap() + return self + } + @discardableResult func tapLocationCellExpandButton(withName name: String) -> Self { let table = app.tables[AccessibilityIdentifier.selectLocationTableView] let matchingCells = table.cells.containing(.any, identifier: name) @@ -44,12 +67,6 @@ class SelectLocationPage: Page { return self } - @discardableResult func closeSelectLocationPage() -> Self { - let doneButton = app.buttons[.closeSelectLocationButton] - doneButton.tap() - return self - } - @discardableResult func tapCustomListEllipsisButton() -> Self { let customListEllipsisButton = app.buttons[AccessibilityIdentifier.openCustomListsMenuButton] customListEllipsisButton.tap() @@ -72,6 +89,16 @@ class SelectLocationPage: Page { app.tables[AccessibilityIdentifier.selectLocationTableView].cells[identifier] } + @discardableResult func tapFilterButton() -> Self { + app.buttons[AccessibilityIdentifier.selectLocationFilterButton].tap() + return self + } + + @discardableResult func tapDoneButton() -> Self { + app.buttons[AccessibilityIdentifier.closeSelectLocationButton].tap() + return self + } + func locationCellIsExpanded(_ name: String) -> Bool { let matchingCells = app.cells.containing(.any, identifier: name) return matchingCells.buttons[AccessibilityIdentifier.expandButton].exists ? false : true