437 lines
17 KiB
Swift
437 lines
17 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import CodexBarCore
|
|
|
|
@Suite(.serialized)
|
|
@MainActor
|
|
struct CodexWebDashboardStrategyAuthorityTests {
|
|
@Test
|
|
func `web dashboard attach converts snapshot with authority attachment email`() throws {
|
|
OpenAIDashboardCacheStore.clear()
|
|
defer { OpenAIDashboardCacheStore.clear() }
|
|
|
|
let authHome = try self.makeAuthHome(
|
|
email: "owner@example.com",
|
|
accountId: "acct-owner")
|
|
defer { try? FileManager.default.removeItem(at: authHome) }
|
|
|
|
let context = self.makeContext(
|
|
authHome: authHome,
|
|
knownOwners: [
|
|
CodexDashboardKnownOwnerCandidate(
|
|
identity: .providerAccount(id: "acct-owner"),
|
|
normalizedEmail: "owner@example.com"),
|
|
])
|
|
let result = try CodexWebDashboardStrategy.makeAuthorizedDashboardResultForTesting(
|
|
dashboard: self.makeDashboard(email: "owner@example.com"),
|
|
context: context,
|
|
routingTargetEmail: "route@example.com")
|
|
|
|
#expect(result.usage.accountEmail(for: .codex) == "owner@example.com")
|
|
#expect(result.credits?.remaining == 42)
|
|
}
|
|
|
|
@Test
|
|
func `web dashboard attach preserves credits when usage limits are absent`() throws {
|
|
OpenAIDashboardCacheStore.clear()
|
|
defer { OpenAIDashboardCacheStore.clear() }
|
|
|
|
let authHome = try self.makeAuthHome(
|
|
email: "owner@example.com",
|
|
accountId: "acct-owner")
|
|
defer { try? FileManager.default.removeItem(at: authHome) }
|
|
|
|
let context = self.makeContext(
|
|
authHome: authHome,
|
|
knownOwners: [
|
|
CodexDashboardKnownOwnerCandidate(
|
|
identity: .providerAccount(id: "acct-owner"),
|
|
normalizedEmail: "owner@example.com"),
|
|
])
|
|
let dashboard = self.makeDashboardWithoutUsageLimits(email: "owner@example.com")
|
|
|
|
let result = try CodexWebDashboardStrategy.makeAuthorizedDashboardResultForTesting(
|
|
dashboard: dashboard,
|
|
context: context,
|
|
routingTargetEmail: "route@example.com")
|
|
|
|
#expect(result.usage.primary == nil)
|
|
#expect(result.usage.secondary == nil)
|
|
#expect(result.usage.updatedAt == dashboard.updatedAt)
|
|
#expect(result.usage.identity?.accountEmail == "owner@example.com")
|
|
#expect(result.usage.identity?.loginMethod == "pro")
|
|
#expect(result.credits?.remaining == 42)
|
|
#expect(result.dashboard == dashboard)
|
|
}
|
|
|
|
@Test
|
|
func `web dashboard display only throws typed policy error`() throws {
|
|
OpenAIDashboardCacheStore.clear()
|
|
defer { OpenAIDashboardCacheStore.clear() }
|
|
|
|
let authHome = try self.makeAuthHome(email: "shared@example.com")
|
|
defer { try? FileManager.default.removeItem(at: authHome) }
|
|
|
|
let context = self.makeContext(
|
|
authHome: authHome,
|
|
knownOwners: [
|
|
CodexDashboardKnownOwnerCandidate(
|
|
identity: .providerAccount(id: "acct-alpha"),
|
|
normalizedEmail: "shared@example.com"),
|
|
CodexDashboardKnownOwnerCandidate(
|
|
identity: .providerAccount(id: "acct-beta"),
|
|
normalizedEmail: "shared@example.com"),
|
|
])
|
|
let dashboard = self.makeDashboard(email: "shared@example.com")
|
|
let expectedDecision = CodexDashboardAuthority.evaluate(
|
|
CodexCLIDashboardAuthorityContext.makeLiveWebInput(
|
|
dashboard: dashboard,
|
|
context: context,
|
|
routingTargetEmail: "route@example.com"))
|
|
|
|
do {
|
|
_ = try CodexWebDashboardStrategy.makeAuthorizedDashboardResultForTesting(
|
|
dashboard: dashboard,
|
|
context: context,
|
|
routingTargetEmail: "route@example.com")
|
|
Issue.record("Expected CodexDashboardPolicyError.displayOnly")
|
|
} catch let error as CodexDashboardPolicyError {
|
|
#expect(error == .displayOnly(expectedDecision))
|
|
} catch {
|
|
Issue.record("Expected CodexDashboardPolicyError.displayOnly, got \(error)")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `web dashboard fail closed throws policy rejection`() throws {
|
|
OpenAIDashboardCacheStore.clear()
|
|
defer { OpenAIDashboardCacheStore.clear() }
|
|
|
|
let authHome = try self.makeAuthHome(
|
|
email: "owner@example.com",
|
|
accountId: "acct-owner")
|
|
defer { try? FileManager.default.removeItem(at: authHome) }
|
|
|
|
let context = self.makeContext(
|
|
authHome: authHome,
|
|
knownOwners: [
|
|
CodexDashboardKnownOwnerCandidate(
|
|
identity: .providerAccount(id: "acct-other"),
|
|
normalizedEmail: "owner@example.com"),
|
|
])
|
|
let dashboard = self.makeDashboard(email: "owner@example.com")
|
|
let expectedDecision = CodexDashboardAuthority.evaluate(
|
|
CodexCLIDashboardAuthorityContext.makeLiveWebInput(
|
|
dashboard: dashboard,
|
|
context: context,
|
|
routingTargetEmail: "route@example.com"))
|
|
|
|
do {
|
|
_ = try CodexWebDashboardStrategy.makeAuthorizedDashboardResultForTesting(
|
|
dashboard: dashboard,
|
|
context: context,
|
|
routingTargetEmail: "route@example.com")
|
|
Issue.record("Expected OpenAIWebCodexError.policyRejected")
|
|
} catch let error as OpenAIWebCodexError {
|
|
#expect(error == .policyRejected(expectedDecision))
|
|
} catch {
|
|
Issue.record("Expected OpenAIWebCodexError.policyRejected, got \(error)")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `web dashboard wrong email throws policy rejection`() throws {
|
|
OpenAIDashboardCacheStore.clear()
|
|
defer { OpenAIDashboardCacheStore.clear() }
|
|
|
|
let authHome = try self.makeAuthHome(
|
|
email: "owner@example.com",
|
|
accountId: "acct-owner")
|
|
defer { try? FileManager.default.removeItem(at: authHome) }
|
|
|
|
let context = self.makeContext(
|
|
authHome: authHome,
|
|
knownOwners: [
|
|
CodexDashboardKnownOwnerCandidate(
|
|
identity: .providerAccount(id: "acct-owner"),
|
|
normalizedEmail: "owner@example.com"),
|
|
])
|
|
let dashboard = self.makeDashboard(email: "other@example.com")
|
|
let expectedDecision = CodexDashboardAuthority.evaluate(
|
|
CodexCLIDashboardAuthorityContext.makeLiveWebInput(
|
|
dashboard: dashboard,
|
|
context: context,
|
|
routingTargetEmail: "owner@example.com"))
|
|
|
|
do {
|
|
_ = try CodexWebDashboardStrategy.makeAuthorizedDashboardResultForTesting(
|
|
dashboard: dashboard,
|
|
context: context,
|
|
routingTargetEmail: "owner@example.com")
|
|
Issue.record("Expected OpenAIWebCodexError.policyRejected")
|
|
} catch let error as OpenAIWebCodexError {
|
|
#expect(error == .policyRejected(expectedDecision))
|
|
if case let .policyRejected(decision) = error {
|
|
#expect(decision.reason == .wrongEmail(expected: "owner@example.com", actual: "other@example.com"))
|
|
}
|
|
} catch {
|
|
Issue.record("Expected OpenAIWebCodexError.policyRejected, got \(error)")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `web dashboard provider account without scoped auth email fail closes on dashboard collision`() throws {
|
|
OpenAIDashboardCacheStore.clear()
|
|
defer { OpenAIDashboardCacheStore.clear() }
|
|
|
|
let authHome = try self.makeAuthHome(email: nil, accountId: "acct-owner")
|
|
defer { try? FileManager.default.removeItem(at: authHome) }
|
|
|
|
let context = self.makeContext(
|
|
authHome: authHome,
|
|
knownOwners: [
|
|
CodexDashboardKnownOwnerCandidate(
|
|
identity: .providerAccount(id: "acct-alpha"),
|
|
normalizedEmail: "shared@example.com"),
|
|
CodexDashboardKnownOwnerCandidate(
|
|
identity: .providerAccount(id: "acct-beta"),
|
|
normalizedEmail: "shared@example.com"),
|
|
])
|
|
let dashboard = self.makeDashboard(email: "shared@example.com")
|
|
let expectedDecision = CodexDashboardAuthority.evaluate(
|
|
CodexCLIDashboardAuthorityContext.makeLiveWebInput(
|
|
dashboard: dashboard,
|
|
context: context,
|
|
routingTargetEmail: "shared@example.com"))
|
|
|
|
do {
|
|
_ = try CodexWebDashboardStrategy.makeAuthorizedDashboardResultForTesting(
|
|
dashboard: dashboard,
|
|
context: context,
|
|
routingTargetEmail: "shared@example.com")
|
|
Issue.record("Expected OpenAIWebCodexError.policyRejected")
|
|
} catch let error as OpenAIWebCodexError {
|
|
#expect(error == .policyRejected(expectedDecision))
|
|
if case let .policyRejected(decision) = error {
|
|
#expect(decision.reason == .providerAccountMissingScopedEmail)
|
|
}
|
|
} catch {
|
|
Issue.record("Expected OpenAIWebCodexError.policyRejected, got \(error)")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `web dashboard attach saves cache with attached email not routing fallback`() throws {
|
|
OpenAIDashboardCacheStore.clear()
|
|
defer { OpenAIDashboardCacheStore.clear() }
|
|
|
|
let authHome = try self.makeAuthHome(
|
|
email: "owner@example.com",
|
|
accountId: "acct-owner")
|
|
defer { try? FileManager.default.removeItem(at: authHome) }
|
|
|
|
let context = self.makeContext(
|
|
authHome: authHome,
|
|
knownOwners: [
|
|
CodexDashboardKnownOwnerCandidate(
|
|
identity: .providerAccount(id: "acct-owner"),
|
|
normalizedEmail: "owner@example.com"),
|
|
])
|
|
|
|
_ = try CodexWebDashboardStrategy.makeAuthorizedDashboardResultForTesting(
|
|
dashboard: self.makeDashboard(email: "owner@example.com"),
|
|
context: context,
|
|
routingTargetEmail: "route@example.com")
|
|
|
|
let cache = try #require(OpenAIDashboardCacheStore.load())
|
|
#expect(cache.accountEmail == "owner@example.com")
|
|
#expect(cache.accountEmail != "route@example.com")
|
|
}
|
|
|
|
@Test
|
|
func `web dashboard fail closed clears stale cache`() throws {
|
|
OpenAIDashboardCacheStore.save(OpenAIDashboardCache(
|
|
accountEmail: "stale@example.com",
|
|
snapshot: self.makeDashboard(email: "stale@example.com")))
|
|
defer { OpenAIDashboardCacheStore.clear() }
|
|
|
|
let authHome = try self.makeAuthHome(
|
|
email: "owner@example.com",
|
|
accountId: "acct-owner")
|
|
defer { try? FileManager.default.removeItem(at: authHome) }
|
|
|
|
let context = self.makeContext(
|
|
authHome: authHome,
|
|
knownOwners: [
|
|
CodexDashboardKnownOwnerCandidate(
|
|
identity: .providerAccount(id: "acct-other"),
|
|
normalizedEmail: "owner@example.com"),
|
|
])
|
|
|
|
do {
|
|
_ = try CodexWebDashboardStrategy.makeAuthorizedDashboardResultForTesting(
|
|
dashboard: self.makeDashboard(email: "owner@example.com"),
|
|
context: context,
|
|
routingTargetEmail: "route@example.com")
|
|
Issue.record("Expected OpenAIWebCodexError.policyRejected")
|
|
} catch is OpenAIWebCodexError {}
|
|
|
|
#expect(OpenAIDashboardCacheStore.load() == nil)
|
|
}
|
|
|
|
@Test
|
|
func `web dashboard display only clears stale cache`() throws {
|
|
OpenAIDashboardCacheStore.save(OpenAIDashboardCache(
|
|
accountEmail: "stale@example.com",
|
|
snapshot: self.makeDashboard(email: "stale@example.com")))
|
|
defer { OpenAIDashboardCacheStore.clear() }
|
|
|
|
let authHome = try self.makeAuthHome(email: "shared@example.com")
|
|
defer { try? FileManager.default.removeItem(at: authHome) }
|
|
|
|
let context = self.makeContext(
|
|
authHome: authHome,
|
|
knownOwners: [
|
|
CodexDashboardKnownOwnerCandidate(
|
|
identity: .providerAccount(id: "acct-alpha"),
|
|
normalizedEmail: "shared@example.com"),
|
|
CodexDashboardKnownOwnerCandidate(
|
|
identity: .providerAccount(id: "acct-beta"),
|
|
normalizedEmail: "shared@example.com"),
|
|
])
|
|
|
|
do {
|
|
_ = try CodexWebDashboardStrategy.makeAuthorizedDashboardResultForTesting(
|
|
dashboard: self.makeDashboard(email: "shared@example.com"),
|
|
context: context,
|
|
routingTargetEmail: "route@example.com")
|
|
Issue.record("Expected CodexDashboardPolicyError.displayOnly")
|
|
} catch is CodexDashboardPolicyError {}
|
|
|
|
#expect(OpenAIDashboardCacheStore.load() == nil)
|
|
}
|
|
|
|
private func makeContext(
|
|
authHome: URL? = nil,
|
|
knownOwners: [CodexDashboardKnownOwnerCandidate]) -> ProviderFetchContext
|
|
{
|
|
let env = authHome.map { ["CODEX_HOME": $0.path] } ?? [:]
|
|
let browserDetection = BrowserDetection(cacheTTL: 0)
|
|
return ProviderFetchContext(
|
|
runtime: .cli,
|
|
sourceMode: .auto,
|
|
includeCredits: true,
|
|
webTimeout: 60,
|
|
webDebugDumpHTML: false,
|
|
verbose: false,
|
|
env: env,
|
|
settings: ProviderSettingsSnapshot.make(
|
|
codex: .init(
|
|
usageDataSource: .auto,
|
|
cookieSource: .auto,
|
|
manualCookieHeader: nil,
|
|
dashboardAuthorityKnownOwners: knownOwners)),
|
|
fetcher: UsageFetcher(environment: env),
|
|
claudeFetcher: ClaudeUsageFetcher(browserDetection: browserDetection),
|
|
browserDetection: browserDetection)
|
|
}
|
|
|
|
private func makeDashboard(email: String) -> OpenAIDashboardSnapshot {
|
|
OpenAIDashboardSnapshot(
|
|
signedInEmail: email,
|
|
codeReviewRemainingPercent: 75,
|
|
codeReviewLimit: RateWindow(
|
|
usedPercent: 25,
|
|
windowMinutes: 60,
|
|
resetsAt: Date(timeIntervalSince1970: 3600),
|
|
resetDescription: nil),
|
|
creditEvents: [
|
|
CreditEvent(
|
|
date: Date(timeIntervalSince1970: 1000),
|
|
service: "codex",
|
|
creditsUsed: 3),
|
|
],
|
|
dailyBreakdown: [],
|
|
usageBreakdown: [],
|
|
creditsPurchaseURL: nil,
|
|
primaryLimit: RateWindow(
|
|
usedPercent: 10,
|
|
windowMinutes: 300,
|
|
resetsAt: Date(timeIntervalSince1970: 7200),
|
|
resetDescription: nil),
|
|
secondaryLimit: nil,
|
|
creditsRemaining: 42,
|
|
accountPlan: "pro",
|
|
updatedAt: Date(timeIntervalSince1970: 2000))
|
|
}
|
|
|
|
private func makeDashboardWithoutUsageLimits(email: String) -> OpenAIDashboardSnapshot {
|
|
OpenAIDashboardSnapshot(
|
|
signedInEmail: email,
|
|
codeReviewRemainingPercent: nil,
|
|
creditEvents: [],
|
|
dailyBreakdown: [],
|
|
usageBreakdown: [],
|
|
creditsPurchaseURL: nil,
|
|
primaryLimit: nil,
|
|
secondaryLimit: nil,
|
|
creditsRemaining: 42,
|
|
accountPlan: "pro",
|
|
updatedAt: Date(timeIntervalSince1970: 2000))
|
|
}
|
|
|
|
private func makeAuthHome(email: String?, accountId: String? = nil) throws -> URL {
|
|
let homeURL = FileManager.default.temporaryDirectory.appendingPathComponent(
|
|
UUID().uuidString,
|
|
isDirectory: true)
|
|
try self.writeCodexAuthFile(homeURL: homeURL, email: email, accountId: accountId)
|
|
return homeURL
|
|
}
|
|
|
|
private func writeCodexAuthFile(
|
|
homeURL: URL,
|
|
email: String?,
|
|
accountId: String?) throws
|
|
{
|
|
try FileManager.default.createDirectory(at: homeURL, withIntermediateDirectories: true)
|
|
var tokens: [String: Any] = [
|
|
"accessToken": "access-token",
|
|
"refreshToken": "refresh-token",
|
|
"idToken": Self.fakeJWT(email: email, accountId: accountId),
|
|
]
|
|
if let accountId {
|
|
tokens["accountId"] = accountId
|
|
}
|
|
let auth = ["tokens": tokens]
|
|
let data = try JSONSerialization.data(withJSONObject: auth)
|
|
try data.write(to: homeURL.appendingPathComponent("auth.json"))
|
|
}
|
|
|
|
private static func fakeJWT(email: String?, accountId: String?) -> String {
|
|
let header = (try? JSONSerialization.data(withJSONObject: ["alg": "none"])) ?? Data()
|
|
var authClaims: [String: Any] = [
|
|
"chatgpt_plan_type": "pro",
|
|
]
|
|
if let accountId {
|
|
authClaims["chatgpt_account_id"] = accountId
|
|
}
|
|
var claims: [String: Any] = [
|
|
"chatgpt_plan_type": "pro",
|
|
"https://api.openai.com/auth": authClaims,
|
|
]
|
|
if let email {
|
|
claims["email"] = email
|
|
}
|
|
let payload = (try? JSONSerialization.data(withJSONObject: claims)) ?? Data()
|
|
|
|
func base64URL(_ data: Data) -> String {
|
|
data.base64EncodedString()
|
|
.replacingOccurrences(of: "=", with: "")
|
|
.replacingOccurrences(of: "+", with: "-")
|
|
.replacingOccurrences(of: "/", with: "_")
|
|
}
|
|
|
|
return "\(base64URL(header)).\(base64URL(payload))."
|
|
}
|
|
}
|