Move ModulePreferences to Profile.userInfo (#993)
Store module preferences in the Profile.userInfo field for atomicity. Access and modification are dramatically simplified, and synchronization comes for free. On the other side, fix provider preferences synchronization by using viewContext for the CloudKit container. Fixes #992
This commit is contained in:
parent
912f62b29e
commit
aeec943c58
|
@ -33,6 +33,5 @@ final class CDExcludedEndpoint: NSManagedObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
@NSManaged var endpoint: String?
|
@NSManaged var endpoint: String?
|
||||||
@NSManaged var modulePreferences: CDModulePreferencesV3?
|
|
||||||
@NSManaged var providerPreferences: CDProviderPreferencesV3?
|
@NSManaged var providerPreferences: CDProviderPreferencesV3?
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
//
|
|
||||||
// CDModulePreferencesV3.swift
|
|
||||||
// Passepartout
|
|
||||||
//
|
|
||||||
// Created by Davide De Rosa on 12/5/24.
|
|
||||||
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
|
||||||
//
|
|
||||||
// https://github.com/passepartoutvpn
|
|
||||||
//
|
|
||||||
// This file is part of Passepartout.
|
|
||||||
//
|
|
||||||
// Passepartout is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// Passepartout is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
//
|
|
||||||
|
|
||||||
import CoreData
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
@objc(CDModulePreferencesV3)
|
|
||||||
final class CDModulePreferencesV3: NSManagedObject {
|
|
||||||
@nonobjc static func fetchRequest() -> NSFetchRequest<CDModulePreferencesV3> {
|
|
||||||
NSFetchRequest<CDModulePreferencesV3>(entityName: "CDModulePreferencesV3")
|
|
||||||
}
|
|
||||||
|
|
||||||
@NSManaged var uuid: UUID?
|
|
||||||
@NSManaged var lastUpdate: Date?
|
|
||||||
@NSManaged var excludedEndpoints: Set<CDExcludedEndpoint>?
|
|
||||||
}
|
|
|
@ -2,14 +2,8 @@
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23507" systemVersion="23H222" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
|
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23507" systemVersion="23H222" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
|
||||||
<entity name="CDExcludedEndpoint" representedClassName="CDExcludedEndpoint" syncable="YES">
|
<entity name="CDExcludedEndpoint" representedClassName="CDExcludedEndpoint" syncable="YES">
|
||||||
<attribute name="endpoint" optional="YES" attributeType="String"/>
|
<attribute name="endpoint" optional="YES" attributeType="String"/>
|
||||||
<relationship name="modulePreferences" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CDModulePreferencesV3" inverseName="excludedEndpoints" inverseEntity="CDModulePreferencesV3"/>
|
|
||||||
<relationship name="providerPreferences" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CDProviderPreferencesV3" inverseName="excludedEndpoints" inverseEntity="CDProviderPreferencesV3"/>
|
<relationship name="providerPreferences" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CDProviderPreferencesV3" inverseName="excludedEndpoints" inverseEntity="CDProviderPreferencesV3"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="CDModulePreferencesV3" representedClassName="CDModulePreferencesV3" syncable="YES">
|
|
||||||
<attribute name="lastUpdate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
|
||||||
<attribute name="uuid" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
|
||||||
<relationship name="excludedEndpoints" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="CDExcludedEndpoint" inverseName="modulePreferences" inverseEntity="CDExcludedEndpoint"/>
|
|
||||||
</entity>
|
|
||||||
<entity name="CDProviderPreferencesV3" representedClassName="CDProviderPreferencesV3" syncable="YES">
|
<entity name="CDProviderPreferencesV3" representedClassName="CDProviderPreferencesV3" syncable="YES">
|
||||||
<attribute name="favoriteServerIds" optional="YES" attributeType="Binary"/>
|
<attribute name="favoriteServerIds" optional="YES" attributeType="Binary"/>
|
||||||
<attribute name="lastUpdate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="lastUpdate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
|
|
@ -1,121 +0,0 @@
|
||||||
//
|
|
||||||
// CDModulePreferencesRepositoryV3.swift
|
|
||||||
// Passepartout
|
|
||||||
//
|
|
||||||
// Created by Davide De Rosa on 12/5/24.
|
|
||||||
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
|
||||||
//
|
|
||||||
// https://github.com/passepartoutvpn
|
|
||||||
//
|
|
||||||
// This file is part of Passepartout.
|
|
||||||
//
|
|
||||||
// Passepartout is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// Passepartout is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
//
|
|
||||||
|
|
||||||
import AppData
|
|
||||||
import CommonLibrary
|
|
||||||
import CoreData
|
|
||||||
import Foundation
|
|
||||||
import PassepartoutKit
|
|
||||||
|
|
||||||
extension AppData {
|
|
||||||
public static func cdModulePreferencesRepositoryV3(context: NSManagedObjectContext, moduleId: UUID) throws -> ModulePreferencesRepository {
|
|
||||||
try CDModulePreferencesRepositoryV3(context: context, moduleId: moduleId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class CDModulePreferencesRepositoryV3: ModulePreferencesRepository {
|
|
||||||
private nonisolated let context: NSManagedObjectContext
|
|
||||||
|
|
||||||
private let entity: CDModulePreferencesV3
|
|
||||||
|
|
||||||
init(context: NSManagedObjectContext, moduleId: UUID) throws {
|
|
||||||
self.context = context
|
|
||||||
|
|
||||||
entity = try context.performAndWait {
|
|
||||||
let request = CDModulePreferencesV3.fetchRequest()
|
|
||||||
request.predicate = NSPredicate(format: "uuid == %@", moduleId.uuidString)
|
|
||||||
request.sortDescriptors = [.init(key: "lastUpdate", ascending: false)]
|
|
||||||
do {
|
|
||||||
let entities = try request.execute()
|
|
||||||
|
|
||||||
// dedup by lastUpdate
|
|
||||||
entities.enumerated().forEach {
|
|
||||||
guard $0.offset > 0 else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
$0.element.excludedEndpoints?.forEach(context.delete(_:))
|
|
||||||
context.delete($0.element)
|
|
||||||
}
|
|
||||||
|
|
||||||
let entity = entities.first ?? CDModulePreferencesV3(context: context)
|
|
||||||
entity.uuid = moduleId
|
|
||||||
entity.lastUpdate = Date()
|
|
||||||
return entity
|
|
||||||
} catch {
|
|
||||||
pp_log(.app, .error, "Unable to load preferences for module \(moduleId): \(error)")
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isExcludedEndpoint(_ endpoint: ExtendedEndpoint) -> Bool {
|
|
||||||
context.performAndWait {
|
|
||||||
entity.excludedEndpoints?.contains {
|
|
||||||
$0.endpoint == endpoint.rawValue
|
|
||||||
} ?? false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func addExcludedEndpoint(_ endpoint: ExtendedEndpoint) {
|
|
||||||
context.performAndWait {
|
|
||||||
let mapper = CoreDataMapper(context: context)
|
|
||||||
let cdEndpoint = mapper.cdExcludedEndpoint(from: endpoint)
|
|
||||||
cdEndpoint.modulePreferences = entity
|
|
||||||
entity.excludedEndpoints?.insert(cdEndpoint)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeExcludedEndpoint(_ endpoint: ExtendedEndpoint) {
|
|
||||||
context.performAndWait {
|
|
||||||
guard let found = entity.excludedEndpoints?.first(where: {
|
|
||||||
$0.endpoint == endpoint.rawValue
|
|
||||||
}) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
entity.excludedEndpoints?.remove(found)
|
|
||||||
context.delete(found)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func save() throws {
|
|
||||||
try context.performAndWait {
|
|
||||||
guard context.hasChanges else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
do {
|
|
||||||
try context.save()
|
|
||||||
} catch {
|
|
||||||
context.rollback()
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func discard() {
|
|
||||||
context.performAndWait {
|
|
||||||
context.rollback()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -51,9 +51,6 @@ struct OpenVPNView: View, ModuleDraftEditing {
|
||||||
@State
|
@State
|
||||||
private var paywallReason: PaywallReason?
|
private var paywallReason: PaywallReason?
|
||||||
|
|
||||||
@StateObject
|
|
||||||
private var preferences = ModulePreferences()
|
|
||||||
|
|
||||||
@StateObject
|
@StateObject
|
||||||
private var providerPreferences = ProviderPreferences()
|
private var providerPreferences = ProviderPreferences()
|
||||||
|
|
||||||
|
@ -88,13 +85,6 @@ struct OpenVPNView: View, ModuleDraftEditing {
|
||||||
.navigationDestination(for: Subroute.self, destination: destination)
|
.navigationDestination(for: Subroute.self, destination: destination)
|
||||||
.themeAnimation(on: draft.wrappedValue.providerId, category: .modules)
|
.themeAnimation(on: draft.wrappedValue.providerId, category: .modules)
|
||||||
.withErrorHandler(errorHandler)
|
.withErrorHandler(errorHandler)
|
||||||
.onLoad {
|
|
||||||
editor.loadPreferences(
|
|
||||||
preferences,
|
|
||||||
from: preferencesManager,
|
|
||||||
forModuleWithId: module.id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -213,7 +203,7 @@ private extension OpenVPNView {
|
||||||
if draft.wrappedValue.providerSelection != nil {
|
if draft.wrappedValue.providerSelection != nil {
|
||||||
return providerPreferences.excludedEndpoints()
|
return providerPreferences.excludedEndpoints()
|
||||||
} else {
|
} else {
|
||||||
return preferences.excludedEndpoints()
|
return editor.excludedEndpoints(for: module.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,17 +28,11 @@ import Foundation
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
|
|
||||||
public final class PreferencesManager: ObservableObject, Sendable {
|
public final class PreferencesManager: ObservableObject, Sendable {
|
||||||
private let modulesFactory: @Sendable (UUID) throws -> ModulePreferencesRepository
|
|
||||||
|
|
||||||
private let providersFactory: @Sendable (ProviderID) throws -> ProviderPreferencesRepository
|
private let providersFactory: @Sendable (ProviderID) throws -> ProviderPreferencesRepository
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
modulesFactory: (@Sendable (UUID) throws -> ModulePreferencesRepository)? = nil,
|
|
||||||
providersFactory: (@Sendable (ProviderID) throws -> ProviderPreferencesRepository)? = nil
|
providersFactory: (@Sendable (ProviderID) throws -> ProviderPreferencesRepository)? = nil
|
||||||
) {
|
) {
|
||||||
self.modulesFactory = modulesFactory ?? { _ in
|
|
||||||
DummyModulePreferencesRepository()
|
|
||||||
}
|
|
||||||
self.providersFactory = providersFactory ?? { _ in
|
self.providersFactory = providersFactory ?? { _ in
|
||||||
DummyProviderPreferencesRepository()
|
DummyProviderPreferencesRepository()
|
||||||
}
|
}
|
||||||
|
@ -46,10 +40,6 @@ public final class PreferencesManager: ObservableObject, Sendable {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension PreferencesManager {
|
extension PreferencesManager {
|
||||||
public func preferencesRepository(forModuleWithId moduleId: UUID) throws -> ModulePreferencesRepository {
|
|
||||||
try modulesFactory(moduleId)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func preferencesRepository(forProviderWithId providerId: ProviderID) throws -> ProviderPreferencesRepository {
|
public func preferencesRepository(forProviderWithId providerId: ProviderID) throws -> ProviderPreferencesRepository {
|
||||||
try providersFactory(providerId)
|
try providersFactory(providerId)
|
||||||
}
|
}
|
||||||
|
@ -57,12 +47,6 @@ extension PreferencesManager {
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
extension PreferencesManager {
|
extension PreferencesManager {
|
||||||
public func preferences(forModuleWithId moduleId: UUID) throws -> ModulePreferences {
|
|
||||||
let object = ModulePreferences()
|
|
||||||
object.repository = try modulesFactory(moduleId)
|
|
||||||
return object
|
|
||||||
}
|
|
||||||
|
|
||||||
public func preferences(forProviderWithId providerId: ProviderID) throws -> ProviderPreferences {
|
public func preferences(forProviderWithId providerId: ProviderID) throws -> ProviderPreferences {
|
||||||
let object = ProviderPreferences()
|
let object = ProviderPreferences()
|
||||||
object.repository = try providersFactory(providerId)
|
object.repository = try providersFactory(providerId)
|
||||||
|
@ -72,24 +56,6 @@ extension PreferencesManager {
|
||||||
|
|
||||||
// MARK: - Dummy
|
// MARK: - Dummy
|
||||||
|
|
||||||
private final class DummyModulePreferencesRepository: ModulePreferencesRepository {
|
|
||||||
func isExcludedEndpoint(_ endpoint: ExtendedEndpoint) -> Bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
func addExcludedEndpoint(_ endpoint: ExtendedEndpoint) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeExcludedEndpoint(_ endpoint: ExtendedEndpoint) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func save() throws {
|
|
||||||
}
|
|
||||||
|
|
||||||
func discard() {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class DummyProviderPreferencesRepository: ProviderPreferencesRepository {
|
private final class DummyProviderPreferencesRepository: ProviderPreferencesRepository {
|
||||||
var favoriteServers: Set<String> = []
|
var favoriteServers: Set<String> = []
|
||||||
|
|
||||||
|
|
|
@ -440,15 +440,15 @@ private extension ProfileManager {
|
||||||
pp_log(.App.profiles, .info, "Start importing remote profiles: \(profiles.map(\.id))")
|
pp_log(.App.profiles, .info, "Start importing remote profiles: \(profiles.map(\.id))")
|
||||||
assert(profiles.count == Set(profiles.map(\.id)).count, "Remote repository must not have duplicates")
|
assert(profiles.count == Set(profiles.map(\.id)).count, "Remote repository must not have duplicates")
|
||||||
|
|
||||||
pp_log(.App.profiles, .debug, "Local attributes:")
|
pp_log(.App.profiles, .debug, "Local fingerprints:")
|
||||||
let localAttributes: [Profile.ID: ProfileAttributes] = allProfiles.values.reduce(into: [:]) {
|
let localFingerprints: [Profile.ID: UUID] = allProfiles.values.reduce(into: [:]) {
|
||||||
$0[$1.id] = $1.attributes
|
$0[$1.id] = $1.attributes.fingerprint
|
||||||
pp_log(.App.profiles, .debug, "\t\($1.id) = \($1.attributes)")
|
pp_log(.App.profiles, .debug, "\t\($1.id) = \($1.attributes.fingerprint.debugDescription)")
|
||||||
}
|
}
|
||||||
pp_log(.App.profiles, .debug, "Remote attributes:")
|
pp_log(.App.profiles, .debug, "Remote fingerprints:")
|
||||||
let remoteAttributes: [Profile.ID: ProfileAttributes] = profiles.reduce(into: [:]) {
|
let remoteFingerprints: [Profile.ID: UUID] = profiles.reduce(into: [:]) {
|
||||||
$0[$1.id] = $1.attributes
|
$0[$1.id] = $1.attributes.fingerprint
|
||||||
pp_log(.App.profiles, .debug, "\t\($1.id) = \($1.attributes)")
|
pp_log(.App.profiles, .debug, "\t\($1.id) = \($1.attributes.fingerprint.debugDescription)")
|
||||||
}
|
}
|
||||||
|
|
||||||
let remotelyDeletedIds = Set(allProfiles.keys).subtracting(Set(allRemoteProfiles.keys))
|
let remotelyDeletedIds = Set(allProfiles.keys).subtracting(Set(allRemoteProfiles.keys))
|
||||||
|
@ -473,8 +473,8 @@ private extension ProfileManager {
|
||||||
idsToRemove.append(remoteProfile.id)
|
idsToRemove.append(remoteProfile.id)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if let localFingerprint = localAttributes[remoteProfile.id]?.fingerprint {
|
if let localFingerprint = localFingerprints[remoteProfile.id] {
|
||||||
guard let remoteFingerprint = remoteAttributes[remoteProfile.id]?.fingerprint,
|
guard let remoteFingerprint = remoteFingerprints[remoteProfile.id],
|
||||||
remoteFingerprint != localFingerprint else {
|
remoteFingerprint != localFingerprint else {
|
||||||
pp_log(.App.profiles, .info, "Skip re-importing local profile \(remoteProfile.id)")
|
pp_log(.App.profiles, .info, "Skip re-importing local profile \(remoteProfile.id)")
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -73,17 +73,6 @@ public struct EditableProfile: MutableProfileType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension EditableProfile {
|
|
||||||
public var attributes: ProfileAttributes {
|
|
||||||
get {
|
|
||||||
userInfo() ?? ProfileAttributes()
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
setUserInfo(newValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Profile {
|
extension Profile {
|
||||||
public func editable() -> EditableProfile {
|
public func editable() -> EditableProfile {
|
||||||
EditableProfile(
|
EditableProfile(
|
||||||
|
@ -112,6 +101,8 @@ extension Module {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: -
|
||||||
|
|
||||||
private extension EditableProfile {
|
private extension EditableProfile {
|
||||||
var activeConnectionModule: (any ModuleBuilder)? {
|
var activeConnectionModule: (any ModuleBuilder)? {
|
||||||
modules.first {
|
modules.first {
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
//
|
|
||||||
// ModulePreferences.swift
|
|
||||||
// Passepartout
|
|
||||||
//
|
|
||||||
// Created by Davide De Rosa on 12/5/24.
|
|
||||||
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
|
||||||
//
|
|
||||||
// https://github.com/passepartoutvpn
|
|
||||||
//
|
|
||||||
// This file is part of Passepartout.
|
|
||||||
//
|
|
||||||
// Passepartout is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// Passepartout is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
//
|
|
||||||
|
|
||||||
import CommonUtils
|
|
||||||
import Foundation
|
|
||||||
import PassepartoutKit
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
public final class ModulePreferences: ObservableObject {
|
|
||||||
public var repository: ModulePreferencesRepository?
|
|
||||||
|
|
||||||
public init() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public func excludedEndpoints() -> ObservableList<ExtendedEndpoint> {
|
|
||||||
ObservableList { [weak self] in
|
|
||||||
self?.repository?.isExcludedEndpoint($0) == true
|
|
||||||
} add: { [weak self] in
|
|
||||||
self?.repository?.addExcludedEndpoint($0)
|
|
||||||
} remove: { [weak self] in
|
|
||||||
self?.repository?.removeExcludedEndpoint($0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
//
|
||||||
|
// ProfileAttributes+ModulePreferences.swift
|
||||||
|
// Passepartout
|
||||||
|
//
|
||||||
|
// Created by Davide De Rosa on 12/9/24.
|
||||||
|
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
||||||
|
//
|
||||||
|
// https://github.com/passepartoutvpn
|
||||||
|
//
|
||||||
|
// This file is part of Passepartout.
|
||||||
|
//
|
||||||
|
// Passepartout is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Passepartout is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CommonUtils
|
||||||
|
import Foundation
|
||||||
|
import GenericJSON
|
||||||
|
import PassepartoutKit
|
||||||
|
|
||||||
|
extension ProfileAttributes {
|
||||||
|
public struct ModulePreferences {
|
||||||
|
private enum Key: String {
|
||||||
|
case excludedEndpoints
|
||||||
|
}
|
||||||
|
|
||||||
|
private(set) var userInfo: [String: AnyHashable]
|
||||||
|
|
||||||
|
init(userInfo: [String: AnyHashable]?) {
|
||||||
|
self.userInfo = userInfo ?? [:]
|
||||||
|
}
|
||||||
|
|
||||||
|
public func isExcludedEndpoint(_ endpoint: ExtendedEndpoint) -> Bool {
|
||||||
|
excludedEndpoints.contains(endpoint.rawValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
public mutating func addExcludedEndpoint(_ endpoint: ExtendedEndpoint) {
|
||||||
|
excludedEndpoints.append(endpoint.rawValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
public mutating func removeExcludedEndpoint(_ endpoint: ExtendedEndpoint) {
|
||||||
|
let rawValue = endpoint.rawValue
|
||||||
|
excludedEndpoints.removeAll {
|
||||||
|
$0 == rawValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ProfileAttributes.ModulePreferences {
|
||||||
|
var excludedEndpoints: [String] {
|
||||||
|
get {
|
||||||
|
userInfo[Key.excludedEndpoints.rawValue] as? [String] ?? []
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
userInfo[Key.excludedEndpoints.rawValue] = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,31 +23,122 @@
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
import CommonUtils
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
|
|
||||||
public struct ProfileAttributes: Hashable, Codable {
|
// WARNING: upcast to [String: AnyHashable] relies on CodableProfileCoder
|
||||||
public var fingerprint: UUID?
|
// implementation returning JSONSerialization
|
||||||
|
|
||||||
public var lastUpdate: Date?
|
extension ProfileType where UserInfoType == AnyHashable {
|
||||||
|
public var attributes: ProfileAttributes {
|
||||||
public var isAvailableForTV: Bool?
|
ProfileAttributes(userInfo: userInfo as? [String: AnyHashable])
|
||||||
|
|
||||||
public init() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(
|
|
||||||
fingerprint: UUID?,
|
|
||||||
lastUpdate: Date?,
|
|
||||||
isAvailableForTV: Bool?
|
|
||||||
) {
|
|
||||||
self.fingerprint = fingerprint
|
|
||||||
self.lastUpdate = lastUpdate
|
|
||||||
self.isAvailableForTV = isAvailableForTV
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension MutableProfileType where UserInfoType == AnyHashable {
|
||||||
|
public var attributes: ProfileAttributes {
|
||||||
|
get {
|
||||||
|
ProfileAttributes(userInfo: userInfo as? [String: AnyHashable])
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
userInfo = newValue.userInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - ProfileAttributes
|
||||||
|
|
||||||
|
public struct ProfileAttributes {
|
||||||
|
fileprivate enum Key: String {
|
||||||
|
case fingerprint
|
||||||
|
|
||||||
|
case lastUpdate
|
||||||
|
|
||||||
|
case isAvailableForTV
|
||||||
|
|
||||||
|
case preferences
|
||||||
|
}
|
||||||
|
|
||||||
|
private(set) var userInfo: [String: AnyHashable]
|
||||||
|
|
||||||
|
init(userInfo: [String: AnyHashable]?) {
|
||||||
|
self.userInfo = userInfo ?? [:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Basic
|
||||||
|
|
||||||
|
extension ProfileAttributes {
|
||||||
|
public var fingerprint: UUID? {
|
||||||
|
get {
|
||||||
|
guard let string = userInfo[Key.fingerprint.rawValue] as? String else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return UUID(uuidString: string)
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
userInfo[Key.fingerprint.rawValue] = newValue?.uuidString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var lastUpdate: Date? {
|
||||||
|
get {
|
||||||
|
guard let interval = userInfo[Key.lastUpdate.rawValue] as? TimeInterval else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return Date(timeIntervalSinceReferenceDate: interval)
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
userInfo[Key.lastUpdate.rawValue] = newValue?.timeIntervalSinceReferenceDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var isAvailableForTV: Bool? {
|
||||||
|
get {
|
||||||
|
userInfo[Key.isAvailableForTV.rawValue] as? Bool
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
userInfo[Key.isAvailableForTV.rawValue] = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Preferences
|
||||||
|
|
||||||
|
extension ProfileAttributes {
|
||||||
|
public func preferences(inModule moduleId: UUID) -> ModulePreferences {
|
||||||
|
ModulePreferences(userInfo: allPreferences[moduleId.uuidString] as? [String: AnyHashable])
|
||||||
|
}
|
||||||
|
|
||||||
|
public mutating func setPreferences(_ module: ModulePreferences, inModule moduleId: UUID) {
|
||||||
|
allPreferences[moduleId.uuidString] = module.userInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
public func preference<T>(inModule moduleId: UUID, block: (ModulePreferences) -> T) -> T? {
|
||||||
|
let module = preferences(inModule: moduleId)
|
||||||
|
return block(module)
|
||||||
|
}
|
||||||
|
|
||||||
|
public mutating func editPreferences(inModule moduleId: UUID, block: (inout ModulePreferences) -> Void) {
|
||||||
|
var module = preferences(inModule: moduleId)
|
||||||
|
block(&module)
|
||||||
|
setPreferences(module, inModule: moduleId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension ProfileAttributes {
|
||||||
|
var allPreferences: [String: AnyHashable] {
|
||||||
|
get {
|
||||||
|
userInfo[Key.preferences.rawValue] as? [String: AnyHashable] ?? [:]
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
userInfo[Key.preferences.rawValue] = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: -
|
||||||
|
|
||||||
extension ProfileAttributes: CustomDebugStringConvertible {
|
extension ProfileAttributes: CustomDebugStringConvertible {
|
||||||
public var debugDescription: String {
|
public var debugDescription: String {
|
||||||
let descs = [
|
let descs = [
|
||||||
|
@ -59,50 +150,10 @@ extension ProfileAttributes: CustomDebugStringConvertible {
|
||||||
},
|
},
|
||||||
isAvailableForTV.map {
|
isAvailableForTV.map {
|
||||||
"isAvailableForTV: \($0)"
|
"isAvailableForTV: \($0)"
|
||||||
}
|
},
|
||||||
|
"allPreferences: \(allPreferences)"
|
||||||
].compactMap { $0 }
|
].compactMap { $0 }
|
||||||
|
|
||||||
return "{\(descs.joined(separator: ", "))}"
|
return "{\(descs.joined(separator: ", "))}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - UserInfoCodable
|
|
||||||
|
|
||||||
extension ProfileAttributes: UserInfoCodable {
|
|
||||||
public init?(userInfo: AnyHashable?) {
|
|
||||||
do {
|
|
||||||
let data = try JSONSerialization.data(withJSONObject: userInfo ?? [:])
|
|
||||||
self = try JSONDecoder().decode(ProfileAttributes.self, from: data)
|
|
||||||
} catch {
|
|
||||||
pp_log(.App.profiles, .error, "Unable to decode ProfileAttributes from dictionary: \(error)")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public var userInfo: AnyHashable? {
|
|
||||||
do {
|
|
||||||
let data = try JSONEncoder().encode(self)
|
|
||||||
return try JSONSerialization.jsonObject(with: data) as? AnyHashable
|
|
||||||
} catch {
|
|
||||||
pp_log(.App.profiles, .error, "Unable to encode ProfileAttributes to dictionary: \(error)")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Profile {
|
|
||||||
public var attributes: ProfileAttributes {
|
|
||||||
userInfo() ?? ProfileAttributes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Profile.Builder {
|
|
||||||
public var attributes: ProfileAttributes {
|
|
||||||
get {
|
|
||||||
userInfo() ?? ProfileAttributes()
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
setUserInfo(newValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
//
|
|
||||||
// ModulePreferencesRepository.swift
|
|
||||||
// Passepartout
|
|
||||||
//
|
|
||||||
// Created by Davide De Rosa on 12/5/24.
|
|
||||||
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
|
||||||
//
|
|
||||||
// https://github.com/passepartoutvpn
|
|
||||||
//
|
|
||||||
// This file is part of Passepartout.
|
|
||||||
//
|
|
||||||
// Passepartout is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// Passepartout is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import PassepartoutKit
|
|
||||||
|
|
||||||
public protocol ModulePreferencesRepository {
|
|
||||||
func isExcludedEndpoint(_ endpoint: ExtendedEndpoint) -> Bool
|
|
||||||
|
|
||||||
func addExcludedEndpoint(_ endpoint: ExtendedEndpoint)
|
|
||||||
|
|
||||||
func removeExcludedEndpoint(_ endpoint: ExtendedEndpoint)
|
|
||||||
|
|
||||||
func save() throws
|
|
||||||
|
|
||||||
func discard()
|
|
||||||
}
|
|
|
@ -37,9 +37,6 @@ public final class ProfileEditor: ObservableObject {
|
||||||
@Published
|
@Published
|
||||||
public var isShared: Bool
|
public var isShared: Bool
|
||||||
|
|
||||||
@Published
|
|
||||||
private var trackedPreferences: [UUID: ModulePreferencesRepository]
|
|
||||||
|
|
||||||
private(set) var removedModules: [UUID: any ModuleBuilder]
|
private(set) var removedModules: [UUID: any ModuleBuilder]
|
||||||
|
|
||||||
public convenience init() {
|
public convenience init() {
|
||||||
|
@ -50,7 +47,6 @@ public final class ProfileEditor: ObservableObject {
|
||||||
public init(profile: Profile) {
|
public init(profile: Profile) {
|
||||||
editableProfile = profile.editable()
|
editableProfile = profile.editable()
|
||||||
isShared = false
|
isShared = false
|
||||||
trackedPreferences = [:]
|
|
||||||
removedModules = [:]
|
removedModules = [:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,7 +57,6 @@ public final class ProfileEditor: ObservableObject {
|
||||||
activeModulesIds: Set(modules.map(\.id))
|
activeModulesIds: Set(modules.map(\.id))
|
||||||
)
|
)
|
||||||
isShared = false
|
isShared = false
|
||||||
trackedPreferences = [:]
|
|
||||||
removedModules = [:]
|
removedModules = [:]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -208,35 +203,11 @@ extension ProfileEditor {
|
||||||
removedModules = [:]
|
removedModules = [:]
|
||||||
}
|
}
|
||||||
|
|
||||||
public func loadPreferences(
|
|
||||||
_ preferences: ModulePreferences,
|
|
||||||
from manager: PreferencesManager,
|
|
||||||
forModuleWithId moduleId: UUID
|
|
||||||
) {
|
|
||||||
do {
|
|
||||||
pp_log(.App.profiles, .debug, "Track preferences for module \(moduleId)")
|
|
||||||
let repository = try trackedPreferences[moduleId] ?? manager.preferencesRepository(forModuleWithId: moduleId)
|
|
||||||
preferences.repository = repository
|
|
||||||
trackedPreferences[moduleId] = repository // @Published
|
|
||||||
} catch {
|
|
||||||
pp_log(.App.profiles, .error, "Unable to track preferences for module \(moduleId): \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
public func save(to profileManager: ProfileManager) async throws -> Profile {
|
public func save(to profileManager: ProfileManager) async throws -> Profile {
|
||||||
do {
|
do {
|
||||||
let newProfile = try build()
|
let newProfile = try build()
|
||||||
try await profileManager.save(newProfile, isLocal: true, remotelyShared: isShared)
|
try await profileManager.save(newProfile, isLocal: true, remotelyShared: isShared)
|
||||||
trackedPreferences.forEach {
|
|
||||||
do {
|
|
||||||
pp_log(.App.profiles, .debug, "Save tracked preferences for module \($0.key)")
|
|
||||||
try $0.value.save()
|
|
||||||
} catch {
|
|
||||||
pp_log(.App.profiles, .error, "Unable to save preferences for profile \(profile.id): \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
trackedPreferences.removeAll()
|
|
||||||
return newProfile
|
return newProfile
|
||||||
} catch {
|
} catch {
|
||||||
pp_log(.App.profiles, .fault, "Unable to save edited profile: \(error)")
|
pp_log(.App.profiles, .fault, "Unable to save edited profile: \(error)")
|
||||||
|
@ -245,11 +216,6 @@ extension ProfileEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func discard() {
|
public func discard() {
|
||||||
trackedPreferences.forEach {
|
|
||||||
pp_log(.App.profiles, .debug, "Discard tracked preferences for module \($0.key)")
|
|
||||||
$0.value.discard()
|
|
||||||
}
|
|
||||||
trackedPreferences.removeAll()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import CommonLibrary
|
import CommonLibrary
|
||||||
|
import CommonUtils
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
@ -42,3 +43,23 @@ extension ProfileEditor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - ModulePreferences
|
||||||
|
|
||||||
|
extension ProfileEditor {
|
||||||
|
public func excludedEndpoints(for moduleId: UUID) -> ObservableList<ExtendedEndpoint> {
|
||||||
|
ObservableList { [weak self] endpoint in
|
||||||
|
self?.profile.attributes.preference(inModule: moduleId) {
|
||||||
|
$0.isExcludedEndpoint(endpoint)
|
||||||
|
} ?? false
|
||||||
|
} add: { [weak self] endpoint in
|
||||||
|
self?.profile.attributes.editPreferences(inModule: moduleId) {
|
||||||
|
$0.addExcludedEndpoint(endpoint)
|
||||||
|
}
|
||||||
|
} remove: { [weak self] endpoint in
|
||||||
|
self?.profile.attributes.editPreferences(inModule: moduleId) {
|
||||||
|
$0.removeExcludedEndpoint(endpoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,123 @@
|
||||||
|
//
|
||||||
|
// ProfileAttributesTests.swift
|
||||||
|
// Passepartout
|
||||||
|
//
|
||||||
|
// Created by Davide De Rosa on 12/10/24.
|
||||||
|
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
||||||
|
//
|
||||||
|
// https://github.com/passepartoutvpn
|
||||||
|
//
|
||||||
|
// This file is part of Passepartout.
|
||||||
|
//
|
||||||
|
// Passepartout is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Passepartout is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
|
||||||
|
@testable import CommonLibrary
|
||||||
|
import Foundation
|
||||||
|
import PassepartoutKit
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
final class ProfileAttributesTests: XCTestCase {
|
||||||
|
func test_givenUserInfo_whenInit_thenReturnsAttributes() {
|
||||||
|
let fingerprint = UUID()
|
||||||
|
let lastUpdate = Date()
|
||||||
|
let isAvailableForTV = true
|
||||||
|
let userInfo: [String: AnyHashable] = [
|
||||||
|
"fingerprint": fingerprint.uuidString,
|
||||||
|
"lastUpdate": lastUpdate.timeIntervalSinceReferenceDate,
|
||||||
|
"isAvailableForTV": isAvailableForTV
|
||||||
|
]
|
||||||
|
|
||||||
|
let sut = ProfileAttributes(userInfo: userInfo)
|
||||||
|
XCTAssertEqual(sut.userInfo, userInfo)
|
||||||
|
XCTAssertEqual(sut.fingerprint, fingerprint)
|
||||||
|
XCTAssertEqual(sut.lastUpdate, lastUpdate)
|
||||||
|
XCTAssertEqual(sut.isAvailableForTV, isAvailableForTV)
|
||||||
|
}
|
||||||
|
|
||||||
|
func test_givenUserInfo_whenSet_thenReturnsAttributes() {
|
||||||
|
let fingerprint = UUID()
|
||||||
|
let lastUpdate = Date()
|
||||||
|
let isAvailableForTV = true
|
||||||
|
let userInfo: [String: AnyHashable] = [
|
||||||
|
"fingerprint": fingerprint.uuidString,
|
||||||
|
"lastUpdate": lastUpdate.timeIntervalSinceReferenceDate,
|
||||||
|
"isAvailableForTV": isAvailableForTV
|
||||||
|
]
|
||||||
|
|
||||||
|
var sut = ProfileAttributes(userInfo: nil)
|
||||||
|
sut.fingerprint = fingerprint
|
||||||
|
sut.lastUpdate = lastUpdate
|
||||||
|
sut.isAvailableForTV = isAvailableForTV
|
||||||
|
XCTAssertEqual(sut.userInfo, userInfo)
|
||||||
|
XCTAssertEqual(sut.fingerprint, fingerprint)
|
||||||
|
XCTAssertEqual(sut.lastUpdate, lastUpdate)
|
||||||
|
XCTAssertEqual(sut.isAvailableForTV, isAvailableForTV)
|
||||||
|
}
|
||||||
|
|
||||||
|
func test_givenUserInfo_whenInit_thenReturnsModulePreferences() {
|
||||||
|
let moduleId1 = UUID()
|
||||||
|
let moduleId2 = UUID()
|
||||||
|
let excludedEndpoints: [String] = [
|
||||||
|
"1.1.1.1:UDP6:1000",
|
||||||
|
"2.2.2.2:TCP4:2000",
|
||||||
|
"3.3.3.3:TCP:3000",
|
||||||
|
]
|
||||||
|
let moduleUserInfo: [String: AnyHashable] = [
|
||||||
|
"excludedEndpoints": excludedEndpoints
|
||||||
|
]
|
||||||
|
let userInfo: [String: AnyHashable] = [
|
||||||
|
"preferences": [
|
||||||
|
moduleId1.uuidString: moduleUserInfo,
|
||||||
|
moduleId2.uuidString: moduleUserInfo
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
let sut = ProfileAttributes(userInfo: userInfo)
|
||||||
|
XCTAssertEqual(sut.userInfo, userInfo)
|
||||||
|
for moduleId in [moduleId1, moduleId2] {
|
||||||
|
let module = sut.preferences(inModule: moduleId)
|
||||||
|
XCTAssertEqual(module.userInfo, moduleUserInfo)
|
||||||
|
XCTAssertEqual(module.excludedEndpoints, excludedEndpoints)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func test_givenUserInfo_whenSet_thenReturnsModulePreferences() {
|
||||||
|
let moduleId1 = UUID()
|
||||||
|
let moduleId2 = UUID()
|
||||||
|
let excludedEndpoints: [String] = [
|
||||||
|
"1.1.1.1:UDP6:1000",
|
||||||
|
"2.2.2.2:TCP4:2000",
|
||||||
|
"3.3.3.3:TCP:3000",
|
||||||
|
]
|
||||||
|
let moduleUserInfo: [String: AnyHashable] = [
|
||||||
|
"excludedEndpoints": excludedEndpoints
|
||||||
|
]
|
||||||
|
let userInfo: [String: AnyHashable] = [
|
||||||
|
"preferences": [
|
||||||
|
moduleId1.uuidString: moduleUserInfo,
|
||||||
|
moduleId2.uuidString: moduleUserInfo
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
var sut = ProfileAttributes(userInfo: nil)
|
||||||
|
for moduleId in [moduleId1, moduleId2] {
|
||||||
|
var module = sut.preferences(inModule: moduleId1)
|
||||||
|
module.excludedEndpoints = excludedEndpoints
|
||||||
|
XCTAssertEqual(module.userInfo, moduleUserInfo)
|
||||||
|
sut.setPreferences(module, inModule: moduleId)
|
||||||
|
}
|
||||||
|
XCTAssertEqual(sut.userInfo, userInfo)
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,15 +41,9 @@ extension Dependencies {
|
||||||
author: nil
|
author: nil
|
||||||
)
|
)
|
||||||
return PreferencesManager(
|
return PreferencesManager(
|
||||||
modulesFactory: {
|
|
||||||
try AppData.cdModulePreferencesRepositoryV3(
|
|
||||||
context: preferencesStore.backgroundContext(),
|
|
||||||
moduleId: $0
|
|
||||||
)
|
|
||||||
},
|
|
||||||
providersFactory: {
|
providersFactory: {
|
||||||
try AppData.cdProviderPreferencesRepositoryV3(
|
try AppData.cdProviderPreferencesRepositoryV3(
|
||||||
context: preferencesStore.backgroundContext(),
|
context: preferencesStore.context,
|
||||||
providerId: $0
|
providerId: $0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,9 +44,9 @@ extension DefaultTunnelProcessor: PacketTunnelProcessor {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let modulesPreferences = try preferencesManager.preferencesRepository(forModuleWithId: moduleBuilder.id)
|
let preferences = builder.attributes.preferences(inModule: moduleBuilder.id)
|
||||||
moduleBuilder.configurationBuilder?.remotes?.removeAll {
|
moduleBuilder.configurationBuilder?.remotes?.removeAll {
|
||||||
modulesPreferences.isExcludedEndpoint($0)
|
preferences.isExcludedEndpoint($0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let providerId = moduleBuilder.providerId {
|
if let providerId = moduleBuilder.providerId {
|
||||||
|
|
Loading…
Reference in New Issue