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

335 lines
15 KiB
Swift

import Foundation
import Testing
@testable import CodexBar
@testable import CodexBarCLI
@testable import CodexBarCore
@Suite(.serialized)
struct OpenAIAPIProjectScopeTests {
@Test
@MainActor
func `token account strips configured project in app environment builder`() {
let settings = Self.makeSettingsStore(suite: "OpenAIAPIProjectScopeTests-app")
settings.openAIAPIKey = "config-token"
settings.openAIAPIProjectID = "proj_config"
settings.addTokenAccount(provider: .openai, label: "Configured account", token: "first-account-token")
settings.addTokenAccount(provider: .openai, label: "Selected account", token: "selected-account-token")
let selectedAccount = settings.tokenAccounts(for: .openai)[1]
let env = ProviderRegistry.makeEnvironment(
base: [OpenAIAPISettingsReader.projectIDEnvironmentKey: "proj_env"],
provider: .openai,
settings: settings,
tokenOverride: TokenAccountOverride(provider: .openai, account: selectedAccount))
#expect(env[OpenAIAPISettingsReader.adminAPIKeyEnvironmentKey] == "selected-account-token")
#expect(env[OpenAIAPISettingsReader.adminAPIKeyEnvironmentKey] != "config-token")
#expect(env[OpenAIAPISettingsReader.adminAPIKeyEnvironmentKey] != "first-account-token")
#expect(env[OpenAIAPISettingsReader.projectIDEnvironmentKey] == nil)
}
@Test
func `token account strips configured project in CLI environment builder`() throws {
let account = ProviderTokenAccount(
id: UUID(),
label: "Project account",
token: "account-token",
addedAt: Date().timeIntervalSince1970,
lastUsed: nil)
let accounts = ProviderTokenAccountData(version: 1, accounts: [account], activeIndex: 0)
let config = CodexBarConfig(
providers: [
ProviderConfig(
id: .openai,
apiKey: "config-token",
workspaceID: "proj_config",
tokenAccounts: accounts),
])
let selection = TokenAccountCLISelection(label: nil, index: nil, allAccounts: false)
let tokenContext = try TokenAccountCLIContext(selection: selection, config: config, verbose: false)
let env = tokenContext.environment(
base: [OpenAIAPISettingsReader.projectIDEnvironmentKey: "proj_env"],
provider: .openai,
account: account)
#expect(env[OpenAIAPISettingsReader.adminAPIKeyEnvironmentKey] == "account-token")
#expect(env[OpenAIAPISettingsReader.adminAPIKeyEnvironmentKey] != "config-token")
#expect(env[OpenAIAPISettingsReader.projectIDEnvironmentKey] == nil)
}
@Test
@MainActor
func `configured app project scopes admin usage strategy`() async throws {
let settings = Self.makeSettingsStore(suite: "OpenAIAPIProjectScopeTests-configured-project")
settings.openAIAPIKey = "config-token"
settings.openAIAPIProjectID = "proj_config"
let env = ProviderRegistry.makeEnvironment(
base: [:],
provider: .openai,
settings: settings,
tokenOverride: nil)
let strategy = OpenAIAPIBalanceFetchStrategy(
usageFetcher: { credential, historyDays in
#expect(credential.apiKey == "config-token")
#expect(credential.projectID == "proj_config")
#expect(historyDays == 30)
return OpenAIAPIUsageSnapshot(
daily: [],
updatedAt: Date(timeIntervalSince1970: 1_700_000_000),
projectID: credential.projectID)
},
balanceFetcher: { _ in
Issue.record("Configured project usage should not fetch legacy organization balance.")
return OpenAIAPICreditBalanceSnapshot(
totalGranted: 100,
totalUsed: 25,
totalAvailable: 75,
nextGrantExpiry: nil,
updatedAt: Date(timeIntervalSince1970: 1_700_000_000))
})
let result = try await strategy.fetch(Self.makeContext(env: env))
#expect(result.sourceLabel == "admin-api:project")
#expect(result.usage.identity?.loginMethod == "Admin API: proj_config")
#expect(result.usage.identity?.accountOrganization == "Project: proj_config")
}
@Test
func `legacy API key environment can scope admin usage by project`() async throws {
let strategy = OpenAIAPIBalanceFetchStrategy(
usageFetcher: { credential, historyDays in
#expect(credential.apiKey == "sk-admin-legacy")
#expect(credential.projectID == "proj_legacy")
#expect(credential.usesAdminKey == false)
#expect(historyDays == 30)
return OpenAIAPIUsageSnapshot(
daily: [],
updatedAt: Date(timeIntervalSince1970: 1_700_000_000),
projectID: credential.projectID)
},
balanceFetcher: { _ in
Issue.record("Legacy OPENAI_API_KEY project usage should not fetch unscoped balance.")
return OpenAIAPICreditBalanceSnapshot(
totalGranted: 100,
totalUsed: 25,
totalAvailable: 75,
nextGrantExpiry: nil,
updatedAt: Date(timeIntervalSince1970: 1_700_000_000))
})
let result = try await strategy.fetch(Self.makeContext(
env: [
OpenAIAPISettingsReader.apiKeyEnvironmentKey: "sk-admin-legacy",
OpenAIAPISettingsReader.projectIDEnvironmentKey: "proj_legacy",
]))
#expect(result.sourceLabel == "admin-api:project")
#expect(result.usage.identity?.loginMethod == "Admin API: proj_legacy")
#expect(result.usage.identity?.accountOrganization == "Project: proj_legacy")
}
@Test
func `ambient project with legacy API key preserves billing fallback`() async throws {
let strategy = OpenAIAPIBalanceFetchStrategy(
usageFetcher: { credential, historyDays in
#expect(credential.apiKey == "sk-ambient")
#expect(credential.projectID == "proj_ambient")
#expect(credential.usesAdminKey == false)
#expect(historyDays == 30)
throw OpenAIAPIUsageError.apiError(endpoint: "costs", statusCode: 403)
},
balanceFetcher: { apiKey in
#expect(apiKey == "sk-ambient")
return OpenAIAPICreditBalanceSnapshot(
totalGranted: 100,
totalUsed: 25,
totalAvailable: 75,
nextGrantExpiry: nil,
updatedAt: Date(timeIntervalSince1970: 1_700_000_000))
})
let result = try await strategy.fetch(Self.makeContext(
env: [
OpenAIAPISettingsReader.apiKeyEnvironmentKey: "sk-ambient",
OpenAIAPISettingsReader.projectIDEnvironmentKey: "proj_ambient",
]))
#expect(result.sourceLabel == "billing-api")
#expect(result.usage.identity?.loginMethod == "API balance: $75.00")
#expect(result.usage.identity?.accountOrganization == nil)
}
@Test
func `project filtered admin usage does not fall back on service failure`() async {
let usageFailure = OpenAIAPIUsageError.apiError(endpoint: "costs", statusCode: 500)
let strategy = OpenAIAPIBalanceFetchStrategy(
usageFetcher: { credential, historyDays in
#expect(credential.apiKey == "sk-test")
#expect(credential.projectID == "proj_abc")
#expect(credential.usesAdminKey == true)
#expect(historyDays == 30)
throw usageFailure
},
balanceFetcher: { _ in
Issue.record("Project-filtered usage must not fall back to organization balance.")
return OpenAIAPICreditBalanceSnapshot(
totalGranted: 100,
totalUsed: 25,
totalAvailable: 75,
nextGrantExpiry: nil,
updatedAt: Date(timeIntervalSince1970: 1_700_000_000))
})
do {
_ = try await strategy.fetch(Self.makeContext(
env: [
OpenAIAPISettingsReader.adminAPIKeyEnvironmentKey: "sk-test",
OpenAIAPISettingsReader.projectIDEnvironmentKey: "proj_abc",
]))
Issue.record("Expected project-filtered admin usage failure.")
} catch let error as OpenAIAPIUsageError {
#expect(error == usageFailure)
} catch {
Issue.record("Expected OpenAIAPIUsageError, got \(error)")
}
}
@Test
func `project filtered admin usage does not fall back on credential rejection`() async {
let usageFailure = OpenAIAPIUsageError.apiError(endpoint: "costs", statusCode: 403)
let strategy = OpenAIAPIBalanceFetchStrategy(
usageFetcher: { credential, historyDays in
#expect(credential.apiKey == "sk-test")
#expect(credential.projectID == "proj_abc")
#expect(historyDays == 30)
throw usageFailure
},
balanceFetcher: { _ in
Issue.record("Project-filtered usage must fail closed instead of showing unscoped balance.")
return OpenAIAPICreditBalanceSnapshot(
totalGranted: 100,
totalUsed: 25,
totalAvailable: 75,
nextGrantExpiry: nil,
updatedAt: Date(timeIntervalSince1970: 1_700_000_000))
})
do {
_ = try await strategy.fetch(Self.makeContext(
env: [
OpenAIAPISettingsReader.adminAPIKeyEnvironmentKey: "sk-test",
OpenAIAPISettingsReader.projectIDEnvironmentKey: "proj_abc",
]))
Issue.record("Expected project-filtered admin credential failure.")
} catch let error as OpenAIAPIUsageError {
#expect(error == usageFailure)
} catch {
Issue.record("Expected OpenAIAPIUsageError, got \(error)")
}
}
@Test
func `project filtered admin usage reports project source label`() async throws {
let strategy = OpenAIAPIBalanceFetchStrategy(
usageFetcher: { credential, _ in
#expect(credential.apiKey == "sk-test")
#expect(credential.projectID == "proj_abc")
return OpenAIAPIUsageSnapshot(
daily: [],
updatedAt: Date(timeIntervalSince1970: 1_700_000_000),
projectID: credential.projectID)
},
balanceFetcher: { _ in
Issue.record("Project-filtered usage should not fetch legacy balance after admin usage succeeds.")
return OpenAIAPICreditBalanceSnapshot(
totalGranted: 100,
totalUsed: 25,
totalAvailable: 75,
nextGrantExpiry: nil,
updatedAt: Date(timeIntervalSince1970: 1_700_000_000))
})
let result = try await strategy.fetch(Self.makeContext(
env: [
OpenAIAPISettingsReader.adminAPIKeyEnvironmentKey: "sk-test",
OpenAIAPISettingsReader.projectIDEnvironmentKey: "proj_abc",
]))
#expect(result.sourceLabel == "admin-api:project")
#expect(result.usage.identity?.loginMethod == "Admin API: proj_abc")
#expect(result.usage.identity?.accountOrganization == "Project: proj_abc")
}
@Test
func `project scope follows final environment even when selected account flag is present`() async throws {
let accountID = UUID()
let strategy = OpenAIAPIBalanceFetchStrategy(
usageFetcher: { credential, historyDays in
#expect(credential.apiKey == "sk-test")
#expect(credential.projectID == "proj_env")
#expect(historyDays == 30)
return OpenAIAPIUsageSnapshot(
daily: [],
updatedAt: Date(timeIntervalSince1970: 1_700_000_000),
projectID: credential.projectID)
},
balanceFetcher: { _ in
Issue.record("Final project-scoped environments should not fetch legacy balance.")
return OpenAIAPICreditBalanceSnapshot(
totalGranted: 100,
totalUsed: 25,
totalAvailable: 75,
nextGrantExpiry: nil,
updatedAt: Date(timeIntervalSince1970: 1_700_000_000))
})
let result = try await strategy.fetch(Self.makeContext(
env: [
OpenAIAPISettingsReader.adminAPIKeyEnvironmentKey: "sk-test",
OpenAIAPISettingsReader.projectIDEnvironmentKey: "proj_env",
],
selectedTokenAccountID: accountID))
#expect(result.sourceLabel == "admin-api:project")
#expect(result.usage.identity?.loginMethod == "Admin API: proj_env")
#expect(result.usage.identity?.accountOrganization == "Project: proj_env")
}
private static func makeContext(
env: [String: String],
selectedTokenAccountID: UUID? = nil,
historyDays: Int = 30) -> ProviderFetchContext
{
let browserDetection = BrowserDetection(cacheTTL: 0)
return ProviderFetchContext(
runtime: .app,
sourceMode: .api,
includeCredits: false,
webTimeout: 1,
webDebugDumpHTML: false,
verbose: false,
env: env,
settings: nil,
fetcher: UsageFetcher(environment: env),
claudeFetcher: ClaudeUsageFetcher(browserDetection: browserDetection),
browserDetection: browserDetection,
selectedTokenAccountID: selectedTokenAccountID,
costUsageHistoryDays: historyDays)
}
@MainActor
private static func makeSettingsStore(suite: String) -> SettingsStore {
let defaults = UserDefaults(suiteName: suite)!
defaults.removePersistentDomain(forName: suite)
return SettingsStore(
userDefaults: defaults,
configStore: testConfigStore(suiteName: suite),
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore(),
tokenAccountStore: InMemoryTokenAccountStore())
}
}