import Foundation import SwiftUI import LocalAuthentication final class MockProtocol: URLProtocol, @unchecked Sendable { static var handler: ((URLRequest) throws -> (Int, String))! 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) let response = HTTPURLResponse(url: request.url!, statusCode: status, httpVersion: nil, headerFields: ["Content-Type": "application/json"])! client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) client?.urlProtocol(self, didLoad: Data(body.utf8)); client?.urlProtocolDidFinishLoading(self) } catch { client?.urlProtocol(self, didFailWithError: error) } } override func stopLoading() {} } @main struct ContractTests { static var checks = 0 static func check(_ condition: Bool, _ name: String) { precondition(condition, name); checks += 1; print("PASS: \(name)") } static func body(_ 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(); var buffer = [UInt8](repeating: 0, count: 1024) while stream.hasBytesAvailable { let count = stream.read(&buffer, maxLength: buffer.count); if count <= 0 { break }; data.append(buffer, count: count) } return data } @MainActor static func main() async throws { for server in ["http://example.test", "https://u:p@example.test", "https://example.test/?password=x"] { do { _ = try Transport(Credentials(server: server)); fatalError("Insecure URL accepted") } catch { check(true, "Reject unsafe URL: \(server)") } } let config = URLSessionConfiguration.ephemeral; config.protocolClasses = [MockProtocol.self] let transport = try Transport(Credentials(server: "https://example.test/qbit", username: "user+name", password: "p&= +雪", proxyUser: "proxy", proxyPassword: "test-only"), configuration: config) MockProtocol.handler = { request in check(request.url!.path == "/qbit/api/v2/auth/login", "Preserve reverse proxy subpath") check(request.httpMethod == "POST", "Login uses POST") check(request.value(forHTTPHeaderField: "Origin") == "https://example.test", "Same origin CSRF header") check(request.value(forHTTPHeaderField: "Referer") == "https://example.test/qbit/", "Referer includes base path") check(request.value(forHTTPHeaderField: "Authorization")?.hasPrefix("Basic ") == true, "Optional proxy authentication") let encoded = String(decoding: body(request), as: UTF8.self) var c = URLComponents(); c.percentEncodedQuery = encoded check(c.queryItems?.first(where: { $0.name == "password" })?.value == "p&= +雪", "Password form encoding round trip") return (200, "Ok.") } try await transport.login() MockProtocol.handler = { request in check(request.url!.path == "/qbit/api/v2/torrents/files", "Query stays out of URL path") check(URLComponents(url: request.url!, resolvingAgainstBaseURL: false)?.queryItems?.first?.value == "abcdef", "Torrent hash query preserved") return (200, "[]") } let files: [TorrentFile] = try await transport.get("torrents/files?hash=abcdef") check(files.isEmpty, "Decode empty file list") MockProtocol.handler = { _ in (200, "Fails.") } do { try await transport.login(); fatalError("Rejected login accepted") } catch { check(true, "Reject HTTP 200 login failure") } for status in [401, 403, 404, 500] { MockProtocol.handler = { _ in (status, "private response must not appear") } do { _ = try await transport.request("app/version"); fatalError("HTTP error accepted") } catch { check(!error.localizedDescription.contains("private response"), "HTTP \(status) produces safe actionable error") } } MockProtocol.handler = { request in let payload = String(decoding: body(request), as: UTF8.self) check(request.value(forHTTPHeaderField: "Content-Type")?.hasPrefix("multipart/form-data; boundary=") == true, "Multipart boundary") check(payload.contains("name=\"torrents\"; filename=\"sample.torrent\""), "Correct torrent upload field") check(payload.contains("torrent-content"), "Torrent bytes included") check(payload.contains("name=\"savepath\""), "Upload options included") return (200, "Ok.") } _ = try await transport.request("torrents/add", ["savepath":"/downloads"], multipart: (Data("torrent-content".utf8), "sample.torrent")) let sample = #"{"hash":"abc","name":"Example","state":"downloading","progress":0.5,"size":1000,"dlspeed":100,"upspeed":10,"eta":5,"ratio":0.1,"category":"","tags":""}"# var t = try JSONDecoder().decode(Torrent.self, from: Data(sample.utf8)) for state in ["downloading", "forcedDL", "metaDL", "forcedMetaDL"] { t.state = state; check(t.active, "Home includes \(state)") } for state in ["uploading", "stalledDL", "queuedDL", "pausedDL", "stoppedDL", "error", "checkingDL"] { t.state = state; check(!t.active, "Home excludes \(state)") } let originalURL = URL(string: "https://example.test/qbit/api/v2/auth/login")! let redirect = HTTPURLResponse(url: originalURL, statusCode: 307, httpVersion: nil, headerFields: nil)! let task = transport.session.dataTask(with: originalURL) for target in ["http://example.test/api/v2/auth/login", "https://attacker.test/login", "https://example.test:8443/login"] { var accepted = true transport.urlSession(transport.session, task: task, willPerformHTTPRedirection: redirect, newRequest: URLRequest(url: URL(string: target)!)) { accepted = $0 != nil } check(!accepted, "Reject unsafe redirect: \(target)") } var accepted = false transport.urlSession(transport.session, task: task, willPerformHTTPRedirection: redirect, newRequest: URLRequest(url: originalURL)) { accepted = $0 != nil } check(accepted, "Allow same-origin HTTPS redirect") let cookie = HTTPCookie(properties: [.domain: "example.test", .path: "/", .name: "SID", .value: "mock-session-only", .secure: "TRUE"])! transport.session.configuration.httpCookieStorage?.setCookie(cookie) let another = try Transport(Credentials(server: "https://example.test")) check(another.session.configuration.httpCookieStorage?.cookies?.contains(where: { $0.name == "SID" }) != true, "Cookie jar isolated from other connections") let model = AppModel() model.client = transport; model.connected = true; model.locked = false let gate = DispatchSemaphore(value: 0) var preferenceRequests = 0 MockProtocol.handler = { _ in preferenceRequests += 1 gate.wait() return (200, "{\"save_path\":\"/downloads\"}") } let first = Task { await model.loadPreferences() } while !model.preferencesLoading { await Task.yield() } let second = Task { await model.loadPreferences() } await Task.yield() first.cancel() // Simulate SwiftUI removing the original Settings view. gate.signal() await first.value; await second.value check(preferenceRequests == 1, "Concurrent Settings loads share one request") check(model.preferences["save_path"] as? String == "/downloads", "View cancellation preserves session-owned preference load") check(!model.preferencesLoading && model.error == nil, "Cancelled view leaves no spinner or alert") for failure in [URLError(.cancelled), URLError(.timedOut)] { MockProtocol.handler = { _ in throw failure } await model.loadPreferences() check((model.error != nil) == (failure.code == .timedOut), "Only expected cancellation is silent: \(failure.code)") check(model.preferences["save_path"] as? String == "/downloads", "Failed reload retains preferences") } model.error = nil check(!model.shouldReport(CancellationError(), from: transport), "Swift task cancellation is silent") let disconnectGate = DispatchSemaphore(value: 0) MockProtocol.handler = { _ in disconnectGate.wait() return (200, "{\"save_path\":\"/stale\"}") } let pending = Task { await model.loadPreferences() } while !model.preferencesLoading { await Task.yield() } model.disconnect() disconnectGate.signal() await pending.value check(model.preferences.isEmpty && model.error == nil, "In-flight disconnect cannot restore stale preferences or alert") model.client = another model.report(URLError(.networkConnectionLost), from: transport) check(model.error == nil, "Retired session cannot raise a global alert") model.disconnect() check(model.preferences.isEmpty && !model.preferencesLoading, "Disconnect clears session preferences") let startup = AppModel() startup.credentials = Credentials(server: "https://example.test") startup.locked = false check(!startup.canRestoreSession, "Default server without a saved user does not auto-login") await startup.restoreSessionIfNeeded() check(!startup.busy && startup.error == nil, "Fresh installation stays on connection form") startup.credentials = Credentials(server: "http://invalid.test", username: "test-user") startup.locked = true await startup.restoreSessionIfNeeded() check(startup.error == nil && !startup.restoringSession, "App lock prevents automatic network connection") startup.locked = false await startup.restoreSessionIfNeeded() check(startup.error != nil && !startup.connected && !startup.restoringSession, "Failed automatic login returns to connection form") startup.error = nil await startup.restoreSessionIfNeeded() check(startup.error == nil, "Failed automatic login is not retried on view updates") let disconnected = AppModel() disconnected.locked = false disconnected.credentials = Credentials(server: "http://invalid.test", username: "test-user") disconnected.disconnect() await disconnected.restoreSessionIfNeeded() check(disconnected.error == nil && !disconnected.connected, "Explicit disconnect suppresses automatic login for this launch") let protection = AppModel() protection.locked = true; protection.biometric = true var authCalls = 0 protection.authenticate = { _ in authCalls += 1; return true } await protection.updateScene(.active) check(!protection.locked && authCalls == 1, "Face ID starts automatically on opening the locked app") await protection.updateScene(.inactive) await protection.updateScene(.active) check(authCalls == 1, "Authentication inactive/active transitions do not prompt again") await protection.updateScene(.background) check(protection.locked, "Background locks protected app") protection.authenticate = { _ in authCalls += 1; throw LAError(.userCancel) } await protection.updateScene(.active) check(protection.locked && protection.error == nil, "Cancelled Face ID stays locked without an alert") await protection.updateScene(.inactive) await protection.updateScene(.active) check(authCalls == 2, "Cancelled authentication does not automatically loop") protection.authenticate = { _ in authCalls += 1; return true } await protection.unlock() check(!protection.locked && authCalls == 3, "Manual unlock remains available after cancellation") await protection.updateScene(.background) await protection.updateScene(.active) check(!protection.locked && authCalls == 4, "Returning from background automatically authenticates again") await protection.updateScene(.background) protection.authenticate = { _ in authCalls += 1 await protection.unlock() return true } await protection.updateScene(.active) check(authCalls == 5 && !protection.authenticating, "Overlapping unlock attempts share one authentication prompt") await protection.updateScene(.background) protection.authenticate = { _ in await protection.updateScene(.background) return true } await protection.updateScene(.active) check(protection.locked, "An old authentication result cannot unlock after backgrounding") protection.authenticate = { _ in await protection.updateScene(.inactive) return true } await protection.updateScene(.active) check(!protection.locked, "Face ID success while system prompt is inactive unlocks the app") another.close(); transport.close(); print("\(checks) contract checks passed.") } }