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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
|
//
// ShadowsocksLoaderTests.swift
// MullvadVPNTests
//
// Created by Mojgan on 2024-05-29.
// Copyright © 2025 Mullvad VPN AB. All rights reserved.
//
import MullvadMockData
@preconcurrency import XCTest
@testable import MullvadREST
@testable import MullvadSettings
@testable import MullvadTypes
class ShadowsocksLoaderTests: XCTestCase {
private let sampleRelays = ServerRelaysResponseStubs.sampleRelays
private var shadowsocksConfigurationCache: ShadowsocksConfigurationCacheStub!
private var relaySelector: ShadowsocksRelaySelectorStub!
private var shadowsocksLoader: ShadowsocksLoader!
private var relayConstraints = RelayConstraints()
private var settingsListener = TunnelSettingsListener()
override func setUpWithError() throws {
shadowsocksConfigurationCache = ShadowsocksConfigurationCacheStub()
relaySelector = ShadowsocksRelaySelectorStub(relays: sampleRelays)
relaySelector.exitBridgeResult = .success(
try XCTUnwrap(
closetRelayTo(
location: relayConstraints.exitLocations,
port: relayConstraints.port,
filter: relayConstraints.filter,
in: sampleRelays
)))
relaySelector.entryBridgeResult = .success(
try XCTUnwrap(
closetRelayTo(
location: relayConstraints.entryLocations,
port: relayConstraints.port,
filter: relayConstraints.filter,
in: sampleRelays
)))
shadowsocksLoader = ShadowsocksLoader(
cache: shadowsocksConfigurationCache,
relaySelector: relaySelector,
settingsUpdater: SettingsUpdater(listener: settingsListener)
)
}
func testLoadConfigWithMultihopDisabled() throws {
settingsListener.onNewSettings?(LatestTunnelSettings(tunnelMultihopState: .off))
relaySelector.entryBridgeResult = .failure(ShadowsocksRelaySelectorStubError())
let configuration = try XCTUnwrap(shadowsocksLoader.load())
XCTAssertEqual(configuration, try XCTUnwrap(shadowsocksConfigurationCache.read()))
}
func testLoadConfigWithMultihopEnabled() throws {
settingsListener.onNewSettings?(LatestTunnelSettings(tunnelMultihopState: .on))
relaySelector.exitBridgeResult = .failure(ShadowsocksRelaySelectorStubError())
let configuration = try XCTUnwrap(shadowsocksLoader.load())
XCTAssertEqual(configuration, try XCTUnwrap(shadowsocksConfigurationCache.read()))
}
func testConstraintsUpdateClearsCache() throws {
relayConstraints = RelayConstraints(
entryLocations: .only(UserSelectedRelays(locations: [.city("ca", "tor")])),
exitLocations: .only(UserSelectedRelays(locations: [.country("ae")]))
)
settingsListener.onNewSettings?(LatestTunnelSettings(relayConstraints: relayConstraints))
XCTAssertNil(shadowsocksConfigurationCache.cachedConfiguration)
}
func testMultihopUpdateClearsCache() throws {
settingsListener.onNewSettings?(LatestTunnelSettings(tunnelMultihopState: .off))
XCTAssertNil(shadowsocksConfigurationCache.cachedConfiguration)
}
private func closetRelayTo(
location: RelayConstraint<UserSelectedRelays>,
port: RelayConstraint<UInt16>,
filter: RelayConstraint<RelayFilter>,
in: REST.ServerRelaysResponse
) -> REST.BridgeRelay? {
RelaySelector.Shadowsocks.closestRelay(
location: location,
port: port,
filter: filter,
in: sampleRelays
)
}
}
class ShadowsocksRelaySelectorStub: ShadowsocksRelaySelectorProtocol, @unchecked Sendable {
var entryBridgeResult: Result<REST.BridgeRelay, Error> = .failure(ShadowsocksRelaySelectorStubError())
var exitBridgeResult: Result<REST.BridgeRelay, Error> = .failure(ShadowsocksRelaySelectorStubError())
private let relays: REST.ServerRelaysResponse
init(relays: REST.ServerRelaysResponse) {
self.relays = relays
}
func selectRelay(with settings: LatestTunnelSettings) throws -> REST.BridgeRelay? {
switch settings.tunnelMultihopState {
case .on:
try entryBridgeResult.get()
case .off:
try exitBridgeResult.get()
}
}
func getBridges() throws -> REST.ServerShadowsocks? {
RelaySelector.Shadowsocks.tcpBridge(from: relays)
}
}
class ShadowsocksConfigurationCacheStub: ShadowsocksConfigurationCacheProtocol, @unchecked Sendable {
private(set) var cachedConfiguration: ShadowsocksConfiguration?
func read() throws -> ShadowsocksConfiguration {
guard let cachedConfiguration else {
throw ShadowsocksConfigurationCacheStubError()
}
return cachedConfiguration
}
func write(_ configuration: ShadowsocksConfiguration) throws {
self.cachedConfiguration = configuration
}
func clear() throws {
self.cachedConfiguration = nil
}
}
private struct ShadowsocksRelaySelectorStubError: Error {}
private struct ShadowsocksConfigurationCacheStubError: Error {}
|