295 lines
11 KiB
Swift
295 lines
11 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import CodexBarCore
|
|
|
|
@Suite(.serialized)
|
|
struct OpenRouterUsageStatsTests {
|
|
@Test
|
|
func `to usage snapshot uses key quota for primary window`() {
|
|
let snapshot = OpenRouterUsageSnapshot(
|
|
totalCredits: 50,
|
|
totalUsage: 45.3895596325,
|
|
balance: 4.6104403675,
|
|
usedPercent: 90.779119265,
|
|
keyLimit: 20,
|
|
keyUsage: 5,
|
|
rateLimit: nil,
|
|
updatedAt: Date(timeIntervalSince1970: 1_739_841_600))
|
|
|
|
let usage = snapshot.toUsageSnapshot()
|
|
|
|
#expect(usage.primary?.usedPercent == 25)
|
|
#expect(usage.primary?.resetsAt == nil)
|
|
#expect(usage.primary?.resetDescription == nil)
|
|
#expect(usage.openRouterUsage?.keyQuotaStatus == .available)
|
|
}
|
|
|
|
@Test
|
|
func `to usage snapshot without valid key limit omits primary window`() {
|
|
let snapshot = OpenRouterUsageSnapshot(
|
|
totalCredits: 50,
|
|
totalUsage: 45.3895596325,
|
|
balance: 4.6104403675,
|
|
usedPercent: 90.779119265,
|
|
keyLimit: nil,
|
|
keyUsage: nil,
|
|
rateLimit: nil,
|
|
updatedAt: Date(timeIntervalSince1970: 1_739_841_600))
|
|
|
|
let usage = snapshot.toUsageSnapshot()
|
|
|
|
#expect(usage.primary == nil)
|
|
#expect(usage.openRouterUsage?.keyQuotaStatus == .unavailable)
|
|
}
|
|
|
|
@Test
|
|
func `to usage snapshot when no limit configured omits primary and marks no limit`() {
|
|
let snapshot = OpenRouterUsageSnapshot(
|
|
totalCredits: 50,
|
|
totalUsage: 45.3895596325,
|
|
balance: 4.6104403675,
|
|
usedPercent: 90.779119265,
|
|
keyDataFetched: true,
|
|
keyLimit: nil,
|
|
keyUsage: nil,
|
|
rateLimit: nil,
|
|
updatedAt: Date(timeIntervalSince1970: 1_739_841_600))
|
|
|
|
let usage = snapshot.toUsageSnapshot()
|
|
|
|
#expect(usage.primary == nil)
|
|
#expect(usage.openRouterUsage?.keyQuotaStatus == .noLimitConfigured)
|
|
}
|
|
|
|
@Test
|
|
func `sanitizers redact sensitive token shapes`() {
|
|
let body = """
|
|
{"error":"bad token sk-or-v1-abc123","token":"secret-token","authorization":"Bearer sk-or-v1-xyz789"}
|
|
"""
|
|
|
|
let summary = OpenRouterUsageFetcher._sanitizedResponseBodySummaryForTesting(body)
|
|
let debugBody = OpenRouterUsageFetcher._redactedDebugResponseBodyForTesting(body)
|
|
|
|
#expect(summary.contains("sk-or-v1-[REDACTED]"))
|
|
#expect(summary.contains("\"token\":\"[REDACTED]\""))
|
|
#expect(!summary.contains("secret-token"))
|
|
#expect(!summary.contains("sk-or-v1-abc123"))
|
|
|
|
#expect(debugBody?.contains("sk-or-v1-[REDACTED]") == true)
|
|
#expect(debugBody?.contains("\"token\":\"[REDACTED]\"") == true)
|
|
#expect(debugBody?.contains("secret-token") == false)
|
|
#expect(debugBody?.contains("sk-or-v1-xyz789") == false)
|
|
}
|
|
|
|
@Test
|
|
func `non200 fetch throws generic HTTP error without body details`() async throws {
|
|
let registered = URLProtocol.registerClass(OpenRouterStubURLProtocol.self)
|
|
defer {
|
|
if registered {
|
|
URLProtocol.unregisterClass(OpenRouterStubURLProtocol.self)
|
|
}
|
|
OpenRouterStubURLProtocol.handler = nil
|
|
}
|
|
|
|
OpenRouterStubURLProtocol.handler = { request in
|
|
guard let url = request.url else { throw URLError(.badURL) }
|
|
let body = #"{"error":"invalid sk-or-v1-super-secret","token":"dont-leak-me"}"#
|
|
return Self.makeResponse(url: url, body: body, statusCode: 401)
|
|
}
|
|
|
|
do {
|
|
_ = try await OpenRouterUsageFetcher.fetchUsage(
|
|
apiKey: "sk-or-v1-test",
|
|
environment: ["OPENROUTER_API_URL": "https://openrouter.test/api/v1"])
|
|
Issue.record("Expected OpenRouterUsageError.apiError")
|
|
} catch let error as OpenRouterUsageError {
|
|
guard case let .apiError(message) = error else {
|
|
Issue.record("Expected apiError, got: \(error)")
|
|
return
|
|
}
|
|
#expect(message == "HTTP 401")
|
|
#expect(!message.contains("dont-leak-me"))
|
|
#expect(!message.contains("sk-or-v1-super-secret"))
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `fetch usage sets credits timeout and client headers`() async throws {
|
|
let registered = URLProtocol.registerClass(OpenRouterStubURLProtocol.self)
|
|
defer {
|
|
if registered {
|
|
URLProtocol.unregisterClass(OpenRouterStubURLProtocol.self)
|
|
}
|
|
OpenRouterStubURLProtocol.handler = nil
|
|
}
|
|
|
|
OpenRouterStubURLProtocol.handler = { request in
|
|
guard let url = request.url else { throw URLError(.badURL) }
|
|
switch url.path {
|
|
case "/api/v1/credits":
|
|
#expect(request.timeoutInterval == 15)
|
|
#expect(request.value(forHTTPHeaderField: "HTTP-Referer") == "https://codexbar.example")
|
|
#expect(request.value(forHTTPHeaderField: "X-Title") == "CodexBar QA")
|
|
let body = #"{"data":{"total_credits":100,"total_usage":40}}"#
|
|
return Self.makeResponse(url: url, body: body, statusCode: 200)
|
|
case "/api/v1/key":
|
|
let body = #"""
|
|
{"data":{
|
|
"limit":20,
|
|
"usage":0.5,
|
|
"usage_daily":0.12,
|
|
"usage_weekly":0.74,
|
|
"usage_monthly":4.56,
|
|
"rate_limit":{"requests":120,"interval":"10s"}
|
|
}}
|
|
"""#
|
|
return Self.makeResponse(url: url, body: body, statusCode: 200)
|
|
default:
|
|
return Self.makeResponse(url: url, body: "{}", statusCode: 404)
|
|
}
|
|
}
|
|
|
|
let usage = try await OpenRouterUsageFetcher.fetchUsage(
|
|
apiKey: "sk-or-v1-test",
|
|
environment: [
|
|
"OPENROUTER_API_URL": "https://openrouter.test/api/v1",
|
|
"OPENROUTER_HTTP_REFERER": " https://codexbar.example ",
|
|
"OPENROUTER_X_TITLE": "CodexBar QA",
|
|
])
|
|
|
|
#expect(usage.totalCredits == 100)
|
|
#expect(usage.totalUsage == 40)
|
|
#expect(usage.keyDataFetched)
|
|
#expect(usage.keyLimit == 20)
|
|
#expect(usage.keyUsage == 0.5)
|
|
#expect(usage.keyUsageDaily == 0.12)
|
|
#expect(usage.keyUsageWeekly == 0.74)
|
|
#expect(usage.keyUsageMonthly == 4.56)
|
|
#expect(usage.keyRemaining == 19.5)
|
|
#expect(usage.keyUsedPercent == 2.5)
|
|
#expect(usage.keyQuotaStatus == .available)
|
|
}
|
|
|
|
@Test
|
|
func `fetch usage when key endpoint fails marks quota unavailable`() async throws {
|
|
let registered = URLProtocol.registerClass(OpenRouterStubURLProtocol.self)
|
|
defer {
|
|
if registered {
|
|
URLProtocol.unregisterClass(OpenRouterStubURLProtocol.self)
|
|
}
|
|
OpenRouterStubURLProtocol.handler = nil
|
|
}
|
|
|
|
OpenRouterStubURLProtocol.handler = { request in
|
|
guard let url = request.url else { throw URLError(.badURL) }
|
|
switch url.path {
|
|
case "/api/v1/credits":
|
|
let body = #"{"data":{"total_credits":100,"total_usage":40}}"#
|
|
return Self.makeResponse(url: url, body: body, statusCode: 200)
|
|
case "/api/v1/key":
|
|
return Self.makeResponse(url: url, body: "{}", statusCode: 500)
|
|
default:
|
|
return Self.makeResponse(url: url, body: "{}", statusCode: 404)
|
|
}
|
|
}
|
|
|
|
let usage = try await OpenRouterUsageFetcher.fetchUsage(
|
|
apiKey: "sk-or-v1-test",
|
|
environment: ["OPENROUTER_API_URL": "https://openrouter.test/api/v1"])
|
|
|
|
#expect(!usage.keyDataFetched)
|
|
#expect(usage.keyQuotaStatus == .unavailable)
|
|
}
|
|
|
|
@Test
|
|
func `key enrichment timeout does not wait for operation that ignores cancellation`() async throws {
|
|
let startedAt = ContinuousClock.now
|
|
|
|
let fetched = try await OpenRouterUsageFetcher._boundedKeyFetchForTesting(
|
|
timeout: .milliseconds(20))
|
|
{
|
|
await withCheckedContinuation { continuation in
|
|
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
|
|
continuation.resume()
|
|
}
|
|
}
|
|
}
|
|
|
|
let elapsed = startedAt.duration(to: .now)
|
|
#expect(!fetched)
|
|
#expect(elapsed < .milliseconds(300))
|
|
|
|
try await Task.sleep(for: .milliseconds(550))
|
|
}
|
|
|
|
@Test
|
|
func `usage snapshot round trip persists open router usage metadata`() throws {
|
|
let openRouter = OpenRouterUsageSnapshot(
|
|
totalCredits: 50,
|
|
totalUsage: 45.3895596325,
|
|
balance: 4.6104403675,
|
|
usedPercent: 90.779119265,
|
|
keyDataFetched: true,
|
|
keyLimit: nil,
|
|
keyUsage: nil,
|
|
keyUsageDaily: 0.12,
|
|
keyUsageWeekly: 0.74,
|
|
keyUsageMonthly: 4.56,
|
|
rateLimit: nil,
|
|
updatedAt: Date(timeIntervalSince1970: 1_739_841_600))
|
|
let snapshot = openRouter.toUsageSnapshot()
|
|
|
|
let encoder = JSONEncoder()
|
|
let data = try encoder.encode(snapshot)
|
|
let decoded = try JSONDecoder().decode(UsageSnapshot.self, from: data)
|
|
|
|
#expect(decoded.openRouterUsage?.keyDataFetched == true)
|
|
#expect(decoded.openRouterUsage?.keyQuotaStatus == .noLimitConfigured)
|
|
#expect(decoded.openRouterUsage?.keyUsageDaily == 0.12)
|
|
#expect(decoded.openRouterUsage?.keyUsageWeekly == 0.74)
|
|
#expect(decoded.openRouterUsage?.keyUsageMonthly == 4.56)
|
|
}
|
|
|
|
private static func makeResponse(
|
|
url: URL,
|
|
body: String,
|
|
statusCode: Int = 200) -> (HTTPURLResponse, Data)
|
|
{
|
|
let response = HTTPURLResponse(
|
|
url: url,
|
|
statusCode: statusCode,
|
|
httpVersion: "HTTP/1.1",
|
|
headerFields: ["Content-Type": "application/json"])!
|
|
return (response, Data(body.utf8))
|
|
}
|
|
}
|
|
|
|
final class OpenRouterStubURLProtocol: URLProtocol {
|
|
nonisolated(unsafe) static var handler: ((URLRequest) throws -> (HTTPURLResponse, Data))?
|
|
|
|
override static func canInit(with request: URLRequest) -> Bool {
|
|
request.url?.host == "openrouter.test"
|
|
}
|
|
|
|
override static func canonicalRequest(for request: URLRequest) -> URLRequest {
|
|
request
|
|
}
|
|
|
|
override func startLoading() {
|
|
guard let handler = Self.handler else {
|
|
self.client?.urlProtocol(self, didFailWithError: URLError(.badServerResponse))
|
|
return
|
|
}
|
|
do {
|
|
let (response, data) = try handler(self.request)
|
|
self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
|
|
self.client?.urlProtocol(self, didLoad: data)
|
|
self.client?.urlProtocolDidFinishLoading(self)
|
|
} catch {
|
|
self.client?.urlProtocol(self, didFailWithError: error)
|
|
}
|
|
}
|
|
|
|
override func stopLoading() {}
|
|
}
|