Files
web-terminal/ios/Packages/TestSupport/Sources/TestSupport/FakeHTTPTransport.swift
Yaojia Wang cbaa08daba feat(ios): W0 scaffold + day-1 spike + WireProtocol frozen contract + TestSupport doubles
T-iOS-1: ios/ XcodeGen project (iOS 17, Swift 6 strict concurrency, ATS per PLAN §5.2), 5 SPM package shells, CI skeleton
T-iOS-2: Origin spike vs real server — URLSessionWebSocketTask custom Origin CONFIRMED (no Starscream); 16MiB replay + EMSGSIZE(40) errno correction written back to plan
T-iOS-3: WireProtocol frozen contract, 59 tests, 100% line coverage, cross-impl vectors vs src/protocol.ts via tsx
T-iOS-4: FakeTransport/FakeClock/FakeHTTPTransport doubles
Verify: independent agent re-ran all acceptance — 6/6 PASS
2026-07-04 21:19:30 +02:00

119 lines
4.4 KiB
Swift

import Foundation
import WireProtocol
/// Errors `FakeHTTPTransport` produces on its own (Equatable for exact
/// `#expect(throws:)` assertions).
public enum FakeHTTPTransportError: Error, Equatable, Sendable {
/// `send` was called for a URL/method with no queued response the test
/// forgot to script it. Loud and identifying, never a silent hang.
case noQueuedResponse(method: String, url: URL)
/// The request had no URL at all (malformed test input).
case requestMissingURL
/// `HTTPURLResponse` refused the scripted status/headers (should not
/// happen with sane inputs; surfaced explicitly rather than force-unwrapped).
case responseConstructionFailed(url: URL)
}
/// In-memory `HTTPTransport` double (T-iOS-4). `APIClient` cannot tell it
/// apart from the `URLSession`-backed implementation (plan §3.4).
///
/// - **Queue responses per URL/method** (FIFO per route): `queueSuccess` /
/// `queueFailure`. An unqueued route throws `.noQueuedResponse` immediately.
/// - **Record requests verbatim, headers included** so Origin-iff-G tests
/// (plan §3.4 ) can assert exactly which requests carried `Origin`.
public actor FakeHTTPTransport: HTTPTransport {
/// Route identity: HTTP method (uppercased) + exact URL.
public struct RouteKey: Hashable, Sendable {
public let method: String
public let url: URL
public init(method: String, url: URL) {
self.method = method.uppercased()
self.url = url
}
}
private enum QueuedResult {
case success(status: Int, headers: [String: String], body: Data)
case failure(any Error)
}
/// Default HTTP method for queueing/matching when a request omits one.
public static let defaultMethod = "GET"
/// Default scripted success status.
public static let defaultOKStatus = 200
private static let httpVersion = "HTTP/1.1"
private var queues: [RouteKey: [QueuedResult]] = [:]
/// Every request passed to `send`, in order, verbatim (URL, method,
/// headers, body) including ones that found no queued response.
public private(set) var recordedRequests: [URLRequest] = []
public init() {}
// MARK: - Scripting
/// Queue one successful response for `method url` (FIFO per route).
public func queueSuccess(
method: String = FakeHTTPTransport.defaultMethod,
url: URL,
status: Int = FakeHTTPTransport.defaultOKStatus,
headers: [String: String] = [:],
body: Data = Data()
) {
enqueue(.success(status: status, headers: headers, body: body),
for: RouteKey(method: method, url: url))
}
/// Queue one transport-level failure for `method url` (e.g. connection
/// refused `PairingError.hostUnreachable` classification tests).
public func queueFailure(
method: String = FakeHTTPTransport.defaultMethod,
url: URL,
error: any Error
) {
enqueue(.failure(error), for: RouteKey(method: method, url: url))
}
// MARK: - HTTPTransport
public func send(_ request: URLRequest) async throws -> (Data, HTTPURLResponse) {
recordedRequests.append(request)
guard let url = request.url else {
throw FakeHTTPTransportError.requestMissingURL
}
let key = RouteKey(method: request.httpMethod ?? Self.defaultMethod, url: url)
guard var queue = queues[key], !queue.isEmpty else {
throw FakeHTTPTransportError.noQueuedResponse(method: key.method, url: url)
}
let next = queue.removeFirst()
queues[key] = queue
switch next {
case .failure(let error):
throw error
case .success(let status, let headers, let body):
return (body, try Self.makeResponse(url: url, status: status, headers: headers))
}
}
// MARK: - Internals
private func enqueue(_ result: QueuedResult, for key: RouteKey) {
queues[key, default: []].append(result)
}
private static func makeResponse(
url: URL,
status: Int,
headers: [String: String]
) throws -> HTTPURLResponse {
guard let response = HTTPURLResponse(
url: url, statusCode: status, httpVersion: httpVersion, headerFields: headers
) else {
throw FakeHTTPTransportError.responseConstructionFailed(url: url)
}
return response
}
}