244 lines
9.2 KiB
Swift
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)
|
|
}
|
|
}
|