237 lines
8.1 KiB
Swift
237 lines
8.1 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import CodexBarCore
|
|
|
|
@Suite(.serialized)
|
|
struct CrofUsageFetcherTests {
|
|
@Test
|
|
func `usage URL points at public usage API`() {
|
|
#expect(CrofUsageFetcher.usageURL.absoluteString == "https://crof.ai/usage_api/")
|
|
}
|
|
|
|
@Test
|
|
func `usage response parses credits and request quota`() throws {
|
|
let json = """
|
|
{"credits":10.0,"requests_plan":1000,"usable_requests":998}
|
|
"""
|
|
|
|
let snapshot = try CrofUsageFetcher._parseSnapshotForTesting(Data(json.utf8))
|
|
|
|
#expect(snapshot.credits == 10)
|
|
#expect(snapshot.requestsPlan == 1000)
|
|
#expect(snapshot.usableRequests == 998)
|
|
}
|
|
|
|
@Test
|
|
func `usage snapshot maps usable requests to remaining quota`() {
|
|
let snapshot = CrofUsageSnapshot(
|
|
credits: 10,
|
|
requestsPlan: 1000,
|
|
usableRequests: 998,
|
|
updatedAt: Date(timeIntervalSince1970: 1_777_800_000))
|
|
|
|
let usage = snapshot.toUsageSnapshot()
|
|
|
|
#expect(usage.primary?.usedPercent == 1)
|
|
#expect(usage.primary?.windowMinutes == 1440)
|
|
#expect(usage.primary?.resetDescription == "998 requests left")
|
|
#expect(usage.secondary?.usedPercent == 0)
|
|
#expect(usage.secondary?.resetDescription == "$10.00")
|
|
#expect(usage.identity?.providerID == .crof)
|
|
#expect(usage.identity?.loginMethod == "API key")
|
|
}
|
|
|
|
@Test
|
|
func `usage snapshot floors credit balance to cents`() {
|
|
let snapshot = CrofUsageSnapshot(
|
|
credits: 9.9999,
|
|
requestsPlan: 1000,
|
|
usableRequests: 998)
|
|
|
|
#expect(snapshot.toUsageSnapshot().secondary?.resetDescription == "$9.99")
|
|
}
|
|
|
|
@Test
|
|
func `usage snapshot resets requests at next America Chicago midnight`() throws {
|
|
var utc = Calendar(identifier: .gregorian)
|
|
utc.timeZone = try #require(TimeZone(secondsFromGMT: 0))
|
|
let updatedAt = try #require(utc.date(from: DateComponents(
|
|
year: 2026,
|
|
month: 5,
|
|
day: 8,
|
|
hour: 18,
|
|
minute: 30)))
|
|
let expectedReset = try #require(utc.date(from: DateComponents(
|
|
year: 2026,
|
|
month: 5,
|
|
day: 9,
|
|
hour: 5)))
|
|
let snapshot = CrofUsageSnapshot(
|
|
credits: 10,
|
|
requestsPlan: 1000,
|
|
usableRequests: 998,
|
|
updatedAt: updatedAt)
|
|
|
|
#expect(snapshot.toUsageSnapshot().primary?.resetsAt == expectedReset)
|
|
}
|
|
|
|
@Test
|
|
func `usage snapshot clamps overreported usable requests`() {
|
|
let snapshot = CrofUsageSnapshot(
|
|
credits: 0,
|
|
requestsPlan: 1000,
|
|
usableRequests: 1200)
|
|
|
|
#expect(snapshot.toUsageSnapshot().primary?.usedPercent == 0)
|
|
}
|
|
|
|
@Test
|
|
func `usage snapshot treats zero plan as exhausted`() {
|
|
let snapshot = CrofUsageSnapshot(
|
|
credits: 0,
|
|
requestsPlan: 0,
|
|
usableRequests: 0)
|
|
|
|
#expect(snapshot.toUsageSnapshot().primary?.usedPercent == 100)
|
|
}
|
|
|
|
@Test
|
|
func `fetch sends bearer token`() async throws {
|
|
defer {
|
|
CrofStubURLProtocol.handler = nil
|
|
CrofStubURLProtocol.requests = []
|
|
}
|
|
CrofStubURLProtocol.requests = []
|
|
CrofStubURLProtocol.handler = { request in
|
|
guard let url = request.url else { throw URLError(.badURL) }
|
|
#expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer crof-test")
|
|
#expect(request.value(forHTTPHeaderField: "Accept") == "application/json")
|
|
return try Self.makeResponse(
|
|
url: url,
|
|
body: #"{"credits":10.0,"requests_plan":1000,"usable_requests":998}"#)
|
|
}
|
|
|
|
let snapshot = try await CrofUsageFetcher.fetchUsage(apiKey: "crof-test", session: Self.makeSession())
|
|
|
|
#expect(snapshot.usableRequests == 998)
|
|
#expect(CrofStubURLProtocol.requests.map(\.url?.absoluteString) == ["https://crof.ai/usage_api/"])
|
|
}
|
|
|
|
@Test
|
|
func `descriptor supports auto and API source modes`() {
|
|
let descriptor = ProviderDescriptorRegistry.descriptor(for: .crof)
|
|
#expect(descriptor.metadata.displayName == "Crof")
|
|
#expect(descriptor.metadata.dashboardURL == "https://crof.ai/dashboard")
|
|
#expect(descriptor.fetchPlan.sourceModes == [.auto, .api])
|
|
#expect(descriptor.branding.iconResourceName == "ProviderIcon-crof")
|
|
}
|
|
|
|
@Test
|
|
func `settings reader uses CROF_API_KEY`() {
|
|
let token = CrofSettingsReader.apiKey(environment: [
|
|
CrofSettingsReader.apiKeyEnvironmentKeys[0]: " crof-token ",
|
|
])
|
|
|
|
#expect(token == "crof-token")
|
|
}
|
|
|
|
@Test
|
|
func `token resolver uses crof environment token`() {
|
|
let env = [CrofSettingsReader.apiKeyEnvironmentKeys[0]: "crof-token"]
|
|
let resolution = ProviderTokenResolver.crofResolution(environment: env)
|
|
|
|
#expect(resolution?.token == "crof-token")
|
|
#expect(resolution?.source == .environment)
|
|
}
|
|
|
|
@Test
|
|
func `config API key override feeds crof environment`() {
|
|
let config = ProviderConfig(id: .crof, apiKey: "config-token")
|
|
let env = ProviderConfigEnvironment.applyAPIKeyOverride(
|
|
base: [:],
|
|
provider: .crof,
|
|
config: config)
|
|
|
|
#expect(env[CrofSettingsReader.apiKeyEnvironmentKeys[0]] == "config-token")
|
|
#expect(ProviderTokenResolver.crofToken(environment: env) == "config-token")
|
|
}
|
|
|
|
@Test
|
|
func `config API key leaves existing crof environment token alone`() {
|
|
let key = CrofSettingsReader.apiKeyEnvironmentKeys[0]
|
|
let config = ProviderConfig(id: .crof, apiKey: "config-token")
|
|
let env = ProviderConfigEnvironment.applyAPIKeyOverride(
|
|
base: [key: "env-token"],
|
|
provider: .crof,
|
|
config: config)
|
|
|
|
#expect(env[key] == "env-token")
|
|
#expect(ProviderTokenResolver.crofToken(environment: env) == "env-token")
|
|
}
|
|
|
|
@Test
|
|
func `missing credentials fetch call throws missing credentials`() async {
|
|
do {
|
|
_ = try await CrofUsageFetcher.fetchUsage(apiKey: " ")
|
|
Issue.record("Expected missingCredentials error")
|
|
} catch let error as CrofUsageError {
|
|
#expect(error == .missingCredentials)
|
|
} catch {
|
|
Issue.record("Unexpected error: \(error)")
|
|
}
|
|
}
|
|
|
|
private static func makeSession() -> URLSession {
|
|
let config = URLSessionConfiguration.ephemeral
|
|
config.protocolClasses = [CrofStubURLProtocol.self]
|
|
return URLSession(configuration: config)
|
|
}
|
|
|
|
private static func makeResponse(
|
|
url: URL,
|
|
body: String,
|
|
statusCode: Int = 200) throws -> (HTTPURLResponse, Data)
|
|
{
|
|
guard let response = HTTPURLResponse(
|
|
url: url,
|
|
statusCode: statusCode,
|
|
httpVersion: nil,
|
|
headerFields: ["Content-Type": "application/json"])
|
|
else {
|
|
throw URLError(.badServerResponse)
|
|
}
|
|
return (response, Data(body.utf8))
|
|
}
|
|
}
|
|
|
|
final class CrofStubURLProtocol: URLProtocol {
|
|
nonisolated(unsafe) static var handler: ((URLRequest) throws -> (HTTPURLResponse, Data))?
|
|
nonisolated(unsafe) static var requests: [URLRequest] = []
|
|
|
|
override static func canInit(with request: URLRequest) -> Bool {
|
|
request.url?.host == "crof.ai"
|
|
}
|
|
|
|
override static func canonicalRequest(for request: URLRequest) -> URLRequest {
|
|
request
|
|
}
|
|
|
|
override func startLoading() {
|
|
Self.requests.append(self.request)
|
|
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() {}
|
|
}
|