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
|
||||
|
||||
extension NSNumber {
|
||||
|
||||
|
||||
/// [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.
|
||||
|
|
@ -30,12 +30,12 @@ extension NSNumber {
|
|||
/// "active": true,
|
||||
/// "preferences": ["color": "blue", "theme": "dark"]
|
||||
/// ]
|
||||
///
|
||||
///
|
||||
/// 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"
|
||||
/// ```
|
||||
public final class FormEncoder: CustomStringConvertible {
|
||||
|
||||
|
||||
/// Encodes a dictionary of parameters into a URL-encoded form string.
|
||||
///
|
||||
/// The encoding follows these rules:
|
||||
|
|
@ -119,7 +119,7 @@ public final class FormEncoder: CustomStringConvertible {
|
|||
let escaped = string.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) ?? string
|
||||
return escaped
|
||||
}
|
||||
|
||||
|
||||
public var description: String {
|
||||
"FormEncoder"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,19 +8,19 @@ import Foundation
|
|||
|
||||
/// Content types that can be automatically handled by HTTPRequest.
|
||||
public enum HTTPContentType: Sendable, CustomStringConvertible {
|
||||
|
||||
|
||||
/// application/x-www-form-urlencoded
|
||||
case formEncoded
|
||||
|
||||
|
||||
/// No specific content type
|
||||
case none
|
||||
|
||||
|
||||
/// application/json
|
||||
case json
|
||||
|
||||
|
||||
/// multipart/form-data (set automatically when parts are added)
|
||||
case multipart
|
||||
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .formEncoded:
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ public enum HTTPMethod: String, Sendable, CustomStringConvertible {
|
|||
var string: String {
|
||||
rawValue.uppercased()
|
||||
}
|
||||
|
||||
|
||||
public var description: String {
|
||||
string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,19 +51,19 @@ public struct HTTPRequest: Sendable, CustomStringConvertible {
|
|||
|
||||
/// The HTTP method for this request.
|
||||
public var method: HTTPMethod
|
||||
|
||||
|
||||
/// The target URL for this request.
|
||||
public var url: URL
|
||||
|
||||
|
||||
/// The content type for the request body.
|
||||
public var contentType: HTTPContentType
|
||||
|
||||
|
||||
/// Parameters to be encoded according to the content type.
|
||||
public var parameters: [String: any Sendable]?
|
||||
|
||||
/// Additional HTTP headers for the request.
|
||||
public var headers: [String: String] = [:]
|
||||
|
||||
|
||||
/// Multipart form parts (automatically sets contentType to .multipart when non-empty).
|
||||
public var parts: [MultipartFormEncoder.Part] = [] {
|
||||
didSet {
|
||||
|
|
@ -123,7 +123,7 @@ public struct HTTPRequest: Sendable, CustomStringConvertible {
|
|||
}
|
||||
|
||||
#if canImport(UIKit)
|
||||
|
||||
|
||||
/// Adds a JPEG image to the multipart form (iOS/tvOS only).
|
||||
/// - Parameters:
|
||||
/// - name: The form field name
|
||||
|
|
@ -148,7 +148,7 @@ public struct HTTPRequest: Sendable, CustomStringConvertible {
|
|||
public mutating func addHeader(name: String, value: String) {
|
||||
headers[name] = value
|
||||
}
|
||||
|
||||
|
||||
public var description: String {
|
||||
"<HTTPRequest \(method) \(url)>"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@ import Foundation
|
|||
|
||||
/// Specific errors for HTTP request processing.
|
||||
public enum HTTPRequestError: Error, LocalizedError, CustomStringConvertible {
|
||||
|
||||
|
||||
/// An HTTP error occurred (non-2xx status code).
|
||||
case http
|
||||
|
||||
|
||||
/// An unknown error occurred (typically when URLResponse isn't HTTPURLResponse).
|
||||
case unknown
|
||||
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .http:
|
||||
|
|
@ -23,7 +23,7 @@ public enum HTTPRequestError: Error, LocalizedError, CustomStringConvertible {
|
|||
return "An unknown error occurred"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public var failureReason: String? {
|
||||
switch self {
|
||||
case .http:
|
||||
|
|
@ -32,7 +32,7 @@ public enum HTTPRequestError: Error, LocalizedError, CustomStringConvertible {
|
|||
return "An unexpected error occurred during the request"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public var recoverySuggestion: String? {
|
||||
switch self {
|
||||
case .http:
|
||||
|
|
@ -41,7 +41,7 @@ public enum HTTPRequestError: Error, LocalizedError, CustomStringConvertible {
|
|||
return "Check network connectivity and try again"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .http:
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ private let log = Logger(subsystem: "co.1se.Osiris", category: "HTTPResponse")
|
|||
/// ```swift
|
||||
/// let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
|
||||
/// let httpResponse = HTTPResponse(response: response, data: data, error: error)
|
||||
///
|
||||
///
|
||||
/// switch httpResponse {
|
||||
/// case .success(let httpURLResponse, let data):
|
||||
/// print("Success: \(httpURLResponse.statusCode)")
|
||||
|
|
@ -32,10 +32,10 @@ private let log = Logger(subsystem: "co.1se.Osiris", category: "HTTPResponse")
|
|||
/// }
|
||||
/// ```
|
||||
public enum HTTPResponse: CustomStringConvertible {
|
||||
|
||||
|
||||
/// A successful response (2xx status code) with the HTTP response and optional body data.
|
||||
case success(HTTPURLResponse, Data?)
|
||||
|
||||
|
||||
/// A failed response with the error, optional HTTP response, and optional body data.
|
||||
case failure(Error, HTTPURLResponse?, Data?)
|
||||
|
||||
|
|
@ -125,7 +125,7 @@ public enum HTTPResponse: CustomStringConvertible {
|
|||
return [:]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case let .success(response, data):
|
||||
|
|
|
|||
|
|
@ -24,13 +24,13 @@
|
|||
import Foundation
|
||||
|
||||
extension MultipartFormEncoder {
|
||||
|
||||
|
||||
/// Contains the encoded multipart form data for in-memory storage.
|
||||
public struct BodyData: CustomStringConvertible {
|
||||
|
||||
|
||||
/// The content type header value including boundary.
|
||||
public let contentType: String
|
||||
|
||||
|
||||
/// The encoded form data.
|
||||
public let data: Data
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ extension MultipartFormEncoder {
|
|||
public var contentLength: Int {
|
||||
data.count
|
||||
}
|
||||
|
||||
|
||||
public var description: String {
|
||||
"<BodyData size=\(contentLength)>"
|
||||
}
|
||||
|
|
@ -46,16 +46,16 @@ extension MultipartFormEncoder {
|
|||
|
||||
/// Contains the encoded multipart form data written to a file for streaming.
|
||||
public struct BodyFile: CustomStringConvertible {
|
||||
|
||||
|
||||
/// The content type header value including boundary.
|
||||
public let contentType: String
|
||||
|
||||
|
||||
/// The URL of the temporary file containing the encoded data.
|
||||
public let url: URL
|
||||
|
||||
|
||||
/// The length of the encoded data in bytes.
|
||||
public let contentLength: Int64
|
||||
|
||||
|
||||
public var description: String {
|
||||
"<BodyFile file=\(url.lastPathComponent) size=\(contentLength)>"
|
||||
}
|
||||
|
|
@ -66,16 +66,16 @@ extension MultipartFormEncoder {
|
|||
|
||||
/// The content types supported in multipart forms.
|
||||
public enum Content: Equatable, Sendable, CustomStringConvertible {
|
||||
|
||||
|
||||
/// Plain text content.
|
||||
case text(String)
|
||||
|
||||
|
||||
/// Binary data with MIME type and filename.
|
||||
case binaryData(Data, type: String, filename: String)
|
||||
|
||||
|
||||
/// Binary data from a file with size, MIME type and filename.
|
||||
case binaryFile(URL, size: Int64, type: String, filename: String)
|
||||
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case let .text(value):
|
||||
|
|
@ -91,7 +91,7 @@ extension MultipartFormEncoder {
|
|||
|
||||
/// The form field name for this part.
|
||||
public let name: String
|
||||
|
||||
|
||||
/// The content of this part.
|
||||
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))
|
||||
}
|
||||
|
||||
|
||||
public var description: String {
|
||||
"<Part name=\(name) content=\(content)>"
|
||||
}
|
||||
|
|
@ -151,30 +151,30 @@ extension MultipartFormEncoder {
|
|||
/// .text("jane@example.net", name: "email"),
|
||||
/// .data(imageData, name: "avatar", type: "image/jpeg", filename: "avatar.jpg")
|
||||
/// ]
|
||||
///
|
||||
///
|
||||
/// // Encode to memory (< 50MB)
|
||||
/// let bodyData = try encoder.encodeData(parts: parts)
|
||||
///
|
||||
///
|
||||
/// // Or encode to file for streaming
|
||||
/// let bodyFile = try encoder.encodeFile(parts: parts)
|
||||
/// ```
|
||||
public final class MultipartFormEncoder: CustomStringConvertible {
|
||||
|
||||
|
||||
/// Errors that can occur during multipart encoding.
|
||||
public enum Error: Swift.Error, CustomStringConvertible {
|
||||
|
||||
|
||||
/// The specified file cannot be read or is invalid.
|
||||
case invalidFile(URL)
|
||||
|
||||
|
||||
/// The output file cannot be created or written to.
|
||||
case invalidOutputFile(URL)
|
||||
|
||||
|
||||
/// An error occurred while reading from or writing to a stream.
|
||||
case streamError
|
||||
|
||||
|
||||
/// The total data size exceeds the 50MB limit for in-memory encoding.
|
||||
case tooMuchDataForMemory
|
||||
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case let .invalidFile(url):
|
||||
|
|
@ -359,7 +359,7 @@ public final class MultipartFormEncoder: CustomStringConvertible {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public var description: String {
|
||||
"<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.
|
||||
public enum RequestBuilderError: Error {
|
||||
|
||||
|
||||
/// The form data could not be encoded properly.
|
||||
case invalidFormData(HTTPRequest)
|
||||
}
|
||||
|
|
@ -31,7 +31,7 @@ public enum RequestBuilderError: Error {
|
|||
/// contentType: .json,
|
||||
/// parameters: ["name": "Jane", "email": "jane@example.net"]
|
||||
/// )
|
||||
///
|
||||
///
|
||||
/// let urlRequest = try RequestBuilder.build(request: httpRequest)
|
||||
/// let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
|
||||
/// let httpResponse = HTTPResponse(response: response, data: data, error: error)
|
||||
|
|
@ -39,7 +39,7 @@ public enum RequestBuilderError: Error {
|
|||
/// }
|
||||
/// ```
|
||||
public final class RequestBuilder {
|
||||
|
||||
|
||||
/// 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:
|
||||
|
|
@ -60,7 +60,7 @@ public final class RequestBuilder {
|
|||
public class func build(request: HTTPRequest) throws -> URLRequest {
|
||||
var result = URLRequest(url: request.url)
|
||||
result.httpMethod = request.method.string
|
||||
|
||||
|
||||
for (name, value) in request.headers {
|
||||
result.addValue(value, forHTTPHeaderField: name)
|
||||
}
|
||||
|
|
@ -83,7 +83,7 @@ public final class RequestBuilder {
|
|||
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
private class func encodeMultipartContent(to urlRequest: inout URLRequest, request: HTTPRequest) throws {
|
||||
let encoder = MultipartFormEncoder()
|
||||
let body = try encoder.encodeData(parts: request.parts)
|
||||
|
|
@ -91,28 +91,28 @@ public final class RequestBuilder {
|
|||
urlRequest.addValue("\(body.contentLength)", forHTTPHeaderField: "Content-Length")
|
||||
urlRequest.httpBody = body.data
|
||||
}
|
||||
|
||||
|
||||
private class func encodeParameters(to urlRequest: inout URLRequest, request: HTTPRequest, parameters: [String: any Sendable]) throws {
|
||||
switch request.contentType {
|
||||
case .json:
|
||||
try encodeJSONParameters(to: &urlRequest, parameters: parameters)
|
||||
|
||||
|
||||
case .none:
|
||||
log.warning("Cannot serialize parameters without a content type, falling back to form encoding")
|
||||
fallthrough
|
||||
case .formEncoded:
|
||||
try encodeFormParameters(to: &urlRequest, request: request, parameters: parameters)
|
||||
|
||||
|
||||
case .multipart:
|
||||
try encodeMultipartContent(to: &urlRequest, request: request)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class func encodeJSONParameters(to urlRequest: inout URLRequest, parameters: [String: any Sendable]) throws {
|
||||
urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
urlRequest.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: [])
|
||||
}
|
||||
|
||||
|
||||
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")
|
||||
guard let formData = FormEncoder.encode(parameters).data(using: .utf8) else {
|
||||
|
|
@ -120,23 +120,23 @@ public final class RequestBuilder {
|
|||
}
|
||||
urlRequest.httpBody = formData
|
||||
}
|
||||
|
||||
|
||||
private class func encodeQueryParameters(to urlRequest: inout URLRequest, parameters: [String: any Sendable]) throws {
|
||||
guard let url = urlRequest.url else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
|
||||
let newQueryItems = parameters.compactMap { (key, value) -> URLQueryItem? in
|
||||
URLQueryItem(name: key, value: String(describing: value))
|
||||
}
|
||||
|
||||
|
||||
if let existingQueryItems = components?.queryItems {
|
||||
components?.queryItems = existingQueryItems + newQueryItems
|
||||
} else {
|
||||
} else if !newQueryItems.isEmpty {
|
||||
components?.queryItems = newQueryItems
|
||||
}
|
||||
|
||||
|
||||
urlRequest.url = components?.url ?? url
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,44 +13,44 @@ class FormEncoderTests: XCTestCase {
|
|||
let result = FormEncoder.encode([:])
|
||||
XCTAssertEqual(result, "")
|
||||
}
|
||||
|
||||
|
||||
func testEncodeSingleStringValue() {
|
||||
let parameters = ["name": "Jane Doe"]
|
||||
let result = FormEncoder.encode(parameters)
|
||||
XCTAssertEqual(result, "name=Jane%20Doe")
|
||||
}
|
||||
|
||||
|
||||
func testEncodeMultipleStringValues() {
|
||||
let parameters = ["name": "John", "email": "john@example.net"]
|
||||
let result = FormEncoder.encode(parameters)
|
||||
// Keys should be sorted alphabetically
|
||||
XCTAssertEqual(result, "email=john%40example.net&name=John")
|
||||
}
|
||||
|
||||
|
||||
func testEncodeIntegerValue() {
|
||||
let parameters = ["age": 30]
|
||||
let result = FormEncoder.encode(parameters)
|
||||
XCTAssertEqual(result, "age=30")
|
||||
}
|
||||
|
||||
|
||||
func testEncodeBooleanValues() {
|
||||
let parameters = ["active": true, "verified": false]
|
||||
let result = FormEncoder.encode(parameters)
|
||||
XCTAssertEqual(result, "active=1&verified=0")
|
||||
}
|
||||
|
||||
|
||||
func testEncodeNSNumberBooleanValues() {
|
||||
let parameters = ["active": NSNumber(value: true), "verified": NSNumber(value: false)]
|
||||
let result = FormEncoder.encode(parameters)
|
||||
XCTAssertEqual(result, "active=1&verified=0")
|
||||
}
|
||||
|
||||
|
||||
func testEncodeNSNumberIntegerValues() {
|
||||
let parameters = ["count": NSNumber(value: 42)]
|
||||
let result = FormEncoder.encode(parameters)
|
||||
XCTAssertEqual(result, "count=42")
|
||||
}
|
||||
|
||||
|
||||
func testEncodeNestedDictionary() {
|
||||
let personData: [String: any Sendable] = ["name": "Jane", "age": 30]
|
||||
let parameters: [String: any Sendable] = ["person": personData]
|
||||
|
|
@ -60,13 +60,13 @@ class FormEncoderTests: XCTestCase {
|
|||
let expected2 = "person%5Bname%5D=Jane&person%5Bage%5D=30"
|
||||
XCTAssertTrue(result == expected1 || result == expected2, "Result '\(result)' doesn't match either expected format")
|
||||
}
|
||||
|
||||
|
||||
func testEncodeArray() {
|
||||
let parameters = ["tags": ["swift", "ios", "mobile"]]
|
||||
let result = FormEncoder.encode(parameters)
|
||||
XCTAssertEqual(result, "tags%5B%5D=swift&tags%5B%5D=ios&tags%5B%5D=mobile")
|
||||
}
|
||||
|
||||
|
||||
func testEncodeComplexNestedStructure() {
|
||||
let preferences: [String: any Sendable] = ["theme": "dark", "notifications": true]
|
||||
let tags: [any Sendable] = ["rockstar", "swiftie"]
|
||||
|
|
@ -76,7 +76,7 @@ class FormEncoderTests: XCTestCase {
|
|||
"tags": tags
|
||||
]
|
||||
let parameters: [String: any Sendable] = ["person": personData]
|
||||
|
||||
|
||||
let result = FormEncoder.encode(parameters)
|
||||
// The actual order depends on how the dictionary is sorted, so let's test the components
|
||||
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=swiftie"))
|
||||
}
|
||||
|
||||
|
||||
func testEncodeSpecialCharacters() {
|
||||
let parameters = ["message": "Hello & welcome to Abbey Road Studios! 100% music magic guaranteed."]
|
||||
let result = FormEncoder.encode(parameters)
|
||||
XCTAssertEqual(result, "message=Hello%20%26%20welcome%20to%20Abbey%20Road%20Studios%21%20100%25%20music%20magic%20guaranteed.")
|
||||
}
|
||||
|
||||
|
||||
func testEncodeUnicodeCharacters() {
|
||||
let parameters = ["emoji": "🚀👨💻", "chinese": "你好"]
|
||||
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")
|
||||
}
|
||||
|
||||
|
||||
func testKeysAreSortedAlphabetically() {
|
||||
let parameters = ["zebra": "z", "alpha": "a", "beta": "b"]
|
||||
let result = FormEncoder.encode(parameters)
|
||||
XCTAssertEqual(result, "alpha=a&beta=b&zebra=z")
|
||||
}
|
||||
|
||||
|
||||
func testEncodeDoubleValue() {
|
||||
let parameters = ["price": 19.99]
|
||||
let result = FormEncoder.encode(parameters)
|
||||
XCTAssertEqual(result, "price=19.99")
|
||||
}
|
||||
|
||||
|
||||
func testEncodeNilValuesAsStrings() {
|
||||
// Swift's Any type handling - nil values become "<null>" strings
|
||||
let parameters = ["optional": NSNull()]
|
||||
let result = FormEncoder.encode(parameters)
|
||||
XCTAssertEqual(result, "optional=%3Cnull%3E")
|
||||
}
|
||||
|
||||
|
||||
func testRFC3986Compliance() {
|
||||
// Test that reserved characters are properly encoded according to RFC 3986
|
||||
let parameters = ["reserved": "!*'();:@&=+$,/?#[]"]
|
||||
|
|
@ -124,14 +124,14 @@ class FormEncoderTests: XCTestCase {
|
|||
// 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")
|
||||
}
|
||||
|
||||
|
||||
func testURLQueryAllowedCharacters() {
|
||||
// Test characters that should NOT be encoded
|
||||
let parameters = ["allowed": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"]
|
||||
let result = FormEncoder.encode(parameters)
|
||||
XCTAssertEqual(result, "allowed=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~")
|
||||
}
|
||||
|
||||
|
||||
func testMixedDataTypes() {
|
||||
let array: [any Sendable] = [1, 2, 3]
|
||||
let nested: [String: any Sendable] = ["key": "nested_value"]
|
||||
|
|
@ -143,7 +143,7 @@ class FormEncoderTests: XCTestCase {
|
|||
"array": array,
|
||||
"nested": nested
|
||||
]
|
||||
|
||||
|
||||
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"
|
||||
XCTAssertEqual(result, expected)
|
||||
|
|
@ -152,29 +152,29 @@ class FormEncoderTests: XCTestCase {
|
|||
|
||||
// Test the NSNumber extension
|
||||
class NSNumberBoolExtensionTests: XCTestCase {
|
||||
|
||||
|
||||
func testNSNumberIsBoolForBooleans() {
|
||||
let trueNumber = NSNumber(value: true)
|
||||
let falseNumber = NSNumber(value: false)
|
||||
|
||||
|
||||
XCTAssertTrue(trueNumber.isBool)
|
||||
XCTAssertTrue(falseNumber.isBool)
|
||||
}
|
||||
|
||||
|
||||
func testNSNumberIsBoolForIntegers() {
|
||||
let intNumber = NSNumber(value: 42)
|
||||
let zeroNumber = NSNumber(value: 0)
|
||||
let oneNumber = NSNumber(value: 1)
|
||||
|
||||
|
||||
XCTAssertFalse(intNumber.isBool)
|
||||
XCTAssertFalse(zeroNumber.isBool)
|
||||
XCTAssertFalse(oneNumber.isBool)
|
||||
}
|
||||
|
||||
|
||||
func testNSNumberIsBoolForDoubles() {
|
||||
let doubleNumber = NSNumber(value: 3.14)
|
||||
let zeroDouble = NSNumber(value: 0.0)
|
||||
|
||||
|
||||
XCTAssertFalse(doubleNumber.isBool)
|
||||
XCTAssertFalse(zeroDouble.isBool)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,51 +9,51 @@
|
|||
import XCTest
|
||||
|
||||
class HTTPRequestErrorTests: XCTestCase {
|
||||
|
||||
|
||||
func testHTTPError() {
|
||||
let error = HTTPRequestError.http
|
||||
XCTAssertEqual(error.localizedDescription, "HTTP request failed with non-2xx status code")
|
||||
XCTAssertEqual(error.failureReason, "The server returned an error status code")
|
||||
XCTAssertEqual(error.recoverySuggestion, "Check the server response for error details")
|
||||
}
|
||||
|
||||
|
||||
func testUnknownError() {
|
||||
let error = HTTPRequestError.unknown
|
||||
XCTAssertEqual(error.localizedDescription, "An unknown error occurred")
|
||||
XCTAssertEqual(error.failureReason, "An unexpected error occurred during the request")
|
||||
XCTAssertEqual(error.recoverySuggestion, "Check network connectivity and try again")
|
||||
}
|
||||
|
||||
|
||||
func testErrorDescriptionIsNeverNil() {
|
||||
let allErrors: [HTTPRequestError] = [
|
||||
.http,
|
||||
.unknown
|
||||
]
|
||||
|
||||
|
||||
for error in allErrors {
|
||||
XCTAssertNotNil(error.errorDescription)
|
||||
XCTAssertFalse(error.errorDescription!.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func testFailureReasonIsNeverNil() {
|
||||
let allErrors: [HTTPRequestError] = [
|
||||
.http,
|
||||
.unknown
|
||||
]
|
||||
|
||||
|
||||
for error in allErrors {
|
||||
XCTAssertNotNil(error.failureReason)
|
||||
XCTAssertFalse(error.failureReason!.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func testRecoverySuggestionIsNeverNil() {
|
||||
let allErrors: [HTTPRequestError] = [
|
||||
.http,
|
||||
.unknown
|
||||
]
|
||||
|
||||
|
||||
for error in allErrors {
|
||||
XCTAssertNotNil(error.recoverySuggestion)
|
||||
XCTAssertFalse(error.recoverySuggestion!.isEmpty)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import XCTest
|
|||
|
||||
class HTTPRequestTests: XCTestCase {
|
||||
let baseURL = URL(string: "https://api.example.net")!
|
||||
|
||||
|
||||
func testHTTPRequestInitialization() {
|
||||
let request = HTTPRequest(method: .get, url: baseURL)
|
||||
XCTAssertEqual(request.method, .get)
|
||||
|
|
@ -20,74 +20,74 @@ class HTTPRequestTests: XCTestCase {
|
|||
XCTAssertTrue(request.headers.isEmpty)
|
||||
XCTAssertTrue(request.parts.isEmpty)
|
||||
}
|
||||
|
||||
|
||||
func testHTTPRequestWithParameters() {
|
||||
let params = ["key": "value", "number": 42] as [String: any Sendable]
|
||||
let request = HTTPRequest(method: .post, url: baseURL, contentType: .json, parameters: params)
|
||||
|
||||
|
||||
XCTAssertEqual(request.method, .post)
|
||||
XCTAssertEqual(request.contentType, .json)
|
||||
XCTAssertNotNil(request.parameters)
|
||||
}
|
||||
|
||||
|
||||
func testGETConvenience() {
|
||||
let request = HTTPRequest.get(baseURL)
|
||||
XCTAssertEqual(request.method, .get)
|
||||
XCTAssertEqual(request.url, baseURL)
|
||||
XCTAssertEqual(request.contentType, .none)
|
||||
}
|
||||
|
||||
|
||||
func testPOSTConvenience() {
|
||||
let params = ["name": "Jane"]
|
||||
let request = HTTPRequest.post(baseURL, contentType: .json, parameters: params)
|
||||
|
||||
|
||||
XCTAssertEqual(request.method, .post)
|
||||
XCTAssertEqual(request.contentType, .json)
|
||||
XCTAssertNotNil(request.parameters)
|
||||
}
|
||||
|
||||
|
||||
func testPUTConvenience() {
|
||||
let params = ["name": "Jane"]
|
||||
let request = HTTPRequest.put(baseURL, contentType: .formEncoded, parameters: params)
|
||||
|
||||
|
||||
XCTAssertEqual(request.method, .put)
|
||||
XCTAssertEqual(request.contentType, .formEncoded)
|
||||
XCTAssertNotNil(request.parameters)
|
||||
}
|
||||
|
||||
|
||||
func testDELETEConvenience() {
|
||||
let request = HTTPRequest.delete(baseURL)
|
||||
XCTAssertEqual(request.method, .delete)
|
||||
XCTAssertEqual(request.url, baseURL)
|
||||
XCTAssertEqual(request.contentType, .none)
|
||||
}
|
||||
|
||||
|
||||
func testMultipartPartsAutomaticallySetContentType() {
|
||||
var request = HTTPRequest.post(baseURL)
|
||||
XCTAssertEqual(request.contentType, .none)
|
||||
|
||||
|
||||
request.parts = [.text("value", name: "field")]
|
||||
XCTAssertEqual(request.contentType, .multipart)
|
||||
}
|
||||
|
||||
|
||||
#if canImport(UIKit)
|
||||
func testAddMultipartJPEG() {
|
||||
var request = HTTPRequest.post(baseURL)
|
||||
|
||||
|
||||
// Create a simple 1x1 pixel image
|
||||
let size = CGSize(width: 1, height: 1)
|
||||
UIGraphicsBeginImageContext(size)
|
||||
let image = UIGraphicsGetImageFromCurrentImageContext()!
|
||||
UIGraphicsEndImageContext()
|
||||
|
||||
|
||||
request.addMultipartJPEG(name: "avatar", image: image, quality: 0.8, filename: "test.jpg")
|
||||
|
||||
|
||||
XCTAssertEqual(request.parts.count, 1)
|
||||
XCTAssertEqual(request.contentType, .multipart)
|
||||
|
||||
|
||||
let part = request.parts.first!
|
||||
XCTAssertEqual(part.name, "avatar")
|
||||
|
||||
|
||||
if case let .binaryData(_, type, filename) = part.content {
|
||||
XCTAssertEqual(type, "image/jpeg")
|
||||
XCTAssertEqual(filename, "test.jpg")
|
||||
|
|
@ -95,71 +95,71 @@ class HTTPRequestTests: XCTestCase {
|
|||
XCTFail("Expected binary data content")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func testAddMultipartJPEGWithInvalidQuality() {
|
||||
var request = HTTPRequest.post(baseURL)
|
||||
|
||||
|
||||
// Create a valid image
|
||||
let size = CGSize(width: 1, height: 1)
|
||||
UIGraphicsBeginImageContext(size)
|
||||
let image = UIGraphicsGetImageFromCurrentImageContext()!
|
||||
UIGraphicsEndImageContext()
|
||||
|
||||
|
||||
// Test with extreme quality values that might cause issues
|
||||
request.addMultipartJPEG(name: "avatar1", image: image, quality: -1.0)
|
||||
request.addMultipartJPEG(name: "avatar2", image: image, quality: 2.0)
|
||||
|
||||
|
||||
// The method should handle extreme quality values gracefully
|
||||
// Either by clamping them or by having jpegData handle them
|
||||
XCTAssertTrue(request.parts.count >= 0) // Should not crash
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
func testHTTPRequestPATCHConvenience() {
|
||||
let params = ["status": "active"]
|
||||
let request = HTTPRequest(method: .patch, url: baseURL, contentType: .json, parameters: params)
|
||||
|
||||
|
||||
XCTAssertEqual(request.method, .patch)
|
||||
XCTAssertEqual(request.contentType, .json)
|
||||
XCTAssertNotNil(request.parameters)
|
||||
}
|
||||
|
||||
|
||||
func testHTTPRequestWithMultipleHeaders() {
|
||||
var request = HTTPRequest.get(baseURL)
|
||||
request.addHeader(name: "Authorization", value: "Bearer token123")
|
||||
request.addHeader(name: "User-Agent", value: "Osiris/2.0")
|
||||
request.addHeader(name: "Accept", value: "application/json")
|
||||
|
||||
|
||||
XCTAssertEqual(request.headers["Authorization"], "Bearer token123")
|
||||
XCTAssertEqual(request.headers["User-Agent"], "Osiris/2.0")
|
||||
XCTAssertEqual(request.headers["Accept"], "application/json")
|
||||
XCTAssertEqual(request.headers.count, 3)
|
||||
}
|
||||
|
||||
|
||||
func testHTTPRequestOverwriteHeaders() {
|
||||
var request = HTTPRequest.get(baseURL)
|
||||
request.addHeader(name: "Accept", value: "application/xml")
|
||||
request.addHeader(name: "Accept", value: "application/json") // Should overwrite
|
||||
|
||||
|
||||
XCTAssertEqual(request.headers["Accept"], "application/json")
|
||||
XCTAssertEqual(request.headers.count, 1)
|
||||
}
|
||||
|
||||
|
||||
func testHTTPRequestWithEmptyMultipartParts() {
|
||||
var request = HTTPRequest.post(baseURL)
|
||||
request.parts = [] // Empty parts array
|
||||
|
||||
|
||||
XCTAssertEqual(request.contentType, .none) // Should not be set to multipart
|
||||
XCTAssertTrue(request.parts.isEmpty)
|
||||
}
|
||||
|
||||
|
||||
func testHTTPRequestMultipartPartsResetContentType() {
|
||||
var request = HTTPRequest.post(baseURL, contentType: .json)
|
||||
XCTAssertEqual(request.contentType, .json)
|
||||
|
||||
|
||||
request.parts = [.text("test", name: "field")]
|
||||
XCTAssertEqual(request.contentType, .multipart) // Should be automatically changed
|
||||
|
||||
|
||||
request.parts = [] // Clear parts
|
||||
XCTAssertEqual(request.contentType, .multipart) // Should remain multipart
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ class HTTPResponseTests: XCTestCase {
|
|||
let url = URL(string: "https://api.example.net")!
|
||||
let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: ["Content-Type": "application/json"])!
|
||||
let data = Data("{}".utf8)
|
||||
|
||||
|
||||
let response = HTTPResponse(response: httpResponse, data: data, error: nil)
|
||||
|
||||
|
||||
if case let .success(urlResponse, responseData) = response {
|
||||
XCTAssertEqual(urlResponse.statusCode, 200)
|
||||
XCTAssertEqual(responseData, data)
|
||||
|
|
@ -23,14 +23,14 @@ class HTTPResponseTests: XCTestCase {
|
|||
XCTFail("Expected success response")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func testFailureResponseWithError() {
|
||||
let url = URL(string: "https://api.example.net")!
|
||||
let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)!
|
||||
let error = NSError(domain: "test", code: 1, userInfo: nil)
|
||||
|
||||
|
||||
let response = HTTPResponse(response: httpResponse, data: nil, error: error)
|
||||
|
||||
|
||||
if case let .failure(responseError, urlResponse, responseData) = response {
|
||||
XCTAssertEqual((responseError as NSError).domain, "test")
|
||||
XCTAssertEqual(urlResponse?.statusCode, 200)
|
||||
|
|
@ -39,14 +39,14 @@ class HTTPResponseTests: XCTestCase {
|
|||
XCTFail("Expected failure response")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func testFailureResponseWithHTTPError() {
|
||||
let url = URL(string: "https://api.example.net")!
|
||||
let httpResponse = HTTPURLResponse(url: url, statusCode: 404, httpVersion: nil, headerFields: nil)!
|
||||
let data = Data("Not Found".utf8)
|
||||
|
||||
|
||||
let response = HTTPResponse(response: httpResponse, data: data, error: nil)
|
||||
|
||||
|
||||
if case let .failure(error, urlResponse, responseData) = response {
|
||||
XCTAssertTrue(error is HTTPRequestError)
|
||||
XCTAssertEqual(urlResponse?.statusCode, 404)
|
||||
|
|
@ -55,114 +55,114 @@ class HTTPResponseTests: XCTestCase {
|
|||
XCTFail("Expected failure response")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func testResponseWithoutHTTPURLResponse() {
|
||||
let response = HTTPResponse(response: nil, data: nil, error: nil)
|
||||
|
||||
|
||||
if case let .failure(error, _, _) = response {
|
||||
XCTAssertTrue(error is HTTPRequestError)
|
||||
} else {
|
||||
XCTFail("Expected failure response")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func testDataProperty() {
|
||||
let data = Data("test".utf8)
|
||||
let url = URL(string: "https://api.example.net")!
|
||||
let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)!
|
||||
|
||||
|
||||
let successResponse = HTTPResponse(response: httpResponse, data: data, error: nil)
|
||||
XCTAssertEqual(successResponse.data, data)
|
||||
|
||||
|
||||
let httpErrorResponse = HTTPURLResponse(url: url, statusCode: 400, httpVersion: nil, headerFields: nil)!
|
||||
let failureResponse = HTTPResponse(response: httpErrorResponse, data: data, error: nil)
|
||||
XCTAssertEqual(failureResponse.data, data)
|
||||
}
|
||||
|
||||
|
||||
func testStatusProperty() {
|
||||
let url = URL(string: "https://api.example.net")!
|
||||
let httpResponse = HTTPURLResponse(url: url, statusCode: 201, httpVersion: nil, headerFields: nil)!
|
||||
|
||||
|
||||
let response = HTTPResponse(response: httpResponse, data: nil, error: nil)
|
||||
XCTAssertEqual(response.status, 201)
|
||||
}
|
||||
|
||||
|
||||
func testHeadersProperty() {
|
||||
let url = URL(string: "https://api.example.net")!
|
||||
let headers = ["Content-Type": "application/json", "X-Custom": "value"]
|
||||
let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: headers)!
|
||||
|
||||
|
||||
let response = HTTPResponse(response: httpResponse, data: nil, error: nil)
|
||||
XCTAssertEqual(response.headers["Content-Type"] as? String, "application/json")
|
||||
XCTAssertEqual(response.headers["X-Custom"] as? String, "value")
|
||||
}
|
||||
|
||||
|
||||
func testBodyStringProperty() {
|
||||
let url = URL(string: "https://api.example.net")!
|
||||
let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)!
|
||||
let data = Data("Hello, World!".utf8)
|
||||
|
||||
|
||||
let response = HTTPResponse(response: httpResponse, data: data, error: nil)
|
||||
XCTAssertEqual(response.bodyString, "Hello, World!")
|
||||
}
|
||||
|
||||
|
||||
func testBodyStringPropertyWithNoData() {
|
||||
let url = URL(string: "https://api.example.net")!
|
||||
let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)!
|
||||
|
||||
|
||||
let response = HTTPResponse(response: httpResponse, data: nil, error: nil)
|
||||
XCTAssertEqual(response.bodyString, "")
|
||||
}
|
||||
|
||||
|
||||
func testDictionaryFromJSONProperty() {
|
||||
let url = URL(string: "https://api.example.net")!
|
||||
let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)!
|
||||
let json = ["name": "John", "age": 30] as [String: any Sendable]
|
||||
let data = try! JSONSerialization.data(withJSONObject: json)
|
||||
|
||||
|
||||
let response = HTTPResponse(response: httpResponse, data: data, error: nil)
|
||||
let dictionary = response.dictionaryFromJSON
|
||||
|
||||
|
||||
XCTAssertEqual(dictionary["name"] as? String, "John")
|
||||
XCTAssertEqual(dictionary["age"] as? Int, 30)
|
||||
}
|
||||
|
||||
|
||||
func testDictionaryFromJSONPropertyWithInvalidJSON() {
|
||||
let url = URL(string: "https://api.example.net")!
|
||||
let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)!
|
||||
let data = Data("invalid json".utf8)
|
||||
|
||||
|
||||
let response = HTTPResponse(response: httpResponse, data: data, error: nil)
|
||||
let dictionary = response.dictionaryFromJSON
|
||||
|
||||
|
||||
XCTAssertTrue(dictionary.isEmpty)
|
||||
}
|
||||
|
||||
|
||||
func testDictionaryFromJSONPropertyWithNoData() {
|
||||
let url = URL(string: "https://api.example.net")!
|
||||
let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)!
|
||||
|
||||
|
||||
let response = HTTPResponse(response: httpResponse, data: nil, error: nil)
|
||||
XCTAssertEqual(response.bodyString, "")
|
||||
}
|
||||
|
||||
|
||||
func testDictionaryFromJSONPropertyWithNonDictionaryJSON() {
|
||||
let url = URL(string: "https://api.example.net")!
|
||||
let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)!
|
||||
let arrayJSON = try! JSONSerialization.data(withJSONObject: ["item1", "item2", "item3"])
|
||||
|
||||
|
||||
let response = HTTPResponse(response: httpResponse, data: arrayJSON, error: nil)
|
||||
let dictionary = response.dictionaryFromJSON
|
||||
|
||||
|
||||
XCTAssertTrue(dictionary.isEmpty)
|
||||
}
|
||||
|
||||
|
||||
func testUnderlyingResponseProperty() {
|
||||
let url = URL(string: "https://api.example.net")!
|
||||
let httpResponse = HTTPURLResponse(url: url, statusCode: 201, httpVersion: "HTTP/1.1", headerFields: ["Server": "nginx"])!
|
||||
|
||||
|
||||
let response = HTTPResponse(response: httpResponse, data: nil, error: nil)
|
||||
|
||||
|
||||
if case let .success(underlyingResponse, _) = response {
|
||||
XCTAssertEqual(underlyingResponse.statusCode, 201)
|
||||
XCTAssertEqual(underlyingResponse.allHeaderFields["Server"] as? String, "nginx")
|
||||
|
|
@ -170,41 +170,41 @@ class HTTPResponseTests: XCTestCase {
|
|||
XCTFail("Expected success response")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func testResponseStringDescription() {
|
||||
let url = URL(string: "https://api.example.net")!
|
||||
let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)!
|
||||
let data = Data("test response".utf8)
|
||||
|
||||
|
||||
let successResponse = HTTPResponse(response: httpResponse, data: data, error: nil)
|
||||
let description = String(describing: successResponse)
|
||||
XCTAssertTrue(description.contains("success"))
|
||||
|
||||
|
||||
let failureResponse = HTTPResponse(response: httpResponse, data: data, error: HTTPRequestError.http)
|
||||
let failureDescription = String(describing: failureResponse)
|
||||
XCTAssertTrue(failureDescription.contains("failure"))
|
||||
}
|
||||
|
||||
|
||||
func testResponseWithDifferentStatusCodes() {
|
||||
let url = URL(string: "https://api.example.net")!
|
||||
|
||||
|
||||
// Test various 2xx success codes
|
||||
for statusCode in [200, 201, 202, 204, 206] {
|
||||
let httpResponse = HTTPURLResponse(url: url, statusCode: statusCode, httpVersion: nil, headerFields: nil)!
|
||||
let response = HTTPResponse(response: httpResponse, data: nil, error: nil)
|
||||
|
||||
|
||||
if case .success = response {
|
||||
// Expected
|
||||
} else {
|
||||
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] {
|
||||
let httpResponse = HTTPURLResponse(url: url, statusCode: statusCode, httpVersion: nil, headerFields: nil)!
|
||||
let response = HTTPResponse(response: httpResponse, data: nil, error: nil)
|
||||
|
||||
|
||||
if case .failure = response {
|
||||
// Expected
|
||||
} else {
|
||||
|
|
@ -212,25 +212,25 @@ class HTTPResponseTests: XCTestCase {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func testResponseWithBinaryData() {
|
||||
let url = URL(string: "https://api.example.net")!
|
||||
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 response = HTTPResponse(response: httpResponse, data: binaryData, error: nil)
|
||||
|
||||
|
||||
XCTAssertEqual(response.data, binaryData)
|
||||
// bodyString should handle binary data gracefully - it will be empty since this isn't valid UTF-8
|
||||
let bodyString = response.bodyString
|
||||
XCTAssertTrue(bodyString.isEmpty) // Binary data that isn't valid UTF-8 returns empty string
|
||||
}
|
||||
|
||||
|
||||
func testResponseStatusPropertyEdgeCases() {
|
||||
// Test with no HTTP response - creates dummy HTTPURLResponse with status 0
|
||||
let responseNoHTTP = HTTPResponse(response: nil, data: nil, error: nil)
|
||||
XCTAssertEqual(responseNoHTTP.status, 0)
|
||||
|
||||
|
||||
// Test with URLResponse that's not HTTPURLResponse - creates dummy HTTPURLResponse with status 0
|
||||
let url = URL(string: "file:///test.txt")!
|
||||
let fileResponse = URLResponse(url: url, mimeType: "text/plain", expectedContentLength: 10, textEncodingName: nil)
|
||||
|
|
|
|||
|
|
@ -10,153 +10,153 @@ import XCTest
|
|||
|
||||
class RequestBuilderTests: XCTestCase {
|
||||
let baseURL = URL(string: "https://api.example.net/users")!
|
||||
|
||||
|
||||
func testBuildBasicGETRequest() throws {
|
||||
let httpRequest = HTTPRequest.get(baseURL)
|
||||
let urlRequest = try RequestBuilder.build(request: httpRequest)
|
||||
|
||||
|
||||
XCTAssertEqual(urlRequest.url, baseURL)
|
||||
XCTAssertEqual(urlRequest.httpMethod, "GET")
|
||||
XCTAssertNil(urlRequest.httpBody)
|
||||
}
|
||||
|
||||
|
||||
func testBuildBasicPOSTRequest() throws {
|
||||
let httpRequest = HTTPRequest.post(baseURL)
|
||||
let urlRequest = try RequestBuilder.build(request: httpRequest)
|
||||
|
||||
|
||||
XCTAssertEqual(urlRequest.url, baseURL)
|
||||
XCTAssertEqual(urlRequest.httpMethod, "POST")
|
||||
XCTAssertNil(urlRequest.httpBody)
|
||||
}
|
||||
|
||||
|
||||
func testBuildRequestWithHeaders() throws {
|
||||
var httpRequest = HTTPRequest.get(baseURL)
|
||||
httpRequest.headers = ["Authorization": "Bearer token", "X-Custom": "value"]
|
||||
|
||||
|
||||
let urlRequest = try RequestBuilder.build(request: httpRequest)
|
||||
|
||||
|
||||
XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "Authorization"), "Bearer token")
|
||||
XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "X-Custom"), "value")
|
||||
}
|
||||
|
||||
|
||||
func testBuildJSONRequest() throws {
|
||||
let parameters = ["name": "Jane", "age": 30] as [String: any Sendable]
|
||||
let httpRequest = HTTPRequest.post(baseURL, contentType: .json, parameters: parameters)
|
||||
|
||||
|
||||
let urlRequest = try RequestBuilder.build(request: httpRequest)
|
||||
|
||||
|
||||
XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "Content-Type"), "application/json")
|
||||
XCTAssertNotNil(urlRequest.httpBody)
|
||||
|
||||
|
||||
// Verify the JSON content
|
||||
let bodyData = urlRequest.httpBody!
|
||||
let decodedJSON = try JSONSerialization.jsonObject(with: bodyData) as! [String: any Sendable]
|
||||
XCTAssertEqual(decodedJSON["name"] as? String, "Jane")
|
||||
XCTAssertEqual(decodedJSON["age"] as? Int, 30)
|
||||
}
|
||||
|
||||
|
||||
func testBuildFormEncodedRequest() throws {
|
||||
let parameters = ["email": "john@example.net", "password": "TaylorSwift1989"]
|
||||
let httpRequest = HTTPRequest.post(baseURL, contentType: .formEncoded, parameters: parameters)
|
||||
|
||||
|
||||
let urlRequest = try RequestBuilder.build(request: httpRequest)
|
||||
|
||||
|
||||
XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "Content-Type"), "application/x-www-form-urlencoded")
|
||||
XCTAssertNotNil(urlRequest.httpBody)
|
||||
|
||||
|
||||
let bodyString = String(data: urlRequest.httpBody!, encoding: .utf8)!
|
||||
XCTAssertTrue(bodyString.contains("email=john%40example.net"))
|
||||
XCTAssertTrue(bodyString.contains("password=TaylorSwift1989"))
|
||||
}
|
||||
|
||||
|
||||
// Note: Testing .none content type with parameters would trigger an assertion failure
|
||||
// This is by design - developers should specify an appropriate content type
|
||||
|
||||
|
||||
func testBuildMultipartRequest() throws {
|
||||
var httpRequest = HTTPRequest.post(baseURL)
|
||||
httpRequest.parts = [
|
||||
.text("Jane Doe", name: "name"),
|
||||
.data(Data("test".utf8), name: "file", type: "text/plain", filename: "test.txt")
|
||||
]
|
||||
|
||||
|
||||
let urlRequest = try RequestBuilder.build(request: httpRequest)
|
||||
|
||||
|
||||
let contentType = urlRequest.value(forHTTPHeaderField: "Content-Type")
|
||||
XCTAssertNotNil(contentType)
|
||||
XCTAssertTrue(contentType!.hasPrefix("multipart/form-data; boundary="))
|
||||
|
||||
|
||||
let contentLength = urlRequest.value(forHTTPHeaderField: "Content-Length")
|
||||
XCTAssertNotNil(contentLength)
|
||||
XCTAssertGreaterThan(Int(contentLength!)!, 0)
|
||||
|
||||
|
||||
XCTAssertNotNil(urlRequest.httpBody)
|
||||
}
|
||||
|
||||
|
||||
func testBuildRequestWithInvalidFormData() throws {
|
||||
// Create a parameter that would cause UTF-8 encoding to 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.
|
||||
|
||||
|
||||
// Since FormEncoder is quite robust and UTF-8 encoding rarely fails,
|
||||
// we'll test this by creating a subclass that can force the failure
|
||||
// But for now, we'll document this edge case exists
|
||||
XCTAssertNoThrow(try RequestBuilder.build(request: HTTPRequest.post(baseURL, contentType: .formEncoded, parameters: ["test": "value"])))
|
||||
}
|
||||
|
||||
|
||||
func testBuildRequestWithAllHTTPMethods() throws {
|
||||
let methods: [HTTPMethod] = [.get, .post, .put, .patch, .delete]
|
||||
|
||||
|
||||
for method in methods {
|
||||
let httpRequest = HTTPRequest(method: method, url: baseURL)
|
||||
let urlRequest = try RequestBuilder.build(request: httpRequest)
|
||||
|
||||
|
||||
XCTAssertEqual(urlRequest.httpMethod, method.string)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func testBuildRequestPreservesURL() throws {
|
||||
let complexURL = URL(string: "https://api.example.net/users?page=1#section")!
|
||||
let httpRequest = HTTPRequest.get(complexURL)
|
||||
|
||||
|
||||
let urlRequest = try RequestBuilder.build(request: httpRequest)
|
||||
|
||||
|
||||
XCTAssertEqual(urlRequest.url, complexURL)
|
||||
}
|
||||
|
||||
|
||||
func testMultipleHeadersWithSameName() throws {
|
||||
var httpRequest = HTTPRequest.get(baseURL)
|
||||
httpRequest.headers = ["Accept": "application/json"]
|
||||
|
||||
|
||||
let urlRequest = try RequestBuilder.build(request: httpRequest)
|
||||
|
||||
|
||||
XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "Accept"), "application/json")
|
||||
}
|
||||
|
||||
|
||||
func testBuildRequestWithEmptyMultipartParts() throws {
|
||||
var httpRequest = HTTPRequest.post(baseURL)
|
||||
httpRequest.parts = []
|
||||
httpRequest.contentType = .multipart // Explicitly set to multipart
|
||||
|
||||
|
||||
let urlRequest = try RequestBuilder.build(request: httpRequest)
|
||||
|
||||
|
||||
let contentType = try XCTUnwrap(urlRequest.value(forHTTPHeaderField: "Content-Type"))
|
||||
XCTAssertTrue(contentType.hasPrefix("multipart/form-data; boundary="))
|
||||
XCTAssertNotNil(urlRequest.httpBody)
|
||||
}
|
||||
|
||||
|
||||
func testBuildRequestWithLargeMultipartData() throws {
|
||||
var httpRequest = HTTPRequest.post(baseURL)
|
||||
let largeData = Data(repeating: 65, count: 1024 * 1024) // 1MB of 'A' characters
|
||||
httpRequest.parts = [
|
||||
.data(largeData, name: "largefile", type: "application/octet-stream", filename: "large.bin")
|
||||
]
|
||||
|
||||
|
||||
let urlRequest = try RequestBuilder.build(request: httpRequest)
|
||||
|
||||
|
||||
XCTAssertNotNil(urlRequest.httpBody)
|
||||
XCTAssertGreaterThan(urlRequest.httpBody!.count, 1024 * 1024)
|
||||
}
|
||||
|
||||
|
||||
func testBuildRequestWithSpecialCharactersInHeaders() throws {
|
||||
var httpRequest = HTTPRequest.get(baseURL)
|
||||
httpRequest.headers = [
|
||||
|
|
@ -164,42 +164,42 @@ class RequestBuilderTests: XCTestCase {
|
|||
"X-Unicode": "🚀 rocket emoji",
|
||||
"X-Empty": ""
|
||||
]
|
||||
|
||||
|
||||
let urlRequest = try RequestBuilder.build(request: httpRequest)
|
||||
|
||||
|
||||
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-Empty"), "")
|
||||
}
|
||||
|
||||
|
||||
func testBuildRequestWithNilParameters() throws {
|
||||
let httpRequest = HTTPRequest.post(baseURL, contentType: .json, parameters: nil)
|
||||
let urlRequest = try RequestBuilder.build(request: httpRequest)
|
||||
|
||||
|
||||
// RequestBuilder may not set Content-Type if there are no parameters to encode
|
||||
XCTAssertNil(urlRequest.httpBody)
|
||||
}
|
||||
|
||||
|
||||
func testBuildRequestWithEmptyParameters() throws {
|
||||
let httpRequest = HTTPRequest.post(baseURL, contentType: .json, parameters: [:])
|
||||
let urlRequest = try RequestBuilder.build(request: httpRequest)
|
||||
|
||||
|
||||
XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "Content-Type"), "application/json")
|
||||
XCTAssertNotNil(urlRequest.httpBody)
|
||||
|
||||
|
||||
let bodyString = String(data: urlRequest.httpBody!, encoding: .utf8)!
|
||||
XCTAssertEqual(bodyString, "{}")
|
||||
}
|
||||
|
||||
|
||||
func testBuildRequestSetsContentType() throws {
|
||||
let httpRequest = HTTPRequest.post(baseURL, contentType: .json, parameters: ["test": "value"])
|
||||
let urlRequest = try RequestBuilder.build(request: httpRequest)
|
||||
|
||||
|
||||
// RequestBuilder should set the correct content type when there are parameters to encode
|
||||
let contentType = urlRequest.value(forHTTPHeaderField: "Content-Type")
|
||||
XCTAssertTrue(contentType?.contains("application/json") == true)
|
||||
}
|
||||
|
||||
|
||||
func testBuildRequestWithComplexJSONParameters() throws {
|
||||
let nestedData: [String: any Sendable] = ["theme": "dark", "notifications": true]
|
||||
let arrayData: [any Sendable] = ["rock", "pop", "jazz"]
|
||||
|
|
@ -211,86 +211,99 @@ class RequestBuilderTests: XCTestCase {
|
|||
"genres": arrayData
|
||||
] as [String: any Sendable]
|
||||
]
|
||||
|
||||
|
||||
let httpRequest = HTTPRequest.post(baseURL, contentType: .json, parameters: complexParams)
|
||||
let urlRequest = try RequestBuilder.build(request: httpRequest)
|
||||
|
||||
|
||||
XCTAssertNotNil(urlRequest.httpBody)
|
||||
let jsonObject = try JSONSerialization.jsonObject(with: urlRequest.httpBody!) as! [String: Any]
|
||||
let person = jsonObject["person"] as! [String: Any]
|
||||
XCTAssertEqual(person["name"] as? String, "David Bowie")
|
||||
XCTAssertEqual(person["age"] as? Int, 69)
|
||||
}
|
||||
|
||||
|
||||
func testBuildRequestWithNoneContentTypeFallsBackToFormEncoding() throws {
|
||||
// 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 urlRequest = try RequestBuilder.build(request: httpRequest)
|
||||
|
||||
|
||||
// Should fall back to form encoding and log a warning
|
||||
XCTAssertEqual(urlRequest.value(forHTTPHeaderField: "Content-Type"), "application/x-www-form-urlencoded")
|
||||
XCTAssertNotNil(urlRequest.httpBody)
|
||||
|
||||
|
||||
let bodyString = String(data: urlRequest.httpBody!, encoding: .utf8)!
|
||||
XCTAssertTrue(bodyString.contains("email=freddie%40example.net"))
|
||||
XCTAssertTrue(bodyString.contains("band=Queen"))
|
||||
}
|
||||
|
||||
|
||||
func testBuildGETRequestWithQueryParameters() throws {
|
||||
let httpRequest = HTTPRequest.get(baseURL, parameters: ["name": "John Doe", "email": "john@example.net"])
|
||||
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 ?? ""
|
||||
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("?"), "URL should contain query separator")
|
||||
}
|
||||
|
||||
|
||||
func testBuildDELETERequestWithQueryParameters() throws {
|
||||
let httpRequest = HTTPRequest.delete(baseURL, parameters: ["id": "123", "confirm": "true"])
|
||||
let urlRequest = try RequestBuilder.build(request: httpRequest)
|
||||
|
||||
|
||||
XCTAssertEqual(urlRequest.httpMethod, "DELETE")
|
||||
XCTAssertNil(urlRequest.httpBody)
|
||||
XCTAssertNil(urlRequest.value(forHTTPHeaderField: "Content-Type"))
|
||||
|
||||
|
||||
let urlString = urlRequest.url?.absoluteString ?? ""
|
||||
XCTAssertTrue(urlString.contains("id=123"))
|
||||
XCTAssertTrue(urlString.contains("confirm=true"))
|
||||
XCTAssertTrue(urlString.contains("?"))
|
||||
}
|
||||
|
||||
|
||||
func testBuildGETRequestWithExistingQueryString() throws {
|
||||
let urlWithQuery = URL(string: "https://api.example.net/users?existing=param")!
|
||||
let httpRequest = HTTPRequest.get(urlWithQuery, parameters: ["new": "value"])
|
||||
let urlRequest = try RequestBuilder.build(request: httpRequest)
|
||||
|
||||
|
||||
let urlString = urlRequest.url?.absoluteString ?? ""
|
||||
XCTAssertTrue(urlString.contains("existing=param"))
|
||||
XCTAssertTrue(urlString.contains("new=value"))
|
||||
XCTAssertTrue(urlString.contains("&"))
|
||||
}
|
||||
|
||||
|
||||
func testBuildGETRequestWithMultipartThrowsError() throws {
|
||||
var httpRequest = HTTPRequest.get(baseURL, parameters: ["name": "value"])
|
||||
httpRequest.contentType = HTTPContentType.multipart
|
||||
|
||||
|
||||
XCTAssertThrowsError(try RequestBuilder.build(request: httpRequest)) { error in
|
||||
XCTAssertTrue(error is RequestBuilderError)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func testBuildDELETERequestWithPartsThrowsError() throws {
|
||||
var httpRequest = HTTPRequest.delete(baseURL, parameters: ["id": "123"])
|
||||
httpRequest.parts = [MultipartFormEncoder.Part.text("value", name: "test")]
|
||||
|
||||
|
||||
XCTAssertThrowsError(try RequestBuilder.build(request: httpRequest)) { error in
|
||||
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