Files
steipete--codexbar/Tests/CodexBarTests/ManagedCodexAccountCoordinatorTests.swift
2026-07-13 12:22:33 +08:00

158 lines
6.0 KiB
Swift

import CodexBarCore
import Foundation
import Testing
@testable import CodexBar
@Suite(.serialized)
@MainActor
struct ManagedCodexAccountCoordinatorTests {
@Test
func `coordinator exposes in flight state and rejects overlapping managed authentication`() async throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
defer { try? FileManager.default.removeItem(at: root) }
let existingAccountID = try #require(UUID(uuidString: "AAAAAAAA-BBBB-CCCC-DDDD-111111111111"))
let runner = BlockingManagedCodexLoginRunner()
let service = ManagedCodexAccountService(
store: InMemoryManagedCodexAccountStoreForCoordinatorTests(
accounts: ManagedCodexAccountSet(version: 1, accounts: [])),
homeFactory: CoordinatorTestManagedCodexHomeFactory(root: root),
loginRunner: runner,
identityReader: CoordinatorStubManagedCodexIdentityReader(email: "user@example.com"))
let coordinator = ManagedCodexAccountCoordinator(service: service)
let authTask = Task { try await coordinator.authenticateManagedAccount(existingAccountID: existingAccountID) }
await runner.waitUntilStarted()
#expect(coordinator.isAuthenticatingManagedAccount)
#expect(coordinator.authenticatingManagedAccountID == existingAccountID)
await #expect(throws: ManagedCodexAccountCoordinatorError.authenticationInProgress) {
try await coordinator.authenticateManagedAccount()
}
await runner.resume()
let account = try await authTask.value
#expect(account.email == "user@example.com")
#expect(coordinator.isAuthenticatingManagedAccount == false)
#expect(coordinator.authenticatingManagedAccountID == nil)
}
@Test
func `coordinator clears in flight state after managed login timeout`() async throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
defer { try? FileManager.default.removeItem(at: root) }
let loginResult = CodexLoginRunner.Result(outcome: .timedOut, output: "timed out")
let existingAccountID = try #require(UUID(uuidString: "AAAAAAAA-BBBB-CCCC-DDDD-222222222222"))
let service = ManagedCodexAccountService(
store: InMemoryManagedCodexAccountStoreForCoordinatorTests(
accounts: ManagedCodexAccountSet(version: 1, accounts: [])),
homeFactory: CoordinatorTestManagedCodexHomeFactory(root: root),
loginRunner: TimedOutManagedCodexLoginRunner(result: loginResult),
identityReader: CoordinatorStubManagedCodexIdentityReader(email: "user@example.com"))
let coordinator = ManagedCodexAccountCoordinator(service: service)
do {
_ = try await coordinator.authenticateManagedAccount(existingAccountID: existingAccountID, timeout: 0.2)
Issue.record("Expected managed login timeout to throw")
} catch let error as ManagedCodexAccountServiceError {
#expect(error == .loginFailed(loginResult))
} catch {
Issue.record("Expected ManagedCodexAccountServiceError.loginFailed, got \(error)")
}
#expect(coordinator.isAuthenticatingManagedAccount == false)
#expect(coordinator.authenticatingManagedAccountID == nil)
}
}
private actor BlockingManagedCodexLoginRunner: ManagedCodexLoginRunning {
private var waiters: [CheckedContinuation<CodexLoginRunner.Result, Never>] = []
private var startedWaiters: [CheckedContinuation<Void, Never>] = []
private var didStart = false
func run(homePath _: String, timeout _: TimeInterval) async -> CodexLoginRunner.Result {
self.didStart = true
self.startedWaiters.forEach { $0.resume() }
self.startedWaiters.removeAll()
return await withCheckedContinuation { continuation in
self.waiters.append(continuation)
}
}
func waitUntilStarted() async {
if self.didStart { return }
await withCheckedContinuation { continuation in
self.startedWaiters.append(continuation)
}
}
func resume() {
let result = CodexLoginRunner.Result(outcome: .success, output: "ok")
self.waiters.forEach { $0.resume(returning: result) }
self.waiters.removeAll()
}
}
private struct TimedOutManagedCodexLoginRunner: ManagedCodexLoginRunning {
let result: CodexLoginRunner.Result
func run(homePath _: String, timeout _: TimeInterval) async -> CodexLoginRunner.Result {
self.result
}
}
private final class InMemoryManagedCodexAccountStoreForCoordinatorTests: ManagedCodexAccountStoring,
@unchecked Sendable {
var snapshot: ManagedCodexAccountSet
init(accounts: ManagedCodexAccountSet) {
self.snapshot = accounts
}
func loadAccounts() throws -> ManagedCodexAccountSet {
self.snapshot
}
func storeAccounts(_ accounts: ManagedCodexAccountSet) throws {
self.snapshot = accounts
}
func ensureFileExists() throws -> URL {
FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
}
}
private final class CoordinatorTestManagedCodexHomeFactory: ManagedCodexHomeProducing, @unchecked Sendable {
let root: URL
init(root: URL) {
self.root = root
}
func makeHomeURL() -> URL {
self.root.appendingPathComponent(UUID().uuidString, isDirectory: true)
}
func validateManagedHomeForDeletion(_ url: URL) throws {
try ManagedCodexHomeFactory(root: self.root).validateManagedHomeForDeletion(url)
}
}
private final class CoordinatorStubManagedCodexIdentityReader: ManagedCodexIdentityReading, @unchecked Sendable {
let email: String
init(email: String) {
self.email = email
}
func loadAccountIdentity(homePath _: String) throws -> CodexAuthBackedAccount {
CodexAuthBackedAccount(
identity: CodexIdentityResolver.resolve(accountId: nil, email: self.email),
email: self.email,
plan: "Pro")
}
}