diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 2c0596e2..35446d05 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -25,5 +25,6 @@ jobs:
access_token: ${{ secrets.ACCESS_TOKEN }}
- name: Run tests
run: |
- cd Passepartout/Library
- swift test
+ #cd Passepartout/Library
+ #swift test
+ bundle exec fastlane test
diff --git a/.gitignore b/.gitignore
index b72852fe..e6249374 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,3 +20,4 @@ default.profraw
.env.secret*
*.storekit
tmp
+*.sqlite-*
diff --git a/Gemfile.lock b/Gemfile.lock
index f3394373..06460bdf 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -58,8 +58,8 @@ GEM
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.0)
- aws-partitions (1.1001.0)
- aws-sdk-core (3.211.0)
+ aws-partitions (1.1004.0)
+ aws-sdk-core (3.212.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
@@ -67,7 +67,7 @@ GEM
aws-sdk-kms (1.95.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sigv4 (~> 1.5)
- aws-sdk-s3 (1.169.0)
+ aws-sdk-s3 (1.170.1)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
@@ -121,7 +121,7 @@ GEM
fastlane-plugin-translate_gpt (0.1.8.2)
loco_strings (~> 0.1.4.1)
ruby-openai (~> 3.7)
- fastlane-plugin-versioning (0.6.0)
+ fastlane-plugin-versioning (0.7.0)
fastlane-sirp (1.0.0)
sysrandom (~> 1.0)
gh_inspector (1.1.3)
@@ -170,7 +170,7 @@ GEM
multi_xml (>= 0.5.2)
httpclient (2.8.3)
jmespath (1.6.2)
- json (2.7.5)
+ json (2.8.1)
jwt (2.9.3)
base64
loco_strings (0.1.4.1)
@@ -190,7 +190,7 @@ GEM
racc (~> 1.4)
nokogiri (1.16.7-arm64-darwin)
racc (~> 1.4)
- optparse (0.5.0)
+ optparse (0.6.0)
os (1.1.4)
plist (3.7.1)
public_suffix (6.0.1)
diff --git a/Passepartout/App/AppDelegate.swift b/Passepartout/App/AppDelegate.swift
index 9e6c0e53..0c33e2df 100644
--- a/Passepartout/App/AppDelegate.swift
+++ b/Passepartout/App/AppDelegate.swift
@@ -36,5 +36,5 @@ final class AppDelegate: NSObject {
func configure(with uiConfiguring: UILibraryConfiguring) {
UILibrary(uiConfiguring)
.configure(with: context)
- }
+ }
}
diff --git a/Passepartout/Library/.swiftpm/xcode/xcshareddata/xcschemes/Library-Package.xcscheme b/Passepartout/Library/.swiftpm/xcode/xcshareddata/xcschemes/Library-Package.xcscheme
index 082343e1..d3e6193f 100644
--- a/Passepartout/Library/.swiftpm/xcode/xcshareddata/xcschemes/Library-Package.xcscheme
+++ b/Passepartout/Library/.swiftpm/xcode/xcshareddata/xcschemes/Library-Package.xcscheme
@@ -91,6 +91,20 @@
ReferencedContainer = "container:">
+
+
+
+
+
+
+
+
.
+//
+
+import Foundation
+
+public struct MigratableProfile: Sendable {
+ public let id: UUID
+
+ public let name: String
+
+ public let lastUpdate: Date?
+
+ public init(id: UUID, name: String, lastUpdate: Date?) {
+ self.id = id
+ self.name = name
+ self.lastUpdate = lastUpdate
+ }
+}
diff --git a/Passepartout/Library/Sources/CommonLibrary/Shared.swift b/Passepartout/Library/Sources/CommonLibrary/Shared.swift
index 7104b8f2..cc591493 100644
--- a/Passepartout/Library/Sources/CommonLibrary/Shared.swift
+++ b/Passepartout/Library/Sources/CommonLibrary/Shared.swift
@@ -32,6 +32,8 @@ extension LoggerDestination {
public enum App {
public static let iap = LoggerDestination(category: "app.iap")
+ public static let migration = LoggerDestination(category: "app.migration")
+
public static let profiles = LoggerDestination(category: "app.profiles")
}
}
diff --git a/Passepartout/Library/Sources/CommonUtils/Business/CoreDataPersistentStore.swift b/Passepartout/Library/Sources/CommonUtils/Business/CoreDataPersistentStore.swift
index 335a24db..e746410d 100644
--- a/Passepartout/Library/Sources/CommonUtils/Business/CoreDataPersistentStore.swift
+++ b/Passepartout/Library/Sources/CommonUtils/Business/CoreDataPersistentStore.swift
@@ -35,13 +35,14 @@ public protocol CoreDataPersistentStoreLogger {
}
public final class CoreDataPersistentStore {
- private let logger: CoreDataPersistentStoreLogger
+ private let logger: CoreDataPersistentStoreLogger?
private let container: NSPersistentContainer
public convenience init(
- logger: CoreDataPersistentStoreLogger,
+ logger: CoreDataPersistentStoreLogger? = nil,
containerName: String,
+ baseURL: URL? = nil,
model: NSManagedObjectModel,
cloudKitIdentifier: String?,
author: String?
@@ -49,10 +50,14 @@ public final class CoreDataPersistentStore {
let container: NSPersistentContainer
if let cloudKitIdentifier {
container = NSPersistentCloudKitContainer(name: containerName, managedObjectModel: model)
- logger.debug("Set up CloudKit container (\(cloudKitIdentifier)): \(containerName)")
+ logger?.debug("Set up CloudKit container (\(cloudKitIdentifier)): \(containerName)")
} else {
container = NSPersistentContainer(name: containerName, managedObjectModel: model)
- logger.debug("Set up local container: \(containerName)")
+ logger?.debug("Set up local container: \(containerName)")
+ }
+ if let baseURL {
+ let url = baseURL.appending(component: "\(containerName).sqlite")
+ container.persistentStoreDescriptions = [.init(url: url)]
}
self.init(
logger: logger,
@@ -63,7 +68,7 @@ public final class CoreDataPersistentStore {
}
private init(
- logger: CoreDataPersistentStoreLogger,
+ logger: CoreDataPersistentStoreLogger?,
container: NSPersistentContainer,
cloudKitIdentifier: String?,
author: String?
@@ -74,7 +79,7 @@ public final class CoreDataPersistentStore {
guard let desc = container.persistentStoreDescriptions.first else {
fatalError("Unable to read persistent store description")
}
- logger.debug("Container description: \(desc)")
+ logger?.debug("Container description: \(desc)")
// optional container identifier for CloudKit, first in entitlements otherwise
if let cloudKitIdentifier {
@@ -98,7 +103,7 @@ public final class CoreDataPersistentStore {
container.viewContext.automaticallyMergesChangesFromParent = true
if let author {
- logger.debug("Setting transaction author: \(author)")
+ logger?.debug("Setting transaction author: \(author)")
container.viewContext.transactionAuthor = author
}
}
@@ -141,7 +146,7 @@ extension CoreDataPersistentStore {
try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: $0, options: nil)
}
} catch {
- logger.warning("Unable to truncate persistent store: \(error)")
+ logger?.warning("Unable to truncate persistent store: \(error)")
}
}
}
diff --git a/Passepartout/Library/Sources/LegacyV2/CDProfileRepositoryV2.swift b/Passepartout/Library/Sources/LegacyV2/CDProfileRepositoryV2.swift
index 1f867f12..99cf3019 100644
--- a/Passepartout/Library/Sources/LegacyV2/CDProfileRepositoryV2.swift
+++ b/Passepartout/Library/Sources/LegacyV2/CDProfileRepositoryV2.swift
@@ -23,6 +23,7 @@
// along with Passepartout. If not, see .
//
+import CommonLibrary
import CoreData
import Foundation
import PassepartoutKit
@@ -41,31 +42,78 @@ final class CDProfileRepositoryV2 {
self.context = context
}
- // FIXME: #642, migrate profiles properly
- func migratedProfiles() async throws -> [Profile] {
+ func migratableProfiles() async throws -> [MigratableProfile] {
+ try await fetchProfiles(
+ prefetch: {
+ $0.propertiesToFetch = ["uuid", "name", "lastUpdate"]
+ },
+ map: {
+ $0.compactMap {
+ guard $0.value.encryptedJSON ?? $0.value.json != nil else {
+ pp_log(.App.migration, .error, "Unable to migrate profile \($0.key): missing JSON")
+ return nil
+ }
+ return MigratableProfile(
+ id: $0.key,
+ name: $0.value.name ?? $0.key.uuidString,
+ lastUpdate: $0.value.lastUpdate
+ )
+ }
+ }
+ )
+ }
+
+ func profiles() async throws -> [ProfileV2] {
+ let decoder = JSONDecoder()
+ return try await fetchProfiles(
+ map: {
+ $0.compactMap {
+ guard let json = $0.value.encryptedJSON ?? $0.value.json else {
+ pp_log(.App.migration, .error, "Unable to migrate profile \($0.key): missing JSON")
+ return nil
+ }
+ do {
+ return try decoder.decode(ProfileV2.self, from: json)
+ } catch {
+ pp_log(.App.migration, .error, "Unable to migrate profile \($0.key): \(error)")
+ return nil
+ }
+ }
+ }
+ )
+ }
+}
+
+private extension CDProfileRepositoryV2 {
+ func fetchProfiles(
+ prefetch: ((NSFetchRequest) -> Void)? = nil,
+ map: @escaping ([UUID: CDProfile]) -> [T]
+ ) async throws -> [T] {
try await context.perform { [weak self] in
guard let self else {
return []
}
- do {
- let request = CDProfile.fetchRequest()
- let existing = try context.fetch(request)
-// existing.forEach {
-// guard let json = $0.encryptedJSON,
-// let string = String(data: json, encoding: .utf8) else {
-// return
-// }
-// print(">>> \(string)")
-// }
- return existing.compactMap {
- guard let name = $0.name else {
- return nil
- }
- return try? Profile.Builder(name: name).tryBuild()
+
+ let request = CDProfile.fetchRequest()
+ request.sortDescriptors = [
+ .init(key: "lastUpdate", ascending: false)
+ ]
+ prefetch?(request)
+ let existing = try context.fetch(request)
+
+ var deduped: [UUID: CDProfile] = [:]
+ existing.forEach {
+ guard let uuid = $0.uuid else {
+ return
}
- } catch {
- throw error
+ guard !deduped.keys.contains(uuid) else {
+ pp_log(.App.migration, .info, "Skip older duplicate of profile \(uuid)")
+ return
+ }
+ deduped[uuid] = $0
}
+
+ return map(deduped)
}
}
}
diff --git a/Passepartout/Library/Sources/LegacyV2/CDProfile.swift b/Passepartout/Library/Sources/LegacyV2/Domain/CDProfile.swift
similarity index 100%
rename from Passepartout/Library/Sources/LegacyV2/CDProfile.swift
rename to Passepartout/Library/Sources/LegacyV2/Domain/CDProfile.swift
diff --git a/Passepartout/Library/Sources/LegacyV2/Domain/MapperV2.swift b/Passepartout/Library/Sources/LegacyV2/Domain/MapperV2.swift
new file mode 100644
index 00000000..adb85d2a
--- /dev/null
+++ b/Passepartout/Library/Sources/LegacyV2/Domain/MapperV2.swift
@@ -0,0 +1,186 @@
+//
+// MapperV2.swift
+// Passepartout
+//
+// Created by Davide De Rosa on 11/12/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 .
+//
+
+import Foundation
+import PassepartoutKit
+
+struct MapperV2 {
+ func toProfileV3(_ v2: ProfileV2) throws -> Profile {
+ var builder = Profile.Builder(id: v2.id)
+ var modules: [Module] = []
+
+ builder.name = v2.header.name
+ builder.attributes.lastUpdate = v2.header.lastUpdate
+
+ modules.append(toOnDemandModule(v2.onDemand))
+
+ if let provider = v2.provider {
+ if let module = try toProviderModule(provider) {
+ let providerId = ProviderID(rawValue: provider.name)
+ modules.append(module)
+ builder.setProviderId(providerId, forModuleWithId: module.id)
+ }
+ } else if let ovpn = v2.host?.ovpnSettings {
+ modules.append(try toOpenVPNModule(ovpn))
+ } else if let wg = v2.host?.wgSettings {
+ modules.append(try toWireGuardModule(wg))
+ }
+
+ try toNetworkModules(v2.networkSettings).forEach {
+ modules.append($0)
+ }
+
+ builder.modules = modules
+ builder.activeModulesIds = Set(modules.map(\.id))
+ return try builder.tryBuild()
+ }
+}
+
+extension MapperV2 {
+ func toOnDemandModule(_ v2: ProfileV2.OnDemand) -> OnDemandModule {
+ var builder = OnDemandModule.Builder()
+ builder.isEnabled = v2.isEnabled
+ switch v2.policy {
+ case .any:
+ builder.policy = .any
+ case .excluding:
+ builder.policy = .excluding
+ case .including:
+ builder.policy = .including
+ }
+ builder.withSSIDs = v2.withSSIDs
+ builder.withOtherNetworks = Set(v2.withOtherNetworks.map {
+ switch $0 {
+ case .ethernet:
+ return .ethernet
+ case .mobile:
+ return .mobile
+ }
+ })
+ return builder.tryBuild()
+ }
+}
+
+extension MapperV2 {
+ func toOpenVPNModule(_ v2: ProfileV2.OpenVPNSettings) throws -> OpenVPNModule {
+ var builder = OpenVPNModule.Builder()
+ builder.configurationBuilder = v2.configuration.builder()
+ builder.credentials = v2.account.map(toOpenVPNCredentials)
+ return try builder.tryBuild()
+ }
+
+ func toOpenVPNCredentials(_ v2: ProfileV2.Account) -> OpenVPN.Credentials {
+ OpenVPN.Credentials.Builder(username: v2.username, password: v2.password)
+ .build()
+ }
+
+ func toWireGuardModule(_ v2: ProfileV2.WireGuardSettings) throws -> WireGuardModule {
+ var builder = WireGuardModule.Builder()
+ builder.configurationBuilder = v2.configuration.configuration.builder()
+ return try builder.tryBuild()
+ }
+}
+
+extension MapperV2 {
+ func toProviderModule(_ v2: ProfileV2.Provider) throws -> OpenVPNModule? {
+ assert(v2.vpnSettings.count == 1)
+ guard let entry = v2.vpnSettings.first else {
+ return nil
+ }
+ assert(entry.key == .openVPN)
+ let settings = entry.value
+
+ var builder = OpenVPNModule.Builder()
+ builder.credentials = settings.account.map(toOpenVPNCredentials)
+ return try builder.tryBuild()
+ }
+}
+
+extension MapperV2 {
+ func toNetworkModules(_ v2: ProfileV2.NetworkSettings) throws -> [Module] {
+ var modules: [Module] = []
+ if v2.dns.choice == .manual {
+ modules.append(try toDNSModule(v2.dns))
+ }
+ if v2.proxy.choice == .manual {
+ modules.append(try toHTTPProxyModule(v2.proxy))
+ }
+ if v2.gateway.choice == .manual || v2.mtu.choice == .manual {
+ modules.append(try toIPModule(v2.gateway, v2MTU: v2.mtu))
+ }
+ return modules
+ }
+
+ func toDNSModule(_ v2: Network.DNSSettings) throws -> DNSModule {
+ var builder = DNSModule.Builder()
+ builder.protocolType = v2.dnsProtocol ?? .cleartext
+ builder.servers = v2.dnsServers ?? []
+ builder.domainName = v2.dnsDomain
+ builder.searchDomains = v2.dnsSearchDomains
+ builder.dohURL = v2.dnsHTTPSURL?.absoluteString ?? ""
+ builder.dotHostname = v2.dnsTLSServerName ?? ""
+ return try builder.tryBuild()
+ }
+
+ func toHTTPProxyModule(_ v2: Network.ProxySettings) throws -> HTTPProxyModule {
+ var builder = HTTPProxyModule.Builder()
+ builder.address = v2.proxyAddress ?? ""
+ builder.port = v2.proxyPort ?? 0
+ builder.secureAddress = v2.proxyAddress ?? ""
+ builder.securePort = v2.proxyPort ?? 0
+ builder.pacURLString = v2.proxyAutoConfigurationURL?.absoluteString ?? ""
+ builder.bypassDomains = v2.proxyBypassDomains ?? []
+ return try builder.tryBuild()
+ }
+
+ func toIPModule(_ v2Gateway: Network.GatewaySettings?, v2MTU: Network.MTUSettings?) throws -> IPModule {
+ var builder = IPModule.Builder()
+
+ if let v2Gateway, v2Gateway.choice == .manual {
+ let defaultRoute = Route(defaultWithGateway: nil)
+
+ if v2Gateway.isDefaultIPv4 {
+ builder.ipv4 = IPSettings(subnet: nil)
+ .including(routes: [defaultRoute])
+ } else {
+ builder.ipv4 = IPSettings(subnet: nil)
+ .excluding(routes: [defaultRoute])
+ }
+
+ if v2Gateway.isDefaultIPv6 {
+ builder.ipv6 = IPSettings(subnet: nil)
+ .including(routes: [defaultRoute])
+ } else {
+ builder.ipv6 = IPSettings(subnet: nil)
+ .excluding(routes: [defaultRoute])
+ }
+ }
+ if let v2MTU, v2MTU.choice == .manual {
+ builder.mtu = v2MTU.mtuBytes
+ }
+
+ return builder.tryBuild()
+ }
+}
diff --git a/Passepartout/Library/Sources/LegacyV2/Domain/Network.swift b/Passepartout/Library/Sources/LegacyV2/Domain/Network.swift
new file mode 100644
index 00000000..29c2609e
--- /dev/null
+++ b/Passepartout/Library/Sources/LegacyV2/Domain/Network.swift
@@ -0,0 +1,159 @@
+//
+// Network.swift
+// Passepartout
+//
+// Created by Davide De Rosa on 2/15/22.
+// 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 .
+//
+
+import Foundation
+import PassepartoutKit
+
+enum Network {
+}
+
+extension Network {
+ enum Choice: String, Codable {
+ case automatic // OpenVPN pulls from server
+
+ case manual
+
+ static let defaultChoice: Choice = .automatic
+ }
+}
+
+protocol NetworkChoiceRepresentable {
+ var choice: Network.Choice { get set }
+}
+
+protocol GatewaySettingsProviding {
+ var isDefaultIPv4: Bool { get }
+
+ var isDefaultIPv6: Bool { get }
+}
+
+protocol DNSSettingsProviding {
+ var dnsProtocol: DNSProtocol? { get }
+
+ var dnsServers: [String]? { get }
+
+ var dnsDomain: String? { get }
+
+ var dnsSearchDomains: [String]? { get }
+
+ var dnsHTTPSURL: URL? { get }
+
+ var dnsTLSServerName: String? { get }
+}
+
+protocol ProxySettingsProviding {
+ var proxyServer: Endpoint? { get }
+
+ var proxyBypassDomains: [String]? { get }
+
+ var proxyAutoConfigurationURL: URL? { get }
+}
+
+protocol MTUSettingsProviding {
+ var mtuBytes: Int { get }
+}
+
+//
+
+extension Network {
+ struct GatewaySettings: Codable, Equatable, NetworkChoiceRepresentable, GatewaySettingsProviding {
+ var choice: Network.Choice
+
+ var isDefaultIPv4 = true
+
+ var isDefaultIPv6 = true
+ }
+}
+
+extension Network {
+ struct DNSSettings: Codable, Equatable, NetworkChoiceRepresentable, DNSSettingsProviding {
+ enum ConfigurationType: String, Codable {
+ case plain
+
+ case https
+
+ case tls
+
+ case disabled
+ }
+
+ var choice: Network.Choice
+
+ var configurationType: ConfigurationType = .plain
+
+ var dnsProtocol: DNSProtocol? {
+ DNSProtocol(rawValue: configurationType.rawValue)
+ }
+
+ var dnsServers: [String]?
+
+ var dnsDomain: String?
+
+ var dnsSearchDomains: [String]?
+
+ var dnsHTTPSURL: URL?
+
+ var dnsTLSServerName: String?
+ }
+}
+
+extension Network {
+ struct ProxySettings: Codable, Equatable, NetworkChoiceRepresentable, ProxySettingsProviding {
+ enum ConfigurationType: String, Codable {
+ case manual
+
+ case pac
+
+ case disabled
+ }
+
+ var choice: Network.Choice
+
+ var configurationType: ConfigurationType = .manual
+
+ var proxyAddress: String?
+
+ var proxyPort: UInt16?
+
+ var proxyBypassDomains: [String]?
+
+ var proxyAutoConfigurationURL: URL?
+
+ var proxyServer: Endpoint? {
+ guard let address = proxyAddress, let port = proxyPort, !address.isEmpty, port > 0 else {
+ return nil
+ }
+ return try? Endpoint(address, port)
+ }
+ }
+}
+
+extension Network {
+ struct MTUSettings: Codable, Equatable, NetworkChoiceRepresentable, MTUSettingsProviding {
+ var choice: Network.Choice
+
+ var mtuBytes = 0
+ }
+}
diff --git a/Passepartout/Library/Sources/LegacyV2/Domain/Profile+Account.swift b/Passepartout/Library/Sources/LegacyV2/Domain/Profile+Account.swift
new file mode 100644
index 00000000..0d6238e6
--- /dev/null
+++ b/Passepartout/Library/Sources/LegacyV2/Domain/Profile+Account.swift
@@ -0,0 +1,58 @@
+//
+// Profile+Account.swift
+// Passepartout
+//
+// Created by Davide De Rosa on 4/6/22.
+// 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 .
+//
+
+import Foundation
+
+extension ProfileV2 {
+ struct Account: Codable, Equatable {
+ enum AuthenticationMethod: String, Codable {
+ case persistent
+
+ case interactive
+
+ case totp
+ }
+
+ var authenticationMethod: AuthenticationMethod?
+
+ var username: String
+
+ var password: String
+
+ var isEmpty: Bool {
+ username.isEmpty && password.isEmpty
+ }
+
+ init() {
+ username = ""
+ password = ""
+ }
+
+ init(_ username: String, _ password: String) {
+ self.username = username
+ self.password = password
+ }
+ }
+}
diff --git a/Passepartout/Library/Sources/LegacyV2/Domain/Profile+Header.swift b/Passepartout/Library/Sources/LegacyV2/Domain/Profile+Header.swift
new file mode 100644
index 00000000..3d66c2d7
--- /dev/null
+++ b/Passepartout/Library/Sources/LegacyV2/Domain/Profile+Header.swift
@@ -0,0 +1,70 @@
+//
+// Profile+Header.swift
+// Passepartout
+//
+// Created by Davide De Rosa on 2/17/22.
+// 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 .
+//
+
+import Foundation
+
+extension ProfileV2 {
+ struct Header: Codable, Identifiable, Hashable {
+ let uuid: UUID
+
+ var name: String
+
+ let providerName: ProviderName?
+
+ let lastUpdate: Date?
+
+ init(
+ uuid: UUID = UUID(),
+ name: String = "",
+ providerName: ProviderName? = nil,
+ lastUpdate: Date? = nil
+ ) {
+ self.uuid = uuid
+ self.name = name
+ self.providerName = providerName
+ self.lastUpdate = lastUpdate ?? Date()
+ }
+
+ // MARK: Hashable
+
+ static func == (lhs: Self, rhs: Self) -> Bool {
+ lhs.uuid == rhs.uuid &&
+ lhs.name == rhs.name &&
+ lhs.providerName == rhs.providerName
+ }
+
+ func hash(into hasher: inout Hasher) {
+ hasher.combine(uuid)
+ hasher.combine(name)
+ hasher.combine(providerName)
+ }
+
+ // MARK: Identifiable
+
+ var id: UUID {
+ uuid
+ }
+ }
+}
diff --git a/Passepartout/Library/Sources/LegacyV2/Domain/Profile+Host.swift b/Passepartout/Library/Sources/LegacyV2/Domain/Profile+Host.swift
new file mode 100644
index 00000000..b6cd7513
--- /dev/null
+++ b/Passepartout/Library/Sources/LegacyV2/Domain/Profile+Host.swift
@@ -0,0 +1,34 @@
+//
+// Profile+Host.swift
+// Passepartout
+//
+// Created by Davide De Rosa on 3/10/22.
+// 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 .
+//
+
+import Foundation
+
+extension ProfileV2 {
+ struct Host: Codable, Equatable {
+ var ovpnSettings: OpenVPNSettings?
+
+ var wgSettings: WireGuardSettings?
+ }
+}
diff --git a/Passepartout/Library/Sources/LegacyV2/Domain/Profile+NetworkSettings.swift b/Passepartout/Library/Sources/LegacyV2/Domain/Profile+NetworkSettings.swift
new file mode 100644
index 00000000..833f0d66
--- /dev/null
+++ b/Passepartout/Library/Sources/LegacyV2/Domain/Profile+NetworkSettings.swift
@@ -0,0 +1,53 @@
+//
+// Profile+NetworkSettings.swift
+// Passepartout
+//
+// Created by Davide De Rosa on 2/15/22.
+// 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 .
+//
+
+import Foundation
+
+extension ProfileV2 {
+ struct NetworkSettings: Codable, Equatable {
+ var gateway: Network.GatewaySettings
+
+ var dns: Network.DNSSettings
+
+ var proxy: Network.ProxySettings
+
+ var mtu: Network.MTUSettings
+
+ var resolvesHostname = true
+
+ var keepsAliveOnSleep = true
+
+ init(choice: Network.Choice) {
+ gateway = Network.GatewaySettings(choice: choice)
+ dns = Network.DNSSettings(choice: choice)
+ proxy = Network.ProxySettings(choice: choice)
+ mtu = Network.MTUSettings(choice: choice)
+ }
+
+ init() {
+ self.init(choice: .defaultChoice)
+ }
+ }
+}
diff --git a/Passepartout/Library/Sources/LegacyV2/Domain/Profile+OnDemand.swift b/Passepartout/Library/Sources/LegacyV2/Domain/Profile+OnDemand.swift
new file mode 100644
index 00000000..9cc609b4
--- /dev/null
+++ b/Passepartout/Library/Sources/LegacyV2/Domain/Profile+OnDemand.swift
@@ -0,0 +1,55 @@
+//
+// Profile+OnDemand.swift
+// Passepartout
+//
+// Created by Davide De Rosa on 2/17/22.
+// 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 .
+//
+
+import Foundation
+
+extension ProfileV2 {
+ struct OnDemand: Codable, Equatable {
+ enum Policy: String, Codable {
+ case any
+
+ case including
+
+ case excluding // "trusted networks"
+ }
+
+ enum OtherNetwork: String, Codable {
+ case mobile
+
+ case ethernet
+ }
+
+ var isEnabled = true
+
+ var policy: Policy = .excluding
+
+ var withSSIDs: [String: Bool] = [:]
+
+ var withOtherNetworks: Set = []
+
+ init() {
+ }
+ }
+}
diff --git a/Passepartout/Library/Sources/LegacyV2/Domain/Profile+OpenVPNSettings.swift b/Passepartout/Library/Sources/LegacyV2/Domain/Profile+OpenVPNSettings.swift
new file mode 100644
index 00000000..910c9415
--- /dev/null
+++ b/Passepartout/Library/Sources/LegacyV2/Domain/Profile+OpenVPNSettings.swift
@@ -0,0 +1,61 @@
+//
+// Profile+OpenVPNSettings.swift
+// Passepartout
+//
+// Created by Davide De Rosa on 2/16/22.
+// 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 .
+//
+
+import Foundation
+import PassepartoutKit
+
+extension ProfileV2 {
+ struct OpenVPNSettings: Codable, Equatable, VPNProtocolProviding {
+ var vpnProtocol: VPNProtocolType {
+ .openVPN
+ }
+
+ var configuration: OpenVPN.Configuration
+
+ var account: Account?
+
+ var customEndpoint: Endpoint?
+
+ init(configuration: OpenVPN.Configuration) {
+ self.configuration = configuration
+ }
+ }
+
+ init(_ id: UUID = UUID(), name: String, configuration: OpenVPN.Configuration) {
+ let header = Header(
+ uuid: id,
+ name: name,
+ providerName: nil
+ )
+ self.init(header, configuration: configuration)
+ }
+
+ init(_ header: Header, configuration: OpenVPN.Configuration) {
+ self.header = header
+ currentVPNProtocol = .openVPN
+ host = Host()
+ host?.ovpnSettings = OpenVPNSettings(configuration: configuration)
+ }
+}
diff --git a/Passepartout/Library/Sources/LegacyV2/Domain/Profile+Provider.swift b/Passepartout/Library/Sources/LegacyV2/Domain/Profile+Provider.swift
new file mode 100644
index 00000000..e4268bd1
--- /dev/null
+++ b/Passepartout/Library/Sources/LegacyV2/Domain/Profile+Provider.swift
@@ -0,0 +1,58 @@
+//
+// Profile+Provider.swift
+// Passepartout
+//
+// Created by Davide De Rosa on 2/15/22.
+// 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 .
+//
+
+import Foundation
+import PassepartoutKit
+
+typealias ProviderName = String
+
+extension ProfileV2 {
+ struct Provider: Codable, Equatable {
+ struct Settings: Codable, Equatable {
+ var account: Account?
+
+ var serverId: String?
+
+ var presetId: String?
+
+ var favoriteLocationIds: Set?
+
+ var customEndpoint: Endpoint?
+
+ init() {
+ }
+ }
+
+ let name: ProviderName
+
+ var vpnSettings: [VPNProtocolType: Settings] = [:]
+
+ var randomizesServer: Bool?
+
+ init(_ name: ProviderName) {
+ self.name = name
+ }
+ }
+}
diff --git a/Passepartout/Library/Sources/LegacyV2/Domain/Profile+WireGuardSettings.swift b/Passepartout/Library/Sources/LegacyV2/Domain/Profile+WireGuardSettings.swift
new file mode 100644
index 00000000..04d7b0f0
--- /dev/null
+++ b/Passepartout/Library/Sources/LegacyV2/Domain/Profile+WireGuardSettings.swift
@@ -0,0 +1,79 @@
+//
+// Profile+WireGuardSettings.swift
+// Passepartout
+//
+// Created by Davide De Rosa on 2/17/22.
+// 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 .
+//
+
+import Foundation
+import PassepartoutWireGuard
+import PassepartoutWireGuardGo
+
+extension ProfileV2 {
+ struct WireGuardSettings: Codable, Equatable, VPNProtocolProviding {
+ struct WrappedConfiguration: Codable, Equatable {
+ let configuration: WireGuard.Configuration
+
+ init(configuration: WireGuard.Configuration) {
+ self.configuration = configuration
+ }
+
+ public init(from decoder: Decoder) throws {
+ let container = try decoder.singleValueContainer()
+ let wg = try container.decode(String.self)
+ configuration = try StandardWireGuardParser().configuration(from: wg)
+ }
+
+ public func encode(to encoder: Encoder) throws {
+ let wg = try StandardWireGuardParser().string(from: configuration)
+ var container = encoder.singleValueContainer()
+ try container.encode(wg)
+ }
+ }
+
+ var vpnProtocol: VPNProtocolType {
+ .wireGuard
+ }
+
+ var configuration: WrappedConfiguration
+
+ init(configuration: WireGuard.Configuration) {
+ self.configuration = WrappedConfiguration(configuration: configuration)
+ }
+
+ }
+
+ init(_ id: UUID = UUID(), name: String, configuration: WireGuard.Configuration) {
+ let header = Header(
+ uuid: id,
+ name: name,
+ providerName: nil
+ )
+ self.init(header, configuration: configuration)
+ }
+
+ init(_ header: Header, configuration: WireGuard.Configuration) {
+ self.header = header
+ currentVPNProtocol = .wireGuard
+ host = Host()
+ host?.wgSettings = WireGuardSettings(configuration: configuration)
+ }
+}
diff --git a/Passepartout/Library/Sources/LegacyV2/Domain/Profile.swift b/Passepartout/Library/Sources/LegacyV2/Domain/Profile.swift
new file mode 100644
index 00000000..56ada650
--- /dev/null
+++ b/Passepartout/Library/Sources/LegacyV2/Domain/Profile.swift
@@ -0,0 +1,118 @@
+//
+// Profile.swift
+// Passepartout
+//
+// Created by Davide De Rosa on 2/11/22.
+// 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 .
+//
+
+import Foundation
+
+enum VPNProtocolType: String, RawRepresentable, Codable {
+ case openVPN = "ovpn"
+
+ case wireGuard = "wg"
+}
+
+protocol VPNProtocolProviding {
+ var vpnProtocol: VPNProtocolType { get }
+}
+
+protocol ProfileSubtype {
+ var vpnProtocols: [VPNProtocolType] { get }
+}
+
+struct ProfileV2: Identifiable, Codable, Equatable {
+ var header: Header
+
+ var currentVPNProtocol: VPNProtocolType
+
+ var networkSettings = NetworkSettings()
+
+ var onDemand = OnDemand()
+
+ var connectionExpirationDate: Date?
+
+ var host: Host?
+
+ var provider: Provider?
+
+ init(_ header: Header) {
+ self.header = header
+ currentVPNProtocol = .openVPN
+ }
+
+ init(_ id: UUID = UUID(), name: String) {
+ header = Header(
+ uuid: id,
+ name: name,
+ providerName: nil
+ )
+ currentVPNProtocol = .openVPN
+ }
+
+ init(_ id: UUID = UUID(), name: String, provider: Provider) {
+ let header = Header(
+ uuid: id,
+ name: name,
+ providerName: provider.name
+ )
+ self.init(header, provider: provider)
+ }
+
+ init(_ header: Header, provider: Provider) {
+ guard let firstVPNProtocol = provider.vpnSettings.keys.first else {
+ fatalError("No VPN protocols defined in provider")
+ }
+ self.header = header
+ currentVPNProtocol = firstVPNProtocol
+ self.provider = provider
+ }
+
+ // MARK: Identifiable
+
+ var id: UUID {
+ header.id
+ }
+}
+
+extension ProfileV2 {
+ static let placeholder = ProfileV2(
+ UUID(uuidString: "00000000-0000-0000-0000-000000000000")!,
+ name: ""
+ )
+
+ static func isPlaceholder(_ id: UUID) -> Bool {
+ id == placeholder.id
+ }
+
+ var isPlaceholder: Bool {
+ header.id == Self.placeholder.id
+ }
+}
+
+extension ProfileV2 {
+ var isExpired: Bool {
+ guard let connectionExpirationDate else {
+ return false
+ }
+ return Date().distance(to: connectionExpirationDate) <= .zero
+ }
+}
diff --git a/Passepartout/Library/Sources/LegacyV2/LegacyV2.swift b/Passepartout/Library/Sources/LegacyV2/LegacyV2.swift
index e0dce423..00d42423 100644
--- a/Passepartout/Library/Sources/LegacyV2/LegacyV2.swift
+++ b/Passepartout/Library/Sources/LegacyV2/LegacyV2.swift
@@ -23,6 +23,7 @@
// along with Passepartout. If not, see .
//
+import CommonLibrary
import CommonUtils
import Foundation
import PassepartoutKit
@@ -30,16 +31,18 @@ import PassepartoutKit
public final class LegacyV2 {
private let profilesRepository: CDProfileRepositoryV2
- private let cloudKitIdentifier: String
+ private let cloudKitIdentifier: String?
public init(
+ coreDataLogger: CoreDataPersistentStoreLogger?,
profilesContainerName: String,
- cloudKitIdentifier: String,
- coreDataLogger: CoreDataPersistentStoreLogger
+ baseURL: URL? = nil,
+ cloudKitIdentifier: String?
) {
let store = CoreDataPersistentStore(
logger: coreDataLogger,
containerName: profilesContainerName,
+ baseURL: baseURL,
model: CDProfileRepositoryV2.model,
cloudKitIdentifier: cloudKitIdentifier,
author: nil
@@ -47,8 +50,42 @@ public final class LegacyV2 {
profilesRepository = CDProfileRepositoryV2(context: store.context)
self.cloudKitIdentifier = cloudKitIdentifier
}
+}
- public func fetchProfiles() async throws -> [Profile] {
- try await profilesRepository.migratedProfiles()
+// MARK: - Mapping
+
+extension LegacyV2 {
+ public func fetchMigratableProfiles() async throws -> [MigratableProfile] {
+ try await profilesRepository.migratableProfiles()
+ }
+
+ public func fetchProfiles(selection: Set) async throws -> (migrated: [Profile], failed: Set) {
+ let profilesV2 = try await profilesRepository.profiles()
+
+ var migrated: [Profile] = []
+ var failed: Set = []
+ let mapper = MapperV2()
+
+ profilesV2.forEach {
+ guard selection.contains($0.id) else {
+ return
+ }
+ do {
+ let mapped = try mapper.toProfileV3($0)
+ migrated.append(mapped)
+ } catch {
+ pp_log(.App.migration, .error, "Unable to migrate profile \($0.id): \(error)")
+ failed.insert($0.id)
+ }
+ }
+ return (migrated, failed)
+ }
+}
+
+// MARK: - Legacy profiles
+
+extension LegacyV2 {
+ func fetchProfilesV2() async throws -> [ProfileV2] {
+ try await profilesRepository.profiles()
}
}
diff --git a/Passepartout/Library/Sources/LegacyV2/Profiles.xcdatamodeld/Profiles.xcdatamodel/contents b/Passepartout/Library/Sources/LegacyV2/Profiles.xcdatamodeld/Profiles.xcdatamodel/contents
index 347ee32b..d386b6f1 100644
--- a/Passepartout/Library/Sources/LegacyV2/Profiles.xcdatamodeld/Profiles.xcdatamodel/contents
+++ b/Passepartout/Library/Sources/LegacyV2/Profiles.xcdatamodeld/Profiles.xcdatamodel/contents
@@ -1,5 +1,5 @@
-
+
diff --git a/Passepartout/Library/Tests/LegacyV2Tests/LegacyV2Tests.swift b/Passepartout/Library/Tests/LegacyV2Tests/LegacyV2Tests.swift
new file mode 100644
index 00000000..91cede42
--- /dev/null
+++ b/Passepartout/Library/Tests/LegacyV2Tests/LegacyV2Tests.swift
@@ -0,0 +1,232 @@
+//
+// LegacyV2Tests.swift
+// Passepartout
+//
+// Created by Davide De Rosa on 11/12/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 .
+//
+
+import CommonUtils
+import Foundation
+@testable import LegacyV2
+import PassepartoutKit
+import XCTest
+
+final class LegacyV2Tests: XCTestCase {
+ func test_givenStore_whenFetchV2_thenReturnsProfilesV2() async throws {
+ let sut = newStore()
+
+ let profilesV2 = try await sut.fetchProfilesV2()
+ XCTAssertEqual(profilesV2.count, 6)
+ XCTAssertEqual(Set(profilesV2.map(\.header.name)), [
+ "Hide.me",
+ "ProtonVPN",
+ "TorGuard",
+ "vps-ta-cert-cbc256-lzo",
+ "vps-wg",
+ "Windscribe"
+ ])
+ }
+
+ func test_givenStore_whenFetch_thenReturnsMigratableProfiles() async throws {
+ let sut = newStore()
+
+ let migratable = try await sut.fetchMigratableProfiles()
+ let expectedIDs = [
+ "069F76BD-1F6B-425C-AD83-62477A8B6558",
+ "239AD322-7440-4198-990A-D91379916FE2",
+ "38208B87-0545-4B11-A762-D04ED7CB904F",
+ "5D108793-7F62-4B4C-B194-0A7204C02E99",
+ "8A568345-85C4-44C1-A9C4-612E8B07ADC5",
+ "981E7CBD-7733-4CF3-9A51-2777614ED5D4"
+ ]
+ let expectedNames = [
+ "Hide.me",
+ "ProtonVPN",
+ "TorGuard",
+ "vps-ta-cert-cbc256-lzo",
+ "vps-wg",
+ "Windscribe"
+ ]
+
+ XCTAssertEqual(migratable.count, 6)
+ XCTAssertEqual(Set(migratable.map(\.id)), Set(expectedIDs.compactMap(UUID.init(uuidString:))))
+ XCTAssertEqual(Set(migratable.map(\.name)), Set(expectedNames))
+ }
+
+ func test_givenStore_whenMigrateHideMe_thenIsExpected() async throws {
+ let sut = newStore()
+
+ let id = try XCTUnwrap(UUID(uuidString: "8A568345-85C4-44C1-A9C4-612E8B07ADC5"))
+ let result = try await sut.fetchProfiles(selection: [id])
+ let migrated = result.migrated
+ XCTAssertEqual(migrated.count, 1)
+ XCTAssertTrue(result.failed.isEmpty)
+
+ let profile = try XCTUnwrap(migrated.first)
+ XCTAssertEqual(profile.id, id)
+ XCTAssertEqual(profile.name, "Hide.me")
+ XCTAssertEqual(profile.attributes.lastUpdate, Date(timeIntervalSinceReferenceDate: 673117681.24825))
+
+ XCTAssertEqual(profile.modules.count, 3)
+
+ let onDemand = try XCTUnwrap(profile.firstModule(ofType: OnDemandModule.self))
+ XCTAssertTrue(onDemand.isEnabled)
+ XCTAssertEqual(onDemand.policy, .excluding)
+ XCTAssertEqual(onDemand.withSSIDs, [
+ "Safe Wi-Fi": true,
+ "Friend's House": false
+ ])
+ XCTAssertTrue(onDemand.withOtherNetworks.isEmpty)
+
+ let openVPN = try XCTUnwrap(profile.firstModule(ofType: OpenVPNModule.self))
+ XCTAssertEqual(openVPN.credentials?.username, "foo")
+ XCTAssertEqual(openVPN.credentials?.password, "bar")
+
+ let dns = try XCTUnwrap(profile.firstModule(ofType: DNSModule.self))
+ let dohURL = try XCTUnwrap(URL(string: "https://1.1.1.1/dns-query"))
+ XCTAssertEqual(dns.protocolType, .https(url: dohURL))
+ XCTAssertEqual(dns.servers, [
+ Address(rawValue: "1.1.1.1"),
+ Address(rawValue: "1.0.0.1")
+ ])
+ }
+
+ func test_givenStore_whenMigrateProtonVPN_thenIsExpected() async throws {
+ let sut = newStore()
+
+ let id = try XCTUnwrap(UUID(uuidString: "981E7CBD-7733-4CF3-9A51-2777614ED5D4"))
+ let result = try await sut.fetchProfiles(selection: [id])
+ let migrated = result.migrated
+ XCTAssertEqual(migrated.count, 1)
+ XCTAssertTrue(result.failed.isEmpty)
+
+ XCTAssertEqual(migrated.count, 1)
+ let profile = try XCTUnwrap(migrated.first)
+ XCTAssertEqual(profile.id, id)
+ XCTAssertEqual(profile.name, "ProtonVPN")
+ XCTAssertEqual(profile.attributes.lastUpdate, Date(timeIntervalSinceReferenceDate: 724509584.854822))
+
+ XCTAssertEqual(profile.modules.count, 2)
+
+ let onDemand = try XCTUnwrap(profile.firstModule(ofType: OnDemandModule.self))
+ XCTAssertTrue(onDemand.isEnabled)
+ XCTAssertEqual(onDemand.policy, .excluding)
+ XCTAssertTrue(onDemand.withSSIDs.isEmpty)
+ XCTAssertTrue(onDemand.withOtherNetworks.isEmpty)
+
+ let openVPN = try XCTUnwrap(profile.firstModule(ofType: OpenVPNModule.self))
+ XCTAssertEqual(openVPN.credentials?.username, "foo")
+ XCTAssertEqual(openVPN.credentials?.password, "bar")
+ }
+
+ func test_givenStore_whenMigrateVPSOpenVPN_thenIsExpected() async throws {
+ let sut = newStore()
+
+ let id = try XCTUnwrap(UUID(uuidString: "239AD322-7440-4198-990A-D91379916FE2"))
+ let result = try await sut.fetchProfiles(selection: [id])
+ let migrated = result.migrated
+ XCTAssertEqual(migrated.count, 1)
+ XCTAssertTrue(result.failed.isEmpty)
+
+ XCTAssertEqual(migrated.count, 1)
+ let profile = try XCTUnwrap(migrated.first)
+ XCTAssertEqual(profile.id, id)
+ XCTAssertEqual(profile.name, "vps-ta-cert-cbc256-lzo")
+ XCTAssertEqual(profile.attributes.lastUpdate, Date(timeIntervalSinceReferenceDate: 726164772.28976))
+
+ XCTAssertEqual(profile.modules.count, 2)
+
+ let onDemand = try XCTUnwrap(profile.firstModule(ofType: OnDemandModule.self))
+ XCTAssertTrue(onDemand.isEnabled)
+ XCTAssertEqual(onDemand.policy, .excluding)
+ XCTAssertTrue(onDemand.withSSIDs.isEmpty)
+ XCTAssertTrue(onDemand.withOtherNetworks.isEmpty)
+
+ let openVPN = try XCTUnwrap(profile.firstModule(ofType: OpenVPNModule.self))
+ XCTAssertNil(openVPN.credentials)
+ let cfg = try XCTUnwrap(openVPN.configuration)
+ XCTAssertEqual(cfg.remotes, [
+ try .init("1.2.3.4", .init(.udp, 1198))
+ ])
+ XCTAssertEqual(cfg.authUserPass, false)
+ XCTAssertEqual(cfg.cipher, .aes256cbc)
+ XCTAssertEqual(cfg.digest, .sha256)
+ XCTAssertEqual(cfg.keepAliveInterval, 25.0)
+ XCTAssertEqual(cfg.checksEKU, true)
+ XCTAssertEqual(cfg.tlsWrap?.strategy, .auth)
+ }
+
+ func test_givenStore_whenMigrateVPSWireGuard_thenIsExpected() async throws {
+ let sut = newStore()
+
+ let id = try XCTUnwrap(UUID(uuidString: "069F76BD-1F6B-425C-AD83-62477A8B6558"))
+ let result = try await sut.fetchProfiles(selection: [id])
+ let migrated = result.migrated
+ XCTAssertEqual(migrated.count, 1)
+ XCTAssertTrue(result.failed.isEmpty)
+
+ XCTAssertEqual(migrated.count, 1)
+ let profile = try XCTUnwrap(migrated.first)
+ XCTAssertEqual(profile.id, id)
+ XCTAssertEqual(profile.name, "vps-wg")
+ XCTAssertEqual(profile.attributes.lastUpdate, Date(timeIntervalSinceReferenceDate: 727398252.46203))
+
+ XCTAssertEqual(profile.modules.count, 2)
+
+ let onDemand = try XCTUnwrap(profile.firstModule(ofType: OnDemandModule.self))
+ XCTAssertFalse(onDemand.isEnabled)
+ XCTAssertEqual(onDemand.policy, .including)
+ XCTAssertTrue(onDemand.withSSIDs.isEmpty)
+ XCTAssertTrue(onDemand.withOtherNetworks.isEmpty)
+
+ let wireGuard = try XCTUnwrap(profile.firstModule(ofType: WireGuardModule.self))
+ let cfg = try XCTUnwrap(wireGuard.configuration)
+ XCTAssertEqual(cfg.interface.privateKey.rawValue, "6L8Cv9zpG8RTDDwvZMhv6OR3kGdd+yATuKnMQWVLT1Q=")
+ XCTAssertEqual(cfg.interface.addresses, [
+ try .init("4.5.6.7", 32)
+ ])
+ XCTAssertEqual(cfg.interface.dns?.servers, [
+ try XCTUnwrap(Address(rawValue: "1.1.1.1"))
+ ])
+ XCTAssertNil(cfg.interface.mtu)
+ XCTAssertEqual(cfg.peers.count, 1)
+ let peer = try XCTUnwrap(cfg.peers.first)
+ XCTAssertEqual(peer.publicKey.rawValue, "JZc2trzk1WZTOUTjag1lcUZ2ePpFQYSpU2d0wqAw6mU=")
+ XCTAssertEqual(peer.endpoint?.rawValue, "8.8.8.8:55555")
+ XCTAssertEqual(peer.allowedIPs, [
+ try .init("0.0.0.0", 0)
+ ])
+ }
+}
+
+private extension LegacyV2Tests {
+ func newStore() -> LegacyV2 {
+ guard let baseURL = Bundle.module.url(forResource: "Resources", withExtension: nil) else {
+ fatalError()
+ }
+ return LegacyV2(
+ coreDataLogger: nil,
+ profilesContainerName: "Profiles",
+ baseURL: baseURL,
+ cloudKitIdentifier: nil
+ )
+ }
+}
diff --git a/Passepartout/Library/Tests/LegacyV2Tests/MapperV2Tests.swift b/Passepartout/Library/Tests/LegacyV2Tests/MapperV2Tests.swift
new file mode 100644
index 00000000..c0ae1e51
--- /dev/null
+++ b/Passepartout/Library/Tests/LegacyV2Tests/MapperV2Tests.swift
@@ -0,0 +1,60 @@
+//
+// MapperV2Tests.swift
+// Passepartout
+//
+// Created by Davide De Rosa on 11/12/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 .
+//
+
+import Foundation
+@testable import LegacyV2
+import PassepartoutKit
+import XCTest
+
+final class MapperV2Tests: XCTestCase {
+ func test_givenMapper_whenDefaultGateway_thenIncludesDefaultRoute() throws {
+ let sut = MapperV2()
+ var settings = Network.GatewaySettings(choice: .manual)
+ var module: IPModule
+
+ settings.isDefaultIPv4 = true
+ module = try sut.toIPModule(settings, v2MTU: nil)
+ XCTAssertTrue(module.ipv4?.includesDefaultRoute ?? false)
+
+ settings.isDefaultIPv6 = true
+ module = try sut.toIPModule(settings, v2MTU: nil)
+ XCTAssertTrue(module.ipv6?.includesDefaultRoute ?? false)
+ }
+
+ func test_givenMapper_whenNotDefaultGateway_thenExcludesDefaultRoute() throws {
+ let sut = MapperV2()
+ var settings = Network.GatewaySettings(choice: .manual)
+ var module: IPModule
+ let defaultRoute = Route(defaultWithGateway: nil)
+
+ settings.isDefaultIPv4 = false
+ module = try sut.toIPModule(settings, v2MTU: nil)
+ XCTAssertTrue(module.ipv4?.excludedRoutes.contains(defaultRoute) ?? false)
+
+ settings.isDefaultIPv6 = false
+ module = try sut.toIPModule(settings, v2MTU: nil)
+ XCTAssertTrue(module.ipv6?.excludedRoutes.contains(defaultRoute) ?? false)
+ }
+}
diff --git a/Passepartout/Library/Tests/LegacyV2Tests/Resources/Profiles.sqlite b/Passepartout/Library/Tests/LegacyV2Tests/Resources/Profiles.sqlite
new file mode 100644
index 00000000..cf6bd637
Binary files /dev/null and b/Passepartout/Library/Tests/LegacyV2Tests/Resources/Profiles.sqlite differ
diff --git a/Passepartout/Passepartout.xctestplan b/Passepartout/Passepartout.xctestplan
index f59bfe2b..b4c3bdc6 100644
--- a/Passepartout/Passepartout.xctestplan
+++ b/Passepartout/Passepartout.xctestplan
@@ -32,6 +32,13 @@
"identifier" : "CommonLibraryTests",
"name" : "CommonLibraryTests"
}
+ },
+ {
+ "target" : {
+ "containerPath" : "container:Passepartout\/Library",
+ "identifier" : "LegacyV2Tests",
+ "name" : "LegacyV2Tests"
+ }
}
],
"version" : 1
diff --git a/fastlane/.env b/fastlane/.env
index 562f8922..24fd4515 100644
--- a/fastlane/.env
+++ b/fastlane/.env
@@ -2,6 +2,7 @@ LC_ALL="en_US.UTF-8"
LANG="en_US.UTF-8"
GYM_SCHEME="Passepartout"
+SCAN_SCHEME="Passepartout"
FL_VERSION_NUMBER_TARGET="Passepartout"
FL_BUILD_NUMBER_PROJECT="Passepartout.xcodeproj"
PILOT_BETA_APP_DESCRIPTION="Passepartout is your go-to app for VPN and privacy."
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index 647ccd36..9e65c859 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -61,6 +61,15 @@ lane :bump do |options|
)
end
+desc "Run app tests"
+lane :test do
+ scan(
+ clean: true,
+ xcargs: "CODE_SIGNING_ALLOWED=NO",
+ verbose: true
+ )
+end
+
desc "Push a new beta build to TestFlight"
lane :beta do
preface = ENV["TESTFLIGHT_PREFACE"]