Skip to content

Commit 48b15e7

Browse files
acb-mvbuggmagnet
authored andcommitted
Implement initial tests: start rotation on account set, stop on account unset
1 parent 8e993df commit 48b15e7

File tree

9 files changed

+148
-32
lines changed

9 files changed

+148
-32
lines changed

ios/MullvadREST/ApiHandlers/RESTAccountsProxy.swift

+14
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,17 @@ extension REST {
145145
public let number: String
146146
}
147147
}
148+
149+
extension REST.NewAccountData {
150+
public static func mockValue() -> REST.NewAccountData {
151+
return REST.NewAccountData(
152+
id: UUID().uuidString,
153+
expiry: Date().addingTimeInterval(3600),
154+
maxPorts: 2,
155+
canAddPorts: false,
156+
maxDevices: 5,
157+
canAddDevices: false,
158+
number: "1234567890123456"
159+
)
160+
}
161+
}

ios/MullvadVPN.xcodeproj/project.pbxproj

+8
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
06AC116228F94C450037AF9A /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; };
4343
449872E12B7BBC5400094DDC /* TunnelSettingsUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449872E02B7BBC5400094DDC /* TunnelSettingsUpdate.swift */; };
4444
449872E42B7CB96300094DDC /* TunnelSettingsUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449872E32B7CB96300094DDC /* TunnelSettingsUpdateTests.swift */; };
45+
449EB9FD2B95F8AD00DFA4EB /* DeviceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449EB9FC2B95F8AD00DFA4EB /* DeviceMock.swift */; };
46+
449EB9FF2B95FF2500DFA4EB /* AccountMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449EB9FE2B95FF2500DFA4EB /* AccountMock.swift */; };
4547
44DD7D242B6CFFD70005F67F /* StartTunnelOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44DD7D232B6CFFD70005F67F /* StartTunnelOperationTests.swift */; };
4648
44DD7D272B6D18FB0005F67F /* MockTunnelInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44DD7D262B6D18FB0005F67F /* MockTunnelInteractor.swift */; };
4749
44DD7D292B7113CA0005F67F /* MockTunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44DD7D282B7113CA0005F67F /* MockTunnel.swift */; };
@@ -1294,6 +1296,8 @@
12941296
06FAE67D28F83CA50033DD93 /* RESTTransport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RESTTransport.swift; sourceTree = "<group>"; };
12951297
449872E02B7BBC5400094DDC /* TunnelSettingsUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsUpdate.swift; sourceTree = "<group>"; };
12961298
449872E32B7CB96300094DDC /* TunnelSettingsUpdateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsUpdateTests.swift; sourceTree = "<group>"; };
1299+
449EB9FC2B95F8AD00DFA4EB /* DeviceMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceMock.swift; sourceTree = "<group>"; };
1300+
449EB9FE2B95FF2500DFA4EB /* AccountMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountMock.swift; sourceTree = "<group>"; };
12971301
44DD7D232B6CFFD70005F67F /* StartTunnelOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartTunnelOperationTests.swift; sourceTree = "<group>"; };
12981302
44DD7D262B6D18FB0005F67F /* MockTunnelInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTunnelInteractor.swift; sourceTree = "<group>"; };
12991303
44DD7D282B7113CA0005F67F /* MockTunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTunnel.swift; sourceTree = "<group>"; };
@@ -2202,7 +2206,9 @@
22022206
44DD7D252B6D18E90005F67F /* Mocks */ = {
22032207
isa = PBXGroup;
22042208
children = (
2209+
449EB9FE2B95FF2500DFA4EB /* AccountMock.swift */,
22052210
7A9BE5A82B90806800E2A7D0 /* CustomListsRepositoryStub.swift */,
2211+
449EB9FC2B95F8AD00DFA4EB /* DeviceMock.swift */,
22062212
44DD7D282B7113CA0005F67F /* MockTunnel.swift */,
22072213
44DD7D262B6D18FB0005F67F /* MockTunnelInteractor.swift */,
22082214
);
@@ -4854,6 +4860,7 @@
48544860
A9A5FA272ACB05160083449F /* VPNConnectionProtocol.swift in Sources */,
48554861
A9A5FA282ACB05160083449F /* WgKeyRotation.swift in Sources */,
48564862
449872E42B7CB96300094DDC /* TunnelSettingsUpdateTests.swift in Sources */,
4863+
449EB9FD2B95F8AD00DFA4EB /* DeviceMock.swift in Sources */,
48574864
A9A5FA292ACB05160083449F /* AddressCacheTests.swift in Sources */,
48584865
A9B6AC182ADE8F4300F7802A /* MigrationManagerTests.swift in Sources */,
48594866
7A9BE5AB2B909A1700E2A7D0 /* LocationDataSourceProtocol.swift in Sources */,
@@ -4866,6 +4873,7 @@
48664873
A9A5FA2F2ACB05160083449F /* FixedWidthIntegerArithmeticsTests.swift in Sources */,
48674874
A9A5FA302ACB05160083449F /* InputTextFormatterTests.swift in Sources */,
48684875
F0B0E6972AFE6E7E001DC66B /* XCTest+Async.swift in Sources */,
4876+
449EB9FF2B95FF2500DFA4EB /* AccountMock.swift in Sources */,
48694877
7ADCB2DA2B6A730400C88F89 /* IPOverrideRepositoryStub.swift in Sources */,
48704878
A9A5FA312ACB05160083449F /* MockFileCache.swift in Sources */,
48714879
A9A5FA322ACB05160083449F /* RelayCacheTests.swift in Sources */,

ios/MullvadVPN/TunnelManager/TunnelManager.swift

+15-3
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ final class TunnelManager: StorePaymentObserver {
6161
private var networkMonitor: NWPathMonitor?
6262

6363
private var privateKeyRotationTimer: DispatchSourceTimer?
64-
private var isRunningPeriodicPrivateKeyRotation = false
64+
public private(set) var isRunningPeriodicPrivateKeyRotation = false
65+
public private(set) var nextKeyRotationDate: Date?
6566

6667
private var tunnelStatusPollTimer: DispatchSourceTimer?
6768
private var isPolling = false
@@ -111,7 +112,7 @@ final class TunnelManager: StorePaymentObserver {
111112
nslock.lock()
112113
defer { nslock.unlock() }
113114

114-
guard !isRunningPeriodicPrivateKeyRotation else { return }
115+
guard !isRunningPeriodicPrivateKeyRotation, deviceState.isLoggedIn else { return }
115116

116117
logger.debug("Start periodic private key rotation.")
117118

@@ -131,6 +132,14 @@ final class TunnelManager: StorePaymentObserver {
131132
updatePrivateKeyRotationTimer()
132133
}
133134

135+
func startOrStopPeriodicPrivateKeyRotation() {
136+
if deviceState.isLoggedIn {
137+
startPeriodicPrivateKeyRotation()
138+
} else {
139+
stopPeriodicPrivateKeyRotation()
140+
}
141+
}
142+
134143
func getNextKeyRotationDate() -> Date? {
135144
nslock.lock()
136145
defer { nslock.unlock() }
@@ -144,9 +153,11 @@ final class TunnelManager: StorePaymentObserver {
144153

145154
privateKeyRotationTimer?.cancel()
146155
privateKeyRotationTimer = nil
156+
nextKeyRotationDate = nil
147157

148158
guard isRunningPeriodicPrivateKeyRotation,
149159
let scheduleDate = getNextKeyRotationDate() else { return }
160+
nextKeyRotationDate = scheduleDate
150161

151162
let timer = DispatchSource.makeTimerSource(queue: .main)
152163

@@ -334,7 +345,8 @@ final class TunnelManager: StorePaymentObserver {
334345

335346
operation.completionQueue = .main
336347
operation.completionHandler = { [weak self] result in
337-
self?.updatePrivateKeyRotationTimer()
348+
guard let self else { return }
349+
startOrStopPeriodicPrivateKeyRotation()
338350

339351
completionHandler(result)
340352
}

ios/MullvadVPNTests/AccountsProxy+Stubs.swift

+5-1
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,15 @@ import Foundation
1111
@testable import MullvadTypes
1212

1313
struct AccountsProxyStub: RESTAccountHandling {
14+
var createAccountResult: Result<REST.NewAccountData, Error>?
1415
func createAccount(
1516
retryStrategy: REST.RetryStrategy,
1617
completion: @escaping MullvadREST.ProxyCompletionHandler<REST.NewAccountData>
1718
) -> Cancellable {
18-
AnyCancellable()
19+
if let createAccountResult = createAccountResult {
20+
completion(createAccountResult)
21+
}
22+
return AnyCancellable()
1923
}
2024

2125
func getAccountData(accountNumber: String) -> any RESTRequestExecutor<Account> {

ios/MullvadVPNTests/DeviceCheckOperationTests.swift

-25
Original file line numberDiff line numberDiff line change
@@ -526,31 +526,6 @@ private extension StoredDeviceData {
526526
}
527527
}
528528

529-
private extension Device {
530-
static func mock(publicKey: PublicKey) -> Device {
531-
Device(
532-
id: "device-id",
533-
name: "device-name",
534-
pubkey: publicKey,
535-
hijackDNS: false,
536-
created: Date(),
537-
ipv4Address: IPAddressRange(from: "127.0.0.1/32")!,
538-
ipv6Address: IPAddressRange(from: "::ff/64")!
539-
)
540-
}
541-
}
542-
543-
private extension Account {
544-
static func mock(expiry: Date = .distantFuture) -> Account {
545-
Account(
546-
id: "account-id",
547-
expiry: expiry,
548-
maxDevices: 5,
549-
canAddDevices: true
550-
)
551-
}
552-
}
553-
554529
private extension KeyRotationStatus {
555530
/// Returns `true` if key rotation status is `.attempted`.
556531
var isAttempted: Bool {

ios/MullvadVPNTests/DevicesProxy+Stubs.swift

+5-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import Foundation
1212
@testable import WireGuardKitTypes
1313

1414
struct DevicesProxyStub: DeviceHandling {
15+
let mockDevice = Device.mock(publicKey: PrivateKey().publicKey)
1516
func getDevice(
1617
accountNumber: String,
1718
identifier: String,
@@ -35,7 +36,8 @@ struct DevicesProxyStub: DeviceHandling {
3536
retryStrategy: REST.RetryStrategy,
3637
completion: @escaping ProxyCompletionHandler<Device>
3738
) -> Cancellable {
38-
AnyCancellable()
39+
completion(.success(mockDevice))
40+
return AnyCancellable()
3941
}
4042

4143
func deleteDevice(
@@ -44,7 +46,8 @@ struct DevicesProxyStub: DeviceHandling {
4446
retryStrategy: REST.RetryStrategy,
4547
completion: @escaping ProxyCompletionHandler<Bool>
4648
) -> Cancellable {
47-
AnyCancellable()
49+
completion(.success(true))
50+
return AnyCancellable()
4851
}
4952

5053
func rotateDeviceKey(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//
2+
// AccountMock.swift
3+
// MullvadVPNTests
4+
//
5+
// Created by Andrew Bulhak on 2024-03-04.
6+
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
7+
//
8+
9+
import MullvadTypes
10+
11+
extension Account {
12+
static func mock(expiry: Date = .distantFuture) -> Account {
13+
Account(
14+
id: "account-id",
15+
expiry: expiry,
16+
maxDevices: 5,
17+
canAddDevices: true
18+
)
19+
}
20+
}
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//
2+
// DeviceMock.swift
3+
// MullvadVPNTests
4+
//
5+
// Created by Andrew Bulhak on 2024-03-04.
6+
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import MullvadTypes
11+
import WireGuardKitTypes
12+
13+
extension Device {
14+
static func mock(publicKey: PublicKey) -> Device {
15+
Device(
16+
id: "device-id",
17+
name: "Devicey McDeviceface",
18+
pubkey: publicKey,
19+
hijackDNS: false,
20+
created: Date(),
21+
ipv4Address: IPAddressRange(from: "127.0.0.1/32")!,
22+
ipv6Address: IPAddressRange(from: "::ff/64")!
23+
)
24+
}
25+
}

ios/MullvadVPNTests/TunnelManagerTests.swift

+56-1
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,21 @@
66
// Copyright © 2023 Mullvad VPN AB. All rights reserved.
77
//
88

9+
import MullvadREST
10+
@testable import MullvadSettings
911
import XCTest
1012

1113
final class TunnelManagerTests: XCTestCase {
14+
static let store = InMemorySettingsStore<SettingNotFound>()
15+
16+
override class func setUp() {
17+
SettingsManager.unitTestStore = store
18+
}
19+
20+
override class func tearDown() {
21+
SettingsManager.unitTestStore = nil
22+
}
23+
1224
func testTunnelManager() {
1325
let application = UIApplicationStub()
1426
let tunnelStore = TunnelStoreStub()
@@ -17,7 +29,27 @@ final class TunnelManagerTests: XCTestCase {
1729
let devicesProxy = DevicesProxyStub()
1830
let apiProxy = APIProxyStub()
1931
let accessTokenManager = AccessTokenManagerStub()
32+
let tunnelManager = TunnelManager(
33+
application: application,
34+
tunnelStore: tunnelStore,
35+
relayCacheTracker: relayCacheTracker,
36+
accountsProxy: accountProxy,
37+
devicesProxy: devicesProxy,
38+
apiProxy: apiProxy,
39+
accessTokenManager: accessTokenManager
40+
)
41+
XCTAssertNotNil(tunnelManager)
42+
}
2043

44+
func testLogInStartsKeyRotations() async throws {
45+
let application = UIApplicationStub()
46+
let tunnelStore = TunnelStoreStub()
47+
let relayCacheTracker = RelayCacheTrackerStub()
48+
var accountProxy = AccountsProxyStub()
49+
let devicesProxy = DevicesProxyStub()
50+
let apiProxy = APIProxyStub()
51+
let accessTokenManager = AccessTokenManagerStub()
52+
accountProxy.createAccountResult = .success(REST.NewAccountData.mockValue())
2153
let tunnelManager = TunnelManager(
2254
application: application,
2355
tunnelStore: tunnelStore,
@@ -27,7 +59,30 @@ final class TunnelManagerTests: XCTestCase {
2759
apiProxy: apiProxy,
2860
accessTokenManager: accessTokenManager
2961
)
62+
_ = try await tunnelManager.setNewAccount()
63+
XCTAssertEqual(tunnelManager.isRunningPeriodicPrivateKeyRotation, true)
64+
}
3065

31-
XCTAssertNotNil(tunnelManager)
66+
func testLogOutStopsKeyRotations() async throws {
67+
let application = UIApplicationStub()
68+
let tunnelStore = TunnelStoreStub()
69+
let relayCacheTracker = RelayCacheTrackerStub()
70+
var accountProxy = AccountsProxyStub()
71+
let devicesProxy = DevicesProxyStub()
72+
let apiProxy = APIProxyStub()
73+
let accessTokenManager = AccessTokenManagerStub()
74+
accountProxy.createAccountResult = .success(REST.NewAccountData.mockValue())
75+
let tunnelManager = TunnelManager(
76+
application: application,
77+
tunnelStore: tunnelStore,
78+
relayCacheTracker: relayCacheTracker,
79+
accountsProxy: accountProxy,
80+
devicesProxy: devicesProxy,
81+
apiProxy: apiProxy,
82+
accessTokenManager: accessTokenManager
83+
)
84+
_ = try await tunnelManager.setNewAccount()
85+
await tunnelManager.unsetAccount()
86+
XCTAssertEqual(tunnelManager.isRunningPeriodicPrivateKeyRotation, false)
3287
}
3388
}

0 commit comments

Comments
 (0)