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

244 lines
9.2 KiB
Swift

import Foundation
import Testing
@testable import CodexBarCore
struct OpenAIAPICreditBalanceTests {
private func makeContext(
apiKey: String = "sk-test",
usesAdminKey: Bool = false,
projectID: String? = nil,
selectedTokenAccountID: UUID? = nil,
historyDays: Int = 30) -> ProviderFetchContext
{
let browserDetection = BrowserDetection(cacheTTL: 0)
let apiKeyEnvironmentKey = usesAdminKey
? OpenAIAPISettingsReader.adminAPIKeyEnvironmentKey
: OpenAIAPISettingsReader.apiKeyEnvironmentKey
var env = [apiKeyEnvironmentKey: apiKey]
if let projectID {
env[OpenAIAPISettingsReader.projectIDEnvironmentKey] = projectID
}
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)
}
@Test
func `prefers admin key environment variable`() {
let token = OpenAIAPISettingsReader.apiKey(environment: [
"OPENAI_API_KEY": "sk-project",
"OPENAI_ADMIN_KEY": "sk-admin",
])
#expect(token == "sk-admin")
}
@Test
func `parses credit grants balance`() throws {
let now = Date(timeIntervalSince1970: 1_700_000_000)
let json = """
{
"object": "credit_summary",
"total_granted": 25.5,
"total_used": 7.25,
"total_available": 18.25,
"grants": {
"object": "list",
"data": [
{
"grant_amount": 10.0,
"used_amount": 1.0,
"effective_at": 1690000000,
"expires_at": 1800000000
}
]
}
}
"""
let snapshot = try OpenAIAPICreditBalanceFetcher._parseSnapshotForTesting(Data(json.utf8), now: now)
#expect(snapshot.totalGranted == 25.5)
#expect(snapshot.totalUsed == 7.25)
#expect(snapshot.totalAvailable == 18.25)
#expect(snapshot.nextGrantExpiry == Date(timeIntervalSince1970: 1_800_000_000))
}
@Test
func `maps balance to usage snapshot`() {
let now = Date(timeIntervalSince1970: 1_700_000_000)
let balance = OpenAIAPICreditBalanceSnapshot(
totalGranted: 100,
totalUsed: 40,
totalAvailable: 60,
nextGrantExpiry: Date(timeIntervalSince1970: 1_800_000_000),
updatedAt: now)
let usage = balance.toUsageSnapshot()
#expect(usage.primary?.usedPercent == 40)
#expect(usage.primary?.resetDescription == "$60.00 available")
#expect(usage.providerCost?.used == 40)
#expect(usage.providerCost?.limit == 100)
#expect(usage.identity?.providerID == .openai)
#expect(usage.identity?.loginMethod == "API balance: $60.00")
}
@Test
func `maps unauthorized legacy balance to admin key guidance`() async {
let transport = ProviderHTTPTransportStub { request in
let url = try #require(request.url)
let response = HTTPURLResponse(
url: url,
statusCode: 401,
httpVersion: nil,
headerFields: nil)!
return (Data("{}".utf8), response)
}
do {
_ = try await OpenAIAPICreditBalanceFetcher.fetchBalance(
apiKey: "sk-test",
session: transport)
Issue.record("Expected credential rejection")
} catch let error as OpenAIAPICreditBalanceError {
#expect(error == .unauthorized)
#expect(error.errorDescription?.contains("organization Admin API key") == true)
#expect(error.errorDescription?.contains("service-account keys") == true)
} catch {
Issue.record("Expected OpenAIAPICreditBalanceError, got \(error)")
}
}
@Test
func `falls back to legacy billing when admin usage rejects credentials`() async throws {
let strategy = OpenAIAPIBalanceFetchStrategy(
usageFetcher: { _, _ in
throw OpenAIAPIUsageError.apiError(endpoint: "costs", statusCode: 403)
},
balanceFetcher: { _ in
OpenAIAPICreditBalanceSnapshot(
totalGranted: 100,
totalUsed: 25,
totalAvailable: 75,
nextGrantExpiry: nil,
updatedAt: Date(timeIntervalSince1970: 1_700_000_000))
})
let result = try await strategy.fetch(self.makeContext())
#expect(result.sourceLabel == "billing-api")
#expect(result.usage.identity?.loginMethod == "API balance: $75.00")
}
@Test
func `legacy API key without project ID falls back to legacy billing`() async throws {
let strategy = OpenAIAPIBalanceFetchStrategy(
usageFetcher: { credential, historyDays in
#expect(credential.apiKey == "sk-test")
#expect(credential.projectID == nil)
#expect(historyDays == 30)
throw OpenAIAPIUsageError.apiError(endpoint: "costs", statusCode: 403)
},
balanceFetcher: { apiKey in
#expect(apiKey == "sk-test")
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())
#expect(result.sourceLabel == "billing-api")
#expect(result.usage.identity?.loginMethod == "API balance: $75.00")
#expect(result.usage.identity?.accountOrganization == nil)
}
@Test
func `selected token account uses scrubbed final environment for legacy fallback`() async throws {
let accountID = UUID()
let strategy = OpenAIAPIBalanceFetchStrategy(
usageFetcher: { credential, historyDays in
#expect(credential.apiKey == "account-token")
#expect(credential.projectID == nil)
#expect(historyDays == 30)
throw OpenAIAPIUsageError.apiError(endpoint: "costs", statusCode: 403)
},
balanceFetcher: { apiKey in
#expect(apiKey == "account-token")
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(
apiKey: "account-token",
usesAdminKey: true,
selectedTokenAccountID: accountID))
#expect(result.sourceLabel == "billing-api")
#expect(result.usage.identity?.loginMethod == "API balance: $75.00")
#expect(result.usage.identity?.accountOrganization == nil)
}
@Test
func `preserves admin usage error when legacy fallback also fails`() async {
let usageFailure = OpenAIAPIUsageError.parseFailed(endpoint: "costs", message: "changed")
let strategy = OpenAIAPIBalanceFetchStrategy(
usageFetcher: { _, _ in throw usageFailure },
balanceFetcher: { _ in throw OpenAIAPICreditBalanceError.forbidden })
do {
_ = try await strategy.fetch(self.makeContext())
Issue.record("Expected admin usage failure")
} catch let error as OpenAIAPIUsageError {
#expect(error == usageFailure)
} catch {
Issue.record("Expected OpenAIAPIUsageError, got \(error)")
}
}
@Test
func `falls back to credit balance when admin usage endpoint is unavailable`() async throws {
let strategy = OpenAIAPIBalanceFetchStrategy(
usageFetcher: { credential, historyDays in
#expect(credential.apiKey == "sk-test")
#expect(credential.projectID == nil)
#expect(historyDays == 90)
throw OpenAIAPIUsageError.apiError(endpoint: "costs", statusCode: 500)
},
balanceFetcher: { apiKey in
#expect(apiKey == "sk-test")
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(historyDays: 90))
#expect(result.sourceLabel == "billing-api")
#expect(result.usage.providerCost?.used == 25)
#expect(result.usage.providerCost?.limit == 100)
}
}