import XCTest @testable import Voice private final class ChatURLProtocol: URLProtocol { static var handler: ((URLRequest) throws -> (Int, Data))? override class func canInit(with request: URLRequest) -> Bool { true } override class func canonicalRequest(for request: URLRequest) -> URLRequest { request } override func startLoading() { do { let (status, body) = try Self.handler!(request) client?.urlProtocol(self, didReceive: HTTPURLResponse(url: request.url!, statusCode: status, httpVersion: nil, headerFields: nil)!, cacheStoragePolicy: .notAllowed) client?.urlProtocol(self, didLoad: body); client?.urlProtocolDidFinishLoading(self) } catch { client?.urlProtocol(self, didFailWithError: error) } } override func stopLoading() {} } final class ChatTransportTests: XCTestCase { func testBearerSameOriginQueryAndRawUpload() async throws { guard case .success(let config) = ServerConfig.parse("https://hope.example:9178") else { XCTFail(); return } let configuration = URLSessionConfiguration.ephemeral configuration.protocolClasses = [ChatURLProtocol.self] let api = ChatAPI(config: config, token: "offline-test-bearer", session: URLSession(configuration: configuration)) ChatURLProtocol.handler = { request in XCTAssertEqual(request.url?.host, "hope.example") XCTAssertEqual(request.url?.scheme, "https") XCTAssertEqual(request.value(forHTTPHeaderField: "Authorization"), "Bearer offline-test-bearer") XCTAssertEqual(URLComponents(url: request.url!, resolvingAgainstBaseURL: false)?.queryItems?.first?.value, "a & b") return (200, Data("{\"conversations\":[],\"next_cursor\":null}".utf8)) } _ = try await api.request("api/conversations", query: [URLQueryItem(name: "q", value: "a & b")]) ChatURLProtocol.handler = { request in XCTAssertEqual(request.httpMethod, "POST") XCTAssertEqual(request.url?.path, "/api/attachments") XCTAssertEqual(request.value(forHTTPHeaderField: "X-Filename"), "notes.txt") XCTAssertEqual(request.value(forHTTPHeaderField: "Content-Type"), "text/plain") return (201, Data("{}".utf8)) } _ = try await api.upload(data: Data("test".utf8), filename: "notes.txt", mime: "text/plain") api.cancel(); ChatURLProtocol.handler = nil } func testUnauthorizedAndRedirectStatusCannotBecomeSuccess() async throws { guard case .success(let config) = ServerConfig.parse("https://hope.example") else { XCTFail(); return } let configuration = URLSessionConfiguration.ephemeral; configuration.protocolClasses = [ChatURLProtocol.self] let api = ChatAPI(config: config, token: "offline", session: URLSession(configuration: configuration)) for status in [401, 403, 302, 409] { ChatURLProtocol.handler = { _ in (status, Data()) } do { _ = try await api.request("api/conversations"); XCTFail("Expected rejection") } catch ChatFailure.status(let actual) { XCTAssertEqual(actual, status) } } api.cancel(); ChatURLProtocol.handler = nil } }