Skip to content

Commit

Permalink
Merge branch 'unit-tests-for-starttunneloperation-ios-480'
Browse files Browse the repository at this point in the history
  • Loading branch information
buggmagnet committed Feb 7, 2024
2 parents 1053b63 + 2b42d8f commit 808a142
Show file tree
Hide file tree
Showing 6 changed files with 264 additions and 3 deletions.
20 changes: 20 additions & 0 deletions ios/MullvadVPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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>"; };
Expand Down Expand Up @@ -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 = (
Expand Down Expand Up @@ -2737,6 +2752,7 @@
58B0A2A1238EE67E00BC001D /* MullvadVPNTests */ = {
isa = PBXGroup;
children = (
44DD7D252B6D18E90005F67F /* Mocks */,
A900E9BF2ACC661900C95F67 /* AccessTokenManager+Stubs.swift */,
7A6F2FA42AFA3CB2006D0856 /* AccountExpiryTests.swift */,
A900E9B72ACC5C2B00C95F67 /* AccountsProxy+Stubs.swift */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
3 changes: 2 additions & 1 deletion ios/MullvadVPN/TunnelManager/Tunnel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
60 changes: 60 additions & 0 deletions ios/MullvadVPNTests/Mocks/MockTunnel.swift
Original file line number Diff line number Diff line change
@@ -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 {}
}
79 changes: 79 additions & 0 deletions ios/MullvadVPNTests/Mocks/MockTunnelInteractor.swift
Original file line number Diff line number Diff line change
@@ -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()
}
}
100 changes: 100 additions & 0 deletions ios/MullvadVPNTests/StartTunnelOperationTests.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
5 changes: 3 additions & 2 deletions ios/Shared/ApplicationTarget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 808a142

Please sign in to comment.