summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj20
-rw-r--r--ios/MullvadVPN/TunnelManager/Tunnel.swift3
-rw-r--r--ios/MullvadVPNTests/Mocks/MockTunnel.swift60
-rw-r--r--ios/MullvadVPNTests/Mocks/MockTunnelInteractor.swift79
-rw-r--r--ios/MullvadVPNTests/StartTunnelOperationTests.swift100
-rw-r--r--ios/Shared/ApplicationTarget.swift5
6 files changed, 264 insertions, 3 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 17c25c9e92..683962d1d3 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -38,6 +38,9 @@
06799AFC28F98EE300ACD94E /* AddressCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06AC114128F8413A0037AF9A /* AddressCache.swift */; };
0697D6E728F01513007A9E99 /* TransportMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0697D6E628F01513007A9E99 /* TransportMonitor.swift */; };
06AC116228F94C450037AF9A /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; };
+ 44DD7D242B6CFFD70005F67F /* StartTunnelOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44DD7D232B6CFFD70005F67F /* StartTunnelOperationTests.swift */; };
+ 44DD7D272B6D18FB0005F67F /* MockTunnelInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44DD7D262B6D18FB0005F67F /* MockTunnelInteractor.swift */; };
+ 44DD7D292B7113CA0005F67F /* MockTunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44DD7D282B7113CA0005F67F /* MockTunnel.swift */; };
5803B4B02940A47300C23744 /* TunnelConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5803B4AF2940A47300C23744 /* TunnelConfiguration.swift */; };
5803B4B22940A48700C23744 /* TunnelStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5803B4B12940A48700C23744 /* TunnelStore.swift */; };
5807E2C02432038B00F5FF30 /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2BF2432038B00F5FF30 /* String+Split.swift */; };
@@ -1238,6 +1241,9 @@
06FAE67A28F83CA50033DD93 /* RESTDevicesProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RESTDevicesProxy.swift; sourceTree = "<group>"; };
06FAE67B28F83CA50033DD93 /* REST.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = REST.swift; sourceTree = "<group>"; };
06FAE67D28F83CA50033DD93 /* RESTTransport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RESTTransport.swift; sourceTree = "<group>"; };
+ 44DD7D232B6CFFD70005F67F /* StartTunnelOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartTunnelOperationTests.swift; sourceTree = "<group>"; };
+ 44DD7D262B6D18FB0005F67F /* MockTunnelInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTunnelInteractor.swift; sourceTree = "<group>"; };
+ 44DD7D282B7113CA0005F67F /* MockTunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTunnel.swift; sourceTree = "<group>"; };
5802EBC42A8E44AC00E5CE4C /* AppRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRoutes.swift; sourceTree = "<group>"; };
5802EBC62A8E457A00E5CE4C /* AppRouteProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouteProtocol.swift; sourceTree = "<group>"; };
5802EBC82A8E45BA00E5CE4C /* ApplicationRouterDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationRouterDelegate.swift; sourceTree = "<group>"; };
@@ -2094,6 +2100,15 @@
path = MullvadREST;
sourceTree = "<group>";
};
+ 44DD7D252B6D18E90005F67F /* Mocks */ = {
+ isa = PBXGroup;
+ children = (
+ 44DD7D282B7113CA0005F67F /* MockTunnel.swift */,
+ 44DD7D262B6D18FB0005F67F /* MockTunnelInteractor.swift */,
+ );
+ path = Mocks;
+ sourceTree = "<group>";
+ };
5802EBC32A8E447000E5CE4C /* Router */ = {
isa = PBXGroup;
children = (
@@ -2737,6 +2752,7 @@
58B0A2A1238EE67E00BC001D /* MullvadVPNTests */ = {
isa = PBXGroup;
children = (
+ 44DD7D252B6D18E90005F67F /* Mocks */,
A900E9BF2ACC661900C95F67 /* AccessTokenManager+Stubs.swift */,
7A6F2FA42AFA3CB2006D0856 /* AccountExpiryTests.swift */,
A900E9B72ACC5C2B00C95F67 /* AccountsProxy+Stubs.swift */,
@@ -2767,6 +2783,7 @@
584B26F3237434D00073B10E /* RelaySelectorTests.swift */,
A900E9B92ACC5D0600C95F67 /* RESTRequestExecutor+Stubs.swift */,
A9C342C42ACC42130045F00E /* ServerRelaysResponse+Stubs.swift */,
+ 44DD7D232B6CFFD70005F67F /* StartTunnelOperationTests.swift */,
5807E2C1243203D000F5FF30 /* StringTests.swift */,
A9A5F9A12ACB003D0083449F /* TunnelManagerTests.swift */,
A9E0317B2ACBFC7E0095D843 /* TunnelStore+Stubs.swift */,
@@ -4524,6 +4541,7 @@
A9A5FA3D2ACB05D90083449F /* DeviceCheck.swift in Sources */,
A900E9B82ACC5C2B00C95F67 /* AccountsProxy+Stubs.swift in Sources */,
A9A5FA3E2ACB05D90083449F /* DeviceCheckOperation.swift in Sources */,
+ 44DD7D272B6D18FB0005F67F /* MockTunnelInteractor.swift in Sources */,
A9A5FA3F2ACB05D90083449F /* DeviceCheckRemoteService.swift in Sources */,
A9A5FA402ACB05D90083449F /* DeviceCheckRemoteServiceProtocol.swift in Sources */,
A9A5FA412ACB05D90083449F /* DeviceStateAccessor.swift in Sources */,
@@ -4600,6 +4618,7 @@
A9A5FA112ACB05160083449F /* TransportMonitor.swift in Sources */,
A9B6AC1A2ADE8FBB00F7802A /* InMemorySettingsStore.swift in Sources */,
A9A5FA132ACB05160083449F /* LoadTunnelConfigurationOperation.swift in Sources */,
+ 44DD7D292B7113CA0005F67F /* MockTunnel.swift in Sources */,
A9A5FA142ACB05160083449F /* MapConnectionStatusOperation.swift in Sources */,
A9A5FA152ACB05160083449F /* RedeemVoucherOperation.swift in Sources */,
A9A5FA162ACB05160083449F /* RotateKeyOperation.swift in Sources */,
@@ -4632,6 +4651,7 @@
A9A5FA292ACB05160083449F /* AddressCacheTests.swift in Sources */,
A9B6AC182ADE8F4300F7802A /* MigrationManagerTests.swift in Sources */,
A9A5FA2A2ACB05160083449F /* CoordinatesTests.swift in Sources */,
+ 44DD7D242B6CFFD70005F67F /* StartTunnelOperationTests.swift in Sources */,
A9A5FA2B2ACB05160083449F /* CustomDateComponentsFormattingTests.swift in Sources */,
A9A5FA2C2ACB05160083449F /* DeviceCheckOperationTests.swift in Sources */,
A9A5FA2D2ACB05160083449F /* DurationTests.swift in Sources */,
diff --git a/ios/MullvadVPN/TunnelManager/Tunnel.swift b/ios/MullvadVPN/TunnelManager/Tunnel.swift
index 1ff8f9179b..5b85473e83 100644
--- a/ios/MullvadVPN/TunnelManager/Tunnel.swift
+++ b/ios/MullvadVPN/TunnelManager/Tunnel.swift
@@ -21,11 +21,12 @@ protocol TunnelStatusObserver {
}
protocol TunnelProtocol: AnyObject {
+ associatedtype TunnelManagerProtocol: VPNTunnelProviderManagerProtocol
var status: NEVPNStatus { get }
var isOnDemandEnabled: Bool { get set }
var startDate: Date? { get }
- init(tunnelProvider: TunnelProviderManagerType)
+ init(tunnelProvider: TunnelManagerProtocol)
func addObserver(_ observer: any TunnelStatusObserver)
func removeObserver(_ observer: any TunnelStatusObserver)
diff --git a/ios/MullvadVPNTests/Mocks/MockTunnel.swift b/ios/MullvadVPNTests/Mocks/MockTunnel.swift
new file mode 100644
index 0000000000..b9daa63c87
--- /dev/null
+++ b/ios/MullvadVPNTests/Mocks/MockTunnel.swift
@@ -0,0 +1,60 @@
+//
+// MockTunnel.swift
+// MullvadVPNTests
+//
+// Created by Andrew Bulhak on 2024-02-05.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import NetworkExtension
+
+class MockTunnel: TunnelProtocol {
+ typealias TunnelManagerProtocol = SimulatorTunnelProviderManager
+
+ var status: NEVPNStatus
+
+ var isOnDemandEnabled: Bool
+
+ var startDate: Date?
+
+ required init(tunnelProvider: TunnelManagerProtocol) {
+ status = .disconnected
+ isOnDemandEnabled = false
+ startDate = nil
+ }
+
+ // Observers are currently unimplemented
+ func addObserver(_ observer: TunnelStatusObserver) {}
+
+ func removeObserver(_ observer: TunnelStatusObserver) {}
+
+ func addBlockObserver(
+ queue: DispatchQueue?,
+ handler: @escaping (any TunnelProtocol, NEVPNStatus) -> Void
+ ) -> TunnelStatusBlockObserver {
+ fatalError("MockTunnel.addBlockObserver Not implemented")
+ }
+
+ func logFormat() -> String {
+ ""
+ }
+
+ func saveToPreferences(_ completion: @escaping (Error?) -> Void) {
+ completion(nil)
+ }
+
+ func removeFromPreferences(completion: @escaping (Error?) -> Void) {
+ completion(nil)
+ }
+
+ func setConfiguration(_ configuration: TunnelConfiguration) {}
+
+ func start(options: [String: NSObject]?) throws {
+ startDate = Date()
+ }
+
+ func stop() {}
+
+ func sendProviderMessage(_ messageData: Data, responseHandler: ((Data?) -> Void)?) throws {}
+}
diff --git a/ios/MullvadVPNTests/Mocks/MockTunnelInteractor.swift b/ios/MullvadVPNTests/Mocks/MockTunnelInteractor.swift
new file mode 100644
index 0000000000..49784143e8
--- /dev/null
+++ b/ios/MullvadVPNTests/Mocks/MockTunnelInteractor.swift
@@ -0,0 +1,79 @@
+//
+// MockTunnelInteractor.swift
+// MullvadVPNTests
+//
+// Created by Andrew Bulhak on 2024-02-02.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadSettings
+import PacketTunnelCore
+
+// this is still very minimal, and will be fleshed out as needed.
+class MockTunnelInteractor: TunnelInteractor {
+ var isConfigurationLoaded: Bool
+
+ var settings: MullvadSettings.LatestTunnelSettings
+
+ var deviceState: MullvadSettings.DeviceState
+
+ var onUpdateTunnelStatus: ((TunnelStatus) -> Void)?
+
+ var tunnel: (any TunnelProtocol)?
+
+ init(
+ isConfigurationLoaded: Bool,
+ settings: MullvadSettings.LatestTunnelSettings,
+ deviceState: MullvadSettings.DeviceState,
+ onUpdateTunnelStatus: ((TunnelStatus) -> Void)? = nil
+ ) {
+ self.isConfigurationLoaded = isConfigurationLoaded
+ self.settings = settings
+ self.deviceState = deviceState
+ self.onUpdateTunnelStatus = onUpdateTunnelStatus
+ self.tunnel = nil
+ self.tunnelStatus = TunnelStatus()
+ }
+
+ func getPersistentTunnels() -> [any TunnelProtocol] {
+ return []
+ }
+
+ func createNewTunnel() -> any TunnelProtocol {
+ return MockTunnel(tunnelProvider: SimulatorTunnelProviderManager())
+ }
+
+ func setTunnel(_ tunnel: (any TunnelProtocol)?, shouldRefreshTunnelState: Bool) {
+ self.tunnel = tunnel
+ }
+
+ var tunnelStatus: TunnelStatus
+
+ func updateTunnelStatus(_ block: (inout TunnelStatus) -> Void) -> TunnelStatus {
+ var tunnelStatus = self.tunnelStatus
+ block(&tunnelStatus)
+ onUpdateTunnelStatus?(tunnelStatus)
+ return tunnelStatus
+ }
+
+ func setConfigurationLoaded() {}
+
+ func setSettings(_ settings: MullvadSettings.LatestTunnelSettings, persist: Bool) {}
+
+ func setDeviceState(_ deviceState: MullvadSettings.DeviceState, persist: Bool) {}
+
+ func removeLastUsedAccount() {}
+
+ func handleRestError(_ error: Error) {}
+
+ func startTunnel() {}
+
+ func prepareForVPNConfigurationDeletion() {}
+
+ struct NotImplementedError: Error {}
+
+ func selectRelay() throws -> PacketTunnelCore.SelectedRelay {
+ throw NotImplementedError()
+ }
+}
diff --git a/ios/MullvadVPNTests/StartTunnelOperationTests.swift b/ios/MullvadVPNTests/StartTunnelOperationTests.swift
new file mode 100644
index 0000000000..5dfd5d904f
--- /dev/null
+++ b/ios/MullvadVPNTests/StartTunnelOperationTests.swift
@@ -0,0 +1,100 @@
+//
+// StartTunnelOperationTests.swift
+// MullvadVPNTests
+//
+// Created by Andrew Bulhak on 2024-02-02.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import MullvadSettings
+import Network
+import Operations
+import WireGuardKitTypes
+import XCTest
+
+class StartTunnelOperationTests: XCTestCase {
+ // MARK: utility code for setting up tests
+
+ let testQueue = DispatchQueue(label: "StartTunnelOperationTests.testQueue")
+ let operationQueue = AsyncOperationQueue()
+
+ let loggedInDeviceState = DeviceState.loggedIn(
+ StoredAccountData(
+ identifier: "",
+ number: "",
+ expiry: .distantFuture
+ ),
+ StoredDeviceData(
+ creationDate: Date(),
+ identifier: "",
+ name: "",
+ hijackDNS: false,
+ ipv4Address: IPAddressRange(from: "127.0.0.1/32")!,
+ ipv6Address: IPAddressRange(from: "::ff/64")!,
+ wgKeyData: StoredWgKeyData(creationDate: Date(), privateKey: PrivateKey())
+ )
+ )
+
+ func makeInteractor(deviceState: DeviceState, tunnelState: TunnelState? = nil) -> MockTunnelInteractor {
+ let interactor = MockTunnelInteractor(
+ isConfigurationLoaded: true,
+ settings: LatestTunnelSettings(),
+ deviceState: deviceState
+ )
+ if let tunnelState {
+ interactor.tunnelStatus = TunnelStatus(state: tunnelState)
+ }
+ return interactor
+ }
+
+ // MARK: the tests
+
+ func testFailsIfNotLoggedIn() throws {
+ let expectation = expectation(description: "Start tunnel operation failed")
+ let operation = StartTunnelOperation(
+ dispatchQueue: testQueue,
+ interactor: makeInteractor(deviceState: .loggedOut)
+ ) { result in
+ guard case .failure = result else {
+ XCTFail("Operation returned \(result), not failure")
+ return
+ }
+ expectation.fulfill()
+ }
+
+ operationQueue.addOperation(operation)
+ wait(for: [expectation], timeout: 1.0)
+ }
+
+ func testSetsReconnectIfDisconnecting() {
+ let interactor = makeInteractor(deviceState: loggedInDeviceState, tunnelState: .disconnecting(.nothing))
+ var tunnelStatus = TunnelStatus()
+ interactor.onUpdateTunnelStatus = { status in tunnelStatus = status }
+ let expectation = expectation(description: "Tunnel status set to reconnect")
+
+ let operation = StartTunnelOperation(
+ dispatchQueue: testQueue,
+ interactor: interactor
+ ) { result in
+ XCTAssertEqual(tunnelStatus.state, .disconnecting(.reconnect))
+ expectation.fulfill()
+ }
+ operationQueue.addOperation(operation)
+ wait(for: [expectation], timeout: 1.0)
+ }
+
+ func testStartsTunnelIfDisconnected() {
+ let interactor = makeInteractor(deviceState: loggedInDeviceState, tunnelState: .disconnected)
+ let expectation = expectation(description: "Make tunnel provider and start tunnel")
+ let operation = StartTunnelOperation(
+ dispatchQueue: testQueue,
+ interactor: interactor
+ ) { result in
+ XCTAssertNotNil(interactor.tunnel)
+ XCTAssertNotNil(interactor.tunnel?.startDate)
+ expectation.fulfill()
+ }
+ operationQueue.addOperation(operation)
+ wait(for: [expectation], timeout: 1.0)
+ }
+}
diff --git a/ios/Shared/ApplicationTarget.swift b/ios/Shared/ApplicationTarget.swift
index f46fa2c64e..98b0c97917 100644
--- a/ios/Shared/ApplicationTarget.swift
+++ b/ios/Shared/ApplicationTarget.swift
@@ -13,8 +13,9 @@ enum ApplicationTarget: CaseIterable {
/// Returns target bundle identifier.
var bundleIdentifier: String {
- // swiftlint:disable:next force_cast
- let mainBundleIdentifier = Bundle.main.object(forInfoDictionaryKey: "MainApplicationIdentifier") as! String
+ // "MainApplicationIdentifier" does not exist if running tests
+ let mainBundleIdentifier = Bundle.main
+ .object(forInfoDictionaryKey: "MainApplicationIdentifier") as? String ?? "tests"
switch self {
case .mainApp:
return mainBundleIdentifier