From 3d803f42e551faa825673f9832245c0bd4096f7f Mon Sep 17 00:00:00 2001 From: Niklas Berglund Date: Tue, 30 Jan 2024 12:33:53 +0100 Subject: [PATCH] Add account creation and deletion tests --- ios/MullvadVPN.xcodeproj/project.pbxproj | 85 +++- .../xcshareddata/swiftpm/Package.resolved | 22 ++ .../Classes/AccessbilityIdentifier.swift | 7 + .../Coordinators/ChangeLogCoordinator.swift | 1 + .../AccountDeletionContentView.swift | 2 + .../Alert/AlertPresentation.swift | 1 + .../Alert/AlertViewController.swift | 5 +- .../Welcome/WelcomeContentView.swift | 2 + .../Login/LoginContentView.swift | 1 + .../OutOfTime/OutOfTimeContentView.swift | 1 + ios/MullvadVPNUITests/AccountTests.swift | 40 ++ ios/MullvadVPNUITests/MullvadApi.swift | 4 + .../Networking/MullvadAPIWrapper.swift | 59 ++- .../Pages/AccountDeletionPage.swift | 39 ++ .../Pages/ChangeLogAlert.swift | 24 ++ ios/MullvadVPNUITests/Pages/LoginPage.swift | 5 + .../Pages/OutOfTimePage.swift | 19 + .../Pages/SelectLocationPage.swift | 1 + .../Pages/SettingsPage.swift | 1 + .../Pages/TermsOfServicePage.swift | 1 + .../Pages/TunnelControlPage.swift | 1 + ios/MullvadVPNUITests/Pages/WelcomePage.swift | 34 ++ .../Test base classes/BaseUITestCase.swift | 12 +- .../LoggedInWithTimeUITestCase.swift | 1 + .../LoggedInWithoutTimeUITestCase.swift | 1 + .../LoggedOutUITestCase.swift | 1 + ios/TestPlans/MullvadVPNUITestsAll.xctestplan | 8 +- mullvad-api/src/ios_ffi.rs | 374 ------------------ mullvad-api/src/lib.rs | 21 - 29 files changed, 358 insertions(+), 415 deletions(-) create mode 100644 ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 ios/MullvadVPNUITests/Pages/AccountDeletionPage.swift create mode 100644 ios/MullvadVPNUITests/Pages/ChangeLogAlert.swift create mode 100644 ios/MullvadVPNUITests/Pages/OutOfTimePage.swift create mode 100644 ios/MullvadVPNUITests/Pages/WelcomePage.swift delete mode 100644 mullvad-api/src/ios_ffi.rs diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index ac00bedd381a..c5fd1e2ce4d3 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -596,6 +596,7 @@ 850201DB2B503D7700EF8C96 /* RelayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850201DA2B503D7700EF8C96 /* RelayTests.swift */; }; 850201DD2B503D8C00EF8C96 /* SelectLocationPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850201DC2B503D8C00EF8C96 /* SelectLocationPage.swift */; }; 850201DF2B5040A500EF8C96 /* TunnelControlPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850201DE2B5040A500EF8C96 /* TunnelControlPage.swift */; }; + 85139B2D2B84B4A700734217 /* OutOfTimePage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85139B2C2B84B4A700734217 /* OutOfTimePage.swift */; }; 852969282B4D9C1F007EAD4C /* AccountTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852969272B4D9C1F007EAD4C /* AccountTests.swift */; }; 852969332B4E9232007EAD4C /* Page.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852969322B4E9232007EAD4C /* Page.swift */; }; 852969352B4E9270007EAD4C /* LoginPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852969342B4E9270007EAD4C /* LoginPage.swift */; }; @@ -605,15 +606,19 @@ 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 */; }; - 85557B142B5983CF00795FE1 /* MullvadAPIWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85557B132B5983CF00795FE1 /* MullvadAPIWrapper.swift */; }; 85557B162B5ABBBE00795FE1 /* XCUIElementQuery+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85557B152B5ABBBE00795FE1 /* XCUIElementQuery+Extensions.swift */; }; 85557B1E2B5FB8C700795FE1 /* HeaderBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85557B1D2B5FB8C700795FE1 /* HeaderBar.swift */; }; 85557B202B5FBBD700795FE1 /* AccountPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85557B1F2B5FBBD700795FE1 /* AccountPage.swift */; }; 855D9F5B2B63E56B00D7C64D /* ProblemReportPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855D9F5A2B63E56B00D7C64D /* ProblemReportPage.swift */; }; + 8587A05D2B84D43100152938 /* ChangeLogAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8587A05C2B84D43100152938 /* ChangeLogAlert.swift */; }; 8590896C2B61763B003AF5F5 /* LoggedInWithoutTimeUITestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859089682B61763B003AF5F5 /* LoggedInWithoutTimeUITestCase.swift */; }; 8590896F2B61763B003AF5F5 /* LoggedOutUITestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8590896B2B61763B003AF5F5 /* LoggedOutUITestCase.swift */; }; + 85B267612B849ADB0098E3CD /* mullvad-api.h in Headers */ = {isa = PBXBuildFile; fileRef = 85B267602B849ADB0098E3CD /* mullvad-api.h */; }; 85D2B0B12B6BD32400DF9DA7 /* BaseUITestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8590896A2B61763B003AF5F5 /* BaseUITestCase.swift */; }; 85E3BDE52B70E18C00FA71FD /* Networking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85E3BDE42B70E18C00FA71FD /* Networking.swift */; }; + 85EC620C2B838D10005AFFB5 /* MullvadAPIWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85557B132B5983CF00795FE1 /* MullvadAPIWrapper.swift */; }; + 85FB5A0C2B6903990015DCED /* WelcomePage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85FB5A0B2B6903990015DCED /* WelcomePage.swift */; }; + 85FB5A102B6960A30015DCED /* AccountDeletionPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85FB5A0F2B6960A30015DCED /* AccountDeletionPage.swift */; }; A900E9B82ACC5C2B00C95F67 /* AccountsProxy+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A900E9B72ACC5C2B00C95F67 /* AccountsProxy+Stubs.swift */; }; A900E9BA2ACC5D0600C95F67 /* RESTRequestExecutor+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A900E9B92ACC5D0600C95F67 /* RESTRequestExecutor+Stubs.swift */; }; A900E9BC2ACC609200C95F67 /* DevicesProxy+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A900E9BB2ACC609200C95F67 /* DevicesProxy+Stubs.swift */; }; @@ -1239,7 +1244,7 @@ /* Begin PBXFileReference section */ 01EF6F292B6A473900125696 /* MullvadApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadApi.swift; sourceTree = ""; }; - 01EF6F2D2B6A51B100125696 /* mullvad-api.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "mullvad-api.h"; path = "../../mullvad-api/include/mullvad-api.h"; sourceTree = ""; }; + 01EF6F2D2B6A51B100125696 /* mullvad-api.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "mullvad-api.h"; path = "../mullvad-api/include/mullvad-api.h"; sourceTree = ""; }; 01EF6F2F2B6A588300125696 /* aarch64-apple-ios */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "aarch64-apple-ios"; path = "../target/aarch64-apple-ios"; sourceTree = ""; }; 01EF6F312B6A58F000125696 /* debug */ = {isa = PBXFileReference; lastKnownFileType = folder; name = debug; path = "../target/aarch64-apple-ios/debug"; sourceTree = ""; }; 01EF6F332B6A590700125696 /* libmullvad_api.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libmullvad_api.a; path = "../target/aarch64-apple-ios/debug/libmullvad_api.a"; sourceTree = ""; }; @@ -1815,6 +1820,7 @@ 850201DC2B503D8C00EF8C96 /* SelectLocationPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationPage.swift; sourceTree = ""; }; 850201DE2B5040A500EF8C96 /* TunnelControlPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelControlPage.swift; sourceTree = ""; }; 850201E22B51A93C00EF8C96 /* SettingsPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsPage.swift; sourceTree = ""; }; + 85139B2C2B84B4A700734217 /* OutOfTimePage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutOfTimePage.swift; sourceTree = ""; }; 8518F6372B60157E009EB113 /* LoggedInWithoutTimeUITestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggedInWithoutTimeUITestCase.swift; sourceTree = ""; }; 852969252B4D9C1F007EAD4C /* MullvadVPNUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MullvadVPNUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 852969272B4D9C1F007EAD4C /* AccountTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountTests.swift; sourceTree = ""; }; @@ -1833,11 +1839,16 @@ 85557B1D2B5FB8C700795FE1 /* HeaderBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderBar.swift; sourceTree = ""; }; 85557B1F2B5FBBD700795FE1 /* AccountPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountPage.swift; sourceTree = ""; }; 855D9F5A2B63E56B00D7C64D /* ProblemReportPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemReportPage.swift; sourceTree = ""; }; + 8587A05C2B84D43100152938 /* ChangeLogAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangeLogAlert.swift; sourceTree = ""; }; 859089682B61763B003AF5F5 /* LoggedInWithoutTimeUITestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoggedInWithoutTimeUITestCase.swift; sourceTree = ""; }; 859089692B61763B003AF5F5 /* LoggedInWithTimeUITestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoggedInWithTimeUITestCase.swift; sourceTree = ""; }; 8590896A2B61763B003AF5F5 /* BaseUITestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseUITestCase.swift; sourceTree = ""; }; 8590896B2B61763B003AF5F5 /* LoggedOutUITestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoggedOutUITestCase.swift; sourceTree = ""; }; + 85B267602B849ADB0098E3CD /* mullvad-api.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "mullvad-api.h"; path = "../../mullvad-api/include/mullvad-api.h"; sourceTree = ""; }; 85E3BDE42B70E18C00FA71FD /* Networking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Networking.swift; sourceTree = ""; }; + 85FB59F92B67F2140015DCED /* MullvadAPIWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadAPIWrapper.swift; sourceTree = ""; }; + 85FB5A0B2B6903990015DCED /* WelcomePage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomePage.swift; sourceTree = ""; }; + 85FB5A0F2B6960A30015DCED /* AccountDeletionPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletionPage.swift; sourceTree = ""; }; A900E9B72ACC5C2B00C95F67 /* AccountsProxy+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountsProxy+Stubs.swift"; sourceTree = ""; }; A900E9B92ACC5D0600C95F67 /* RESTRequestExecutor+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RESTRequestExecutor+Stubs.swift"; sourceTree = ""; }; A900E9BB2ACC609200C95F67 /* DevicesProxy+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DevicesProxy+Stubs.swift"; sourceTree = ""; }; @@ -3096,6 +3107,7 @@ 7A88DCDD2A8FABBE00D2FF0E /* RoutingTests */, 58CE5E61224146200008646E /* Products */, 584F991F2902CBDD001F858D /* Frameworks */, + 85EC620B2B838B3A005AFFB5 /* Recovered References */, ); sourceTree = ""; }; @@ -3503,12 +3515,10 @@ 852969262B4D9C1F007EAD4C /* MullvadVPNUITests */ = { isa = PBXGroup; children = ( + 85B267602B849ADB0098E3CD /* mullvad-api.h */, 852969272B4D9C1F007EAD4C /* AccountTests.swift */, 85557B112B594FC900795FE1 /* ConnectivityTests.swift */, - 01EF6F352B6A5AEF00125696 /* BridgingHeader.h */, 852969372B4ED20E007EAD4C /* Info.plist */, - 01EF6F2D2B6A51B100125696 /* mullvad-api.h */, - 01EF6F292B6A473900125696 /* MullvadApi.swift */, 85557B0C2B591B0F00795FE1 /* Networking */, 852969312B4E9220007EAD4C /* Pages */, 850201DA2B503D7700EF8C96 /* RelayTests.swift */, @@ -3523,16 +3533,20 @@ 852969312B4E9220007EAD4C /* Pages */ = { isa = PBXGroup; children = ( + 85FB5A0F2B6960A30015DCED /* AccountDeletionPage.swift */, 85557B1F2B5FBBD700795FE1 /* AccountPage.swift */, 8529693B2B4F0257007EAD4C /* Alert.swift */, + 8587A05C2B84D43100152938 /* ChangeLogAlert.swift */, 85557B1D2B5FB8C700795FE1 /* HeaderBar.swift */, 852969342B4E9270007EAD4C /* LoginPage.swift */, + 85139B2C2B84B4A700734217 /* OutOfTimePage.swift */, 852969322B4E9232007EAD4C /* Page.swift */, 855D9F5A2B63E56B00D7C64D /* ProblemReportPage.swift */, 850201DC2B503D8C00EF8C96 /* SelectLocationPage.swift */, 850201E22B51A93C00EF8C96 /* SettingsPage.swift */, 852969392B4F0238007EAD4C /* TermsOfServicePage.swift */, 850201DE2B5040A500EF8C96 /* TunnelControlPage.swift */, + 85FB5A0B2B6903990015DCED /* WelcomePage.swift */, ); path = Pages; sourceTree = ""; @@ -3548,6 +3562,14 @@ path = Networking; sourceTree = ""; }; + 85EC620B2B838B3A005AFFB5 /* Recovered References */ = { + isa = PBXGroup; + children = ( + 85FB59F92B67F2140015DCED /* MullvadAPIWrapper.swift */, + ); + name = "Recovered References"; + sourceTree = ""; + }; A907639F2B2857D50045ADF0 /* Socks5 */ = { isa = PBXGroup; children = ( @@ -3747,6 +3769,7 @@ buildActionMask = 2147483647; files = ( A9DF789C2B7D1E410094E4AD /* BridgingHeader.h in Headers */, + 85B267612B849ADB0098E3CD /* mullvad-api.h in Headers */, A9DF789B2B7D1DF10094E4AD /* mullvad-api.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4230,6 +4253,8 @@ 8529692C2B4D9C1F007EAD4C /* PBXTargetDependency */, ); name = MullvadVPNUITests; + packageProductDependencies = ( + ); productName = MullvadVPNUITests; productReference = 852969252B4D9C1F007EAD4C /* MullvadVPNUITests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; @@ -5475,12 +5500,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 85EC620C2B838D10005AFFB5 /* MullvadAPIWrapper.swift in Sources */, A9DF789D2B7D1E8B0094E4AD /* LoggedInWithTimeUITestCase.swift in Sources */, 85D2B0B12B6BD32400DF9DA7 /* BaseUITestCase.swift in Sources */, 8529693C2B4F0257007EAD4C /* Alert.swift in Sources */, 850201DD2B503D8C00EF8C96 /* SelectLocationPage.swift in Sources */, 850201DB2B503D7700EF8C96 /* RelayTests.swift in Sources */, - 85557B142B5983CF00795FE1 /* MullvadAPIWrapper.swift in Sources */, + 85139B2D2B84B4A700734217 /* OutOfTimePage.swift in Sources */, 852969362B4E9724007EAD4C /* AccessbilityIdentifier.swift in Sources */, 85E3BDE52B70E18C00FA71FD /* Networking.swift in Sources */, 8590896C2B61763B003AF5F5 /* LoggedInWithoutTimeUITestCase.swift in Sources */, @@ -5490,10 +5516,13 @@ 85557B102B59215F00795FE1 /* FirewallRule.swift in Sources */, 85557B0E2B591B2600795FE1 /* FirewallAPIClient.swift in Sources */, 852969282B4D9C1F007EAD4C /* AccountTests.swift in Sources */, + 8587A05D2B84D43100152938 /* ChangeLogAlert.swift in Sources */, 01EF6F2A2B6A473900125696 /* MullvadApi.swift in Sources */, + 85FB5A102B6960A30015DCED /* AccountDeletionPage.swift in Sources */, 85557B162B5ABBBE00795FE1 /* XCUIElementQuery+Extensions.swift in Sources */, 855D9F5B2B63E56B00D7C64D /* ProblemReportPage.swift in Sources */, 8529693A2B4F0238007EAD4C /* TermsOfServicePage.swift in Sources */, + 85FB5A0C2B6903990015DCED /* WelcomePage.swift in Sources */, 850201DF2B5040A500EF8C96 /* TunnelControlPage.swift in Sources */, 85557B1E2B5FB8C700795FE1 /* HeaderBar.swift in Sources */, 85557B122B594FC900795FE1 /* ConnectivityTests.swift in Sources */, @@ -6286,6 +6315,7 @@ ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ""; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; @@ -6850,8 +6880,12 @@ "DEVELOPMENT_TEAM[sdk=macosx*]" = CKG9MXH72F; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); GENERATE_INFOPLIST_FILE = YES; - HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/../mullvad-api/include"; + HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/mullvad-api/include"; INFOPLIST_FILE = MullvadVPNUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.2; "LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64]" = "$(PROJECT_DIR)/../target/aarch64-apple-ios/debug"; @@ -6863,8 +6897,8 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "MullvadVPN app integration tests"; - SECURITY_GROUP_IDENTIFIER = group.net.mullvad.MullvadVPN; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SECURITY_GROUP_IDENTIFIER = ""; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG MULLVAD_ENVIRONMENT_PRODUCTION $(inherited)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_OBJC_BRIDGING_HEADER = "$(PROJECT_DIR)/MullvadVPNUITests/BridgingHeader.h"; SWIFT_VERSION = 5.0; @@ -6875,13 +6909,14 @@ }; 8529692E2B4D9C1F007EAD4C /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 852969382B4ED818007EAD4C /* UITests.xcconfig */; buildSettings = { APPLICATION_IDENTIFIER = net.mullvad.MullvadVPN; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; - HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/../mullvad-api/include"; + HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/mullvad-api/include"; INFOPLIST_FILE = MullvadVPNUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.2; "LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64]" = "$(PROJECT_DIR)/../target/aarch64-apple-ios/release"; @@ -6891,7 +6926,7 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "$(APPLICATION_IDENTIFIER).MullvadVPNUITests"; PRODUCT_NAME = "$(TARGET_NAME)"; - SECURITY_GROUP_IDENTIFIER = group.net.mullvad.MullvadVPN; + SECURITY_GROUP_IDENTIFIER = ""; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_OBJC_BRIDGING_HEADER = "$(PROJECT_DIR)/MullvadVPNUITests/BridgingHeader.h"; SWIFT_VERSION = 5.0; @@ -7475,25 +7510,53 @@ }; A93A1D062B59145C00F7796C /* Staging */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 852969382B4ED818007EAD4C /* UITests.xcconfig */; buildSettings = { + APPLICATION_IDENTIFIER = net.mullvad.MullvadVPN; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = CKG9MXH72F; + "DEVELOPMENT_TEAM[sdk=macosx*]" = CKG9MXH72F; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GENERATE_INFOPLIST_FILE = YES; HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/../mullvad-api/include"; + INFOPLIST_FILE = MullvadVPNUITests/Info.plist; "LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64]" = "$(PROJECT_DIR)/../target/aarch64-apple-ios/debug"; "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=arm64]" = "$(PROJECT_DIR)/../target/aarch64-apple-ios-sim/debug"; "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=x86_64]" = "$(PROJECT_DIR)/../target/x86_64-apple-ios/debug"; + PRODUCT_BUNDLE_IDENTIFIER = "$(APPLICATION_IDENTIFIER).MullvadVPNUITests"; PRODUCT_NAME = MullvadVPNUITests; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "MullvadVPN app integration tests"; + SECURITY_GROUP_IDENTIFIER = ""; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG MULLVAD_ENVIRONMENT_STAGING"; SWIFT_OBJC_BRIDGING_HEADER = "$(PROJECT_DIR)/MullvadVPNUITests/BridgingHeader.h"; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = MullvadVPN; }; name = Staging; }; A93A1D072B59145C00F7796C /* MockRelease */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 852969382B4ED818007EAD4C /* UITests.xcconfig */; buildSettings = { + APPLICATION_IDENTIFIER = net.mullvad.MullvadVPN; + GCC_PREPROCESSOR_DEFINITIONS = ""; + GENERATE_INFOPLIST_FILE = YES; HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/../mullvad-api/include"; + INFOPLIST_FILE = MullvadVPNUITests/Info.plist; "LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64]" = "$(PROJECT_DIR)/../target/aarch64-apple-ios/release"; "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=arm64]" = "$(PROJECT_DIR)/../target/aarch64-apple-ios-sim/release"; "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=x86_64]" = "$(PROJECT_DIR)/../target/x86_64-apple-ios/release"; + PRODUCT_BUNDLE_IDENTIFIER = "$(APPLICATION_IDENTIFIER).MullvadVPNUITests"; PRODUCT_NAME = MullvadVPNUITests; + SECURITY_GROUP_IDENTIFIER = ""; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = MULLVAD_ENVIRONMENT_PRODUCTION; SWIFT_OBJC_BRIDGING_HEADER = "$(PROJECT_DIR)/MullvadVPNUITests/BridgingHeader.h"; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = MullvadVPN; }; name = MockRelease; }; diff --git a/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 000000000000..02691892fed1 --- /dev/null +++ b/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,22 @@ +{ + "pins" : [ + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "173f567a2dfec11d74588eea82cecea555bdc0bc", + "version" : "1.4.0" + } + }, + { + "identity" : "wireguard-apple", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mullvad/wireguard-apple.git", + "state" : { + "revision" : "11a00c20dc03f2751db47e94f585c0778c7bde82" + } + } + ], + "version" : 2 +} diff --git a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift index 855ea3c7483f..b37962ad504a 100644 --- a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift +++ b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift @@ -17,6 +17,7 @@ public enum AccessibilityIdentifier: String { case cancelButton case connectionPanelButton case collapseButton + case createAccountButton case deleteButton case disconnectButton case infoButton @@ -47,19 +48,24 @@ public enum AccessibilityIdentifier: String { // Labels case headerDeviceNameLabel case connectionStatusLabel + case welcomeAccountNumberLabel // Views case accountView case alertContainerView case alertTitle + case changeLogAlert case headerBarView case loginView + case outOfTimeView case termsOfServiceView case selectLocationView case selectLocationTableView case settingsTableView case tunnelControlView case problemReportView + case welcomeView + case deleteAccountView // Other UI elements case connectionPanelInAddressRow @@ -70,6 +76,7 @@ public enum AccessibilityIdentifier: String { case selectLocationSearchTextField case problemReportEmailTextField case problemReportMessageTextView + case deleteAccountTextField // DNS settings case dnsSettings diff --git a/ios/MullvadVPN/Coordinators/ChangeLogCoordinator.swift b/ios/MullvadVPN/Coordinators/ChangeLogCoordinator.swift index c3e53cc9f44b..031ba6a97204 100644 --- a/ios/MullvadVPN/Coordinators/ChangeLogCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/ChangeLogCoordinator.swift @@ -26,6 +26,7 @@ final class ChangeLogCoordinator: Coordinator, Presentable { func start() { let presentation = AlertPresentation( id: "change-log-ok-alert", + accessibilityIdentifier: .changeLogAlert, header: interactor.viewModel.header, title: interactor.viewModel.title, attributedMessage: interactor.viewModel.body, diff --git a/ios/MullvadVPN/View controllers/AccountDeletion/AccountDeletionContentView.swift b/ios/MullvadVPN/View controllers/AccountDeletion/AccountDeletionContentView.swift index 1929b47dd947..f5f0bb2aa802 100644 --- a/ios/MullvadVPN/View controllers/AccountDeletion/AccountDeletionContentView.swift +++ b/ios/MullvadVPN/View controllers/AccountDeletion/AccountDeletionContentView.swift @@ -81,6 +81,7 @@ class AccountDeletionContentView: UIView { private lazy var accountTextField: AccountTextField = { let groupingStyle = AccountTextField.GroupingStyle.lastPart let textField = AccountTextField(groupingStyle: groupingStyle) + textField.accessibilityIdentifier = .deleteAccountTextField textField.font = .preferredFont(forTextStyle: .body, weight: .bold) textField.placeholder = Array(repeating: "X", count: 4).joined() textField.placeholderTextColor = .lightGray @@ -346,6 +347,7 @@ class AccountDeletionContentView: UIView { } private func setupAppearance() { + accessibilityIdentifier = .deleteAccountView translatesAutoresizingMaskIntoConstraints = false backgroundColor = .secondaryColor directionalLayoutMargins = UIMetrics.contentLayoutMargins diff --git a/ios/MullvadVPN/View controllers/Alert/AlertPresentation.swift b/ios/MullvadVPN/View controllers/Alert/AlertPresentation.swift index b0056b7ea9fc..afd67b6e8113 100644 --- a/ios/MullvadVPN/View controllers/Alert/AlertPresentation.swift +++ b/ios/MullvadVPN/View controllers/Alert/AlertPresentation.swift @@ -24,6 +24,7 @@ struct AlertAction { struct AlertPresentation: Identifiable, CustomDebugStringConvertible { let id: String + var accessibilityIdentifier: AccessibilityIdentifier? var header: String? var icon: AlertIcon? var title: String? diff --git a/ios/MullvadVPN/View controllers/Alert/AlertViewController.swift b/ios/MullvadVPN/View controllers/Alert/AlertViewController.swift index 2f1796b0639c..542714a15488 100644 --- a/ios/MullvadVPN/View controllers/Alert/AlertViewController.swift +++ b/ios/MullvadVPN/View controllers/Alert/AlertViewController.swift @@ -56,8 +56,6 @@ class AlertViewController: UIViewController { view.backgroundColor = .secondaryColor view.layer.cornerRadius = 11 - view.accessibilityIdentifier = .alertContainerView - return view }() @@ -112,6 +110,9 @@ class AlertViewController: UIViewController { view.backgroundColor = .black.withAlphaComponent(0.5) + let accessibilityIdentifier = presentation.accessibilityIdentifier ?? .alertContainerView + view.accessibilityIdentifier = accessibilityIdentifier + setContent() setConstraints() } diff --git a/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeContentView.swift b/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeContentView.swift index 466a6b24ad37..4e055ad2f3c6 100644 --- a/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeContentView.swift +++ b/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeContentView.swift @@ -54,6 +54,7 @@ final class WelcomeContentView: UIView { private let accountNumberLabel: UILabel = { let label = UILabel() + label.accessibilityIdentifier = .welcomeAccountNumberLabel label.adjustsFontForContentSizeCategory = true label.lineBreakMode = .byWordWrapping label.numberOfLines = .zero @@ -192,6 +193,7 @@ final class WelcomeContentView: UIView { override init(frame: CGRect) { super.init(frame: frame) + accessibilityIdentifier = .welcomeView backgroundColor = .primaryColor directionalLayoutMargins = UIMetrics.contentLayoutMargins backgroundColor = .secondaryColor diff --git a/ios/MullvadVPN/View controllers/Login/LoginContentView.swift b/ios/MullvadVPN/View controllers/Login/LoginContentView.swift index c994286d51c1..5e1bfec865a6 100644 --- a/ios/MullvadVPN/View controllers/Login/LoginContentView.swift +++ b/ios/MullvadVPN/View controllers/Login/LoginContentView.swift @@ -84,6 +84,7 @@ class LoginContentView: UIView { let createAccountButton: AppButton = { let button = AppButton(style: .default) + button.accessibilityIdentifier = .createAccountButton button.translatesAutoresizingMaskIntoConstraints = false button.setTitle(NSLocalizedString( "CREATE_ACCOUNT_BUTTON_LABEL", diff --git a/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeContentView.swift b/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeContentView.swift index edd3f501058b..843fd0cda4d8 100644 --- a/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeContentView.swift +++ b/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeContentView.swift @@ -95,6 +95,7 @@ class OutOfTimeContentView: UIView { override init(frame: CGRect) { super.init(frame: frame) + accessibilityIdentifier = .outOfTimeView translatesAutoresizingMaskIntoConstraints = false backgroundColor = .secondaryColor directionalLayoutMargins = UIMetrics.contentLayoutMargins diff --git a/ios/MullvadVPNUITests/AccountTests.swift b/ios/MullvadVPNUITests/AccountTests.swift index 319ce2350973..e12152291b01 100644 --- a/ios/MullvadVPNUITests/AccountTests.swift +++ b/ios/MullvadVPNUITests/AccountTests.swift @@ -15,6 +15,46 @@ class AccountTests: LoggedOutUITestCase { try super.setUpWithError() } + override func tearDownWithError() throws {} + + func testCreateAccount() throws { + LoginPage(app) + .tapCreateAccountButton() + + // Verify welcome page is shown and get account number from it + let accountNumber = WelcomePage(app).getAccountNumber() + + try MullvadAPIWrapper().deleteAccount(accountNumber) + } + + func testDeleteAccount() throws { + let accountNumber = try MullvadAPIWrapper().createAccount() + + LoginPage(app) + .tapAccountNumberTextField() + .enterText(accountNumber) + .tapAccountNumberSubmitButton() + + OutOfTimePage(app) + + HeaderBar(app) + .tapAccountButton() + + AccountPage(app) + .tapDeleteAccountButton() + + AccountDeletionPage(app) + .enterText(String(accountNumber.suffix(4))) + .tapDeleteAccountButton() + + // Attempt to login with deleted account and verify that it fails + LoginPage(app) + .tapAccountNumberTextField() + .enterText(accountNumber) + .tapAccountNumberSubmitButton() + .verifyFailIconShown() + } + func testLogin() throws { LoginPage(app) .tapAccountNumberTextField() diff --git a/ios/MullvadVPNUITests/MullvadApi.swift b/ios/MullvadVPNUITests/MullvadApi.swift index 6fc8446d3eab..13560ffbd95c 100644 --- a/ios/MullvadVPNUITests/MullvadApi.swift +++ b/ios/MullvadVPNUITests/MullvadApi.swift @@ -35,6 +35,10 @@ struct InitMutableBufferError: Error { class MullvadApi { private var clientContext = MullvadApiClient() + /// Initialize the Mullvad API client + /// - Parameters: + /// - apiAddress: Address of the Mullvad API server in the format \:\ + /// - hostname: Hostname of the Mullvad API server init(apiAddress: String, hostname: String) throws { let result = mullvad_api_client_initialize( &clientContext, diff --git a/ios/MullvadVPNUITests/Networking/MullvadAPIWrapper.swift b/ios/MullvadVPNUITests/Networking/MullvadAPIWrapper.swift index 71343dd4f39a..6baacb997dca 100644 --- a/ios/MullvadVPNUITests/Networking/MullvadAPIWrapper.swift +++ b/ios/MullvadVPNUITests/Networking/MullvadAPIWrapper.swift @@ -1,5 +1,5 @@ // -// AppAPI.swift +// MullvadAPIWrapper.swift // MullvadVPNUITests // // Created by Niklas Berglund on 2024-01-18. @@ -11,6 +11,7 @@ import XCTest enum MullvadAPIError: Error { case incorrectConfigurationFormat + case requestError } class MullvadAPIWrapper { @@ -18,11 +19,19 @@ class MullvadAPIWrapper { static let hostName = Bundle(for: MullvadAPIWrapper.self) .infoDictionary?["ApiHostName"] as! String + private var mullvadAPI: MullvadApi + /// API endpoint configuration value in the format : static let endpoint = Bundle(for: MullvadAPIWrapper.self) .infoDictionary?["ApiEndpoint"] as! String // swiftlint:enable force_cast + init() throws { + let apiAddress = try Self.getAPIIPAddress() + ":" + Self.getAPIPort() + let hostname = Self.getAPIHostname() + mullvadAPI = try MullvadApi(apiAddress: apiAddress, hostname: hostname) + } + public static func getAPIHostname() -> String { return hostName } @@ -42,4 +51,52 @@ class MullvadAPIWrapper { return port } + + /// Generate a mock WireGuard key + private func generateMockWireGuardKey() -> Data { + var bytes = [UInt8]() + + for _ in 0 ..< 44 { + bytes.append(UInt8.random(in: 0 ..< 255)) + } + + return Data(bytes) + } + + func createAccount() -> String { + do { + let accountNumber = try mullvadAPI.createAccount() + return accountNumber + } catch { + XCTFail("Failed to create account using app API") + return String() + } + } + + func deleteAccount(_ accountNumber: String) { + do { + try mullvadAPI.delete(account: accountNumber) + } catch { + XCTFail("Failed to delete account using app API") + } + } + + /// Add another device to specified account. A dummy WireGuard key will be generated. + func addDevice(_ account: String) throws { + let devicePublicKey = generateMockWireGuardKey() + + do { + try mullvadAPI.addDevice(forAccount: account, publicKey: devicePublicKey) + } catch { + throw MullvadAPIError.requestError + } + } + + func getAccountExpiry(_ account: String) throws -> UInt64 { + do { + return try mullvadAPI.getExpiry(forAccount: account) + } catch { + throw MullvadAPIError.requestError + } + } } diff --git a/ios/MullvadVPNUITests/Pages/AccountDeletionPage.swift b/ios/MullvadVPNUITests/Pages/AccountDeletionPage.swift new file mode 100644 index 000000000000..30bf01034ebd --- /dev/null +++ b/ios/MullvadVPNUITests/Pages/AccountDeletionPage.swift @@ -0,0 +1,39 @@ +// +// AccountDeletionPage.swift +// MullvadVPNUITests +// +// Created by Niklas Berglund on 2024-01-30. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import XCTest + +class AccountDeletionPage: Page { + @discardableResult override init(_ app: XCUIApplication) { + super.init(app) + + self.pageAccessibilityIdentifier = .deleteAccountView + waitForPageToBeShown() + } + + @discardableResult func tapTextField() -> Self { + app.textFields[AccessibilityIdentifier.deleteAccountTextField].tap() + return self + } + + @discardableResult func tapDeleteAccountButton() -> Self { + guard let pageAccessibilityIdentifier = self.pageAccessibilityIdentifier else { + XCTFail("Page's accessibility identifier not set") + return self + } + + app.otherElements[pageAccessibilityIdentifier].buttons[AccessibilityIdentifier.deleteButton].tap() + return self + } + + @discardableResult func tapCancelButton() -> Self { + app.buttons[AccessibilityIdentifier.cancelButton].tap() + return self + } +} diff --git a/ios/MullvadVPNUITests/Pages/ChangeLogAlert.swift b/ios/MullvadVPNUITests/Pages/ChangeLogAlert.swift new file mode 100644 index 000000000000..70d67d0294fc --- /dev/null +++ b/ios/MullvadVPNUITests/Pages/ChangeLogAlert.swift @@ -0,0 +1,24 @@ +// +// ChangeLogAlert.swift +// MullvadVPNUITests +// +// Created by Niklas Berglund on 2024-02-20. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import XCTest + +class ChangeLogAlert: Page { + @discardableResult override init(_ app: XCUIApplication) { + super.init(app) + + self.pageAccessibilityIdentifier = .changeLogAlert + waitForPageToBeShown() + } + + @discardableResult func tapOkay() -> Self { + app.buttons[AccessibilityIdentifier.alertOkButton].tap() + return self + } +} diff --git a/ios/MullvadVPNUITests/Pages/LoginPage.swift b/ios/MullvadVPNUITests/Pages/LoginPage.swift index 399943fe527c..96c6ebea8936 100644 --- a/ios/MullvadVPNUITests/Pages/LoginPage.swift +++ b/ios/MullvadVPNUITests/Pages/LoginPage.swift @@ -27,6 +27,11 @@ class LoginPage: Page { return self } + @discardableResult public func tapCreateAccountButton() -> Self { + app.buttons[AccessibilityIdentifier.createAccountButton].tap() + return self + } + @discardableResult public func verifyDeviceLabelShown() -> Self { XCTAssertTrue( app.staticTexts[AccessibilityIdentifier.headerDeviceNameLabel] diff --git a/ios/MullvadVPNUITests/Pages/OutOfTimePage.swift b/ios/MullvadVPNUITests/Pages/OutOfTimePage.swift new file mode 100644 index 000000000000..0950aa9147ce --- /dev/null +++ b/ios/MullvadVPNUITests/Pages/OutOfTimePage.swift @@ -0,0 +1,19 @@ +// +// OutOfTimePage.swift +// MullvadVPNUITests +// +// Created by Niklas Berglund on 2024-02-20. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import XCTest + +class OutOfTimePage: Page { + @discardableResult override init(_ app: XCUIApplication) { + super.init(app) + + self.pageAccessibilityIdentifier = .outOfTimeView + waitForPageToBeShown() + } +} diff --git a/ios/MullvadVPNUITests/Pages/SelectLocationPage.swift b/ios/MullvadVPNUITests/Pages/SelectLocationPage.swift index f841101ff630..c5c54325adf2 100644 --- a/ios/MullvadVPNUITests/Pages/SelectLocationPage.swift +++ b/ios/MullvadVPNUITests/Pages/SelectLocationPage.swift @@ -14,6 +14,7 @@ class SelectLocationPage: Page { super.init(app) self.pageAccessibilityIdentifier = .selectLocationView + waitForPageToBeShown() } @discardableResult func tapLocationCell(withName name: String) -> Self { diff --git a/ios/MullvadVPNUITests/Pages/SettingsPage.swift b/ios/MullvadVPNUITests/Pages/SettingsPage.swift index 72acac4019a6..63b3227d2965 100644 --- a/ios/MullvadVPNUITests/Pages/SettingsPage.swift +++ b/ios/MullvadVPNUITests/Pages/SettingsPage.swift @@ -14,6 +14,7 @@ class SettingsPage: Page { super.init(app) self.pageAccessibilityIdentifier = .settingsTableView + waitForPageToBeShown() } @discardableResult func tapVPNSettingsCell() -> Self { diff --git a/ios/MullvadVPNUITests/Pages/TermsOfServicePage.swift b/ios/MullvadVPNUITests/Pages/TermsOfServicePage.swift index b1a569290db6..0f32bf7be6be 100644 --- a/ios/MullvadVPNUITests/Pages/TermsOfServicePage.swift +++ b/ios/MullvadVPNUITests/Pages/TermsOfServicePage.swift @@ -14,6 +14,7 @@ class TermsOfServicePage: Page { super.init(app) self.pageAccessibilityIdentifier = .termsOfServiceView + waitForPageToBeShown() } @discardableResult func tapAgreeButton() -> Self { diff --git a/ios/MullvadVPNUITests/Pages/TunnelControlPage.swift b/ios/MullvadVPNUITests/Pages/TunnelControlPage.swift index 860b9280240c..37813b36c47d 100644 --- a/ios/MullvadVPNUITests/Pages/TunnelControlPage.swift +++ b/ios/MullvadVPNUITests/Pages/TunnelControlPage.swift @@ -14,6 +14,7 @@ class TunnelControlPage: Page { super.init(app) self.pageAccessibilityIdentifier = .tunnelControlView + waitForPageToBeShown() } @discardableResult func tapSelectLocationButton() -> Self { diff --git a/ios/MullvadVPNUITests/Pages/WelcomePage.swift b/ios/MullvadVPNUITests/Pages/WelcomePage.swift new file mode 100644 index 000000000000..d6e8048413b1 --- /dev/null +++ b/ios/MullvadVPNUITests/Pages/WelcomePage.swift @@ -0,0 +1,34 @@ +// +// WelcomePage.swift +// MullvadVPNUITests +// +// Created by Niklas Berglund on 2024-01-30. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import XCTest + +class WelcomePage: Page { + @discardableResult override init(_ app: XCUIApplication) { + super.init(app) + + self.pageAccessibilityIdentifier = .welcomeView + waitForPageToBeShown() + } + + @discardableResult func tapAddTimeButton() -> Self { + app.buttons[AccessibilityIdentifier.purchaseButton].tap() + return self + } + + @discardableResult func tapRedeemButton() -> Self { + app.buttons[AccessibilityIdentifier.redeemVoucherButton].tap() + return self + } + + func getAccountNumber() -> String { + let labelValue = app.staticTexts[AccessibilityIdentifier.welcomeAccountNumberLabel].label + return labelValue.replacingOccurrences(of: " ", with: "") + } +} diff --git a/ios/MullvadVPNUITests/Test base classes/BaseUITestCase.swift b/ios/MullvadVPNUITests/Test base classes/BaseUITestCase.swift index 00a73cd36c1c..2929cd660d46 100644 --- a/ios/MullvadVPNUITests/Test base classes/BaseUITestCase.swift +++ b/ios/MullvadVPNUITests/Test base classes/BaseUITestCase.swift @@ -75,11 +75,17 @@ class BaseUITestCase: XCTestCase { if termsOfServiceIsShown { TermsOfServicePage(app) .tapAgreeButton() + } + } - Alert(app) // Changes alert - .tapOkay() + func discardChangeLogIfShown() { + let changeLogIsShown = app + .otherElements[AccessibilityIdentifier.changeLogAlert.rawValue] + .waitForExistence(timeout: 1.0) - LoginPage(app) + if changeLogIsShown { + ChangeLogAlert(app) + .tapOkay() } } diff --git a/ios/MullvadVPNUITests/Test base classes/LoggedInWithTimeUITestCase.swift b/ios/MullvadVPNUITests/Test base classes/LoggedInWithTimeUITestCase.swift index fc1fa747f8eb..58b9a13dcd9c 100644 --- a/ios/MullvadVPNUITests/Test base classes/LoggedInWithTimeUITestCase.swift +++ b/ios/MullvadVPNUITests/Test base classes/LoggedInWithTimeUITestCase.swift @@ -14,6 +14,7 @@ class LoggedInWithTimeUITestCase: BaseUITestCase { super.setUp() agreeToTermsOfServiceIfShown() + discardChangeLogIfShown() logoutIfLoggedIn() login(accountNumber: hasTimeAccountNumber) diff --git a/ios/MullvadVPNUITests/Test base classes/LoggedInWithoutTimeUITestCase.swift b/ios/MullvadVPNUITests/Test base classes/LoggedInWithoutTimeUITestCase.swift index c7ab76e31bca..a72d280ede12 100644 --- a/ios/MullvadVPNUITests/Test base classes/LoggedInWithoutTimeUITestCase.swift +++ b/ios/MullvadVPNUITests/Test base classes/LoggedInWithoutTimeUITestCase.swift @@ -14,6 +14,7 @@ class LoggedInWithoutTimeUITestCase: BaseUITestCase { super.setUp() agreeToTermsOfServiceIfShown() + discardChangeLogIfShown() logoutIfLoggedIn() login(accountNumber: noTimeAccountNumber) diff --git a/ios/MullvadVPNUITests/Test base classes/LoggedOutUITestCase.swift b/ios/MullvadVPNUITests/Test base classes/LoggedOutUITestCase.swift index a89c6e732ef2..dd0a96098f3c 100644 --- a/ios/MullvadVPNUITests/Test base classes/LoggedOutUITestCase.swift +++ b/ios/MullvadVPNUITests/Test base classes/LoggedOutUITestCase.swift @@ -14,6 +14,7 @@ class LoggedOutUITestCase: BaseUITestCase { super.setUp() agreeToTermsOfServiceIfShown() + discardChangeLogIfShown() logoutIfLoggedIn() // Relaunch app so that tests start from a deterministic state diff --git a/ios/TestPlans/MullvadVPNUITestsAll.xctestplan b/ios/TestPlans/MullvadVPNUITestsAll.xctestplan index 29951c80fe1b..14cb0990dc0c 100644 --- a/ios/TestPlans/MullvadVPNUITestsAll.xctestplan +++ b/ios/TestPlans/MullvadVPNUITestsAll.xctestplan @@ -17,9 +17,11 @@ }, "testTargets" : [ { - "selectedTests" : [ - "AccountTests\/testLogin()", - "AccountTests\/testLoginWithIncorrectAccountNumber()" + "skippedTests" : [ + "BaseUITestCase", + "LoggedInWithTimeUITestCase", + "LoggedInWithoutTimeUITestCase", + "LoggedOutUITestCase" ], "target" : { "containerPath" : "container:MullvadVPN.xcodeproj", diff --git a/mullvad-api/src/ios_ffi.rs b/mullvad-api/src/ios_ffi.rs deleted file mode 100644 index 7184723036c3..000000000000 --- a/mullvad-api/src/ios_ffi.rs +++ /dev/null @@ -1,374 +0,0 @@ -use std::{ffi::CString, net::SocketAddr, ptr, sync::Arc}; - -use crate::{ - rest::{self, MullvadRestHandle}, - AccountsProxy, DevicesProxy, -}; - -#[derive(Debug, PartialEq)] -#[repr(C)] -pub enum MullvadApiErrorKind { - NoError = 0, - StringParsing = -1, - SocketAddressParsing = -2, - AsyncRuntimeInitialization = -3, - BadResponse = -4, - BufferTooSmall = -5, -} - -/// MullvadApiErrorKind contains a description and an error kind. If the error kind is -/// `MullvadApiErrorKind` is NoError, the pointer will be nil. -#[repr(C)] -pub struct MullvadApiError { - description: *mut i8, - kind: MullvadApiErrorKind, -} - -impl MullvadApiError { - fn new(kind: MullvadApiErrorKind, error: &dyn std::error::Error) -> Self { - let description = CString::new(format!("{error:?}")).unwrap_or_default(); - Self { - description: description.into_raw(), - kind, - } - } - - fn api_err(error: &rest::Error) -> Self { - Self::new(MullvadApiErrorKind::BadResponse, error) - } - - fn with_str(kind: MullvadApiErrorKind, description: &str) -> Self { - let description = CString::new(description).unwrap_or_default(); - Self { - description: description.into_raw(), - kind, - } - } - - fn ok() -> MullvadApiError { - Self { - description: CString::new("").unwrap().into_raw(), - kind: MullvadApiErrorKind::NoError, - } - } - - fn drop(self) { - let _ = unsafe { CString::from_raw(self.description) }; - } -} - -/// IosMullvadApiClient is an FFI interface to our `mullvad-api`. It is a thread-safe to accessing -/// our API. -#[derive(Clone)] -#[repr(C)] -pub struct IosMullvadApiClient { - ptr: *const IosApiClientContext, -} - -impl IosMullvadApiClient { - fn new(context: IosApiClientContext) -> Self { - let sync_context = Arc::new(context); - let ptr = Arc::into_raw(sync_context); - Self { ptr } - } - - unsafe fn from_raw(self) -> Arc { - unsafe { - Arc::increment_strong_count(self.ptr); - } - - Arc::from_raw(self.ptr) - } -} - -struct IosApiClientContext { - tokio_runtime: tokio::runtime::Runtime, - api_runtime: crate::Runtime, - api_hostname: String, -} - -impl IosApiClientContext { - fn rest_handle(self: Arc) -> MullvadRestHandle { - self.tokio_runtime.block_on( - self.api_runtime - .static_mullvad_rest_handle(self.api_hostname.clone()), - ) - } - - fn devices_proxy(self: Arc) -> DevicesProxy { - crate::DevicesProxy::new(self.rest_handle()) - } - - fn accounts_proxy(self: Arc) -> AccountsProxy { - crate::AccountsProxy::new(self.rest_handle()) - } - - fn tokio_handle(self: &Arc) -> tokio::runtime::Handle { - self.tokio_runtime.handle().clone() - } -} - -/// Paramters: -/// `api_address`: pointer to UTF-8 string containing a socket address representation -/// ("143.32.4.32:9090"), the port is mandatory. -/// -/// `api_address_len`: size of the API address string -#[no_mangle] -pub extern "C" fn mullvad_api_initialize_api_runtime( - context_ptr: *mut IosMullvadApiClient, - api_address_ptr: *const u8, - api_address_len: usize, - hostname: *const u8, - hostname_len: usize, -) -> MullvadApiError { - let Some(addr_str) = (unsafe { string_from_raw_ptr(api_address_ptr, api_address_len) }) else { - return MullvadApiError::with_str( - MullvadApiErrorKind::StringParsing, - "Failed to parse API socket address string", - ); - }; - let Some(api_hostname) = (unsafe { string_from_raw_ptr(hostname, hostname_len) }) else { - return MullvadApiError::with_str( - MullvadApiErrorKind::StringParsing, - "Failed to parse API host name", - ); - }; - - let Ok(api_address): Result = addr_str.parse() else { - return MullvadApiError::with_str( - MullvadApiErrorKind::SocketAddressParsing, - "Failed to parse API socket address", - ); - }; - - let mut runtime_builder = tokio::runtime::Builder::new_multi_thread(); - - runtime_builder.worker_threads(2).enable_all(); - let tokio_runtime = match runtime_builder.build() { - Ok(runtime) => runtime, - Err(err) => { - return MullvadApiError::new(MullvadApiErrorKind::AsyncRuntimeInitialization, &err); - } - }; - - // It is imperative that the REST runtime is created within an async context, otherwise - // ApiAvailability panics. - let api_runtime = tokio_runtime.block_on(async { - crate::Runtime::with_static_addr(tokio_runtime.handle().clone(), api_address) - }); - - let ios_context = IosApiClientContext { - tokio_runtime, - api_runtime, - api_hostname, - }; - - let context = IosMullvadApiClient::new(ios_context); - - unsafe { - std::ptr::write(context_ptr, context); - } - - MullvadApiError::ok() -} - -#[no_mangle] -pub extern "C" fn mullvad_api_remove_all_devices( - context: IosMullvadApiClient, - account_str_ptr: *const u8, - account_str_len: usize, -) -> MullvadApiError { - let ctx = unsafe { context.from_raw() }; - let Some(account) = (unsafe { string_from_raw_ptr(account_str_ptr, account_str_len) }) else { - return MullvadApiError::with_str( - MullvadApiErrorKind::StringParsing, - "Failed to parse account number", - ); - }; - - let runtime = ctx.tokio_handle(); - let device_proxy = ctx.devices_proxy(); - let result = runtime.block_on(async move { - let devices = device_proxy.list(account.clone()).await?; - for device in devices { - device_proxy.remove(account.clone(), device.id).await?; - } - Result::<_, rest::Error>::Ok(()) - }); - - match result { - Ok(()) => MullvadApiError::ok(), - Err(err) => MullvadApiError::api_err(&err), - } -} - -#[no_mangle] -pub extern "C" fn mullvad_api_get_expiry( - context: IosMullvadApiClient, - account_str_ptr: *const u8, - account_str_len: usize, - expiry_unix_timestamp: *mut i64, -) -> MullvadApiError { - let Some(account) = (unsafe { string_from_raw_ptr(account_str_ptr, account_str_len) }) else { - return MullvadApiError::with_str( - MullvadApiErrorKind::StringParsing, - "Failed to parse account number", - ); - }; - - let ctx = unsafe { context.from_raw() }; - let runtime = ctx.tokio_handle(); - - let account_proxy = ctx.accounts_proxy(); - let result: Result<_, rest::Error> = runtime.block_on(async move { - let expiry = account_proxy.get_data(account).await?.expiry; - let expiry_timestamp = expiry.timestamp(); - - Ok(expiry_timestamp) - }); - - match result { - Ok(expiry) => { - // SAFETY: It is assumed that expiry_timestamp is a valid pointer to a `libc::timespec` - unsafe { - std::ptr::write(expiry_unix_timestamp, expiry); - } - MullvadApiError::ok() - } - Err(err) => MullvadApiError::api_err(&err), - } -} - -/// Args: -/// context: `IosApiContext` -/// public_key: a pointer to a valid 32 byte array representing a WireGuard public key -#[no_mangle] -pub extern "C" fn mullvad_api_add_device( - context: IosMullvadApiClient, - account_str_ptr: *const u8, - account_str_len: usize, - public_key_ptr: *const u8, -) -> MullvadApiError { - let Some(account) = (unsafe { string_from_raw_ptr(account_str_ptr, account_str_len) }) else { - return MullvadApiError::with_str( - MullvadApiErrorKind::StringParsing, - "Failed to parse account number", - ); - }; - - let public_key_bytes: [u8; 32] = unsafe { std::ptr::read(public_key_ptr as *const _) }; - let public_key = public_key_bytes.into(); - - let ctx = unsafe { context.from_raw() }; - let runtime = ctx.tokio_handle(); - - let devices_proxy = ctx.devices_proxy(); - - let result: Result<_, rest::Error> = runtime.block_on(async move { - let (new_device, _) = devices_proxy.create(account, public_key).await?; - Ok(new_device) - }); - - match result { - Ok(_result) => MullvadApiError::ok(), - Err(err) => MullvadApiError::api_err(&err), - } -} - -/// Args: -/// context: `IosApiContext` -/// account_str_ptr: A pointer to a byte buffer large enough to contain a valid account number -/// string. -/// account_str_len: A pointer to an unsigned pointer-sized integer specifying the length of the -/// input buffer. If the buffer is big enough and a new account is created, it will contain the -/// amount of bytes that were written to the buffer. -#[no_mangle] -pub extern "C" fn mullvad_api_create_account( - context: IosMullvadApiClient, - account_str_ptr: *mut u8, - account_str_len: *mut usize, -) -> MullvadApiError { - let ctx = unsafe { context.from_raw() }; - let runtime = ctx.tokio_handle(); - let buffer_len = unsafe { ptr::read(account_str_len) }; - - let accounts_proxy = ctx.accounts_proxy(); - - let result: Result<_, rest::Error> = runtime.block_on(async move { - let new_account = accounts_proxy.create_account().await?; - Ok(new_account) - }); - - match result { - Ok(new_account) => { - let new_account_bytes = new_account.into_bytes(); - if new_account_bytes.len() > buffer_len { - return MullvadApiError::with_str( - MullvadApiErrorKind::BufferTooSmall, - "Buffer for account number is too small", - ); - } - unsafe { - ptr::copy( - new_account_bytes.as_ptr(), - account_str_ptr, - new_account_bytes.len(), - ); - } - - MullvadApiError::ok() - } - Err(err) => MullvadApiError::api_err(&err), - } -} - -/// Args: -/// context: `IosApiContext` -/// public_key: a pointer to a valid 32 byte array representing a WireGuard public key -#[no_mangle] -pub extern "C" fn mullvad_api_delete_account( - context: IosMullvadApiClient, - account_str_ptr: *const u8, - account_str_len: usize, -) -> MullvadApiError { - let ctx = unsafe { context.from_raw() }; - let runtime = ctx.tokio_handle(); - - let Some(account) = (unsafe { string_from_raw_ptr(account_str_ptr, account_str_len) }) else { - return MullvadApiError::with_str( - MullvadApiErrorKind::StringParsing, - "Failed to parse account number", - ); - }; - - let accounts_proxy = ctx.accounts_proxy(); - - let result: Result<_, rest::Error> = runtime.block_on(async move { - let new_account = accounts_proxy.delete_account(account).await?; - Ok(new_account) - }); - - match result { - Ok(()) => MullvadApiError::ok(), - Err(err) => MullvadApiError::api_err(&err), - } -} - -#[no_mangle] -pub extern "C" fn mullvad_api_runtime_drop(context: IosMullvadApiClient) { - unsafe { Arc::decrement_strong_count(context.ptr) } -} - -/// The return value is only valid for the lifetime of the `ptr` that's passed in -/// -/// SAFETY: `ptr` must be valid for `size` bytes -unsafe fn string_from_raw_ptr(ptr: *const u8, size: usize) -> Option { - let slice = unsafe { std::slice::from_raw_parts(ptr, size) }; - - String::from_utf8(slice.to_vec()).ok() -} - -#[no_mangle] -pub extern "C" fn mullvad_api_error_drop(error: MullvadApiError) { - error.drop() -} diff --git a/mullvad-api/src/lib.rs b/mullvad-api/src/lib.rs index eabda74da781..e78535dfc0e9 100644 --- a/mullvad-api/src/lib.rs +++ b/mullvad-api/src/lib.rs @@ -454,27 +454,6 @@ impl Runtime { rest::MullvadRestHandle::new(service, factory, self.availability_handle()) } - /// This is only to be used in test code - pub async fn static_mullvad_rest_handle(&self, hostname: String) -> rest::MullvadRestHandle { - let service = self - .new_request_service( - Some(hostname.clone()), - futures::stream::repeat(ApiConnectionMode::Direct), - #[cfg(target_os = "android")] - self.socket_bypass_tx.clone(), - ) - .await; - let token_store = access::AccessTokenStore::new(service.clone()); - let factory = rest::RequestFactory::new(hostname, Some(token_store)); - - rest::MullvadRestHandle::new( - service, - factory, - self.address_cache.clone(), - self.availability_handle(), - ) - } - /// Returns a new request service handle pub fn rest_handle(&self) -> rest::RequestServiceHandle { self.new_request_service(