mirror of
https://github.com/passepartoutvpn/passepartout-apple.git
synced 2025-01-08 17:52:38 +00:00
aeec943c58
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
680 lines
25 KiB
Swift
680 lines
25 KiB
Swift
//
|
|
// ProfileManagerTests.swift
|
|
// Passepartout
|
|
//
|
|
// Created by Davide De Rosa on 9/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 <http://www.gnu.org/licenses/>.
|
|
//
|
|
|
|
import Combine
|
|
@testable import CommonLibrary
|
|
import Foundation
|
|
import PassepartoutKit
|
|
import XCTest
|
|
|
|
@MainActor
|
|
final class ProfileManagerTests: XCTestCase {
|
|
private var subscriptions: Set<AnyCancellable> = []
|
|
}
|
|
|
|
extension ProfileManagerTests {
|
|
}
|
|
|
|
// MARK: - View
|
|
|
|
extension ProfileManagerTests {
|
|
func test_givenStatic_whenNotReady_thenHasProfiles() {
|
|
let profile = newProfile()
|
|
let sut = ProfileManager(profiles: [profile])
|
|
XCTAssertFalse(sut.isReady)
|
|
XCTAssertFalse(sut.hasProfiles)
|
|
XCTAssertTrue(sut.previews.isEmpty)
|
|
}
|
|
|
|
func test_givenRepository_whenNotReady_thenHasNoProfiles() {
|
|
let repository = InMemoryProfileRepository(profiles: [])
|
|
let sut = ProfileManager(repository: repository, remoteRepositoryBlock: nil)
|
|
XCTAssertFalse(sut.isReady)
|
|
XCTAssertFalse(sut.hasProfiles)
|
|
XCTAssertTrue(sut.previews.isEmpty)
|
|
}
|
|
|
|
func test_givenRepository_whenReady_thenHasProfiles() async throws {
|
|
let profile = newProfile()
|
|
let repository = InMemoryProfileRepository(profiles: [profile])
|
|
let sut = ProfileManager(repository: repository, remoteRepositoryBlock: nil)
|
|
|
|
try await waitForReady(sut)
|
|
XCTAssertTrue(sut.isReady)
|
|
XCTAssertTrue(sut.hasProfiles)
|
|
XCTAssertEqual(sut.previews.count, 1)
|
|
XCTAssertEqual(sut.profile(withId: profile.id), profile)
|
|
}
|
|
|
|
func test_givenRepository_whenSearch_thenIsSearching() async throws {
|
|
let profile1 = newProfile("foo")
|
|
let profile2 = newProfile("bar")
|
|
let repository = InMemoryProfileRepository(profiles: [profile1, profile2])
|
|
let sut = ProfileManager(repository: repository, remoteRepositoryBlock: nil)
|
|
|
|
try await waitForReady(sut)
|
|
XCTAssertTrue(sut.isReady)
|
|
XCTAssertTrue(sut.hasProfiles)
|
|
XCTAssertEqual(sut.previews.count, 2)
|
|
|
|
try await wait(sut, "Search", until: .filteredProfiles) {
|
|
$0.search(byName: "ar")
|
|
}
|
|
XCTAssertTrue(sut.isSearching)
|
|
XCTAssertEqual(sut.previews.count, 1)
|
|
let found = try XCTUnwrap(sut.previews.last)
|
|
XCTAssertEqual(found.id, profile2.id)
|
|
}
|
|
|
|
func test_givenRepositoryAndProcessor_whenReady_thenHasInvokedProcessor() async throws {
|
|
let profile = newProfile()
|
|
let repository = InMemoryProfileRepository(profiles: [profile])
|
|
let processor = MockProfileProcessor()
|
|
processor.requiredFeatures = [.appleTV, .onDemand]
|
|
let sut = ProfileManager(repository: repository, remoteRepositoryBlock: nil, processor: processor)
|
|
|
|
try await waitForReady(sut)
|
|
XCTAssertTrue(sut.isReady)
|
|
|
|
XCTAssertEqual(processor.isIncludedCount, 1)
|
|
XCTAssertEqual(processor.requiredFeaturesCount, 1)
|
|
XCTAssertEqual(processor.willRebuildCount, 0)
|
|
XCTAssertEqual(sut.requiredFeatures(forProfileWithId: profile.id), processor.requiredFeatures)
|
|
}
|
|
|
|
func test_givenRepositoryAndProcessor_whenIncludedProfiles_thenLoadsIncluded() async throws {
|
|
let localProfiles = [
|
|
newProfile("local1"),
|
|
newProfile("local2"),
|
|
newProfile("local3")
|
|
]
|
|
let repository = InMemoryProfileRepository(profiles: localProfiles)
|
|
let processor = MockProfileProcessor()
|
|
processor.isIncludedBlock = {
|
|
$0.name == "local2"
|
|
}
|
|
let sut = ProfileManager(repository: repository, remoteRepositoryBlock: nil, processor: processor)
|
|
|
|
try await waitForReady(sut)
|
|
XCTAssertTrue(sut.isReady)
|
|
|
|
XCTAssertEqual(sut.previews.count, 1)
|
|
XCTAssertEqual(sut.previews.first?.name, "local2")
|
|
}
|
|
|
|
func test_givenRepositoryAndProcessor_whenRequiredFeaturesChange_thenMustReload() async throws {
|
|
let profile = newProfile()
|
|
let repository = InMemoryProfileRepository(profiles: [profile])
|
|
let processor = MockProfileProcessor()
|
|
processor.requiredFeatures = [.appleTV, .onDemand]
|
|
let sut = ProfileManager(repository: repository, remoteRepositoryBlock: nil, processor: processor)
|
|
|
|
try await waitForReady(sut)
|
|
XCTAssertTrue(sut.isReady)
|
|
|
|
XCTAssertEqual(sut.requiredFeatures(forProfileWithId: profile.id), processor.requiredFeatures)
|
|
processor.requiredFeatures = [.interactiveLogin]
|
|
XCTAssertNotEqual(sut.requiredFeatures(forProfileWithId: profile.id), processor.requiredFeatures)
|
|
sut.reloadRequiredFeatures()
|
|
XCTAssertEqual(sut.requiredFeatures(forProfileWithId: profile.id), processor.requiredFeatures)
|
|
|
|
processor.requiredFeatures = nil
|
|
XCTAssertNotNil(sut.requiredFeatures(forProfileWithId: profile.id))
|
|
sut.reloadRequiredFeatures()
|
|
XCTAssertNil(sut.requiredFeatures(forProfileWithId: profile.id))
|
|
processor.requiredFeatures = []
|
|
sut.reloadRequiredFeatures()
|
|
XCTAssertNil(sut.requiredFeatures(forProfileWithId: profile.id))
|
|
}
|
|
}
|
|
|
|
// MARK: - Edit
|
|
|
|
extension ProfileManagerTests {
|
|
func test_givenRepository_whenSave_thenIsSaved() async throws {
|
|
let repository = InMemoryProfileRepository(profiles: [])
|
|
let sut = ProfileManager(repository: repository, remoteRepositoryBlock: nil)
|
|
|
|
try await waitForReady(sut)
|
|
XCTAssertTrue(sut.isReady)
|
|
XCTAssertFalse(sut.hasProfiles)
|
|
|
|
let profile = newProfile()
|
|
try await wait(sut, "Save", until: .save(profile)) {
|
|
try await $0.save(profile)
|
|
}
|
|
XCTAssertEqual(sut.previews.count, 1)
|
|
XCTAssertEqual(sut.profile(withId: profile.id), profile)
|
|
}
|
|
|
|
func test_givenRepository_whenSaveExisting_thenIsReplaced() async throws {
|
|
let profile = newProfile("oldName")
|
|
let repository = InMemoryProfileRepository(profiles: [profile])
|
|
let sut = ProfileManager(repository: repository, remoteRepositoryBlock: nil)
|
|
|
|
try await waitForReady(sut)
|
|
XCTAssertTrue(sut.isReady)
|
|
XCTAssertEqual(sut.previews.first?.id, profile.id)
|
|
|
|
var builder = profile.builder()
|
|
builder.name = "newName"
|
|
let renamedProfile = try builder.tryBuild()
|
|
|
|
try await wait(sut, "Save", until: .save(renamedProfile)) {
|
|
try await $0.save(renamedProfile)
|
|
}
|
|
XCTAssertEqual(sut.previews.first?.name, renamedProfile.name)
|
|
}
|
|
|
|
func test_givenRepositoryAndProcessor_whenSave_thenProcessorIsNotInvoked() async throws {
|
|
let repository = InMemoryProfileRepository(profiles: [])
|
|
let processor = MockProfileProcessor()
|
|
let sut = ProfileManager(repository: repository, remoteRepositoryBlock: nil, processor: processor)
|
|
|
|
try await waitForReady(sut)
|
|
XCTAssertTrue(sut.isReady)
|
|
XCTAssertFalse(sut.hasProfiles)
|
|
|
|
let profile = newProfile()
|
|
try await sut.save(profile)
|
|
XCTAssertEqual(processor.willRebuildCount, 0)
|
|
try await sut.save(profile, isLocal: false)
|
|
XCTAssertEqual(processor.willRebuildCount, 0)
|
|
}
|
|
|
|
func test_givenRepositoryAndProcessor_whenSaveLocal_thenProcessorIsInvoked() async throws {
|
|
let repository = InMemoryProfileRepository(profiles: [])
|
|
let processor = MockProfileProcessor()
|
|
let sut = ProfileManager(repository: repository, remoteRepositoryBlock: nil, processor: processor)
|
|
|
|
try await waitForReady(sut)
|
|
XCTAssertTrue(sut.isReady)
|
|
XCTAssertFalse(sut.hasProfiles)
|
|
|
|
let profile = newProfile()
|
|
try await sut.save(profile, isLocal: true)
|
|
XCTAssertEqual(processor.willRebuildCount, 1)
|
|
}
|
|
|
|
func test_givenRepository_whenSave_thenIsStoredToBackUpRepository() async throws {
|
|
let repository = InMemoryProfileRepository(profiles: [])
|
|
let backupRepository = InMemoryProfileRepository(profiles: [])
|
|
let sut = ProfileManager(repository: repository, backupRepository: backupRepository, remoteRepositoryBlock: nil)
|
|
|
|
try await waitForReady(sut)
|
|
XCTAssertTrue(sut.isReady)
|
|
XCTAssertFalse(sut.hasProfiles)
|
|
|
|
let profile = newProfile()
|
|
let exp = expectation(description: "Backup")
|
|
backupRepository
|
|
.profilesPublisher
|
|
.sink {
|
|
guard !$0.isEmpty else {
|
|
return
|
|
}
|
|
XCTAssertEqual($0.first, profile)
|
|
exp.fulfill()
|
|
}
|
|
.store(in: &subscriptions)
|
|
|
|
try await sut.save(profile)
|
|
await fulfillment(of: [exp], timeout: CommonLibraryTests.timeout)
|
|
}
|
|
|
|
func test_givenRepository_whenRemove_thenIsRemoved() async throws {
|
|
let profile = newProfile()
|
|
let repository = InMemoryProfileRepository(profiles: [profile])
|
|
let sut = ProfileManager(repository: repository, remoteRepositoryBlock: nil)
|
|
|
|
try await waitForReady(sut)
|
|
XCTAssertTrue(sut.isReady)
|
|
XCTAssertTrue(sut.hasProfiles)
|
|
|
|
try await wait(sut, "Remove", until: .remove([profile.id])) {
|
|
await $0.remove(withId: profile.id)
|
|
}
|
|
XCTAssertTrue(sut.previews.isEmpty)
|
|
}
|
|
}
|
|
|
|
// MARK: - Remote/Attributes
|
|
|
|
extension ProfileManagerTests {
|
|
func test_givenRemoteRepository_whenSaveRemotelyShared_thenIsStoredToRemoteRepository() async throws {
|
|
let profile = newProfile()
|
|
let repository = InMemoryProfileRepository()
|
|
let remoteRepository = InMemoryProfileRepository()
|
|
let sut = ProfileManager(repository: repository, remoteRepositoryBlock: { _ in
|
|
remoteRepository
|
|
})
|
|
|
|
try await waitForReady(sut)
|
|
|
|
let exp = expectation(description: "Remote")
|
|
remoteRepository
|
|
.profilesPublisher
|
|
.sink {
|
|
guard !$0.isEmpty else {
|
|
return
|
|
}
|
|
XCTAssertEqual($0.first, profile)
|
|
exp.fulfill()
|
|
}
|
|
.store(in: &subscriptions)
|
|
|
|
try await sut.save(profile, remotelyShared: true)
|
|
await fulfillment(of: [exp], timeout: CommonLibraryTests.timeout)
|
|
|
|
XCTAssertTrue(sut.isRemotelyShared(profileWithId: profile.id))
|
|
}
|
|
|
|
func test_givenRemoteRepository_whenSaveNotRemotelyShared_thenIsRemoveFromRemoteRepository() async throws {
|
|
let profile = newProfile()
|
|
let repository = InMemoryProfileRepository(profiles: [profile])
|
|
let remoteRepository = InMemoryProfileRepository(profiles: [profile])
|
|
let sut = ProfileManager(repository: repository, remoteRepositoryBlock: { _ in
|
|
remoteRepository
|
|
})
|
|
|
|
try await waitForReady(sut)
|
|
|
|
let exp = expectation(description: "Remote")
|
|
remoteRepository
|
|
.profilesPublisher
|
|
.sink {
|
|
guard $0.isEmpty else {
|
|
return
|
|
}
|
|
exp.fulfill()
|
|
}
|
|
.store(in: &subscriptions)
|
|
|
|
try await sut.save(profile, remotelyShared: false)
|
|
await fulfillment(of: [exp], timeout: CommonLibraryTests.timeout)
|
|
|
|
XCTAssertFalse(sut.isRemotelyShared(profileWithId: profile.id))
|
|
}
|
|
}
|
|
|
|
// MARK: - Shortcuts
|
|
|
|
extension ProfileManagerTests {
|
|
func test_givenRepository_whenNew_thenReturnsProfileWithNewName() async throws {
|
|
let profile = newProfile("example")
|
|
let repository = InMemoryProfileRepository(profiles: [profile])
|
|
let sut = ProfileManager(repository: repository, remoteRepositoryBlock: nil)
|
|
|
|
try await waitForReady(sut)
|
|
XCTAssertEqual(sut.previews.count, 1)
|
|
|
|
let newName = sut.firstUniqueName(from: profile.name)
|
|
XCTAssertEqual(newName, "example.1")
|
|
}
|
|
|
|
func test_givenRepository_whenDuplicate_thenSavesProfileWithNewName() async throws {
|
|
let profile = newProfile("example")
|
|
let repository = InMemoryProfileRepository(profiles: [profile])
|
|
let sut = ProfileManager(repository: repository, remoteRepositoryBlock: nil)
|
|
|
|
try await waitForReady(sut)
|
|
|
|
try await wait(sut, "Duplicate 1", until: .localProfiles) {
|
|
try await $0.duplicate(profileWithId: profile.id)
|
|
}
|
|
XCTAssertEqual(sut.previews.count, 2)
|
|
|
|
try await wait(sut, "Duplicate 2", until: .localProfiles) {
|
|
try await $0.duplicate(profileWithId: profile.id)
|
|
}
|
|
XCTAssertEqual(sut.previews.count, 3)
|
|
|
|
try await wait(sut, "Duplicate 3", until: .localProfiles) {
|
|
try await $0.duplicate(profileWithId: profile.id)
|
|
}
|
|
XCTAssertEqual(sut.previews.count, 4)
|
|
|
|
XCTAssertEqual(sut.previews.map(\.name), [
|
|
"example",
|
|
"example.1",
|
|
"example.2",
|
|
"example.3"
|
|
])
|
|
}
|
|
}
|
|
|
|
// MARK: - Observation
|
|
|
|
extension ProfileManagerTests {
|
|
func test_givenRemoteRepository_whenUpdatesWithNewProfiles_thenImportsAll() async throws {
|
|
let localProfiles = [
|
|
newProfile("local1"),
|
|
newProfile("local2")
|
|
]
|
|
let remoteProfiles = [
|
|
newProfile("remote1"),
|
|
newProfile("remote2"),
|
|
newProfile("remote3")
|
|
]
|
|
let allProfiles = localProfiles + remoteProfiles
|
|
let repository = InMemoryProfileRepository(profiles: localProfiles)
|
|
let remoteRepository = InMemoryProfileRepository(profiles: remoteProfiles)
|
|
let sut = ProfileManager(repository: repository, remoteRepositoryBlock: { _ in
|
|
remoteRepository
|
|
})
|
|
|
|
try await wait(sut, "Remote import", until: .stopRemoteImport) {
|
|
try await $0.observeLocal()
|
|
try await $0.observeRemote(true)
|
|
}
|
|
XCTAssertEqual(sut.previews.count, allProfiles.count)
|
|
|
|
XCTAssertEqual(Set(sut.previews), Set(allProfiles.map { ProfilePreview($0) }))
|
|
localProfiles.forEach {
|
|
XCTAssertFalse(sut.isRemotelyShared(profileWithId: $0.id))
|
|
}
|
|
remoteProfiles.forEach {
|
|
XCTAssertTrue(sut.isRemotelyShared(profileWithId: $0.id))
|
|
}
|
|
}
|
|
|
|
func test_givenRemoteRepository_whenUpdatesWithExistingProfiles_thenReplacesLocal() async throws {
|
|
let l1 = UUID()
|
|
let l2 = UUID()
|
|
let l3 = UUID()
|
|
let r3 = UUID()
|
|
let localProfiles = [
|
|
newProfile("local1", id: l1),
|
|
newProfile("local2", id: l2),
|
|
newProfile("local3", id: l3)
|
|
]
|
|
let remoteProfiles = [
|
|
newProfile("remote1", id: l1),
|
|
newProfile("remote2", id: l2),
|
|
newProfile("remote3", id: r3)
|
|
]
|
|
let repository = InMemoryProfileRepository(profiles: localProfiles)
|
|
let remoteRepository = InMemoryProfileRepository(profiles: remoteProfiles)
|
|
let sut = ProfileManager(repository: repository, remoteRepositoryBlock: { _ in
|
|
remoteRepository
|
|
})
|
|
|
|
try await wait(sut, "Remote import", until: .stopRemoteImport) {
|
|
try await $0.observeLocal()
|
|
try await $0.observeRemote(true)
|
|
}
|
|
XCTAssertEqual(sut.previews.count, 4) // unique IDs
|
|
|
|
sut.previews.forEach {
|
|
switch $0.id {
|
|
case l1:
|
|
XCTAssertEqual($0.name, "remote1")
|
|
XCTAssertTrue(sut.isRemotelyShared(profileWithId: $0.id))
|
|
case l2:
|
|
XCTAssertEqual($0.name, "remote2")
|
|
XCTAssertTrue(sut.isRemotelyShared(profileWithId: $0.id))
|
|
case l3:
|
|
XCTAssertEqual($0.name, "local3")
|
|
XCTAssertFalse(sut.isRemotelyShared(profileWithId: $0.id))
|
|
case r3:
|
|
XCTAssertEqual($0.name, "remote3")
|
|
XCTAssertTrue(sut.isRemotelyShared(profileWithId: $0.id))
|
|
default:
|
|
XCTFail("Unknown profile: \($0.id)")
|
|
}
|
|
}
|
|
}
|
|
|
|
func test_givenRemoteRepository_whenUpdatesWithNotIncludedProfiles_thenImportsNone() async throws {
|
|
let localProfiles = [
|
|
newProfile("local1"),
|
|
newProfile("local2")
|
|
]
|
|
let remoteProfiles = [
|
|
newProfile("remote1"),
|
|
newProfile("remote2"),
|
|
newProfile("remote3")
|
|
]
|
|
let allProfiles = localProfiles + remoteProfiles
|
|
let repository = InMemoryProfileRepository(profiles: localProfiles)
|
|
let remoteRepository = InMemoryProfileRepository(profiles: remoteProfiles)
|
|
let processor = MockProfileProcessor()
|
|
processor.isIncludedBlock = {
|
|
!$0.name.hasPrefix("remote")
|
|
}
|
|
let sut = ProfileManager(repository: repository, remoteRepositoryBlock: { _ in
|
|
remoteRepository
|
|
}, processor: processor)
|
|
|
|
try await wait(sut, "Remote import", until: .stopRemoteImport) {
|
|
try await $0.observeLocal()
|
|
try await $0.observeRemote(true)
|
|
}
|
|
|
|
XCTAssertEqual(processor.isIncludedCount, allProfiles.count)
|
|
XCTAssertEqual(Set(sut.previews), Set(localProfiles.map { ProfilePreview($0) }))
|
|
localProfiles.forEach {
|
|
XCTAssertFalse(sut.isRemotelyShared(profileWithId: $0.id))
|
|
}
|
|
remoteProfiles.forEach {
|
|
XCTAssertNil(sut.profile(withId: $0.id))
|
|
XCTAssertTrue(sut.isRemotelyShared(profileWithId: $0.id))
|
|
}
|
|
}
|
|
|
|
func test_givenRemoteRepository_whenUpdatesWithSameFingerprint_thenDoesNotImport() async throws {
|
|
let l1 = UUID()
|
|
let l2 = UUID()
|
|
let fp1 = UUID()
|
|
let localProfiles = [
|
|
newProfile("local1", id: l1, fingerprint: fp1),
|
|
newProfile("local2", id: l2, fingerprint: UUID())
|
|
]
|
|
let remoteProfiles = [
|
|
newProfile("remote1", id: l1, fingerprint: fp1),
|
|
newProfile("remote2", id: l2, fingerprint: UUID())
|
|
]
|
|
let repository = InMemoryProfileRepository(profiles: localProfiles)
|
|
let remoteRepository = InMemoryProfileRepository(profiles: remoteProfiles)
|
|
let processor = MockProfileProcessor()
|
|
let sut = ProfileManager(repository: repository, remoteRepositoryBlock: { _ in
|
|
remoteRepository
|
|
}, processor: processor)
|
|
|
|
try await wait(sut, "Remote import", until: .stopRemoteImport) {
|
|
try await $0.observeLocal()
|
|
try await $0.observeRemote(true)
|
|
}
|
|
|
|
try sut.previews.forEach {
|
|
let profile = try XCTUnwrap(sut.profile(withId: $0.id))
|
|
XCTAssertTrue(sut.isRemotelyShared(profileWithId: $0.id))
|
|
switch $0.id {
|
|
case l1:
|
|
XCTAssertEqual(profile.name, "local1")
|
|
XCTAssertEqual(profile.attributes.fingerprint, localProfiles[0].attributes.fingerprint)
|
|
case l2:
|
|
XCTAssertEqual(profile.name, "remote2")
|
|
XCTAssertEqual(profile.attributes.fingerprint, remoteProfiles[1].attributes.fingerprint)
|
|
default:
|
|
XCTFail("Unknown profile: \($0.id)")
|
|
}
|
|
}
|
|
}
|
|
|
|
func test_givenRemoteRepository_whenUpdatesMultipleTimes_thenLatestImportWins() async throws {
|
|
let localProfiles = [
|
|
newProfile("local1"),
|
|
newProfile("local2")
|
|
]
|
|
let repository = InMemoryProfileRepository(profiles: localProfiles)
|
|
let remoteRepository = InMemoryProfileRepository()
|
|
let sut = ProfileManager(repository: repository, remoteRepositoryBlock: { _ in
|
|
remoteRepository
|
|
})
|
|
|
|
try await wait(sut, "Remote import", until: .stopRemoteImport) {
|
|
try await $0.observeLocal()
|
|
try await $0.observeRemote(true)
|
|
}
|
|
XCTAssertEqual(sut.previews.count, localProfiles.count)
|
|
|
|
let r1 = UUID()
|
|
let r2 = UUID()
|
|
let r3 = UUID()
|
|
let fp1 = UUID()
|
|
let fp2 = UUID()
|
|
let fp3 = UUID()
|
|
|
|
try await wait(sut, "Multiple imports", until: .stopRemoteImport) {
|
|
$0.previews.count == 5
|
|
} after: { _ in
|
|
remoteRepository.profiles = [
|
|
newProfile("remote1", id: r1)
|
|
]
|
|
remoteRepository.profiles = [
|
|
newProfile("remote1", id: r1),
|
|
newProfile("remote2", id: r2)
|
|
]
|
|
remoteRepository.profiles = [
|
|
newProfile("remote1", id: r1, fingerprint: fp1),
|
|
newProfile("remote2", id: r2, fingerprint: fp2),
|
|
newProfile("remote3", id: r3, fingerprint: fp3)
|
|
]
|
|
}
|
|
|
|
localProfiles.forEach {
|
|
XCTAssertFalse(sut.isRemotelyShared(profileWithId: $0.id))
|
|
}
|
|
remoteRepository.profiles.forEach {
|
|
XCTAssertTrue(sut.isRemotelyShared(profileWithId: $0.id))
|
|
switch $0.id {
|
|
case r1:
|
|
XCTAssertEqual($0.attributes.fingerprint, fp1)
|
|
case r2:
|
|
XCTAssertEqual($0.attributes.fingerprint, fp2)
|
|
case r3:
|
|
XCTAssertEqual($0.attributes.fingerprint, fp3)
|
|
default:
|
|
XCTFail("Unknown profile: \($0.id)")
|
|
}
|
|
}
|
|
}
|
|
|
|
func test_givenRemoteRepository_whenRemoteIsDeleted_thenLocalIsRetained() async throws {
|
|
let profile = newProfile()
|
|
let localProfiles = [profile]
|
|
let remoteProfiles = [profile]
|
|
let repository = InMemoryProfileRepository(profiles: localProfiles)
|
|
let remoteRepository = InMemoryProfileRepository(profiles: remoteProfiles)
|
|
let sut = ProfileManager(repository: repository, remoteRepositoryBlock: { _ in
|
|
remoteRepository
|
|
})
|
|
|
|
try await wait(sut, "Remote import", until: .stopRemoteImport) {
|
|
try await $0.observeLocal()
|
|
try await $0.observeRemote(true)
|
|
}
|
|
XCTAssertEqual(sut.previews.count, 1)
|
|
|
|
try await wait(sut, "Remote reset", until: .stopRemoteImport) { _ in
|
|
remoteRepository.profiles = []
|
|
}
|
|
XCTAssertEqual(sut.previews.count, 1)
|
|
XCTAssertEqual(sut.previews.first, ProfilePreview(profile))
|
|
}
|
|
|
|
func test_givenRemoteRepositoryAndMirroring_whenRemoteIsDeleted_thenLocalIsDeleted() async throws {
|
|
let profile = newProfile()
|
|
let localProfiles = [profile]
|
|
let repository = InMemoryProfileRepository(profiles: localProfiles)
|
|
let remoteRepository = InMemoryProfileRepository(profiles: localProfiles)
|
|
let sut = ProfileManager(repository: repository, remoteRepositoryBlock: { _ in
|
|
remoteRepository
|
|
}, mirrorsRemoteRepository: true)
|
|
|
|
try await wait(sut, "Remote import", until: .stopRemoteImport) {
|
|
try await $0.observeLocal()
|
|
try await $0.observeRemote(true)
|
|
}
|
|
XCTAssertEqual(sut.previews.count, 1)
|
|
|
|
try await wait(sut, "Remote reset", until: .stopRemoteImport) { _ in
|
|
remoteRepository.profiles = []
|
|
}
|
|
XCTAssertFalse(sut.hasProfiles)
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
private extension ProfileManagerTests {
|
|
func newProfile(_ name: String = "", id: UUID? = nil, fingerprint: UUID? = nil) -> Profile {
|
|
do {
|
|
var builder = Profile.Builder(id: id ?? UUID())
|
|
builder.name = name
|
|
if let fingerprint {
|
|
builder.attributes.fingerprint = fingerprint
|
|
}
|
|
return try builder.tryBuild()
|
|
} catch {
|
|
fatalError(error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
func waitForReady(_ sut: ProfileManager, importingRemote: Bool = true) async throws {
|
|
try await wait(sut, "Ready", until: .ready) {
|
|
try await $0.observeLocal()
|
|
try await $0.observeRemote(importingRemote)
|
|
}
|
|
}
|
|
|
|
func wait(
|
|
_ sut: ProfileManager,
|
|
_ description: String,
|
|
until event: ProfileManager.Event,
|
|
condition: @escaping (ProfileManager) -> Bool = { _ in true },
|
|
after action: (ProfileManager) async throws -> Void
|
|
) async throws {
|
|
let exp = expectation(description: description)
|
|
var wasMet = false
|
|
|
|
sut.didChange
|
|
.sink {
|
|
guard !wasMet else {
|
|
return
|
|
}
|
|
if $0 == event, condition(sut) {
|
|
wasMet = true
|
|
exp.fulfill()
|
|
}
|
|
}
|
|
.store(in: &subscriptions)
|
|
|
|
try await action(sut)
|
|
await fulfillment(of: [exp], timeout: CommonLibraryTests.timeout)
|
|
}
|
|
}
|