Files
steipete--codexbar/Tests/CodexBarTests/ProviderHTTPClientTests.swift
T
2026-07-13 12:22:33 +08:00

274 lines
10 KiB
Swift

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() {}
}