254 lines
10 KiB
Swift
254 lines
10 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import CodexBarCore
|
|
#if canImport(FoundationNetworking)
|
|
import FoundationNetworking
|
|
#endif
|
|
|
|
struct CodexRateLimitResetCreditsTests {
|
|
@Test
|
|
func `resolves URL from chat GPT config`() {
|
|
let config = "chatgpt_base_url = \"https://chatgpt.com/backend-api/\"\n"
|
|
let url = CodexOAuthUsageFetcher._resolveRateLimitResetCreditsURLForTesting(configContents: config)
|
|
#expect(url.absoluteString == "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits")
|
|
}
|
|
|
|
@Test
|
|
func `request scopes auth and account with bounded timeout`() async throws {
|
|
let transport = ProviderHTTPTransportStub { request in
|
|
#expect(request.url?.absoluteString == "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits")
|
|
#expect(request.httpMethod == "GET")
|
|
#expect(request.timeoutInterval == 4)
|
|
#expect(request.cachePolicy == .reloadIgnoringLocalCacheData)
|
|
#expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer test-token")
|
|
#expect(request.value(forHTTPHeaderField: "ChatGPT-Account-ID") == "account-123")
|
|
#expect(request.value(forHTTPHeaderField: "OpenAI-Beta") == "codex-1")
|
|
#expect(request.value(forHTTPHeaderField: "originator") == "Codex Desktop")
|
|
let url = try #require(request.url)
|
|
let response = try #require(HTTPURLResponse(
|
|
url: url,
|
|
statusCode: 200,
|
|
httpVersion: nil,
|
|
headerFields: nil))
|
|
return (Data(#"{"credits":[],"available_count":0}"#.utf8), response)
|
|
}
|
|
|
|
let snapshot = try await CodexAuthenticatedHTTPTransport.$overrideForTesting.withValue(transport) {
|
|
try await CodexOAuthUsageFetcher.fetchRateLimitResetCredits(
|
|
accessToken: "test-token",
|
|
accountId: "account-123",
|
|
env: ["CODEX_HOME": "/tmp/codexbar-reset-credit-request-test"])
|
|
}
|
|
|
|
#expect(snapshot.availableCount == 0)
|
|
#expect(await transport.requests().count == 1)
|
|
}
|
|
|
|
@Test
|
|
func `rejects negative available count`() async throws {
|
|
let transport = ProviderHTTPTransportStub { request in
|
|
let url = try #require(request.url)
|
|
let response = try #require(HTTPURLResponse(
|
|
url: url,
|
|
statusCode: 200,
|
|
httpVersion: nil,
|
|
headerFields: nil))
|
|
return (Data(#"{"credits":[],"available_count":-1}"#.utf8), response)
|
|
}
|
|
|
|
do {
|
|
_ = try await CodexOAuthUsageFetcher.fetchRateLimitResetCredits(
|
|
accessToken: "test-token",
|
|
accountId: nil,
|
|
env: ["CODEX_HOME": "/tmp/codexbar-negative-reset-credit-test"],
|
|
session: transport)
|
|
Issue.record("Expected invalid response")
|
|
} catch CodexOAuthFetchError.invalidResponse {
|
|
// Expected.
|
|
} catch {
|
|
Issue.record("Unexpected error: \(error)")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `decodes credits and skips stale available expiry`() throws {
|
|
let json = """
|
|
{
|
|
"credits": [
|
|
{
|
|
"id": "RateLimitResetCredit_expired_available",
|
|
"reset_type": "codex_rate_limits",
|
|
"status": "available",
|
|
"granted_at": "2026-05-18T00:39:53Z",
|
|
"expires_at": "2026-06-17T00:39:53Z"
|
|
},
|
|
{
|
|
"id": "RateLimitResetCredit_later",
|
|
"reset_type": "codex_rate_limits",
|
|
"status": "available",
|
|
"granted_at": "2026-06-18T00:39:53.731630Z",
|
|
"expires_at": "2026-07-18T00:39:53.731630Z",
|
|
"redeem_started_at": null,
|
|
"redeemed_at": null,
|
|
"profile_image_url": "https://example.com/codex.png",
|
|
"profile_user_id": "Codex Team",
|
|
"title": "One free rate limit reset",
|
|
"description": "Thanks for using Codex!"
|
|
},
|
|
{
|
|
"id": "RateLimitResetCredit_earlier",
|
|
"reset_type": "codex_rate_limits",
|
|
"status": "available",
|
|
"granted_at": "2026-06-12T04:03:43.263391Z",
|
|
"expires_at": "2026-07-12T04:03:43.263391Z",
|
|
"redeem_started_at": null,
|
|
"redeemed_at": null,
|
|
"title": "One free rate limit reset",
|
|
"description": "Thanks for using Codex!"
|
|
},
|
|
{
|
|
"id": "RateLimitResetCredit_future_status",
|
|
"reset_type": "codex_rate_limits",
|
|
"status": "future_status",
|
|
"granted_at": "2026-06-12T04:03:43Z",
|
|
"expires_at": "2026-07-10T04:03:43Z",
|
|
"redeem_started_at": null,
|
|
"redeemed_at": null,
|
|
"title": "One free rate limit reset",
|
|
"description": "Thanks for using Codex!"
|
|
}
|
|
],
|
|
"available_count": 2
|
|
}
|
|
"""
|
|
|
|
let now = try #require(ISO8601DateFormatter().date(from: "2026-07-01T00:00:00Z"))
|
|
let snapshot = try CodexOAuthUsageFetcher._decodeRateLimitResetCreditsForTesting(
|
|
Data(json.utf8),
|
|
now: now)
|
|
|
|
#expect(snapshot.availableCount == 2)
|
|
#expect(snapshot.credits.count == 4)
|
|
#expect(snapshot.credits[0].resetType == "codex_rate_limits")
|
|
#expect(snapshot.credits[3].status == .unknown("future_status"))
|
|
#expect(snapshot.nextExpiringAvailableCredit?.id == CodexRateLimitResetCredit.stableID(
|
|
forProviderID: "RateLimitResetCredit_earlier"))
|
|
#expect(snapshot.credits.allSatisfy { !$0.id.contains("RateLimitResetCredit_") })
|
|
|
|
let usage = UsageSnapshot(
|
|
primary: nil,
|
|
secondary: nil,
|
|
codexResetCredits: snapshot,
|
|
updatedAt: now)
|
|
let encoded = try JSONEncoder().encode(usage)
|
|
let encodedText = try #require(String(data: encoded, encoding: .utf8))
|
|
#expect(!encodedText.contains("RateLimitResetCredit_earlier"))
|
|
#expect(!String(reflecting: usage).contains("RateLimitResetCredit_earlier"))
|
|
|
|
let roundTripped = try JSONDecoder().decode(UsageSnapshot.self, from: encoded)
|
|
#expect(roundTripped.codexResetCredits?.credits.map(\.id) == snapshot.credits.map(\.id))
|
|
}
|
|
|
|
@Test
|
|
func `available inventory keeps no-expiry credits and sorts deterministically`() {
|
|
let now = Date(timeIntervalSince1970: 1_788_134_400)
|
|
let tiedExpiry = now.addingTimeInterval(3600)
|
|
let snapshot = CodexRateLimitResetCreditsSnapshot(
|
|
credits: [
|
|
Self.credit(id: "nil-b", status: .available, expiresAt: nil),
|
|
Self.credit(id: "expired", status: .available, expiresAt: now),
|
|
Self.credit(id: "finite-b", status: .available, expiresAt: tiedExpiry),
|
|
Self.credit(id: "redeemed", status: .redeemed, expiresAt: now.addingTimeInterval(7200)),
|
|
Self.credit(id: "nil-a", status: .available, expiresAt: nil),
|
|
Self.credit(id: "finite-a", status: .available, expiresAt: tiedExpiry),
|
|
],
|
|
availableCount: 99,
|
|
updatedAt: now)
|
|
|
|
let inventory = snapshot.availableInventory(at: now)
|
|
|
|
#expect(inventory.count == 4)
|
|
let expectedFiniteIDs = ["finite-a", "finite-b"]
|
|
.map(CodexRateLimitResetCredit.stableID(forProviderID:))
|
|
.sorted()
|
|
let expectedNoExpiryIDs = ["nil-a", "nil-b"]
|
|
.map(CodexRateLimitResetCredit.stableID(forProviderID:))
|
|
.sorted()
|
|
#expect(inventory.credits.map(\.id) == expectedFiniteIDs + expectedNoExpiryIDs)
|
|
#expect(inventory.nextExpiringCredit?.id == expectedFiniteIDs.first)
|
|
}
|
|
|
|
@Test
|
|
func `provider IDs always hash even when shaped like persisted stable IDs`() throws {
|
|
let canonicalLookingRawID = "codex-reset-credit-v1-" + String(repeating: "a", count: 64)
|
|
let json = """
|
|
{
|
|
"credits": [{
|
|
"id": "\(canonicalLookingRawID)",
|
|
"reset_type": "codex_rate_limits",
|
|
"status": "available",
|
|
"granted_at": "2026-06-18T00:39:53Z",
|
|
"expires_at": null
|
|
}],
|
|
"available_count": 1
|
|
}
|
|
"""
|
|
let now = Date(timeIntervalSince1970: 1_788_134_400)
|
|
|
|
let decoded = try CodexOAuthUsageFetcher._decodeRateLimitResetCreditsForTesting(
|
|
Data(json.utf8),
|
|
now: now)
|
|
let decodedID = try #require(decoded.credits.first?.id)
|
|
let expectedID = CodexRateLimitResetCredit.stableID(forProviderID: canonicalLookingRawID)
|
|
|
|
#expect(decodedID == expectedID)
|
|
#expect(decodedID != canonicalLookingRawID)
|
|
|
|
let publicModel = Self.credit(id: canonicalLookingRawID, status: .available, expiresAt: nil)
|
|
#expect(publicModel.id == expectedID)
|
|
#expect(publicModel.id != canonicalLookingRawID)
|
|
|
|
let encoded = try JSONEncoder().encode(publicModel)
|
|
let roundTripped = try JSONDecoder().decode(CodexRateLimitResetCredit.self, from: encoded)
|
|
#expect(roundTripped.id == expectedID)
|
|
|
|
let ordinaryFirst = Self.credit(id: "ordinary-provider-id", status: .available, expiresAt: nil)
|
|
let ordinarySecond = Self.credit(id: "ordinary-provider-id", status: .available, expiresAt: nil)
|
|
#expect(ordinaryFirst.id == ordinarySecond.id)
|
|
#expect(ordinaryFirst.id != "ordinary-provider-id")
|
|
}
|
|
|
|
@Test
|
|
func `reset credit GET preserves transport cancellation`() async throws {
|
|
let transport = ProviderHTTPTransportStub { request in
|
|
#expect(request.httpMethod == "GET")
|
|
throw URLError(.cancelled)
|
|
}
|
|
|
|
await #expect(throws: CancellationError.self) {
|
|
_ = try await CodexOAuthUsageFetcher.fetchRateLimitResetCredits(
|
|
accessToken: "test-token",
|
|
accountId: "account-123",
|
|
env: ["CODEX_HOME": "/tmp/codexbar-reset-credit-cancellation-test"],
|
|
session: transport)
|
|
}
|
|
}
|
|
|
|
private static func credit(
|
|
id: String,
|
|
status: CodexRateLimitResetCreditStatus,
|
|
expiresAt: Date?) -> CodexRateLimitResetCredit
|
|
{
|
|
CodexRateLimitResetCredit(
|
|
id: id,
|
|
resetType: "codex_rate_limits",
|
|
status: status,
|
|
grantedAt: Date(timeIntervalSince1970: 1_788_000_000),
|
|
expiresAt: expiresAt,
|
|
redeemStartedAt: nil,
|
|
redeemedAt: nil,
|
|
title: nil,
|
|
description: nil)
|
|
}
|
|
}
|