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

472 lines
18 KiB
Swift

import Foundation
import Testing
@testable import CodexBarCore
@Suite(.serialized)
struct OllamaUsageFetcherRetryMappingTests {
private func makeContext(
sourceMode: ProviderSourceMode,
env: [String: String] = [:],
settings: ProviderSettingsSnapshot? = nil) -> ProviderFetchContext
{
let browserDetection = BrowserDetection(cacheTTL: 0)
return ProviderFetchContext(
runtime: .cli,
sourceMode: sourceMode,
includeCredits: false,
webTimeout: 1,
webDebugDumpHTML: false,
verbose: false,
env: env,
settings: settings,
fetcher: UsageFetcher(),
claudeFetcher: ClaudeUsageFetcher(browserDetection: browserDetection),
browserDetection: browserDetection)
}
@Test
func `api key reader trims configured environment key`() {
let token = OllamaAPISettingsReader.apiKey(environment: ["OLLAMA_API_KEY": " 'ollama-test' "])
#expect(token == "ollama-test")
}
@Test
func `api tags response maps to API key identity`() throws {
let now = Date(timeIntervalSince1970: 1_700_000_000)
let snapshot = try OllamaAPIUsageFetcher._parseTagsForTesting(
Data(#"{"models":[{"name":"gpt-oss:120b"}]}"#.utf8),
now: now)
let usage = snapshot.toUsageSnapshot()
#expect(snapshot.modelCount == 1)
#expect(usage.primary == nil)
#expect(usage.identity?.providerID == .ollama)
#expect(usage.identity?.loginMethod == "API key")
#expect(usage.updatedAt == now)
}
@Test
func `auto mode keeps web quota strategy before api key verification`() async {
let descriptor = OllamaProviderDescriptor.makeDescriptor()
let context = self.makeContext(
sourceMode: .auto,
env: ["OLLAMA_API_KEY": "ollama-test"],
settings: ProviderSettingsSnapshot.make(
ollama: .init(cookieSource: .auto, manualCookieHeader: nil)))
let strategies = await descriptor.fetchPlan.pipeline.resolveStrategies(context)
#expect(strategies.map(\.id) == ["ollama.web", "ollama.api"])
}
@Test
func `auto mode uses api only when ollama cookies are off`() async {
let descriptor = OllamaProviderDescriptor.makeDescriptor()
let context = self.makeContext(
sourceMode: .auto,
env: ["OLLAMA_API_KEY": "ollama-test"],
settings: ProviderSettingsSnapshot.make(
ollama: .init(cookieSource: .off, manualCookieHeader: nil)))
let strategies = await descriptor.fetchPlan.pipeline.resolveStrategies(context)
#expect(strategies.map(\.id) == ["ollama.api"])
}
@Test
func `web strategy falls back to api key in auto mode`() {
let context = self.makeContext(
sourceMode: .auto,
env: ["OLLAMA_API_KEY": "ollama-test"])
let strategy = OllamaStatusFetchStrategy()
#expect(strategy.shouldFallback(on: OllamaUsageError.parseFailed("missing"), context: context))
}
@Test(arguments: [401, 403])
func `api fetch sends bearer token and rejects unauthorized key`(statusCode: Int) async throws {
let url = try #require(URL(string: "https://ollama.com/api/web_search"))
let transport = ProviderHTTPTransportHandler { request in
#expect(request.url == url)
#expect(request.httpMethod == "POST")
#expect(request.httpBody == Data(#"{"query":""}"#.utf8))
#expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer ollama-test")
let response = HTTPURLResponse(
url: url,
statusCode: statusCode,
httpVersion: "HTTP/1.1",
headerFields: nil)!
return (Data("{}".utf8), response)
}
do {
_ = try await OllamaAPIUsageFetcher.fetchUsage(apiKey: "ollama-test", transport: transport)
Issue.record("Expected unauthorized API error")
} catch let error as OllamaUsageError {
guard case .apiUnauthorized = error else {
Issue.record("Expected apiUnauthorized, got \(error)")
return
}
#expect(error.localizedDescription == "Ollama API key is invalid or revoked.")
} catch {
Issue.record("Expected OllamaUsageError.apiUnauthorized, got \(error)")
}
}
@Test
func `authorized validation continues to model catalog`() async throws {
let validationURL = try #require(URL(string: "https://ollama.test/api/web_search"))
let tagsURL = try #require(URL(string: "https://ollama.test/api/tags"))
let transport = ProviderHTTPTransportHandler { request in
let statusCode: Int
let data: Data
switch request.url {
case validationURL:
statusCode = 400
data = Data(#"{"error":"query is required"}"#.utf8)
case tagsURL:
statusCode = 200
data = Data(#"{"models":[{}]}"#.utf8)
default:
Issue.record("Unexpected Ollama API URL")
statusCode = 500
data = Data()
}
let response = HTTPURLResponse(
url: request.url!,
statusCode: statusCode,
httpVersion: "HTTP/1.1",
headerFields: nil)!
return (data, response)
}
let snapshot = try await OllamaAPIUsageFetcher.fetchUsage(
apiKey: "ollama-test",
tagsURL: tagsURL,
validationURL: validationURL,
transport: transport)
#expect(snapshot.modelCount == 1)
}
@Test(arguments: [401, 403])
func `authorized validation still rejects unauthorized model catalog`(statusCode: Int) async throws {
let validationURL = try #require(URL(string: "https://ollama.test/api/web_search"))
let tagsURL = try #require(URL(string: "https://ollama.test/api/tags"))
let transport = ProviderHTTPTransportHandler { request in
let responseStatus = request.url == validationURL ? 400 : statusCode
let response = HTTPURLResponse(
url: request.url!,
statusCode: responseStatus,
httpVersion: "HTTP/1.1",
headerFields: nil)!
return (Data("{}".utf8), response)
}
do {
_ = try await OllamaAPIUsageFetcher.fetchUsage(
apiKey: "ollama-test",
tagsURL: tagsURL,
validationURL: validationURL,
transport: transport)
Issue.record("Expected unauthorized model catalog error")
} catch let error as OllamaUsageError {
guard case .apiUnauthorized = error else {
Issue.record("Expected apiUnauthorized, got \(error)")
return
}
} catch {
Issue.record("Expected OllamaUsageError.apiUnauthorized, got \(error)")
}
}
@Test
func `custom catalog derives validation on the same origin`() async throws {
let tagsURL = try #require(URL(string: "https://private.example/prefix/api/tags"))
let validationURL = try #require(URL(string: "https://private.example/prefix/api/web_search"))
let transport = ProviderHTTPTransportHandler { request in
let statusCode: Int
let data: Data
switch request.url {
case validationURL:
statusCode = 400
data = Data(#"{"error":"query is required"}"#.utf8)
case tagsURL:
statusCode = 200
data = Data(#"{"models":[{}]}"#.utf8)
default:
Issue.record("Unexpected Ollama API URL")
statusCode = 500
data = Data()
}
#expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer private-key")
let response = HTTPURLResponse(
url: request.url!,
statusCode: statusCode,
httpVersion: "HTTP/1.1",
headerFields: nil)!
return (data, response)
}
let snapshot = try await OllamaAPIUsageFetcher.fetchUsage(
apiKey: "private-key",
tagsURL: tagsURL,
transport: transport)
#expect(snapshot.modelCount == 1)
}
@Test
func `cross origin validation endpoint is rejected before sending credentials`() async throws {
let tagsURL = try #require(URL(string: "https://private.example/api/tags"))
let validationURL = try #require(URL(string: "https://ollama.com/api/web_search"))
let transport = ProviderHTTPTransportHandler { _ in
Issue.record("Cross-origin endpoints must fail before transport")
throw URLError(.badURL)
}
do {
_ = try await OllamaAPIUsageFetcher.fetchUsage(
apiKey: "private-key",
tagsURL: tagsURL,
validationURL: validationURL,
transport: transport)
Issue.record("Expected a same-origin validation error")
} catch let error as OllamaUsageError {
guard case let .networkError(message) = error else {
Issue.record("Expected networkError, got \(error)")
return
}
#expect(message == "Ollama key validation and model catalog endpoints must share an origin.")
} catch {
Issue.record("Expected OllamaUsageError.networkError, got \(error)")
}
}
@Test
func `non loopback HTTP catalog is rejected before sending credentials`() async throws {
let tagsURL = try #require(URL(string: "http://private.example/api/tags"))
let transport = ProviderHTTPTransportHandler { _ in
Issue.record("Insecure non-loopback endpoints must fail before transport")
throw URLError(.badURL)
}
do {
_ = try await OllamaAPIUsageFetcher.fetchUsage(
apiKey: "private-key",
tagsURL: tagsURL,
transport: transport)
Issue.record("Expected an insecure endpoint error")
} catch let error as OllamaUsageError {
guard case let .networkError(message) = error else {
Issue.record("Expected networkError, got \(error)")
return
}
#expect(message == "Ollama API endpoints must use HTTPS or loopback HTTP.")
} catch {
Issue.record("Expected OllamaUsageError.networkError, got \(error)")
}
}
@Test
func `api validation preserves cancellation`() async {
let transport = ProviderHTTPTransportHandler { _ in
throw CancellationError()
}
await #expect(throws: CancellationError.self) {
_ = try await OllamaAPIUsageFetcher.fetchUsage(apiKey: "ollama-test", transport: transport)
}
}
@Test
func `model catalog fetch preserves URL cancellation`() async throws {
let validationURL = try #require(URL(string: "https://ollama.test/api/web_search"))
let tagsURL = try #require(URL(string: "https://ollama.test/api/tags"))
let transport = ProviderHTTPTransportHandler { request in
guard request.url == validationURL else { throw URLError(.cancelled) }
let response = HTTPURLResponse(
url: validationURL,
statusCode: 400,
httpVersion: "HTTP/1.1",
headerFields: nil)!
return (Data(#"{"error":"query is required"}"#.utf8), response)
}
await #expect(throws: CancellationError.self) {
_ = try await OllamaAPIUsageFetcher.fetchUsage(
apiKey: "ollama-test",
tagsURL: tagsURL,
validationURL: validationURL,
transport: transport)
}
}
@Test
func `unproven validation status fails closed`() async throws {
let validationURL = try #require(URL(string: "https://ollama.test/api/web_search"))
let tagsURL = try #require(URL(string: "https://ollama.test/api/tags"))
let transport = ProviderHTTPTransportHandler { request in
#expect(request.url == validationURL)
let response = HTTPURLResponse(
url: validationURL,
statusCode: 422,
httpVersion: "HTTP/1.1",
headerFields: nil)!
return (Data(#"{"error":"unprocessable"}"#.utf8), response)
}
do {
_ = try await OllamaAPIUsageFetcher.fetchUsage(
apiKey: "ollama-test",
tagsURL: tagsURL,
validationURL: validationURL,
transport: transport)
Issue.record("Expected an HTTP 422 network error")
} catch let error as OllamaUsageError {
guard case let .networkError(message) = error else {
Issue.record("Expected networkError, got \(error)")
return
}
#expect(message == "HTTP 422")
} catch {
Issue.record("Expected OllamaUsageError.networkError, got \(error)")
}
}
@Test
func `missing usage shape surfaces public parse failed message`() async {
defer { OllamaRetryMappingStubURLProtocol.handler = nil }
OllamaRetryMappingStubURLProtocol.handler = { request in
guard let url = request.url else { throw URLError(.badURL) }
let body = "<html><body>No usage data rendered.</body></html>"
return Self.makeResponse(url: url, body: body, statusCode: 200)
}
let fetcher = self.makeCookieFetcher()
do {
_ = try await fetcher.fetch(
cookieHeaderOverride: "session=test-cookie",
manualCookieMode: true)
Issue.record("Expected OllamaUsageError.parseFailed")
} catch let error as OllamaUsageError {
guard case let .parseFailed(message) = error else {
Issue.record("Expected parseFailed, got \(error)")
return
}
#expect(message == "Missing Ollama usage data.")
} catch {
Issue.record("Expected OllamaUsageError.parseFailed, got \(error)")
}
}
@Test
func `workos sign in landing surfaces invalid credentials before parsing`() async throws {
defer { OllamaRetryMappingStubURLProtocol.handler = nil }
let landingURL = try #require(URL(
string: "https://signin.ollama.com/?client_id=test&authorization_session_id=expired"))
OllamaRetryMappingStubURLProtocol.handler = { request in
#expect(request.url == URL(string: "https://ollama.com/settings"))
let body = "<html><body>Sign in to Ollama</body></html>"
return Self.makeResponse(url: landingURL, body: body, statusCode: 200)
}
let fetcher = self.makeCookieFetcher()
do {
_ = try await fetcher.fetch(
cookieHeaderOverride: "session=expired-cookie",
manualCookieMode: true)
Issue.record("Expected OllamaUsageError.invalidCredentials")
} catch let error as OllamaUsageError {
guard case .invalidCredentials = error else {
Issue.record("Expected invalidCredentials, got \(error)")
return
}
} catch {
Issue.record("Expected OllamaUsageError.invalidCredentials, got \(error)")
}
}
@Test
func `workos sign in service failure remains a network error`() async throws {
defer { OllamaRetryMappingStubURLProtocol.handler = nil }
let landingURL = try #require(URL(string: "https://signin.ollama.com/"))
OllamaRetryMappingStubURLProtocol.handler = { _ in
Self.makeResponse(url: landingURL, body: "Service unavailable", statusCode: 503)
}
let fetcher = self.makeCookieFetcher()
do {
_ = try await fetcher.fetch(
cookieHeaderOverride: "session=expired-cookie",
manualCookieMode: true)
Issue.record("Expected OllamaUsageError.networkError")
} catch let error as OllamaUsageError {
guard case let .networkError(message) = error else {
Issue.record("Expected networkError, got \(error)")
return
}
#expect(message == "HTTP 503")
} catch {
Issue.record("Expected OllamaUsageError.networkError, got \(error)")
}
}
private func makeCookieFetcher() -> OllamaUsageFetcher {
OllamaUsageFetcher(
browserDetection: BrowserDetection(cacheTTL: 0),
makeURLSession: { delegate in
let config = URLSessionConfiguration.ephemeral
config.protocolClasses = [OllamaRetryMappingStubURLProtocol.self]
return URLSession(configuration: config, delegate: delegate, delegateQueue: nil)
})
}
private static func makeResponse(
url: URL,
body: String,
statusCode: Int) -> (HTTPURLResponse, Data)
{
let response = HTTPURLResponse(
url: url,
statusCode: statusCode,
httpVersion: "HTTP/1.1",
headerFields: ["Content-Type": "text/html"])!
return (response, Data(body.utf8))
}
}
final class OllamaRetryMappingStubURLProtocol: URLProtocol {
nonisolated(unsafe) static var handler: ((URLRequest) throws -> (HTTPURLResponse, Data))?
override static func canInit(with request: URLRequest) -> Bool {
guard let host = request.url?.host?.lowercased() else { return false }
return host == "ollama.com" || host == "www.ollama.com"
}
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() {}
}