Fix stray question marks

This commit is contained in:
Sami Samhuri 2025-06-15 16:59:44 -07:00
parent bcf402db8f
commit bc3ce2c93e
No known key found for this signature in database
13 changed files with 258 additions and 245 deletions

View file

@ -5,7 +5,7 @@
import Foundation import Foundation
extension NSNumber { extension NSNumber {
/// [From Argo](https://github.com/thoughtbot/Argo/blob/3da833411e2633bc01ce89542ac16803a163e0f0/Argo/Extensions/NSNumber.swift) /// [From Argo](https://github.com/thoughtbot/Argo/blob/3da833411e2633bc01ce89542ac16803a163e0f0/Argo/Extensions/NSNumber.swift)
/// ///
/// - Returns: `true` if this instance represent a `CFBoolean` under the hood, as opposed to say a double or integer. /// - Returns: `true` if this instance represent a `CFBoolean` under the hood, as opposed to say a double or integer.
@ -30,12 +30,12 @@ extension NSNumber {
/// "active": true, /// "active": true,
/// "preferences": ["color": "blue", "theme": "dark"] /// "preferences": ["color": "blue", "theme": "dark"]
/// ] /// ]
/// ///
/// let encoded = FormEncoder.encode(parameters) /// let encoded = FormEncoder.encode(parameters)
/// // Result: "active=1&age=30&email=jane%40example.net&name=Jane%20Doe&preferences%5Bcolor%5D=blue&preferences%5Btheme%5D=dark" /// // Result: "active=1&age=30&email=jane%40example.net&name=Jane%20Doe&preferences%5Bcolor%5D=blue&preferences%5Btheme%5D=dark"
/// ``` /// ```
public final class FormEncoder: CustomStringConvertible { public final class FormEncoder: CustomStringConvertible {
/// Encodes a dictionary of parameters into a URL-encoded form string. /// Encodes a dictionary of parameters into a URL-encoded form string.
/// ///
/// The encoding follows these rules: /// The encoding follows these rules:
@ -119,7 +119,7 @@ public final class FormEncoder: CustomStringConvertible {
let escaped = string.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) ?? string let escaped = string.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) ?? string
return escaped return escaped
} }
public var description: String { public var description: String {
"FormEncoder" "FormEncoder"
} }

View file

@ -8,19 +8,19 @@ import Foundation
/// Content types that can be automatically handled by HTTPRequest. /// Content types that can be automatically handled by HTTPRequest.
public enum HTTPContentType: Sendable, CustomStringConvertible { public enum HTTPContentType: Sendable, CustomStringConvertible {
/// application/x-www-form-urlencoded /// application/x-www-form-urlencoded
case formEncoded case formEncoded
/// No specific content type /// No specific content type
case none case none
/// application/json /// application/json
case json case json
/// multipart/form-data (set automatically when parts are added) /// multipart/form-data (set automatically when parts are added)
case multipart case multipart
public var description: String { public var description: String {
switch self { switch self {
case .formEncoded: case .formEncoded:

View file

@ -18,7 +18,7 @@ public enum HTTPMethod: String, Sendable, CustomStringConvertible {
var string: String { var string: String {
rawValue.uppercased() rawValue.uppercased()
} }
public var description: String { public var description: String {
string string
} }

View file

@ -51,19 +51,19 @@ public struct HTTPRequest: Sendable, CustomStringConvertible {
/// The HTTP method for this request. /// The HTTP method for this request.
public var method: HTTPMethod public var method: HTTPMethod
/// The target URL for this request. /// The target URL for this request.
public var url: URL public var url: URL
/// The content type for the request body. /// The content type for the request body.
public var contentType: HTTPContentType public var contentType: HTTPContentType
/// Parameters to be encoded according to the content type. /// Parameters to be encoded according to the content type.
public var parameters: [String: any Sendable]? public var parameters: [String: any Sendable]?
/// Additional HTTP headers for the request. /// Additional HTTP headers for the request.
public var headers: [String: String] = [:] public var headers: [String: String] = [:]
/// Multipart form parts (automatically sets contentType to .multipart when non-empty). /// Multipart form parts (automatically sets contentType to .multipart when non-empty).
public var parts: [MultipartFormEncoder.Part] = [] { public var parts: [MultipartFormEncoder.Part] = [] {
didSet { didSet {
@ -123,7 +123,7 @@ public struct HTTPRequest: Sendable, CustomStringConvertible {
} }
#if canImport(UIKit) #if canImport(UIKit)
/// Adds a JPEG image to the multipart form (iOS/tvOS only). /// Adds a JPEG image to the multipart form (iOS/tvOS only).
/// - Parameters: /// - Parameters:
/// - name: The form field name /// - name: The form field name
@ -148,7 +148,7 @@ public struct HTTPRequest: Sendable, CustomStringConvertible {
public mutating func addHeader(name: String, value: String) { public mutating func addHeader(name: String, value: String) {
headers[name] = value headers[name] = value
} }
public var description: String { public var description: String {
"<HTTPRequest \(method) \(url)>" "<HTTPRequest \(method) \(url)>"
} }

View file

@ -8,13 +8,13 @@ import Foundation
/// Specific errors for HTTP request processing. /// Specific errors for HTTP request processing.
public enum HTTPRequestError: Error, LocalizedError, CustomStringConvertible { public enum HTTPRequestError: Error, LocalizedError, CustomStringConvertible {
/// An HTTP error occurred (non-2xx status code). /// An HTTP error occurred (non-2xx status code).
case http case http
/// An unknown error occurred (typically when URLResponse isn't HTTPURLResponse). /// An unknown error occurred (typically when URLResponse isn't HTTPURLResponse).
case unknown case unknown
public var errorDescription: String? { public var errorDescription: String? {
switch self { switch self {
case .http: case .http:
@ -23,7 +23,7 @@ public enum HTTPRequestError: Error, LocalizedError, CustomStringConvertible {
return "An unknown error occurred" return "An unknown error occurred"
} }
} }
public var failureReason: String? { public var failureReason: String? {
switch self { switch self {
case .http: case .http:
@ -32,7 +32,7 @@ public enum HTTPRequestError: Error, LocalizedError, CustomStringConvertible {
return "An unexpected error occurred during the request" return "An unexpected error occurred during the request"
} }
} }
public var recoverySuggestion: String? { public var recoverySuggestion: String? {
switch self { switch self {
case .http: case .http:
@ -41,7 +41,7 @@ public enum HTTPRequestError: Error, LocalizedError, CustomStringConvertible {
return "Check network connectivity and try again" return "Check network connectivity and try again"
} }
} }
public var description: String { public var description: String {
switch self { switch self {
case .http: case .http:

View file

@ -20,7 +20,7 @@ private let log = Logger(subsystem: "co.1se.Osiris", category: "HTTPResponse")
/// ```swift /// ```swift
/// let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in /// let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
/// let httpResponse = HTTPResponse(response: response, data: data, error: error) /// let httpResponse = HTTPResponse(response: response, data: data, error: error)
/// ///
/// switch httpResponse { /// switch httpResponse {
/// case .success(let httpURLResponse, let data): /// case .success(let httpURLResponse, let data):
/// print("Success: \(httpURLResponse.statusCode)") /// print("Success: \(httpURLResponse.statusCode)")
@ -32,10 +32,10 @@ private let log = Logger(subsystem: "co.1se.Osiris", category: "HTTPResponse")
/// } /// }
/// ``` /// ```
public enum HTTPResponse: CustomStringConvertible { public enum HTTPResponse: CustomStringConvertible {
/// A successful response (2xx status code) with the HTTP response and optional body data. /// A successful response (2xx status code) with the HTTP response and optional body data.
case success(HTTPURLResponse, Data?) case success(HTTPURLResponse, Data?)
/// A failed response with the error, optional HTTP response, and optional body data. /// A failed response with the error, optional HTTP response, and optional body data.
case failure(Error, HTTPURLResponse?, Data?) case failure(Error, HTTPURLResponse?, Data?)
@ -125,7 +125,7 @@ public enum HTTPResponse: CustomStringConvertible {
return [:] return [:]
} }
} }
public var description: String { public var description: String {
switch self { switch self {
case let .success(response, data): case let .success(response, data):

View file

@ -24,13 +24,13 @@
import Foundation import Foundation
extension MultipartFormEncoder { extension MultipartFormEncoder {
/// Contains the encoded multipart form data for in-memory storage. /// Contains the encoded multipart form data for in-memory storage.
public struct BodyData: CustomStringConvertible { public struct BodyData: CustomStringConvertible {
/// The content type header value including boundary. /// The content type header value including boundary.
public let contentType: String public let contentType: String
/// The encoded form data. /// The encoded form data.
public let data: Data public let data: Data
@ -38,7 +38,7 @@ extension MultipartFormEncoder {
public var contentLength: Int { public var contentLength: Int {
data.count data.count
} }
public var description: String { public var description: String {
"<BodyData size=\(contentLength)>" "<BodyData size=\(contentLength)>"
} }
@ -46,16 +46,16 @@ extension MultipartFormEncoder {
/// Contains the encoded multipart form data written to a file for streaming. /// Contains the encoded multipart form data written to a file for streaming.
public struct BodyFile: CustomStringConvertible { public struct BodyFile: CustomStringConvertible {
/// The content type header value including boundary. /// The content type header value including boundary.
public let contentType: String public let contentType: String
/// The URL of the temporary file containing the encoded data. /// The URL of the temporary file containing the encoded data.
public let url: URL public let url: URL
/// The length of the encoded data in bytes. /// The length of the encoded data in bytes.
public let contentLength: Int64 public let contentLength: Int64
public var description: String { public var description: String {
"<BodyFile file=\(url.lastPathComponent) size=\(contentLength)>" "<BodyFile file=\(url.lastPathComponent) size=\(contentLength)>"
} }
@ -66,16 +66,16 @@ extension MultipartFormEncoder {
/// The content types supported in multipart forms. /// The content types supported in multipart forms.
public enum Content: Equatable, Sendable, CustomStringConvertible { public enum Content: Equatable, Sendable, CustomStringConvertible {
/// Plain text content. /// Plain text content.
case text(String) case text(String)
/// Binary data with MIME type and filename. /// Binary data with MIME type and filename.
case binaryData(Data, type: String, filename: String) case binaryData(Data, type: String, filename: String)
/// Binary data from a file with size, MIME type and filename. /// Binary data from a file with size, MIME type and filename.
case binaryFile(URL, size: Int64, type: String, filename: String) case binaryFile(URL, size: Int64, type: String, filename: String)
public var description: String { public var description: String {
switch self { switch self {
case let .text(value): case let .text(value):
@ -91,7 +91,7 @@ extension MultipartFormEncoder {
/// The form field name for this part. /// The form field name for this part.
public let name: String public let name: String
/// The content of this part. /// The content of this part.
public let content: Content public let content: Content
@ -130,7 +130,7 @@ extension MultipartFormEncoder {
} }
return Part(name: name, content: .binaryFile(url, size: size, type: type, filename: filename ?? url.lastPathComponent)) return Part(name: name, content: .binaryFile(url, size: size, type: type, filename: filename ?? url.lastPathComponent))
} }
public var description: String { public var description: String {
"<Part name=\(name) content=\(content)>" "<Part name=\(name) content=\(content)>"
} }
@ -151,30 +151,30 @@ extension MultipartFormEncoder {
/// .text("jane@example.net", name: "email"), /// .text("jane@example.net", name: "email"),
/// .data(imageData, name: "avatar", type: "image/jpeg", filename: "avatar.jpg") /// .data(imageData, name: "avatar", type: "image/jpeg", filename: "avatar.jpg")
/// ] /// ]
/// ///
/// // Encode to memory (< 50MB) /// // Encode to memory (< 50MB)
/// let bodyData = try encoder.encodeData(parts: parts) /// let bodyData = try encoder.encodeData(parts: parts)
/// ///
/// // Or encode to file for streaming /// // Or encode to file for streaming
/// let bodyFile = try encoder.encodeFile(parts: parts) /// let bodyFile = try encoder.encodeFile(parts: parts)
/// ``` /// ```
public final class MultipartFormEncoder: CustomStringConvertible { public final class MultipartFormEncoder: CustomStringConvertible {
/// Errors that can occur during multipart encoding. /// Errors that can occur during multipart encoding.
public enum Error: Swift.Error, CustomStringConvertible { public enum Error: Swift.Error, CustomStringConvertible {
/// The specified file cannot be read or is invalid. /// The specified file cannot be read or is invalid.
case invalidFile(URL) case invalidFile(URL)
/// The output file cannot be created or written to. /// The output file cannot be created or written to.
case invalidOutputFile(URL) case invalidOutputFile(URL)
/// An error occurred while reading from or writing to a stream. /// An error occurred while reading from or writing to a stream.
case streamError case streamError
/// The total data size exceeds the 50MB limit for in-memory encoding. /// The total data size exceeds the 50MB limit for in-memory encoding.
case tooMuchDataForMemory case tooMuchDataForMemory
public var description: String { public var description: String {
switch self { switch self {
case let .invalidFile(url): case let .invalidFile(url):
@ -359,7 +359,7 @@ public final class MultipartFormEncoder: CustomStringConvertible {
} }
} }
} }
public var description: String { public var description: String {
"<MultipartFormEncoder boundary=\(boundary)>" "<MultipartFormEncoder boundary=\(boundary)>"
} }

View file

@ -11,7 +11,7 @@ private let log = Logger(subsystem: "co.1se.Osiris", category: "RequestBuilder")
/// Errors that can occur when building URLRequest from HTTPRequest. /// Errors that can occur when building URLRequest from HTTPRequest.
public enum RequestBuilderError: Error { public enum RequestBuilderError: Error {
/// The form data could not be encoded properly. /// The form data could not be encoded properly.
case invalidFormData(HTTPRequest) case invalidFormData(HTTPRequest)
} }
@ -31,7 +31,7 @@ public enum RequestBuilderError: Error {
/// contentType: .json, /// contentType: .json,
/// parameters: ["name": "Jane", "email": "jane@example.net"] /// parameters: ["name": "Jane", "email": "jane@example.net"]
/// ) /// )
/// ///
/// let urlRequest = try RequestBuilder.build(request: httpRequest) /// let urlRequest = try RequestBuilder.build(request: httpRequest)
/// let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in /// let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
/// let httpResponse = HTTPResponse(response: response, data: data, error: error) /// let httpResponse = HTTPResponse(response: response, data: data, error: error)
@ -39,7 +39,7 @@ public enum RequestBuilderError: Error {
/// } /// }
/// ``` /// ```
public final class RequestBuilder { public final class RequestBuilder {
/// Converts an HTTPRequest to a URLRequest ready for use with URLSession. /// Converts an HTTPRequest to a URLRequest ready for use with URLSession.
/// ///
/// This method handles encoding of parameters according to the request's method and content type: /// This method handles encoding of parameters according to the request's method and content type:
@ -60,7 +60,7 @@ public final class RequestBuilder {
public class func build(request: HTTPRequest) throws -> URLRequest { public class func build(request: HTTPRequest) throws -> URLRequest {
var result = URLRequest(url: request.url) var result = URLRequest(url: request.url)
result.httpMethod = request.method.string result.httpMethod = request.method.string
for (name, value) in request.headers { for (name, value) in request.headers {
result.addValue(value, forHTTPHeaderField: name) result.addValue(value, forHTTPHeaderField: name)
} }
@ -83,7 +83,7 @@ public final class RequestBuilder {
return result return result
} }
private class func encodeMultipartContent(to urlRequest: inout URLRequest, request: HTTPRequest) throws { private class func encodeMultipartContent(to urlRequest: inout URLRequest, request: HTTPRequest) throws {
let encoder = MultipartFormEncoder() let encoder = MultipartFormEncoder()
let body = try encoder.encodeData(parts: request.parts) let body = try encoder.encodeData(parts: request.parts)
@ -91,28 +91,28 @@ public final class RequestBuilder {
urlRequest.addValue("\(body.contentLength)", forHTTPHeaderField: "Content-Length") urlRequest.addValue("\(body.contentLength)", forHTTPHeaderField: "Content-Length")
urlRequest.httpBody = body.data urlRequest.httpBody = body.data
} }
private class func encodeParameters(to urlRequest: inout URLRequest, request: HTTPRequest, parameters: [String: any Sendable]) throws { private class func encodeParameters(to urlRequest: inout URLRequest, request: HTTPRequest, parameters: [String: any Sendable]) throws {
switch request.contentType { switch request.contentType {
case .json: case .json:
try encodeJSONParameters(to: &urlRequest, parameters: parameters) try encodeJSONParameters(to: &urlRequest, parameters: parameters)
case .none: case .none:
log.warning("Cannot serialize parameters without a content type, falling back to form encoding") log.warning("Cannot serialize parameters without a content type, falling back to form encoding")
fallthrough fallthrough
case .formEncoded: case .formEncoded:
try encodeFormParameters(to: &urlRequest, request: request, parameters: parameters) try encodeFormParameters(to: &urlRequest, request: request, parameters: parameters)
case .multipart: case .multipart:
try encodeMultipartContent(to: &urlRequest, request: request) try encodeMultipartContent(to: &urlRequest, request: request)
} }
} }
private class func encodeJSONParameters(to urlRequest: inout URLRequest, parameters: [String: any Sendable]) throws { private class func encodeJSONParameters(to urlRequest: inout URLRequest, parameters: [String: any Sendable]) throws {
urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type") urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
urlRequest.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: []) urlRequest.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: [])
} }
private class func encodeFormParameters(to urlRequest: inout URLRequest, request: HTTPRequest, parameters: [String: any Sendable]) throws { private class func encodeFormParameters(to urlRequest: inout URLRequest, request: HTTPRequest, parameters: [String: any Sendable]) throws {
urlRequest.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") urlRequest.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
guard let formData = FormEncoder.encode(parameters).data(using: .utf8) else { guard let formData = FormEncoder.encode(parameters).data(using: .utf8) else {
@ -120,23 +120,23 @@ public final class RequestBuilder {
} }
urlRequest.httpBody = formData urlRequest.httpBody = formData
} }
private class func encodeQueryParameters(to urlRequest: inout URLRequest, parameters: [String: any Sendable]) throws { private class func encodeQueryParameters(to urlRequest: inout URLRequest, parameters: [String: any Sendable]) throws {
guard let url = urlRequest.url else { guard let url = urlRequest.url else {
return return
} }
var components = URLComponents(url: url, resolvingAgainstBaseURL: false) var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
let newQueryItems = parameters.compactMap { (key, value) -> URLQueryItem? in let newQueryItems = parameters.compactMap { (key, value) -> URLQueryItem? in
URLQueryItem(name: key, value: String(describing: value)) URLQueryItem(name: key, value: String(describing: value))
} }
if let existingQueryItems = components?.queryItems { if let existingQueryItems = components?.queryItems {
components?.queryItems = existingQueryItems + newQueryItems components?.queryItems = existingQueryItems + newQueryItems
} else { } else if !newQueryItems.isEmpty {
components?.queryItems = newQueryItems components?.queryItems = newQueryItems
} }
urlRequest.url = components?.url ?? url urlRequest.url = components?.url ?? url
} }
} }

View file

@ -13,44 +13,44 @@ class FormEncoderTests: XCTestCase {
let result = FormEncoder.encode([:]) let result = FormEncoder.encode([:])
XCTAssertEqual(result, "") XCTAssertEqual(result, "")
} }
func testEncodeSingleStringValue() { func testEncodeSingleStringValue() {
let parameters = ["name": "Jane Doe"] let parameters = ["name": "Jane Doe"]
let result = FormEncoder.encode(parameters) let result = FormEncoder.encode(parameters)
XCTAssertEqual(result, "name=Jane%20Doe") XCTAssertEqual(result, "name=Jane%20Doe")
} }
func testEncodeMultipleStringValues() { func testEncodeMultipleStringValues() {
let parameters = ["name": "John", "email": "john@example.net"] let parameters = ["name": "John", "email": "john@example.net"]
let result = FormEncoder.encode(parameters) let result = FormEncoder.encode(parameters)
// Keys should be sorted alphabetically // Keys should be sorted alphabetically
XCTAssertEqual(result, "email=john%40example.net&name=John") XCTAssertEqual(result, "email=john%40example.net&name=John")
} }
func testEncodeIntegerValue() { func testEncodeIntegerValue() {
let parameters = ["age": 30] let parameters = ["age": 30]
let result = FormEncoder.encode(parameters) let result = FormEncoder.encode(parameters)
XCTAssertEqual(result, "age=30") XCTAssertEqual(result, "age=30")
} }
func testEncodeBooleanValues() { func testEncodeBooleanValues() {
let parameters = ["active": true, "verified": false] let parameters = ["active": true, "verified": false]
let result = FormEncoder.encode(parameters) let result = FormEncoder.encode(parameters)
XCTAssertEqual(result, "active=1&verified=0") XCTAssertEqual(result, "active=1&verified=0")
} }
func testEncodeNSNumberBooleanValues() { func testEncodeNSNumberBooleanValues() {
let parameters = ["active": NSNumber(value: true), "verified": NSNumber(value: false)] let parameters = ["active": NSNumber(value: true), "verified": NSNumber(value: false)]
let result = FormEncoder.encode(parameters) let result = FormEncoder.encode(parameters)
XCTAssertEqual(result, "active=1&verified=0") XCTAssertEqual(result, "active=1&verified=0")
} }
func testEncodeNSNumberIntegerValues() { func testEncodeNSNumberIntegerValues() {
let parameters = ["count": NSNumber(value: 42)] let parameters = ["count": NSNumber(value: 42)]
let result = FormEncoder.encode(parameters) let result = FormEncoder.encode(parameters)
XCTAssertEqual(result, "count=42") XCTAssertEqual(result, "count=42")
} }
func testEncodeNestedDictionary() { func testEncodeNestedDictionary() {
let personData: [String: any Sendable] = ["name": "Jane", "age": 30] let personData: [String: any Sendable] = ["name": "Jane", "age": 30]
let parameters: [String: any Sendable] = ["person": personData] let parameters: [String: any Sendable] = ["person": personData]
@ -60,13 +60,13 @@ class FormEncoderTests: XCTestCase {
let expected2 = "person%5Bname%5D=Jane&person%5Bage%5D=30" let expected2 = "person%5Bname%5D=Jane&person%5Bage%5D=30"
XCTAssertTrue(result == expected1 || result == expected2, "Result '\(result)' doesn't match either expected format") XCTAssertTrue(result == expected1 || result == expected2, "Result '\(result)' doesn't match either expected format")
} }
func testEncodeArray() { func testEncodeArray() {
let parameters = ["tags": ["swift", "ios", "mobile"]] let parameters = ["tags": ["swift", "ios", "mobile"]]
let result = FormEncoder.encode(parameters) let result = FormEncoder.encode(parameters)
XCTAssertEqual(result, "tags%5B%5D=swift&tags%5B%5D=ios&tags%5B%5D=mobile") XCTAssertEqual(result, "tags%5B%5D=swift&tags%5B%5D=ios&tags%5B%5D=mobile")
} }
func testEncodeComplexNestedStructure() { func testEncodeComplexNestedStructure() {
let preferences: [String: any Sendable] = ["theme": "dark", "notifications": true] let preferences: [String: any Sendable] = ["theme": "dark", "notifications": true]
let tags: [any Sendable] = ["rockstar", "swiftie"] let tags: [any Sendable] = ["rockstar", "swiftie"]
@ -76,7 +76,7 @@ class FormEncoderTests: XCTestCase {
"tags": tags "tags": tags
] ]
let parameters: [String: any Sendable] = ["person": personData] let parameters: [String: any Sendable] = ["person": personData]
let result = FormEncoder.encode(parameters) let result = FormEncoder.encode(parameters)
// The actual order depends on how the dictionary is sorted, so let's test the components // The actual order depends on how the dictionary is sorted, so let's test the components
XCTAssertTrue(result.contains("person%5Bname%5D=Jane")) XCTAssertTrue(result.contains("person%5Bname%5D=Jane"))
@ -85,38 +85,38 @@ class FormEncoderTests: XCTestCase {
XCTAssertTrue(result.contains("person%5Btags%5D%5B%5D=rockstar")) XCTAssertTrue(result.contains("person%5Btags%5D%5B%5D=rockstar"))
XCTAssertTrue(result.contains("person%5Btags%5D%5B%5D=swiftie")) XCTAssertTrue(result.contains("person%5Btags%5D%5B%5D=swiftie"))
} }
func testEncodeSpecialCharacters() { func testEncodeSpecialCharacters() {
let parameters = ["message": "Hello & welcome to Abbey Road Studios! 100% music magic guaranteed."] let parameters = ["message": "Hello & welcome to Abbey Road Studios! 100% music magic guaranteed."]
let result = FormEncoder.encode(parameters) let result = FormEncoder.encode(parameters)
XCTAssertEqual(result, "message=Hello%20%26%20welcome%20to%20Abbey%20Road%20Studios%21%20100%25%20music%20magic%20guaranteed.") XCTAssertEqual(result, "message=Hello%20%26%20welcome%20to%20Abbey%20Road%20Studios%21%20100%25%20music%20magic%20guaranteed.")
} }
func testEncodeUnicodeCharacters() { func testEncodeUnicodeCharacters() {
let parameters = ["emoji": "🚀👨‍💻", "chinese": "你好"] let parameters = ["emoji": "🚀👨‍💻", "chinese": "你好"]
let result = FormEncoder.encode(parameters) let result = FormEncoder.encode(parameters)
XCTAssertEqual(result, "chinese=%E4%BD%A0%E5%A5%BD&emoji=%F0%9F%9A%80%F0%9F%91%A8%E2%80%8D%F0%9F%92%BB") XCTAssertEqual(result, "chinese=%E4%BD%A0%E5%A5%BD&emoji=%F0%9F%9A%80%F0%9F%91%A8%E2%80%8D%F0%9F%92%BB")
} }
func testKeysAreSortedAlphabetically() { func testKeysAreSortedAlphabetically() {
let parameters = ["zebra": "z", "alpha": "a", "beta": "b"] let parameters = ["zebra": "z", "alpha": "a", "beta": "b"]
let result = FormEncoder.encode(parameters) let result = FormEncoder.encode(parameters)
XCTAssertEqual(result, "alpha=a&beta=b&zebra=z") XCTAssertEqual(result, "alpha=a&beta=b&zebra=z")
} }
func testEncodeDoubleValue() { func testEncodeDoubleValue() {
let parameters = ["price": 19.99] let parameters = ["price": 19.99]
let result = FormEncoder.encode(parameters) let result = FormEncoder.encode(parameters)
XCTAssertEqual(result, "price=19.99") XCTAssertEqual(result, "price=19.99")
} }
func testEncodeNilValuesAsStrings() { func testEncodeNilValuesAsStrings() {
// Swift's Any type handling - nil values become "<null>" strings // Swift's Any type handling - nil values become "<null>" strings
let parameters = ["optional": NSNull()] let parameters = ["optional": NSNull()]
let result = FormEncoder.encode(parameters) let result = FormEncoder.encode(parameters)
XCTAssertEqual(result, "optional=%3Cnull%3E") XCTAssertEqual(result, "optional=%3Cnull%3E")
} }
func testRFC3986Compliance() { func testRFC3986Compliance() {
// Test that reserved characters are properly encoded according to RFC 3986 // Test that reserved characters are properly encoded according to RFC 3986
let parameters = ["reserved": "!*'();:@&=+$,/?#[]"] let parameters = ["reserved": "!*'();:@&=+$,/?#[]"]
@ -124,14 +124,14 @@ class FormEncoderTests: XCTestCase {
// According to the implementation, ? and / are NOT encoded per RFC 3986 Section 3.4 // According to the implementation, ? and / are NOT encoded per RFC 3986 Section 3.4
XCTAssertEqual(result, "reserved=%21%2A%27%28%29%3B%3A%40%26%3D%2B%24%2C/?%23%5B%5D") XCTAssertEqual(result, "reserved=%21%2A%27%28%29%3B%3A%40%26%3D%2B%24%2C/?%23%5B%5D")
} }
func testURLQueryAllowedCharacters() { func testURLQueryAllowedCharacters() {
// Test characters that should NOT be encoded // Test characters that should NOT be encoded
let parameters = ["allowed": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"] let parameters = ["allowed": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"]
let result = FormEncoder.encode(parameters) let result = FormEncoder.encode(parameters)
XCTAssertEqual(result, "allowed=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~") XCTAssertEqual(result, "allowed=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~")
} }
func testMixedDataTypes() { func testMixedDataTypes() {
let array: [any Sendable] = [1, 2, 3] let array: [any Sendable] = [1, 2, 3]
let nested: [String: any Sendable] = ["key": "nested_value"] let nested: [String: any Sendable] = ["key": "nested_value"]
@ -143,7 +143,7 @@ class FormEncoderTests: XCTestCase {
"array": array, "array": array,
"nested": nested "nested": nested
] ]
let result = FormEncoder.encode(parameters) let result = FormEncoder.encode(parameters)
let expected = "array%5B%5D=1&array%5B%5D=2&array%5B%5D=3&boolean=1&double=3.14&integer=42&nested%5Bkey%5D=nested_value&string=value" let expected = "array%5B%5D=1&array%5B%5D=2&array%5B%5D=3&boolean=1&double=3.14&integer=42&nested%5Bkey%5D=nested_value&string=value"
XCTAssertEqual(result, expected) XCTAssertEqual(result, expected)
@ -152,29 +152,29 @@ class FormEncoderTests: XCTestCase {
// Test the NSNumber extension // Test the NSNumber extension
class NSNumberBoolExtensionTests: XCTestCase { class NSNumberBoolExtensionTests: XCTestCase {
func testNSNumberIsBoolForBooleans() { func testNSNumberIsBoolForBooleans() {
let trueNumber = NSNumber(value: true) let trueNumber = NSNumber(value: true)
let falseNumber = NSNumber(value: false) let falseNumber = NSNumber(value: false)
XCTAssertTrue(trueNumber.isBool) XCTAssertTrue(trueNumber.isBool)
XCTAssertTrue(falseNumber.isBool) XCTAssertTrue(falseNumber.isBool)
} }
func testNSNumberIsBoolForIntegers() { func testNSNumberIsBoolForIntegers() {
let intNumber = NSNumber(value: 42) let intNumber = NSNumber(value: 42)
let zeroNumber = NSNumber(value: 0) let zeroNumber = NSNumber(value: 0)
let oneNumber = NSNumber(value: 1) let oneNumber = NSNumber(value: 1)
XCTAssertFalse(intNumber.isBool) XCTAssertFalse(intNumber.isBool)
XCTAssertFalse(zeroNumber.isBool) XCTAssertFalse(zeroNumber.isBool)
XCTAssertFalse(oneNumber.isBool) XCTAssertFalse(oneNumber.isBool)
} }
func testNSNumberIsBoolForDoubles() { func testNSNumberIsBoolForDoubles() {
let doubleNumber = NSNumber(value: 3.14) let doubleNumber = NSNumber(value: 3.14)
let zeroDouble = NSNumber(value: 0.0) let zeroDouble = NSNumber(value: 0.0)
XCTAssertFalse(doubleNumber.isBool) XCTAssertFalse(doubleNumber.isBool)
XCTAssertFalse(zeroDouble.isBool) XCTAssertFalse(zeroDouble.isBool)
} }

View file

@ -9,51 +9,51 @@
import XCTest import XCTest
class HTTPRequestErrorTests: XCTestCase { class HTTPRequestErrorTests: XCTestCase {
func testHTTPError() { func testHTTPError() {
let error = HTTPRequestError.http let error = HTTPRequestError.http
XCTAssertEqual(error.localizedDescription, "HTTP request failed with non-2xx status code") XCTAssertEqual(error.localizedDescription, "HTTP request failed with non-2xx status code")
XCTAssertEqual(error.failureReason, "The server returned an error status code") XCTAssertEqual(error.failureReason, "The server returned an error status code")
XCTAssertEqual(error.recoverySuggestion, "Check the server response for error details") XCTAssertEqual(error.recoverySuggestion, "Check the server response for error details")
} }
func testUnknownError() { func testUnknownError() {
let error = HTTPRequestError.unknown let error = HTTPRequestError.unknown
XCTAssertEqual(error.localizedDescription, "An unknown error occurred") XCTAssertEqual(error.localizedDescription, "An unknown error occurred")
XCTAssertEqual(error.failureReason, "An unexpected error occurred during the request") XCTAssertEqual(error.failureReason, "An unexpected error occurred during the request")
XCTAssertEqual(error.recoverySuggestion, "Check network connectivity and try again") XCTAssertEqual(error.recoverySuggestion, "Check network connectivity and try again")
} }
func testErrorDescriptionIsNeverNil() { func testErrorDescriptionIsNeverNil() {
let allErrors: [HTTPRequestError] = [ let allErrors: [HTTPRequestError] = [
.http, .http,
.unknown .unknown
] ]
for error in allErrors { for error in allErrors {
XCTAssertNotNil(error.errorDescription) XCTAssertNotNil(error.errorDescription)
XCTAssertFalse(error.errorDescription!.isEmpty) XCTAssertFalse(error.errorDescription!.isEmpty)
} }
} }
func testFailureReasonIsNeverNil() { func testFailureReasonIsNeverNil() {
let allErrors: [HTTPRequestError] = [ let allErrors: [HTTPRequestError] = [
.http, .http,
.unknown .unknown
] ]
for error in allErrors { for error in allErrors {
XCTAssertNotNil(error.failureReason) XCTAssertNotNil(error.failureReason)
XCTAssertFalse(error.failureReason!.isEmpty) XCTAssertFalse(error.failureReason!.isEmpty)
} }
} }
func testRecoverySuggestionIsNeverNil() { func testRecoverySuggestionIsNeverNil() {
let allErrors: [HTTPRequestError] = [ let allErrors: [HTTPRequestError] = [
.http, .http,
.unknown .unknown
] ]
for error in allErrors { for error in allErrors {
XCTAssertNotNil(error.recoverySuggestion) XCTAssertNotNil(error.recoverySuggestion)
XCTAssertFalse(error.recoverySuggestion!.isEmpty) XCTAssertFalse(error.recoverySuggestion!.isEmpty)

View file

@ -10,7 +10,7 @@ import XCTest
class HTTPRequestTests: XCTestCase { class HTTPRequestTests: XCTestCase {
let baseURL = URL(string: "https://api.example.net")! let baseURL = URL(string: "https://api.example.net")!
func testHTTPRequestInitialization() { func testHTTPRequestInitialization() {
let request = HTTPRequest(method: .get, url: baseURL) let request = HTTPRequest(method: .get, url: baseURL)
XCTAssertEqual(request.method, .get) XCTAssertEqual(request.method, .get)
@ -20,74 +20,74 @@ class HTTPRequestTests: XCTestCase {
XCTAssertTrue(request.headers.isEmpty) XCTAssertTrue(request.headers.isEmpty)
XCTAssertTrue(request.parts.isEmpty) XCTAssertTrue(request.parts.isEmpty)
} }
func testHTTPRequestWithParameters() { func testHTTPRequestWithParameters() {
let params = ["key": "value", "number": 42] as [String: any Sendable] let params = ["key": "value", "number": 42] as [String: any Sendable]
let request = HTTPRequest(method: .post, url: baseURL, contentType: .json, parameters: params) let request = HTTPRequest(method: .post, url: baseURL, contentType: .json, parameters: params)
XCTAssertEqual(request.method, .post) XCTAssertEqual(request.method, .post)
XCTAssertEqual(request.contentType, .json) XCTAssertEqual(request.contentType, .json)
XCTAssertNotNil(request.parameters) XCTAssertNotNil(request.parameters)
} }
func testGETConvenience() { func testGETConvenience() {
let request = HTTPRequest.get(baseURL) let request = HTTPRequest.get(baseURL)
XCTAssertEqual(request.method, .get) XCTAssertEqual(request.method, .get)
XCTAssertEqual(request.url, baseURL) XCTAssertEqual(request.url, baseURL)
XCTAssertEqual(request.contentType, .none) XCTAssertEqual(request.contentType, .none)
} }
func testPOSTConvenience() { func testPOSTConvenience() {
let params = ["name": "Jane"] let params = ["name": "Jane"]
let request = HTTPRequest.post(baseURL, contentType: .json, parameters: params) let request = HTTPRequest.post(baseURL, contentType: .json, parameters: params)
XCTAssertEqual(request.method, .post) XCTAssertEqual(request.method, .post)
XCTAssertEqual(request.contentType, .json) XCTAssertEqual(request.contentType, .json)
XCTAssertNotNil(request.parameters) XCTAssertNotNil(request.parameters)
} }
func testPUTConvenience() { func testPUTConvenience() {
let params = ["name": "Jane"] let params = ["name": "Jane"]
let request = HTTPRequest.put(baseURL, contentType: .formEncoded, parameters: params) let request = HTTPRequest.put(baseURL, contentType: .formEncoded, parameters: params)
XCTAssertEqual(request.method, .put) XCTAssertEqual(request.method, .put)
XCTAssertEqual(request.contentType, .formEncoded) XCTAssertEqual(request.contentType, .formEncoded)
XCTAssertNotNil(request.parameters) XCTAssertNotNil(request.parameters)
} }
func testDELETEConvenience() { func testDELETEConvenience() {
let request = HTTPRequest.delete(baseURL) let request = HTTPRequest.delete(baseURL)
XCTAssertEqual(request.method, .delete) XCTAssertEqual(request.method, .delete)
XCTAssertEqual(request.url, baseURL) XCTAssertEqual(request.url, baseURL)
XCTAssertEqual(request.contentType, .none) XCTAssertEqual(request.contentType, .none)
} }
func testMultipartPartsAutomaticallySetContentType() { func testMultipartPartsAutomaticallySetContentType() {
var request = HTTPRequest.post(baseURL) var request = HTTPRequest.post(baseURL)
XCTAssertEqual(request.contentType, .none) XCTAssertEqual(request.contentType, .none)
request.parts = [.text("value", name: "field")] request.parts = [.text("value", name: "field")]
XCTAssertEqual(request.contentType, .multipart) XCTAssertEqual(request.contentType, .multipart)
} }
#if canImport(UIKit) #if canImport(UIKit)
func testAddMultipartJPEG() { func testAddMultipartJPEG() {
var request = HTTPRequest.post(baseURL) var request = HTTPRequest.post(baseURL)
// Create a simple 1x1 pixel image // Create a simple 1x1 pixel image
let size = CGSize(width: 1, height: 1) let size = CGSize(width: 1, height: 1)
UIGraphicsBeginImageContext(size) UIGraphicsBeginImageContext(size)
let image = UIGraphicsGetImageFromCurrentImageContext()! let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext() UIGraphicsEndImageContext()
request.addMultipartJPEG(name: "avatar", image: image, quality: 0.8, filename: "test.jpg") request.addMultipartJPEG(name: "avatar", image: image, quality: 0.8, filename: "test.jpg")
XCTAssertEqual(request.parts.count, 1) XCTAssertEqual(request.parts.count, 1)
XCTAssertEqual(request.contentType, .multipart) XCTAssertEqual(request.contentType, .multipart)
let part = request.parts.first! let part = request.parts.first!
XCTAssertEqual(part.name, "avatar") XCTAssertEqual(part.name, "avatar")
if case let .binaryData(_, type, filename) = part.content { if case let .binaryData(_, type, filename) = part.content {
XCTAssertEqual(type, "image/jpeg") XCTAssertEqual(type, "image/jpeg")
XCTAssertEqual(filename, "test.jpg") XCTAssertEqual(filename, "test.jpg")
@ -95,71 +95,71 @@ class HTTPRequestTests: XCTestCase {
XCTFail("Expected binary data content") XCTFail("Expected binary data content")
} }
} }
func testAddMultipartJPEGWithInvalidQuality() { func testAddMultipartJPEGWithInvalidQuality() {
var request = HTTPRequest.post(baseURL) var request = HTTPRequest.post(baseURL)
// Create a valid image // Create a valid image
let size = CGSize(width: 1, height: 1) let size = CGSize(width: 1, height: 1)
UIGraphicsBeginImageContext(size) UIGraphicsBeginImageContext(size)
let image = UIGraphicsGetImageFromCurrentImageContext()! let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext() UIGraphicsEndImageContext()
// Test with extreme quality values that might cause issues // Test with extreme quality values that might cause issues
request.addMultipartJPEG(name: "avatar1", image: image, quality: -1.0) request.addMultipartJPEG(name: "avatar1", image: image, quality: -1.0)
request.addMultipartJPEG(name: "avatar2", image: image, quality: 2.0) request.addMultipartJPEG(name: "avatar2", image: image, quality: 2.0)
// The method should handle extreme quality values gracefully // The method should handle extreme quality values gracefully
// Either by clamping them or by having jpegData handle them // Either by clamping them or by having jpegData handle them
XCTAssertTrue(request.parts.count >= 0) // Should not crash XCTAssertTrue(request.parts.count >= 0) // Should not crash
} }
#endif #endif
func testHTTPRequestPATCHConvenience() { func testHTTPRequestPATCHConvenience() {
let params = ["status": "active"] let params = ["status": "active"]
let request = HTTPRequest(method: .patch, url: baseURL, contentType: .json, parameters: params) let request = HTTPRequest(method: .patch, url: baseURL, contentType: .json, parameters: params)
XCTAssertEqual(request.method, .patch) XCTAssertEqual(request.method, .patch)
XCTAssertEqual(request.contentType, .json) XCTAssertEqual(request.contentType, .json)
XCTAssertNotNil(request.parameters) XCTAssertNotNil(request.parameters)
} }
func testHTTPRequestWithMultipleHeaders() { func testHTTPRequestWithMultipleHeaders() {
var request = HTTPRequest.get(baseURL) var request = HTTPRequest.get(baseURL)
request.addHeader(name: "Authorization", value: "Bearer token123") request.addHeader(name: "Authorization", value: "Bearer token123")
request.addHeader(name: "User-Agent", value: "Osiris/2.0") request.addHeader(name: "User-Agent", value: "Osiris/2.0")
request.addHeader(name: "Accept", value: "application/json") request.addHeader(name: "Accept", value: "application/json")
XCTAssertEqual(request.headers["Authorization"], "Bearer token123") XCTAssertEqual(request.headers["Authorization"], "Bearer token123")
XCTAssertEqual(request.headers["User-Agent"], "Osiris/2.0") XCTAssertEqual(request.headers["User-Agent"], "Osiris/2.0")
XCTAssertEqual(request.headers["Accept"], "application/json") XCTAssertEqual(request.headers["Accept"], "application/json")
XCTAssertEqual(request.headers.count, 3) XCTAssertEqual(request.headers.count, 3)
} }
func testHTTPRequestOverwriteHeaders() { func testHTTPRequestOverwriteHeaders() {
var request = HTTPRequest.get(baseURL) var request = HTTPRequest.get(baseURL)
request.addHeader(name: "Accept", value: "application/xml") request.addHeader(name: "Accept", value: "application/xml")
request.addHeader(name: "Accept", value: "application/json") // Should overwrite request.addHeader(name: "Accept", value: "application/json") // Should overwrite
XCTAssertEqual(request.headers["Accept"], "application/json") XCTAssertEqual(request.headers["Accept"], "application/json")
XCTAssertEqual(request.headers.count, 1) XCTAssertEqual(request.headers.count, 1)
} }
func testHTTPRequestWithEmptyMultipartParts() { func testHTTPRequestWithEmptyMultipartParts() {
var request = HTTPRequest.post(baseURL) var request = HTTPRequest.post(baseURL)
request.parts = [] // Empty parts array request.parts = [] // Empty parts array
XCTAssertEqual(request.contentType, .none) // Should not be set to multipart XCTAssertEqual(request.contentType, .none) // Should not be set to multipart
XCTAssertTrue(request.parts.isEmpty) XCTAssertTrue(request.parts.isEmpty)
} }
func testHTTPRequestMultipartPartsResetContentType() { func testHTTPRequestMultipartPartsResetContentType() {
var request = HTTPRequest.post(baseURL, contentType: .json) var request = HTTPRequest.post(baseURL, contentType: .json)
XCTAssertEqual(request.contentType, .json) XCTAssertEqual(request.contentType, .json)
request.parts = [.text("test", name: "field")] request.parts = [.text("test", name: "field")]
XCTAssertEqual(request.contentType, .multipart) // Should be automatically changed XCTAssertEqual(request.contentType, .multipart) // Should be automatically changed
request.parts = [] // Clear parts request.parts = [] // Clear parts
XCTAssertEqual(request.contentType, .multipart) // Should remain multipart XCTAssertEqual(request.contentType, .multipart) // Should remain multipart
} }

View file

@ -13,9 +13,9 @@ class HTTPResponseTests: XCTestCase {
let url = URL(string: "https://api.example.net")! let url = URL(string: "https://api.example.net")!
let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: ["Content-Type": "application/json"])! let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: ["Content-Type": "application/json"])!
let data = Data("{}".utf8) let data = Data("{}".utf8)
let response = HTTPResponse(response: httpResponse, data: data, error: nil) let response = HTTPResponse(response: httpResponse, data: data, error: nil)
if case let .success(urlResponse, responseData) = response { if case let .success(urlResponse, responseData) = response {
XCTAssertEqual(urlResponse.statusCode, 200) XCTAssertEqual(urlResponse.statusCode, 200)
XCTAssertEqual(responseData, data) XCTAssertEqual(responseData, data)
@ -23,14 +23,14 @@ class HTTPResponseTests: XCTestCase {
XCTFail("Expected success response") XCTFail("Expected success response")
} }
} }
func testFailureResponseWithError() { func testFailureResponseWithError() {
let url = URL(string: "https://api.example.net")! let url = URL(string: "https://api.example.net")!
let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)!
let error = NSError(domain: "test", code: 1, userInfo: nil) let error = NSError(domain: "test", code: 1, userInfo: nil)
let response = HTTPResponse(response: httpResponse, data: nil, error: error) let response = HTTPResponse(response: httpResponse, data: nil, error: error)
if case let .failure(responseError, urlResponse, responseData) = response { if case let .failure(responseError, urlResponse, responseData) = response {
XCTAssertEqual((responseError as NSError).domain, "test") XCTAssertEqual((responseError as NSError).domain, "test")
XCTAssertEqual(urlResponse?.statusCode, 200) XCTAssertEqual(urlResponse?.statusCode, 200)
@ -39,14 +39,14 @@ class HTTPResponseTests: XCTestCase {
XCTFail("Expected failure response") XCTFail("Expected failure response")
} }
} }
func testFailureResponseWithHTTPError() { func testFailureResponseWithHTTPError() {
let url = URL(string: "https://api.example.net")! let url = URL(string: "https://api.example.net")!
let httpResponse = HTTPURLResponse(url: url, statusCode: 404, httpVersion: nil, headerFields: nil)! let httpResponse = HTTPURLResponse(url: url, statusCode: 404, httpVersion: nil, headerFields: nil)!
let data = Data("Not Found".utf8) let data = Data("Not Found".utf8)
let response = HTTPResponse(response: httpResponse, data: data, error: nil) let response = HTTPResponse(response: httpResponse, data: data, error: nil)
if case let .failure(error, urlResponse, responseData) = response { if case let .failure(error, urlResponse, responseData) = response {
XCTAssertTrue(error is HTTPRequestError) XCTAssertTrue(error is HTTPRequestError)
XCTAssertEqual(urlResponse?.statusCode, 404) XCTAssertEqual(urlResponse?.statusCode, 404)
@ -55,114 +55,114 @@ class HTTPResponseTests: XCTestCase {
XCTFail("Expected failure response") XCTFail("Expected failure response")
} }
} }
func testResponseWithoutHTTPURLResponse() { func testResponseWithoutHTTPURLResponse() {
let response = HTTPResponse(response: nil, data: nil, error: nil) let response = HTTPResponse(response: nil, data: nil, error: nil)
if case let .failure(error, _, _) = response { if case let .failure(error, _, _) = response {
XCTAssertTrue(error is HTTPRequestError) XCTAssertTrue(error is HTTPRequestError)
} else { } else {
XCTFail("Expected failure response") XCTFail("Expected failure response")
} }
} }
func testDataProperty() { func testDataProperty() {
let data = Data("test".utf8) let data = Data("test".utf8)
let url = URL(string: "https://api.example.net")! let url = URL(string: "https://api.example.net")!
let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)!
let successResponse = HTTPResponse(response: httpResponse, data: data, error: nil) let successResponse = HTTPResponse(response: httpResponse, data: data, error: nil)
XCTAssertEqual(successResponse.data, data) XCTAssertEqual(successResponse.data, data)
let httpErrorResponse = HTTPURLResponse(url: url, statusCode: 400, httpVersion: nil, headerFields: nil)! let httpErrorResponse = HTTPURLResponse(url: url, statusCode: 400, httpVersion: nil, headerFields: nil)!
let failureResponse = HTTPResponse(response: httpErrorResponse, data: data, error: nil) let failureResponse = HTTPResponse(response: httpErrorResponse, data: data, error: nil)
XCTAssertEqual(failureResponse.data, data) XCTAssertEqual(failureResponse.data, data)
} }
func testStatusProperty() { func testStatusProperty() {
let url = URL(string: "https://api.example.net")! let url = URL(string: "https://api.example.net")!
let httpResponse = HTTPURLResponse(url: url, statusCode: 201, httpVersion: nil, headerFields: nil)! let httpResponse = HTTPURLResponse(url: url, statusCode: 201, httpVersion: nil, headerFields: nil)!
let response = HTTPResponse(response: httpResponse, data: nil, error: nil) let response = HTTPResponse(response: httpResponse, data: nil, error: nil)
XCTAssertEqual(response.status, 201) XCTAssertEqual(response.status, 201)
} }
func testHeadersProperty() { func testHeadersProperty() {
let url = URL(string: "https://api.example.net")! let url = URL(string: "https://api.example.net")!
let headers = ["Content-Type": "application/json", "X-Custom": "value"] let headers = ["Content-Type": "application/json", "X-Custom": "value"]
let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: headers)! let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: headers)!
let response = HTTPResponse(response: httpResponse, data: nil, error: nil) let response = HTTPResponse(response: httpResponse, data: nil, error: nil)
XCTAssertEqual(response.headers["Content-Type"] as? String, "application/json") XCTAssertEqual(response.headers["Content-Type"] as? String, "application/json")
XCTAssertEqual(response.headers["X-Custom"] as? String, "value") XCTAssertEqual(response.headers["X-Custom"] as? String, "value")
} }
func testBodyStringProperty() { func testBodyStringProperty() {
let url = URL(string: "https://api.example.net")! let url = URL(string: "https://api.example.net")!
let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)!
let data = Data("Hello, World!".utf8) let data = Data("Hello, World!".utf8)
let response = HTTPResponse(response: httpResponse, data: data, error: nil) let response = HTTPResponse(response: httpResponse, data: data, error: nil)
XCTAssertEqual(response.bodyString, "Hello, World!") XCTAssertEqual(response.bodyString, "Hello, World!")
} }
func testBodyStringPropertyWithNoData() { func testBodyStringPropertyWithNoData() {
let url = URL(string: "https://api.example.net")! let url = URL(string: "https://api.example.net")!
let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)!
let response = HTTPResponse(response: httpResponse, data: nil, error: nil) let response = HTTPResponse(response: httpResponse, data: nil, error: nil)
XCTAssertEqual(response.bodyString, "") XCTAssertEqual(response.bodyString, "")
} }
func testDictionaryFromJSONProperty() { func testDictionaryFromJSONProperty() {
let url = URL(string: "https://api.example.net")! let url = URL(string: "https://api.example.net")!
let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)!
let json = ["name": "John", "age": 30] as [String: any Sendable] let json = ["name": "John", "age": 30] as [String: any Sendable]
let data = try! JSONSerialization.data(withJSONObject: json) let data = try! JSONSerialization.data(withJSONObject: json)
let response = HTTPResponse(response: httpResponse, data: data, error: nil) let response = HTTPResponse(response: httpResponse, data: data, error: nil)
let dictionary = response.dictionaryFromJSON let dictionary = response.dictionaryFromJSON
XCTAssertEqual(dictionary["name"] as? String, "John") XCTAssertEqual(dictionary["name"] as? String, "John")
XCTAssertEqual(dictionary["age"] as? Int, 30) XCTAssertEqual(dictionary["age"] as? Int, 30)
} }
func testDictionaryFromJSONPropertyWithInvalidJSON() { func testDictionaryFromJSONPropertyWithInvalidJSON() {
let url = URL(string: "https://api.example.net")! let url = URL(string: "https://api.example.net")!
let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)!
let data = Data("invalid json".utf8) let data = Data("invalid json".utf8)
let response = HTTPResponse(response: httpResponse, data: data, error: nil) let response = HTTPResponse(response: httpResponse, data: data, error: nil)
let dictionary = response.dictionaryFromJSON let dictionary = response.dictionaryFromJSON
XCTAssertTrue(dictionary.isEmpty) XCTAssertTrue(dictionary.isEmpty)
} }
func testDictionaryFromJSONPropertyWithNoData() { func testDictionaryFromJSONPropertyWithNoData() {
let url = URL(string: "https://api.example.net")! let url = URL(string: "https://api.example.net")!
let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)!
let response = HTTPResponse(response: httpResponse, data: nil, error: nil) let response = HTTPResponse(response: httpResponse, data: nil, error: nil)
XCTAssertEqual(response.bodyString, "") XCTAssertEqual(response.bodyString, "")
} }
func testDictionaryFromJSONPropertyWithNonDictionaryJSON() { func testDictionaryFromJSONPropertyWithNonDictionaryJSON() {
let url = URL(string: "https://api.example.net")! let url = URL(string: "https://api.example.net")!
let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)!
let arrayJSON = try! JSONSerialization.data(withJSONObject: ["item1", "item2", "item3"]) let arrayJSON = try! JSONSerialization.data(withJSONObject: ["item1", "item2", "item3"])
let response = HTTPResponse(response: httpResponse, data: arrayJSON, error: nil) let response = HTTPResponse(response: httpResponse, data: arrayJSON, error: nil)
let dictionary = response.dictionaryFromJSON let dictionary = response.dictionaryFromJSON
XCTAssertTrue(dictionary.isEmpty) XCTAssertTrue(dictionary.isEmpty)
} }
func testUnderlyingResponseProperty() { func testUnderlyingResponseProperty() {
let url = URL(string: "https://api.example.net")! let url = URL(string: "https://api.example.net")!
let httpResponse = HTTPURLResponse(url: url, statusCode: 201, httpVersion: "HTTP/1.1", headerFields: ["Server": "nginx"])! let httpResponse = HTTPURLResponse(url: url, statusCode: 201, httpVersion: "HTTP/1.1", headerFields: ["Server": "nginx"])!
let response = HTTPResponse(response: httpResponse, data: nil, error: nil) let response = HTTPResponse(response: httpResponse, data: nil, error: nil)
if case let .success(underlyingResponse, _) = response { if case let .success(underlyingResponse, _) = response {
XCTAssertEqual(underlyingResponse.statusCode, 201) XCTAssertEqual(underlyingResponse.statusCode, 201)
XCTAssertEqual(underlyingResponse.allHeaderFields["Server"] as? String, "nginx") XCTAssertEqual(underlyingResponse.allHeaderFields["Server"] as? String, "nginx")
@ -170,41 +170,41 @@ class HTTPResponseTests: XCTestCase {
XCTFail("Expected success response") XCTFail("Expected success response")
} }
} }
func testResponseStringDescription() { func testResponseStringDescription() {
let url = URL(string: "https://api.example.net")! let url = URL(string: "https://api.example.net")!
let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)!
let data = Data("test response".utf8) let data = Data("test response".utf8)
let successResponse = HTTPResponse(response: httpResponse, data: data, error: nil) let successResponse = HTTPResponse(response: httpResponse, data: data, error: nil)
let description = String(describing: successResponse) let description = String(describing: successResponse)
XCTAssertTrue(description.contains("success")) XCTAssertTrue(description.contains("success"))
let failureResponse = HTTPResponse(response: httpResponse, data: data, error: HTTPRequestError.http) let failureResponse = HTTPResponse(response: httpResponse, data: data, error: HTTPRequestError.http)
let failureDescription = String(describing: failureResponse) let failureDescription = String(describing: failureResponse)
XCTAssertTrue(failureDescription.contains("failure")) XCTAssertTrue(failureDescription.contains("failure"))
} }
func testResponseWithDifferentStatusCodes() { func testResponseWithDifferentStatusCodes() {
let url = URL(string: "https://api.example.net")! let url = URL(string: "https://api.example.net")!
// Test various 2xx success codes // Test various 2xx success codes
for statusCode in [200, 201, 202, 204, 206] { for statusCode in [200, 201, 202, 204, 206] {
let httpResponse = HTTPURLResponse(url: url, statusCode: statusCode, httpVersion: nil, headerFields: nil)! let httpResponse = HTTPURLResponse(url: url, statusCode: statusCode, httpVersion: nil, headerFields: nil)!
let response = HTTPResponse(response: httpResponse, data: nil, error: nil) let response = HTTPResponse(response: httpResponse, data: nil, error: nil)
if case .success = response { if case .success = response {
// Expected // Expected
} else { } else {
XCTFail("Status code \(statusCode) should be success") XCTFail("Status code \(statusCode) should be success")
} }
} }
// Test various error status codes // Test various error status codes
for statusCode in [300, 400, 401, 404, 500, 503] { for statusCode in [300, 400, 401, 404, 500, 503] {
let httpResponse = HTTPURLResponse(url: url, statusCode: statusCode, httpVersion: nil, headerFields: nil)! let httpResponse = HTTPURLResponse(url: url, statusCode: statusCode, httpVersion: nil, headerFields: nil)!
let response = HTTPResponse(response: httpResponse, data: nil, error: nil) let response = HTTPResponse(response: httpResponse, data: nil, error: nil)
if case .failure = response { if case .failure = response {
// Expected // Expected
} else { } else {
@ -212,25 +212,25 @@ class HTTPResponseTests: XCTestCase {
} }
} }
} }
func testResponseWithBinaryData() { func testResponseWithBinaryData() {
let url = URL(string: "https://api.example.net")! let url = URL(string: "https://api.example.net")!
let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)!
let binaryData = Data([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) // PNG header let binaryData = Data([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) // PNG header
let response = HTTPResponse(response: httpResponse, data: binaryData, error: nil) let response = HTTPResponse(response: httpResponse, data: binaryData, error: nil)
XCTAssertEqual(response.data, binaryData) XCTAssertEqual(response.data, binaryData)
// bodyString should handle binary data gracefully - it will be empty since this isn't valid UTF-8 // bodyString should handle binary data gracefully - it will be empty since this isn't valid UTF-8
let bodyString = response.bodyString let bodyString = response.bodyString
XCTAssertTrue(bodyString.isEmpty) // Binary data that isn't valid UTF-8 returns empty string XCTAssertTrue(bodyString.isEmpty) // Binary data that isn't valid UTF-8 returns empty string
} }
func testResponseStatusPropertyEdgeCases() { func testResponseStatusPropertyEdgeCases() {
// Test with no HTTP response - creates dummy HTTPURLResponse with status 0 // Test with no HTTP response - creates dummy HTTPURLResponse with status 0
let responseNoHTTP = HTTPResponse(response: nil, data: nil, error: nil) let responseNoHTTP = HTTPResponse(response: nil, data: nil, error: nil)
XCTAssertEqual(responseNoHTTP.status, 0) XCTAssertEqual(responseNoHTTP.status, 0)
// Test with URLResponse that's not HTTPURLResponse - creates dummy HTTPURLResponse with status 0 // Test with URLResponse that's not HTTPURLResponse - creates dummy HTTPURLResponse with status 0
let url = URL(string: "file:///test.txt")! let url = URL(string: "file:///test.txt")!
let fileResponse = URLResponse(url: url, mimeType: "text/plain", expectedContentLength: 10, textEncodingName: nil) let fileResponse = URLResponse(url: url, mimeType: "text/plain", expectedContentLength: 10, textEncodingName: nil)

View file

@ -10,153 +10,153 @@ import XCTest
class RequestBuilderTests: XCTestCase { class RequestBuilderTests: XCTestCase {
let baseURL = URL(string: "https://api.example.net/users")! let baseURL = URL(string: "https://api.example.net/users")!
func testBuildBasicGETRequest() throws { func testBuildBasicGETRequest() throws {
let httpRequest = HTTPRequest.get(baseURL) let httpRequest = HTTPRequest.get(baseURL)
let urlRequest = try RequestBuilder.build(request: httpRequest) let urlRequest = try RequestBuilder.build(request: httpRequest)
XCTAssertEqual(urlRequest.url, baseURL) XCTAssertEqual(urlRequest.url, baseURL)
XCTAssertEqual(urlRequest.httpMethod, "GET") XCTAssertEqual(urlRequest.httpMethod, "GET")
XCTAssertNil(urlRequest.httpBody) XCTAssertNil(urlRequest.httpBody)
} }
func testBuildBasicPOSTRequest() throws { func testBuildBasicPOSTRequest() throws {
let httpRequest = HTTPRequest.post(baseURL) let httpRequest = HTTPRequest.post(baseURL)
let urlRequest = try RequestBuilder.build(request: httpRequest) let urlRequest = try RequestBuilder.build(request: httpRequest)
XCTAssertEqual(urlRequest.url, baseURL) XCTAssertEqual(urlRequest.url, baseURL)
XCTAssertEqual(urlRequest.httpMethod, "POST") XCTAssertEqual(urlRequest.httpMethod, "POST")
XCTAssertNil(urlRequest.httpBody) XCTAssertNil(urlRequest.httpBody)
} }
func testBuildRequestWithHeaders() throws { func testBuildRequestWithHeaders() throws {
var httpRequest = HTTPRequest.get(baseURL) var httpRequest = HTTPRequest.get(baseURL)
httpRequest.headers = ["Authorization": "Bearer token", "X-Custom": "value"] httpRequest.headers = ["Authorization": "Bearer token", "X-Custom": "value"]
let urlRequest = try RequestBuilder.build(request: httpRequest) let urlRequest = try RequestBuilder.build(request: httpRequest)
XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "Authorization"), "Bearer token") XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "Authorization"), "Bearer token")
XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "X-Custom"), "value") XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "X-Custom"), "value")
} }
func testBuildJSONRequest() throws { func testBuildJSONRequest() throws {
let parameters = ["name": "Jane", "age": 30] as [String: any Sendable] let parameters = ["name": "Jane", "age": 30] as [String: any Sendable]
let httpRequest = HTTPRequest.post(baseURL, contentType: .json, parameters: parameters) let httpRequest = HTTPRequest.post(baseURL, contentType: .json, parameters: parameters)
let urlRequest = try RequestBuilder.build(request: httpRequest) let urlRequest = try RequestBuilder.build(request: httpRequest)
XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "Content-Type"), "application/json") XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "Content-Type"), "application/json")
XCTAssertNotNil(urlRequest.httpBody) XCTAssertNotNil(urlRequest.httpBody)
// Verify the JSON content // Verify the JSON content
let bodyData = urlRequest.httpBody! let bodyData = urlRequest.httpBody!
let decodedJSON = try JSONSerialization.jsonObject(with: bodyData) as! [String: any Sendable] let decodedJSON = try JSONSerialization.jsonObject(with: bodyData) as! [String: any Sendable]
XCTAssertEqual(decodedJSON["name"] as? String, "Jane") XCTAssertEqual(decodedJSON["name"] as? String, "Jane")
XCTAssertEqual(decodedJSON["age"] as? Int, 30) XCTAssertEqual(decodedJSON["age"] as? Int, 30)
} }
func testBuildFormEncodedRequest() throws { func testBuildFormEncodedRequest() throws {
let parameters = ["email": "john@example.net", "password": "TaylorSwift1989"] let parameters = ["email": "john@example.net", "password": "TaylorSwift1989"]
let httpRequest = HTTPRequest.post(baseURL, contentType: .formEncoded, parameters: parameters) let httpRequest = HTTPRequest.post(baseURL, contentType: .formEncoded, parameters: parameters)
let urlRequest = try RequestBuilder.build(request: httpRequest) let urlRequest = try RequestBuilder.build(request: httpRequest)
XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "Content-Type"), "application/x-www-form-urlencoded") XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "Content-Type"), "application/x-www-form-urlencoded")
XCTAssertNotNil(urlRequest.httpBody) XCTAssertNotNil(urlRequest.httpBody)
let bodyString = String(data: urlRequest.httpBody!, encoding: .utf8)! let bodyString = String(data: urlRequest.httpBody!, encoding: .utf8)!
XCTAssertTrue(bodyString.contains("email=john%40example.net")) XCTAssertTrue(bodyString.contains("email=john%40example.net"))
XCTAssertTrue(bodyString.contains("password=TaylorSwift1989")) XCTAssertTrue(bodyString.contains("password=TaylorSwift1989"))
} }
// Note: Testing .none content type with parameters would trigger an assertion failure // Note: Testing .none content type with parameters would trigger an assertion failure
// This is by design - developers should specify an appropriate content type // This is by design - developers should specify an appropriate content type
func testBuildMultipartRequest() throws { func testBuildMultipartRequest() throws {
var httpRequest = HTTPRequest.post(baseURL) var httpRequest = HTTPRequest.post(baseURL)
httpRequest.parts = [ httpRequest.parts = [
.text("Jane Doe", name: "name"), .text("Jane Doe", name: "name"),
.data(Data("test".utf8), name: "file", type: "text/plain", filename: "test.txt") .data(Data("test".utf8), name: "file", type: "text/plain", filename: "test.txt")
] ]
let urlRequest = try RequestBuilder.build(request: httpRequest) let urlRequest = try RequestBuilder.build(request: httpRequest)
let contentType = urlRequest.value(forHTTPHeaderField: "Content-Type") let contentType = urlRequest.value(forHTTPHeaderField: "Content-Type")
XCTAssertNotNil(contentType) XCTAssertNotNil(contentType)
XCTAssertTrue(contentType!.hasPrefix("multipart/form-data; boundary=")) XCTAssertTrue(contentType!.hasPrefix("multipart/form-data; boundary="))
let contentLength = urlRequest.value(forHTTPHeaderField: "Content-Length") let contentLength = urlRequest.value(forHTTPHeaderField: "Content-Length")
XCTAssertNotNil(contentLength) XCTAssertNotNil(contentLength)
XCTAssertGreaterThan(Int(contentLength!)!, 0) XCTAssertGreaterThan(Int(contentLength!)!, 0)
XCTAssertNotNil(urlRequest.httpBody) XCTAssertNotNil(urlRequest.httpBody)
} }
func testBuildRequestWithInvalidFormData() throws { func testBuildRequestWithInvalidFormData() throws {
// Create a parameter that would cause UTF-8 encoding to fail // Create a parameter that would cause UTF-8 encoding to fail
// FormEncoder.encode() returns a String, but String.data(using: .utf8) could theoretically fail // FormEncoder.encode() returns a String, but String.data(using: .utf8) could theoretically fail
// However, this is extremely rare in practice. Let's test the error path by creating a mock scenario. // However, this is extremely rare in practice. Let's test the error path by creating a mock scenario.
// Since FormEncoder is quite robust and UTF-8 encoding rarely fails, // Since FormEncoder is quite robust and UTF-8 encoding rarely fails,
// we'll test this by creating a subclass that can force the failure // we'll test this by creating a subclass that can force the failure
// But for now, we'll document this edge case exists // But for now, we'll document this edge case exists
XCTAssertNoThrow(try RequestBuilder.build(request: HTTPRequest.post(baseURL, contentType: .formEncoded, parameters: ["test": "value"]))) XCTAssertNoThrow(try RequestBuilder.build(request: HTTPRequest.post(baseURL, contentType: .formEncoded, parameters: ["test": "value"])))
} }
func testBuildRequestWithAllHTTPMethods() throws { func testBuildRequestWithAllHTTPMethods() throws {
let methods: [HTTPMethod] = [.get, .post, .put, .patch, .delete] let methods: [HTTPMethod] = [.get, .post, .put, .patch, .delete]
for method in methods { for method in methods {
let httpRequest = HTTPRequest(method: method, url: baseURL) let httpRequest = HTTPRequest(method: method, url: baseURL)
let urlRequest = try RequestBuilder.build(request: httpRequest) let urlRequest = try RequestBuilder.build(request: httpRequest)
XCTAssertEqual(urlRequest.httpMethod, method.string) XCTAssertEqual(urlRequest.httpMethod, method.string)
} }
} }
func testBuildRequestPreservesURL() throws { func testBuildRequestPreservesURL() throws {
let complexURL = URL(string: "https://api.example.net/users?page=1#section")! let complexURL = URL(string: "https://api.example.net/users?page=1#section")!
let httpRequest = HTTPRequest.get(complexURL) let httpRequest = HTTPRequest.get(complexURL)
let urlRequest = try RequestBuilder.build(request: httpRequest) let urlRequest = try RequestBuilder.build(request: httpRequest)
XCTAssertEqual(urlRequest.url, complexURL) XCTAssertEqual(urlRequest.url, complexURL)
} }
func testMultipleHeadersWithSameName() throws { func testMultipleHeadersWithSameName() throws {
var httpRequest = HTTPRequest.get(baseURL) var httpRequest = HTTPRequest.get(baseURL)
httpRequest.headers = ["Accept": "application/json"] httpRequest.headers = ["Accept": "application/json"]
let urlRequest = try RequestBuilder.build(request: httpRequest) let urlRequest = try RequestBuilder.build(request: httpRequest)
XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "Accept"), "application/json") XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "Accept"), "application/json")
} }
func testBuildRequestWithEmptyMultipartParts() throws { func testBuildRequestWithEmptyMultipartParts() throws {
var httpRequest = HTTPRequest.post(baseURL) var httpRequest = HTTPRequest.post(baseURL)
httpRequest.parts = [] httpRequest.parts = []
httpRequest.contentType = .multipart // Explicitly set to multipart httpRequest.contentType = .multipart // Explicitly set to multipart
let urlRequest = try RequestBuilder.build(request: httpRequest) let urlRequest = try RequestBuilder.build(request: httpRequest)
let contentType = try XCTUnwrap(urlRequest.value(forHTTPHeaderField: "Content-Type")) let contentType = try XCTUnwrap(urlRequest.value(forHTTPHeaderField: "Content-Type"))
XCTAssertTrue(contentType.hasPrefix("multipart/form-data; boundary=")) XCTAssertTrue(contentType.hasPrefix("multipart/form-data; boundary="))
XCTAssertNotNil(urlRequest.httpBody) XCTAssertNotNil(urlRequest.httpBody)
} }
func testBuildRequestWithLargeMultipartData() throws { func testBuildRequestWithLargeMultipartData() throws {
var httpRequest = HTTPRequest.post(baseURL) var httpRequest = HTTPRequest.post(baseURL)
let largeData = Data(repeating: 65, count: 1024 * 1024) // 1MB of 'A' characters let largeData = Data(repeating: 65, count: 1024 * 1024) // 1MB of 'A' characters
httpRequest.parts = [ httpRequest.parts = [
.data(largeData, name: "largefile", type: "application/octet-stream", filename: "large.bin") .data(largeData, name: "largefile", type: "application/octet-stream", filename: "large.bin")
] ]
let urlRequest = try RequestBuilder.build(request: httpRequest) let urlRequest = try RequestBuilder.build(request: httpRequest)
XCTAssertNotNil(urlRequest.httpBody) XCTAssertNotNil(urlRequest.httpBody)
XCTAssertGreaterThan(urlRequest.httpBody!.count, 1024 * 1024) XCTAssertGreaterThan(urlRequest.httpBody!.count, 1024 * 1024)
} }
func testBuildRequestWithSpecialCharactersInHeaders() throws { func testBuildRequestWithSpecialCharactersInHeaders() throws {
var httpRequest = HTTPRequest.get(baseURL) var httpRequest = HTTPRequest.get(baseURL)
httpRequest.headers = [ httpRequest.headers = [
@ -164,42 +164,42 @@ class RequestBuilderTests: XCTestCase {
"X-Unicode": "🚀 rocket emoji", "X-Unicode": "🚀 rocket emoji",
"X-Empty": "" "X-Empty": ""
] ]
let urlRequest = try RequestBuilder.build(request: httpRequest) let urlRequest = try RequestBuilder.build(request: httpRequest)
XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "X-Custom-Header"), "value with spaces and symbols: !@#$%") XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "X-Custom-Header"), "value with spaces and symbols: !@#$%")
XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "X-Unicode"), "🚀 rocket emoji") XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "X-Unicode"), "🚀 rocket emoji")
XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "X-Empty"), "") XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "X-Empty"), "")
} }
func testBuildRequestWithNilParameters() throws { func testBuildRequestWithNilParameters() throws {
let httpRequest = HTTPRequest.post(baseURL, contentType: .json, parameters: nil) let httpRequest = HTTPRequest.post(baseURL, contentType: .json, parameters: nil)
let urlRequest = try RequestBuilder.build(request: httpRequest) let urlRequest = try RequestBuilder.build(request: httpRequest)
// RequestBuilder may not set Content-Type if there are no parameters to encode // RequestBuilder may not set Content-Type if there are no parameters to encode
XCTAssertNil(urlRequest.httpBody) XCTAssertNil(urlRequest.httpBody)
} }
func testBuildRequestWithEmptyParameters() throws { func testBuildRequestWithEmptyParameters() throws {
let httpRequest = HTTPRequest.post(baseURL, contentType: .json, parameters: [:]) let httpRequest = HTTPRequest.post(baseURL, contentType: .json, parameters: [:])
let urlRequest = try RequestBuilder.build(request: httpRequest) let urlRequest = try RequestBuilder.build(request: httpRequest)
XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "Content-Type"), "application/json") XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "Content-Type"), "application/json")
XCTAssertNotNil(urlRequest.httpBody) XCTAssertNotNil(urlRequest.httpBody)
let bodyString = String(data: urlRequest.httpBody!, encoding: .utf8)! let bodyString = String(data: urlRequest.httpBody!, encoding: .utf8)!
XCTAssertEqual(bodyString, "{}") XCTAssertEqual(bodyString, "{}")
} }
func testBuildRequestSetsContentType() throws { func testBuildRequestSetsContentType() throws {
let httpRequest = HTTPRequest.post(baseURL, contentType: .json, parameters: ["test": "value"]) let httpRequest = HTTPRequest.post(baseURL, contentType: .json, parameters: ["test": "value"])
let urlRequest = try RequestBuilder.build(request: httpRequest) let urlRequest = try RequestBuilder.build(request: httpRequest)
// RequestBuilder should set the correct content type when there are parameters to encode // RequestBuilder should set the correct content type when there are parameters to encode
let contentType = urlRequest.value(forHTTPHeaderField: "Content-Type") let contentType = urlRequest.value(forHTTPHeaderField: "Content-Type")
XCTAssertTrue(contentType?.contains("application/json") == true) XCTAssertTrue(contentType?.contains("application/json") == true)
} }
func testBuildRequestWithComplexJSONParameters() throws { func testBuildRequestWithComplexJSONParameters() throws {
let nestedData: [String: any Sendable] = ["theme": "dark", "notifications": true] let nestedData: [String: any Sendable] = ["theme": "dark", "notifications": true]
let arrayData: [any Sendable] = ["rock", "pop", "jazz"] let arrayData: [any Sendable] = ["rock", "pop", "jazz"]
@ -211,86 +211,99 @@ class RequestBuilderTests: XCTestCase {
"genres": arrayData "genres": arrayData
] as [String: any Sendable] ] as [String: any Sendable]
] ]
let httpRequest = HTTPRequest.post(baseURL, contentType: .json, parameters: complexParams) let httpRequest = HTTPRequest.post(baseURL, contentType: .json, parameters: complexParams)
let urlRequest = try RequestBuilder.build(request: httpRequest) let urlRequest = try RequestBuilder.build(request: httpRequest)
XCTAssertNotNil(urlRequest.httpBody) XCTAssertNotNil(urlRequest.httpBody)
let jsonObject = try JSONSerialization.jsonObject(with: urlRequest.httpBody!) as! [String: Any] let jsonObject = try JSONSerialization.jsonObject(with: urlRequest.httpBody!) as! [String: Any]
let person = jsonObject["person"] as! [String: Any] let person = jsonObject["person"] as! [String: Any]
XCTAssertEqual(person["name"] as? String, "David Bowie") XCTAssertEqual(person["name"] as? String, "David Bowie")
XCTAssertEqual(person["age"] as? Int, 69) XCTAssertEqual(person["age"] as? Int, 69)
} }
func testBuildRequestWithNoneContentTypeFallsBackToFormEncoding() throws { func testBuildRequestWithNoneContentTypeFallsBackToFormEncoding() throws {
// Test the .none content type fallthrough case with a warning // Test the .none content type fallthrough case with a warning
let httpRequest = HTTPRequest.post(baseURL, contentType: .none, parameters: ["email": "freddie@example.net", "band": "Queen"]) let httpRequest = HTTPRequest.post(baseURL, contentType: .none, parameters: ["email": "freddie@example.net", "band": "Queen"])
let urlRequest = try RequestBuilder.build(request: httpRequest) let urlRequest = try RequestBuilder.build(request: httpRequest)
// Should fall back to form encoding and log a warning // Should fall back to form encoding and log a warning
XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "Content-Type"), "application/x-www-form-urlencoded") XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "Content-Type"), "application/x-www-form-urlencoded")
XCTAssertNotNil(urlRequest.httpBody) XCTAssertNotNil(urlRequest.httpBody)
let bodyString = String(data: urlRequest.httpBody!, encoding: .utf8)! let bodyString = String(data: urlRequest.httpBody!, encoding: .utf8)!
XCTAssertTrue(bodyString.contains("email=freddie%40example.net")) XCTAssertTrue(bodyString.contains("email=freddie%40example.net"))
XCTAssertTrue(bodyString.contains("band=Queen")) XCTAssertTrue(bodyString.contains("band=Queen"))
} }
func testBuildGETRequestWithQueryParameters() throws { func testBuildGETRequestWithQueryParameters() throws {
let httpRequest = HTTPRequest.get(baseURL, parameters: ["name": "John Doe", "email": "john@example.net"]) let httpRequest = HTTPRequest.get(baseURL, parameters: ["name": "John Doe", "email": "john@example.net"])
let urlRequest = try RequestBuilder.build(request: httpRequest) let urlRequest = try RequestBuilder.build(request: httpRequest)
XCTAssertEqual(urlRequest.httpMethod, "GET") XCTAssertEqual(urlRequest.httpMethod, "GET")
XCTAssertNil(urlRequest.httpBody) XCTAssertNil(urlRequest.httpBody)
XCTAssertNil(urlRequest.value(forHTTPHeaderField: "Content-Type")) XCTAssertNil(urlRequest.value(forHTTPHeaderField: "Content-Type"))
let urlString = urlRequest.url?.absoluteString ?? "" let urlString = urlRequest.url?.absoluteString ?? ""
XCTAssertTrue(urlString.contains("name=John%20Doe"), "URL should contain encoded name parameter") XCTAssertTrue(urlString.contains("name=John%20Doe"), "URL should contain encoded name parameter")
XCTAssertTrue(urlString.contains("email=john@example.net"), "URL should contain email parameter") XCTAssertTrue(urlString.contains("email=john@example.net"), "URL should contain email parameter")
XCTAssertTrue(urlString.contains("?"), "URL should contain query separator") XCTAssertTrue(urlString.contains("?"), "URL should contain query separator")
} }
func testBuildDELETERequestWithQueryParameters() throws { func testBuildDELETERequestWithQueryParameters() throws {
let httpRequest = HTTPRequest.delete(baseURL, parameters: ["id": "123", "confirm": "true"]) let httpRequest = HTTPRequest.delete(baseURL, parameters: ["id": "123", "confirm": "true"])
let urlRequest = try RequestBuilder.build(request: httpRequest) let urlRequest = try RequestBuilder.build(request: httpRequest)
XCTAssertEqual(urlRequest.httpMethod, "DELETE") XCTAssertEqual(urlRequest.httpMethod, "DELETE")
XCTAssertNil(urlRequest.httpBody) XCTAssertNil(urlRequest.httpBody)
XCTAssertNil(urlRequest.value(forHTTPHeaderField: "Content-Type")) XCTAssertNil(urlRequest.value(forHTTPHeaderField: "Content-Type"))
let urlString = urlRequest.url?.absoluteString ?? "" let urlString = urlRequest.url?.absoluteString ?? ""
XCTAssertTrue(urlString.contains("id=123")) XCTAssertTrue(urlString.contains("id=123"))
XCTAssertTrue(urlString.contains("confirm=true")) XCTAssertTrue(urlString.contains("confirm=true"))
XCTAssertTrue(urlString.contains("?")) XCTAssertTrue(urlString.contains("?"))
} }
func testBuildGETRequestWithExistingQueryString() throws { func testBuildGETRequestWithExistingQueryString() throws {
let urlWithQuery = URL(string: "https://api.example.net/users?existing=param")! let urlWithQuery = URL(string: "https://api.example.net/users?existing=param")!
let httpRequest = HTTPRequest.get(urlWithQuery, parameters: ["new": "value"]) let httpRequest = HTTPRequest.get(urlWithQuery, parameters: ["new": "value"])
let urlRequest = try RequestBuilder.build(request: httpRequest) let urlRequest = try RequestBuilder.build(request: httpRequest)
let urlString = urlRequest.url?.absoluteString ?? "" let urlString = urlRequest.url?.absoluteString ?? ""
XCTAssertTrue(urlString.contains("existing=param")) XCTAssertTrue(urlString.contains("existing=param"))
XCTAssertTrue(urlString.contains("new=value")) XCTAssertTrue(urlString.contains("new=value"))
XCTAssertTrue(urlString.contains("&")) XCTAssertTrue(urlString.contains("&"))
} }
func testBuildGETRequestWithMultipartThrowsError() throws { func testBuildGETRequestWithMultipartThrowsError() throws {
var httpRequest = HTTPRequest.get(baseURL, parameters: ["name": "value"]) var httpRequest = HTTPRequest.get(baseURL, parameters: ["name": "value"])
httpRequest.contentType = HTTPContentType.multipart httpRequest.contentType = HTTPContentType.multipart
XCTAssertThrowsError(try RequestBuilder.build(request: httpRequest)) { error in XCTAssertThrowsError(try RequestBuilder.build(request: httpRequest)) { error in
XCTAssertTrue(error is RequestBuilderError) XCTAssertTrue(error is RequestBuilderError)
} }
} }
func testBuildDELETERequestWithPartsThrowsError() throws { func testBuildDELETERequestWithPartsThrowsError() throws {
var httpRequest = HTTPRequest.delete(baseURL, parameters: ["id": "123"]) var httpRequest = HTTPRequest.delete(baseURL, parameters: ["id": "123"])
httpRequest.parts = [MultipartFormEncoder.Part.text("value", name: "test")] httpRequest.parts = [MultipartFormEncoder.Part.text("value", name: "test")]
XCTAssertThrowsError(try RequestBuilder.build(request: httpRequest)) { error in XCTAssertThrowsError(try RequestBuilder.build(request: httpRequest)) { error in
XCTAssertTrue(error is RequestBuilderError) XCTAssertTrue(error is RequestBuilderError)
} }
} }
func testBuildGETRequestWithEmptyParametersDoesNotIncludeQueryString() throws {
let httpRequest = HTTPRequest.get(baseURL, parameters: [:])
let urlRequest = try RequestBuilder.build(request: httpRequest)
XCTAssertEqual(urlRequest.httpMethod, "GET")
XCTAssertNil(urlRequest.httpBody)
XCTAssertNil(urlRequest.value(forHTTPHeaderField: "Content-Type"))
let urlString = urlRequest.url?.absoluteString ?? ""
XCTAssertEqual(urlString, baseURL.absoluteString, "URL should not contain query string when parameters are empty")
XCTAssertFalse(urlString.contains("?"), "URL should not contain question mark when parameters are empty")
}
} }