diff --git a/ios/MullvadRESTTests/AccessMethodRepositoryStub.swift b/ios/MullvadMockData/MullvadREST/AccessMethodRepository+Stub.swift similarity index 51% rename from ios/MullvadRESTTests/AccessMethodRepositoryStub.swift rename to ios/MullvadMockData/MullvadREST/AccessMethodRepository+Stub.swift index a2640df952ed..c21941f4af41 100644 --- a/ios/MullvadRESTTests/AccessMethodRepositoryStub.swift +++ b/ios/MullvadMockData/MullvadREST/AccessMethodRepository+Stub.swift @@ -1,5 +1,5 @@ // -// AccessMethodRepositoryStub.swift +// AccessMethodRepository+Stub.swift // MullvadRESTTests // // Created by Mojgan on 2024-01-02. @@ -9,27 +9,27 @@ import Combine import MullvadSettings -struct AccessMethodRepositoryStub: AccessMethodRepositoryDataSource { - var directAccess: PersistentAccessMethod +public struct AccessMethodRepositoryStub: AccessMethodRepositoryDataSource { + public var directAccess: PersistentAccessMethod - var accessMethodsPublisher: AnyPublisher<[PersistentAccessMethod], Never> { + public var accessMethodsPublisher: AnyPublisher<[PersistentAccessMethod], Never> { passthroughSubject.eraseToAnyPublisher() } let passthroughSubject: CurrentValueSubject<[PersistentAccessMethod], Never> = CurrentValueSubject([]) - init(accessMethods: [PersistentAccessMethod]) { + public init(accessMethods: [PersistentAccessMethod]) { directAccess = accessMethods.first(where: { $0.kind == .direct })! passthroughSubject.send(accessMethods) } - func fetchAll() -> [PersistentAccessMethod] { + public func fetchAll() -> [PersistentAccessMethod] { passthroughSubject.value } - func saveLastReachable(_ method: PersistentAccessMethod) {} + public func saveLastReachable(_ method: PersistentAccessMethod) {} - func fetchLastReachable() -> PersistentAccessMethod { + public func fetchLastReachable() -> PersistentAccessMethod { directAccess } } diff --git a/ios/MullvadMockData/MullvadREST/RelaySelectorStub.swift b/ios/MullvadMockData/MullvadREST/RelaySelectorStub.swift index f404aff1e275..4445aa8cc8c4 100644 --- a/ios/MullvadMockData/MullvadREST/RelaySelectorStub.swift +++ b/ios/MullvadMockData/MullvadREST/RelaySelectorStub.swift @@ -11,14 +11,18 @@ import MullvadTypes import WireGuardKitTypes /// Relay selector stub that accepts a block that can be used to provide custom implementation. -public struct RelaySelectorStub: RelaySelectorProtocol { - let block: (RelayConstraints, UInt) throws -> SelectedRelays +public final class RelaySelectorStub: RelaySelectorProtocol { + var selectedRelaysResult: (RelayConstraints, UInt) throws -> SelectedRelays + + init(selectedRelaysResult: @escaping (RelayConstraints, UInt) throws -> SelectedRelays) { + self.selectedRelaysResult = selectedRelaysResult + } public func selectRelays( with constraints: RelayConstraints, connectionAttemptCount: UInt ) throws -> SelectedRelays { - return try block(constraints, connectionAttemptCount) + return try selectedRelaysResult(constraints, connectionAttemptCount) } } @@ -53,4 +57,11 @@ extension RelaySelectorStub { ) } } + + /// Returns a relay selector that cannot satisfy constraints . + public static func unsatisfied() -> RelaySelectorStub { + return RelaySelectorStub { _, _ in + throw NoRelaysSatisfyingConstraintsError() + } + } } diff --git a/ios/MullvadRESTTests/TransportStrategyTests.swift b/ios/MullvadRESTTests/TransportStrategyTests.swift index 3174686ff62d..b1875c87e529 100644 --- a/ios/MullvadRESTTests/TransportStrategyTests.swift +++ b/ios/MullvadRESTTests/TransportStrategyTests.swift @@ -6,6 +6,7 @@ // Copyright © 2023 Mullvad VPN AB. All rights reserved. // +@testable import MullvadMockData @testable import MullvadREST @testable import MullvadSettings @testable import MullvadTypes diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 8282c9095d3d..d9ba33a69776 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -829,14 +829,12 @@ A9D9A4CD2C36D54E004088DD /* UDPConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585A02EA2A4B285800C6CAFF /* UDPConnection.swift */; }; A9D9A4CE2C36D54E004088DD /* TunnelObfuscationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58695A9F2A4ADA9200328DB3 /* TunnelObfuscationTests.swift */; }; A9D9A4CF2C36D54E004088DD /* TCPConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585A02EC2A4B28F300C6CAFF /* TCPConnection.swift */; }; - A9D9A4D02C36DAFD004088DD /* PostQuantumKeyExchangeActor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A948809A2BC9308D0090A44C /* PostQuantumKeyExchangeActor.swift */; }; A9D9A4D22C36DBAF004088DD /* MullvadPostQuantum+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C308392C19DDA7008715F1 /* MullvadPostQuantum+Stubs.swift */; }; A9D9A4D42C36E1EA004088DD /* mullvad_rust_runtime.h in Headers */ = {isa = PBXBuildFile; fileRef = A9D9A4D32C36E1EA004088DD /* mullvad_rust_runtime.h */; settings = {ATTRIBUTES = (Private, ); }; }; A9DF789B2B7D1DF10094E4AD /* mullvad-api.h in Headers */ = {isa = PBXBuildFile; fileRef = 01EF6F2D2B6A51B100125696 /* mullvad-api.h */; settings = {ATTRIBUTES = (Private, ); }; }; 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 */; }; - A9E0317C2ACBFC7E0095D843 /* TunnelStore+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E0317B2ACBFC7E0095D843 /* TunnelStore+Stubs.swift */; }; A9E0317F2ACC331C0095D843 /* TunnelStatusBlockObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E0317D2ACC32920095D843 /* TunnelStatusBlockObserver.swift */; }; A9E034642ABB302000E59A5A /* UIEdgeInsets+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E034632ABB302000E59A5A /* UIEdgeInsets+Extensions.swift */; }; E1187ABC289BBB850024E748 /* OutOfTimeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1187ABA289BBB850024E748 /* OutOfTimeViewController.swift */; }; @@ -846,7 +844,6 @@ F006CCFC2B99CC8400C6C2AC /* EditLocationsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F006CCFB2B99CC8400C6C2AC /* EditLocationsCoordinator.swift */; }; F0077EEE2C52844800DAB2AA /* KeyExchangingResultStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0FBD98E2C4A60CC00EE5323 /* KeyExchangingResultStub.swift */; }; F01528BB2BFF3FEE00B01D00 /* ShadowsocksRelaySelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = F01528BA2BFF3FEE00B01D00 /* ShadowsocksRelaySelector.swift */; }; - F0164EBA2B4456D30020268D /* AccessMethodRepositoryStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164EB92B4456D30020268D /* AccessMethodRepositoryStub.swift */; }; F0164EBC2B482E430020268D /* AppStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164EBB2B482E430020268D /* AppStorage.swift */; }; F0164EBE2B4BFF940020268D /* ShadowsocksLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164EBD2B4BFF940020268D /* ShadowsocksLoader.swift */; }; F0164EC32B4C49D30020268D /* ShadowsocksLoaderStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164EC22B4C49D30020268D /* ShadowsocksLoaderStub.swift */; }; @@ -887,6 +884,7 @@ F062B94D2C16E09700B6D47A /* TunnelSettingsManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F062B94C2C16E09700B6D47A /* TunnelSettingsManagerTests.swift */; }; F072D3CF2C07122400906F64 /* MultihopUpdaterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F072D3CE2C07122400906F64 /* MultihopUpdaterTests.swift */; }; F072D3D22C071AD100906F64 /* ShadowsocksLoaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F072D3D12C071AD100906F64 /* ShadowsocksLoaderTests.swift */; }; + F073FCB32C6617D70062EA1D /* TunnelStore+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = F073FCB22C6617D70062EA1D /* TunnelStore+Stubs.swift */; }; F07751552C50F149006E6A12 /* PostQuantumKeyExchangeActorStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C4C9BF2C495E7500A79006 /* PostQuantumKeyExchangeActorStub.swift */; }; F07751572C50F149006E6A12 /* PostQuantumKeyExchangingPipelineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F053F4B92C4A94D300FBD937 /* PostQuantumKeyExchangingPipelineTests.swift */; }; F07751582C50F149006E6A12 /* MultiHopPostQuantumKeyExchangingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C4C9BD2C49477B00A79006 /* MultiHopPostQuantumKeyExchangingTests.swift */; }; @@ -895,6 +893,7 @@ F07BF2622A26279100042943 /* RedeemVoucherOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07BF2612A26279100042943 /* RedeemVoucherOperation.swift */; }; F07C9D952B220C77006F1C5E /* libmullvad_ios.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 01F1FF1D29F0627D007083C3 /* libmullvad_ios.a */; }; F07CFF2029F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07CFF1F29F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift */; }; + F07F63CE2C63E5790027A351 /* AccessMethodRepository+Stub.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164EB92B4456D30020268D /* AccessMethodRepository+Stub.swift */; }; F08827872B318C840020A383 /* ShadowsocksCipherOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DFF7D92B02862E00F864E0 /* ShadowsocksCipherOptions.swift */; }; F08827882B318F960020A383 /* PersistentAccessMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586C0D962B04E0AC00E7CDD7 /* PersistentAccessMethod.swift */; }; F08827892B3192110020A383 /* AccessMethodRepositoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EF875A2B16385400C098B2 /* AccessMethodRepositoryProtocol.swift */; }; @@ -2041,7 +2040,6 @@ A9D9A4D32C36E1EA004088DD /* mullvad_rust_runtime.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = mullvad_rust_runtime.h; path = include/mullvad_rust_runtime.h; sourceTree = ""; }; A9E031762ACB08950095D843 /* UIApplication+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Extensions.swift"; sourceTree = ""; }; A9E031792ACB0AE70095D843 /* UIApplication+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Stubs.swift"; sourceTree = ""; }; - A9E0317B2ACBFC7E0095D843 /* TunnelStore+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TunnelStore+Stubs.swift"; sourceTree = ""; }; A9E0317D2ACC32920095D843 /* TunnelStatusBlockObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelStatusBlockObserver.swift; sourceTree = ""; }; A9E034632ABB302000E59A5A /* UIEdgeInsets+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIEdgeInsets+Extensions.swift"; sourceTree = ""; }; A9EB4F9C2B7FAB21002A2D7A /* PostQuantumKeyNegotiator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostQuantumKeyNegotiator.swift; sourceTree = ""; }; @@ -2053,7 +2051,7 @@ E1FD0DF428AA7CE400299DB4 /* StatusActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActivityView.swift; sourceTree = ""; }; F006CCFB2B99CC8400C6C2AC /* EditLocationsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditLocationsCoordinator.swift; sourceTree = ""; }; F01528BA2BFF3FEE00B01D00 /* ShadowsocksRelaySelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksRelaySelector.swift; sourceTree = ""; }; - F0164EB92B4456D30020268D /* AccessMethodRepositoryStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessMethodRepositoryStub.swift; sourceTree = ""; }; + F0164EB92B4456D30020268D /* AccessMethodRepository+Stub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccessMethodRepository+Stub.swift"; sourceTree = ""; }; F0164EBB2B482E430020268D /* AppStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStorage.swift; sourceTree = ""; }; F0164EBD2B4BFF940020268D /* ShadowsocksLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksLoader.swift; sourceTree = ""; }; F0164EC22B4C49D30020268D /* ShadowsocksLoaderStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksLoaderStub.swift; sourceTree = ""; }; @@ -2092,6 +2090,7 @@ F062B94C2C16E09700B6D47A /* TunnelSettingsManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsManagerTests.swift; sourceTree = ""; }; F072D3CE2C07122400906F64 /* MultihopUpdaterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultihopUpdaterTests.swift; sourceTree = ""; }; F072D3D12C071AD100906F64 /* ShadowsocksLoaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksLoaderTests.swift; sourceTree = ""; }; + F073FCB22C6617D70062EA1D /* TunnelStore+Stubs.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TunnelStore+Stubs.swift"; sourceTree = ""; }; F07B53562C53B5270024F547 /* LocalNetworkIPs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNetworkIPs.swift; sourceTree = ""; }; F07BF2572A26112D00042943 /* InputTextFormatterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputTextFormatterTests.swift; sourceTree = ""; }; F07BF2612A26279100042943 /* RedeemVoucherOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedeemVoucherOperation.swift; sourceTree = ""; }; @@ -2513,7 +2512,7 @@ A9A5F9A12ACB003D0083449F /* TunnelManagerTests.swift */, F0A0868F2C22D6A700BF83E7 /* TunnelSettingsStrategyTests.swift */, 44BB5F992BE529FE002520EB /* TunnelStateTests.swift */, - A9E0317B2ACBFC7E0095D843 /* TunnelStore+Stubs.swift */, + F073FCB22C6617D70062EA1D /* TunnelStore+Stubs.swift */, A9E031792ACB0AE70095D843 /* UIApplication+Stubs.swift */, 58165EBD2A262CBB00688EAD /* WgKeyRotationTests.swift */, ); @@ -3710,7 +3709,6 @@ 58FBFBE7291622580020E046 /* MullvadRESTTests */ = { isa = PBXGroup; children = ( - F0164EB92B4456D30020268D /* AccessMethodRepositoryStub.swift */, 58FBFBE8291622580020E046 /* ExponentialBackoffTests.swift */, A932D9F22B5EB61100999395 /* HeadRequestTests.swift */, 58BDEB9E2A98F6B400F578F2 /* Mocks */, @@ -4091,6 +4089,7 @@ F0ACE3172BE4E487006D5333 /* MullvadREST */ = { isa = PBXGroup; children = ( + F0164EB92B4456D30020268D /* AccessMethodRepository+Stub.swift */, A900E9BF2ACC661900C95F67 /* AccessTokenManager+Stubs.swift */, A900E9B72ACC5C2B00C95F67 /* AccountsProxy+Stubs.swift */, A900E9BD2ACC654100C95F67 /* APIProxy+Stubs.swift */, @@ -5271,7 +5270,6 @@ A9A5FA072ACB05160083449F /* SimulatorVPNConnection.swift in Sources */, 7A6F2FA52AFA3CB2006D0856 /* AccountExpiryTests.swift in Sources */, A9A5FA082ACB05160083449F /* StorePaymentBlockObserver.swift in Sources */, - A9E0317C2ACBFC7E0095D843 /* TunnelStore+Stubs.swift in Sources */, 7A516C3C2B712F0B00BBD33D /* IPOverrideWrapperTests.swift in Sources */, A9A5FA092ACB05160083449F /* SendStoreReceiptOperation.swift in Sources */, A9A5FA0A2ACB05160083449F /* StorePaymentEvent.swift in Sources */, @@ -5343,6 +5341,7 @@ A9A5FA312ACB05160083449F /* MockFileCache.swift in Sources */, A9A5FA322ACB05160083449F /* RelayCacheTests.swift in Sources */, A9A5FA332ACB05160083449F /* RelaySelectorTests.swift in Sources */, + F073FCB32C6617D70062EA1D /* TunnelStore+Stubs.swift in Sources */, 58DFF7D32B02570000F864E0 /* MarkdownStylingOptions.swift in Sources */, A9A5FA342ACB05160083449F /* StringTests.swift in Sources */, 7A52F96C2C17450C00B133B9 /* RelaySelectorWrapperTests.swift in Sources */, @@ -5972,7 +5971,6 @@ buildActionMask = 2147483647; files = ( 58B465702A98C53300467203 /* RequestExecutorTests.swift in Sources */, - F0164EBA2B4456D30020268D /* AccessMethodRepositoryStub.swift in Sources */, A917352129FAAA5200D5DCFD /* TransportStrategyTests.swift in Sources */, 58FBFBE9291622580020E046 /* ExponentialBackoffTests.swift in Sources */, F0164EC32B4C49D30020268D /* ShadowsocksLoaderStub.swift in Sources */, @@ -6070,7 +6068,6 @@ files = ( A9D9A4B12C36D10E004088DD /* ShadowSocksProxy.swift in Sources */, A9D9A4BB2C36D397004088DD /* PostQuantumKeyNegotiator.swift in Sources */, - A9D9A4D02C36DAFD004088DD /* PostQuantumKeyExchangeActor.swift in Sources */, A9D9A4B22C36D12D004088DD /* UDPOverTCPObfuscator.swift in Sources */, A9173C322C36CCDD00F6A08C /* PacketTunnelProvider+TCPConnection.swift in Sources */, F05919802C45515200C301F3 /* PostQuantumKeyExchangeActor.swift in Sources */, @@ -6096,6 +6093,7 @@ buildActionMask = 2147483647; files = ( F0ACE31D2BE4E4F2006D5333 /* DevicesProxy+Stubs.swift in Sources */, + F07F63CE2C63E5790027A351 /* AccessMethodRepository+Stub.swift in Sources */, F0ACE31E2BE4E4F2006D5333 /* AccountsProxy+Stubs.swift in Sources */, F0ACE3202BE4E4F2006D5333 /* AccessTokenManager+Stubs.swift in Sources */, F0ACE32C2BE4E77E006D5333 /* DeviceMock.swift in Sources */, diff --git a/ios/MullvadVPN/AddressCacheTracker/AddressCacheTracker.swift b/ios/MullvadVPN/AddressCacheTracker/AddressCacheTracker.swift index f36d9b6df618..048d63d0b0ae 100644 --- a/ios/MullvadVPN/AddressCacheTracker/AddressCacheTracker.swift +++ b/ios/MullvadVPN/AddressCacheTracker/AddressCacheTracker.swift @@ -103,7 +103,7 @@ final class AddressCacheTracker { operation.addObserver( BackgroundObserver( - application: application, + backgroundTaskProvider: application, name: "Update endpoints", cancelUponExpiration: true ) diff --git a/ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift b/ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift index 33b0b6cacfda..8893436cb639 100644 --- a/ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift +++ b/ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift @@ -120,7 +120,7 @@ final class RelayCacheTracker: RelayCacheTrackerProtocol { operation.addObserver( BackgroundObserver( - application: application, + backgroundTaskProvider: application, name: "Update relays", cancelUponExpiration: true ) diff --git a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift index 193f79b9878b..2bd8898e847f 100644 --- a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift +++ b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift @@ -62,6 +62,9 @@ final class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { do { setInternalStateConnected(with: try selectedRelays ?? pickRelays()) completionHandler(nil) + } catch let error where error is NoRelaysSatisfyingConstraintsError { + observedState = .error(ObservedBlockedState(reason: .noRelaysSatisfyingConstraints)) + completionHandler(error) } catch { providerLogger.error( error: error, diff --git a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorVPNConnection.swift b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorVPNConnection.swift index 6b235d285c9a..5ca0b16f1eb9 100644 --- a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorVPNConnection.swift +++ b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorVPNConnection.swift @@ -9,6 +9,7 @@ #if targetEnvironment(simulator) import Foundation +import MullvadREST import NetworkExtension class SimulatorVPNConnection: NSObject, VPNConnectionProtocol { @@ -94,6 +95,9 @@ class SimulatorVPNConnection: NSObject, VPNConnectionProtocol { if error == nil { self.status = .connected self.connectedDate = Date() + } else if error is NoRelaysSatisfyingConstraintsError { + self.reasserting = true + self.connectedDate = nil } else { self.status = .disconnected self.connectedDate = nil diff --git a/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift b/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift index 4b130109bc8c..9db60518d7b2 100644 --- a/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift +++ b/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift @@ -232,7 +232,7 @@ final class StorePaymentManager: NSObject, SKPaymentTransactionObserver { } accountOperation.addObserver(BackgroundObserver( - application: backgroundTaskProvider, + backgroundTaskProvider: backgroundTaskProvider, name: "Validate account number", cancelUponExpiration: false )) @@ -267,7 +267,7 @@ final class StorePaymentManager: NSObject, SKPaymentTransactionObserver { operation.addObserver( BackgroundObserver( - application: backgroundTaskProvider, + backgroundTaskProvider: backgroundTaskProvider, name: "Send AppStore receipt", cancelUponExpiration: true ) diff --git a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift index b605b85b476d..a93e33b0b116 100644 --- a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift +++ b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift @@ -22,7 +22,7 @@ class MapConnectionStatusOperation: AsyncOperation { private let logger = Logger(label: "TunnelManager.MapConnectionStatusOperation") - init( + required init( queue: DispatchQueue, interactor: TunnelInteractor, connectionStatus: NEVPNStatus, diff --git a/ios/MullvadVPN/TunnelManager/SendTunnelProviderMessageOperation.swift b/ios/MullvadVPN/TunnelManager/SendTunnelProviderMessageOperation.swift index cb9afb10dd01..04c574640ca6 100644 --- a/ios/MullvadVPN/TunnelManager/SendTunnelProviderMessageOperation.swift +++ b/ios/MullvadVPN/TunnelManager/SendTunnelProviderMessageOperation.swift @@ -23,7 +23,7 @@ private let defaultTimeout: Duration = .seconds(5) final class SendTunnelProviderMessageOperation: ResultOperation { typealias DecoderHandler = (Data?) throws -> Output - private let application: UIApplication + private let backgroundTaskProvider: BackgroundTaskProvider private let tunnel: any TunnelProtocol private let message: TunnelProviderMessage private let timeout: Duration @@ -38,14 +38,14 @@ final class SendTunnelProviderMessageOperation: ResultOperation init( dispatchQueue: DispatchQueue, - application: UIApplication, + backgroundTaskProvider: BackgroundTaskProvider, tunnel: any TunnelProtocol, message: TunnelProviderMessage, timeout: Duration? = nil, decoderHandler: @escaping DecoderHandler, completionHandler: CompletionHandler? ) { - self.application = application + self.backgroundTaskProvider = backgroundTaskProvider self.tunnel = tunnel self.message = message self.timeout = timeout ?? defaultTimeout @@ -60,7 +60,7 @@ final class SendTunnelProviderMessageOperation: ResultOperation addObserver( BackgroundObserver( - application: application, + backgroundTaskProvider: backgroundTaskProvider, name: "Send tunnel provider message: \(message)", cancelUponExpiration: true ) @@ -193,7 +193,7 @@ final class SendTunnelProviderMessageOperation: ResultOperation return } - guard application.backgroundTimeRemaining > timeout else { + guard backgroundTaskProvider.backgroundTimeRemaining > timeout else { finish(result: .failure(SendTunnelProviderMessageError.notEnoughBackgroundTime)) return } @@ -218,7 +218,7 @@ final class SendTunnelProviderMessageOperation: ResultOperation extension SendTunnelProviderMessageOperation where Output: Codable { convenience init( dispatchQueue: DispatchQueue, - application: UIApplication, + backgroundTaskProvider: BackgroundTaskProvider, tunnel: any TunnelProtocol, message: TunnelProviderMessage, timeout: Duration? = nil, @@ -226,7 +226,7 @@ extension SendTunnelProviderMessageOperation where Output: Codable { ) { self.init( dispatchQueue: dispatchQueue, - application: application, + backgroundTaskProvider: backgroundTaskProvider, tunnel: tunnel, message: message, timeout: timeout, @@ -245,7 +245,7 @@ extension SendTunnelProviderMessageOperation where Output: Codable { extension SendTunnelProviderMessageOperation where Output == Void { convenience init( dispatchQueue: DispatchQueue, - application: UIApplication, + backgroundTaskProvider: BackgroundTaskProvider, tunnel: any TunnelProtocol, message: TunnelProviderMessage, timeout: Duration? = nil, @@ -253,7 +253,7 @@ extension SendTunnelProviderMessageOperation where Output == Void { ) { self.init( dispatchQueue: dispatchQueue, - application: application, + backgroundTaskProvider: backgroundTaskProvider, tunnel: tunnel, message: message, timeout: timeout, diff --git a/ios/MullvadVPN/TunnelManager/Tunnel+Messaging.swift b/ios/MullvadVPN/TunnelManager/Tunnel+Messaging.swift index 04a231c5c497..5f1d731d8968 100644 --- a/ios/MullvadVPN/TunnelManager/Tunnel+Messaging.swift +++ b/ios/MullvadVPN/TunnelManager/Tunnel+Messaging.swift @@ -29,7 +29,7 @@ extension TunnelProtocol { ) -> Cancellable { let operation = SendTunnelProviderMessageOperation( dispatchQueue: dispatchQueue, - application: .shared, + backgroundTaskProvider: backgroundTaskProvider, tunnel: self, message: .reconnectTunnel(nextRelays), completionHandler: completionHandler @@ -46,7 +46,7 @@ extension TunnelProtocol { ) -> Cancellable { let operation = SendTunnelProviderMessageOperation( dispatchQueue: dispatchQueue, - application: .shared, + backgroundTaskProvider: backgroundTaskProvider, tunnel: self, message: .getTunnelStatus, completionHandler: completionHandler @@ -64,7 +64,7 @@ extension TunnelProtocol { ) -> Cancellable { let operation = SendTunnelProviderMessageOperation( dispatchQueue: dispatchQueue, - application: .shared, + backgroundTaskProvider: backgroundTaskProvider, tunnel: self, message: .sendURLRequest(proxyRequest), timeout: proxyRequestTimeout, @@ -76,7 +76,7 @@ extension TunnelProtocol { let cancelOperation = SendTunnelProviderMessageOperation( dispatchQueue: dispatchQueue, - application: .shared, + backgroundTaskProvider: backgroundTaskProvider, tunnel: self, message: .cancelURLRequest(proxyRequest.id), completionHandler: nil @@ -96,7 +96,7 @@ extension TunnelProtocol { ) -> Cancellable { let operation = SendTunnelProviderMessageOperation( dispatchQueue: dispatchQueue, - application: .shared, + backgroundTaskProvider: backgroundTaskProvider, tunnel: self, message: .privateKeyRotation, completionHandler: completionHandler diff --git a/ios/MullvadVPN/TunnelManager/Tunnel.swift b/ios/MullvadVPN/TunnelManager/Tunnel.swift index 088ff0c888a9..0b767e80d345 100644 --- a/ios/MullvadVPN/TunnelManager/Tunnel.swift +++ b/ios/MullvadVPN/TunnelManager/Tunnel.swift @@ -26,8 +26,9 @@ protocol TunnelProtocol: AnyObject { var status: NEVPNStatus { get } var isOnDemandEnabled: Bool { get set } var startDate: Date? { get } + var backgroundTaskProvider: BackgroundTaskProvider { get } - init(tunnelProvider: TunnelManagerProtocol) + init(tunnelProvider: TunnelManagerProtocol, backgroundTaskProvider: BackgroundTaskProvider) func addObserver(_ observer: any TunnelStatusObserver) func removeObserver(_ observer: any TunnelStatusObserver) @@ -52,6 +53,8 @@ final class Tunnel: TunnelProtocol, Equatable { /// Unique identifier assigned to instance at the time of creation. let identifier = UUID() + var backgroundTaskProvider: any BackgroundTaskProvider + #if DEBUG /// System VPN configuration identifier. /// This property performs a private call to obtain system configuration ID so it does not @@ -114,8 +117,9 @@ final class Tunnel: TunnelProtocol, Equatable { private var _startDate: Date? internal let tunnelProvider: TunnelProviderManagerType - init(tunnelProvider: TunnelProviderManagerType) { + init(tunnelProvider: TunnelProviderManagerType, backgroundTaskProvider: BackgroundTaskProvider) { self.tunnelProvider = tunnelProvider + self.backgroundTaskProvider = backgroundTaskProvider NotificationCenter.default.addObserver( self, selector: #selector(handleVPNStatusChangeNotification(_:)), diff --git a/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift b/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift index 3b6735bd396a..39587cb688e8 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift @@ -9,12 +9,14 @@ import Foundation import MullvadREST import MullvadSettings +import MullvadTypes import PacketTunnelCore protocol TunnelInteractor { // MARK: - Tunnel manipulation var tunnel: (any TunnelProtocol)? { get } + var backgroundTaskProvider: any BackgroundTaskProvider { get } func getPersistentTunnels() -> [any TunnelProtocol] func createNewTunnel() -> any TunnelProtocol diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift index bc3dff4fc7bb..fb03fa2f18ca 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift @@ -42,7 +42,7 @@ final class TunnelManager: StorePaymentObserver { // MARK: - Internal variables - private let application: BackgroundTaskProvider + let application: BackgroundTaskProvider fileprivate let tunnelStore: any TunnelStoreProtocol private let relayCacheTracker: RelayCacheTrackerProtocol private let accountsProxy: RESTAccountHandling @@ -204,7 +204,7 @@ final class TunnelManager: StorePaymentObserver { loadTunnelOperation.addObserver( BackgroundObserver( - application: application, + backgroundTaskProvider: application, name: "Load tunnel configuration", cancelUponExpiration: false ) @@ -244,7 +244,7 @@ final class TunnelManager: StorePaymentObserver { ) operation.addObserver(BackgroundObserver( - application: application, + backgroundTaskProvider: application, name: "Start tunnel", cancelUponExpiration: true )) @@ -279,7 +279,7 @@ final class TunnelManager: StorePaymentObserver { } operation.addObserver(BackgroundObserver( - application: application, + backgroundTaskProvider: application, name: "Stop tunnel", cancelUponExpiration: true )) @@ -315,7 +315,7 @@ final class TunnelManager: StorePaymentObserver { operation.addObserver( BackgroundObserver( - application: application, + backgroundTaskProvider: application, name: "Reconnect tunnel", cancelUponExpiration: true ) @@ -355,7 +355,7 @@ final class TunnelManager: StorePaymentObserver { } operation.addObserver(BackgroundObserver( - application: application, + backgroundTaskProvider: application, name: action.taskName, cancelUponExpiration: true )) @@ -408,7 +408,7 @@ final class TunnelManager: StorePaymentObserver { operation.addObserver( BackgroundObserver( - application: application, + backgroundTaskProvider: application, name: "Update account data", cancelUponExpiration: true ) @@ -437,7 +437,7 @@ final class TunnelManager: StorePaymentObserver { operation.addObserver( BackgroundObserver( - application: application, + backgroundTaskProvider: application, name: "Redeem voucher", cancelUponExpiration: true ) @@ -467,7 +467,7 @@ final class TunnelManager: StorePaymentObserver { operation.addObserver( BackgroundObserver( - application: application, + backgroundTaskProvider: application, name: "Update device data", cancelUponExpiration: true ) @@ -503,7 +503,7 @@ final class TunnelManager: StorePaymentObserver { operation.addObserver( BackgroundObserver( - application: application, + backgroundTaskProvider: application, name: "Rotate private key", cancelUponExpiration: true ) @@ -811,11 +811,11 @@ final class TunnelManager: StorePaymentObserver { logger.error(error: error, message: "Failed to reconnect the tunnel.") } - // Refresh tunnel status only when connecting or reasserting to pick up the next relay, + // Refresh tunnel status only when connecting,reasserting or error to pick up the next relay, // since both states may persist for a long period of time until the tunnel is fully // connected. switch tunnelStatus.state { - case .connecting, .reconnecting: + case .connecting, .reconnecting, .error: logger.debug("Refresh tunnel status due to reconnect.") refreshTunnelStatus() @@ -898,7 +898,7 @@ final class TunnelManager: StorePaymentObserver { operation.addCondition(MutuallyExclusive(category: OperationCategory.deviceStateUpdate.category)) operation.addObserver(BackgroundObserver( - application: application, + backgroundTaskProvider: application, name: "Refresh device state", cancelUponExpiration: true )) @@ -958,7 +958,7 @@ final class TunnelManager: StorePaymentObserver { } operation.addObserver(BackgroundObserver( - application: application, + backgroundTaskProvider: application, name: taskName, cancelUponExpiration: false )) @@ -994,7 +994,7 @@ final class TunnelManager: StorePaymentObserver { } operation.addObserver(BackgroundObserver( - application: application, + backgroundTaskProvider: application, name: taskName, cancelUponExpiration: false )) @@ -1204,6 +1204,10 @@ private struct TunnelInteractorProxy: TunnelInteractor { tunnelManager.tunnel } + var backgroundTaskProvider: any BackgroundTaskProvider { + tunnelManager.application + } + func getPersistentTunnels() -> [any TunnelProtocol] { tunnelManager.tunnelStore.getPersistentTunnels() } diff --git a/ios/MullvadVPN/TunnelManager/TunnelStore.swift b/ios/MullvadVPN/TunnelManager/TunnelStore.swift index 7c9741a7b9a0..705a1f0ab28b 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelStore.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelStore.swift @@ -23,6 +23,7 @@ final class TunnelStore: TunnelStoreProtocol, TunnelStatusObserver { typealias TunnelType = Tunnel private let logger = Logger(label: "TunnelStore") private let lock = NSLock() + private let application: BackgroundTaskProvider /// Persistent tunnels registered with the system. private var persistentTunnels: [TunnelType] = [] @@ -30,7 +31,8 @@ final class TunnelStore: TunnelStoreProtocol, TunnelStatusObserver { /// Newly created tunnels, stored as collection of weak boxes. private var newTunnels: [WeakBox] = [] - init(application: UIApplication) { + init(application: BackgroundTaskProvider) { + self.application = application NotificationCenter.default.addObserver( self, selector: #selector(applicationDidBecomeActive(_:)), @@ -62,7 +64,7 @@ final class TunnelStore: TunnelStoreProtocol, TunnelStatusObserver { } self.persistentTunnels = managers?.map { manager in - let tunnel = Tunnel(tunnelProvider: manager) + let tunnel = Tunnel(tunnelProvider: manager, backgroundTaskProvider: self.application) tunnel.addObserver(self) self.logger.debug( @@ -79,7 +81,7 @@ final class TunnelStore: TunnelStoreProtocol, TunnelStatusObserver { defer { lock.unlock() } let tunnelProviderManager = TunnelProviderManagerType() - let tunnel = TunnelType(tunnelProvider: tunnelProviderManager) + let tunnel = TunnelType(tunnelProvider: tunnelProviderManager, backgroundTaskProvider: application) tunnel.addObserver(self) newTunnels = newTunnels.filter { $0.value != nil } diff --git a/ios/MullvadVPN/TunnelManager/UIApplication+Extensions.swift b/ios/MullvadVPN/TunnelManager/UIApplication+Extensions.swift index 046244582c75..d1a39e40263f 100644 --- a/ios/MullvadVPN/TunnelManager/UIApplication+Extensions.swift +++ b/ios/MullvadVPN/TunnelManager/UIApplication+Extensions.swift @@ -12,6 +12,7 @@ import Foundation import UIKit public protocol BackgroundTaskProvider { + var backgroundTimeRemaining: TimeInterval { get } func endBackgroundTask(_ identifier: UIBackgroundTaskIdentifier) func beginBackgroundTask( diff --git a/ios/MullvadVPNTests/MullvadREST/Shadowsocks/ShadowsocksLoaderTests.swift b/ios/MullvadVPNTests/MullvadREST/Shadowsocks/ShadowsocksLoaderTests.swift index da049b95cdab..253e9545129f 100644 --- a/ios/MullvadVPNTests/MullvadREST/Shadowsocks/ShadowsocksLoaderTests.swift +++ b/ios/MullvadVPNTests/MullvadREST/Shadowsocks/ShadowsocksLoaderTests.swift @@ -94,7 +94,7 @@ class ShadowsocksLoaderTests: XCTestCase { } } -private class ShadowsocksRelaySelectorStub: ShadowsocksRelaySelectorProtocol { +class ShadowsocksRelaySelectorStub: ShadowsocksRelaySelectorProtocol { var entryBridgeResult: Result = .failure(ShadowsocksRelaySelectorStubError()) var exitBridgeResult: Result = .failure(ShadowsocksRelaySelectorStubError()) private let relays: REST.ServerRelaysResponse @@ -117,7 +117,7 @@ private class ShadowsocksRelaySelectorStub: ShadowsocksRelaySelectorProtocol { } } -private class ShadowsocksConfigurationCacheStub: ShadowsocksConfigurationCacheProtocol { +class ShadowsocksConfigurationCacheStub: ShadowsocksConfigurationCacheProtocol { private(set) var cachedConfiguration: ShadowsocksConfiguration? func read() throws -> ShadowsocksConfiguration { diff --git a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/MockTunnel.swift b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/MockTunnel.swift index b9daa63c876d..302e4ca2f755 100644 --- a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/MockTunnel.swift +++ b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/MockTunnel.swift @@ -7,6 +7,7 @@ // import Foundation +import MullvadTypes import NetworkExtension class MockTunnel: TunnelProtocol { @@ -18,10 +19,13 @@ class MockTunnel: TunnelProtocol { var startDate: Date? - required init(tunnelProvider: TunnelManagerProtocol) { + var backgroundTaskProvider: any BackgroundTaskProvider + + required init(tunnelProvider: TunnelManagerProtocol, backgroundTaskProvider: BackgroundTaskProvider) { status = .disconnected isOnDemandEnabled = false startDate = nil + self.backgroundTaskProvider = backgroundTaskProvider } // Observers are currently unimplemented diff --git a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/MockTunnelInteractor.swift b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/MockTunnelInteractor.swift index 3da521592243..6f8999b19cd2 100644 --- a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/MockTunnelInteractor.swift +++ b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/MockTunnelInteractor.swift @@ -9,6 +9,7 @@ import Foundation import MullvadREST import MullvadSettings +import MullvadTypes // this is still very minimal, and will be fleshed out as needed. class MockTunnelInteractor: TunnelInteractor { @@ -22,6 +23,10 @@ class MockTunnelInteractor: TunnelInteractor { var tunnel: (any TunnelProtocol)? + var backgroundTaskProvider: any BackgroundTaskProvider { + UIApplicationStub() + } + init( isConfigurationLoaded: Bool, settings: LatestTunnelSettings, @@ -41,7 +46,10 @@ class MockTunnelInteractor: TunnelInteractor { } func createNewTunnel() -> any TunnelProtocol { - return MockTunnel(tunnelProvider: SimulatorTunnelProviderManager()) + return MockTunnel( + tunnelProvider: SimulatorTunnelProviderManager(), + backgroundTaskProvider: backgroundTaskProvider + ) } func setTunnel(_ tunnel: (any TunnelProtocol)?, shouldRefreshTunnelState: Bool) { diff --git a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift index 3b9dff23d06d..806cc7a412ec 100644 --- a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift +++ b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift @@ -14,8 +14,18 @@ import MullvadREST import XCTest -final class TunnelManagerTests: XCTestCase { +class TunnelManagerTests: XCTestCase { static let store = InMemorySettingsStore() + private var tunnelObserver: TunnelObserver! + + var application: UIApplicationStub! + var relayCacheTracker: RelayCacheTrackerStub! + var accountProxy: AccountsProxyStub! + var accessTokenManager: AccessTokenManagerStub! + var devicesProxy: DevicesProxyStub! + var apiProxy: APIProxyStub! + + var transportProvider: TransportProvider! override class func setUp() { SettingsManager.unitTestStore = store @@ -25,65 +35,100 @@ final class TunnelManagerTests: XCTestCase { SettingsManager.unitTestStore = nil } - func testTunnelManager() { - let application = UIApplicationStub() - let tunnelStore = TunnelStoreStub() - let relayCacheTracker = RelayCacheTrackerStub() - let accountProxy = AccountsProxyStub() - let devicesProxy = DevicesProxyStub(deviceResult: .success(Device.mock(publicKey: PrivateKey().publicKey))) - let apiProxy = APIProxyStub() - let accessTokenManager = AccessTokenManagerStub() - let relaySelector = RelaySelectorStub.nonFallible() + override func setUp() async throws { + application = UIApplicationStub() + relayCacheTracker = RelayCacheTrackerStub() + accountProxy = AccountsProxyStub() + accessTokenManager = AccessTokenManagerStub() + devicesProxy = DevicesProxyStub(deviceResult: .success(Device.mock(publicKey: PrivateKey().publicKey))) + apiProxy = APIProxyStub() + + transportProvider = TransportProvider( + urlSessionTransport: URLSessionTransport(urlSession: REST.makeURLSession()), + addressCache: REST.AddressCache( + canWriteToCache: true, + cacheDirectory: FileManager.default.temporaryDirectory + ), + transportStrategy: TransportStrategy( + datasource: AccessMethodRepositoryStub(accessMethods: [PersistentAccessMethod( + id: UUID(), + name: "direct", + isEnabled: true, + proxyConfiguration: .direct + )]), + shadowsocksLoader: ShadowsocksLoader( + cache: ShadowsocksConfigurationCacheStub(), + relaySelector: ShadowsocksRelaySelectorStub(relays: .mock()), + constraintsUpdater: RelayConstraintsUpdater(), + multihopUpdater: MultihopUpdater(listener: MultihopStateListener()) + ) + ) + ) + + try SettingsManager.writeSettings(LatestTunnelSettings()) + } + + override func tearDown() async throws { + application = nil + relayCacheTracker = nil + accountProxy = nil + accessTokenManager = nil + devicesProxy = nil + apiProxy = nil + transportProvider = nil + tunnelObserver = nil + } + + func testLogInStartsKeyRotations() async throws { + accountProxy.createAccountResult = .success(REST.NewAccountData.mockValue()) + let tunnelManager = TunnelManager( application: application, - tunnelStore: tunnelStore, + tunnelStore: TunnelStoreStub(backgroundTaskProvider: application), relayCacheTracker: relayCacheTracker, accountsProxy: accountProxy, devicesProxy: devicesProxy, apiProxy: apiProxy, accessTokenManager: accessTokenManager, - relaySelector: relaySelector + relaySelector: RelaySelectorStub.nonFallible() ) - XCTAssertNotNil(tunnelManager) + + _ = try await tunnelManager.setNewAccount() + XCTAssertEqual(tunnelManager.isRunningPeriodicPrivateKeyRotation, true) } - func testLogInStartsKeyRotations() async throws { - let application = UIApplicationStub() - let tunnelStore = TunnelStoreStub() - let relayCacheTracker = RelayCacheTrackerStub() - var accountProxy = AccountsProxyStub() - let devicesProxy = DevicesProxyStub(deviceResult: .success(Device.mock(publicKey: PrivateKey().publicKey))) - let apiProxy = APIProxyStub() - let accessTokenManager = AccessTokenManagerStub() + func testLogOutStopsKeyRotations() async throws { accountProxy.createAccountResult = .success(REST.NewAccountData.mockValue()) - let relaySelector = RelaySelectorStub.nonFallible() + let tunnelManager = TunnelManager( application: application, - tunnelStore: tunnelStore, + tunnelStore: TunnelStoreStub(backgroundTaskProvider: application), relayCacheTracker: relayCacheTracker, accountsProxy: accountProxy, devicesProxy: devicesProxy, apiProxy: apiProxy, accessTokenManager: accessTokenManager, - relaySelector: relaySelector + relaySelector: RelaySelectorStub.nonFallible() ) _ = try await tunnelManager.setNewAccount() - XCTAssertEqual(tunnelManager.isRunningPeriodicPrivateKeyRotation, true) + await tunnelManager.unsetAccount() + XCTAssertEqual(tunnelManager.isRunningPeriodicPrivateKeyRotation, false) } - func testLogOutStopsKeyRotations() async throws { - let application = UIApplicationStub() - let tunnelStore = TunnelStoreStub() - let relayCacheTracker = RelayCacheTrackerStub() - var accountProxy = AccountsProxyStub() - let devicesProxy = DevicesProxyStub(deviceResult: .success(Device.mock(publicKey: PrivateKey().publicKey))) - let apiProxy = APIProxyStub() - let accessTokenManager = AccessTokenManagerStub() + /// This test verifies tunnel gets out of `blockedState` after constraints are satisfied. + func testExitBlockedStateAfterSatisfyingConstraints() async throws { + let blockedExpectation = expectation(description: "Relay constraints aren't satisfied!") + let connectedExpectation = expectation(description: "Connected!") + accountProxy.createAccountResult = .success(REST.NewAccountData.mockValue()) - let relaySelector = RelaySelectorStub.nonFallible() + + let relaySelector = RelaySelectorStub { _, _ in + try RelaySelectorStub.unsatisfied().selectRelays(with: RelayConstraints(), connectionAttemptCount: 0) + } + let tunnelManager = TunnelManager( application: application, - tunnelStore: tunnelStore, + tunnelStore: TunnelStore(application: application), relayCacheTracker: relayCacheTracker, accountsProxy: accountProxy, devicesProxy: devicesProxy, @@ -91,8 +136,47 @@ final class TunnelManagerTests: XCTestCase { accessTokenManager: accessTokenManager, relaySelector: relaySelector ) + + let simulatorTunnelProviderHost = SimulatorTunnelProviderHost( + relaySelector: relaySelector, + transportProvider: transportProvider + ) + SimulatorTunnelProvider.shared.delegate = simulatorTunnelProviderHost + + let tunnelObserver = TunnelBlockObserver( + didUpdateTunnelStatus: { _, tunnelStatus in + switch tunnelStatus.state { + case let .error(blockedStateReason) where blockedStateReason == .noRelaysSatisfyingConstraints: + blockedExpectation.fulfill() + relaySelector.selectedRelaysResult = { relayConstraints, connectionAttemptCount in + try RelaySelectorStub.nonFallible().selectRelays( + with: relayConstraints, + connectionAttemptCount: connectionAttemptCount + ) + } + tunnelManager.reconnectTunnel(selectNewRelay: true) + + case .connected: + connectedExpectation.fulfill() + default: + return + } + } + ) + + self.tunnelObserver = tunnelObserver + tunnelManager.addObserver(tunnelObserver) + _ = try await tunnelManager.setNewAccount() - await tunnelManager.unsetAccount() - XCTAssertEqual(tunnelManager.isRunningPeriodicPrivateKeyRotation, false) + + XCTAssertTrue(tunnelManager.deviceState.isLoggedIn) + + tunnelManager.startTunnel() + + await fulfillment( + of: [blockedExpectation, connectedExpectation], + timeout: .UnitTest.timeout, + enforceOrder: true + ) } } diff --git a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelStore+Stubs.swift b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelStore+Stubs.swift index 546583942628..cc89fd776b43 100644 --- a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelStore+Stubs.swift +++ b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelStore+Stubs.swift @@ -7,16 +7,18 @@ // import Foundation +import MullvadTypes import NetworkExtension struct TunnelStoreStub: TunnelStoreProtocol { typealias TunnelType = TunnelStub + let backgroundTaskProvider: any BackgroundTaskProvider func getPersistentTunnels() -> [TunnelType] { [] } func createNewTunnel() -> TunnelType { - TunnelStub(status: .invalid, isOnDemandEnabled: false) + TunnelStub(backgroundTaskProvider: backgroundTaskProvider, status: .invalid, isOnDemandEnabled: false) } } @@ -25,24 +27,37 @@ class DummyTunnelStatusObserver: TunnelStatusObserver { } final class TunnelStub: TunnelProtocol, Equatable { - convenience init(tunnelProvider: TunnelProviderManagerType) { - self.init(status: .invalid, isOnDemandEnabled: false) - } + typealias TunnelManagerProtocol = SimulatorTunnelProviderManager static func == (lhs: TunnelStub, rhs: TunnelStub) -> Bool { ObjectIdentifier(lhs) == ObjectIdentifier(rhs) } - init(status: NEVPNStatus, isOnDemandEnabled: Bool, startDate: Date? = nil) { + convenience init( + tunnelProvider: SimulatorTunnelProviderManager, + backgroundTaskProvider: any BackgroundTaskProvider + ) { + self.init(backgroundTaskProvider: backgroundTaskProvider, status: .invalid, isOnDemandEnabled: false) + } + + init( + backgroundTaskProvider: any BackgroundTaskProvider, + status: NEVPNStatus, + isOnDemandEnabled: Bool, + startDate: Date? = nil + ) { self.status = status self.isOnDemandEnabled = isOnDemandEnabled self.startDate = startDate + self.backgroundTaskProvider = backgroundTaskProvider } func addObserver(_ observer: TunnelStatusObserver) {} func removeObserver(_ observer: TunnelStatusObserver) {} + var backgroundTaskProvider: any BackgroundTaskProvider + var status: NEVPNStatus var isOnDemandEnabled: Bool diff --git a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/UIApplication+Stubs.swift b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/UIApplication+Stubs.swift index 8b8fbe4999ca..a729c88a1857 100644 --- a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/UIApplication+Stubs.swift +++ b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/UIApplication+Stubs.swift @@ -12,6 +12,8 @@ import UIKit @testable import MullvadTypes struct UIApplicationStub: BackgroundTaskProvider { + var backgroundTimeRemaining: TimeInterval { .infinity } + func endBackgroundTask(_ identifier: UIBackgroundTaskIdentifier) {} func beginBackgroundTask( diff --git a/ios/MullvadVPNUITests/ConnectivityTests.swift b/ios/MullvadVPNUITests/ConnectivityTests.swift index a46042fddd9a..32a219d05e83 100644 --- a/ios/MullvadVPNUITests/ConnectivityTests.swift +++ b/ios/MullvadVPNUITests/ConnectivityTests.swift @@ -51,6 +51,7 @@ class ConnectivityTests: LoggedOutUITestCase { } /// Get the app into a blocked state by connecting to a relay then applying a filter which don't find this relay, then verify that app can still communicate by logging out and verifying that the device was successfully removed + // swiftlint:disable:next function_body_length func testAPIReachableWhenBlocked() throws { let hasTimeAccountNumber = getAccountWithTime() addTeardownBlock { diff --git a/ios/Operations/BackgroundObserver.swift b/ios/Operations/BackgroundObserver.swift index 0dcbcb3287f2..2b9181a0737c 100644 --- a/ios/Operations/BackgroundObserver.swift +++ b/ios/Operations/BackgroundObserver.swift @@ -14,13 +14,13 @@ import UIKit @available(iOSApplicationExtension, unavailable) public final class BackgroundObserver: OperationObserver { public let name: String - public let application: BackgroundTaskProvider + public let backgroundTaskProvider: BackgroundTaskProvider public let cancelUponExpiration: Bool private var taskIdentifier: UIBackgroundTaskIdentifier? - public init(application: BackgroundTaskProvider, name: String, cancelUponExpiration: Bool) { - self.application = application + public init(backgroundTaskProvider: BackgroundTaskProvider, name: String, cancelUponExpiration: Bool) { + self.backgroundTaskProvider = backgroundTaskProvider self.name = name self.cancelUponExpiration = cancelUponExpiration } @@ -28,7 +28,7 @@ public final class BackgroundObserver: OperationObserver { public func didAttach(to operation: Operation) { let expirationHandler = cancelUponExpiration ? { operation.cancel() } : nil - taskIdentifier = application.beginBackgroundTask( + taskIdentifier = backgroundTaskProvider.beginBackgroundTask( withName: name, expirationHandler: expirationHandler ) @@ -44,7 +44,7 @@ public final class BackgroundObserver: OperationObserver { public func operationDidFinish(_ operation: Operation, error: Error?) { if let taskIdentifier { - application.endBackgroundTask(taskIdentifier) + backgroundTaskProvider.endBackgroundTask(taskIdentifier) } } } diff --git a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift index 2cb726c962f0..c6b7a7e8cd24 100644 --- a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift @@ -35,6 +35,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { PostQuantumKeyReceiver(tunnelProvider: self) }() + // swiftlint:disable:next function_body_length override init() { Self.configureLogging() providerLogger = Logger(label: "PacketTunnelProvider") diff --git a/ios/PacketTunnelCore/Actor/ObservedState.swift b/ios/PacketTunnelCore/Actor/ObservedState.swift index 43d99fdfea6b..f81602019aa4 100644 --- a/ios/PacketTunnelCore/Actor/ObservedState.swift +++ b/ios/PacketTunnelCore/Actor/ObservedState.swift @@ -65,6 +65,11 @@ public struct ObservedConnectionState: Equatable, Codable { public struct ObservedBlockedState: Equatable, Codable { public var reason: BlockedStateReason public var relayConstraints: RelayConstraints? + + public init(reason: BlockedStateReason, relayConstraints: RelayConstraints? = nil) { + self.reason = reason + self.relayConstraints = relayConstraints + } } extension State { diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor+PostQuantum.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor+PostQuantum.swift index 4793193d3c14..9f996904146a 100644 --- a/ios/PacketTunnelCore/Actor/PacketTunnelActor+PostQuantum.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor+PostQuantum.swift @@ -29,8 +29,7 @@ extension PacketTunnelActor { Called on receipt of the new PQ-negotiated key, to reconnect to the relay, in PQ-secure mode. */ internal func postQuantumConnect() async { - guard let connectionData = state.connectionData - else { + guard let connectionData = state.connectionData else { logger.error("Could not create connection state in PostQuantumConnect") eventChannel.send(.reconnect(.current)) return diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift index f8c0f5997e95..86d6baae72bd 100644 --- a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift @@ -289,13 +289,6 @@ extension PacketTunnelActor { let activeKey = activeKey(from: connectionState, in: settings) - switch targetState { - case .connecting: - state = .connecting(connectionState) - case .reconnecting: - state = .reconnecting(connectionState) - } - let entryConfiguration: TunnelAdapterConfiguration? = if connectionState.selectedRelays.entry != nil { try ConfigurationBuilder( privateKey: activeKey, @@ -343,6 +336,13 @@ extension PacketTunnelActor { // Resume tunnel monitoring and use IPv4 gateway as a probe address. tunnelMonitor.start(probeAddress: connectionState.selectedRelays.exit.endpoint.ipv4Gateway) + + switch targetState { + case .connecting: + state = .connecting(connectionState) + case .reconnecting: + state = .reconnecting(connectionState) + } } /** @@ -355,6 +355,7 @@ extension PacketTunnelActor { - Returns: New connection state or `nil` if current state is at or past `.disconnecting` phase. */ + // swiftlint:disable:next function_body_length internal func makeConnectionState( nextRelays: NextRelays, settings: Settings, diff --git a/ios/PacketTunnelCoreTests/Mocks/PostQuantumKeyExchangingUpdaterStub.swift b/ios/PacketTunnelCoreTests/Mocks/PostQuantumKeyExchangingUpdaterStub.swift index a49b9d0c44d7..5640b1603361 100644 --- a/ios/PacketTunnelCoreTests/Mocks/PostQuantumKeyExchangingUpdaterStub.swift +++ b/ios/PacketTunnelCoreTests/Mocks/PostQuantumKeyExchangingUpdaterStub.swift @@ -11,5 +11,5 @@ import Foundation @testable import PacketTunnelCore final class PostQuantumKeyExchangingUpdaterStub: PostQuantumKeyExchangingUpdaterProtocol { - var reconfigurationHandler: ConfigUpdater? = nil + var reconfigurationHandler: ConfigUpdater? }