498f407585
Introduce optional Apple account-backed credits with scoped gateway access while preserving local and BYOK paths. Refresh assistant behavior, tests, privacy disclosures, docs, and the website for the 2.0 experience.
86 lines
3.5 KiB
Swift
86 lines
3.5 KiB
Swift
// StubURLProtocol.swift
|
|
// OSGKeyboardTests · TestSupport
|
|
//
|
|
// Shared URLProtocol stub for hermetic HTTP client tests (LLM + Cloud ASR).
|
|
|
|
import Foundation
|
|
|
|
/// Per-test stub config holder. Tests set these via `StubURLProtocolStorage.config =`
|
|
/// before invoking the code under test, then reset to nil in cleanup.
|
|
enum StubURLProtocolStorage {
|
|
nonisolated(unsafe) static var config: (statusCode: Int, body: Data)?
|
|
nonisolated(unsafe) static var delaySeconds: Double = 0
|
|
nonisolated(unsafe) static var lastRequest: URLRequest?
|
|
}
|
|
|
|
final class StubURLProtocol: URLProtocol, @unchecked Sendable {
|
|
// URLProtocol requires overridable class methods; `static` cannot satisfy
|
|
// these superclass requirements even though this concrete stub is final.
|
|
// swiftlint:disable:next static_over_final_class
|
|
override class func canInit(with request: URLRequest) -> Bool { true }
|
|
// swiftlint:disable:next static_over_final_class
|
|
override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }
|
|
|
|
override func startLoading() {
|
|
let cfg = StubURLProtocolStorage.config ?? (statusCode: 200, body: Data())
|
|
let delay = StubURLProtocolStorage.delaySeconds
|
|
// Materialize body here (once). Doing it in `canonicalRequest` consumes
|
|
// the stream before `startLoading` can capture it for assertions.
|
|
StubURLProtocolStorage.lastRequest = Self.materializingBody(of: request)
|
|
|
|
// Simulate a slow transport. We honour URLProtocol.stopLoading() so
|
|
// cancellation doesn't leave the test hanging, and we yield to the
|
|
// run loop so `URLSession.data(for:)` actually observes the delay
|
|
// (a busy-wait would never let the cooperative scheduler time out).
|
|
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + delay) { [weak self] in
|
|
guard let self else { return }
|
|
guard self.client != nil else { return }
|
|
let response = HTTPURLResponse(
|
|
url: self.request.url!,
|
|
statusCode: cfg.statusCode,
|
|
httpVersion: "HTTP/1.1",
|
|
headerFields: ["Content-Type": "application/json"]
|
|
)!
|
|
self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
|
|
self.client?.urlProtocol(self, didLoad: cfg.body)
|
|
self.client?.urlProtocolDidFinishLoading(self)
|
|
}
|
|
}
|
|
|
|
override func stopLoading() {}
|
|
|
|
private static func materializingBody(of request: URLRequest) -> URLRequest {
|
|
var req = request
|
|
if req.httpBody == nil, let stream = req.httpBodyStream {
|
|
stream.open()
|
|
defer { stream.close() }
|
|
var data = Data()
|
|
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: 4_096)
|
|
defer { buffer.deallocate() }
|
|
while stream.hasBytesAvailable {
|
|
let read = stream.read(buffer, maxLength: 4_096)
|
|
if read <= 0 { break }
|
|
data.append(buffer, count: read)
|
|
}
|
|
if !data.isEmpty {
|
|
req.httpBody = data
|
|
}
|
|
}
|
|
return req
|
|
}
|
|
}
|
|
|
|
extension StubURLProtocol {
|
|
static func makeEphemeralSession() -> URLSession {
|
|
let cfg = URLSessionConfiguration.ephemeral
|
|
cfg.protocolClasses = [StubURLProtocol.self]
|
|
return URLSession(configuration: cfg)
|
|
}
|
|
|
|
static func reset() {
|
|
StubURLProtocolStorage.config = nil
|
|
StubURLProtocolStorage.delaySeconds = 0
|
|
StubURLProtocolStorage.lastRequest = nil
|
|
}
|
|
}
|