feat(network): add relay connection policies

This commit is contained in:
2026-07-23 15:14:03 +02:00
parent cbace73908
commit a0bcc5dbff
40 changed files with 942 additions and 254 deletions

View File

@@ -26,7 +26,7 @@ final class AppModelTests: XCTestCase {
func testInitializesCoreWithSavedCustomRelayConfiguration() async {
let core = FakeCoreGateway()
let preferences = Fixtures.preferences()
let configuration = RelayConfiguration(mode: .custom, relayURLs: ["https://relay.example"])
let configuration = RelayConfiguration(mode: .strictCustom, relayURLs: ["https://relay.example"])
preferences.setRelayConfiguration(configuration)
_ = makeModel(core, preferences: preferences)

View File

@@ -32,7 +32,7 @@ final class AppPreferencesRepositoryTests: XCTestCase {
repo.setNotificationsEnabled(true)
repo.setReceiveFolder(ReceiveFolder(kind: .iosSecurityScopedUrl, value: "file:///x", displayName: "Custom"))
repo.setRelayConfiguration(RelayConfiguration(
mode: .custom,
mode: .strictCustom,
relayURLs: ["https://relay-one.example", "https://relay-two.example:443"]
))
@@ -44,7 +44,7 @@ final class AppPreferencesRepositoryTests: XCTestCase {
XCTAssertEqual(reloaded.preferences.receiveFolder.displayName, "Custom")
XCTAssertEqual(reloaded.preferences.receiveFolder.kind, .iosSecurityScopedUrl)
XCTAssertEqual(reloaded.preferences.relayConfiguration, RelayConfiguration(
mode: .custom,
mode: .strictCustom,
relayURLs: ["https://relay-one.example", "https://relay-two.example:443"]
))
XCTAssertNotNil(store.data(forKey: "relay_configuration"))
@@ -60,7 +60,7 @@ final class AppPreferencesRepositoryTests: XCTestCase {
XCTAssertEqual(
repo.preferences.relayConfiguration,
RelayConfiguration(mode: .custom, relayURLs: [])
RelayConfiguration(mode: .strictCustom, relayURLs: [])
)
}
@@ -75,7 +75,7 @@ final class AppPreferencesRepositoryTests: XCTestCase {
XCTAssertEqual(
repo.preferences.relayConfiguration,
RelayConfiguration(mode: .custom, relayURLs: [])
RelayConfiguration(mode: .strictCustom, relayURLs: [])
)
}

View File

@@ -94,7 +94,7 @@ final class CoreRepositoryLifecycleTests: XCTestCase {
let concurrentInitialization = await repository.initialize(
appDataDir: "/tmp/second",
networkConfiguration: RelayConfiguration(mode: .custom, relayURLs: ["https://relay.example"])
networkConfiguration: RelayConfiguration(mode: .strictCustom, relayURLs: ["https://relay.example"])
)
assertLifecycleFailure(concurrentInitialization, equals: .transitionInProgress)

View File

@@ -2,6 +2,32 @@ import XCTest
@testable import VniDrop
final class RelayConfigurationTests: XCTestCase {
func testCustomFallbackValidatesAndPreservesItsMode() throws {
let result = try RelayConfigurationValidator.validate(
mode: .customWithDirectFallback,
relayURLs: ["https://relay.example/"]
)
XCTAssertEqual(
result,
RelayConfiguration(
mode: .customWithDirectFallback,
relayURLs: ["https://relay.example"]
)
)
}
func testLocalOnlyRetainsPreviouslySavedRelayURLs() throws {
let retained = ["https://relay.example"]
let result = try RelayConfigurationValidator.validate(
mode: .localOnly,
relayURLs: ["not a URL"],
retainedRelayURLs: retained
)
XCTAssertEqual(result, RelayConfiguration(mode: .localOnly, relayURLs: retained))
}
func testAutomaticModeIgnoresRelayDrafts() throws {
let result = try RelayConfigurationValidator.validate(
mode: .automatic,
@@ -24,32 +50,32 @@ final class RelayConfigurationTests: XCTestCase {
func testCustomModeTrimsValidHTTPSRelayURLs() throws {
let result = try RelayConfigurationValidator.validate(
mode: .custom,
mode: .strictCustom,
relayURLs: [" https://relay.example/ ", "https://backup.example:443"]
)
XCTAssertEqual(result, RelayConfiguration(
mode: .custom,
mode: .strictCustom,
relayURLs: ["https://relay.example", "https://backup.example"]
))
}
func testCustomModeIgnoresEmptyURLRows() throws {
let result = try RelayConfigurationValidator.validate(
mode: .custom,
mode: .strictCustom,
relayURLs: ["", " ", "https://relay.example"]
)
XCTAssertEqual(result.relayURLs, ["https://relay.example"])
}
func testCustomModeRequiresAtLeastOneRelay() {
XCTAssertThrowsError(try RelayConfigurationValidator.validate(mode: .custom, relayURLs: [])) { error in
XCTAssertThrowsError(try RelayConfigurationValidator.validate(mode: .strictCustom, relayURLs: [])) { error in
XCTAssertEqual(error as? RelayConfigurationValidationError, .missingURL)
}
}
func testCustomModeRequiresHTTPS() {
XCTAssertThrowsError(try RelayConfigurationValidator.validate(
mode: .custom,
mode: .strictCustom,
relayURLs: ["http://relay.example"]
)) { error in
XCTAssertEqual(error as? RelayConfigurationValidationError, .httpsRequired(index: 0))
@@ -67,7 +93,7 @@ final class RelayConfigurationTests: XCTestCase {
]
for relayURL in invalidURLs {
XCTAssertThrowsError(
try RelayConfigurationValidator.validate(mode: .custom, relayURLs: [relayURL]),
try RelayConfigurationValidator.validate(mode: .strictCustom, relayURLs: [relayURL]),
"Expected \(relayURL) to be rejected"
) { error in
XCTAssertEqual(error as? RelayConfigurationValidationError, .invalidURL(index: 0))
@@ -77,7 +103,7 @@ final class RelayConfigurationTests: XCTestCase {
func testCustomModeRejectsNormalizedDuplicate() {
XCTAssertThrowsError(try RelayConfigurationValidator.validate(
mode: .custom,
mode: .strictCustom,
relayURLs: ["https://relay.example", "https://RELAY.example:443/"]
)) { error in
XCTAssertEqual(error as? RelayConfigurationValidationError, .duplicateURL(index: 1))
@@ -88,7 +114,7 @@ final class RelayConfigurationTests: XCTestCase {
let relayURLs = (0...RelayConfigurationValidator.maximumRelayCount).map {
"https://relay-\($0).example"
}
XCTAssertThrowsError(try RelayConfigurationValidator.validate(mode: .custom, relayURLs: relayURLs)) { error in
XCTAssertThrowsError(try RelayConfigurationValidator.validate(mode: .strictCustom, relayURLs: relayURLs)) { error in
XCTAssertEqual(error as? RelayConfigurationValidationError, .tooManyURLs)
}
}

View File

@@ -59,13 +59,13 @@ final class SettingsModelTests: XCTestCase {
let core = FakeCoreGateway()
let preferences = Fixtures.preferences()
let model = makeModel(core, preferences: preferences)
model.setRelayMode(.custom)
model.setRelayMode(.strictCustom)
model.setRelayURL(" https://relay.example/ ", at: 0)
model.applyRelayConfiguration()
await waitUntil { preferences.preferences.relayConfiguration.mode == .custom }
let expected = RelayConfiguration(mode: .custom, relayURLs: ["https://relay.example"])
await waitUntil { preferences.preferences.relayConfiguration.mode == .strictCustom }
let expected = RelayConfiguration(mode: .strictCustom, relayURLs: ["https://relay.example"])
XCTAssertEqual(preferences.preferences.relayConfiguration, expected)
XCTAssertEqual(core.initializedNetworkConfigurations, [expected])
XCTAssertFalse(model.state.relayConfigurationIsDirty)
@@ -75,7 +75,7 @@ final class SettingsModelTests: XCTestCase {
let core = FakeCoreGateway()
let preferences = Fixtures.preferences()
let relayURLs = ["https://relay.example", "https://backup.example"]
preferences.setRelayConfiguration(RelayConfiguration(mode: .custom, relayURLs: relayURLs))
preferences.setRelayConfiguration(RelayConfiguration(mode: .strictCustom, relayURLs: relayURLs))
let model = makeModel(core, preferences: preferences)
model.setRelayMode(.automatic)
@@ -86,7 +86,7 @@ final class SettingsModelTests: XCTestCase {
XCTAssertEqual(core.initializedNetworkConfigurations, [
RelayConfiguration(mode: .automatic, relayURLs: relayURLs),
])
model.setRelayMode(.custom)
model.setRelayMode(.strictCustom)
XCTAssertEqual(model.state.relayURLs, relayURLs)
}
@@ -98,7 +98,7 @@ final class SettingsModelTests: XCTestCase {
isInitialized: true,
status: CoreStatus(endpointId: "endpoint", activeTransfers: 0, activeShares: 1)
))
model.setRelayMode(.custom)
model.setRelayMode(.strictCustom)
model.setRelayURL("https://relay.example", at: 0)
model.applyRelayConfiguration()
@@ -114,8 +114,8 @@ final class SettingsModelTests: XCTestCase {
core.initializeResult = .failure(CoreNetworkLifecycleError.activeNetworkWork)
let preferences = Fixtures.preferences()
let model = makeModel(core, preferences: preferences)
let attempted = RelayConfiguration(mode: .custom, relayURLs: ["https://relay.example"])
model.setRelayMode(.custom)
let attempted = RelayConfiguration(mode: .strictCustom, relayURLs: ["https://relay.example"])
model.setRelayMode(.strictCustom)
model.setRelayURL(attempted.relayURLs[0], at: 0)
model.applyRelayConfiguration()
@@ -134,8 +134,8 @@ final class SettingsModelTests: XCTestCase {
core.initializeResults = [.failure(TestError.unimplemented), .success(())]
let preferences = Fixtures.preferences()
let model = makeModel(core, preferences: preferences)
let attempted = RelayConfiguration(mode: .custom, relayURLs: ["https://relay.example"])
model.setRelayMode(.custom)
let attempted = RelayConfiguration(mode: .strictCustom, relayURLs: ["https://relay.example"])
model.setRelayMode(.strictCustom)
model.setRelayURL(attempted.relayURLs[0], at: 0)
model.applyRelayConfiguration()