summaryrefslogtreecommitdiffhomepage
path: root/ios/PacketTunnelCoreTests/TunnelMonitorTests.swift
blob: 60d0a6399c6808a8de941a5d794ca120e2da100b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
//
//  TunnelMonitorTests.swift
//  PacketTunnelCoreTests
//
//  Created by pronebird on 15/08/2023.
//  Copyright © 2025 Mullvad VPN AB. All rights reserved.
//

import MullvadTypes
import Network
import XCTest

@testable import MullvadMockData
@testable import PacketTunnelCore

final class TunnelMonitorTests: XCTestCase {
    let networkCounters = NetworkCounters()

    func testShouldDetermineConnectionEstablished() async throws {
        let connectedExpectation = expectation(description: "Should report connected.")
        let connectionLostExpectation = expectation(description: "Should not report connection loss")
        connectionLostExpectation.isInverted = true

        let pinger = PingerMock(networkStatsReporting: networkCounters) { _, _ in
            return .sendReply()
        }

        let tunnelMonitor = createTunnelMonitor(pinger: pinger, timings: TunnelMonitorTimings())

        tunnelMonitor.onEvent = { event in
            switch event {
            case .connectionEstablished:
                connectedExpectation.fulfill()

            case .connectionLost:
                connectionLostExpectation.fulfill()
            }
        }

        tunnelMonitor.start(probeAddress: .loopback)

        await fulfillment(of: [connectedExpectation, connectionLostExpectation], timeout: .UnitTest.invertedTimeout)
    }

    func testInitialConnectionTimings() async throws {
        // Setup pinger so that it never receives any replies.
        let pinger = PingerMock(networkStatsReporting: networkCounters) { _, _ in .ignore }

        let timings = TunnelMonitorTimings(
            pingTimeout: .milliseconds(300),
            initialEstablishTimeout: .milliseconds(100),
            connectivityCheckInterval: .milliseconds(100)
        )

        let tunnelMonitor = createTunnelMonitor(pinger: pinger, timings: timings)

        var expectedTimings = [
            timings.initialEstablishTimeout.milliseconds,
            timings.initialEstablishTimeout.milliseconds * 2,
            timings.pingTimeout.milliseconds,
            timings.pingTimeout.milliseconds,
        ]

        // Calculate the amount of time necessary to perform the test.
        var timeout = expectedTimings.reduce(0, +)
        // Add leeway into the total amount of expected wait time.
        timeout += timeout / 2

        let expectation = expectation(description: "Should respect all timings.")
        expectation.expectedFulfillmentCount = expectedTimings.count

        // This date will be used to measure the amount of time elapsed between `.connectionLost` events.
        var startDate = Date()

        tunnelMonitor.onEvent = { [weak tunnelMonitor] event in
            guard case .connectionLost = event else { return }

            switch event {
            case .connectionLost:
                XCTAssertFalse(expectedTimings.isEmpty)

                let expectedDuration = expectedTimings.removeFirst()
                let leeway = expectedDuration / 2

                // Compute amount of time elapsed between `.connectionLost` events.
                let timeElapsed = Int(Date().timeIntervalSince(startDate) * 1000)

                XCTAssertEqual(
                    timeElapsed,
                    expectedDuration,
                    accuracy: leeway,
                    "Expected to report connection loss after \(expectedDuration)-\(expectedDuration + leeway) ms, instead reported it after \(timeElapsed) ms."
                )

                expectation.fulfill()

                if !expectedTimings.isEmpty {
                    startDate = Date()

                    // Continue monitoring by calling start() again.
                    tunnelMonitor?.start(probeAddress: .loopback)
                }

            case .connectionEstablished:
                XCTFail("Connection should fail.")
            }
        }

        // Start monitoring.
        tunnelMonitor.start(probeAddress: .loopback)

        await fulfillment(of: [expectation], timeout: TimeInterval(timeout) / 1000)
    }
}

extension TunnelMonitorTests {
    private func createTunnelMonitor(pinger: PingerProtocol, timings: TunnelMonitorTimings) -> TunnelMonitor {
        return TunnelMonitor(
            eventQueue: .main,
            pinger: pinger,
            tunnelDeviceInfo: TunnelDeviceInfoStub(networkStatsProviding: networkCounters),
            timings: timings
        )
    }
}