From a2bab9ba4349d03cf3c3b771a865ef71ca857ad0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Em=C4=ABls?= Date: Tue, 30 Jan 2024 16:10:01 +0100 Subject: [PATCH 1/4] Add FFI for mullvad-api --- mullvad-api/src/ios_ffi.rs | 317 +++++++++++++++++++++++++++++++++++++ mullvad-api/src/lib.rs | 21 +++ 2 files changed, 338 insertions(+) create mode 100644 mullvad-api/src/ios_ffi.rs diff --git a/mullvad-api/src/ios_ffi.rs b/mullvad-api/src/ios_ffi.rs new file mode 100644 index 000000000000..ed5e0794a80b --- /dev/null +++ b/mullvad-api/src/ios_ffi.rs @@ -0,0 +1,317 @@ +use std::{net::SocketAddr, sync::Arc}; + +use crate::{ + rest::{self, MullvadRestHandle}, + AccountsProxy, DevicesProxy, +}; + +#[derive(Debug, PartialEq)] +#[repr(i32)] +pub enum FfiError { + NoError = 0, + StringParsing = -1, + SocketAddressParsing = -2, + AsyncRuntimeInitialization = -3, + BadResponse = -4, + BufferTooSmall = -5, +} + +/// 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, +) -> FfiError { + let Some(addr_str) = (unsafe { string_from_raw_ptr(api_address_ptr, api_address_len) }) else { + return FfiError::StringParsing; + }; + let Some(api_hostname) = (unsafe { string_from_raw_ptr(hostname, hostname_len) }) else { + return FfiError::StringParsing; + }; + + let Ok(api_address): Result = addr_str.parse() else { + return FfiError::SocketAddressParsing; + }; + + let mut runtime_builder = tokio::runtime::Builder::new_multi_thread(); + + runtime_builder.worker_threads(2).enable_all(); + let Ok(tokio_runtime) = runtime_builder.build() else { + return FfiError::AsyncRuntimeInitialization; + }; + + // 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); + } + + FfiError::NoError +} + +#[no_mangle] +pub extern "C" fn mullvad_api_remove_all_devices( + context: IosMullvadApiClient, + account_str_ptr: *const u8, + account_str_len: usize, +) -> FfiError { + let ctx = unsafe { context.from_raw() }; + let Some(account) = (unsafe { string_from_raw_ptr(account_str_ptr, account_str_len) }) else { + return FfiError::StringParsing; + }; + + 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(()) => FfiError::NoError, + Err(_err) => FfiError::BadResponse, + } +} + +#[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, +) -> FfiError { + let Some(account) = (unsafe { string_from_raw_ptr(account_str_ptr, account_str_len) }) else { + return FfiError::StringParsing; + }; + + 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); + } + FfiError::NoError + } + Err(_err) => FfiError::BadResponse, + } +} + +/// 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, +) -> FfiError { + let Some(account) = (unsafe { string_from_raw_ptr(account_str_ptr, account_str_len) }) else { + return FfiError::StringParsing; + }; + 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) => FfiError::NoError, + Err(_err) => FfiError::BadResponse, + } +} + +/// 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: *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), + } +} + +#[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() +} diff --git a/mullvad-api/src/lib.rs b/mullvad-api/src/lib.rs index c4d5665a0131..d7bc6acb47ac 100644 --- a/mullvad-api/src/lib.rs +++ b/mullvad-api/src/lib.rs @@ -454,6 +454,27 @@ 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( From 0ce658e0986e77a4af01aa267733d5f53777204b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Em=C4=ABls?= Date: Wed, 31 Jan 2024 10:23:09 +0100 Subject: [PATCH 2/4] Add MullvadApi to MullvadVPNUITetsts --- ios/MullvadVPN.xcodeproj/project.pbxproj | 1 + .../xcshareddata/swiftpm/Package.resolved | 22 ------------------- 2 files changed, 1 insertion(+), 22 deletions(-) delete mode 100644 ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index ca2ba8b21114..3c291ce3b2fd 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -3102,6 +3102,7 @@ 58CE5E57224146200008646E = { isa = PBXGroup; children = ( + 01EF6F2D2B6A51B100125696 /* mullvad-api.h */, 58F3C0A824A50C0E003E76BE /* Assets */, 58ECD29023F178FD004298B6 /* Configurations */, 589A454A28DDF59B00565204 /* Shared */, diff --git a/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 02691892fed1..000000000000 --- a/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,22 +0,0 @@ -{ - "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 -} From 1c64c5ff5f300e9c02913c652e6121a47f783755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Em=C4=ABls?= Date: Wed, 31 Jan 2024 13:44:56 +0100 Subject: [PATCH 3/4] Improve error handling --- mullvad-api/src/ios_ffi.rs | 145 ++++++++++++++++++++++++++----------- 1 file changed, 101 insertions(+), 44 deletions(-) diff --git a/mullvad-api/src/ios_ffi.rs b/mullvad-api/src/ios_ffi.rs index ed5e0794a80b..7184723036c3 100644 --- a/mullvad-api/src/ios_ffi.rs +++ b/mullvad-api/src/ios_ffi.rs @@ -1,4 +1,4 @@ -use std::{net::SocketAddr, sync::Arc}; +use std::{ffi::CString, net::SocketAddr, ptr, sync::Arc}; use crate::{ rest::{self, MullvadRestHandle}, @@ -6,8 +6,8 @@ use crate::{ }; #[derive(Debug, PartialEq)] -#[repr(i32)] -pub enum FfiError { +#[repr(C)] +pub enum MullvadApiErrorKind { NoError = 0, StringParsing = -1, SocketAddressParsing = -2, @@ -16,6 +16,47 @@ pub enum FfiError { 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)] @@ -79,23 +120,35 @@ pub extern "C" fn mullvad_api_initialize_api_runtime( api_address_len: usize, hostname: *const u8, hostname_len: usize, -) -> FfiError { +) -> MullvadApiError { let Some(addr_str) = (unsafe { string_from_raw_ptr(api_address_ptr, api_address_len) }) else { - return FfiError::StringParsing; + 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 FfiError::StringParsing; + return MullvadApiError::with_str( + MullvadApiErrorKind::StringParsing, + "Failed to parse API host name", + ); }; let Ok(api_address): Result = addr_str.parse() else { - return FfiError::SocketAddressParsing; + 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 Ok(tokio_runtime) = runtime_builder.build() else { - return FfiError::AsyncRuntimeInitialization; + 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 @@ -116,7 +169,7 @@ pub extern "C" fn mullvad_api_initialize_api_runtime( std::ptr::write(context_ptr, context); } - FfiError::NoError + MullvadApiError::ok() } #[no_mangle] @@ -124,10 +177,13 @@ pub extern "C" fn mullvad_api_remove_all_devices( context: IosMullvadApiClient, account_str_ptr: *const u8, account_str_len: usize, -) -> FfiError { +) -> MullvadApiError { let ctx = unsafe { context.from_raw() }; let Some(account) = (unsafe { string_from_raw_ptr(account_str_ptr, account_str_len) }) else { - return FfiError::StringParsing; + return MullvadApiError::with_str( + MullvadApiErrorKind::StringParsing, + "Failed to parse account number", + ); }; let runtime = ctx.tokio_handle(); @@ -141,8 +197,8 @@ pub extern "C" fn mullvad_api_remove_all_devices( }); match result { - Ok(()) => FfiError::NoError, - Err(_err) => FfiError::BadResponse, + Ok(()) => MullvadApiError::ok(), + Err(err) => MullvadApiError::api_err(&err), } } @@ -152,9 +208,12 @@ pub extern "C" fn mullvad_api_get_expiry( account_str_ptr: *const u8, account_str_len: usize, expiry_unix_timestamp: *mut i64, -) -> FfiError { +) -> MullvadApiError { let Some(account) = (unsafe { string_from_raw_ptr(account_str_ptr, account_str_len) }) else { - return FfiError::StringParsing; + return MullvadApiError::with_str( + MullvadApiErrorKind::StringParsing, + "Failed to parse account number", + ); }; let ctx = unsafe { context.from_raw() }; @@ -174,9 +233,9 @@ pub extern "C" fn mullvad_api_get_expiry( unsafe { std::ptr::write(expiry_unix_timestamp, expiry); } - FfiError::NoError + MullvadApiError::ok() } - Err(_err) => FfiError::BadResponse, + Err(err) => MullvadApiError::api_err(&err), } } @@ -189,10 +248,14 @@ pub extern "C" fn mullvad_api_add_device( account_str_ptr: *const u8, account_str_len: usize, public_key_ptr: *const u8, -) -> FfiError { +) -> MullvadApiError { let Some(account) = (unsafe { string_from_raw_ptr(account_str_ptr, account_str_len) }) else { - return FfiError::StringParsing; + 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(); @@ -207,8 +270,8 @@ pub extern "C" fn mullvad_api_add_device( }); match result { - Ok(_result) => FfiError::NoError, - Err(_err) => FfiError::BadResponse, + Ok(_result) => MullvadApiError::ok(), + Err(err) => MullvadApiError::api_err(&err), } } @@ -265,39 +328,28 @@ pub extern "C" fn mullvad_api_create_account( #[no_mangle] pub extern "C" fn mullvad_api_delete_account( context: IosMullvadApiClient, - account_str_ptr: *mut u8, - account_str_len: *mut usize, + account_str_ptr: *const u8, + account_str_len: usize, ) -> MullvadApiError { let ctx = unsafe { context.from_raw() }; let runtime = ctx.tokio_handle(); - let buffer_len = unsafe { ptr::read(account_str_len) }; + + 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.create_account().await?; + let new_account = accounts_proxy.delete_account(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() - } + Ok(()) => MullvadApiError::ok(), Err(err) => MullvadApiError::api_err(&err), } } @@ -315,3 +367,8 @@ unsafe fn string_from_raw_ptr(ptr: *const u8, size: usize) -> Option { String::from_utf8(slice.to_vec()).ok() } + +#[no_mangle] +pub extern "C" fn mullvad_api_error_drop(error: MullvadApiError) { + error.drop() +} From 61ad690fcf058a82d7a016e50ffe289836ae28b2 Mon Sep 17 00:00:00 2001 From: Niklas Berglund Date: Tue, 30 Jan 2024 12:33:53 +0100 Subject: [PATCH 4/4] Add account creation and deletion tests --- ios/MullvadVPN.xcodeproj/project.pbxproj | 61 ++- .../xcshareddata/swiftpm/Package.resolved | 22 ++ .../Classes/AccessbilityIdentifier.swift | 9 + .../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 + .../RevokedDeviceViewController.swift | 2 + ios/MullvadVPNUITests/AccountTests.swift | 46 +++ ios/MullvadVPNUITests/MullvadApi.swift | 4 + .../Networking/MullvadAPIWrapper.swift | 65 ++- .../Pages/AccountDeletionPage.swift | 39 ++ .../Pages/ChangeLogAlert.swift | 24 ++ ios/MullvadVPNUITests/Pages/HeaderBar.swift | 7 +- ios/MullvadVPNUITests/Pages/LoginPage.swift | 5 + .../Pages/OutOfTimePage.swift | 19 + .../Pages/RevokedDevicePage.swift | 26 ++ .../Pages/SelectLocationPage.swift | 1 + .../Pages/SettingsPage.swift | 1 + .../Pages/TermsOfServicePage.swift | 1 + .../Pages/TunnelControlPage.swift | 1 + ios/MullvadVPNUITests/Pages/WelcomePage.swift | 34 ++ ios/MullvadVPNUITests/README.md | 4 +- .../Test base classes/BaseUITestCase.swift | 28 +- .../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 - 33 files changed, 386 insertions(+), 432 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/RevokedDevicePage.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 3c291ce3b2fd..815b5a268f59 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 01EF6F2A2B6A473900125696 /* MullvadApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01EF6F292B6A473900125696 /* MullvadApi.swift */; }; 01EF6F342B6A590700125696 /* libmullvad_api.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 01EF6F332B6A590700125696 /* libmullvad_api.a */; }; 062B45A328FD4CA700746E77 /* le_root_cert.cer in Resources */ = {isa = PBXBuildFile; fileRef = 06799AB428F98CE700ACD94E /* le_root_cert.cer */; }; 062B45BC28FD8C3B00746E77 /* RESTDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062B45BB28FD8C3B00746E77 /* RESTDefaults.swift */; }; @@ -608,6 +607,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 */; }; @@ -618,16 +618,23 @@ 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 */; }; + 8556EB522B9A1C6900D26DD4 /* MullvadApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8556EB512B9A1C6900D26DD4 /* MullvadApi.swift */; }; + 8556EB542B9A1D7100D26DD4 /* BridgingHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = 8556EB532B9A1D7100D26DD4 /* BridgingHeader.h */; }; + 8556EB562B9B0AC500D26DD4 /* RevokedDevicePage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8556EB552B9B0AC500D26DD4 /* RevokedDevicePage.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 */; }; 85C7A2E92B89024B00035D5A /* SettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C7A2E82B89024B00035D5A /* SettingsTests.swift */; }; 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 */; }; @@ -787,7 +794,6 @@ A9C342C52ACC42130045F00E /* ServerRelaysResponse+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C342C42ACC42130045F00E /* ServerRelaysResponse+Stubs.swift */; }; A9D99B9A2A1F7C3200DE27D3 /* RESTTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FAE67D28F83CA50033DD93 /* RESTTransport.swift */; }; A9DF789B2B7D1DF10094E4AD /* mullvad-api.h in Headers */ = {isa = PBXBuildFile; fileRef = 01EF6F2D2B6A51B100125696 /* mullvad-api.h */; settings = {ATTRIBUTES = (Private, ); }; }; - A9DF789C2B7D1E410094E4AD /* BridgingHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = 01EF6F352B6A5AEF00125696 /* BridgingHeader.h */; settings = {ATTRIBUTES = (Public, ); }; }; A9DF789D2B7D1E8B0094E4AD /* LoggedInWithTimeUITestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859089692B61763B003AF5F5 /* LoggedInWithTimeUITestCase.swift */; }; A9E031782ACB09930095D843 /* UIApplication+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E031762ACB08950095D843 /* UIApplication+Extensions.swift */; }; A9E0317A2ACB0AE70095D843 /* UIApplication+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E031792ACB0AE70095D843 /* UIApplication+Stubs.swift */; }; @@ -1250,12 +1256,10 @@ /* End PBXCopyFilesBuildPhase section */ /* 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 = ""; }; - 01EF6F352B6A5AEF00125696 /* BridgingHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BridgingHeader.h; sourceTree = ""; }; 01F1FF1D29F0627D007083C3 /* libshadowsocks_proxy.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libshadowsocks_proxy.a; path = ../target/debug/libshadowsocks_proxy.a; sourceTree = ""; }; 062B45BB28FD8C3B00746E77 /* RESTDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTDefaults.swift; sourceTree = ""; }; 063687AF28EB083800BE7161 /* ProxyURLRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyURLRequest.swift; sourceTree = ""; }; @@ -1835,6 +1839,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 = ""; }; @@ -1853,13 +1858,20 @@ 85557B152B5ABBBE00795FE1 /* XCUIElementQuery+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCUIElementQuery+Extensions.swift"; sourceTree = ""; }; 85557B1D2B5FB8C700795FE1 /* HeaderBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderBar.swift; sourceTree = ""; }; 85557B1F2B5FBBD700795FE1 /* AccountPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountPage.swift; sourceTree = ""; }; + 8556EB512B9A1C6900D26DD4 /* MullvadApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MullvadApi.swift; path = MullvadVPNUITests/MullvadApi.swift; sourceTree = ""; }; + 8556EB532B9A1D7100D26DD4 /* BridgingHeader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BridgingHeader.h; sourceTree = ""; }; + 8556EB552B9B0AC500D26DD4 /* RevokedDevicePage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RevokedDevicePage.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 = ""; }; 85C7A2E82B89024B00035D5A /* SettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTests.swift; sourceTree = ""; }; 85E3BDE42B70E18C00FA71FD /* Networking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Networking.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 = ""; }; @@ -3102,6 +3114,7 @@ 58CE5E57224146200008646E = { isa = PBXGroup; children = ( + 8556EB512B9A1C6900D26DD4 /* MullvadApi.swift */, 01EF6F2D2B6A51B100125696 /* mullvad-api.h */, 58F3C0A824A50C0E003E76BE /* Assets */, 58ECD29023F178FD004298B6 /* Configurations */, @@ -3545,12 +3558,11 @@ 852969262B4D9C1F007EAD4C /* MullvadVPNUITests */ = { isa = PBXGroup; children = ( + 85B267602B849ADB0098E3CD /* mullvad-api.h */, 852969272B4D9C1F007EAD4C /* AccountTests.swift */, + 8556EB532B9A1D7100D26DD4 /* BridgingHeader.h */, 85557B112B594FC900795FE1 /* ConnectivityTests.swift */, - 01EF6F352B6A5AEF00125696 /* BridgingHeader.h */, 852969372B4ED20E007EAD4C /* Info.plist */, - 01EF6F2D2B6A51B100125696 /* mullvad-api.h */, - 01EF6F292B6A473900125696 /* MullvadApi.swift */, 85557B0C2B591B0F00795FE1 /* Networking */, 852969312B4E9220007EAD4C /* Pages */, 850201DA2B503D7700EF8C96 /* RelayTests.swift */, @@ -3564,17 +3576,22 @@ 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 */, 8532E6862B8CCED600ACECD1 /* ProblemReportSubmittedPage.swift */, + 8556EB552B9B0AC500D26DD4 /* RevokedDevicePage.swift */, 850201DC2B503D8C00EF8C96 /* SelectLocationPage.swift */, 850201E22B51A93C00EF8C96 /* SettingsPage.swift */, 852969392B4F0238007EAD4C /* TermsOfServicePage.swift */, 850201DE2B5040A500EF8C96 /* TunnelControlPage.swift */, + 85FB5A0B2B6903990015DCED /* WelcomePage.swift */, ); path = Pages; sourceTree = ""; @@ -3788,7 +3805,8 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - A9DF789C2B7D1E410094E4AD /* BridgingHeader.h in Headers */, + 85B267612B849ADB0098E3CD /* mullvad-api.h in Headers */, + 8556EB542B9A1D7100D26DD4 /* BridgingHeader.h in Headers */, A9DF789B2B7D1DF10094E4AD /* mullvad-api.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4272,6 +4290,8 @@ 8529692C2B4D9C1F007EAD4C /* PBXTargetDependency */, ); name = MullvadVPNUITests; + packageProductDependencies = ( + ); productName = MullvadVPNUITests; productReference = 852969252B4D9C1F007EAD4C /* MullvadVPNUITests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; @@ -5527,12 +5547,14 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 8556EB522B9A1C6900D26DD4 /* MullvadApi.swift in Sources */, + 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 */, 85C7A2E92B89024B00035D5A /* SettingsTests.swift in Sources */, @@ -5540,14 +5562,17 @@ 8590896F2B61763B003AF5F5 /* LoggedOutUITestCase.swift in Sources */, 85557B202B5FBBD700795FE1 /* AccountPage.swift in Sources */, 852969352B4E9270007EAD4C /* LoginPage.swift in Sources */, + 8556EB562B9B0AC500D26DD4 /* RevokedDevicePage.swift in Sources */, 85557B102B59215F00795FE1 /* FirewallRule.swift in Sources */, 85557B0E2B591B2600795FE1 /* FirewallAPIClient.swift in Sources */, 852969282B4D9C1F007EAD4C /* AccountTests.swift in Sources */, - 01EF6F2A2B6A473900125696 /* MullvadApi.swift in Sources */, + 8587A05D2B84D43100152938 /* ChangeLogAlert.swift in Sources */, + 85FB5A102B6960A30015DCED /* AccountDeletionPage.swift in Sources */, 85557B162B5ABBBE00795FE1 /* XCUIElementQuery+Extensions.swift in Sources */, 855D9F5B2B63E56B00D7C64D /* ProblemReportPage.swift in Sources */, 8529693A2B4F0238007EAD4C /* TermsOfServicePage.swift in Sources */, 8532E6872B8CCED600ACECD1 /* ProblemReportSubmittedPage.swift in Sources */, + 85FB5A0C2B6903990015DCED /* WelcomePage.swift in Sources */, 850201DF2B5040A500EF8C96 /* TunnelControlPage.swift in Sources */, 85557B1E2B5FB8C700795FE1 /* HeaderBar.swift in Sources */, 85557B122B594FC900795FE1 /* ConnectivityTests.swift in Sources */, @@ -6340,6 +6365,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; @@ -6904,8 +6930,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"; @@ -6938,7 +6968,7 @@ 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"; @@ -7561,11 +7591,14 @@ 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; SWIFT_ACTIVE_COMPILATION_CONDITIONS = MULLVAD_ENVIRONMENT_PRODUCTION; SWIFT_OBJC_BRIDGING_HEADER = "$(PROJECT_DIR)/MullvadVPNUITests/BridgingHeader.h"; 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 a5dc7f26a5fa..8a911b97c120 100644 --- a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift +++ b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift @@ -17,8 +17,10 @@ public enum AccessibilityIdentifier: String { case cancelButton case connectionPanelButton case collapseButton + case createAccountButton case deleteButton case disconnectButton + case revokedDeviceLoginButton case infoButton case learnAboutPrivacyButton case loginBarButton @@ -47,13 +49,16 @@ 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 @@ -61,6 +66,9 @@ public enum AccessibilityIdentifier: String { case tunnelControlView case problemReportView case problemReportSubmittedView + case revokedDeviceView + case welcomeView + case deleteAccountView // Other UI elements case connectionPanelInAddressRow @@ -71,6 +79,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 a116e51839a9..bfdb60c70c41 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/MullvadVPN/View controllers/RevokedDevice/RevokedDeviceViewController.swift b/ios/MullvadVPN/View controllers/RevokedDevice/RevokedDeviceViewController.swift index d4dc37616de8..ea80ff14ef17 100644 --- a/ios/MullvadVPN/View controllers/RevokedDevice/RevokedDeviceViewController.swift +++ b/ios/MullvadVPN/View controllers/RevokedDevice/RevokedDeviceViewController.swift @@ -62,6 +62,7 @@ class RevokedDeviceViewController: UIViewController, RootContainment { private lazy var logoutButton: AppButton = { let button = AppButton(style: .default) + button.accessibilityIdentifier = .revokedDeviceLoginButton button.translatesAutoresizingMaskIntoConstraints = false button.setTitle( NSLocalizedString( @@ -108,6 +109,7 @@ class RevokedDeviceViewController: UIViewController, RootContainment { override func viewDidLoad() { super.viewDidLoad() + view.accessibilityIdentifier = .revokedDeviceView view.backgroundColor = .secondaryColor view.directionalLayoutMargins = UIMetrics.contentLayoutMargins diff --git a/ios/MullvadVPNUITests/AccountTests.swift b/ios/MullvadVPNUITests/AccountTests.swift index f15195b69d57..e12152291b01 100644 --- a/ios/MullvadVPNUITests/AccountTests.swift +++ b/ios/MullvadVPNUITests/AccountTests.swift @@ -9,6 +9,52 @@ import XCTest class AccountTests: LoggedOutUITestCase { + override func setUpWithError() throws { + continueAfterFailure = false + + 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 3c23f8209d23..a1a22b5c4031 100644 --- a/ios/MullvadVPNUITests/MullvadApi.swift +++ b/ios/MullvadVPNUITests/MullvadApi.swift @@ -38,6 +38,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..ba870b0ba7b8 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. @@ -10,7 +10,8 @@ import Foundation import XCTest enum MullvadAPIError: Error { - case incorrectConfigurationFormat + case invalidEndpointFormatError + case requestError } class MullvadAPIWrapper { @@ -18,18 +19,22 @@ 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 - public static func getAPIHostname() -> String { - return hostName + init() throws { + let apiAddress = try Self.getAPIIPAddress() + ":" + Self.getAPIPort() + let hostname = Self.hostName + mullvadAPI = try MullvadApi(apiAddress: apiAddress, hostname: hostname) } public static func getAPIIPAddress() throws -> String { guard let ipAddress = endpoint.components(separatedBy: ":").first else { - throw MullvadAPIError.incorrectConfigurationFormat + throw MullvadAPIError.invalidEndpointFormatError } return ipAddress @@ -37,9 +42,57 @@ class MullvadAPIWrapper { public static func getAPIPort() throws -> String { guard let port = endpoint.components(separatedBy: ":").last else { - throw MullvadAPIError.incorrectConfigurationFormat + throw MullvadAPIError.invalidEndpointFormatError } 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/HeaderBar.swift b/ios/MullvadVPNUITests/Pages/HeaderBar.swift index cf0473af8ccd..129d721b3745 100644 --- a/ios/MullvadVPNUITests/Pages/HeaderBar.swift +++ b/ios/MullvadVPNUITests/Pages/HeaderBar.swift @@ -10,6 +10,9 @@ import Foundation import XCTest class HeaderBar: Page { + lazy var accountButton = app.buttons[AccessibilityIdentifier.accountButton] + lazy var settingsButton = app.buttons[AccessibilityIdentifier.settingsButton] + @discardableResult override init(_ app: XCUIApplication) { super.init(app) @@ -18,12 +21,12 @@ class HeaderBar: Page { } @discardableResult func tapAccountButton() -> Self { - app.buttons[AccessibilityIdentifier.accountButton.rawValue].tap() + accountButton.tap() return self } @discardableResult func tapSettingsButton() -> Self { - app.buttons[AccessibilityIdentifier.settingsButton.rawValue].tap() + settingsButton.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/RevokedDevicePage.swift b/ios/MullvadVPNUITests/Pages/RevokedDevicePage.swift new file mode 100644 index 000000000000..4b8dd6dd7206 --- /dev/null +++ b/ios/MullvadVPNUITests/Pages/RevokedDevicePage.swift @@ -0,0 +1,26 @@ +// +// RevokedDevicePage.swift +// MullvadVPNUITests +// +// Created by Niklas Berglund on 2024-03-08. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import XCTest + +class RevokedDevicePage: Page { + @discardableResult override init(_ app: XCUIApplication) { + super.init(app) + + self.pageAccessibilityIdentifier = .revokedDeviceView + waitForPageToBeShown() + } + + @discardableResult func tapGoToLogin() -> Self { + app.buttons[AccessibilityIdentifier.revokedDeviceLoginButton] + .tap() + + return self + } +} 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/README.md b/ios/MullvadVPNUITests/README.md index 8602409322c8..6f2b4f0f085e 100644 --- a/ios/MullvadVPNUITests/README.md +++ b/ios/MullvadVPNUITests/README.md @@ -25,10 +25,10 @@ - `brew install go@1.19` ## GitHub runner setup -1. Ask GitHub admin for new runner token and set it up according to the steps presented, pass `--labels ios-test` to `config.sh` when running it. By default it will also have the labels `self-hosted` and `macOS` which are required as well. +1. Ask GitHub admin for new runner token and setup steps from GitHub. Set it up according to the steps, pass `--labels ios-test` to `config.sh` when running it. By default it will also have the labels `self-hosted` and `macOS` which are required as well. 2. Make sure GitHub actions secrets for the repository are correctly set up: - `IOS_DEVICE_PIN_CODE` - Device passcode if the device require it, otherwise leave blank. Devices used with CI should not require passcode. - `IOS_HAS_TIME_ACCOUNT_NUMBER` - Production server account without time left - `IOS_NO_TIME_ACCOUNT_NUMBER` - Production server account with time added to it - `IOS_TEST_DEVICE_IDENTIFIER_UUID` - unique identifier for the test device. Create new identifier with `uuidgen`. - - `IOS_TEST_DEVICE_UDID` - the iOS device's UDID. \ No newline at end of file + - `IOS_TEST_DEVICE_UDID` - the iOS device's UDID. diff --git a/ios/MullvadVPNUITests/Test base classes/BaseUITestCase.swift b/ios/MullvadVPNUITests/Test base classes/BaseUITestCase.swift index c99f25463ec1..2e4a252c76e1 100644 --- a/ios/MullvadVPNUITests/Test base classes/BaseUITestCase.swift +++ b/ios/MullvadVPNUITests/Test base classes/BaseUITestCase.swift @@ -76,11 +76,17 @@ class BaseUITestCase: XCTestCase { if termsOfServiceIsShown { TermsOfServicePage(app) .tapAgreeButton() + } + } - Alert(app) // Changes alert - .tapOkay() + func dismissChangeLogIfShown() { + let changeLogIsShown = app + .otherElements[AccessibilityIdentifier.changeLogAlert.rawValue] + .waitForExistence(timeout: 1.0) - LoginPage(app) + if changeLogIsShown { + ChangeLogAlert(app) + .tapOkay() } } @@ -95,11 +101,17 @@ class BaseUITestCase: XCTestCase { func logoutIfLoggedIn() { if isLoggedIn() { - HeaderBar(app) - .tapAccountButton() - - AccountPage(app) - .tapLogOutButton() + if app.buttons[AccessibilityIdentifier.accountButton].exists { + HeaderBar(app) + .tapAccountButton() + + AccountPage(app) + .tapLogOutButton() + } else { + // Workaround for revoked device view not showing account button + RevokedDevicePage(app) + .tapGoToLogin() + } LoginPage(app) } diff --git a/ios/MullvadVPNUITests/Test base classes/LoggedInWithTimeUITestCase.swift b/ios/MullvadVPNUITests/Test base classes/LoggedInWithTimeUITestCase.swift index fc1fa747f8eb..fcf6d0f5e2cb 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() + dismissChangeLogIfShown() logoutIfLoggedIn() login(accountNumber: hasTimeAccountNumber) diff --git a/ios/MullvadVPNUITests/Test base classes/LoggedInWithoutTimeUITestCase.swift b/ios/MullvadVPNUITests/Test base classes/LoggedInWithoutTimeUITestCase.swift index c7ab76e31bca..8c40c4e76abe 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() + dismissChangeLogIfShown() logoutIfLoggedIn() login(accountNumber: noTimeAccountNumber) diff --git a/ios/MullvadVPNUITests/Test base classes/LoggedOutUITestCase.swift b/ios/MullvadVPNUITests/Test base classes/LoggedOutUITestCase.swift index a89c6e732ef2..64d04142962f 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() + dismissChangeLogIfShown() 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 d7bc6acb47ac..c4d5665a0131 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(