Test MigrationManager (#904)

Decouple importer from ProfileManager.
This commit is contained in:
Davide 2024-11-21 19:44:20 +01:00 committed by GitHub
parent d74c94c125
commit 5bec574841
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 310 additions and 4 deletions

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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])
}
}

View File

@ -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
}
}

View File

@ -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)
}
}
}