import Foundation /// Test double for `URLProtocol`. Intercepts every request on a `URLSession` /// configured with it, records the outgoing `URLRequest`, and replays a canned /// response or error supplied by the test. /// /// Register via: /// ``` /// let cfg = URLSessionConfiguration.ephemeral /// cfg.protocolClasses = [StubURLProtocol.self] /// let session = URLSession(configuration: cfg) /// ``` final class StubURLProtocol: URLProtocol, @unchecked Sendable { struct Stub { var statusCode: Int = 200 var headers: [String: String] = ["Content-Type": "application/json"] var body: Data = Data() var error: Error? /// Bytes reported through `URLSession`'s upload progress, in order. var uploadProgressChunks: [Int] = [] } // MARK: - Test-facing state (guarded) private static let lock = NSLock() // Access is serialised through `lock`; the unsafe opt-out is the documented // pattern for lock-guarded mutable statics under strict concurrency. nonisolated(unsafe) private static var _stub = Stub() nonisolated(unsafe) private static var _capturedRequests: [URLRequest] = [] nonisolated(unsafe) private static var _capturedBodies: [Data] = [] static func setStub(_ stub: Stub) { lock.lock(); defer { lock.unlock() } _stub = stub _capturedRequests = [] _capturedBodies = [] } static func reset() { setStub(Stub()) } static var capturedRequests: [URLRequest] { lock.lock(); defer { lock.unlock() } return _capturedRequests } /// Body of the last intercepted request. `URLProtocol` strips `httpBody` for /// stream bodies, so this reads `httpBodyStream` when needed. static var lastCapturedBody: Data? { lock.lock(); defer { lock.unlock() } return _capturedBodies.last } static var requestCount: Int { lock.lock(); defer { lock.unlock() } return _capturedRequests.count } private static func currentStub() -> Stub { lock.lock(); defer { lock.unlock() } return _stub } private static func record(_ request: URLRequest, body: Data) { lock.lock(); defer { lock.unlock() } _capturedRequests.append(request) _capturedBodies.append(body) } // MARK: - URLProtocol override class func canInit(with request: URLRequest) -> Bool { true } override class func canonicalRequest(for request: URLRequest) -> URLRequest { request } override func startLoading() { let stub = Self.currentStub() Self.record(request, body: Self.bodyData(from: request)) guard let client = client else { return } if let error = stub.error { client.urlProtocol(self, didFailWithError: error) return } let url = request.url ?? URL(string: "https://stub.invalid")! let response = HTTPURLResponse(url: url, statusCode: stub.statusCode, httpVersion: "HTTP/1.1", headerFields: stub.headers)! client.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) client.urlProtocol(self, didLoad: stub.body) client.urlProtocolDidFinishLoading(self) } override func stopLoading() {} // MARK: - Body extraction private static func bodyData(from request: URLRequest) -> Data { if let body = request.httpBody { return body } guard let stream = request.httpBodyStream else { return Data() } stream.open() defer { stream.close() } var data = Data() let bufferSize = 64 * 1024 var buffer = [UInt8](repeating: 0, count: bufferSize) while stream.hasBytesAvailable { let read = stream.read(&buffer, maxLength: bufferSize) if read <= 0 { break } data.append(buffer, count: read) } return data } }