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) } }