2022-06-25 20:11:45 +00:00
|
|
|
//
|
2024-09-23 13:02:26 +00:00
|
|
|
// ProfileManagerTests.swift
|
2022-06-25 20:11:45 +00:00
|
|
|
// Passepartout
|
|
|
|
//
|
2024-09-23 13:02:26 +00:00
|
|
|
// Created by Davide De Rosa on 9/12/24.
|
2024-01-14 13:34:21 +00:00
|
|
|
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
|
2022-06-25 20:11:45 +00:00
|
|
|
//
|
|
|
|
// 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/>.
|
|
|
|
//
|
|
|
|
|
2024-11-21 15:03:57 +00:00
|
|
|
import Combine
|
2024-11-03 12:16:13 +00:00
|
|
|
@testable import CommonLibrary
|
2023-04-04 07:50:45 +00:00
|
|
|
import Foundation
|
2024-11-21 15:03:57 +00:00
|
|
|
import PassepartoutKit
|
2024-09-23 13:02:26 +00:00
|
|
|
import XCTest
|
2022-06-25 20:11:45 +00:00
|
|
|
|
2024-11-21 15:03:57 +00:00
|
|
|
@MainActor
|
2024-09-23 13:02:26 +00:00
|
|
|
final class ProfileManagerTests: XCTestCase {
|
2024-11-21 15:03:57 +00:00
|
|
|
private let timeout = 1.0
|
|
|
|
|
|
|
|
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)
|
2024-11-22 11:52:51 +00:00
|
|
|
XCTAssertTrue(sut.previews.isEmpty)
|
2024-11-21 15:03:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func test_givenRepository_whenNotReady_thenHasNoProfiles() {
|
|
|
|
let repository = InMemoryProfileRepository(profiles: [])
|
|
|
|
let sut = ProfileManager(repository: repository, remoteRepositoryBlock: nil)
|
|
|
|
XCTAssertFalse(sut.isReady)
|
|
|
|
XCTAssertFalse(sut.hasProfiles)
|
2024-11-22 11:52:51 +00:00
|
|
|
XCTAssertTrue(sut.previews.isEmpty)
|
2024-11-21 15:03:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
2024-11-22 11:52:51 +00:00
|
|
|
XCTAssertEqual(sut.previews.count, 1)
|
2024-11-21 15:03:57 +00:00
|
|
|
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)
|
2024-11-22 11:52:51 +00:00
|
|
|
XCTAssertEqual(sut.previews.count, 2)
|
2024-11-21 15:03:57 +00:00
|
|
|
|
2024-11-23 18:26:33 +00:00
|
|
|
try await wait(sut, "Search") {
|
2024-11-21 15:03:57 +00:00
|
|
|
$0.search(byName: "ar")
|
|
|
|
} until: {
|
2024-11-22 11:52:51 +00:00
|
|
|
$0.previews.count == 1
|
2024-11-21 15:03:57 +00:00
|
|
|
}
|
|
|
|
XCTAssertTrue(sut.isSearching)
|
2024-11-22 11:52:51 +00:00
|
|
|
let found = try XCTUnwrap(sut.previews.last)
|
2024-11-21 15:03:57 +00:00
|
|
|
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.willRebuildCount, 0)
|
|
|
|
XCTAssertEqual(processor.verifyCount, 1)
|
|
|
|
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)
|
|
|
|
|
2024-11-22 11:52:51 +00:00
|
|
|
XCTAssertEqual(sut.previews.count, 1)
|
|
|
|
XCTAssertEqual(sut.previews.first?.name, "local2")
|
2024-11-21 15:03:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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()
|
2024-11-23 18:26:33 +00:00
|
|
|
try await wait(sut, "Save") {
|
2024-11-21 15:03:57 +00:00
|
|
|
try await $0.save(profile)
|
|
|
|
} until: {
|
|
|
|
$0.hasProfiles
|
|
|
|
}
|
2024-11-22 11:52:51 +00:00
|
|
|
XCTAssertEqual(sut.previews.count, 1)
|
2024-11-21 15:03:57 +00:00
|
|
|
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)
|
2024-11-22 11:52:51 +00:00
|
|
|
XCTAssertEqual(sut.previews.first?.id, profile.id)
|
2024-11-21 15:03:57 +00:00
|
|
|
|
|
|
|
var builder = profile.builder()
|
|
|
|
builder.name = "newName"
|
|
|
|
let renamedProfile = try builder.tryBuild()
|
|
|
|
|
2024-11-23 18:26:33 +00:00
|
|
|
try await wait(sut, "Save") {
|
2024-11-21 15:03:57 +00:00
|
|
|
try await $0.save(renamedProfile)
|
|
|
|
} until: {
|
2024-11-22 11:52:51 +00:00
|
|
|
$0.previews.first?.name == renamedProfile.name
|
2024-11-21 15:03:57 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
2024-11-21 23:46:58 +00:00
|
|
|
try await sut.save(profile, isLocal: false)
|
2024-11-21 15:03:57 +00:00
|
|
|
XCTAssertEqual(processor.willRebuildCount, 0)
|
|
|
|
}
|
|
|
|
|
2024-11-21 23:46:58 +00:00
|
|
|
func test_givenRepositoryAndProcessor_whenSaveLocal_thenProcessorIsInvoked() async throws {
|
2024-11-21 15:03:57 +00:00
|
|
|
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()
|
2024-11-21 23:46:58 +00:00
|
|
|
try await sut.save(profile, isLocal: true)
|
2024-11-21 15:03:57 +00:00
|
|
|
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: 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)
|
|
|
|
|
2024-11-23 18:26:33 +00:00
|
|
|
try await wait(sut, "Remove") {
|
2024-11-21 15:03:57 +00:00
|
|
|
await $0.remove(withId: profile.id)
|
|
|
|
} until: {
|
|
|
|
!$0.hasProfiles
|
|
|
|
}
|
2024-11-22 11:52:51 +00:00
|
|
|
XCTAssertTrue(sut.previews.isEmpty)
|
2024-11-21 15:03:57 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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: 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: 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)
|
2024-11-22 11:52:51 +00:00
|
|
|
XCTAssertEqual(sut.previews.count, 1)
|
2024-11-21 15:03:57 +00:00
|
|
|
|
|
|
|
let newProfile = sut.new(withName: profile.name)
|
|
|
|
XCTAssertEqual(newProfile.name, "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)
|
|
|
|
|
2024-11-23 18:26:33 +00:00
|
|
|
try await wait(sut, "Duplicate 1") {
|
2024-11-21 15:03:57 +00:00
|
|
|
try await $0.duplicate(profileWithId: profile.id)
|
|
|
|
} until: {
|
2024-11-22 11:52:51 +00:00
|
|
|
$0.previews.count == 2
|
2024-11-21 15:03:57 +00:00
|
|
|
}
|
|
|
|
|
2024-11-23 18:26:33 +00:00
|
|
|
try await wait(sut, "Duplicate 2") {
|
2024-11-21 15:03:57 +00:00
|
|
|
try await $0.duplicate(profileWithId: profile.id)
|
|
|
|
} until: {
|
2024-11-22 11:52:51 +00:00
|
|
|
$0.previews.count == 3
|
2024-11-21 15:03:57 +00:00
|
|
|
}
|
|
|
|
|
2024-11-23 18:26:33 +00:00
|
|
|
try await wait(sut, "Duplicate 3") {
|
2024-11-21 15:03:57 +00:00
|
|
|
try await $0.duplicate(profileWithId: profile.id)
|
|
|
|
} until: {
|
2024-11-22 11:52:51 +00:00
|
|
|
$0.previews.count == 4
|
2024-11-21 15:03:57 +00:00
|
|
|
}
|
|
|
|
|
2024-11-22 11:52:51 +00:00
|
|
|
XCTAssertEqual(sut.previews.map(\.name), [
|
2024-11-21 15:03:57 +00:00
|
|
|
"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
|
|
|
|
})
|
|
|
|
|
2024-11-23 18:26:33 +00:00
|
|
|
try await wait(sut, "Remote import") {
|
2024-11-21 15:03:57 +00:00
|
|
|
try await $0.observeLocal()
|
|
|
|
try await $0.observeRemote(true)
|
|
|
|
} until: {
|
2024-11-22 11:52:51 +00:00
|
|
|
$0.previews.count == allProfiles.count
|
2024-11-21 15:03:57 +00:00
|
|
|
}
|
|
|
|
|
2024-11-22 11:52:51 +00:00
|
|
|
XCTAssertEqual(Set(sut.previews), Set(allProfiles.map { ProfilePreview($0) }))
|
2024-11-21 15:03:57 +00:00
|
|
|
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
|
|
|
|
})
|
|
|
|
|
2024-11-23 18:26:33 +00:00
|
|
|
try await wait(sut, "Remote import") {
|
2024-11-21 15:03:57 +00:00
|
|
|
try await $0.observeLocal()
|
|
|
|
try await $0.observeRemote(true)
|
|
|
|
} until: {
|
2024-11-22 11:52:51 +00:00
|
|
|
$0.previews.count == 4 // unique IDs
|
2024-11-21 15:03:57 +00:00
|
|
|
}
|
|
|
|
|
2024-11-22 11:52:51 +00:00
|
|
|
sut.previews.forEach {
|
2024-11-21 15:03:57 +00:00
|
|
|
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:
|
2024-11-21 22:01:08 +00:00
|
|
|
XCTFail("Unknown profile: \($0.id)")
|
2024-11-21 15:03:57 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
var didImport = false
|
|
|
|
observeRemoteImport(sut) {
|
|
|
|
didImport = true
|
|
|
|
}
|
2024-11-23 18:26:33 +00:00
|
|
|
try await wait(sut, "Remote import") {
|
|
|
|
try await $0.observeLocal()
|
|
|
|
try await $0.observeRemote(true)
|
2024-11-21 15:03:57 +00:00
|
|
|
} until: { _ in
|
|
|
|
didImport
|
|
|
|
}
|
|
|
|
|
|
|
|
XCTAssertEqual(processor.isIncludedCount, allProfiles.count)
|
2024-11-22 11:52:51 +00:00
|
|
|
XCTAssertEqual(Set(sut.previews), Set(localProfiles.map { ProfilePreview($0) }))
|
2024-11-21 15:03:57 +00:00
|
|
|
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)
|
|
|
|
|
|
|
|
var didImport = false
|
|
|
|
observeRemoteImport(sut) {
|
|
|
|
didImport = true
|
|
|
|
}
|
2024-11-23 18:26:33 +00:00
|
|
|
try await wait(sut, "Remote import") {
|
|
|
|
try await $0.observeLocal()
|
|
|
|
try await $0.observeRemote(true)
|
2024-11-21 15:03:57 +00:00
|
|
|
} until: { _ in
|
|
|
|
didImport
|
|
|
|
}
|
|
|
|
|
2024-11-22 11:52:51 +00:00
|
|
|
try sut.previews.forEach {
|
2024-11-21 15:03:57 +00:00
|
|
|
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:
|
2024-11-21 22:01:08 +00:00
|
|
|
XCTFail("Unknown profile: \($0.id)")
|
2024-11-21 15:03:57 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
})
|
|
|
|
|
2024-11-23 18:26:33 +00:00
|
|
|
try await wait(sut, "Remote import") {
|
2024-11-21 15:03:57 +00:00
|
|
|
try await $0.observeLocal()
|
|
|
|
try await $0.observeRemote(true)
|
|
|
|
} until: {
|
2024-11-22 11:52:51 +00:00
|
|
|
$0.previews.count == localProfiles.count
|
2024-11-21 15:03:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
let r1 = UUID()
|
|
|
|
let r2 = UUID()
|
|
|
|
let r3 = UUID()
|
|
|
|
let fp1 = UUID()
|
|
|
|
let fp2 = UUID()
|
|
|
|
let fp3 = UUID()
|
|
|
|
|
2024-11-23 18:26:33 +00:00
|
|
|
try await wait(sut, "Multiple imports") { _ in
|
2024-11-21 15:03:57 +00:00
|
|
|
remoteRepository.profiles = [
|
|
|
|
newProfile("remote1", id: r1)
|
|
|
|
]
|
|
|
|
remoteRepository.profiles = [
|
|
|
|
newProfile("remote1", id: r1),
|
2024-11-23 18:26:33 +00:00
|
|
|
newProfile("remote2", id: r2)
|
2024-11-21 15:03:57 +00:00
|
|
|
]
|
|
|
|
remoteRepository.profiles = [
|
|
|
|
newProfile("remote1", id: r1, fingerprint: fp1),
|
|
|
|
newProfile("remote2", id: r2, fingerprint: fp2),
|
|
|
|
newProfile("remote3", id: r3, fingerprint: fp3)
|
|
|
|
]
|
|
|
|
} until: {
|
2024-11-22 11:52:51 +00:00
|
|
|
$0.previews.count == 5
|
2024-11-21 15:03:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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:
|
2024-11-21 22:01:08 +00:00
|
|
|
XCTFail("Unknown profile: \($0.id)")
|
2024-11-21 15:03:57 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
})
|
|
|
|
|
|
|
|
var didImport = false
|
|
|
|
observeRemoteImport(sut) {
|
|
|
|
didImport = true
|
|
|
|
}
|
2024-11-23 18:26:33 +00:00
|
|
|
try await wait(sut, "Remote import") {
|
2024-11-21 15:03:57 +00:00
|
|
|
try await $0.observeLocal()
|
|
|
|
try await $0.observeRemote(true)
|
|
|
|
} until: {
|
2024-11-22 11:52:51 +00:00
|
|
|
$0.previews.count == 1
|
2024-11-21 15:03:57 +00:00
|
|
|
}
|
2024-11-23 18:26:33 +00:00
|
|
|
try await wait(sut, "Remote reset") { _ in
|
2024-11-21 15:03:57 +00:00
|
|
|
remoteRepository.profiles = []
|
|
|
|
} until: { _ in
|
|
|
|
didImport
|
|
|
|
}
|
|
|
|
|
2024-11-22 11:52:51 +00:00
|
|
|
XCTAssertEqual(sut.previews.count, 1)
|
|
|
|
XCTAssertEqual(sut.previews.first, ProfilePreview(profile))
|
2024-11-21 15:03:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func test_givenRemoteRepositoryAndMirroring_whenRemoteIsDeleted_thenLocalIsDeleted() async throws {
|
|
|
|
let profile = newProfile()
|
|
|
|
let localProfiles = [profile]
|
|
|
|
let repository = InMemoryProfileRepository(profiles: localProfiles)
|
|
|
|
let remoteRepository = InMemoryProfileRepository()
|
|
|
|
let sut = ProfileManager(repository: repository, remoteRepositoryBlock: { _ in
|
|
|
|
remoteRepository
|
|
|
|
}, mirrorsRemoteRepository: true)
|
|
|
|
|
|
|
|
var didImport = false
|
|
|
|
observeRemoteImport(sut) {
|
|
|
|
didImport = true
|
|
|
|
}
|
2024-11-23 18:26:33 +00:00
|
|
|
try await wait(sut, "Remote import") {
|
2024-11-21 15:03:57 +00:00
|
|
|
try await $0.observeLocal()
|
|
|
|
try await $0.observeRemote(true)
|
|
|
|
} until: {
|
2024-11-22 11:52:51 +00:00
|
|
|
$0.previews.count == 1
|
2024-11-21 15:03:57 +00:00
|
|
|
}
|
2024-11-23 18:26:33 +00:00
|
|
|
try await wait(sut, "Remote reset") { _ in
|
2024-11-21 15:03:57 +00:00
|
|
|
remoteRepository.profiles = []
|
|
|
|
} until: { _ in
|
|
|
|
didImport
|
|
|
|
}
|
|
|
|
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 {
|
2024-11-23 18:26:33 +00:00
|
|
|
try await wait(sut, "Ready") {
|
2024-11-21 15:03:57 +00:00
|
|
|
try await $0.observeLocal()
|
|
|
|
try await $0.observeRemote(importingRemote)
|
|
|
|
} until: {
|
|
|
|
$0.isReady
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func observeRemoteImport(_ sut: ProfileManager, block: @escaping () -> Void) {
|
|
|
|
sut
|
|
|
|
.didChange
|
|
|
|
.sink {
|
|
|
|
if case .stopRemoteImport = $0 {
|
|
|
|
block()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.store(in: &subscriptions)
|
|
|
|
}
|
|
|
|
|
|
|
|
func wait(
|
|
|
|
_ sut: ProfileManager,
|
2024-11-23 18:26:33 +00:00
|
|
|
_ description: String,
|
2024-11-21 15:03:57 +00:00
|
|
|
after action: (ProfileManager) async throws -> Void,
|
|
|
|
until condition: @escaping (ProfileManager) -> Bool
|
|
|
|
) async throws {
|
2024-11-23 18:26:33 +00:00
|
|
|
let exp = expectation(description: description)
|
2024-11-21 15:03:57 +00:00
|
|
|
var wasMet = false
|
|
|
|
sut.objectWillChange
|
|
|
|
.sink {
|
|
|
|
guard !wasMet else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if condition(sut) {
|
|
|
|
wasMet = true
|
|
|
|
exp.fulfill()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.store(in: &subscriptions)
|
|
|
|
|
|
|
|
try await action(sut)
|
|
|
|
await fulfillment(of: [exp], timeout: timeout)
|
2022-06-25 20:11:45 +00:00
|
|
|
}
|
|
|
|
}
|