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

959 lines
45 KiB
Swift

import CodexBarCore
import Foundation
import Testing
@testable import CodexBar
@Suite(.serialized)
@MainActor
struct ManagedCodexAccountServiceTests {
@Test
func `upsert preserves uuid for matching canonical email`() async throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
defer { try? FileManager.default.removeItem(at: root) }
let fileURL = root.appendingPathComponent("managed.json", isDirectory: false)
let store = FileManagedCodexAccountStore(fileURL: fileURL, fileManager: .default)
let service = ManagedCodexAccountService(
store: store,
homeFactory: TestManagedCodexHomeFactory(root: root),
loginRunner: StubManagedCodexLoginRunner.success,
identityReader: StubManagedCodexIdentityReader.accounts([
.init(identity: .providerAccount(id: "account-live"), email: "user@example.com", plan: "Pro"),
.init(identity: .providerAccount(id: "account-live"), email: "user@example.com", plan: "Pro"),
]),
workspaceResolver: StubManagedCodexWorkspaceResolver())
let first = try await service.authenticateManagedAccount()
let second = try await service.authenticateManagedAccount()
let snapshot = try store.loadAccounts()
#expect(first.id == second.id)
#expect(second.email == "user@example.com")
#expect(second.providerAccountID == "account-live")
#expect(snapshot.accounts.count == 1)
#expect(second.managedHomePath.hasPrefix(root.standardizedFileURL.path + "/"))
}
@Test
func `new authentication appends managed account without implicit selection side effect`() async throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
defer { try? FileManager.default.removeItem(at: root) }
let firstID = try #require(UUID(uuidString: "11111111-1111-1111-1111-111111111111"))
let firstAccount = ManagedCodexAccount(
id: firstID,
email: "first@example.com",
managedHomePath: root.appendingPathComponent("accounts/first", isDirectory: true).path,
createdAt: 1,
updatedAt: 1,
lastAuthenticatedAt: 1)
let store = InMemoryManagedCodexAccountStore(
accounts: ManagedCodexAccountSet(
version: 1,
accounts: [firstAccount]))
let service = ManagedCodexAccountService(
store: store,
homeFactory: TestManagedCodexHomeFactory(root: root),
loginRunner: StubManagedCodexLoginRunner.success,
identityReader: StubManagedCodexIdentityReader.accounts([
.init(identity: .providerAccount(id: "account-second"), email: "second@example.com", plan: "Pro"),
]),
workspaceResolver: StubManagedCodexWorkspaceResolver())
let authenticated = try await service.authenticateManagedAccount()
#expect(store.snapshot.accounts.count == 2)
#expect(authenticated.email == "second@example.com")
#expect(authenticated.providerAccountID == "account-second")
}
@Test
func `same email provider backed workspaces coexist across sequential add account flows`() async throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
defer { try? FileManager.default.removeItem(at: root) }
let store = InMemoryManagedCodexAccountStore(
accounts: ManagedCodexAccountSet(
version: FileManagedCodexAccountStore.currentVersion,
accounts: []))
let service = ManagedCodexAccountService(
store: store,
homeFactory: TestManagedCodexHomeFactory(root: root),
loginRunner: StubManagedCodexLoginRunner.success,
identityReader: StubManagedCodexIdentityReader.accounts([
.init(identity: .providerAccount(id: "workspace-personal"), email: "alice@example.com", plan: "Pro"),
.init(identity: .providerAccount(id: "workspace-team"), email: "alice@example.com", plan: "Pro"),
]),
workspaceResolver: StubManagedCodexWorkspaceResolver(identities: [
"workspace-personal": CodexOpenAIWorkspaceIdentity(
workspaceAccountID: "workspace-personal",
workspaceLabel: "Personal"),
"workspace-team": CodexOpenAIWorkspaceIdentity(
workspaceAccountID: "workspace-team",
workspaceLabel: "Team"),
]))
let personal = try await service.authenticateManagedAccount()
let team = try await service.authenticateManagedAccount()
let storedPersonal = try #require(
store.snapshot.account(email: "alice@example.com", providerAccountID: "workspace-personal"))
let storedTeam = try #require(
store.snapshot.account(email: "alice@example.com", providerAccountID: "workspace-team"))
#expect(store.snapshot.accounts.count == 2)
#expect(personal.id == storedPersonal.id)
#expect(team.id == storedTeam.id)
#expect(personal.id != team.id)
#expect(storedPersonal.providerAccountID == "workspace-personal")
#expect(storedPersonal.workspaceLabel == "Personal")
#expect(storedTeam.providerAccountID == "workspace-team")
#expect(storedTeam.workspaceLabel == "Team")
#expect(storedPersonal.managedHomePath != storedTeam.managedHomePath)
#expect(FileManager.default.fileExists(atPath: storedPersonal.managedHomePath))
#expect(FileManager.default.fileExists(atPath: storedTeam.managedHomePath))
}
@Test
func `same workspace provider id with different emails does not overwrite existing account`() async throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
let existingHome = root.appendingPathComponent("accounts/existing", isDirectory: true)
try FileManager.default.createDirectory(at: existingHome, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: root) }
let existingID = try #require(UUID(uuidString: "10101010-2020-3030-4040-505050505050"))
let existingAccount = ManagedCodexAccount(
id: existingID,
email: "mi.chaelfmk5542@gmail.com",
providerAccountID: "team-4107",
workspaceLabel: "4107",
workspaceAccountID: "team-4107",
managedHomePath: existingHome.path,
createdAt: 1,
updatedAt: 1,
lastAuthenticatedAt: 1)
let store = InMemoryManagedCodexAccountStore(
accounts: ManagedCodexAccountSet(
version: FileManagedCodexAccountStore.currentVersion,
accounts: [existingAccount]))
let service = ManagedCodexAccountService(
store: store,
homeFactory: TestManagedCodexHomeFactory(root: root),
loginRunner: StubManagedCodexLoginRunner.success,
identityReader: StubManagedCodexIdentityReader.accounts([
.init(identity: .providerAccount(id: "team-4107"), email: "mich.aelfmk5542@gmail.com", plan: "Team"),
]),
workspaceResolver: StubManagedCodexWorkspaceResolver(identities: [
"team-4107": CodexOpenAIWorkspaceIdentity(
workspaceAccountID: "team-4107",
workspaceLabel: "4107"),
]))
let added = try await service.authenticateManagedAccount()
let original = try #require(
store.snapshot.account(email: "mi.chaelfmk5542@gmail.com", providerAccountID: "team-4107"))
let newAccount = try #require(
store.snapshot.account(email: "mich.aelfmk5542@gmail.com", providerAccountID: "team-4107"))
#expect(store.snapshot.accounts.count == 2)
#expect(original.id == existingID)
#expect(original.managedHomePath == existingHome.path)
#expect(newAccount.id == added.id)
#expect(newAccount.id != existingID)
#expect(newAccount.workspaceLabel == "4107")
#expect(FileManager.default.fileExists(atPath: existingHome.path))
#expect(FileManager.default.fileExists(atPath: newAccount.managedHomePath))
}
@Test
func `selected workspace is persisted and used as account identity`() async throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
defer { try? FileManager.default.removeItem(at: root) }
let store = InMemoryManagedCodexAccountStore(
accounts: ManagedCodexAccountSet(
version: FileManagedCodexAccountStore.currentVersion,
accounts: []))
let workspaces = [
CodexOpenAIWorkspaceIdentity(
workspaceAccountID: "workspace-personal",
workspaceLabel: "Personal"),
CodexOpenAIWorkspaceIdentity(
workspaceAccountID: "workspace-team",
workspaceLabel: "Team"),
]
let service = ManagedCodexAccountService(
store: store,
homeFactory: TestManagedCodexHomeFactory(root: root),
loginRunner: WritingManagedCodexLoginRunner(
credentials: CodexOAuthCredentials(
accessToken: "access-token",
refreshToken: "refresh-token",
idToken: nil,
accountId: "workspace-personal",
lastRefresh: nil)),
identityReader: StubManagedCodexIdentityReader.accounts([
.init(identity: .providerAccount(id: "workspace-personal"), email: "alice@example.com", plan: "Pro"),
]),
workspaceResolver: StubManagedCodexWorkspaceResolver(
identities: Dictionary(uniqueKeysWithValues: workspaces.map { ($0.workspaceAccountID, $0) }),
availableIdentities: workspaces),
workspaceSelector: StubManagedCodexWorkspaceSelector(selectedWorkspaceID: "workspace-team"))
let account = try await service.authenticateManagedAccount()
let credentials = try CodexOAuthCredentialsStore.load(env: ["CODEX_HOME": account.managedHomePath])
#expect(account.providerAccountID == "workspace-team")
#expect(account.workspaceLabel == "Team")
#expect(credentials.accountId == "workspace-team")
#expect(store.snapshot.accounts.count == 1)
}
@Test
func `reauth keeps previous home when store write fails`() async throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
let existingHome = root.appendingPathComponent("accounts/existing", isDirectory: true)
try FileManager.default.createDirectory(at: existingHome, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: root) }
let existingAccountID = try #require(UUID(uuidString: "11111111-2222-3333-4444-555555555555"))
let existingAccount = ManagedCodexAccount(
id: existingAccountID,
email: "user@example.com",
managedHomePath: existingHome.path,
createdAt: 1,
updatedAt: 1,
lastAuthenticatedAt: 1)
let store = FailingManagedCodexAccountStore(
accounts: ManagedCodexAccountSet(
version: 1,
accounts: [existingAccount]))
let service = ManagedCodexAccountService(
store: store,
homeFactory: TestManagedCodexHomeFactory(root: root),
loginRunner: StubManagedCodexLoginRunner.success,
identityReader: StubManagedCodexIdentityReader.accounts([
.init(identity: .providerAccount(id: "account-live"), email: "user@example.com", plan: "Pro"),
]),
workspaceResolver: StubManagedCodexWorkspaceResolver())
await #expect(throws: TestManagedCodexAccountStoreError.writeFailed) {
try await service.authenticateManagedAccount()
}
let newHome = root.appendingPathComponent("accounts/account-1", isDirectory: true)
#expect(FileManager.default.fileExists(atPath: existingHome.path))
#expect(FileManager.default.fileExists(atPath: newHome.path) == false)
#expect(store.snapshot.accounts.count == 1)
#expect(store.snapshot.accounts.first?.managedHomePath == existingHome.path)
}
@Test
func `reauth reconciles by provider account id before existing account id`() async throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
let alphaHome = root.appendingPathComponent("accounts/alpha", isDirectory: true)
let betaHome = root.appendingPathComponent("accounts/beta", isDirectory: true)
try FileManager.default.createDirectory(at: alphaHome, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: betaHome, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: root) }
let alphaID = try #require(UUID(uuidString: "AAAAAAAA-BBBB-CCCC-DDDD-111111111111"))
let betaID = try #require(UUID(uuidString: "BBBBBBBB-CCCC-DDDD-EEEE-222222222222"))
let alphaAccount = ManagedCodexAccount(
id: alphaID,
email: "shared@example.com",
providerAccountID: "account-alpha",
managedHomePath: alphaHome.path,
createdAt: 1,
updatedAt: 1,
lastAuthenticatedAt: 1)
let betaAccount = ManagedCodexAccount(
id: betaID,
email: "shared@example.com",
providerAccountID: "account-beta",
managedHomePath: betaHome.path,
createdAt: 2,
updatedAt: 2,
lastAuthenticatedAt: 2)
let store = InMemoryManagedCodexAccountStore(
accounts: ManagedCodexAccountSet(
version: 1,
accounts: [alphaAccount, betaAccount]))
let service = ManagedCodexAccountService(
store: store,
homeFactory: TestManagedCodexHomeFactory(root: root),
loginRunner: StubManagedCodexLoginRunner.success,
identityReader: StubManagedCodexIdentityReader.accounts([
.init(identity: .providerAccount(id: "account-beta"), email: "SHARED@example.com", plan: "Pro"),
]),
workspaceResolver: StubManagedCodexWorkspaceResolver())
let account = try await service.authenticateManagedAccount(existingAccountID: alphaAccount.id)
let storedAlpha = try #require(store.snapshot.account(id: alphaAccount.id))
let storedBeta = try #require(store.snapshot.account(id: betaAccount.id))
#expect(account.id == betaAccount.id)
#expect(store.snapshot.accounts.count == 2)
#expect(storedAlpha.email == "shared@example.com")
#expect(storedAlpha.providerAccountID == "account-alpha")
#expect(storedAlpha.managedHomePath == alphaHome.path)
#expect(storedBeta.email == "shared@example.com")
#expect(storedBeta.providerAccountID == "account-beta")
#expect(storedBeta.managedHomePath.hasPrefix(root.standardizedFileURL.path + "/"))
#expect(storedBeta.managedHomePath != betaHome.path)
#expect(FileManager.default.fileExists(atPath: alphaHome.path))
#expect(FileManager.default.fileExists(atPath: betaHome.path) == false)
#expect(FileManager.default.fileExists(atPath: storedBeta.managedHomePath))
}
@Test
func `reauth to different account does not overwrite existing account id match`() async throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
let aliceHome = root.appendingPathComponent("accounts/alice", isDirectory: true)
try FileManager.default.createDirectory(at: aliceHome, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: root) }
let aliceID = try #require(UUID(uuidString: "12121212-3434-5656-7878-909090909090"))
let aliceAccount = ManagedCodexAccount(
id: aliceID,
email: "alice@example.com",
providerAccountID: "account-alice",
managedHomePath: aliceHome.path,
createdAt: 1,
updatedAt: 1,
lastAuthenticatedAt: 1)
let store = InMemoryManagedCodexAccountStore(accounts: ManagedCodexAccountSet(
version: FileManagedCodexAccountStore.currentVersion,
accounts: [aliceAccount]))
let service = ManagedCodexAccountService(
store: store,
homeFactory: TestManagedCodexHomeFactory(root: root),
loginRunner: StubManagedCodexLoginRunner.success,
identityReader: StubManagedCodexIdentityReader.accounts([
.init(identity: .providerAccount(id: "account-bob"), email: "bob@example.com", plan: "Pro"),
]),
workspaceResolver: StubManagedCodexWorkspaceResolver())
let account = try await service.authenticateManagedAccount(existingAccountID: aliceID)
let storedAlice = try #require(store.snapshot.account(id: aliceID))
let storedBob = try #require(store.snapshot.account(email: "bob@example.com"))
#expect(account.id != aliceID)
#expect(account.id == storedBob.id)
#expect(store.snapshot.accounts.count == 2)
#expect(storedAlice.email == "alice@example.com")
#expect(storedAlice.providerAccountID == "account-alice")
#expect(storedAlice.managedHomePath == aliceHome.path)
#expect(storedBob.email == "bob@example.com")
#expect(storedBob.providerAccountID == "account-bob")
#expect(storedBob.managedHomePath.hasPrefix(root.standardizedFileURL.path + "/"))
#expect(storedBob.managedHomePath != aliceHome.path)
#expect(FileManager.default.fileExists(atPath: aliceHome.path))
#expect(FileManager.default.fileExists(atPath: storedBob.managedHomePath))
}
@Test
func `reauth on same email different workspace does not overwrite selected workspace`() async throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
let personalHome = root.appendingPathComponent("accounts/personal", isDirectory: true)
try FileManager.default.createDirectory(at: personalHome, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: root) }
let personalID = try #require(UUID(uuidString: "31313131-4242-5353-6464-757575757575"))
let personalAccount = ManagedCodexAccount(
id: personalID,
email: "alice@example.com",
providerAccountID: "workspace-personal",
workspaceLabel: "Personal",
workspaceAccountID: "workspace-personal",
managedHomePath: personalHome.path,
createdAt: 1,
updatedAt: 1,
lastAuthenticatedAt: 1)
let store = InMemoryManagedCodexAccountStore(accounts: ManagedCodexAccountSet(
version: FileManagedCodexAccountStore.currentVersion,
accounts: [personalAccount]))
let service = ManagedCodexAccountService(
store: store,
homeFactory: TestManagedCodexHomeFactory(root: root),
loginRunner: StubManagedCodexLoginRunner.success,
identityReader: StubManagedCodexIdentityReader.accounts([
.init(identity: .providerAccount(id: "workspace-team"), email: "alice@example.com", plan: "Pro"),
]),
workspaceResolver: StubManagedCodexWorkspaceResolver(identities: [
"workspace-team": CodexOpenAIWorkspaceIdentity(
workspaceAccountID: "workspace-team",
workspaceLabel: "Team"),
]))
let account = try await service.authenticateManagedAccount(existingAccountID: personalID)
let storedPersonal = try #require(store.snapshot.account(id: personalID))
let storedTeam = try #require(
store.snapshot.account(email: "alice@example.com", providerAccountID: "workspace-team"))
#expect(account.id == storedTeam.id)
#expect(account.id != personalID)
#expect(store.snapshot.accounts.count == 2)
#expect(storedPersonal.providerAccountID == "workspace-personal")
#expect(storedPersonal.workspaceLabel == "Personal")
#expect(storedPersonal.managedHomePath == personalHome.path)
#expect(storedTeam.providerAccountID == "workspace-team")
#expect(storedTeam.workspaceLabel == "Team")
#expect(storedTeam.managedHomePath != personalHome.path)
#expect(FileManager.default.fileExists(atPath: personalHome.path))
#expect(FileManager.default.fileExists(atPath: storedTeam.managedHomePath))
}
@Test
func `legacy row collapses onto provider backed row when provider id resolves elsewhere`() async throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
let legacyHome = root.appendingPathComponent("accounts/legacy", isDirectory: true)
let providerHome = root.appendingPathComponent("accounts/provider", isDirectory: true)
try FileManager.default.createDirectory(at: legacyHome, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: providerHome, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: root) }
let legacyID = try #require(UUID(uuidString: "AAAAAAAA-1111-2222-3333-444444444444"))
let providerID = try #require(UUID(uuidString: "BBBBBBBB-1111-2222-3333-444444444444"))
let legacyAccount = ManagedCodexAccount(
id: legacyID,
email: "shared@example.com",
managedHomePath: legacyHome.path,
createdAt: 1,
updatedAt: 1,
lastAuthenticatedAt: 1)
let providerAccount = ManagedCodexAccount(
id: providerID,
email: "shared@example.com",
providerAccountID: "account-real",
managedHomePath: providerHome.path,
createdAt: 2,
updatedAt: 2,
lastAuthenticatedAt: 2)
let store = InMemoryManagedCodexAccountStore(accounts: ManagedCodexAccountSet(
version: FileManagedCodexAccountStore.currentVersion,
accounts: [legacyAccount, providerAccount]))
let service = ManagedCodexAccountService(
store: store,
homeFactory: TestManagedCodexHomeFactory(root: root),
loginRunner: StubManagedCodexLoginRunner.success,
identityReader: StubManagedCodexIdentityReader.accounts([
.init(identity: .providerAccount(id: "account-real"), email: "shared@example.com", plan: "Pro"),
]),
workspaceResolver: StubManagedCodexWorkspaceResolver())
let account = try await service.authenticateManagedAccount(existingAccountID: legacyAccount.id)
#expect(account.id == providerAccount.id)
#expect(store.snapshot.accounts.count == 1)
#expect(store.snapshot.accounts.first?.id == providerAccount.id)
#expect(store.snapshot.accounts.first?.providerAccountID == "account-real")
#expect(FileManager.default.fileExists(atPath: legacyHome.path) == false)
#expect(FileManager.default.fileExists(atPath: providerHome.path) == false)
#expect(FileManager.default.fileExists(atPath: account.managedHomePath))
}
@Test
func `fresh provider login removes stale legacy row without explicit existing account id`() async throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
let legacyHome = root.appendingPathComponent("accounts/legacy", isDirectory: true)
let providerHome = root.appendingPathComponent("accounts/provider", isDirectory: true)
try FileManager.default.createDirectory(at: legacyHome, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: providerHome, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: root) }
let legacyID = try #require(UUID(uuidString: "CCCCCCCC-1111-2222-3333-444444444444"))
let providerID = try #require(UUID(uuidString: "DDDDDDDD-1111-2222-3333-444444444444"))
let legacyAccount = ManagedCodexAccount(
id: legacyID,
email: "shared@example.com",
managedHomePath: legacyHome.path,
createdAt: 1,
updatedAt: 1,
lastAuthenticatedAt: 1)
let providerAccount = ManagedCodexAccount(
id: providerID,
email: "shared@example.com",
providerAccountID: "account-real",
managedHomePath: providerHome.path,
createdAt: 2,
updatedAt: 2,
lastAuthenticatedAt: 2)
let store = InMemoryManagedCodexAccountStore(accounts: ManagedCodexAccountSet(
version: FileManagedCodexAccountStore.currentVersion,
accounts: [legacyAccount, providerAccount]))
let service = ManagedCodexAccountService(
store: store,
homeFactory: TestManagedCodexHomeFactory(root: root),
loginRunner: StubManagedCodexLoginRunner.success,
identityReader: StubManagedCodexIdentityReader.accounts([
.init(identity: .providerAccount(id: "account-real"), email: "shared@example.com", plan: "Pro"),
]),
workspaceResolver: StubManagedCodexWorkspaceResolver())
let account = try await service.authenticateManagedAccount()
#expect(account.id == providerAccount.id)
#expect(store.snapshot.accounts.count == 1)
#expect(store.snapshot.accounts.first?.id == providerAccount.id)
#expect(FileManager.default.fileExists(atPath: legacyHome.path) == false)
#expect(FileManager.default.fileExists(atPath: providerHome.path) == false)
#expect(FileManager.default.fileExists(atPath: account.managedHomePath))
}
@Test
func `authentication persists workspace metadata and tolerates missing workspace label`() async throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
defer { try? FileManager.default.removeItem(at: root) }
let store = InMemoryManagedCodexAccountStore(
accounts: ManagedCodexAccountSet(version: FileManagedCodexAccountStore.currentVersion, accounts: []))
let service = ManagedCodexAccountService(
store: store,
homeFactory: TestManagedCodexHomeFactory(root: root),
loginRunner: StubManagedCodexLoginRunner.success,
identityReader: StubManagedCodexIdentityReader.accounts([
.init(identity: .providerAccount(id: "account-live"), email: "user@example.com", plan: "Pro"),
.init(identity: .providerAccount(id: "account-fallback"), email: "fallback@example.com", plan: "Pro"),
]),
workspaceResolver: StubManagedCodexWorkspaceResolver(identities: [
"account-live": CodexOpenAIWorkspaceIdentity(
workspaceAccountID: "account-live",
workspaceLabel: "Team Alpha"),
"account-fallback": CodexOpenAIWorkspaceIdentity(
workspaceAccountID: "account-fallback",
workspaceLabel: nil),
]))
let labeled = try await service.authenticateManagedAccount()
let fallback = try await service.authenticateManagedAccount()
#expect(labeled.providerAccountID == "account-live")
#expect(labeled.workspaceAccountID == "account-live")
#expect(labeled.workspaceLabel == "Team Alpha")
#expect(fallback.providerAccountID == "account-fallback")
#expect(fallback.workspaceAccountID == "account-fallback")
#expect(fallback.workspaceLabel == nil)
}
@Test
func `reauth preserves stored provider metadata when refresh cannot resolve account id`() async throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
let existingHome = root.appendingPathComponent("accounts/existing", isDirectory: true)
try FileManager.default.createDirectory(at: existingHome, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: root) }
let existingID = try #require(UUID(uuidString: "EEEEEEEE-1111-2222-3333-444444444444"))
let existingAccount = ManagedCodexAccount(
id: existingID,
email: "user@example.com",
providerAccountID: "account-live",
workspaceLabel: "Team Alpha",
workspaceAccountID: "account-live",
managedHomePath: existingHome.path,
createdAt: 1,
updatedAt: 1,
lastAuthenticatedAt: 1)
let store = InMemoryManagedCodexAccountStore(accounts: ManagedCodexAccountSet(
version: FileManagedCodexAccountStore.currentVersion,
accounts: [existingAccount]))
let service = ManagedCodexAccountService(
store: store,
homeFactory: TestManagedCodexHomeFactory(root: root),
loginRunner: StubManagedCodexLoginRunner.success,
identityReader: StubManagedCodexIdentityReader.accounts([
.init(
identity: .emailOnly(normalizedEmail: "user@example.com"),
email: "user@example.com",
plan: "Pro"),
]),
workspaceResolver: StubManagedCodexWorkspaceResolver())
let account = try await service.authenticateManagedAccount(existingAccountID: existingID)
let stored = try #require(store.snapshot.account(id: existingID))
#expect(account.id == existingID)
#expect(account.providerAccountID == "account-live")
#expect(account.workspaceAccountID == "account-live")
#expect(account.workspaceLabel == "Team Alpha")
#expect(stored.providerAccountID == "account-live")
#expect(stored.workspaceAccountID == "account-live")
#expect(stored.workspaceLabel == "Team Alpha")
#expect(FileManager.default.fileExists(atPath: existingHome.path) == false)
#expect(FileManager.default.fileExists(atPath: account.managedHomePath))
}
@Test
func `auth failure cleanup uses managed root safety check`() async throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
let outsideHome = FileManager.default.temporaryDirectory.appendingPathComponent(
UUID().uuidString,
isDirectory: true)
defer {
try? FileManager.default.removeItem(at: root)
try? FileManager.default.removeItem(at: outsideHome)
}
let store = InMemoryManagedCodexAccountStore(
accounts: ManagedCodexAccountSet(version: 1, accounts: []))
let service = ManagedCodexAccountService(
store: store,
homeFactory: UnsafeManagedCodexHomeFactory(root: root, homeURL: outsideHome),
loginRunner: StubManagedCodexLoginRunner(
result: CodexLoginRunner.Result(outcome: .failed(status: 1), output: "nope")),
identityReader: StubManagedCodexIdentityReader.emails([]),
workspaceResolver: StubManagedCodexWorkspaceResolver())
await #expect(throws: ManagedCodexAccountServiceError.self) {
try await service.authenticateManagedAccount()
}
#expect(FileManager.default.fileExists(atPath: outsideHome.path))
#expect(store.snapshot.accounts.isEmpty)
}
@Test
func `auth failure preserves codex login result output`() async throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
defer { try? FileManager.default.removeItem(at: root) }
let loginResult = CodexLoginRunner.Result(
outcome: .failed(status: 42),
output: "OAuth callback used the wrong browser profile")
let service = ManagedCodexAccountService(
store: InMemoryManagedCodexAccountStore(
accounts: ManagedCodexAccountSet(version: 1, accounts: [])),
homeFactory: TestManagedCodexHomeFactory(root: root),
loginRunner: StubManagedCodexLoginRunner(result: loginResult),
identityReader: StubManagedCodexIdentityReader.emails([]),
workspaceResolver: StubManagedCodexWorkspaceResolver())
do {
_ = try await service.authenticateManagedAccount()
Issue.record("Expected managed Codex login failure")
} catch let error as ManagedCodexAccountServiceError {
guard case let .loginFailed(capturedResult) = error else {
Issue.record("Expected loginFailed, got \(error)")
return
}
#expect(capturedResult == loginResult)
#expect(error.userFacingMessage.contains("codex --version"))
#expect(error.userFacingMessage.contains("OAuth callback used the wrong browser profile"))
}
}
@Test
func `remove deletes managed home under managed root`() async throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
let home = root.appendingPathComponent("accounts/account-a", isDirectory: true)
try FileManager.default.createDirectory(at: home, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: root) }
let accountID = try #require(UUID(uuidString: "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE"))
let account = ManagedCodexAccount(
id: accountID,
email: "user@example.com",
managedHomePath: home.path,
createdAt: 1,
updatedAt: 1,
lastAuthenticatedAt: 1)
let store = InMemoryManagedCodexAccountStore(
accounts: ManagedCodexAccountSet(version: 1, accounts: [account]))
let service = ManagedCodexAccountService(
store: store,
homeFactory: TestManagedCodexHomeFactory(root: root),
loginRunner: StubManagedCodexLoginRunner.success,
identityReader: StubManagedCodexIdentityReader.emails([]),
workspaceResolver: StubManagedCodexWorkspaceResolver())
try await service.removeManagedAccount(id: account.id)
#expect(store.snapshot.accounts.isEmpty)
#expect(FileManager.default.fileExists(atPath: home.path) == false)
}
@Test
func `remove keeps remaining managed account records`() async throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
let firstHome = root.appendingPathComponent("accounts/account-a", isDirectory: true)
let secondHome = root.appendingPathComponent("accounts/account-b", isDirectory: true)
try FileManager.default.createDirectory(at: firstHome, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: secondHome, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: root) }
let firstID = try #require(UUID(uuidString: "AAAAAAAA-1111-1111-1111-111111111111"))
let secondID = try #require(UUID(uuidString: "BBBBBBBB-2222-2222-2222-222222222222"))
let first = ManagedCodexAccount(
id: firstID,
email: "first@example.com",
managedHomePath: firstHome.path,
createdAt: 1,
updatedAt: 1,
lastAuthenticatedAt: 1)
let second = ManagedCodexAccount(
id: secondID,
email: "second@example.com",
managedHomePath: secondHome.path,
createdAt: 2,
updatedAt: 2,
lastAuthenticatedAt: 2)
let store = InMemoryManagedCodexAccountStore(
accounts: ManagedCodexAccountSet(
version: 1,
accounts: [first, second]))
let service = ManagedCodexAccountService(
store: store,
homeFactory: TestManagedCodexHomeFactory(root: root),
loginRunner: StubManagedCodexLoginRunner.success,
identityReader: StubManagedCodexIdentityReader.emails([]),
workspaceResolver: StubManagedCodexWorkspaceResolver())
try await service.removeManagedAccount(id: second.id)
#expect(store.snapshot.accounts.count == 1)
#expect(store.snapshot.accounts.first?.id == first.id)
#expect(FileManager.default.fileExists(atPath: secondHome.path) == false)
}
@Test
func `remove keeps persisted account when store write fails`() async throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
let home = root.appendingPathComponent("accounts/account-a", isDirectory: true)
try FileManager.default.createDirectory(at: home, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: root) }
let accountID = try #require(UUID(uuidString: "CCCCCCCC-DDDD-EEEE-FFFF-000000000000"))
let account = ManagedCodexAccount(
id: accountID,
email: "user@example.com",
managedHomePath: home.path,
createdAt: 1,
updatedAt: 1,
lastAuthenticatedAt: 1)
let store = FailingManagedCodexAccountStore(
accounts: ManagedCodexAccountSet(version: 1, accounts: [account]))
let service = ManagedCodexAccountService(
store: store,
homeFactory: TestManagedCodexHomeFactory(root: root),
loginRunner: StubManagedCodexLoginRunner.success,
identityReader: StubManagedCodexIdentityReader.emails([]),
workspaceResolver: StubManagedCodexWorkspaceResolver())
await #expect(throws: TestManagedCodexAccountStoreError.writeFailed) {
try await service.removeManagedAccount(id: account.id)
}
#expect(store.snapshot.accounts.count == 1)
#expect(store.snapshot.accounts.first?.managedHomePath == home.path)
#expect(FileManager.default.fileExists(atPath: home.path))
}
@Test
func `remove drops account but leaves unsafe home untouched`() async throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
let outsideRoot = FileManager.default.temporaryDirectory.appendingPathComponent(
UUID().uuidString,
isDirectory: true)
try FileManager.default.createDirectory(at: outsideRoot, withIntermediateDirectories: true)
defer {
try? FileManager.default.removeItem(at: root)
try? FileManager.default.removeItem(at: outsideRoot)
}
let accountID = try #require(UUID(uuidString: "BBBBBBBB-CCCC-DDDD-EEEE-FFFFFFFFFFFF"))
let account = ManagedCodexAccount(
id: accountID,
email: "user@example.com",
managedHomePath: outsideRoot.path,
createdAt: 1,
updatedAt: 1,
lastAuthenticatedAt: 1)
let store = InMemoryManagedCodexAccountStore(
accounts: ManagedCodexAccountSet(version: 1, accounts: [account]))
let service = ManagedCodexAccountService(
store: store,
homeFactory: TestManagedCodexHomeFactory(root: root),
loginRunner: StubManagedCodexLoginRunner.success,
identityReader: StubManagedCodexIdentityReader.emails([]),
workspaceResolver: StubManagedCodexWorkspaceResolver())
try await service.removeManagedAccount(id: account.id)
#expect(store.snapshot.accounts.isEmpty)
#expect(FileManager.default.fileExists(atPath: outsideRoot.path))
}
}
private final class InMemoryManagedCodexAccountStore: 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 FailingManagedCodexAccountStore: ManagedCodexAccountStoring, @unchecked Sendable {
var snapshot: ManagedCodexAccountSet
init(accounts: ManagedCodexAccountSet) {
self.snapshot = accounts
}
func loadAccounts() throws -> ManagedCodexAccountSet {
self.snapshot
}
func storeAccounts(_ accounts: ManagedCodexAccountSet) throws {
_ = accounts
throw TestManagedCodexAccountStoreError.writeFailed
}
func ensureFileExists() throws -> URL {
FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
}
}
private final class TestManagedCodexHomeFactory: ManagedCodexHomeProducing, @unchecked Sendable {
let root: URL
private let lock = NSLock()
private var index: Int = 0
init(root: URL) {
self.root = root
}
private func nextPathComponent() -> String {
self.lock.lock()
defer { self.lock.unlock() }
self.index += 1
return "accounts/account-\(self.index)"
}
func makeHomeURL() -> URL {
self.root.appendingPathComponent(self.nextPathComponent(), isDirectory: true)
}
func validateManagedHomeForDeletion(_ url: URL) throws {
try ManagedCodexHomeFactory(root: self.root).validateManagedHomeForDeletion(url)
}
}
private struct UnsafeManagedCodexHomeFactory: ManagedCodexHomeProducing {
let root: URL
let homeURL: URL
func makeHomeURL() -> URL {
self.homeURL
}
func validateManagedHomeForDeletion(_ url: URL) throws {
try ManagedCodexHomeFactory(root: self.root).validateManagedHomeForDeletion(url)
}
}
private struct StubManagedCodexLoginRunner: ManagedCodexLoginRunning {
let result: CodexLoginRunner.Result
func run(homePath: String, timeout: TimeInterval) async -> CodexLoginRunner.Result {
self.result
}
static let success = StubManagedCodexLoginRunner(
result: CodexLoginRunner.Result(outcome: .success, output: "ok"))
}
private struct WritingManagedCodexLoginRunner: ManagedCodexLoginRunning {
let credentials: CodexOAuthCredentials
func run(homePath: String, timeout _: TimeInterval) async -> CodexLoginRunner.Result {
do {
try CodexOAuthCredentialsStore.save(self.credentials, env: ["CODEX_HOME": homePath])
return CodexLoginRunner.Result(outcome: .success, output: "ok")
} catch {
return CodexLoginRunner.Result(outcome: .failed(status: 1), output: String(describing: error))
}
}
}
private enum TestManagedCodexAccountStoreError: Error, Equatable {
case writeFailed
}
private final class StubManagedCodexIdentityReader: ManagedCodexIdentityReading, @unchecked Sendable {
private let lock = NSLock()
private var identities: [CodexAuthBackedAccount]
init(identities: [CodexAuthBackedAccount]) {
self.identities = identities
}
func loadAccountIdentity(homePath _: String) throws -> CodexAuthBackedAccount {
self.lock.lock()
defer { self.lock.unlock() }
guard !self.identities.isEmpty else {
return CodexAuthBackedAccount(identity: .unresolved, email: nil, plan: nil)
}
return self.identities.removeFirst()
}
static func emails(_ emails: [String]) -> StubManagedCodexIdentityReader {
StubManagedCodexIdentityReader(identities: emails.map { email in
CodexAuthBackedAccount(
identity: CodexIdentityResolver.resolve(accountId: nil, email: email),
email: email,
plan: "Pro")
})
}
static func accounts(_ accounts: [CodexAuthBackedAccount]) -> StubManagedCodexIdentityReader {
StubManagedCodexIdentityReader(identities: accounts)
}
}
private struct StubManagedCodexWorkspaceResolver: ManagedCodexWorkspaceResolving {
let identities: [String: CodexOpenAIWorkspaceIdentity]
let availableIdentities: [CodexOpenAIWorkspaceIdentity]
init(
identities: [String: CodexOpenAIWorkspaceIdentity] = [:],
availableIdentities: [CodexOpenAIWorkspaceIdentity] = [])
{
self.identities = identities
self.availableIdentities = availableIdentities
}
func resolveWorkspaceIdentity(
homePath _: String,
providerAccountID: String) async -> CodexOpenAIWorkspaceIdentity?
{
self.identities[providerAccountID]
}
func availableWorkspaceIdentities(homePath _: String) async -> [CodexOpenAIWorkspaceIdentity] {
self.availableIdentities
}
}
private struct StubManagedCodexWorkspaceSelector: ManagedCodexWorkspaceSelecting {
let selectedWorkspaceID: String?
func selectWorkspace(
email _: String,
currentWorkspaceID _: String?,
workspaces: [CodexOpenAIWorkspaceIdentity]) async -> CodexOpenAIWorkspaceIdentity?
{
workspaces.first { $0.workspaceAccountID == self.selectedWorkspaceID }
}
}