chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import CodexBarCore
|
||||
|
||||
@Suite(.serialized)
|
||||
struct ProviderHTTPClientTests {
|
||||
@Test
|
||||
func `default client configuration fails blocked connections promptly`() {
|
||||
let configuration = ProviderHTTPClient.defaultConfiguration()
|
||||
|
||||
#expect(configuration.timeoutIntervalForRequest == 30)
|
||||
#expect(configuration.timeoutIntervalForResource == 90)
|
||||
#if !os(Linux)
|
||||
#expect(configuration.waitsForConnectivity == false)
|
||||
#endif
|
||||
}
|
||||
|
||||
@Test
|
||||
func `client loads requests through an injected session`() async throws {
|
||||
StubURLProtocol.requests = []
|
||||
StubURLProtocol.handler = { request in
|
||||
StubURLProtocol.requests.append(request)
|
||||
let response = try HTTPURLResponse(
|
||||
url: #require(request.url),
|
||||
statusCode: 200,
|
||||
httpVersion: "HTTP/1.1",
|
||||
headerFields: ["Content-Type": "application/json"])!
|
||||
return (Data(#"{"ok":true}"#.utf8), response)
|
||||
}
|
||||
defer {
|
||||
StubURLProtocol.handler = nil
|
||||
StubURLProtocol.requests = []
|
||||
}
|
||||
|
||||
let configuration = URLSessionConfiguration.ephemeral
|
||||
configuration.protocolClasses = [StubURLProtocol.self]
|
||||
let client = ProviderHTTPClient(session: URLSession(configuration: configuration))
|
||||
let request = try URLRequest(url: #require(URL(string: "https://example.com/status")))
|
||||
|
||||
let (data, response) = try await client.data(for: request)
|
||||
|
||||
let body = try #require(String(data: data, encoding: .utf8))
|
||||
#expect(body == #"{"ok":true}"#)
|
||||
#expect((response as? HTTPURLResponse)?.statusCode == 200)
|
||||
#expect(StubURLProtocol.requests.count == 1)
|
||||
#expect(StubURLProtocol.requests.first?.url?.host == "example.com")
|
||||
}
|
||||
|
||||
@Test
|
||||
func `response helper unwraps HTTP responses`() async throws {
|
||||
let transport = ProviderHTTPTransportHandler { request in
|
||||
let response = try HTTPURLResponse(
|
||||
url: #require(request.url),
|
||||
statusCode: 204,
|
||||
httpVersion: "HTTP/1.1",
|
||||
headerFields: ["X-Test": "ok"])!
|
||||
return (Data("done".utf8), response)
|
||||
}
|
||||
let request = try URLRequest(url: #require(URL(string: "https://example.com/ok")))
|
||||
|
||||
let response = try await transport.response(for: request)
|
||||
|
||||
#expect(response.statusCode == 204)
|
||||
#expect(response.response.value(forHTTPHeaderField: "X-Test") == "ok")
|
||||
#expect(String(data: response.data, encoding: .utf8) == "done")
|
||||
}
|
||||
|
||||
@Test
|
||||
func `response helper rejects non HTTP responses`() async throws {
|
||||
let transport = ProviderHTTPTransportHandler { request in
|
||||
let response = URLResponse(
|
||||
url: request.url ?? URL(string: "https://example.com/not-http")!,
|
||||
mimeType: nil,
|
||||
expectedContentLength: 0,
|
||||
textEncodingName: nil)
|
||||
return (Data(), response)
|
||||
}
|
||||
let request = try URLRequest(url: #require(URL(string: "https://example.com/not-http")))
|
||||
|
||||
await #expect(throws: URLError.self) {
|
||||
_ = try await transport.response(for: request)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func `response helper retries transient HTTP status once`() async throws {
|
||||
let script = ScriptedHTTPTransport(statusCodes: [503, 200])
|
||||
let request = try URLRequest(url: #require(URL(string: "https://example.com/retry")))
|
||||
|
||||
let response = try await script.response(for: request, retryPolicy: .testOneRetry)
|
||||
|
||||
#expect(response.statusCode == 200)
|
||||
#expect(await script.requestCount() == 2)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `response helper retries transient URL error once`() async throws {
|
||||
let script = ScriptedHTTPTransport(results: [
|
||||
.failure(URLError(.timedOut)),
|
||||
.success(200),
|
||||
])
|
||||
let request = try URLRequest(url: #require(URL(string: "https://example.com/retry-error")))
|
||||
|
||||
let response = try await script.response(for: request, retryPolicy: .testOneRetry)
|
||||
|
||||
#expect(response.statusCode == 200)
|
||||
#expect(await script.requestCount() == 2)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `response helper does not retry non idempotent methods`() async throws {
|
||||
let script = ScriptedHTTPTransport(statusCodes: [503, 200])
|
||||
var request = try URLRequest(url: #require(URL(string: "https://example.com/post")))
|
||||
request.httpMethod = "POST"
|
||||
|
||||
let response = try await script.response(for: request, retryPolicy: .testOneRetry)
|
||||
|
||||
#expect(response.statusCode == 503)
|
||||
#expect(await script.requestCount() == 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `response helper does not retry auth failures`() async throws {
|
||||
let script = ScriptedHTTPTransport(statusCodes: [403, 200])
|
||||
let request = try URLRequest(url: #require(URL(string: "https://example.com/forbidden")))
|
||||
|
||||
let response = try await script.response(for: request, retryPolicy: .testOneRetry)
|
||||
|
||||
#expect(response.statusCode == 403)
|
||||
#expect(await script.requestCount() == 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `redirect guard blocks cross origin redirects`() throws {
|
||||
var redirectRequest = try URLRequest(url: #require(URL(string: "https://attacker.example/capture")))
|
||||
redirectRequest.setValue("[REDACTED]", forHTTPHeaderField: "Cookie")
|
||||
redirectRequest.setValue("[REDACTED]", forHTTPHeaderField: "x-api-key")
|
||||
|
||||
let guarded = ProviderHTTPRedirectGuardDelegate.guardedRedirectRequest(
|
||||
originalURL: URL(string: "https://provider.example/usage"),
|
||||
redirectRequest: redirectRequest)
|
||||
|
||||
#expect(guarded == nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `redirect guard blocks non HTTPS redirects`() throws {
|
||||
var redirectRequest = try URLRequest(url: #require(URL(string: "http://provider.example/capture")))
|
||||
redirectRequest.setValue("[REDACTED]", forHTTPHeaderField: "Cookie")
|
||||
|
||||
let guarded = ProviderHTTPRedirectGuardDelegate.guardedRedirectRequest(
|
||||
originalURL: URL(string: "https://provider.example/usage"),
|
||||
redirectRequest: redirectRequest)
|
||||
|
||||
#expect(guarded == nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `redirect guard blocks redirects without an original URL`() throws {
|
||||
let redirectRequest = try URLRequest(url: #require(URL(string: "https://provider.example/usage/next")))
|
||||
|
||||
let guarded = ProviderHTTPRedirectGuardDelegate.guardedRedirectRequest(
|
||||
originalURL: nil,
|
||||
redirectRequest: redirectRequest)
|
||||
|
||||
#expect(guarded == nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `redirect guard blocks port changes`() throws {
|
||||
let redirectRequest = try URLRequest(url: #require(URL(string: "https://provider.example:8443/usage")))
|
||||
|
||||
let guarded = ProviderHTTPRedirectGuardDelegate.guardedRedirectRequest(
|
||||
originalURL: URL(string: "https://provider.example/usage"),
|
||||
redirectRequest: redirectRequest)
|
||||
|
||||
#expect(guarded == nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `redirect guard preserves same origin HTTPS requests`() throws {
|
||||
var redirectRequest = try URLRequest(url: #require(URL(string: "https://provider.example/usage/next")))
|
||||
redirectRequest.setValue("[REDACTED]", forHTTPHeaderField: "Cookie")
|
||||
redirectRequest.setValue("[REDACTED]", forHTTPHeaderField: "Authorization")
|
||||
redirectRequest.setValue("[REDACTED]", forHTTPHeaderField: "x-api-key")
|
||||
redirectRequest.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
|
||||
let guarded = try #require(ProviderHTTPRedirectGuardDelegate.guardedRedirectRequest(
|
||||
originalURL: URL(string: "https://provider.example/usage"),
|
||||
redirectRequest: redirectRequest))
|
||||
|
||||
#expect(guarded.value(forHTTPHeaderField: "Cookie") == "[REDACTED]")
|
||||
#expect(guarded.value(forHTTPHeaderField: "Authorization") == "[REDACTED]")
|
||||
#expect(guarded.value(forHTTPHeaderField: "x-api-key") == "[REDACTED]")
|
||||
#expect(guarded.value(forHTTPHeaderField: "Accept") == "application/json")
|
||||
}
|
||||
}
|
||||
|
||||
extension ProviderHTTPRetryPolicy {
|
||||
fileprivate static let testOneRetry = ProviderHTTPRetryPolicy(
|
||||
maxRetries: 1,
|
||||
baseDelaySeconds: 0,
|
||||
maxDelaySeconds: 0)
|
||||
}
|
||||
|
||||
private actor ScriptedHTTPTransport: ProviderHTTPTransport {
|
||||
enum Result {
|
||||
case success(Int)
|
||||
case failure(URLError)
|
||||
}
|
||||
|
||||
private var results: [Result]
|
||||
private var requests: [URLRequest] = []
|
||||
|
||||
init(statusCodes: [Int]) {
|
||||
self.results = statusCodes.map(Result.success)
|
||||
}
|
||||
|
||||
init(results: [Result]) {
|
||||
self.results = results
|
||||
}
|
||||
|
||||
func requestCount() -> Int {
|
||||
self.requests.count
|
||||
}
|
||||
|
||||
func data(for request: URLRequest) throws -> (Data, URLResponse) {
|
||||
self.requests.append(request)
|
||||
let next = self.results.isEmpty ? .success(200) : self.results.removeFirst()
|
||||
switch next {
|
||||
case let .success(statusCode):
|
||||
let response = HTTPURLResponse(
|
||||
url: request.url ?? URL(string: "https://example.com")!,
|
||||
statusCode: statusCode,
|
||||
httpVersion: "HTTP/1.1",
|
||||
headerFields: nil)!
|
||||
return (Data(#"{"ok":true}"#.utf8), response)
|
||||
case let .failure(error):
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class StubURLProtocol: URLProtocol {
|
||||
nonisolated(unsafe) static var handler: ((URLRequest) throws -> (Data, URLResponse))?
|
||||
nonisolated(unsafe) static var requests: [URLRequest] = []
|
||||
|
||||
override static func canInit(with request: URLRequest) -> Bool {
|
||||
true
|
||||
}
|
||||
|
||||
override static func canonicalRequest(for request: URLRequest) -> URLRequest {
|
||||
request
|
||||
}
|
||||
|
||||
override func startLoading() {
|
||||
guard let handler = Self.handler else {
|
||||
self.client?.urlProtocol(self, didFailWithError: URLError(.cannotLoadFromNetwork))
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let (data, response) = try handler(self.request)
|
||||
self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
|
||||
self.client?.urlProtocol(self, didLoad: data)
|
||||
self.client?.urlProtocolDidFinishLoading(self)
|
||||
} catch {
|
||||
self.client?.urlProtocol(self, didFailWithError: error)
|
||||
}
|
||||
}
|
||||
|
||||
override func stopLoading() {}
|
||||
}
|
||||
Reference in New Issue
Block a user