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

243 lines
10 KiB
Swift

import Foundation
import Testing
@testable import CodexBarCore
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
@Suite(.serialized)
struct CodexOAuthRequestTests {
@Test
func `authenticated transport disables shared network state`() {
let configuration = CodexAuthenticatedHTTPTransport.makeConfiguration()
#expect(configuration.urlCache == nil)
#expect(configuration.requestCachePolicy == .reloadIgnoringLocalCacheData)
#expect(configuration.httpCookieStorage == nil)
#expect(configuration.httpShouldSetCookies == false)
#expect(configuration.urlCredentialStorage == nil)
}
@Test
func `usage requests fetch distinct cacheable responses for each account`() async throws {
defer { CodexOAuthAccountURLProtocol.reset() }
CodexOAuthAccountURLProtocol.reset()
let configuration = CodexAuthenticatedHTTPTransport.makeConfiguration()
configuration.protocolClasses = [CodexOAuthAccountURLProtocol.self]
let transport = CodexAuthenticatedHTTPTransport.makeClient(configuration: configuration)
let (refreshed, depleted) = try await CodexAuthenticatedHTTPTransport.$overrideForTesting
.withValue(transport) {
let refreshed = try await CodexOAuthUsageFetcher.fetchUsage(
accessToken: "token-a",
accountId: "account-a",
env: ["CODEX_HOME": "/tmp/codexbar-oauth-request-test"])
let depleted = try await CodexOAuthUsageFetcher.fetchUsage(
accessToken: "token-b",
accountId: "account-b",
env: ["CODEX_HOME": "/tmp/codexbar-oauth-request-test"])
return (refreshed, depleted)
}
#expect(refreshed.rateLimit?.primaryWindow?.usedPercent == 7)
#expect(refreshed.rateLimit?.secondaryWindow?.usedPercent == 9)
#expect(depleted.rateLimit?.primaryWindow?.usedPercent == 100)
#expect(depleted.rateLimit?.secondaryWindow?.usedPercent == 63)
#expect(depleted.additionalRateLimits?.first?.rateLimit?.primaryWindow?.usedPercent == 4)
let requests = CodexOAuthAccountURLProtocol.recordedRequests
#expect(requests.count == 2)
#expect(requests.allSatisfy { $0.cachePolicy == .reloadIgnoringLocalCacheData })
#expect(requests.map { $0.value(forHTTPHeaderField: "ChatGPT-Account-Id") } == ["account-a", "account-b"])
}
#if os(macOS)
@MainActor
@Test
func `dashboard cookie requests fetch distinct cacheable responses`() async {
defer { CodexOAuthAccountURLProtocol.reset() }
CodexOAuthAccountURLProtocol.reset()
let configuration = CodexAuthenticatedHTTPTransport.makeConfiguration()
configuration.protocolClasses = [CodexOAuthAccountURLProtocol.self]
let transport = CodexAuthenticatedHTTPTransport.makeClient(configuration: configuration)
let (usageA, usageB) = await CodexAuthenticatedHTTPTransport.$overrideForTesting
.withValue(transport) {
let usageA = await OpenAIDashboardFetcher.fetchDashboardUsageAPI(
cookieHeader: "session=a",
deadline: nil,
logger: { _ in })
let usageB = await OpenAIDashboardFetcher.fetchDashboardUsageAPI(
cookieHeader: "session=b",
deadline: nil,
logger: { _ in })
return (usageA, usageB)
}
#expect(usageA?.primaryLimit?.usedPercent == 7)
#expect(usageB?.primaryLimit?.usedPercent == 100)
let requests = CodexOAuthAccountURLProtocol.recordedRequests
#expect(requests.count == 2)
#expect(requests.allSatisfy { $0.cachePolicy == .reloadIgnoringLocalCacheData })
#expect(requests.map { $0.value(forHTTPHeaderField: "Cookie") } == ["session=a", "session=b"])
}
@MainActor
@Test
func `dashboard and cookie importer identity calls use isolated transport`() async throws {
defer { CodexOAuthAccountURLProtocol.reset() }
CodexOAuthAccountURLProtocol.reset()
let configuration = CodexAuthenticatedHTTPTransport.makeConfiguration()
configuration.protocolClasses = [CodexOAuthAccountURLProtocol.self]
let transport = CodexAuthenticatedHTTPTransport.makeClient(configuration: configuration)
let cookie = try #require(HTTPCookie(properties: [
.domain: "chatgpt.com",
.path: "/",
.name: "session",
.value: "a",
]))
let (dashboardEmail, importerEmail) = try await CodexAuthenticatedHTTPTransport.$overrideForTesting
.withValue(transport) {
let dashboardEmail = await OpenAIDashboardFetcher.fetchSignedInEmailFromAPI(
cookieHeader: "session=a",
deadline: nil,
logger: { _ in })
let importer = OpenAIDashboardBrowserCookieImporter(browserDetection: BrowserDetection(cacheTTL: 0))
let importerEmail = try await importer.fetchSignedInEmailFromAPI(
cookies: [cookie],
deadline: nil,
logger: { _ in })
return (dashboardEmail, importerEmail)
}
#expect(dashboardEmail == "account-a@example.com")
#expect(importerEmail == "account-a@example.com")
let requests = CodexOAuthAccountURLProtocol.recordedRequests
#expect(requests.count == 2)
#expect(requests.allSatisfy { $0.cachePolicy == .reloadIgnoringLocalCacheData })
#expect(requests.allSatisfy { $0.url?.path == "/backend-api/me" })
}
#endif
@Test
func `token refresh request uses isolated cache policy`() async throws {
let transport = ProviderHTTPTransportStub { request in
#expect(request.url?.absoluteString == "https://auth.openai.com/oauth/token")
#expect(request.httpMethod == "POST")
#expect(request.cachePolicy == .reloadIgnoringLocalCacheData)
let url = try #require(request.url)
let response = try #require(HTTPURLResponse(
url: url,
statusCode: 200,
httpVersion: nil,
headerFields: ["Cache-Control": "public, max-age=300"]))
return (Data(#"{"access_token":"new-a","refresh_token":"new-r"}"#.utf8), response)
}
let credentials = CodexOAuthCredentials(
accessToken: "old-a",
refreshToken: "old-r",
idToken: nil,
accountId: "account-a",
lastRefresh: nil)
let refreshed = try await CodexAuthenticatedHTTPTransport.$overrideForTesting.withValue(transport) {
try await CodexTokenRefresher.refresh(credentials)
}
#expect(refreshed.accessToken == "new-a")
#expect(refreshed.refreshToken == "new-r")
#expect(await transport.requests().count == 1)
}
}
private final class CodexOAuthAccountURLProtocol: URLProtocol {
private(set) nonisolated(unsafe) static var recordedRequests: [URLRequest] = []
static func reset() {
self.recordedRequests = []
}
override static func canInit(with _: URLRequest) -> Bool {
true
}
override static func canonicalRequest(for request: URLRequest) -> URLRequest {
request
}
override func startLoading() {
Self.recordedRequests.append(self.request)
guard let url = self.request.url else {
self.client?.urlProtocol(self, didFailWithError: URLError(.badURL))
return
}
let accountKey = self.request.value(forHTTPHeaderField: "ChatGPT-Account-Id")
?? self.request.value(forHTTPHeaderField: "Cookie")
if self.request.url?.path == "/backend-api/me" {
let email = accountKey == "session=a" ? "account-a@example.com" : "account-b@example.com"
guard let response = HTTPURLResponse(
url: url,
statusCode: 200,
httpVersion: nil,
headerFields: ["Cache-Control": "public, max-age=300"])
else {
self.client?.urlProtocol(self, didFailWithError: URLError(.badServerResponse))
return
}
self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed)
self.client?.urlProtocol(self, didLoad: Data(#"{"email":"\#(email)"}"#.utf8))
self.client?.urlProtocolDidFinishLoading(self)
return
}
let payload: String? = switch accountKey {
case "account-a", "session=a": Self.payload(primary: 7, secondary: 9, spark: 2)
case "account-b", "session=b": Self.payload(primary: 100, secondary: 63, spark: 4)
default: nil
}
guard let payload,
let response = HTTPURLResponse(
url: url,
statusCode: 200,
httpVersion: nil,
headerFields: ["Cache-Control": "public, max-age=300"])
else {
self.client?.urlProtocol(self, didFailWithError: URLError(.userAuthenticationRequired))
return
}
self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed)
self.client?.urlProtocol(self, didLoad: Data(payload.utf8))
self.client?.urlProtocolDidFinishLoading(self)
}
override func stopLoading() {}
private static func payload(primary: Int, secondary: Int, spark: Int) -> String {
"""
{
"rate_limit": {
"primary_window": {
"used_percent": \(primary), "reset_at": 1766948068, "limit_window_seconds": 18000
},
"secondary_window": {
"used_percent": \(secondary), "reset_at": 1767407914, "limit_window_seconds": 604800
}
},
"additional_rate_limits": [{
"limit_name": "GPT-5.3-Codex-Spark",
"rate_limit": {
"primary_window": {
"used_percent": \(spark), "reset_at": 1766948068, "limit_window_seconds": 18000
}
}
}]
}
"""
}
}