311 lines
14 KiB
Swift
311 lines
14 KiB
Swift
import CodexBarCore
|
|
import Foundation
|
|
import Testing
|
|
@testable import CodexBar
|
|
|
|
@Suite(.serialized)
|
|
@MainActor
|
|
struct CodexAccountPromotionExecutionTests {
|
|
@Test
|
|
func `executor import store failure cleans up imported home and maps managed store error`() async throws {
|
|
let container = try CodexAccountPromotionTestContainer(
|
|
suiteName: "CodexAccountPromotionExecutionTests-import-cleanup")
|
|
defer { container.tearDown() }
|
|
|
|
let target = try container.createManagedAccount(
|
|
persistedEmail: "beta@example.com",
|
|
authAccountID: "acct-beta")
|
|
try container.persistAccounts([target])
|
|
_ = try container.writeLiveOAuthAuthFile(email: "alpha@example.com", accountID: "acct-alpha")
|
|
let context = try await self.makeContext(container: container, targetID: target.id)
|
|
let executor = CodexDisplacedLivePreservationExecutor(
|
|
store: RecordingManagedCodexAccountStore(base: container.fileStore) { _ in
|
|
throw PromotionTestError.storeWriteFailed
|
|
},
|
|
homeFactory: container.homeFactory,
|
|
fileManager: .default)
|
|
|
|
#expect(throws: CodexAccountPromotionError.managedStoreCommitFailed) {
|
|
try executor.execute(plan: .importNew(reason: .noExistingManagedDestination), context: context)
|
|
}
|
|
|
|
#expect(try container.managedHomeURLs().count == 1)
|
|
#expect(try container.loadAccounts().accounts.count == 1)
|
|
}
|
|
|
|
@Test
|
|
func `executor refresh failure leaves live auth untouched and keeps copied managed auth`() async throws {
|
|
let container = try CodexAccountPromotionTestContainer(
|
|
suiteName: "CodexAccountPromotionExecutionTests-refresh-failure")
|
|
defer { container.tearDown() }
|
|
|
|
let target = try container.createManagedAccount(
|
|
persistedEmail: "beta@example.com",
|
|
authAccountID: "acct-beta")
|
|
let existingManagedLive = try container.createManagedAccount(
|
|
persistedEmail: "alpha@example.com",
|
|
authAccountID: "acct-alpha")
|
|
try container.persistAccounts([target, existingManagedLive])
|
|
let originalManagedAuthData = try container.managedAuthData(for: existingManagedLive)
|
|
let liveAuthData = try container.writeLiveOAuthAuthFile(
|
|
email: "alpha@example.com",
|
|
accountID: "acct-alpha",
|
|
apiKey: "sk-refreshed-live")
|
|
let originalLiveAuthData = try #require(try container.liveAuthData())
|
|
let context = try await self.makeContext(container: container, targetID: target.id)
|
|
let plan = CodexDisplacedLivePreservationPlanner().makePlan(context: context)
|
|
let executor = CodexDisplacedLivePreservationExecutor(
|
|
store: RecordingManagedCodexAccountStore(base: container.fileStore) { accounts in
|
|
if accounts.account(id: existingManagedLive.id)?
|
|
.lastAuthenticatedAt != existingManagedLive.lastAuthenticatedAt
|
|
{
|
|
throw PromotionTestError.storeWriteFailed
|
|
}
|
|
},
|
|
homeFactory: container.homeFactory,
|
|
fileManager: .default)
|
|
|
|
#expect(throws: CodexAccountPromotionError.managedStoreCommitFailed) {
|
|
try executor.execute(plan: plan, context: context)
|
|
}
|
|
|
|
let accounts = try container.loadAccounts().accounts
|
|
let persistedManagedLive = try #require(accounts.first(where: { $0.id == existingManagedLive.id }))
|
|
#expect(try container.liveAuthData() == originalLiveAuthData)
|
|
#expect(try container.managedAuthData(for: persistedManagedLive) != originalManagedAuthData)
|
|
#expect(try container.managedAuthData(for: persistedManagedLive) == liveAuthData)
|
|
}
|
|
|
|
@Test
|
|
func `executor import verifies persisted account after concurrent duplicate collision`() async throws {
|
|
let container = try CodexAccountPromotionTestContainer(
|
|
suiteName: "CodexAccountPromotionExecutionTests-import-collision-repair")
|
|
defer { container.tearDown() }
|
|
|
|
let target = try container.createManagedAccount(
|
|
persistedEmail: "beta@example.com",
|
|
authAccountID: "acct-beta")
|
|
let concurrentID = UUID()
|
|
let concurrentHomeURL = container.managedHomesURL.appendingPathComponent(
|
|
concurrentID.uuidString,
|
|
isDirectory: true)
|
|
try FileManager.default.createDirectory(at: concurrentHomeURL, withIntermediateDirectories: true)
|
|
let concurrentManaged = ManagedCodexAccount(
|
|
id: concurrentID,
|
|
email: "alpha@example.com",
|
|
providerAccountID: "acct-alpha",
|
|
workspaceLabel: "Personal",
|
|
workspaceAccountID: "acct-alpha",
|
|
managedHomePath: concurrentHomeURL.path,
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
lastAuthenticatedAt: 1)
|
|
try container.persistAccounts([target])
|
|
let liveAuthData = try container.writeLiveOAuthAuthFile(email: "alpha@example.com", accountID: "acct-alpha")
|
|
let context = try await self.makeContext(container: container, targetID: target.id)
|
|
let executor = CodexDisplacedLivePreservationExecutor(
|
|
store: ConcurrentDuplicateManagedCodexAccountStore(
|
|
base: container.fileStore,
|
|
concurrentAccount: concurrentManaged),
|
|
homeFactory: container.homeFactory,
|
|
fileManager: .default)
|
|
|
|
let result = try executor.execute(plan: .importNew(reason: .noExistingManagedDestination), context: context)
|
|
|
|
#expect(result.displacedLiveDisposition == .alreadyManaged(managedAccountID: concurrentManaged.id))
|
|
let accounts = try container.loadAccounts().accounts
|
|
let repaired = try #require(accounts.first(where: { $0.id == concurrentManaged.id }))
|
|
#expect(repaired.managedHomePath != concurrentHomeURL.path)
|
|
#expect(try container.managedAuthData(for: repaired) == liveAuthData)
|
|
}
|
|
|
|
@Test
|
|
func `executor refresh filesystem failure maps to managed store error`() async throws {
|
|
let container = try CodexAccountPromotionTestContainer(
|
|
suiteName: "CodexAccountPromotionExecutionTests-refresh-filesystem-failure")
|
|
defer { container.tearDown() }
|
|
|
|
let target = try container.createManagedAccount(
|
|
persistedEmail: "beta@example.com",
|
|
authAccountID: "acct-beta")
|
|
let existingManagedLive = try container.createManagedAccount(
|
|
persistedEmail: "alpha@example.com",
|
|
authAccountID: "acct-alpha")
|
|
try container.persistAccounts([target, existingManagedLive])
|
|
_ = try container.writeLiveOAuthAuthFile(email: "alpha@example.com", accountID: "acct-alpha")
|
|
let context = try await self.makeContext(container: container, targetID: target.id)
|
|
let plan = CodexDisplacedLivePreservationPlanner().makePlan(context: context)
|
|
|
|
let managedHomeURL = URL(fileURLWithPath: existingManagedLive.managedHomePath, isDirectory: true)
|
|
try FileManager.default.removeItem(at: managedHomeURL)
|
|
try Data("blocked".utf8).write(to: managedHomeURL)
|
|
|
|
let executor = CodexDisplacedLivePreservationExecutor(
|
|
store: container.fileStore,
|
|
homeFactory: container.homeFactory,
|
|
fileManager: .default)
|
|
|
|
#expect(throws: CodexAccountPromotionError.managedStoreCommitFailed) {
|
|
try executor.execute(plan: plan, context: context)
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `executor legacy import repair ignores provider backed rows with the same email`() async throws {
|
|
let container = try CodexAccountPromotionTestContainer(
|
|
suiteName: "CodexAccountPromotionExecutionTests-legacy-import-provider-same-email")
|
|
defer { container.tearDown() }
|
|
|
|
let target = try container.createManagedAccount(
|
|
persistedEmail: "beta@example.com",
|
|
authAccountID: "acct-beta")
|
|
let providerManaged = try container.createManagedAccount(
|
|
persistedEmail: "alpha@example.com",
|
|
authAccountID: "acct-existing")
|
|
try container.persistAccounts([target, providerManaged])
|
|
_ = try container.writeLiveOAuthAuthFile(email: "alpha@example.com")
|
|
let context = try await self.makeContext(container: container, targetID: target.id)
|
|
let originalProviderManaged = try #require(try container.loadAccounts().account(id: providerManaged.id))
|
|
|
|
let executor = CodexDisplacedLivePreservationExecutor(
|
|
store: DroppingLegacyImportedAccountStore(
|
|
base: container.fileStore,
|
|
preservedProviderBackedAccount: originalProviderManaged),
|
|
homeFactory: container.homeFactory,
|
|
fileManager: .default)
|
|
|
|
#expect(throws: CodexAccountPromotionError.managedStoreCommitFailed) {
|
|
try executor.execute(plan: .importNew(reason: .noExistingManagedDestination), context: context)
|
|
}
|
|
|
|
let persistedProviderManaged = try #require(try container.loadAccounts().account(id: providerManaged.id))
|
|
#expect(persistedProviderManaged.providerAccountID == originalProviderManaged.providerAccountID)
|
|
#expect(persistedProviderManaged.managedHomePath == originalProviderManaged.managedHomePath)
|
|
}
|
|
|
|
@Test
|
|
func `executor reject preserves stable error mapping`() async throws {
|
|
let container = try CodexAccountPromotionTestContainer(
|
|
suiteName: "CodexAccountPromotionExecutionTests-reject-mapping")
|
|
defer { container.tearDown() }
|
|
|
|
let target = try container.createManagedAccount(
|
|
persistedEmail: "beta@example.com",
|
|
authAccountID: "acct-beta")
|
|
try container.persistAccounts([target])
|
|
let context = try await self.makeContext(container: container, targetID: target.id)
|
|
let executor = CodexDisplacedLivePreservationExecutor(
|
|
store: container.fileStore,
|
|
homeFactory: container.homeFactory,
|
|
fileManager: .default)
|
|
|
|
#expect(throws: CodexAccountPromotionError.liveAccountAPIKeyOnlyUnsupported) {
|
|
try executor.execute(plan: .reject(reason: .liveAPIKeyOnlyUnsupported), context: context)
|
|
}
|
|
#expect(throws: CodexAccountPromotionError.liveAccountUnreadable) {
|
|
try executor.execute(plan: .reject(reason: .liveUnreadable), context: context)
|
|
}
|
|
#expect(throws: CodexAccountPromotionError.liveAccountMissingIdentityForPreservation) {
|
|
try executor.execute(plan: .reject(reason: .liveIdentityMissingForPreservation), context: context)
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `executor rejects target as preservation destination`() async throws {
|
|
let container = try CodexAccountPromotionTestContainer(
|
|
suiteName: "CodexAccountPromotionExecutionTests-target-destination")
|
|
defer { container.tearDown() }
|
|
|
|
let target = try container.createManagedAccount(
|
|
persistedEmail: "beta@example.com",
|
|
authAccountID: "acct-beta")
|
|
try container.persistAccounts([target])
|
|
_ = try container.writeLiveOAuthAuthFile(email: "alpha@example.com", accountID: "acct-alpha")
|
|
let context = try await self.makeContext(container: container, targetID: target.id)
|
|
let executor = CodexDisplacedLivePreservationExecutor(
|
|
store: container.fileStore,
|
|
homeFactory: container.homeFactory,
|
|
fileManager: .default)
|
|
|
|
#expect(throws: CodexAccountPromotionError.managedStoreCommitFailed) {
|
|
try executor.execute(
|
|
plan: .refreshExisting(
|
|
destination: context.target,
|
|
reason: .readableHomeIdentityMatch),
|
|
context: context)
|
|
}
|
|
}
|
|
|
|
private func makeContext(
|
|
container: CodexAccountPromotionTestContainer,
|
|
targetID: UUID)
|
|
async throws -> PreparedPromotionContext
|
|
{
|
|
let builder = PreparedPromotionContextBuilder(
|
|
store: container.fileStore,
|
|
workspaceResolver: container.workspaceResolver,
|
|
snapshotLoader: SettingsStoreCodexAccountReconciliationSnapshotLoader(settingsStore: container.settings),
|
|
authMaterialReader: DefaultCodexAuthMaterialReader(),
|
|
baseEnvironment: container.baseEnvironment,
|
|
fileManager: .default)
|
|
return try await builder.build(targetID: targetID)
|
|
}
|
|
}
|
|
|
|
private final class ConcurrentDuplicateManagedCodexAccountStore: ManagedCodexAccountStoring, @unchecked Sendable {
|
|
let base: any ManagedCodexAccountStoring
|
|
let concurrentAccount: ManagedCodexAccount
|
|
private var didInjectConcurrentAccount = false
|
|
|
|
init(base: any ManagedCodexAccountStoring, concurrentAccount: ManagedCodexAccount) {
|
|
self.base = base
|
|
self.concurrentAccount = concurrentAccount
|
|
}
|
|
|
|
func loadAccounts() throws -> ManagedCodexAccountSet {
|
|
if self.didInjectConcurrentAccount == false {
|
|
self.didInjectConcurrentAccount = true
|
|
let current = try self.base.loadAccounts()
|
|
try self.base.storeAccounts(ManagedCodexAccountSet(
|
|
version: current.version,
|
|
accounts: current.accounts + [self.concurrentAccount]))
|
|
}
|
|
return try self.base.loadAccounts()
|
|
}
|
|
|
|
func storeAccounts(_ accounts: ManagedCodexAccountSet) throws {
|
|
try self.base.storeAccounts(accounts)
|
|
}
|
|
|
|
func ensureFileExists() throws -> URL {
|
|
try self.base.ensureFileExists()
|
|
}
|
|
}
|
|
|
|
private final class DroppingLegacyImportedAccountStore: ManagedCodexAccountStoring, @unchecked Sendable {
|
|
let base: any ManagedCodexAccountStoring
|
|
let preservedProviderBackedAccount: ManagedCodexAccount
|
|
|
|
init(base: any ManagedCodexAccountStoring, preservedProviderBackedAccount: ManagedCodexAccount) {
|
|
self.base = base
|
|
self.preservedProviderBackedAccount = preservedProviderBackedAccount
|
|
}
|
|
|
|
func loadAccounts() throws -> ManagedCodexAccountSet {
|
|
try self.base.loadAccounts()
|
|
}
|
|
|
|
func storeAccounts(_ accounts: ManagedCodexAccountSet) throws {
|
|
let filteredAccounts = accounts.accounts.filter {
|
|
$0.id == self.preservedProviderBackedAccount.id || $0.providerAccountID != nil
|
|
}
|
|
try self.base.storeAccounts(ManagedCodexAccountSet(
|
|
version: accounts.version,
|
|
accounts: filteredAccounts))
|
|
}
|
|
|
|
func ensureFileExists() throws -> URL {
|
|
try self.base.ensureFileExists()
|
|
}
|
|
}
|