Files
2026-07-13 12:22:33 +08:00

812 lines
27 KiB
Swift

import Foundation
import Testing
@testable import CodexBarCore
struct CodexOAuthTests {
private func makeContext(
runtime: ProviderRuntime = .app,
sourceMode: ProviderSourceMode = .auto,
includeCredits: Bool = true,
includeOptionalUsage: Bool = true) -> ProviderFetchContext
{
let browserDetection = BrowserDetection(cacheTTL: 0)
return ProviderFetchContext(
runtime: runtime,
sourceMode: sourceMode,
includeCredits: includeCredits,
includeOptionalUsage: includeOptionalUsage,
webTimeout: 60,
webDebugDumpHTML: false,
verbose: false,
env: [:],
settings: nil,
fetcher: UsageFetcher(),
claudeFetcher: ClaudeUsageFetcher(browserDetection: browserDetection),
browserDetection: browserDetection)
}
@Test
func `parses O auth credentials`() throws {
let json = """
{
"OPENAI_API_KEY": null,
"tokens": {
"access_token": "access-token",
"refresh_token": "refresh-token",
"id_token": "id-token",
"account_id": "account-123"
},
"last_refresh": "2025-12-20T12:34:56Z"
}
"""
let creds = try CodexOAuthCredentialsStore.parse(data: Data(json.utf8))
#expect(creds.accessToken == "access-token")
#expect(creds.refreshToken == "refresh-token")
#expect(creds.idToken == "id-token")
#expect(creds.accountId == "account-123")
#expect(creds.lastRefresh != nil)
}
@Test
func `parses legacy camel case O auth credentials`() throws {
let json = """
{
"OPENAI_API_KEY": null,
"tokens": {
"accessToken": "access-token",
"refreshToken": "refresh-token",
"idToken": "id-token",
"accountId": "account-123"
},
"last_refresh": "2025-12-20T12:34:56Z"
}
"""
let creds = try CodexOAuthCredentialsStore.parse(data: Data(json.utf8))
#expect(creds.accessToken == "access-token")
#expect(creds.refreshToken == "refresh-token")
#expect(creds.idToken == "id-token")
#expect(creds.accountId == "account-123")
#expect(creds.lastRefresh != nil)
}
@Test
func `parses API key credentials`() throws {
let json = """
{
"OPENAI_API_KEY": "sk-test"
}
"""
let creds = try CodexOAuthCredentialsStore.parse(data: Data(json.utf8))
#expect(creds.accessToken == "sk-test")
#expect(creds.refreshToken.isEmpty)
#expect(creds.idToken == nil)
#expect(creds.accountId == nil)
}
@Test
func `reset-credit token load ignores an API key beside O auth tokens`() throws {
let home = FileManager.default.temporaryDirectory
.appendingPathComponent("codexbar-reset-credit-oauth-\(UUID().uuidString)")
try FileManager.default.createDirectory(at: home, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: home) }
let json = """
{
"OPENAI_API_KEY": "sk-test",
"tokens": {
"access_token": "oauth-access-token",
"refresh_token": "oauth-refresh-token",
"account_id": "account-123"
},
"last_refresh": "2026-07-01T12:00:00Z"
}
"""
try Data(json.utf8).write(to: home.appendingPathComponent("auth.json"))
let credentials = try CodexOAuthCredentialsStore.loadOAuthTokens(env: ["CODEX_HOME": home.path])
#expect(credentials.accessToken == "oauth-access-token")
#expect(credentials.refreshToken == "oauth-refresh-token")
#expect(credentials.accountId == "account-123")
}
@Test
func `decodes credits balance string`() throws {
let json = """
{
"plan_type": "pro",
"rate_limit": {
"primary_window": {
"used_percent": 12,
"reset_at": 1766948068,
"limit_window_seconds": 18000
}
},
"credits": {
"has_credits": false,
"unlimited": false,
"balance": "0"
}
}
"""
let response = try CodexOAuthUsageFetcher._decodeUsageResponseForTesting(Data(json.utf8))
#expect(response.planType?.rawValue == "pro")
#expect(response.credits?.balance == 0)
#expect(response.credits?.hasCredits == false)
#expect(response.credits?.unlimited == false)
}
@Test
func `decodes prolite plan type without failing usage mapping`() throws {
let json = """
{
"plan_type": "prolite",
"rate_limit": {
"primary_window": {
"used_percent": 12,
"reset_at": 1766948068,
"limit_window_seconds": 18000
}
}
}
"""
let response = try CodexOAuthUsageFetcher._decodeUsageResponseForTesting(Data(json.utf8))
#expect(response.planType?.rawValue == "prolite")
let creds = CodexOAuthCredentials(
accessToken: "access",
refreshToken: "refresh",
idToken: nil,
accountId: nil,
lastRefresh: Date())
let mapped = try CodexOAuthFetchStrategy._mapUsageForTesting(Data(json.utf8), credentials: creds)
#expect(mapped?.primary?.usedPercent == 12)
}
@Test
func `maps usage windows from O auth`() throws {
let json = """
{
"rate_limit": {
"primary_window": {
"used_percent": 22,
"reset_at": 1766948068,
"limit_window_seconds": 18000
},
"secondary_window": {
"used_percent": 43,
"reset_at": 1767407914,
"limit_window_seconds": 604800
}
}
}
"""
let creds = CodexOAuthCredentials(
accessToken: "access",
refreshToken: "refresh",
idToken: nil,
accountId: nil,
lastRefresh: Date())
let mapped = try CodexOAuthFetchStrategy._mapUsageForTesting(Data(json.utf8), credentials: creds)
let snapshot = try #require(mapped)
#expect(snapshot.primary?.usedPercent == 22)
#expect(snapshot.primary?.windowMinutes == 300)
#expect(snapshot.secondary?.usedPercent == 43)
#expect(snapshot.secondary?.windowMinutes == 10080)
#expect(snapshot.primary?.resetsAt != nil)
#expect(snapshot.secondary?.resetsAt != nil)
}
@Test
func `O auth response with precise windows maps to exact confidence`() throws {
let json = """
{
"rate_limit": {
"primary_window": {
"used_percent": 22,
"reset_at": 1766948068,
"limit_window_seconds": 18000
},
"secondary_window": {
"used_percent": 43,
"reset_at": 1767407914,
"limit_window_seconds": 604800
}
}
}
"""
let creds = CodexOAuthCredentials(
accessToken: "access",
refreshToken: "refresh",
idToken: nil,
accountId: nil,
lastRefresh: Date())
let result = try CodexOAuthFetchStrategy._mapResultForTesting(Data(json.utf8), credentials: creds)
#expect(result.sourceLabel == "oauth")
#expect(result.usage.dataConfidence == .exact)
#expect(result.usage.primary?.usedPercent == 22)
#expect(result.usage.secondary?.usedPercent == 43)
}
@Test
func `O auth response with malformed additional window maps to unknown confidence`() throws {
let json = """
{
"rate_limit": {
"primary_window": {
"used_percent": 22,
"reset_at": 1766948068,
"limit_window_seconds": 18000
}
},
"additional_rate_limits": [
{
"limit_name": "GPT-5.3-Codex-Spark",
"metered_feature": "gpt_5_3_codex_spark",
"rate_limit": {
"primary_window": { "used_percent": "bad" }
}
}
]
}
"""
let creds = CodexOAuthCredentials(
accessToken: "access",
refreshToken: "refresh",
idToken: nil,
accountId: nil,
lastRefresh: Date())
let result = try CodexOAuthFetchStrategy._mapResultForTesting(Data(json.utf8), credentials: creds)
#expect(result.usage.primary?.usedPercent == 22)
#expect(result.usage.extraRateWindows == nil)
#expect(result.usage.dataConfidence == .unknown)
}
@Test
func `maps free weekly only window into secondary`() throws {
let json = """
{
"plan_type": "free",
"rate_limit": {
"primary_window": {
"used_percent": 0,
"reset_at": 1775468693,
"limit_window_seconds": 604800
},
"secondary_window": null
}
}
"""
let creds = CodexOAuthCredentials(
accessToken: "access",
refreshToken: "refresh",
idToken: nil,
accountId: nil,
lastRefresh: Date())
let mapped = try CodexOAuthFetchStrategy._mapUsageForTesting(Data(json.utf8), credentials: creds)
let snapshot = try #require(mapped)
#expect(snapshot.primary == nil)
#expect(snapshot.secondary?.usedPercent == 0)
#expect(snapshot.secondary?.windowMinutes == 10080)
}
@Test
func `keeps single session window as primary`() throws {
let json = """
{
"rate_limit": {
"primary_window": {
"used_percent": 9,
"reset_at": 1766948068,
"limit_window_seconds": 18000
},
"secondary_window": null
}
}
"""
let creds = CodexOAuthCredentials(
accessToken: "access",
refreshToken: "refresh",
idToken: nil,
accountId: nil,
lastRefresh: Date())
let mapped = try CodexOAuthFetchStrategy._mapUsageForTesting(Data(json.utf8), credentials: creds)
let snapshot = try #require(mapped)
#expect(snapshot.primary?.usedPercent == 9)
#expect(snapshot.primary?.windowMinutes == 300)
#expect(snapshot.secondary == nil)
}
@Test
func `preserves unknown single window as primary`() throws {
let json = """
{
"rate_limit": {
"primary_window": {
"used_percent": 17,
"reset_at": 1766948068,
"limit_window_seconds": 32400
},
"secondary_window": null
}
}
"""
let creds = CodexOAuthCredentials(
accessToken: "access",
refreshToken: "refresh",
idToken: nil,
accountId: nil,
lastRefresh: Date())
let mapped = try CodexOAuthFetchStrategy._mapUsageForTesting(Data(json.utf8), credentials: creds)
let snapshot = try #require(mapped)
#expect(snapshot.primary?.usedPercent == 17)
#expect(snapshot.primary?.windowMinutes == 540)
#expect(snapshot.secondary == nil)
}
@Test
func `preserves unknown secondary only window as primary`() throws {
let json = """
{
"rate_limit": {
"primary_window": null,
"secondary_window": {
"used_percent": 17,
"reset_at": 1766948068,
"limit_window_seconds": 32400
}
}
}
"""
let creds = CodexOAuthCredentials(
accessToken: "access",
refreshToken: "refresh",
idToken: nil,
accountId: nil,
lastRefresh: Date())
let mapped = try CodexOAuthFetchStrategy._mapUsageForTesting(Data(json.utf8), credentials: creds)
let snapshot = try #require(mapped)
#expect(snapshot.primary?.usedPercent == 17)
#expect(snapshot.primary?.windowMinutes == 540)
#expect(snapshot.secondary == nil)
}
@Test
func `swaps reversed weekly and unknown windows`() throws {
let json = """
{
"rate_limit": {
"primary_window": {
"used_percent": 43,
"reset_at": 1767407914,
"limit_window_seconds": 604800
},
"secondary_window": {
"used_percent": 17,
"reset_at": 1766948068,
"limit_window_seconds": 32400
}
}
}
"""
let creds = CodexOAuthCredentials(
accessToken: "access",
refreshToken: "refresh",
idToken: nil,
accountId: nil,
lastRefresh: Date())
let mapped = try CodexOAuthFetchStrategy._mapUsageForTesting(Data(json.utf8), credentials: creds)
let snapshot = try #require(mapped)
#expect(snapshot.primary?.usedPercent == 17)
#expect(snapshot.primary?.windowMinutes == 540)
#expect(snapshot.secondary?.usedPercent == 43)
#expect(snapshot.secondary?.windowMinutes == 10080)
}
@Test
func `returns nil when O auth usage has no windows`() throws {
let json = """
{
"rate_limit": {
"primary_window": null,
"secondary_window": null
}
}
"""
let creds = CodexOAuthCredentials(
accessToken: "access",
refreshToken: "refresh",
idToken: nil,
accountId: nil,
lastRefresh: Date())
let snapshot = try CodexOAuthFetchStrategy._mapUsageForTesting(Data(json.utf8), credentials: creds)
#expect(snapshot == nil)
}
@Test
func `keeps valid window when secondary window is malformed`() throws {
let json = """
{
"rate_limit": {
"primary_window": {
"used_percent": 18,
"reset_at": 1766948068,
"limit_window_seconds": 18000
},
"secondary_window": {
"used_percent": "bad",
"reset_at": 1767407914,
"limit_window_seconds": 604800
}
}
}
"""
let creds = CodexOAuthCredentials(
accessToken: "access",
refreshToken: "refresh",
idToken: nil,
accountId: nil,
lastRefresh: Date())
let snapshot = try CodexOAuthFetchStrategy._mapUsageForTesting(Data(json.utf8), credentials: creds)
#expect(snapshot?.primary?.usedPercent == 18)
#expect(snapshot?.secondary == nil)
let result = try CodexOAuthFetchStrategy._mapResultForTesting(Data(json.utf8), credentials: creds)
#expect(result.usage.primary?.usedPercent == 18)
#expect(result.usage.secondary == nil)
#expect(result.usage.dataConfidence == .unknown)
}
@Test
func `auto mode keeps weekly window when primary window is malformed`() throws {
let json = """
{
"rate_limit": {
"primary_window": {
"used_percent": "bad",
"reset_at": 1766948068,
"limit_window_seconds": 18000
},
"secondary_window": {
"used_percent": 43,
"reset_at": 1767407914,
"limit_window_seconds": 604800
}
}
}
"""
let creds = CodexOAuthCredentials(
accessToken: "access",
refreshToken: "refresh",
idToken: nil,
accountId: nil,
lastRefresh: Date())
let result = try CodexOAuthFetchStrategy._mapResultForTesting(
Data(json.utf8),
credentials: creds,
sourceMode: .auto)
#expect(result.usage.primary == nil)
#expect(result.usage.secondary?.usedPercent == 43)
#expect(result.usage.secondary?.windowMinutes == 10080)
}
@Test
func `explicit oauth keeps weekly window when primary window is malformed`() throws {
let json = """
{
"rate_limit": {
"primary_window": {
"used_percent": "bad",
"reset_at": 1766948068,
"limit_window_seconds": 18000
},
"secondary_window": {
"used_percent": 43,
"reset_at": 1767407914,
"limit_window_seconds": 604800
}
}
}
"""
let creds = CodexOAuthCredentials(
accessToken: "access",
refreshToken: "refresh",
idToken: nil,
accountId: nil,
lastRefresh: Date())
let result = try CodexOAuthFetchStrategy._mapResultForTesting(
Data(json.utf8),
credentials: creds,
sourceMode: .oauth)
#expect(result.usage.primary == nil)
#expect(result.usage.secondary?.usedPercent == 43)
#expect(result.usage.secondary?.windowMinutes == 10080)
}
@Test
func `auto mode preserves reversed session window when primary window is malformed`() throws {
let json = """
{
"rate_limit": {
"primary_window": {
"used_percent": "bad",
"reset_at": 1767407914,
"limit_window_seconds": 604800
},
"secondary_window": {
"used_percent": 18,
"reset_at": 1766948068,
"limit_window_seconds": 18000
}
}
}
"""
let creds = CodexOAuthCredentials(
accessToken: "access",
refreshToken: "refresh",
idToken: nil,
accountId: nil,
lastRefresh: Date())
let result = try CodexOAuthFetchStrategy._mapResultForTesting(
Data(json.utf8),
credentials: creds,
sourceMode: .auto)
#expect(result.usage.primary?.usedPercent == 18)
#expect(result.usage.primary?.windowMinutes == 300)
#expect(result.usage.secondary == nil)
}
@Test
func `auto mode keeps weekly window when reversed session window is malformed`() throws {
let json = """
{
"rate_limit": {
"primary_window": {
"used_percent": 43,
"reset_at": 1767407914,
"limit_window_seconds": 604800
},
"secondary_window": {
"used_percent": "bad",
"reset_at": 1766948068,
"limit_window_seconds": 18000
}
}
}
"""
let creds = CodexOAuthCredentials(
accessToken: "access",
refreshToken: "refresh",
idToken: nil,
accountId: nil,
lastRefresh: Date())
let result = try CodexOAuthFetchStrategy._mapResultForTesting(
Data(json.utf8),
credentials: creds,
sourceMode: .auto)
#expect(result.usage.primary == nil)
#expect(result.usage.secondary?.usedPercent == 43)
#expect(result.usage.secondary?.windowMinutes == 10080)
}
@Test
func `explicit oauth keeps weekly window when reversed session window is malformed`() throws {
let json = """
{
"rate_limit": {
"primary_window": {
"used_percent": 43,
"reset_at": 1767407914,
"limit_window_seconds": 604800
},
"secondary_window": {
"used_percent": "bad",
"reset_at": 1766948068,
"limit_window_seconds": 18000
}
}
}
"""
let creds = CodexOAuthCredentials(
accessToken: "access",
refreshToken: "refresh",
idToken: nil,
accountId: nil,
lastRefresh: Date())
let result = try CodexOAuthFetchStrategy._mapResultForTesting(
Data(json.utf8),
credentials: creds,
sourceMode: .oauth)
#expect(result.usage.primary == nil)
#expect(result.usage.secondary?.usedPercent == 43)
#expect(result.usage.secondary?.windowMinutes == 10080)
}
@Test
func `ignores malformed credits payload while keeping usage`() throws {
let json = """
{
"rate_limit": {
"primary_window": {
"used_percent": 22,
"reset_at": 1766948068,
"limit_window_seconds": 18000
}
},
"credits": {
"has_credits": false,
"unlimited": false,
"balance": []
}
}
"""
let response = try CodexOAuthUsageFetcher._decodeUsageResponseForTesting(Data(json.utf8))
#expect(response.credits?.hasCredits == false)
#expect(response.credits?.unlimited == false)
#expect(response.credits?.balance == nil)
let creds = CodexOAuthCredentials(
accessToken: "access",
refreshToken: "refresh",
idToken: nil,
accountId: nil,
lastRefresh: Date())
let snapshot = try CodexOAuthFetchStrategy._mapUsageForTesting(Data(json.utf8), credentials: creds)
#expect(snapshot?.primary?.usedPercent == 22)
}
@Test
func `credits only O auth payload still returns credits result`() throws {
let json = """
{
"rate_limit": {
"primary_window": null,
"secondary_window": null
},
"credits": {
"has_credits": true,
"unlimited": false,
"balance": "14.5"
}
}
"""
let creds = CodexOAuthCredentials(
accessToken: "access",
refreshToken: "refresh",
idToken: nil,
accountId: nil,
lastRefresh: Date())
let result = try CodexOAuthFetchStrategy._mapResultForTesting(Data(json.utf8), credentials: creds)
#expect(result.usage.primary == nil)
#expect(result.usage.secondary == nil)
#expect(result.credits?.remaining == 14.5)
#expect(result.sourceLabel == "oauth")
}
@Test
func `credits only O auth payload returns credits in auto mode`() throws {
let json = """
{
"rate_limit": {
"primary_window": null,
"secondary_window": null
},
"credits": {
"has_credits": true,
"unlimited": false,
"balance": "14.5"
}
}
"""
let creds = CodexOAuthCredentials(
accessToken: "access",
refreshToken: "refresh",
idToken: nil,
accountId: nil,
lastRefresh: Date())
let result = try CodexOAuthFetchStrategy._mapResultForTesting(
Data(json.utf8),
credentials: creds,
sourceMode: .auto)
#expect(result.usage.primary == nil)
#expect(result.usage.secondary == nil)
#expect(result.credits?.remaining == 14.5)
#expect(result.sourceLabel == "oauth")
}
@Test
func `auto mode only falls back from O auth on auth failures`() {
let strategy = CodexOAuthFetchStrategy()
let context = self.makeContext(sourceMode: .auto)
#expect(strategy.shouldFallback(on: CodexOAuthFetchError.unauthorized, context: context))
#expect(strategy.shouldFallback(on: CodexOAuthCredentialsError.notFound, context: context))
#expect(strategy.shouldFallback(on: CodexOAuthCredentialsError.missingTokens, context: context))
#expect(strategy.shouldFallback(on: CodexTokenRefresher.RefreshError.expired, context: context))
#expect(strategy.shouldFallback(on: CodexTokenRefresher.RefreshError.revoked, context: context))
#expect(strategy.shouldFallback(on: CodexTokenRefresher.RefreshError.reused, context: context))
#expect(!strategy.shouldFallback(on: UsageError.noRateLimitsFound, context: context))
#expect(!strategy.shouldFallback(on: CodexOAuthCredentialsError.decodeFailed("bad json"), context: context))
#expect(!strategy.shouldFallback(on: CodexOAuthFetchError.invalidResponse, context: context))
#expect(!strategy.shouldFallback(on: CodexOAuthFetchError.serverError(500, "offline"), context: context))
#expect(!strategy.shouldFallback(
on: CodexOAuthFetchError.networkError(URLError(.notConnectedToInternet)),
context: context))
#expect(!strategy.shouldFallback(
on: CodexTokenRefresher.RefreshError.networkError(URLError(.timedOut)),
context: context))
}
@Test
func `non 401 invalid grant refresh failure is treated as revoked`() {
let data = Data(#"{"error":"invalid_grant"}"#.utf8)
let error = CodexTokenRefresher._refreshFailureErrorForTesting(statusCode: 400, data: data)
switch error {
case .revoked:
break
default:
Issue.record("Expected invalid_grant to be treated as revoked")
}
}
@Test
func `non auth refresh failure remains invalid response`() {
let data = Data(#"{"error":"invalid_request"}"#.utf8)
let error = CodexTokenRefresher._refreshFailureErrorForTesting(statusCode: 400, data: data)
switch error {
case let .invalidResponse(message):
#expect(message == "Status 400")
default:
Issue.record("Expected invalid_request to remain an invalid response")
}
}
@Test
func `explicit O auth mode never falls back to CLI`() {
let strategy = CodexOAuthFetchStrategy()
let context = self.makeContext(sourceMode: .oauth)
#expect(!strategy.shouldFallback(on: CodexOAuthFetchError.unauthorized, context: context))
#expect(!strategy.shouldFallback(on: CodexTokenRefresher.RefreshError.expired, context: context))
}
@Test
func `resolves chat GPT usage URL from config`() {
let config = "chatgpt_base_url = \"https://chatgpt.com/backend-api/\"\n"
let url = CodexOAuthUsageFetcher._resolveUsageURLForTesting(configContents: config)
#expect(url.absoluteString == "https://chatgpt.com/backend-api/wham/usage")
}
@Test
func `resolves codex usage URL from config`() {
let config = "chatgpt_base_url = \"https://api.openai.com\"\n"
let url = CodexOAuthUsageFetcher._resolveUsageURLForTesting(configContents: config)
#expect(url.absoluteString == "https://api.openai.com/api/codex/usage")
}
@Test
func `normalizes chat GPT base URL without backend API`() {
let config = "chatgpt_base_url = \"https://chat.openai.com\"\n"
let url = CodexOAuthUsageFetcher._resolveUsageURLForTesting(configContents: config)
#expect(url.absoluteString == "https://chat.openai.com/backend-api/wham/usage")
}
}