296 lines
12 KiB
Swift
296 lines
12 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import CodexBarCore
|
|
|
|
struct T3ChatUsageFetcherTests {
|
|
private struct StubClaudeFetcher: ClaudeUsageFetching {
|
|
func loadLatestUsage(model _: String) async throws -> ClaudeUsageSnapshot {
|
|
throw ClaudeUsageError.parseFailed("stub")
|
|
}
|
|
|
|
func debugRawProbe(model _: String) async -> String {
|
|
"stub"
|
|
}
|
|
|
|
func detectVersion() -> String? {
|
|
nil
|
|
}
|
|
}
|
|
|
|
private static let now = Date(timeIntervalSince1970: 1_778_000_000)
|
|
// 2026-05-21T12:23:36Z, the usage-window reset that must not drive overage reset display.
|
|
private static let billingNextResetMilliseconds = 1_779_366_216_920
|
|
// 2026-06-06T16:23:29Z, the subscription period end used for overage reset display.
|
|
private static let subscriptionPeriodEndSeconds = 1_780_763_009
|
|
private static let subscriptionPeriodEndMilliseconds = Self.subscriptionPeriodEndSeconds * 1000
|
|
|
|
private static let sampleResponse = [
|
|
#"{"json":{"0":[[0],[null,0,0]]}}"#,
|
|
#"{"json":[0,0,[[{"result":0}],["result",0,1]]]}"#,
|
|
#"{"json":[1,0,[[{"data":0}],["data",0,2]]]}"#,
|
|
#"{"json":[2,0,[[{"subTier":"pro","subscription":{"# +
|
|
#""productId":"pro","productName":"pro","status":"active","# +
|
|
#""currentPeriodStart":1778084609000,"currentPeriodEnd":1780763009000,"# +
|
|
#""canceledAt":null,"trialEndsAt":null},"lifetimeBalance":0,"usageBand":"max","# +
|
|
#""billingNextResetAt":1779366216920,"usageFourHourPercentage":12.5,"# +
|
|
#""usageMonthPercentage":34.25,"usageFourHourNextResetAt":1779366216920,"# +
|
|
#""usagePeriodPercentage":44,"usageWindowNextResetAt":1779366216920}]]]}"#,
|
|
].joined(separator: "\n")
|
|
|
|
@Test
|
|
func `parses customer data from json lines response`() throws {
|
|
let snapshot = try T3ChatUsageParser.parseJSONLines(Self.sampleResponse, now: Self.now)
|
|
|
|
#expect(snapshot.customerData.subTier == "pro")
|
|
#expect(snapshot.customerData.usageBand == "max")
|
|
#expect(snapshot.customerData.usageFourHourPercentage == 12.5)
|
|
#expect(snapshot.customerData.usageMonthPercentage == 34.25)
|
|
#expect(snapshot.customerData.subscription?.status == "active")
|
|
}
|
|
|
|
@Test
|
|
func `maps customer data to base and overage windows`() throws {
|
|
let usage = try T3ChatUsageParser.parseJSONLines(Self.sampleResponse, now: Self.now)
|
|
.toUsageSnapshot()
|
|
|
|
#expect(usage.primary?.usedPercent == 12.5)
|
|
#expect(usage.primary?.windowMinutes == 240)
|
|
#expect(usage.primary?.resetDescription == "Base - max")
|
|
#expect(usage.secondary?.usedPercent == 34.25)
|
|
#expect(usage.secondary?.resetDescription == "Overage")
|
|
#expect(usage.secondary?.resetsAt.map { Int($0.timeIntervalSince1970) } == Self.subscriptionPeriodEndSeconds)
|
|
#expect(usage.identity?.providerID == .t3chat)
|
|
#expect(usage.identity?.loginMethod == "Pro")
|
|
}
|
|
|
|
@Test
|
|
func `falls back to usage period percentage when month percentage is absent`() throws {
|
|
let response = """
|
|
{"json":[2,0,[[{"subTier":"free","usageFourHourPercentage":5,"usagePeriodPercentage":65}]]]}
|
|
"""
|
|
let usage = try T3ChatUsageParser.parseJSONLines(response, now: Self.now)
|
|
.toUsageSnapshot()
|
|
|
|
#expect(usage.primary?.usedPercent == 5)
|
|
#expect(usage.secondary?.usedPercent == 65)
|
|
}
|
|
|
|
@Test
|
|
func `overage reset ignores billing next reset`() throws {
|
|
let response = Self.customerDataResponse(
|
|
#"{"usageMonthPercentage":20,"billingNextResetAt":\#(Self.billingNextResetMilliseconds)}"#)
|
|
let usage = try T3ChatUsageParser.parseJSONLines(response, now: Self.now)
|
|
.toUsageSnapshot()
|
|
|
|
#expect(usage.secondary?.usedPercent == 20)
|
|
#expect(usage.secondary?.resetsAt == nil)
|
|
}
|
|
|
|
@Test
|
|
func `overage reset uses subscription current period end`() throws {
|
|
let currentPeriodEnd = Self.subscriptionPeriodEndMilliseconds
|
|
let response = Self.customerDataResponse(
|
|
#"{"usageMonthPercentage":20,"subscription":{"currentPeriodEnd":\#(currentPeriodEnd)}}"#)
|
|
let usage = try T3ChatUsageParser.parseJSONLines(response, now: Self.now)
|
|
.toUsageSnapshot()
|
|
|
|
#expect(usage.secondary?.usedPercent == 20)
|
|
#expect(usage.secondary?.resetsAt.map { Int($0.timeIntervalSince1970) } == Self.subscriptionPeriodEndSeconds)
|
|
}
|
|
|
|
@Test
|
|
func `fetch sends trpc headers and cookie`() async throws {
|
|
let stub = ProviderHTTPTransportStub { request in
|
|
#expect(request.url?.host == "t3.chat")
|
|
#expect(request.url?.path == "/api/trpc/getCustomerData")
|
|
#expect(request.value(forHTTPHeaderField: "Cookie") == "session=abc")
|
|
#expect(request.value(forHTTPHeaderField: "trpc-accept") == "application/jsonl")
|
|
#expect(request.value(forHTTPHeaderField: "x-trpc-source") == "web-client")
|
|
#expect(request.value(forHTTPHeaderField: "Sec-Fetch-Site") == "same-origin")
|
|
#expect(request.url?.query?.contains("batch=1") == true)
|
|
let response = HTTPURLResponse(
|
|
url: request.url!,
|
|
statusCode: 200,
|
|
httpVersion: nil,
|
|
headerFields: nil)!
|
|
return (Data(Self.sampleResponse.utf8), response)
|
|
}
|
|
|
|
let snapshot = try await T3ChatUsageFetcher.fetchCustomerData(
|
|
cookieHeader: "session=abc",
|
|
now: Self.now,
|
|
transport: stub)
|
|
|
|
#expect(snapshot.customerData.planName == "Pro")
|
|
}
|
|
|
|
@Test
|
|
func `full curl capture forwards browser fingerprint headers`() async throws {
|
|
let curl = """
|
|
curl 'https://t3.chat/api/trpc/getCustomerData?batch=1&input=ignored' \\
|
|
-H 'User-Agent: Mozilla/5.0 Firefox/151.0' \\
|
|
--header "Referer: https://t3.chat/settings/customization" \\
|
|
-H 'trpc-accept: application/jsonl' \\
|
|
-H 'x-trpc-source: web-client' \\
|
|
-H 'x-trpc-batch: true' \\
|
|
-H 'X-Deployment-Id: dpl_test' \\
|
|
-H 'x-client-context: eyJjbGllbnQiOnsidmVyc2lvbiI6IjEuMTIuNCJ9fQ==' \\
|
|
-H 'Cookie: session=abc'
|
|
"""
|
|
let stub = ProviderHTTPTransportStub { request in
|
|
#expect(request.value(forHTTPHeaderField: "Cookie") == "session=abc")
|
|
#expect(request.value(forHTTPHeaderField: "User-Agent") == "Mozilla/5.0 Firefox/151.0")
|
|
#expect(request.value(forHTTPHeaderField: "Referer") == "https://t3.chat/settings/customization")
|
|
#expect(request.value(forHTTPHeaderField: "X-Deployment-Id") == "dpl_test")
|
|
#expect(request.value(forHTTPHeaderField: "x-client-context") ==
|
|
"eyJjbGllbnQiOnsidmVyc2lvbiI6IjEuMTIuNCJ9fQ==")
|
|
let response = HTTPURLResponse(
|
|
url: request.url!,
|
|
statusCode: 200,
|
|
httpVersion: nil,
|
|
headerFields: nil)!
|
|
return (Data(Self.sampleResponse.utf8), response)
|
|
}
|
|
|
|
let fetcher = T3ChatUsageFetcher(browserDetection: BrowserDetection(cacheTTL: 0))
|
|
_ = try await fetcher.fetch(
|
|
cookieHeaderOverride: curl,
|
|
now: Self.now,
|
|
transport: stub)
|
|
}
|
|
|
|
@Test
|
|
func `curl capture forwards ansi quoted and equals header forms`() async throws {
|
|
let curl = """
|
|
curl 'https://t3.chat/api/trpc/getCustomerData?batch=1&input=ignored' \\
|
|
--header=$'User-Agent: Browser\\'s Agent' \\
|
|
--header 'X-Deployment-Id: dpl_test' \\
|
|
-H 'Cookie: session=abc'
|
|
"""
|
|
let stub = ProviderHTTPTransportStub { request in
|
|
#expect(request.value(forHTTPHeaderField: "Cookie") == "session=abc")
|
|
#expect(request.value(forHTTPHeaderField: "User-Agent") == "Browser's Agent")
|
|
#expect(request.value(forHTTPHeaderField: "X-Deployment-Id") == "dpl_test")
|
|
let response = HTTPURLResponse(
|
|
url: request.url!,
|
|
statusCode: 200,
|
|
httpVersion: nil,
|
|
headerFields: nil)!
|
|
return (Data(Self.sampleResponse.utf8), response)
|
|
}
|
|
|
|
let fetcher = T3ChatUsageFetcher(browserDetection: BrowserDetection(cacheTTL: 0))
|
|
_ = try await fetcher.fetch(
|
|
cookieHeaderOverride: curl,
|
|
now: Self.now,
|
|
transport: stub)
|
|
}
|
|
|
|
@Test
|
|
func `full curl capture extracts cookie from long header form`() async throws {
|
|
let curl = """
|
|
curl 'https://t3.chat/api/trpc/getCustomerData?batch=1&input=ignored' \\
|
|
--compressed \\
|
|
--header "Referer: https://t3.chat/settings/customization" \\
|
|
--header "Cookie: session=abc; cf_clearance=token" \\
|
|
--header "X-Deployment-Id: dpl_test"
|
|
"""
|
|
let stub = ProviderHTTPTransportStub { request in
|
|
#expect(request.value(forHTTPHeaderField: "Cookie") == "session=abc; cf_clearance=token")
|
|
#expect(request.value(forHTTPHeaderField: "Referer") == "https://t3.chat/settings/customization")
|
|
#expect(request.value(forHTTPHeaderField: "X-Deployment-Id") == "dpl_test")
|
|
let response = HTTPURLResponse(
|
|
url: request.url!,
|
|
statusCode: 200,
|
|
httpVersion: nil,
|
|
headerFields: nil)!
|
|
return (Data(Self.sampleResponse.utf8), response)
|
|
}
|
|
|
|
let fetcher = T3ChatUsageFetcher(browserDetection: BrowserDetection(cacheTTL: 0))
|
|
_ = try await fetcher.fetch(
|
|
cookieHeaderOverride: curl,
|
|
now: Self.now,
|
|
transport: stub)
|
|
}
|
|
|
|
@Test
|
|
func `manual strategy accepts full curl capture`() async {
|
|
let curl = """
|
|
curl 'https://t3.chat/api/trpc/getCustomerData?batch=1&input=ignored' \\
|
|
--header "Referer: https://t3.chat/settings/customization" \\
|
|
--header "Cookie: session=abc; cf_clearance=token" \\
|
|
--header "X-Deployment-Id: dpl_test"
|
|
"""
|
|
let settings = ProviderSettingsSnapshot.make(
|
|
t3chat: ProviderSettingsSnapshot.T3ChatProviderSettings(
|
|
cookieSource: .manual,
|
|
manualCookieHeader: curl))
|
|
|
|
#expect(await T3ChatWebFetchStrategy().isAvailable(Self.makeContext(settings: settings)))
|
|
}
|
|
|
|
@Test
|
|
func `unauthorized response is invalid credentials`() async throws {
|
|
let stub = ProviderHTTPTransportStub { request in
|
|
let response = HTTPURLResponse(
|
|
url: request.url!,
|
|
statusCode: 401,
|
|
httpVersion: nil,
|
|
headerFields: nil)!
|
|
return (Data("unauthorized".utf8), response)
|
|
}
|
|
|
|
await #expect {
|
|
_ = try await T3ChatUsageFetcher.fetchCustomerData(
|
|
cookieHeader: "session=abc",
|
|
now: Self.now,
|
|
transport: stub)
|
|
} throws: { error in
|
|
guard case T3ChatUsageError.invalidCredentials = error else { return false }
|
|
return true
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `vercel challenge response asks for full curl capture`() async throws {
|
|
let stub = ProviderHTTPTransportStub { request in
|
|
let response = HTTPURLResponse(
|
|
url: request.url!,
|
|
statusCode: 429,
|
|
httpVersion: nil,
|
|
headerFields: ["x-vercel-mitigated": "challenge"])!
|
|
return (Data("checkpoint".utf8), response)
|
|
}
|
|
|
|
await #expect {
|
|
_ = try await T3ChatUsageFetcher.fetchCustomerData(
|
|
cookieHeader: "session=abc",
|
|
now: Self.now,
|
|
transport: stub)
|
|
} throws: { error in
|
|
guard case T3ChatUsageError.vercelChallenge = error else { return false }
|
|
return true
|
|
}
|
|
}
|
|
|
|
private static func customerDataResponse(_ customerDataJSON: String) -> String {
|
|
#"{"json":[2,0,[[\#(customerDataJSON)]]]}"# + "\n"
|
|
}
|
|
|
|
private static func makeContext(settings: ProviderSettingsSnapshot) -> ProviderFetchContext {
|
|
ProviderFetchContext(
|
|
runtime: .app,
|
|
sourceMode: .auto,
|
|
includeCredits: false,
|
|
webTimeout: 1,
|
|
webDebugDumpHTML: false,
|
|
verbose: false,
|
|
env: [:],
|
|
settings: settings,
|
|
fetcher: UsageFetcher(environment: [:]),
|
|
claudeFetcher: StubClaudeFetcher(),
|
|
browserDetection: BrowserDetection(cacheTTL: 0))
|
|
}
|
|
}
|