summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2023-06-08 16:19:27 +0200
committerAndrej Mihajlov <and@mullvad.net>2023-06-08 16:19:27 +0200
commit04e49a9f00467a35138ba12d4c812397797ff1ce (patch)
tree7e0bdaebeb5f5e2187b0fa7b586d3d0cf364f732
parentac8deecc4ebd4a71810006d221292c03e36ad120 (diff)
parentd79201b3c90828e056b7c5bbf516ce551723afed (diff)
downloadmullvadvpn-04e49a9f00467a35138ba12d4c812397797ff1ce.tar.xz
mullvadvpn-04e49a9f00467a35138ba12d4c812397797ff1ce.zip
Merge branch 'set-account-flow'
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj20
-rw-r--r--ios/MullvadVPN/AppDelegate.swift59
-rw-r--r--ios/MullvadVPN/TunnelManager/SetAccountOperation.swift451
-rw-r--r--ios/Operations/InputInjectionBuilder.swift96
-rw-r--r--ios/Operations/InputOperation.swift47
-rw-r--r--ios/Operations/TransformOperation.swift110
-rw-r--r--ios/OperationsTests/OperationInputInjectionTests.swift89
-rw-r--r--ios/OperationsTests/TransformOperationTests.swift93
8 files changed, 247 insertions, 718 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index ad13c84ad4..2be0a3ed02 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -221,7 +221,6 @@
589A454C28DDF5E100565204 /* Swizzle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589A454B28DDF5E100565204 /* Swizzle.swift */; };
589A455C28E094BF00565204 /* OperationSmokeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DF5B7E2852778600E92647 /* OperationSmokeTests.swift */; };
589A455D28E094BF00565204 /* OperationObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583E1E292848DF67004838B3 /* OperationObserverTests.swift */; };
- 589A455E28E094BF00565204 /* OperationInputInjectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DF5B772852178600E92647 /* OperationInputInjectionTests.swift */; };
589A455F28E094BF00565204 /* OperationConditionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580CBFB72848D503007878F0 /* OperationConditionTests.swift */; };
58A1AA8C23F5584C009F7EA6 /* ConnectionPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A1AA8B23F5584B009F7EA6 /* ConnectionPanelView.swift */; };
58A3BDB028A1821A00C8C2C6 /* WgStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A3BDAF28A1821A00C8C2C6 /* WgStats.swift */; };
@@ -234,7 +233,6 @@
58ACF64F26567A7100ACE4B7 /* CustomSwitchContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF64E26567A7100ACE4B7 /* CustomSwitchContainer.swift */; };
58AFC99529F96F7B000829DE /* AsyncBlockOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AFC99429F96F7B000829DE /* AsyncBlockOperationTests.swift */; };
58AFC99729F9753D000829DE /* AsyncResultBlockOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AFC99629F9753D000829DE /* AsyncResultBlockOperationTests.swift */; };
- 58AFC99929F97856000829DE /* TransformOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AFC99829F97856000829DE /* TransformOperationTests.swift */; };
58B0A2A8238EE68200BC001D /* RelaySelectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584B26F3237434D00073B10E /* RelaySelectorTests.swift */; };
58B26E1E2943514300D5980C /* InAppNotificationDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B26E1D2943514300D5980C /* InAppNotificationDescriptor.swift */; };
58B26E22294351EA00D5980C /* InAppNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B26E21294351EA00D5980C /* InAppNotificationProvider.swift */; };
@@ -280,18 +278,15 @@
58D223AD294C8A630029F5F8 /* AsyncBlockOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE22324B3243100F9D8A1 /* AsyncBlockOperation.swift */; };
58D223AE294C8A630029F5F8 /* OutputOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58059DDD28468158002B1049 /* OutputOperation.swift */; };
58D223AF294C8A630029F5F8 /* NoFailedDependenciesCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581813A228E09DCD002817DE /* NoFailedDependenciesCondition.swift */; };
- 58D223B0294C8A630029F5F8 /* TransformOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58059DDB28465E8F002B1049 /* TransformOperation.swift */; };
58D223B1294C8A630029F5F8 /* OperationCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589D28772846250500F9A7B3 /* OperationCondition.swift */; };
58D223B2294C8A630029F5F8 /* OperationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840BE34279EDB16002836BA /* OperationError.swift */; };
58D223B3294C8A630029F5F8 /* OperationObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589D28792846250500F9A7B3 /* OperationObserver.swift */; };
- 58D223B4294C8A630029F5F8 /* InputInjectionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DF5B752852108E00E92647 /* InputInjectionBuilder.swift */; };
58D223B6294C8A630029F5F8 /* BlockCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581813A428E09DE2002817DE /* BlockCondition.swift */; };
58D223B7294C8A630029F5F8 /* MutuallyExclusive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581813A628E09DF2002817DE /* MutuallyExclusive.swift */; };
58D223B8294C8A630029F5F8 /* ResultOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F7D26427EB50A300E4D821 /* ResultOperation.swift */; };
58D223B9294C8A630029F5F8 /* AsyncOperationQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589D28782846250500F9A7B3 /* AsyncOperationQueue.swift */; };
58D223BA294C8A630029F5F8 /* BackgroundObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589D287F28462CB000F9A7B3 /* BackgroundObserver.swift */; };
58D223BB294C8A630029F5F8 /* NoCancelledDependenciesCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581813A028E09DBB002817DE /* NoCancelledDependenciesCondition.swift */; };
- 58D223BC294C8A630029F5F8 /* InputOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DF5B732851FF3F00E92647 /* InputOperation.swift */; };
58D223BD294C8A630029F5F8 /* AsyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E973DD24850EB600096F90 /* AsyncOperation.swift */; };
58D223BE294C8A630029F5F8 /* ResultBlockOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5842102D282D3FC200F24E46 /* ResultBlockOperation.swift */; };
58D223BF294C8AE90029F5F8 /* Operations.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58D223A5294C8A480029F5F8 /* Operations.framework */; };
@@ -807,7 +802,6 @@
06FAE67D28F83CA50033DD93 /* RESTTransport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RESTTransport.swift; sourceTree = "<group>"; };
5803B4AF2940A47300C23744 /* TunnelConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelConfiguration.swift; sourceTree = "<group>"; };
5803B4B12940A48700C23744 /* TunnelStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelStore.swift; sourceTree = "<group>"; };
- 58059DDB28465E8F002B1049 /* TransformOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransformOperation.swift; sourceTree = "<group>"; };
58059DDD28468158002B1049 /* OutputOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputOperation.swift; sourceTree = "<group>"; };
5807E2BF2432038B00F5FF30 /* String+Split.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Split.swift"; sourceTree = "<group>"; };
5807E2C1243203D000F5FF30 /* StringTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringTests.swift; sourceTree = "<group>"; };
@@ -1003,7 +997,6 @@
58AEEF642344A36000C9BBD5 /* KeychainError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainError.swift; sourceTree = "<group>"; };
58AFC99429F96F7B000829DE /* AsyncBlockOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncBlockOperationTests.swift; sourceTree = "<group>"; };
58AFC99629F9753D000829DE /* AsyncResultBlockOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncResultBlockOperationTests.swift; sourceTree = "<group>"; };
- 58AFC99829F97856000829DE /* TransformOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransformOperationTests.swift; sourceTree = "<group>"; };
58B0A2A0238EE67E00BC001D /* MullvadVPNTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MullvadVPNTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
58B0A2A4238EE67E00BC001D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
58B26E1D2943514300D5980C /* InAppNotificationDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppNotificationDescriptor.swift; sourceTree = "<group>"; };
@@ -1057,9 +1050,6 @@
58D223F5294C8FF00029F5F8 /* MullvadLogging.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MullvadLogging.h; sourceTree = "<group>"; };
58D229B6298D1D5200BB5A2D /* URLRequestProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLRequestProxy.swift; sourceTree = "<group>"; };
58DF28A42417CB4B00E836B0 /* StorePaymentManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorePaymentManager.swift; sourceTree = "<group>"; };
- 58DF5B732851FF3F00E92647 /* InputOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputOperation.swift; sourceTree = "<group>"; };
- 58DF5B752852108E00E92647 /* InputInjectionBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputInjectionBuilder.swift; sourceTree = "<group>"; };
- 58DF5B772852178600E92647 /* OperationInputInjectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationInputInjectionTests.swift; sourceTree = "<group>"; };
58DF5B7E2852778600E92647 /* OperationSmokeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationSmokeTests.swift; sourceTree = "<group>"; };
58E07298288031D5008902F8 /* WireGuardAdapterError+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WireGuardAdapterError+Localization.swift"; sourceTree = "<group>"; };
58E0729C28814AAE008902F8 /* PacketTunnelConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelConfiguration.swift; sourceTree = "<group>"; };
@@ -1843,10 +1833,8 @@
58AFC99429F96F7B000829DE /* AsyncBlockOperationTests.swift */,
58AFC99629F9753D000829DE /* AsyncResultBlockOperationTests.swift */,
580CBFB72848D503007878F0 /* OperationConditionTests.swift */,
- 58DF5B772852178600E92647 /* OperationInputInjectionTests.swift */,
583E1E292848DF67004838B3 /* OperationObserverTests.swift */,
58DF5B7E2852778600E92647 /* OperationSmokeTests.swift */,
- 58AFC99829F97856000829DE /* TransformOperationTests.swift */,
);
path = OperationsTests;
sourceTree = "<group>";
@@ -2045,8 +2033,6 @@
589D287F28462CB000F9A7B3 /* BackgroundObserver.swift */,
581813A428E09DE2002817DE /* BlockCondition.swift */,
589D28812846306C00F9A7B3 /* GroupOperation.swift */,
- 58DF5B752852108E00E92647 /* InputInjectionBuilder.swift */,
- 58DF5B732851FF3F00E92647 /* InputOperation.swift */,
581813A628E09DF2002817DE /* MutuallyExclusive.swift */,
581813A028E09DBB002817DE /* NoCancelledDependenciesCondition.swift */,
581813A228E09DCD002817DE /* NoFailedDependenciesCondition.swift */,
@@ -2056,7 +2042,6 @@
58059DDD28468158002B1049 /* OutputOperation.swift */,
5842102D282D3FC200F24E46 /* ResultBlockOperation.swift */,
58F7D26427EB50A300E4D821 /* ResultOperation.swift */,
- 58059DDB28465E8F002B1049 /* TransformOperation.swift */,
);
path = Operations;
sourceTree = "<group>";
@@ -2838,12 +2823,10 @@
buildActionMask = 2147483647;
files = (
589A455F28E094BF00565204 /* OperationConditionTests.swift in Sources */,
- 589A455E28E094BF00565204 /* OperationInputInjectionTests.swift in Sources */,
58AFC99529F96F7B000829DE /* AsyncBlockOperationTests.swift in Sources */,
58AFC99729F9753D000829DE /* AsyncResultBlockOperationTests.swift in Sources */,
589A455C28E094BF00565204 /* OperationSmokeTests.swift in Sources */,
589A455D28E094BF00565204 /* OperationObserverTests.swift in Sources */,
- 58AFC99929F97856000829DE /* TransformOperationTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -3122,8 +3105,6 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 58D223B0294C8A630029F5F8 /* TransformOperation.swift in Sources */,
- 58D223B4294C8A630029F5F8 /* InputInjectionBuilder.swift in Sources */,
58D223B6294C8A630029F5F8 /* BlockCondition.swift in Sources */,
58D223BA294C8A630029F5F8 /* BackgroundObserver.swift in Sources */,
58D223AD294C8A630029F5F8 /* AsyncBlockOperation.swift in Sources */,
@@ -3136,7 +3117,6 @@
58D223B9294C8A630029F5F8 /* AsyncOperationQueue.swift in Sources */,
58D223B8294C8A630029F5F8 /* ResultOperation.swift in Sources */,
58D223AE294C8A630029F5F8 /* OutputOperation.swift in Sources */,
- 58D223BC294C8A630029F5F8 /* InputOperation.swift in Sources */,
58D223BD294C8A630029F5F8 /* AsyncOperation.swift in Sources */,
58D223BE294C8A630029F5F8 /* ResultBlockOperation.swift in Sources */,
58D223B1294C8A630029F5F8 /* OperationCondition.swift in Sources */,
diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift
index 6090ccdd82..c105797191 100644
--- a/ios/MullvadVPN/AppDelegate.swift
+++ b/ios/MullvadVPN/AppDelegate.swift
@@ -120,7 +120,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
registerBackgroundTasks()
setupPaymentHandler()
- setupNotificationHandler()
+ setupNotifications()
addApplicationNotifications(application: application)
startInitialization(application: application)
@@ -362,7 +362,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
storePaymentManager.addPaymentObserver(tunnelManager)
}
- private func setupNotificationHandler() {
+ private func setupNotifications() {
NotificationManager.shared.notificationProviders = [
RegisteredDeviceInAppNotificationProvider(tunnelManager: tunnelManager),
TunnelStatusNotificationProvider(tunnelManager: tunnelManager),
@@ -375,10 +375,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
private func startInitialization(application: UIApplication) {
let wipeSettingsOperation = getWipeSettingsOperation()
- let loadTunnelStoreOperation = AsyncBlockOperation(dispatchQueue: .main) { finish in
- self.tunnelStore.loadPersistentTunnels { error in
+ let loadTunnelStoreOperation = AsyncBlockOperation(dispatchQueue: .main) { [self] finish in
+ tunnelStore.loadPersistentTunnels { [self] error in
if let error {
- self.logger.error(
+ logger.error(
error: error,
message: "Failed to load persistent tunnels."
)
@@ -387,22 +387,30 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
}
}
- let migrateSettingsOperation = ResultBlockOperation<SettingsMigrationResult>(dispatchQueue: .main) { finish in
- SettingsManager.migrateStore(with: self.proxyFactory) { migrationResult in
- let finishHandler = {
- finish(.success(migrationResult))
- }
+ let migrateSettingsOperation = AsyncBlockOperation(dispatchQueue: .main) { [self] finish in
+ SettingsManager.migrateStore(with: proxyFactory) { [self] migrationResult in
+ switch migrationResult {
+ case .success:
+ // Tell the tunnel to re-read tunnel configuration after migration.
+ logger.debug("Reconnect the tunnel after settings migration.")
+ tunnelManager.reconnectTunnel(selectNewRelay: true)
+ fallthrough
- guard case let .failure(error) = migrationResult,
- let migrationUIHandler = application.connectedScenes.compactMap({ scene in
- return scene.delegate as? SettingsMigrationUIHandler
- }).first
- else {
- finishHandler()
- return
- }
+ case .nothing:
+ finish(nil)
+
+ case let .failure(error):
+ let migrationUIHandler = application.connectedScenes.first { $0 is SettingsMigrationUIHandler }
+ as? SettingsMigrationUIHandler
- migrationUIHandler.showMigrationError(error, completionHandler: finishHandler)
+ if let migrationUIHandler {
+ migrationUIHandler.showMigrationError(error) {
+ finish(error)
+ }
+ } else {
+ finish(error)
+ }
+ }
}
}
migrateSettingsOperation.addDependencies([wipeSettingsOperation, loadTunnelStoreOperation])
@@ -424,25 +432,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
}
initTunnelManagerOperation.addDependency(migrateSettingsOperation)
- let reconnectTunnelOperation = TransformOperation<SettingsMigrationResult, Void>(
- dispatchQueue: .main
- ) { migrationResult in
- if case .success = migrationResult {
- self.logger.debug("Reconnect the tunnel after settings migration.")
-
- self.tunnelManager.reconnectTunnel(selectNewRelay: true)
- }
- }
- reconnectTunnelOperation.inject(from: migrateSettingsOperation)
- reconnectTunnelOperation.addDependency(initTunnelManagerOperation)
-
operationQueue.addOperations(
[
wipeSettingsOperation,
loadTunnelStoreOperation,
migrateSettingsOperation,
initTunnelManagerOperation,
- reconnectTunnelOperation,
],
waitUntilFinished: false
)
diff --git a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
index 1b85516871..4c6ddace88 100644
--- a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
@@ -12,7 +12,6 @@ import MullvadREST
import MullvadTypes
import Operations
import class WireGuardKitTypes.PrivateKey
-import class WireGuardKitTypes.PublicKey
enum SetAccountAction {
/// Set new account.
@@ -36,33 +35,6 @@ enum SetAccountAction {
}
}
-private struct SetAccountResult {
- let accountData: StoredAccountData
- let privateKey: PrivateKey
- let device: Device
-}
-
-private struct SetAccountContext: OperationInputContext {
- var accountData: StoredAccountData?
- var privateKey: PrivateKey?
- var device: Device?
-
- func reduce() -> SetAccountResult? {
- guard let accountData,
- let privateKey,
- let device
- else {
- return nil
- }
-
- return SetAccountResult(
- accountData: accountData,
- privateKey: privateKey,
- device: device
- )
- }
-}
-
class SetAccountOperation: ResultOperation<StoredAccountData?> {
private let interactor: TunnelInteractor
private let accountsProxy: REST.AccountsProxy
@@ -70,9 +42,7 @@ class SetAccountOperation: ResultOperation<StoredAccountData?> {
private let action: SetAccountAction
private let logger = Logger(label: "SetAccountOperation")
- private let operationQueue = AsyncOperationQueue()
-
- private var children: [Operation] = []
+ private var tasks: [Cancellable] = []
init(
dispatchQueue: DispatchQueue,
@@ -89,114 +59,168 @@ class SetAccountOperation: ResultOperation<StoredAccountData?> {
super.init(dispatchQueue: dispatchQueue)
}
- override func main() {
- let deleteDeviceOperation = getDeleteDeviceOperation()
- let unsetDeviceStateOperation = getUnsetDeviceStateOperation()
+ // MARK: -
- deleteDeviceOperation.flatMap { unsetDeviceStateOperation.addDependency($0) }
+ override func main() {
+ startLogoutFlow { [self] in
+ switch action {
+ case .new:
+ startNewAccountFlow { [self] result in
+ finish(result: result.map { .some($0) })
+ }
- let setupAccountOperations = getAccountDataOperation()
- .flatMap { accountOperation -> [Operation] in
- accountOperation.addCondition(
- NoFailedDependenciesCondition(ignoreCancellations: false)
- )
- accountOperation.addDependency(unsetDeviceStateOperation)
+ case let .existing(accountNumber):
+ startExistingAccountFlow(accountNumber: accountNumber) { [self] result in
+ finish(result: result.map { .some($0) })
+ }
- let createDeviceOperation = getCreateDeviceOperation()
- createDeviceOperation.addCondition(
- NoFailedDependenciesCondition(ignoreCancellations: false)
- )
- createDeviceOperation.inject(from: accountOperation)
+ case .unset:
+ finish(result: .success(nil))
+ }
+ }
+ }
- let saveSettingsOperation = getSaveSettingsOperation()
- saveSettingsOperation.addCondition(
- NoFailedDependenciesCondition(ignoreCancellations: false)
- )
+ override func operationDidCancel() {
+ tasks.forEach { $0.cancel() }
+ tasks.removeAll()
+ }
- saveSettingsOperation.injectMany(context: SetAccountContext())
- .inject(from: accountOperation, assignOutputTo: \.accountData)
- .inject(from: createDeviceOperation, via: { context, output in
- let (privateKey, device) = output
+ // MARK: - Private
- context.privateKey = privateKey
- context.device = device
- })
- .reduce()
+ /**
+ Begin logout flow by performing the following steps:
- saveSettingsOperation.onFinish { operation, error in
- self.completeOperation(accountData: operation.output)
- }
+ 1. Delete currently logged in device from the API if device is logged in.
+ 2. Transition device state to logged out state.
+ 3. Remove system VPN configuration if exists.
+ 4. Reset tunnel status to disconnected state.
- return [accountOperation, createDeviceOperation, saveSettingsOperation]
- } ?? []
+ Does nothing if device is already logged out.
+ */
+ private func startLogoutFlow(completion: @escaping () -> Void) {
+ switch interactor.deviceState {
+ case let .loggedIn(accountData, deviceData):
+ deleteDevice(accountNumber: accountData.number, deviceIdentifier: deviceData.identifier) { [self] error in
+ unsetDeviceState(completion: completion)
+ }
- var enqueueOperations: [Operation] = [deleteDeviceOperation, unsetDeviceStateOperation]
- .compactMap { $0 }
- enqueueOperations.append(contentsOf: setupAccountOperations)
+ case .revoked:
+ unsetDeviceState(completion: completion)
- if setupAccountOperations.isEmpty {
- let finishingOperation = BlockOperation()
- finishingOperation.completionBlock = { [weak self] in
- self?.completeOperation(accountData: nil)
- }
- finishingOperation.addDependencies(enqueueOperations)
- enqueueOperations.append(finishingOperation)
+ case .loggedOut:
+ completion()
}
-
- children = enqueueOperations
- operationQueue.addOperations(enqueueOperations, waitUntilFinished: false)
}
- override func operationDidCancel() {
- operationQueue.cancelAllOperations()
+ /**
+ Begin login flow with a new account and performing the following steps:
+
+ 1. Create new account via API.
+ 2. Call `continueLoginFlow()` passing the result of account creation request.
+ */
+ private func startNewAccountFlow(completion: @escaping (Result<StoredAccountData, Error>) -> Void) {
+ createAccount { [self] result in
+ continueLoginFlow(result, completion: completion)
+ }
}
- // MARK: - Private
+ /**
+ Begin login flow with an existing account by performing the following steps:
- private func completeOperation(accountData: StoredAccountData?) {
- guard !isCancelled else {
- finish(result: .failure(OperationError.cancelled))
- return
+ 1. Retrieve existing account from the API.
+ 2. Call `continueLoginFlow()` passing the result of account retrieval request.
+ */
+ private func startExistingAccountFlow(
+ accountNumber: String,
+ completion: @escaping (Result<StoredAccountData, Error>) -> Void
+ ) {
+ getAccount(accountNumber: accountNumber) { [self] result in
+ continueLoginFlow(result, completion: completion)
}
+ }
+
+ /**
+ Continue login flow after receiving account data as a part of creating new or retrieving existing account from
+ the API by performing the following steps:
- let errors = children.compactMap { operation -> Error? in
- return (operation as? AsyncOperation)?.error
+ 1. Store last used account number.
+ 2. Create new device with the API.
+ 3. Persist settings.
+ */
+ private func continueLoginFlow(
+ _ result: Result<StoredAccountData, Error>,
+ completion: @escaping (Result<StoredAccountData, Error>) -> Void
+ ) {
+ do {
+ let accountData = try result.get()
+
+ storeLastUsedAccount(accountNumber: accountData.number)
+
+ createDevice(accountNumber: accountData.number) { [self] result in
+ completion(result.map { newDevice in
+ storeSettings(accountData: accountData, newDevice: newDevice)
+
+ return accountData
+ })
+ }
+ } catch {
+ completion(.failure(error))
}
+ }
+
+ /// Store last used account number in settings.
+ /// Errors are ignored but logged.
+ private func storeLastUsedAccount(accountNumber: String) {
+ logger.debug("Store last used account.")
- if let error = errors.first {
- finish(result: .failure(error))
- } else {
- finish(result: .success(accountData))
+ do {
+ try SettingsManager.setLastUsedAccount(accountNumber)
+ } catch {
+ logger.error(error: error, message: "Failed to store last used account number.")
}
}
- private func getAccountDataOperation() -> ResultOperation<StoredAccountData>? {
- switch action {
- case .new:
- return getCreateAccountOperation()
+ /// Store account data and newly created device in settings and transition device state to logged in state.
+ private func storeSettings(accountData: StoredAccountData, newDevice: NewDevice) {
+ logger.debug("Saving settings...")
+
+ // Create stored device data.
+ let restDevice = newDevice.device
+ let storedDeviceData = StoredDeviceData(
+ creationDate: restDevice.created,
+ identifier: restDevice.id,
+ name: restDevice.name,
+ hijackDNS: restDevice.hijackDNS,
+ ipv4Address: restDevice.ipv4Address,
+ ipv6Address: restDevice.ipv6Address,
+ wgKeyData: StoredWgKeyData(
+ creationDate: Date(),
+ privateKey: newDevice.privateKey
+ )
+ )
- case let .existing(accountNumber):
- return getExistingAccountOperation(accountNumber: accountNumber)
+ // Reset tunnel settings.
+ interactor.setSettings(TunnelSettingsV2(), persist: true)
- case .unset:
- return nil
- }
+ // Transition device state to logged in.
+ interactor.setDeviceState(.loggedIn(accountData, storedDeviceData), persist: true)
}
- private func getCreateAccountOperation() -> ResultBlockOperation<StoredAccountData> {
- return ResultBlockOperation<StoredAccountData>(dispatchQueue: dispatchQueue) { finish -> Cancellable in
- self.logger.debug("Create new account...")
+ /// Create new account and produce `StoredAccountData` upon success.
+ private func createAccount(completion: @escaping (Result<StoredAccountData, Error>) -> Void) {
+ logger.debug("Create new account...")
- return self.accountsProxy.createAccount(retryStrategy: .default) { result in
+ let task = accountsProxy.createAccount(retryStrategy: .default) { [self] result in
+ dispatchQueue.async { [self] in
let result = result.inspectError { error in
guard !error.isOperationCancellationError else { return }
- self.logger.error(
+ logger.error(
error: error,
message: "Failed to create new account."
)
}.map { newAccountData -> StoredAccountData in
- self.logger.debug("Created new account.")
+ logger.debug("Created new account.")
return StoredAccountData(
identifier: newAccountData.id,
@@ -205,26 +229,26 @@ class SetAccountOperation: ResultOperation<StoredAccountData?> {
)
}
- finish(result)
+ completion(result)
}
}
+
+ tasks.append(task)
}
- private func getExistingAccountOperation(accountNumber: String) -> ResultOperation<StoredAccountData> {
- return ResultBlockOperation<StoredAccountData>(dispatchQueue: dispatchQueue) { finish -> Cancellable in
- self.logger.debug("Request account data...")
+ /// Get account data from the API and produce `StoredAccountData` upon success.
+ private func getAccount(accountNumber: String, completion: @escaping (Result<StoredAccountData, Error>) -> Void) {
+ logger.debug("Request account data...")
- return self.accountsProxy
- .getAccountData(accountNumber: accountNumber, retryStrategy: .default) { result in
+ let task = accountsProxy
+ .getAccountData(accountNumber: accountNumber, retryStrategy: .default) { [self] result in
+ dispatchQueue.async { [self] in
let result = result.inspectError { error in
guard !error.isOperationCancellationError else { return }
- self.logger.error(
- error: error,
- message: "Failed to receive account data."
- )
+ logger.error(error: error, message: "Failed to receive account data.")
}.map { accountData -> StoredAccountData in
- self.logger.debug("Received account data.")
+ logger.debug("Received account data.")
return StoredAccountData(
identifier: accountData.id,
@@ -233,151 +257,116 @@ class SetAccountOperation: ResultOperation<StoredAccountData?> {
)
}
- finish(result)
+ completion(result)
}
- }
- }
+ }
- private func getDeleteDeviceOperation() -> AsyncBlockOperation? {
- guard case let .loggedIn(accountData, deviceData) = interactor.deviceState else {
- return nil
- }
+ tasks.append(task)
+ }
- let operation = AsyncBlockOperation(dispatchQueue: dispatchQueue) { finish -> Cancellable in
- self.logger.debug("Delete current device...")
+ /// Delete device from API.
+ private func deleteDevice(accountNumber: String, deviceIdentifier: String, completion: @escaping (Error?) -> Void) {
+ logger.debug("Delete current device...")
- return self.devicesProxy.deleteDevice(
- accountNumber: accountData.number,
- identifier: deviceData.identifier,
- retryStrategy: .default
- ) { result in
+ let task = devicesProxy.deleteDevice(
+ accountNumber: accountNumber,
+ identifier: deviceIdentifier,
+ retryStrategy: .default
+ ) { [self] result in
+ dispatchQueue.async { [self] in
switch result {
case let .success(isDeleted):
- self.logger.debug(isDeleted ? "Deleted device." : "Device is already deleted.")
+ logger.debug(isDeleted ? "Deleted device." : "Device is already deleted.")
- case let .failure(error) where !error.isOperationCancellationError:
- self.logger.error(
- error: error,
- message: "Failed to delete device."
- )
-
- default:
- break
+ case let .failure(error):
+ if !error.isOperationCancellationError {
+ logger.error(error: error, message: "Failed to delete device.")
+ }
}
- finish(result.error)
+ completion(result.error)
}
}
- return operation
+ tasks.append(task)
}
- private func getUnsetDeviceStateOperation() -> AsyncBlockOperation {
- return AsyncBlockOperation(dispatchQueue: dispatchQueue) { finish in
- // Tell the caller to unsubscribe from VPN status notifications.
- self.interactor.prepareForVPNConfigurationDeletion()
-
- // Reset tunnel and device state.
- self.interactor.updateTunnelStatus { tunnelStatus in
- tunnelStatus = TunnelStatus()
- tunnelStatus.state = .disconnected
- }
- self.interactor.setDeviceState(.loggedOut, persist: true)
+ /**
+ Transitions device state into logged out state by performing the following tasks:
- // Finish immediately if tunnel provider is not set.
- guard let tunnel = self.interactor.tunnel else {
- finish(nil)
- return
- }
+ 1. Prepare tunnel manager for removal of VPN configuration. In response tunnel manager stops processing VPN status
+ notifications coming from VPN configuration.
+ 2. Reset device staate to logged out and persist it.
+ 3. Remove VPN configuration and release an instance of `Tunnel` object.
+ */
+ private func unsetDeviceState(completion: @escaping () -> Void) {
+ // Tell the caller to unsubscribe from VPN status notifications.
+ interactor.prepareForVPNConfigurationDeletion()
- // Remove VPN configuration.
- tunnel.removeFromPreferences { error in
- self.dispatchQueue.async {
- // Ignore error but log it.
- if let error {
- self.logger.error(
- error: error,
- message: "Failed to remove VPN configuration."
- )
- }
+ // Reset tunnel and device state.
+ interactor.updateTunnelStatus { tunnelStatus in
+ tunnelStatus = TunnelStatus()
+ tunnelStatus.state = .disconnected
+ }
+ interactor.setDeviceState(.loggedOut, persist: true)
- self.interactor.setTunnel(nil, shouldRefreshTunnelState: false)
+ // Finish immediately if tunnel provider is not set.
+ guard let tunnel = interactor.tunnel else {
+ completion()
+ return
+ }
- finish(nil)
+ // Remove VPN configuration.
+ tunnel.removeFromPreferences { [self] error in
+ dispatchQueue.async { [self] in
+ // Ignore error but log it.
+ if let error {
+ logger.error(
+ error: error,
+ message: "Failed to remove VPN configuration."
+ )
}
+
+ interactor.setTunnel(nil, shouldRefreshTunnelState: false)
+
+ completion()
}
}
}
- private func getCreateDeviceOperation() -> TransformOperation<StoredAccountData, (PrivateKey, Device)> {
- return TransformOperation<
- StoredAccountData,
- (PrivateKey, Device)
- >(dispatchQueue: dispatchQueue) { storedAccountData, finish -> Cancellable in
- self.logger.debug("Store last used account.")
+ /// Create new private key and create new device via API.
+ private func createDevice(accountNumber: String, completion: @escaping (Result<NewDevice, Error>) -> Void) {
+ let privateKey = PrivateKey()
- do {
- try SettingsManager.setLastUsedAccount(storedAccountData.number)
- } catch {
- self.logger.error(
- error: error,
- message: "Failed to store last used account number."
- )
- }
-
- self.logger.debug("Create device...")
+ let request = REST.CreateDeviceRequest(
+ publicKey: privateKey.publicKey,
+ hijackDNS: false
+ )
- let privateKey = PrivateKey()
+ logger.debug("Create device...")
- let request = REST.CreateDeviceRequest(
- publicKey: privateKey.publicKey,
- hijackDNS: false
- )
+ let task = devicesProxy
+ .createDevice(accountNumber: accountNumber, request: request, retryStrategy: .default) { [self] result in
+ dispatchQueue.async { [self] in
+ let result = result
+ .map { device in
+ return NewDevice(privateKey: privateKey, device: device)
+ }
+ .inspectError { error in
+ logger.error(error: error, message: "Failed to create device.")
+ }
- return self.devicesProxy.createDevice(
- accountNumber: storedAccountData.number,
- request: request,
- retryStrategy: .default
- ) { result in
- let result = result
- .map { device in
- return (privateKey, device)
- }
- .inspectError { error in
- self.logger.error(error: error, message: "Failed to create device.")
- }
-
- finish(result)
+ completion(result)
+ }
}
- }
- }
-
- private func getSaveSettingsOperation() -> TransformOperation<SetAccountResult, StoredAccountData> {
- return TransformOperation<SetAccountResult, StoredAccountData>(dispatchQueue: dispatchQueue) { input in
- self.logger.debug("Saving settings...")
- let device = input.device
- let newDeviceState = DeviceState.loggedIn(
- input.accountData,
- StoredDeviceData(
- creationDate: device.created,
- identifier: device.id,
- name: device.name,
- hijackDNS: device.hijackDNS,
- ipv4Address: device.ipv4Address,
- ipv6Address: device.ipv6Address,
- wgKeyData: StoredWgKeyData(
- creationDate: Date(),
- lastRotationAttemptDate: nil,
- privateKey: input.privateKey
- )
- )
- )
-
- self.interactor.setSettings(TunnelSettingsV2(), persist: true)
- self.interactor.setDeviceState(newDeviceState, persist: true)
+ tasks.append(task)
+ }
- return input.accountData
- }
+ /// Struct that holds a private key that was used for creating a new device on the API along with the successful
+ /// response from the API.
+ private struct NewDevice {
+ var privateKey: PrivateKey
+ var device: Device
}
}
diff --git a/ios/Operations/InputInjectionBuilder.swift b/ios/Operations/InputInjectionBuilder.swift
deleted file mode 100644
index 0c87ab767a..0000000000
--- a/ios/Operations/InputInjectionBuilder.swift
+++ /dev/null
@@ -1,96 +0,0 @@
-//
-// InputInjectionBuilder.swift
-// Operations
-//
-// Created by pronebird on 09/06/2022.
-// Copyright © 2022 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-
-public protocol OperationInputContext {
- associatedtype Input
-
- func reduce() -> Input?
-}
-
-public final class InputInjectionBuilder<OperationType, Context>
- where OperationType: InputOperation
-{
- public typealias InputBlock = (inout Context) -> Void
-
- private let operation: OperationType
- private var context: Context
- private var inputBlocks: [InputBlock] = []
-
- public init(operation: OperationType, context: Context) {
- self.operation = operation
- self.context = context
- }
-
- public func inject<T>(
- from dependency: T,
- assignOutputTo keyPath: WritableKeyPath<Context, T.Output?>
- ) -> Self
- where T: OutputOperation
- {
- return inject(from: dependency) { context, output in
- context[keyPath: keyPath] = output
- }
- }
-
- public func inject<T>(
- from dependency: T,
- via block: @escaping (inout Context, T.Output) -> Void
- ) -> Self
- where T: OutputOperation
- {
- inputBlocks.append { context in
- if let output = dependency.output {
- block(&context, output)
- }
- }
-
- operation.addDependency(dependency)
-
- return self
- }
-
- public func injectResult<T, Success>(
- from dependency: T,
- via block: @escaping (inout Context, Result<T.Output, Error>) -> Void
- ) -> Self
- where T: ResultOperation<Success>
- {
- inputBlocks.append { context in
- if let result = dependency.result {
- block(&context, result)
- }
- }
-
- operation.addDependency(dependency)
-
- return self
- }
-
- public func reduce(_ reduceBlock: @escaping (Context) -> OperationType.Input?) {
- operation.setInputBlock {
- for inputBlock in self.inputBlocks {
- inputBlock(&self.context)
- }
-
- return reduceBlock(self.context)
- }
- }
-}
-
-extension InputInjectionBuilder
- where Context: OperationInputContext,
- Context.Input == OperationType.Input
-{
- public func reduce() {
- reduce { context in
- return context.reduce()
- }
- }
-}
diff --git a/ios/Operations/InputOperation.swift b/ios/Operations/InputOperation.swift
deleted file mode 100644
index 9c141671ec..0000000000
--- a/ios/Operations/InputOperation.swift
+++ /dev/null
@@ -1,47 +0,0 @@
-//
-// InputOperation.swift
-// Operations
-//
-// Created by pronebird on 09/06/2022.
-// Copyright © 2022 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-
-public protocol InputOperation: Operation {
- associatedtype Input
-
- var input: Input? { get }
-
- func setInputBlock(_ block: @escaping () -> Input?)
-
- func inject<T>(from dependency: T)
- where T: OutputOperation, T.Output == Input
-
- func inject<T>(from dependency: T, via block: @escaping (T.Output) -> Input)
- where T: OutputOperation
-}
-
-extension InputOperation {
- public func inject<T>(from dependency: T) where T: OutputOperation, T.Output == Input {
- inject(from: dependency, via: { $0 })
- }
-
- public func inject<T>(from dependency: T, via block: @escaping (T.Output) -> Input)
- where T: OutputOperation
- {
- setInputBlock {
- return dependency.output.map { value in
- return block(value)
- }
- }
- addDependency(dependency)
- }
-
- public func injectMany<Context>(context: Context) -> InputInjectionBuilder<Self, Context> {
- return InputInjectionBuilder(
- operation: self,
- context: context
- )
- }
-}
diff --git a/ios/Operations/TransformOperation.swift b/ios/Operations/TransformOperation.swift
deleted file mode 100644
index 22946cbe1a..0000000000
--- a/ios/Operations/TransformOperation.swift
+++ /dev/null
@@ -1,110 +0,0 @@
-//
-// TransformOperation.swift
-// Operations
-//
-// Created by pronebird on 31/05/2022.
-// Copyright © 2022 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-import protocol MullvadTypes.Cancellable
-
-public final class TransformOperation<Input, Output>: ResultOperation<Output>, InputOperation {
- public typealias InputBlock = () -> Input?
-
- private let nslock = NSLock()
-
- public var input: Input? {
- return _input
- }
-
- private var __input: Input?
- private var _input: Input? {
- get {
- nslock.lock()
- defer { nslock.unlock() }
- return __input
- }
- set {
- nslock.lock()
- __input = newValue
- nslock.unlock()
- }
- }
-
- private var inputBlock: InputBlock?
-
- private var executor: ((Input, @escaping (Result<Output, Error>) -> Void) -> Cancellable?)?
- private var cancellableTask: Cancellable?
-
- public init(
- dispatchQueue: DispatchQueue? = nil,
- input: Input? = nil,
- block: @escaping (_ input: Input, _ finish: @escaping (Result<Output, Error>) -> Void) -> Void
- ) {
- super.init(dispatchQueue: dispatchQueue)
- __input = input
- executor = { input, finish in
- block(input, finish)
- return nil
- }
- }
-
- public init(
- dispatchQueue: DispatchQueue? = nil,
- input: Input? = nil,
- throwingBlock: @escaping (_ input: Input) throws -> Output
- ) {
- super.init(dispatchQueue: dispatchQueue)
- __input = input
- executor = { input, finish in
- finish(Result { try throwingBlock(input) })
- return nil
- }
- }
-
- public init(
- dispatchQueue: DispatchQueue? = nil,
- input: Input? = nil,
- cancellableTask: @escaping (_ input: Input, _ finish: @escaping (Result<Output, Error>) -> Void) -> Cancellable
- ) {
- super.init(dispatchQueue: dispatchQueue)
- __input = input
- executor = cancellableTask
- }
-
- override public func main() {
- if let inputBlock {
- _input = inputBlock()
- }
-
- guard let inputValue = _input else {
- finish(result: .failure(OperationError.unsatisfiedRequirement))
- return
- }
-
- let executor = executor
- self.executor = nil
-
- assert(executor != nil)
-
- cancellableTask = executor?(inputValue, self.finish)
- }
-
- override public func operationDidCancel() {
- cancellableTask?.cancel()
- }
-
- override public func operationDidFinish() {
- executor = nil
- cancellableTask = nil
- }
-
- // MARK: - Input injection
-
- public func setInputBlock(_ block: @escaping () -> Input?) {
- dispatchQueue.async {
- self.inputBlock = block
- }
- }
-}
diff --git a/ios/OperationsTests/OperationInputInjectionTests.swift b/ios/OperationsTests/OperationInputInjectionTests.swift
deleted file mode 100644
index 119c699954..0000000000
--- a/ios/OperationsTests/OperationInputInjectionTests.swift
+++ /dev/null
@@ -1,89 +0,0 @@
-//
-// OperationInputInjectionTests.swift
-// MullvadVPNTests
-//
-// Created by pronebird on 09/06/2022.
-// Copyright © 2022 Mullvad VPN AB. All rights reserved.
-//
-
-import Operations
-import XCTest
-
-class OperationInputInjectionTests: XCTestCase {
- func testInject() throws {
- let provider = ResultBlockOperation<Int> {
- return 1
- }
-
- let consumer = TransformOperation<Int, Int> { input in
- return input + 1
- }
-
- consumer.inject(from: provider)
-
- let operationQueue = AsyncOperationQueue()
-
- operationQueue.addOperations([provider, consumer], waitUntilFinished: true)
-
- XCTAssertEqual(consumer.output, 2)
- }
-
- func testInjectVia() throws {
- let provider = ResultBlockOperation<Int> {
- return 1
- }
-
- let consumer = TransformOperation<String, Int> { input in
- return Int(input)!
- }
-
- consumer.inject(from: provider) { output in
- return "\(output)"
- }
-
- let operationQueue = AsyncOperationQueue()
-
- operationQueue.addOperations([provider, consumer], waitUntilFinished: true)
-
- XCTAssertEqual(consumer.output, 1)
- }
-
- func testInjectMany() throws {
- struct Context: OperationInputContext {
- var a: Int?
- var b: Int?
-
- func reduce() -> Int? {
- guard let a, let b else { return nil }
-
- return a + b
- }
- }
-
- let operationQueue = AsyncOperationQueue()
-
- let providerA = ResultBlockOperation<Int> {
- return 1
- }
-
- let providerB = ResultBlockOperation<Int> {
- return 2
- }
-
- let consumer = TransformOperation<Int, String> { input in
- return "\(input)"
- }
-
- consumer.injectMany(context: Context())
- .inject(from: providerA, assignOutputTo: \.a)
- .inject(from: providerB, assignOutputTo: \.b)
- .reduce()
-
- operationQueue.addOperations(
- [providerA, providerB, consumer],
- waitUntilFinished: true
- )
-
- XCTAssertEqual(consumer.output, "3")
- }
-}
diff --git a/ios/OperationsTests/TransformOperationTests.swift b/ios/OperationsTests/TransformOperationTests.swift
deleted file mode 100644
index c16bcd3be1..0000000000
--- a/ios/OperationsTests/TransformOperationTests.swift
+++ /dev/null
@@ -1,93 +0,0 @@
-//
-// TransformOperationTests.swift
-// OperationsTests
-//
-// Created by pronebird on 26/04/2023.
-// Copyright © 2023 Mullvad VPN AB. All rights reserved.
-//
-
-import MullvadTypes
-import Operations
-import XCTest
-
-final class TransformOperationTests: XCTestCase {
- let operationQueue = AsyncOperationQueue()
-
- func testBlockTransformOperation() {
- let finishExpectation = expectation(description: "Should finish")
-
- let transform = TransformOperation(input: Int.zero) { input, finish in
- finish(.success(input + 1))
- }
-
- transform.onFinish { op, error in
- XCTAssertEqual(op.result?.value, 1)
-
- finishExpectation.fulfill()
- }
-
- operationQueue.addOperation(transform)
-
- waitForExpectations(timeout: 1)
- }
-
- func testThrowingBlockTransformOperation() {
- let finishExpectation = expectation(description: "Should finish")
-
- let transform = TransformOperation(input: Int.zero) { value in
- throw URLError(.badURL)
- }
-
- transform.onFinish { op, error in
- XCTAssertEqual(error as? URLError, URLError(.badURL))
-
- finishExpectation.fulfill()
- }
-
- operationQueue.addOperation(transform)
-
- waitForExpectations(timeout: 1)
- }
-
- func testCancellableTaskBlockTransformOperation() {
- let finishExpectation = expectation(description: "Should finish")
-
- let transform = TransformOperation<Int, Int>(input: Int.zero) { _, finish -> Cancellable in
- return AnyCancellable {
- finish(.failure(URLError(.cancelled)))
- }
- }
-
- transform.onStart { op in
- op.cancel()
- }
-
- transform.onFinish { op, error in
- XCTAssertEqual(error as? URLError, URLError(.cancelled))
-
- finishExpectation.fulfill()
- }
-
- operationQueue.addOperation(transform)
-
- waitForExpectations(timeout: 1)
- }
-
- func testShouldFailWithUnsatisfiedRequirement() {
- let finishExpectation = expectation(description: "Should finish")
-
- let transform = TransformOperation<Int, Int> { input, finish in
- finish(.success(input))
- }
-
- transform.onFinish { _, error in
- XCTAssertEqual(error as? OperationError, .unsatisfiedRequirement)
-
- finishExpectation.fulfill()
- }
-
- operationQueue.addOperation(transform)
-
- waitForExpectations(timeout: 1)
- }
-}