Skip to content

Commit bad2e83

Browse files
committed
Add test for starting a tunnel from a disconnected state
1 parent ceaadb8 commit bad2e83

File tree

6 files changed

+144
-44
lines changed

6 files changed

+144
-44
lines changed

ios/MullvadVPN.xcodeproj/project.pbxproj

+4
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
06AC116228F94C450037AF9A /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; };
4141
44DD7D242B6CFFD70005F67F /* StartTunnelOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44DD7D232B6CFFD70005F67F /* StartTunnelOperationTests.swift */; };
4242
44DD7D272B6D18FB0005F67F /* MockTunnelInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44DD7D262B6D18FB0005F67F /* MockTunnelInteractor.swift */; };
43+
44DD7D292B7113CA0005F67F /* MockTunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44DD7D282B7113CA0005F67F /* MockTunnel.swift */; };
4344
5803B4B02940A47300C23744 /* TunnelConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5803B4AF2940A47300C23744 /* TunnelConfiguration.swift */; };
4445
5803B4B22940A48700C23744 /* TunnelStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5803B4B12940A48700C23744 /* TunnelStore.swift */; };
4546
5807E2C02432038B00F5FF30 /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2BF2432038B00F5FF30 /* String+Split.swift */; };
@@ -1226,6 +1227,7 @@
12261227
06FAE67D28F83CA50033DD93 /* RESTTransport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RESTTransport.swift; sourceTree = "<group>"; };
12271228
44DD7D232B6CFFD70005F67F /* StartTunnelOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartTunnelOperationTests.swift; sourceTree = "<group>"; };
12281229
44DD7D262B6D18FB0005F67F /* MockTunnelInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTunnelInteractor.swift; sourceTree = "<group>"; };
1230+
44DD7D282B7113CA0005F67F /* MockTunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTunnel.swift; sourceTree = "<group>"; };
12291231
5802EBC42A8E44AC00E5CE4C /* AppRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRoutes.swift; sourceTree = "<group>"; };
12301232
5802EBC62A8E457A00E5CE4C /* AppRouteProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouteProtocol.swift; sourceTree = "<group>"; };
12311233
5802EBC82A8E45BA00E5CE4C /* ApplicationRouterDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationRouterDelegate.swift; sourceTree = "<group>"; };
@@ -2069,6 +2071,7 @@
20692071
44DD7D252B6D18E90005F67F /* Mocks */ = {
20702072
isa = PBXGroup;
20712073
children = (
2074+
44DD7D282B7113CA0005F67F /* MockTunnel.swift */,
20722075
44DD7D262B6D18FB0005F67F /* MockTunnelInteractor.swift */,
20732076
);
20742077
path = Mocks;
@@ -4563,6 +4566,7 @@
45634566
A9A5FA112ACB05160083449F /* TransportMonitor.swift in Sources */,
45644567
A9B6AC1A2ADE8FBB00F7802A /* InMemorySettingsStore.swift in Sources */,
45654568
A9A5FA132ACB05160083449F /* LoadTunnelConfigurationOperation.swift in Sources */,
4569+
44DD7D292B7113CA0005F67F /* MockTunnel.swift in Sources */,
45664570
A9A5FA142ACB05160083449F /* MapConnectionStatusOperation.swift in Sources */,
45674571
A9A5FA152ACB05160083449F /* RedeemVoucherOperation.swift in Sources */,
45684572
A9A5FA162ACB05160083449F /* RotateKeyOperation.swift in Sources */,

ios/MullvadVPN/TunnelManager/Tunnel.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@ protocol TunnelStatusObserver {
2121
}
2222

2323
protocol TunnelProtocol: AnyObject {
24+
associatedtype TunnelManagerProtocol: VPNTunnelProviderManagerProtocol
2425
var status: NEVPNStatus { get }
2526
var isOnDemandEnabled: Bool { get set }
2627
var startDate: Date? { get }
2728

28-
init(tunnelProvider: TunnelProviderManagerType)
29+
init(tunnelProvider: VPNTunnelProviderManagerProtocol)
2930

3031
func addObserver(_ observer: any TunnelStatusObserver)
3132
func removeObserver(_ observer: any TunnelStatusObserver)
+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//
2+
// MockTunnel.swift
3+
// MullvadVPNTests
4+
//
5+
// Created by Andrew Bulhak on 2024-02-05.
6+
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import NetworkExtension
11+
12+
class MockTunnel: TunnelProtocol {
13+
typealias TunnelManagerProtocol = SimulatorTunnelProviderManager
14+
15+
var status: NEVPNStatus
16+
17+
var isOnDemandEnabled: Bool
18+
19+
var startDate: Date?
20+
21+
required init(tunnelProvider: TunnelManagerProtocol) {
22+
status = .disconnected
23+
isOnDemandEnabled = false
24+
startDate = nil
25+
}
26+
27+
// Observers are currently unimplemented
28+
func addObserver(_ observer: TunnelStatusObserver) {}
29+
30+
func removeObserver(_ observer: TunnelStatusObserver) {}
31+
32+
func addBlockObserver(queue: DispatchQueue?, handler: @escaping (any TunnelProtocol, NEVPNStatus) -> Void) -> TunnelStatusBlockObserver {
33+
fatalError("MockTunnel.addBlockObserver Not implemented")
34+
}
35+
36+
func logFormat() -> String {
37+
""
38+
}
39+
40+
func saveToPreferences(_ completion: @escaping (Error?) -> Void) {
41+
completion(nil)
42+
}
43+
44+
func removeFromPreferences(completion: @escaping (Error?) -> Void) {
45+
completion(nil)
46+
}
47+
48+
func setConfiguration(_ configuration: TunnelConfiguration) {}
49+
50+
func start(options: [String : NSObject]?) throws {
51+
startDate = Date()
52+
}
53+
54+
func stop() {}
55+
56+
func sendProviderMessage(_ messageData: Data, responseHandler: ((Data?) -> Void)?) throws {}
57+
58+
59+
}

ios/MullvadVPNTests/Mocks/MockTunnelInteractor.swift

+26-15
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,39 @@ import MullvadSettings
1111
import PacketTunnelCore
1212

1313
// this is still very minimal, and will be fleshed out as needed.
14-
struct MockTunnelInteractor: TunnelInteractor {
14+
class MockTunnelInteractor: TunnelInteractor {
15+
var isConfigurationLoaded: Bool
16+
17+
var settings: MullvadSettings.LatestTunnelSettings
18+
19+
var deviceState: MullvadSettings.DeviceState
20+
1521
var onUpdateTunnelStatus: ((TunnelStatus)->Void)?
1622

17-
var tunnel: (TunnelProtocol)?
23+
var tunnel: (any TunnelProtocol)?
24+
25+
init(isConfigurationLoaded: Bool, settings: MullvadSettings.LatestTunnelSettings, deviceState: MullvadSettings.DeviceState, onUpdateTunnelStatus: ( (TunnelStatus) -> Void)? = nil) {
26+
self.isConfigurationLoaded = isConfigurationLoaded
27+
self.settings = settings
28+
self.deviceState = deviceState
29+
self.onUpdateTunnelStatus = onUpdateTunnelStatus
30+
self.tunnel = nil
31+
self.tunnelStatus = TunnelStatus()
32+
}
1833

19-
func getPersistentTunnels() -> [TunnelProtocol] {
34+
func getPersistentTunnels() -> [any TunnelProtocol] {
2035
return []
2136
}
2237

23-
func createNewTunnel() -> TunnelProtocol {
24-
fatalError()
38+
func createNewTunnel() -> any TunnelProtocol {
39+
return MockTunnel(tunnelProvider: SimulatorTunnelProviderManager())
2540
}
2641

27-
func setTunnel(_ tunnel: (TunnelProtocol)?, shouldRefreshTunnelState: Bool) {
42+
func setTunnel(_ tunnel: (any TunnelProtocol)?, shouldRefreshTunnelState: Bool) {
43+
self.tunnel = tunnel
2844
}
2945

30-
var tunnelStatus: TunnelStatus =
31-
TunnelStatus()
46+
var tunnelStatus: TunnelStatus
3247

3348
func updateTunnelStatus(_ block: (inout TunnelStatus) -> Void) -> TunnelStatus {
3449
var tunnelStatus = self.tunnelStatus
@@ -37,12 +52,6 @@ struct MockTunnelInteractor: TunnelInteractor {
3752
return tunnelStatus
3853
}
3954

40-
var isConfigurationLoaded: Bool
41-
42-
var settings: MullvadSettings.LatestTunnelSettings
43-
44-
var deviceState: MullvadSettings.DeviceState
45-
4655
func setConfigurationLoaded() {}
4756

4857
func setSettings(_ settings: MullvadSettings.LatestTunnelSettings, persist: Bool) {
@@ -59,7 +68,9 @@ struct MockTunnelInteractor: TunnelInteractor {
5968

6069
func prepareForVPNConfigurationDeletion() {}
6170

71+
struct NotImplementedError: Error { }
72+
6273
func selectRelay() throws -> PacketTunnelCore.SelectedRelay {
63-
fatalError()
74+
throw NotImplementedError()
6475
}
6576
}

ios/MullvadVPNTests/StartTunnelOperationTests.swift

+51-26
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,48 @@ import WireGuardKitTypes
1515

1616
class StartTunnelOperationTests: XCTestCase {
1717

18+
//MARK: utility code for setting up tests
19+
1820
let testQueue = DispatchQueue(label: "StartTunnelOperationTests.testQueue")
21+
let operationQueue = AsyncOperationQueue()
22+
23+
let loggedInDeviceState = DeviceState.loggedIn(
24+
StoredAccountData(
25+
identifier: "",
26+
number: "",
27+
expiry: .distantFuture
28+
),
29+
StoredDeviceData(
30+
creationDate: Date(),
31+
identifier: "",
32+
name: "",
33+
hijackDNS: false,
34+
ipv4Address: IPAddressRange(from: "127.0.0.1/32")!,
35+
ipv6Address: IPAddressRange(from: "::ff/64")!,
36+
wgKeyData: StoredWgKeyData(creationDate: Date(), privateKey: PrivateKey())
37+
)
38+
)
39+
40+
func makeInteractor(deviceState: DeviceState, tunnelState: TunnelState? = nil) -> MockTunnelInteractor {
41+
var interactor = MockTunnelInteractor(
42+
isConfigurationLoaded: true,
43+
settings: LatestTunnelSettings(),
44+
deviceState: deviceState
45+
)
46+
if let tunnelState {
47+
interactor.tunnelStatus = TunnelStatus(state: tunnelState)
48+
}
49+
return interactor
50+
}
51+
52+
//MARK: the tests
1953

2054
func testFailsIfNotLoggedIn() throws {
21-
let operationQueue = AsyncOperationQueue()
2255
let settings = LatestTunnelSettings()
2356
let exp = expectation(description:"Start tunnel operation failed")
2457
let operation = StartTunnelOperation(
2558
dispatchQueue: testQueue,
26-
interactor: MockTunnelInteractor(isConfigurationLoaded: true, settings: settings, deviceState: .loggedOut)) { result in
27-
59+
interactor: makeInteractor(deviceState: .loggedOut)) { result in
2860
guard case .failure(_) = result else {
2961
XCTFail("Operation returned \(result), not failure")
3062
return
@@ -37,31 +69,9 @@ class StartTunnelOperationTests: XCTestCase {
3769
}
3870

3971
func testSetsReconnectIfDisconnecting() {
40-
let operationQueue = AsyncOperationQueue()
4172
let settings = LatestTunnelSettings()
42-
var interactor = MockTunnelInteractor(
43-
isConfigurationLoaded: true,
44-
settings: settings,
45-
deviceState: .loggedIn(
46-
StoredAccountData(
47-
identifier: "",
48-
number: "",
49-
expiry: .distantFuture
50-
),
51-
StoredDeviceData(
52-
creationDate: Date(),
53-
identifier: "",
54-
name: "",
55-
hijackDNS: false,
56-
ipv4Address: IPAddressRange(from: "127.0.0.1/32")!,
57-
ipv6Address: IPAddressRange(from: "::ff/64")!,
58-
wgKeyData: StoredWgKeyData(creationDate: Date(), privateKey: PrivateKey())
59-
)
60-
)
61-
)
73+
var interactor = makeInteractor(deviceState: loggedInDeviceState, tunnelState: .disconnecting(.nothing))
6274
var tunnelStatus = TunnelStatus()
63-
tunnelStatus.state = .disconnecting(.nothing)
64-
interactor.tunnelStatus = tunnelStatus
6575
interactor.onUpdateTunnelStatus = { status in tunnelStatus = status }
6676
let expectation = expectation(description:"Tunnel status set to reconnect")
6777

@@ -74,4 +84,19 @@ class StartTunnelOperationTests: XCTestCase {
7484
operationQueue.addOperation(operation)
7585
wait(for: [expectation], timeout: 1.0)
7686
}
87+
88+
func testStartsTunnelIfDisconnected() {
89+
let settings = LatestTunnelSettings()
90+
var interactor = makeInteractor(deviceState: loggedInDeviceState, tunnelState: .disconnected)
91+
let expectation = expectation(description:"Make tunnel provider and start tunnel")
92+
let operation = StartTunnelOperation(
93+
dispatchQueue: testQueue,
94+
interactor: interactor) { result in
95+
XCTAssertNotNil(interactor.tunnel)
96+
XCTAssertNotNil(interactor.tunnel?.startDate)
97+
expectation.fulfill()
98+
}
99+
operationQueue.addOperation(operation)
100+
wait(for: [expectation], timeout: 1.0)
101+
}
77102
}

ios/Shared/ApplicationTarget.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ enum ApplicationTarget: CaseIterable {
1313

1414
/// Returns target bundle identifier.
1515
var bundleIdentifier: String {
16-
// swiftlint:disable:next force_cast
17-
let mainBundleIdentifier = Bundle.main.object(forInfoDictionaryKey: "MainApplicationIdentifier") as! String
16+
// "MainApplicationIdentifier" does not exist if running tests
17+
let mainBundleIdentifier = Bundle.main.object(forInfoDictionaryKey: "MainApplicationIdentifier") as? String ?? "tests"
1818
switch self {
1919
case .mainApp:
2020
return mainBundleIdentifier

0 commit comments

Comments
 (0)