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

543 lines
19 KiB
Swift

import CodexBarCore
import Foundation
import Testing
@testable import CodexBar
@Test
func `FileManagedCodexAccountStore round trip`() throws {
let tempDir = FileManager.default.temporaryDirectory
let fileURL = tempDir.appendingPathComponent("codexbar-managed-codex-accounts-test.json")
defer { try? FileManager.default.removeItem(at: fileURL) }
let firstID = UUID()
let secondID = UUID()
let firstAccount = ManagedCodexAccount(
id: firstID,
email: " FIRST@Example.COM ",
providerAccountID: "account-first",
managedHomePath: "/tmp/managed-home-1",
createdAt: 1000,
updatedAt: 2000,
lastAuthenticatedAt: 3000)
let secondAccount = ManagedCodexAccount(
id: secondID,
email: "second@example.com",
providerAccountID: "account-second",
managedHomePath: "/tmp/managed-home-2",
createdAt: 4000,
updatedAt: 5000,
lastAuthenticatedAt: nil)
let payload = ManagedCodexAccountSet(
version: FileManagedCodexAccountStore.currentVersion,
accounts: [firstAccount, secondAccount])
let store = FileManagedCodexAccountStore(fileURL: fileURL)
try store.storeAccounts(payload)
let contents = try String(contentsOf: fileURL, encoding: .utf8)
let loaded = try store.loadAccounts()
let accountsRange = try #require(contents.range(of: "\"accounts\""))
let versionRange = try #require(contents.range(of: "\"version\""))
#expect(loaded.version == FileManagedCodexAccountStore.currentVersion)
#expect(loaded.accounts.count == 2)
#expect(loaded.accounts[0].email == "first@example.com")
#expect(loaded.accounts[0].providerAccountID == "account-first")
#expect(loaded.account(id: firstID)?.managedHomePath == "/tmp/managed-home-1")
#expect(loaded.account(email: "SECOND@example.com", providerAccountID: "account-second")?.id == secondID)
#expect(contents.contains("\n \"accounts\""))
#expect(accountsRange.lowerBound < versionRange.lowerBound)
#expect(contents.contains("\"activeAccountID\"") == false)
}
@Test
func `FileManagedCodexAccountStore missing file loads empty set`() throws {
let tempDir = FileManager.default.temporaryDirectory
let fileURL = tempDir.appendingPathComponent("codexbar-managed-codex-accounts-nil-active-test.json")
defer { try? FileManager.default.removeItem(at: fileURL) }
try? FileManager.default.removeItem(at: fileURL)
let store = FileManagedCodexAccountStore(fileURL: fileURL)
let initial = try store.loadAccounts()
#expect(initial.version == FileManagedCodexAccountStore.currentVersion)
#expect(initial.accounts.isEmpty)
let account = ManagedCodexAccount(
id: UUID(),
email: "user@example.com",
managedHomePath: "/tmp/managed-home",
createdAt: 10,
updatedAt: 20,
lastAuthenticatedAt: nil)
let payload = ManagedCodexAccountSet(
version: 1,
accounts: [account])
try store.storeAccounts(payload)
let loaded = try store.loadAccounts()
#expect(loaded.version == FileManagedCodexAccountStore.currentVersion)
#expect(loaded.accounts.count == 1)
#expect(loaded.account(email: "USER@example.com")?.id == account.id)
}
@Test
func `FileManagedCodexAccountStore canonicalizes decoded emails`() throws {
let tempDir = FileManager.default.temporaryDirectory
let fileURL = tempDir.appendingPathComponent("codexbar-managed-codex-accounts-decode-test.json")
defer { try? FileManager.default.removeItem(at: fileURL) }
let accountID = UUID()
let json = """
{
"accounts" : [
{
"createdAt" : 10,
"email" : " MIXED@Example.COM ",
"id" : "\(accountID.uuidString)",
"lastAuthenticatedAt" : null,
"managedHomePath" : "/tmp/managed-home",
"updatedAt" : 20
}
],
"version" : 1
}
"""
try json.write(to: fileURL, atomically: true, encoding: .utf8)
let store = FileManagedCodexAccountStore(fileURL: fileURL)
let loaded = try store.loadAccounts()
#expect(loaded.version == FileManagedCodexAccountStore.currentVersion)
#expect(loaded.accounts.first?.email == "mixed@example.com")
#expect(loaded.account(email: "mixed@example.com")?.id == accountID)
}
@Test
func `FileManagedCodexAccountStore drops duplicate canonical emails on load`() throws {
let tempDir = FileManager.default.temporaryDirectory
let fileURL = tempDir.appendingPathComponent("codexbar-managed-codex-accounts-duplicate-email-test.json")
defer { try? FileManager.default.removeItem(at: fileURL) }
let firstID = UUID()
let secondID = UUID()
let json = """
{
"accounts" : [
{
"createdAt" : 10,
"email" : " First@Example.com ",
"id" : "\(firstID.uuidString)",
"lastAuthenticatedAt" : null,
"managedHomePath" : "/tmp/managed-home-1",
"updatedAt" : 20
},
{
"createdAt" : 30,
"email" : "first@example.com",
"id" : "\(secondID.uuidString)",
"lastAuthenticatedAt" : null,
"managedHomePath" : "/tmp/managed-home-2",
"updatedAt" : 40
}
],
"version" : 1
}
"""
try json.write(to: fileURL, atomically: true, encoding: .utf8)
let store = FileManagedCodexAccountStore(fileURL: fileURL)
let loaded = try store.loadAccounts()
#expect(loaded.accounts.count == 1)
#expect(loaded.accounts.first?.id == firstID)
#expect(loaded.accounts.first?.managedHomePath == "/tmp/managed-home-1")
}
@Test
func `FileManagedCodexAccountStore keeps same email rows when hydrated provider account I Ds differ`() 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 firstHome = root.appendingPathComponent("first-home", isDirectory: true)
let secondHome = root.appendingPathComponent("second-home", isDirectory: true)
try writeCodexAuthFile(homeURL: firstHome, email: "user@example.com", accountId: "account-alpha")
try writeCodexAuthFile(homeURL: secondHome, email: "user@example.com", accountId: "account-beta")
let firstID = UUID()
let secondID = UUID()
let json = """
{
"accounts" : [
{
"createdAt" : 10,
"email" : " user@example.com ",
"id" : "\(firstID.uuidString)",
"lastAuthenticatedAt" : null,
"managedHomePath" : "\(firstHome.path)",
"updatedAt" : 20
},
{
"createdAt" : 30,
"email" : "USER@example.com",
"id" : "\(secondID.uuidString)",
"lastAuthenticatedAt" : null,
"managedHomePath" : "\(secondHome.path)",
"updatedAt" : 40
}
],
"version" : 1
}
"""
try json.write(to: fileURL, atomically: true, encoding: .utf8)
let store = FileManagedCodexAccountStore(fileURL: fileURL)
let loaded = try store.loadAccounts()
#expect(loaded.version == FileManagedCodexAccountStore.currentVersion)
#expect(loaded.accounts.count == 2)
#expect(loaded.account(email: "user@example.com", providerAccountID: "account-alpha")?.id == firstID)
#expect(loaded.account(email: "user@example.com", providerAccountID: "account-beta")?.id == secondID)
}
@Test
func `managed account set keeps same provider account I D when emails differ`() {
let firstID = UUID()
let secondID = UUID()
let first = ManagedCodexAccount(
id: firstID,
email: "mi.chaelfmk5542@gmail.com",
providerAccountID: "team-4107",
managedHomePath: "/tmp/managed-home-1",
createdAt: 10,
updatedAt: 20,
lastAuthenticatedAt: nil)
let second = ManagedCodexAccount(
id: secondID,
email: "mich.aelfmk5542@gmail.com",
providerAccountID: "team-4107",
managedHomePath: "/tmp/managed-home-2",
createdAt: 30,
updatedAt: 40,
lastAuthenticatedAt: nil)
let set = ManagedCodexAccountSet(
version: FileManagedCodexAccountStore.currentVersion,
accounts: [first, second])
#expect(set.accounts.count == 2)
#expect(set.account(email: "mi.chaelfmk5542@gmail.com", providerAccountID: "team-4107")?.id == firstID)
#expect(set.account(email: "mich.aelfmk5542@gmail.com", providerAccountID: "team-4107")?.id == secondID)
}
@Test
func `FileManagedCodexAccountStore hydrates provider account I D from id token when account field is absent`() 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 home = root.appendingPathComponent("jwt-only-home", isDirectory: true)
try writeCodexAuthFile(
homeURL: home,
email: "user@example.com",
accountId: "account-jwt-only",
includeAccountIdField: false)
let accountID = UUID()
let json = """
{
"accounts" : [
{
"createdAt" : 10,
"email" : "user@example.com",
"id" : "\(accountID.uuidString)",
"lastAuthenticatedAt" : null,
"managedHomePath" : "\(home.path)",
"updatedAt" : 20
}
],
"version" : 1
}
"""
try json.write(to: fileURL, atomically: true, encoding: .utf8)
let store = FileManagedCodexAccountStore(fileURL: fileURL)
let loaded = try store.loadAccounts()
#expect(loaded.version == FileManagedCodexAccountStore.currentVersion)
#expect(loaded.accounts.count == 1)
#expect(loaded.accounts.first?.providerAccountID == "account-jwt-only")
#expect(loaded.account(email: "user@example.com", providerAccountID: "account-jwt-only")?.id == accountID)
}
@Test
func `FileManagedCodexAccountStore drops duplicate IDs on load`() throws {
let tempDir = FileManager.default.temporaryDirectory
let fileURL = tempDir.appendingPathComponent("codexbar-managed-codex-accounts-duplicate-id-test.json")
defer { try? FileManager.default.removeItem(at: fileURL) }
let sharedID = UUID()
let json = """
{
"accounts" : [
{
"createdAt" : 10,
"email" : "first@example.com",
"id" : "\(sharedID.uuidString)",
"lastAuthenticatedAt" : null,
"managedHomePath" : "/tmp/managed-home-1",
"updatedAt" : 20
},
{
"createdAt" : 30,
"email" : "second@example.com",
"id" : "\(sharedID.uuidString)",
"lastAuthenticatedAt" : null,
"managedHomePath" : "/tmp/managed-home-2",
"updatedAt" : 40
}
],
"version" : 1
}
"""
try json.write(to: fileURL, atomically: true, encoding: .utf8)
let store = FileManagedCodexAccountStore(fileURL: fileURL)
let loaded = try store.loadAccounts()
#expect(loaded.accounts.count == 1)
#expect(loaded.accounts.first?.id == sharedID)
#expect(loaded.accounts.first?.email == "first@example.com")
#expect(loaded.accounts.first?.managedHomePath == "/tmp/managed-home-1")
}
@Test
func `FileManagedCodexAccountStore v1 upgrade keeps deleted home row with nil provider account I D`() throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
defer { try? FileManager.default.removeItem(at: root) }
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
let fileURL = root.appendingPathComponent("managed.json", isDirectory: false)
let missingHome = root.appendingPathComponent("missing-home", isDirectory: true)
let accountID = UUID()
let json = """
{
"accounts" : [
{
"createdAt" : 10,
"email" : "user@example.com",
"id" : "\(accountID.uuidString)",
"lastAuthenticatedAt" : null,
"managedHomePath" : "\(missingHome.path)",
"updatedAt" : 20
}
],
"version" : 1
}
"""
try json.write(to: fileURL, atomically: true, encoding: .utf8)
let store = FileManagedCodexAccountStore(fileURL: fileURL)
let loaded = try store.loadAccounts()
#expect(loaded.version == FileManagedCodexAccountStore.currentVersion)
#expect(loaded.accounts.count == 1)
#expect(loaded.accounts.first?.id == accountID)
#expect(loaded.accounts.first?.providerAccountID == nil)
}
@Test
func `FileManagedCodexAccountStore ignores legacy active account key on load`() throws {
let tempDir = FileManager.default.temporaryDirectory
let fileURL = tempDir.appendingPathComponent("codexbar-managed-codex-accounts-legacy-active-key-test.json")
defer { try? FileManager.default.removeItem(at: fileURL) }
let accountID = UUID()
let danglingID = UUID()
let json = """
{
"accounts" : [
{
"createdAt" : 10,
"email" : "user@example.com",
"id" : "\(accountID.uuidString)",
"lastAuthenticatedAt" : null,
"managedHomePath" : "/tmp/managed-home",
"updatedAt" : 20
}
],
"activeAccountID" : "\(danglingID.uuidString)",
"version" : 1
}
"""
try json.write(to: fileURL, atomically: true, encoding: .utf8)
let store = FileManagedCodexAccountStore(fileURL: fileURL)
let loaded = try store.loadAccounts()
#expect(loaded.accounts.count == 1)
#expect(loaded.account(id: accountID)?.email == "user@example.com")
}
@Test
func `FileManagedCodexAccountStore upgrades v1 rows and writes readable v2 file without reauth`() 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 firstHome = root.appendingPathComponent("first-home", isDirectory: true)
let secondHome = root.appendingPathComponent("second-home", isDirectory: true)
try writeCodexAuthFile(homeURL: firstHome, email: "user@example.com", accountId: "account-alpha")
try writeCodexAuthFile(homeURL: secondHome, email: "second@example.com", accountId: "account-beta")
let firstID = UUID()
let secondID = UUID()
let json = """
{
"accounts" : [
{
"createdAt" : 10,
"email" : "user@example.com",
"id" : "\(firstID.uuidString)",
"lastAuthenticatedAt" : null,
"managedHomePath" : "\(firstHome.path)",
"updatedAt" : 20
},
{
"createdAt" : 30,
"email" : "second@example.com",
"id" : "\(secondID.uuidString)",
"lastAuthenticatedAt" : null,
"managedHomePath" : "\(secondHome.path)",
"updatedAt" : 40
}
],
"version" : 1
}
"""
try json.write(to: fileURL, atomically: true, encoding: .utf8)
let store = FileManagedCodexAccountStore(fileURL: fileURL)
let loaded = try store.loadAccounts()
try store.storeAccounts(loaded)
let reloaded = try store.loadAccounts()
let contents = try String(contentsOf: fileURL, encoding: .utf8)
#expect(loaded.accounts.count == 2)
#expect(loaded.account(email: "user@example.com", providerAccountID: "account-alpha")?.id == firstID)
#expect(loaded.account(email: "second@example.com", providerAccountID: "account-beta")?.id == secondID)
#expect(reloaded.accounts.count == 2)
#expect(contents.contains("account-alpha"))
#expect(contents.contains("account-beta"))
#expect(contents.contains("\"version\" : \(FileManagedCodexAccountStore.currentVersion)"))
}
@Test
func `FileManagedCodexAccountStore rejects unsupported on disk versions`() throws {
let tempDir = FileManager.default.temporaryDirectory
let fileURL = tempDir.appendingPathComponent("codexbar-managed-codex-accounts-unsupported-version-test.json")
defer { try? FileManager.default.removeItem(at: fileURL) }
let accountID = UUID()
let json = """
{
"accounts" : [
{
"createdAt" : 10,
"email" : "user@example.com",
"id" : "\(accountID.uuidString)",
"lastAuthenticatedAt" : null,
"managedHomePath" : "/tmp/managed-home",
"updatedAt" : 20
}
],
"version" : 999
}
"""
try json.write(to: fileURL, atomically: true, encoding: .utf8)
let store = FileManagedCodexAccountStore(fileURL: fileURL)
#expect(throws: FileManagedCodexAccountStoreError.unsupportedVersion(999)) {
try store.loadAccounts()
}
}
@Test
func `FileManagedCodexAccountStore normalizes stored version to current schema`() throws {
let tempDir = FileManager.default.temporaryDirectory
let fileURL = tempDir.appendingPathComponent("codexbar-managed-codex-accounts-version-normalization-test.json")
defer { try? FileManager.default.removeItem(at: fileURL) }
let accountID = UUID()
let account = ManagedCodexAccount(
id: accountID,
email: "user@example.com",
providerAccountID: "account-id",
managedHomePath: "/tmp/managed-home",
createdAt: 10,
updatedAt: 20,
lastAuthenticatedAt: nil)
let payload = ManagedCodexAccountSet(
version: 999,
accounts: [account])
let store = FileManagedCodexAccountStore(fileURL: fileURL)
try store.storeAccounts(payload)
let loaded = try store.loadAccounts()
let contents = try String(contentsOf: fileURL, encoding: .utf8)
#expect(loaded.version == FileManagedCodexAccountStore.currentVersion)
#expect(contents.contains("\"version\" : \(FileManagedCodexAccountStore.currentVersion)"))
#expect(!contents.contains("\"version\" : 999"))
}
private func writeCodexAuthFile(
homeURL: URL,
email: String,
accountId: String,
includeAccountIdField: Bool = true) throws
{
try FileManager.default.createDirectory(at: homeURL, withIntermediateDirectories: true)
var tokens: [String: Any] = [
"accessToken": "access-token",
"refreshToken": "refresh-token",
"idToken": fakeJWT(email: email, accountId: accountId),
]
if includeAccountIdField {
tokens["accountId"] = accountId
}
let data = try JSONSerialization.data(withJSONObject: ["tokens": tokens], options: [.sortedKeys])
try data.write(to: homeURL.appendingPathComponent("auth.json"))
}
private func fakeJWT(email: String, accountId: String) -> String {
let header = (try? JSONSerialization.data(withJSONObject: ["alg": "none"])) ?? Data()
let payload = (try? JSONSerialization.data(withJSONObject: [
"email": email,
"https://api.openai.com/auth": [
"chatgpt_account_id": accountId,
"chatgpt_plan_type": "pro",
],
])) ?? Data()
func base64URL(_ data: Data) -> String {
data.base64EncodedString()
.replacingOccurrences(of: "=", with: "")
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
}
return "\(base64URL(header)).\(base64URL(payload))."
}