parent
d74c94c125
commit
5bec574841
|
@ -26,6 +26,10 @@
|
|||
import Foundation
|
||||
import PassepartoutKit
|
||||
|
||||
public protocol MigrationManagerImporter {
|
||||
func importProfile(_ profile: Profile) async throws
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public final class MigrationManager: ObservableObject {
|
||||
public struct Simulation {
|
||||
|
@ -103,7 +107,7 @@ extension MigrationManager {
|
|||
|
||||
public func importProfiles(
|
||||
_ profiles: [Profile],
|
||||
into manager: ProfileManager,
|
||||
into importer: MigrationManagerImporter,
|
||||
onUpdate: @escaping @MainActor (UUID, MigrationStatus) -> Void
|
||||
) async {
|
||||
profiles.forEach {
|
||||
|
@ -114,7 +118,7 @@ extension MigrationManager {
|
|||
group.addTask {
|
||||
do {
|
||||
try await self.simulateBehavior()
|
||||
try await self.simulateSaveProfile(profile, manager: manager)
|
||||
try await self.simulateSaveProfile(profile, to: importer)
|
||||
await onUpdate(profile.id, .done)
|
||||
} catch {
|
||||
await onUpdate(profile.id, .failed)
|
||||
|
@ -151,11 +155,11 @@ private extension MigrationManager {
|
|||
return try await profileStrategy.fetchProfile(withId: profileId)
|
||||
}
|
||||
|
||||
func simulateSaveProfile(_ profile: Profile, manager: ProfileManager) async throws {
|
||||
func simulateSaveProfile(_ profile: Profile, to importer: MigrationManagerImporter) async throws {
|
||||
if simulation?.fakeProfiles ?? false {
|
||||
return
|
||||
}
|
||||
try await manager.save(profile, force: true)
|
||||
try await importer.importProfile(profile)
|
||||
}
|
||||
|
||||
func simulateDeleteProfiles(withIds profileIds: Set<UUID>) async throws {
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
//
|
||||
// ProfileManager+Importer.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 11/21/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
|
||||
|
||||
extension ProfileManager: MigrationManagerImporter {
|
||||
public func importProfile(_ profile: Profile) async throws {
|
||||
try await save(profile, force: true)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,163 @@
|
|||
//
|
||||
// MigrationManagerTests.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 11/21/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 CommonUtils
|
||||
import Foundation
|
||||
import PassepartoutKit
|
||||
import XCTest
|
||||
|
||||
@MainActor
|
||||
final class MigrationManagerTests: XCTestCase {
|
||||
}
|
||||
|
||||
extension MigrationManagerTests {
|
||||
func test_givenStrategy_whenFetchMigratable_thenReturnsMigratable() async throws {
|
||||
let strategy = MockProfileMigrationStrategy()
|
||||
strategy.migratableProfiles = [
|
||||
.init(id: UUID(), name: "one", lastUpdate: nil),
|
||||
.init(id: UUID(), name: "two", lastUpdate: nil),
|
||||
.init(id: UUID(), name: "three", lastUpdate: nil)
|
||||
]
|
||||
let sut = MigrationManager(profileStrategy: strategy)
|
||||
let migratable = try await sut.fetchMigratableProfiles()
|
||||
XCTAssertEqual(migratable, strategy.migratableProfiles)
|
||||
}
|
||||
|
||||
func test_givenStrategy_whenMigrateProfile_thenReturnsMigrated() async throws {
|
||||
let uuid = UUID()
|
||||
let expProfile = try {
|
||||
var builder = Profile.Builder(id: uuid)
|
||||
builder.name = "foobar"
|
||||
builder.userInfo = ["user": "info"]
|
||||
return try builder.tryBuild()
|
||||
}()
|
||||
|
||||
let strategy = MockProfileMigrationStrategy()
|
||||
strategy.migratableProfiles = [
|
||||
.init(id: uuid, name: expProfile.name, lastUpdate: nil)
|
||||
]
|
||||
strategy.migratedProfiles = [
|
||||
uuid: expProfile
|
||||
]
|
||||
let sut = MigrationManager(profileStrategy: strategy)
|
||||
|
||||
let migratable = try await sut.fetchMigratableProfiles()
|
||||
XCTAssertEqual(migratable.count, 1)
|
||||
let firstMigratable = try XCTUnwrap(migratable.first)
|
||||
|
||||
guard let migrated = try await sut.migratedProfile(withId: firstMigratable.id) else {
|
||||
XCTFail("Profile not found")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(migrated.id, uuid)
|
||||
XCTAssertEqual(migrated.name, "foobar")
|
||||
XCTAssertEqual(migrated.userInfo?["user"], "info")
|
||||
}
|
||||
|
||||
func test_givenStrategy_whenMigrateProfiles_thenReturnsMigratedWithUpdates() async throws {
|
||||
let profile1 = try Profile.Builder(name: "one").tryBuild()
|
||||
let profile2 = try Profile.Builder(name: "two").tryBuild()
|
||||
let strategy = MockProfileMigrationStrategy()
|
||||
strategy.migratableProfiles = [
|
||||
.init(id: profile1.id, name: profile1.name, lastUpdate: nil),
|
||||
.init(id: profile2.id, name: profile2.name, lastUpdate: nil)
|
||||
]
|
||||
strategy.migratedProfiles = [
|
||||
profile1.id: profile1
|
||||
]
|
||||
strategy.failedProfiles = [profile2.id]
|
||||
let sut = MigrationManager(profileStrategy: strategy)
|
||||
|
||||
let migratable = try await sut.fetchMigratableProfiles()
|
||||
|
||||
var pending: Set<UUID> = [profile1.id, profile2.id]
|
||||
let migrated = try await sut.migratedProfiles(migratable) { uuid, status in
|
||||
if pending.contains(uuid) {
|
||||
XCTAssertEqual(status, .pending)
|
||||
pending.remove(uuid)
|
||||
return
|
||||
}
|
||||
switch uuid {
|
||||
case profile1.id:
|
||||
XCTAssertEqual(status, .done)
|
||||
case profile2.id:
|
||||
XCTAssertEqual(status, .failed)
|
||||
default:
|
||||
XCTFail("Unexpected UUID")
|
||||
}
|
||||
}
|
||||
|
||||
XCTAssertEqual(migrated, [profile1])
|
||||
}
|
||||
|
||||
func test_givenStrategy_whenImportProfiles_thenSavesMigratedWithUpdates() async throws {
|
||||
let profile1 = try Profile.Builder(name: "one").tryBuild()
|
||||
let profile2 = try Profile.Builder(name: "two").tryBuild()
|
||||
let profile3 = try Profile.Builder(name: "three").tryBuild()
|
||||
let strategy = MockProfileMigrationStrategy()
|
||||
let sut = MigrationManager(profileStrategy: strategy)
|
||||
|
||||
let migrated = [profile1, profile2, profile3]
|
||||
let importer = MockMigrationManagerImporter(failing: [profile1.id])
|
||||
var pending = Set(migrated.map(\.id))
|
||||
await sut.importProfiles(migrated, into: importer) { uuid, status in
|
||||
if pending.contains(uuid) {
|
||||
XCTAssertEqual(status, .pending)
|
||||
pending.remove(uuid)
|
||||
return
|
||||
}
|
||||
switch uuid {
|
||||
case profile1.id:
|
||||
XCTAssertEqual(status, .failed)
|
||||
case profile2.id:
|
||||
XCTAssertEqual(status, .done)
|
||||
case profile3.id:
|
||||
XCTAssertEqual(status, .done)
|
||||
default:
|
||||
XCTFail("Unexpected UUID")
|
||||
}
|
||||
}
|
||||
|
||||
let imported = await importer.importedProfiles()
|
||||
XCTAssertEqual(imported, [profile2, profile3])
|
||||
}
|
||||
|
||||
func test_givenStrategy_whenDeleteMigratable_thenDeletesMigratable() async throws {
|
||||
let strategy = MockProfileMigrationStrategy()
|
||||
let id1 = UUID()
|
||||
let id2 = UUID()
|
||||
let id3 = UUID()
|
||||
strategy.migratableProfiles = [
|
||||
.init(id: id1, name: "one", lastUpdate: nil),
|
||||
.init(id: id2, name: "two", lastUpdate: nil),
|
||||
.init(id: id3, name: "three", lastUpdate: nil)
|
||||
]
|
||||
let sut = MigrationManager(profileStrategy: strategy)
|
||||
try await sut.deleteMigratableProfiles(withIds: [id1, id2])
|
||||
let migratable = try await sut.fetchMigratableProfiles()
|
||||
XCTAssertEqual(migratable.map(\.id), [id3])
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
//
|
||||
// MockMigrationManagerImporter.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 11/21/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 CommonLibrary
|
||||
import Foundation
|
||||
import PassepartoutKit
|
||||
|
||||
actor MockMigrationManagerImporter: MigrationManagerImporter {
|
||||
private var failing: Set<UUID>
|
||||
|
||||
private var imported: Set<Profile>
|
||||
|
||||
init(failing: Set<UUID>) {
|
||||
self.failing = failing
|
||||
imported = []
|
||||
}
|
||||
|
||||
func importProfile(_ profile: Profile) async throws {
|
||||
guard !failing.contains(profile.id) else {
|
||||
throw AppError.permissionDenied
|
||||
}
|
||||
imported.insert(profile)
|
||||
}
|
||||
|
||||
func importedProfiles() async -> Set<Profile> {
|
||||
imported
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
//
|
||||
// MockProfileMigrationStrategy.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 11/21/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 CommonLibrary
|
||||
import Foundation
|
||||
import PassepartoutKit
|
||||
|
||||
final class MockProfileMigrationStrategy: ProfileMigrationStrategy {
|
||||
var migratableProfiles: [MigratableProfile] = []
|
||||
|
||||
var migratedProfiles: [UUID: Profile] = [:]
|
||||
|
||||
var failedProfiles: Set<UUID> = []
|
||||
|
||||
func fetchMigratableProfiles() async throws -> [MigratableProfile] {
|
||||
migratableProfiles
|
||||
}
|
||||
|
||||
func fetchProfile(withId profileId: UUID) async throws -> Profile? {
|
||||
if failedProfiles.contains(profileId) {
|
||||
throw AppError.permissionDenied
|
||||
}
|
||||
return migratedProfiles[profileId]
|
||||
}
|
||||
|
||||
func deleteProfiles(withIds profileIds: Set<UUID>) async throws {
|
||||
profileIds.forEach { id in
|
||||
migratableProfiles.removeAll {
|
||||
$0.id == id
|
||||
}
|
||||
migratedProfiles.removeValue(forKey: id)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue