mirror of
https://github.com/samsonjs/Osiris.git
synced 2026-03-25 08:55:48 +00:00
Fix stray question marks
This commit is contained in:
parent
bcf402db8f
commit
bc3ce2c93e
13 changed files with 258 additions and 245 deletions
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)>"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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)>"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue