summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJon Petersson <jon.petersson@mullvad.net>2025-10-21 10:38:25 +0200
committerJon Petersson <jon.petersson@mullvad.net>2025-10-21 10:38:25 +0200
commit073743db9ccd1b28a8ebc71a0541fa63f1f092f2 (patch)
treeb6b251112475ea5a6ab693dd4daacc080232acaf
parente9da35c8462a3b102a451c32968747186ffaec1f (diff)
parent1b6c082c3f0db31a2629e60b45703c34eb831a62 (diff)
downloadmullvadvpn-073743db9ccd1b28a8ebc71a0541fa63f1f092f2.tar.xz
mullvadvpn-073743db9ccd1b28a8ebc71a0541fa63f1f092f2.zip
Merge branch 'store-recents-in-the-settings-ios-1337'
-rw-r--r--ios/MullvadSettings/RecentConnections.swift14
-rw-r--r--ios/MullvadSettings/RecentConnectionsRepository.swift79
-rw-r--r--ios/MullvadSettings/RecentConnectionsRepositoryProtocol.swift17
-rw-r--r--ios/MullvadSettings/SettingsStore.swift1
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj26
-rw-r--r--ios/MullvadVPNTests/MullvadSettings/InMemorySettingsStore.swift19
-rw-r--r--ios/MullvadVPNTests/MullvadVPN/View controllers/SelectLocation/RecentConnectionsRepositoryTests.swift95
7 files changed, 241 insertions, 10 deletions
diff --git a/ios/MullvadSettings/RecentConnections.swift b/ios/MullvadSettings/RecentConnections.swift
new file mode 100644
index 0000000000..d4a83c348b
--- /dev/null
+++ b/ios/MullvadSettings/RecentConnections.swift
@@ -0,0 +1,14 @@
+//
+// RecentConnections.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2025-10-15.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+import MullvadTypes
+
+public struct RecentConnections: Codable, Sendable, Equatable {
+ let isEnabled: Bool
+ let entryLocations: [UserSelectedRelays]
+ let exitLocations: [UserSelectedRelays]
+}
diff --git a/ios/MullvadSettings/RecentConnectionsRepository.swift b/ios/MullvadSettings/RecentConnectionsRepository.swift
new file mode 100644
index 0000000000..b655fd6156
--- /dev/null
+++ b/ios/MullvadSettings/RecentConnectionsRepository.swift
@@ -0,0 +1,79 @@
+//
+// RecentConnectionsRepository.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2025-10-15.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+import MullvadTypes
+
+public enum RecentConnectionsRepositoryError: LocalizedError, Hashable {
+ case recentsDisabled
+
+ public var errorDescription: String? {
+ switch self {
+ case .recentsDisabled:
+ "To add the location to the recents, first enable it in the settings."
+ }
+ }
+}
+
+final class RecentConnectionsRepository: RecentConnectionsRepositoryProtocol {
+ private let store: SettingsStore
+ private let maxLimit: UInt
+
+ private let settingsParser: SettingsParser = {
+ SettingsParser(decoder: JSONDecoder(), encoder: JSONEncoder())
+ }()
+
+ init(store: SettingsStore, maxLimit: UInt = 50) {
+ self.store = store
+ self.maxLimit = maxLimit
+ }
+
+ func setRecentsEnabled(_ isEnabled: Bool) throws {
+ // Clear all recents whenever the recents feature status changes.
+ try write(RecentConnections(isEnabled: isEnabled, entryLocations: [], exitLocations: []))
+ }
+
+ func add(_ location: UserSelectedRelays, as type: RecentLocationType) throws {
+ let current = try read()
+ guard current.isEnabled else { throw RecentConnectionsRepositoryError.recentsDisabled }
+ var currentList = current[keyPath: keyPath(for: type)]
+ if let idx = currentList.firstIndex(of: location) { currentList.remove(at: idx) }
+ currentList.insert(location, at: 0)
+ currentList = Array(currentList.prefix(Int(maxLimit)))
+
+ let new =
+ (type == .entry)
+ ? RecentConnections(
+ isEnabled: current.isEnabled, entryLocations: currentList, exitLocations: current.exitLocations)
+ : RecentConnections(
+ isEnabled: current.isEnabled, entryLocations: current.entryLocations, exitLocations: currentList)
+
+ try write(new)
+ }
+
+ func all() throws -> RecentConnections {
+ try read()
+ }
+}
+
+private extension RecentConnectionsRepository {
+ private func keyPath(for type: RecentLocationType) -> KeyPath<RecentConnections, [UserSelectedRelays]> {
+ switch type {
+ case .entry: return \.entryLocations
+ case .exit: return \.exitLocations
+ }
+ }
+
+ private func read() throws -> RecentConnections {
+ let data = try store.read(key: .recentConnections)
+ return try settingsParser.parseUnversionedPayload(as: RecentConnections.self, from: data)
+ }
+
+ private func write(_ value: RecentConnections) throws {
+ let data = try settingsParser.produceUnversionedPayload(value)
+ try store.write(data, for: .recentConnections)
+ }
+}
diff --git a/ios/MullvadSettings/RecentConnectionsRepositoryProtocol.swift b/ios/MullvadSettings/RecentConnectionsRepositoryProtocol.swift
new file mode 100644
index 0000000000..a7521e6f56
--- /dev/null
+++ b/ios/MullvadSettings/RecentConnectionsRepositoryProtocol.swift
@@ -0,0 +1,17 @@
+//
+// RecentConnectionsRepositoryProtocol.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2025-10-15.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+import MullvadTypes
+
+public enum RecentLocationType: CaseIterable {
+ case entry, exit
+}
+public protocol RecentConnectionsRepositoryProtocol {
+ func setRecentsEnabled(_ isEnabled: Bool) throws
+ func add(_ location: UserSelectedRelays, as: RecentLocationType) throws
+ func all() throws -> RecentConnections
+}
diff --git a/ios/MullvadSettings/SettingsStore.swift b/ios/MullvadSettings/SettingsStore.swift
index 78e35e3258..fea9e8d183 100644
--- a/ios/MullvadSettings/SettingsStore.swift
+++ b/ios/MullvadSettings/SettingsStore.swift
@@ -16,6 +16,7 @@ public enum SettingsKey: String, CaseIterable, Sendable {
case customRelayLists = "CustomRelayLists"
case lastUsedAccount = "LastUsedAccount"
case shouldWipeSettings = "ShouldWipeSettings"
+ case recentConnections = "RecentConnections"
}
public protocol SettingsStore: Sendable {
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 2c9b6df726..631f52e6bc 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -978,6 +978,9 @@
F050AE5E2B739A73003F4EDB /* LocationDataSourceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE5D2B739A73003F4EDB /* LocationDataSourceProtocol.swift */; };
F050AE602B73A41E003F4EDB /* AllLocationDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE5F2B73A41E003F4EDB /* AllLocationDataSource.swift */; };
F050AE622B74DBAC003F4EDB /* CustomListsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F050AE612B74DBAC003F4EDB /* CustomListsDataSource.swift */; };
+ F053601D2EA03D8A00D6EDFF /* RecentConnectionsRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F00FEA192E9FC0A600F7EC6C /* RecentConnectionsRepository.swift */; };
+ F053601E2EA03D8A00D6EDFF /* RecentConnections.swift in Sources */ = {isa = PBXBuildFile; fileRef = F00FEA1D2E9FCF2D00F7EC6C /* RecentConnections.swift */; };
+ F053601F2EA03D8A00D6EDFF /* RecentConnectionsRepositoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F00FEA1F2E9FCF7C00F7EC6C /* RecentConnectionsRepositoryProtocol.swift */; };
F0570CD12C4FB8E1007BDF2D /* EphemeralPeerExchangingPipeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = F05919762C453FAF00C301F3 /* EphemeralPeerExchangingPipeline.swift */; };
F0570CD22C4FB8E1007BDF2D /* SingleHopEphemeralPeerExchanger.swift in Sources */ = {isa = PBXBuildFile; fileRef = F05919782C45402E00C301F3 /* SingleHopEphemeralPeerExchanger.swift */; };
F0570CD42C4FB8E1007BDF2D /* MultiHopEphemeralPeerExchanger.swift in Sources */ = {isa = PBXBuildFile; fileRef = F059197C2C454C9200C301F3 /* MultiHopEphemeralPeerExchanger.swift */; };
@@ -1097,6 +1100,7 @@
F0E8E4C32A602E0D00ED26A3 /* AccountDeletionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8E4C22A602E0D00ED26A3 /* AccountDeletionViewModel.swift */; };
F0EF50D52A949F8E0031E8DF /* ChangeLogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0EF50D42A949F8E0031E8DF /* ChangeLogViewModel.swift */; };
F0F146942D9462E100BF78E7 /* RustProblemReportRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F146912D94491200BF78E7 /* RustProblemReportRequestTests.swift */; };
+ F0F313A72E85321D00D55C43 /* RecentConnectionsRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F313A62E85320D00D55C43 /* RecentConnectionsRepositoryTests.swift */; };
F0F316192BF3572B0078DBCF /* RelaySelectorResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F316182BF3572B0078DBCF /* RelaySelectorResult.swift */; };
F0F3161B2BF358590078DBCF /* NoRelaysSatisfyingConstraintsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F3161A2BF358590078DBCF /* NoRelaysSatisfyingConstraintsError.swift */; };
F0F56B092C0E058A009D676B /* ObserverList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CC40EE24A601900019D96E /* ObserverList.swift */; };
@@ -2410,6 +2414,9 @@
E158B35F285381C60002F069 /* String+AccountFormatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+AccountFormatting.swift"; sourceTree = "<group>"; };
E1FD0DF428AA7CE400299DB4 /* StatusActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActivityView.swift; sourceTree = "<group>"; };
F006CCFB2B99CC8400C6C2AC /* EditLocationsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditLocationsCoordinator.swift; sourceTree = "<group>"; };
+ F00FEA192E9FC0A600F7EC6C /* RecentConnectionsRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentConnectionsRepository.swift; sourceTree = "<group>"; };
+ F00FEA1D2E9FCF2D00F7EC6C /* RecentConnections.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentConnections.swift; sourceTree = "<group>"; };
+ F00FEA1F2E9FCF7C00F7EC6C /* RecentConnectionsRepositoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentConnectionsRepositoryProtocol.swift; sourceTree = "<group>"; };
F011221C2E4A3275005DD108 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
F01528BA2BFF3FEE00B01D00 /* ShadowsocksRelaySelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksRelaySelector.swift; sourceTree = "<group>"; };
F0164EB92B4456D30020268D /* AccessMethodRepository+Stub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccessMethodRepository+Stub.swift"; sourceTree = "<group>"; };
@@ -2555,6 +2562,7 @@
F0EF50D42A949F8E0031E8DF /* ChangeLogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangeLogViewModel.swift; sourceTree = "<group>"; };
F0F146912D94491200BF78E7 /* RustProblemReportRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RustProblemReportRequestTests.swift; sourceTree = "<group>"; };
F0F1EF8C2BE8FF0A00CED01D /* LaunchArguments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchArguments.swift; sourceTree = "<group>"; };
+ F0F313A62E85320D00D55C43 /* RecentConnectionsRepositoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentConnectionsRepositoryTests.swift; sourceTree = "<group>"; };
F0F316182BF3572B0078DBCF /* RelaySelectorResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorResult.swift; sourceTree = "<group>"; };
F0F3161A2BF358590078DBCF /* NoRelaysSatisfyingConstraintsError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoRelaysSatisfyingConstraintsError.swift; sourceTree = "<group>"; };
F0FA16082D7F0413007E2546 /* FilterDescriptorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterDescriptorTests.swift; sourceTree = "<group>"; };
@@ -3071,8 +3079,8 @@
581943F228F8014500B0CB5E /* MullvadTypes */ = {
isa = PBXGroup;
children = (
- F0DDE4132B220458006B57A7 /* ShadowsocksConfiguration.swift */,
588D7ED72AF3A533005DF40A /* AccessMethodKind.swift */,
+ A97687C72DD60D5D000D96E8 /* AddressCacheProviding.swift */,
584D26BE270C550B004EA533 /* AnyIPAddress.swift */,
586A951329013235007BAF2B /* AnyIPEndpoint.swift */,
06AC113628F83FD70037AF9A /* Cancellable.swift */,
@@ -3105,14 +3113,14 @@
5898D2AF2902A67C00EB5EBA /* RelayLocation.swift */,
581DA2722A1E227D0046ED47 /* RESTTypes.swift */,
58F1311427E0B2AB007AC5BC /* Result+Extensions.swift */,
+ A98207EE2D9192A300654558 /* ShadowsocksBridgeProviding.swift */,
58DFF7D92B02862E00F864E0 /* ShadowsocksCipherOptions.swift */,
+ F0DDE4132B220458006B57A7 /* ShadowsocksConfiguration.swift */,
F924C5A32DA65F28001F4660 /* Storekit2.swift */,
F0ADF1CC2CFDFF3100299F09 /* StringConversionError.swift */,
+ A9711B2A2D662AE3003DA71D /* SwiftConnectionModeProvider.swift */,
A91614D02B108D1B00F416EB /* TransportLayer.swift */,
58E511E028DDB7F100B0BCDE /* WrappingError.swift */,
- A9711B2A2D662AE3003DA71D /* SwiftConnectionModeProvider.swift */,
- A98207EE2D9192A300654558 /* ShadowsocksBridgeProviding.swift */,
- A97687C72DD60D5D000D96E8 /* AddressCacheProviding.swift */,
);
path = MullvadTypes;
sourceTree = "<group>";
@@ -3804,6 +3812,9 @@
58B2FDD52AA71D2A003EB5C6 /* MullvadSettings.h */,
F0E61CA92BF2911D000C4A95 /* MultihopSettings.swift */,
44DD7D2C2B74E44A0005F67F /* QuantumResistanceSettings.swift */,
+ F00FEA1D2E9FCF2D00F7EC6C /* RecentConnections.swift */,
+ F00FEA192E9FC0A600F7EC6C /* RecentConnectionsRepository.swift */,
+ F00FEA1F2E9FCF7C00F7EC6C /* RecentConnectionsRepositoryProtocol.swift */,
58FF2C02281BDE02009EF542 /* SettingsManager.swift */,
06410E03292D0F7100AFC18C /* SettingsParser.swift */,
06410E06292D108E00AFC18C /* SettingsStore.swift */,
@@ -4444,9 +4455,10 @@
isa = PBXGroup;
children = (
7A9BE5AC2B90DF2D00E2A7D0 /* AllLocationsDataSourceTests.swift */,
- 7A9BE5A42B90760C00E2A7D0 /* CustomListsDataSourceTests.swift */,
F050AE5B2B73797D003F4EDB /* CustomListRepositoryTests.swift */,
+ 7A9BE5A42B90760C00E2A7D0 /* CustomListsDataSourceTests.swift */,
7A9BE5A12B8F88C500E2A7D0 /* LocationNodeTests.swift */,
+ F0F313A62E85320D00D55C43 /* RecentConnectionsRepositoryTests.swift */,
);
path = SelectLocation;
sourceTree = "<group>";
@@ -6212,6 +6224,7 @@
F073FCB32C6617D70062EA1D /* TunnelStore+Stubs.swift in Sources */,
58DFF7D32B02570000F864E0 /* MarkdownStylingOptions.swift in Sources */,
A9A5FA342ACB05160083449F /* StringTests.swift in Sources */,
+ F0F313A72E85321D00D55C43 /* RecentConnectionsRepositoryTests.swift in Sources */,
7A52F96C2C17450C00B133B9 /* RelaySelectorWrapperTests.swift in Sources */,
A9A5FA352ACB05160083449F /* WgKeyRotationTests.swift in Sources */,
F0A7EBB62CF092CC005BB671 /* ApplicationConfiguration.swift in Sources */,
@@ -6261,6 +6274,9 @@
7A5869BC2B56EF3400640D27 /* IPOverrideRepository.swift in Sources */,
7A9F293B2CAC4443005F2089 /* InfoHeaderConfig.swift in Sources */,
F0E61CAB2BF2911D000C4A95 /* MultihopSettings.swift in Sources */,
+ F053601D2EA03D8A00D6EDFF /* RecentConnectionsRepository.swift in Sources */,
+ F053601E2EA03D8A00D6EDFF /* RecentConnections.swift in Sources */,
+ F053601F2EA03D8A00D6EDFF /* RecentConnectionsRepositoryProtocol.swift in Sources */,
58B2FDE82AA71D5C003EB5C6 /* KeychainSettingsStore.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
diff --git a/ios/MullvadVPNTests/MullvadSettings/InMemorySettingsStore.swift b/ios/MullvadVPNTests/MullvadSettings/InMemorySettingsStore.swift
index dcdb108a9b..f62b3b1dcb 100644
--- a/ios/MullvadVPNTests/MullvadSettings/InMemorySettingsStore.swift
+++ b/ios/MullvadVPNTests/MullvadSettings/InMemorySettingsStore.swift
@@ -15,21 +15,30 @@ protocol Instantiable {
class InMemorySettingsStore<ThrownError: Error>: SettingsStore, @unchecked Sendable where ThrownError: Instantiable {
private var settings = [SettingsKey: Data]()
+ let queue = DispatchQueue(label: "com.mullvad.vpn.tests.inMemorySettingsStore")
func read(key: SettingsKey) throws -> Data {
- guard settings.keys.contains(key), let value = settings[key] else { throw ThrownError() }
- return value
+ try queue.sync {
+ guard let value = settings[key] else { throw ThrownError() }
+ return value
+ }
}
func write(_ data: Data, for key: SettingsKey) throws {
- settings[key] = data
+ queue.sync {
+ self.settings[key] = data
+ }
}
func delete(key: SettingsKey) throws {
- settings.removeValue(forKey: key)
+ queue.sync {
+ _ = self.settings.removeValue(forKey: key)
+ }
}
func reset() {
- settings.removeAll()
+ queue.sync {
+ self.settings.removeAll()
+ }
}
}
diff --git a/ios/MullvadVPNTests/MullvadVPN/View controllers/SelectLocation/RecentConnectionsRepositoryTests.swift b/ios/MullvadVPNTests/MullvadVPN/View controllers/SelectLocation/RecentConnectionsRepositoryTests.swift
new file mode 100644
index 0000000000..2d63e64563
--- /dev/null
+++ b/ios/MullvadVPNTests/MullvadVPN/View controllers/SelectLocation/RecentConnectionsRepositoryTests.swift
@@ -0,0 +1,95 @@
+//
+// RecentConnectionsRepositoryTests.swift
+// MullvadVPN
+//
+// Created by Mojgan on 2025-09-25.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+import Testing
+
+@testable import MullvadSettings
+@testable import MullvadTypes
+
+@Suite("RecentConnectionsRepositoryTests")
+final class RecentConnectionsRepositoryTests {
+ let se = UserSelectedRelays(locations: [.country("se")])
+ let fr = UserSelectedRelays(locations: [.country("fr")])
+ let nl = UserSelectedRelays(locations: [.country("nl")])
+ let de = UserSelectedRelays(locations: [.country("de")])
+
+ @Test("Adds locations up to the limit 1 for either entry or exit")
+ func addLocations() throws {
+ let maxLimit: UInt = 1
+ let repository = makeRepository(max: maxLimit)
+ try repository.setRecentsEnabled(true)
+ try addLocations(repository, locations: [se, de], as: .entry)
+ try addLocations(repository, locations: [de], as: .exit)
+
+ let recentsSettings = try repository.all()
+ #expect(recentsSettings.isEnabled)
+ #expect(recentsSettings.exitLocations.count == maxLimit)
+ #expect(recentsSettings.entryLocations.count == maxLimit)
+ }
+
+ @Test("Adds locations up to the default limit (50) for either entry or exit")
+ func addDuplicate() throws {
+ let repository = makeRepository()
+ try repository.setRecentsEnabled(true)
+ try addLocations(repository, locations: [se, de], as: .entry)
+ try addLocations(repository, locations: [de, se, nl, se], as: .exit)
+
+ let recentsSettings = try repository.all()
+ #expect(recentsSettings.isEnabled)
+ #expect(recentsSettings.entryLocations.count == 2)
+ #expect(recentsSettings.exitLocations.count == 3)
+ }
+
+ @Test("Removes all recents connections with disabling recents")
+ func disable() throws {
+ let repository = makeRepository()
+ try repository.setRecentsEnabled(true)
+ try addLocations(repository, locations: [se, de], as: .entry)
+ try addLocations(repository, locations: [de, se, nl], as: .exit)
+
+ var recentConnections = try repository.all()
+ #expect(recentConnections.isEnabled)
+ #expect(recentConnections.entryLocations.count == 2)
+ #expect(recentConnections.exitLocations.count == 3)
+
+ try repository.setRecentsEnabled(false)
+
+ recentConnections = try repository.all()
+ #expect(!recentConnections.isEnabled)
+ #expect(recentConnections.entryLocations.count == 0)
+ #expect(recentConnections.exitLocations.count == 0)
+
+ }
+
+ @Test("Fails with an error if a location is added while recents are disabled.")
+ func addRecentsBeforeEnablingRecents() throws {
+ let repository = makeRepository()
+
+ try repository.setRecentsEnabled(false)
+ let action: () throws -> Void = { [self] in
+ try addLocations(
+ repository,
+ locations: [self.se],
+ as: RecentLocationType.entry
+ )
+ }
+
+ #expect(throws: RecentConnectionsRepositoryError.recentsDisabled, performing: action)
+ }
+
+ private func makeRepository(max: UInt = 50) -> RecentConnectionsRepository {
+ return RecentConnectionsRepository(store: InMemorySettingsStore<SettingNotFound>(), maxLimit: max)
+ }
+
+ private func addLocations(
+ _ repository: RecentConnectionsRepository, locations: [UserSelectedRelays], as type: RecentLocationType
+ ) throws {
+ for location in locations {
+ try repository.add(location, as: type)
+ }
+ }
+}