Files
Termind/FrameworkTests/CloudSyncCoordinatorLifecycleTests.swift
T

105 lines
5.1 KiB
Swift

import CloudKit
import Foundation
import Observation
import SwiftData
import TermindApplication
import TermindDomain
import TermindPersistence
import Testing
import os
@testable import TermindInfrastructure
@Suite @MainActor
struct CloudSyncCoordinatorLifecycleTests {
@Test
func categoryAdaptersKeepIndependentBindingsEnginesAndEnvironmentMarkers() async throws {
let container = try TermindPersistence.makeContainer(inMemory: true)
let context = container.mainContext
let defaults = UserDefaults(suiteName: "CloudSyncCoordinatorLifecycleTests.\(UUID())")!
let settingsStore = SettingsSyncPersistence(context: context, defaults: defaults)
let metricsStore = MetricsSyncPersistence(context: context, defaults: defaults)
try settingsStore.prepare()
try metricsStore.prepare()
let secrets = AISecretStore(context: context, keychain: InMemorySecretStore(), defaults: defaults)
let networks = (0..<3).map { _ in LifecycleNetwork() }
let vault = VaultSyncCoordinator(
context: context, defaults: defaults, environmentIdentifier: "Test",
makeTransport: { _, _ in networks[0].transport })
let settings = SettingsSyncCoordinator(
persistence: settingsStore, secrets: secrets, defaults: defaults, environmentIdentifier: "Test",
makeTransport: { _, _ in networks[1].transport })
let metrics = MetricsSyncCoordinator(
persistence: metricsStore, defaults: defaults, environmentIdentifier: "Test",
makeTransport: { _, _ in networks[2].transport })
let drivers = [vault.lifecycle, settings.lifecycle, metrics.lifecycle]
for driver in drivers { driver.start() }
for driver in drivers { await driver.operation?.value }
#expect(Set(drivers.map(\.zoneID)).count == 3)
#expect(try VaultSyncJournal(context: context).state().boundUserRecordName == "current-user")
#expect(try settingsStore.journal.state().boundUserRecordName == "current-user")
#expect(try metricsStore.journal.state().boundUserRecordName == "current-user")
for key in [
SettingsKey.vaultSyncEnvironment, SettingsKey.settingsSyncEnvironment, SettingsKey.metricsSyncEnvironment,
] {
#expect(defaults.string(forKey: key) == "Test")
}
#expect(secrets.uploadsAuthorized)
vault.pause()
#expect(vault.status == .paused)
#expect(settings.lifecycle.accepts(networks[1].nextEngine))
#expect(metrics.lifecycle.accepts(networks[2].nextEngine))
#expect(secrets.uploadsAuthorized)
let settingsState = try settingsStore.journal.state()
settingsState.pendingUserRecordName = "replacement-user"
try context.save()
let statusChanged = OSAllocatedUnfairLock(initialState: false)
withObservationTracking {
_ = settings.status
} onChange: {
statusChanged.withLock { $0 = true }
}
settings.pause()
#expect(statusChanged.withLock { $0 })
#expect(settings.status == .paused)
#expect(!secrets.uploadsAuthorized)
settings.confirmAccountSwitchMerge()
await settings.lifecycle.operation?.value
// The verified account is still current-user, so the new binding requires fresh consent.
#expect(settingsState.boundUserRecordName == "replacement-user")
#expect(settingsState.pendingUserRecordName == "current-user")
#expect(settings.status == .accountChangeConfirmationRequired)
#expect(try metricsStore.journal.state().boundUserRecordName == "current-user")
#expect(!secrets.uploadsAuthorized)
metrics.pause()
}
@Test
func settingsSecretAuthorizationFollowsSuccessfulFetchAndAccountRetirement() async throws {
let container = try TermindPersistence.makeContainer(inMemory: true)
let defaults = UserDefaults(suiteName: "SettingsLifecycleTests.\(UUID())")!
let store = SettingsSyncPersistence(context: container.mainContext, defaults: defaults)
try store.prepare()
let secrets = AISecretStore(
context: container.mainContext, keychain: InMemorySecretStore(), defaults: defaults)
let network = LifecycleNetwork()
let coordinator = SettingsSyncCoordinator(
persistence: store, secrets: secrets, defaults: defaults, environmentIdentifier: "Test",
makeTransport: { _, _ in network.transport })
network.nextEngine.fetchBody = { throw CKError(.networkFailure) }
coordinator.start()
await coordinator.lifecycle.operation?.value
#expect(!secrets.uploadsAuthorized)
#expect(network.nextEngine.sendCount == 0)
network.nextEngine.fetchBody = {}
coordinator.retry()
await coordinator.lifecycle.operation?.value
#expect(secrets.uploadsAuthorized)
#expect(network.nextEngine.sendCount == 1)
try coordinator.lifecycle.handleAccountChange(.signOut(previousUser: .init(recordName: "current-user")))
#expect(!secrets.uploadsAuthorized)
#expect(!coordinator.lifecycle.accepts(network.nextEngine))
coordinator.pause()
}
}