import Foundation @testable import ManaCore /// URLProtocol-Mock mit Pro-Test-Routing: jede Test-AuthClient- /// Instanz kriegt eine eigene Test-ID als HTTP-Header, der Mock /// routet nach ID zum richtigen Handler. Das löst die Cross-Suite- /// Pollution (mehrere `.serialized`-Suites laufen untereinander /// parallel, der globale Handler-Slot wäre sonst ein Race). /// /// Identisches Pattern wie in mana-swift-ui — wenn weitere Test- /// Helper hier hinzukommen, beide Pakete parallel halten. final class MockURLProtocol: URLProtocol, @unchecked Sendable { typealias Handler = @Sendable (URLRequest) -> Any nonisolated(unsafe) static var handlersStorage: [String: Handler] = [:] static let handlersLock = NSLock() static func register(testID: String, handler: @escaping Handler) { handlersLock.lock(); defer { handlersLock.unlock() } handlersStorage[testID] = handler } static func unregister(testID: String) { handlersLock.lock(); defer { handlersLock.unlock() } handlersStorage.removeValue(forKey: testID) } private static func lookup(testID: String) -> Handler? { handlersLock.lock(); defer { handlersLock.unlock() } return handlersStorage[testID] } final class Capture: @unchecked Sendable { private let lock = NSLock() private var stored: URLRequest? func store(_ r: URLRequest) { lock.lock(); defer { lock.unlock() } stored = r } var request: URLRequest? { lock.lock(); defer { lock.unlock() } return stored } } override class func canInit(with request: URLRequest) -> Bool { true } override class func canonicalRequest(for request: URLRequest) -> URLRequest { request } override func stopLoading() {} override func startLoading() { let testID = request.value(forHTTPHeaderField: "X-Test-ID") ?? "" guard let handler = MockURLProtocol.lookup(testID: testID) else { client?.urlProtocol(self, didFailWithError: URLError(.unknown)) return } let result = handler(request) let status: Int let body: Data let headers: [String: String] if let tuple = result as? (Int, Data, [String: String]) { status = tuple.0; body = tuple.1; headers = tuple.2 } else if let tuple = result as? (Int, Data) { status = tuple.0; body = tuple.1; headers = [:] } else { client?.urlProtocol(self, didFailWithError: URLError(.unknown)) return } let response = HTTPURLResponse( url: request.url!, statusCode: status, httpVersion: "HTTP/1.1", headerFields: headers )! client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) client?.urlProtocol(self, didLoad: body) client?.urlProtocolDidFinishLoading(self) } } /// Bündelt einen `AuthClient` mit seiner Test-ID. @MainActor struct MockedAuth { let auth: AuthClient let testID: String func setHandler(_ handler: @escaping MockURLProtocol.Handler) { MockURLProtocol.register(testID: testID, handler: handler) } } @MainActor func makeMockedAuth() -> MockedAuth { let testID = UUID().uuidString let configuration = URLSessionConfiguration.ephemeral configuration.protocolClasses = [MockURLProtocol.self] configuration.httpAdditionalHeaders = ["X-Test-ID": testID] let session = URLSession(configuration: configuration) let config = DefaultManaAppConfig( authBaseURL: URL(string: "https://auth.test")!, keychainService: "ev.mana.test.\(testID)", keychainAccessGroup: nil ) return MockedAuth(auth: AuthClient(config: config, session: session), testID: testID) } extension URLRequest { /// Liest httpBodyStream in einen Data. URLSession ephemeral-Session /// nutzt manchmal Streams statt httpBody. func bodyStreamData() -> Data? { guard let stream = httpBodyStream else { return nil } stream.open(); defer { stream.close() } var data = Data() let bufferSize = 1024 let buffer = UnsafeMutablePointer.allocate(capacity: bufferSize) defer { buffer.deallocate() } while stream.hasBytesAvailable { let read = stream.read(buffer, maxLength: bufferSize) if read <= 0 { break } data.append(buffer, count: read) } return data } }