From bc3ce2c93e48c66d3c7ea01ee177028ed5fbf984 Mon Sep 17 00:00:00 2001 From: Sami Samhuri Date: Sun, 15 Jun 2025 16:59:44 -0700 Subject: [PATCH] Fix stray question marks --- Sources/Osiris/FormEncoder.swift | 8 +- Sources/Osiris/HTTPContentType.swift | 10 +- Sources/Osiris/HTTPMethod.swift | 2 +- Sources/Osiris/HTTPRequest.swift | 12 +- Sources/Osiris/HTTPRequestError.swift | 12 +- Sources/Osiris/HTTPResponse.swift | 8 +- Sources/Osiris/MultipartFormEncoder.swift | 46 +++--- Sources/Osiris/RequestBuilder.swift | 30 ++-- Tests/OsirisTests/FormEncoderTests.swift | 50 +++--- Tests/OsirisTests/HTTPRequestErrorTests.swift | 16 +- Tests/OsirisTests/HTTPRequestTests.swift | 64 ++++---- Tests/OsirisTests/HTTPResponseTests.swift | 98 ++++++------ Tests/OsirisTests/RequestBuilderTests.swift | 147 ++++++++++-------- 13 files changed, 258 insertions(+), 245 deletions(-) diff --git a/Sources/Osiris/FormEncoder.swift b/Sources/Osiris/FormEncoder.swift index ef68741..ae203bc 100644 --- a/Sources/Osiris/FormEncoder.swift +++ b/Sources/Osiris/FormEncoder.swift @@ -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" } diff --git a/Sources/Osiris/HTTPContentType.swift b/Sources/Osiris/HTTPContentType.swift index 96b830f..ffc26a5 100644 --- a/Sources/Osiris/HTTPContentType.swift +++ b/Sources/Osiris/HTTPContentType.swift @@ -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: diff --git a/Sources/Osiris/HTTPMethod.swift b/Sources/Osiris/HTTPMethod.swift index 4ace74e..9727ba4 100644 --- a/Sources/Osiris/HTTPMethod.swift +++ b/Sources/Osiris/HTTPMethod.swift @@ -18,7 +18,7 @@ public enum HTTPMethod: String, Sendable, CustomStringConvertible { var string: String { rawValue.uppercased() } - + public var description: String { string } diff --git a/Sources/Osiris/HTTPRequest.swift b/Sources/Osiris/HTTPRequest.swift index 03d5e23..0591653 100644 --- a/Sources/Osiris/HTTPRequest.swift +++ b/Sources/Osiris/HTTPRequest.swift @@ -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 { "" } diff --git a/Sources/Osiris/HTTPRequestError.swift b/Sources/Osiris/HTTPRequestError.swift index 3cf61bc..1d2ee63 100644 --- a/Sources/Osiris/HTTPRequestError.swift +++ b/Sources/Osiris/HTTPRequestError.swift @@ -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: diff --git a/Sources/Osiris/HTTPResponse.swift b/Sources/Osiris/HTTPResponse.swift index dbe5f0c..014db2b 100644 --- a/Sources/Osiris/HTTPResponse.swift +++ b/Sources/Osiris/HTTPResponse.swift @@ -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): diff --git a/Sources/Osiris/MultipartFormEncoder.swift b/Sources/Osiris/MultipartFormEncoder.swift index 3f70c05..6e98489 100644 --- a/Sources/Osiris/MultipartFormEncoder.swift +++ b/Sources/Osiris/MultipartFormEncoder.swift @@ -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 { "" } @@ -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 { "" } @@ -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 { "" } @@ -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 { "" } diff --git a/Sources/Osiris/RequestBuilder.swift b/Sources/Osiris/RequestBuilder.swift index 5f4f809..7f9d925 100644 --- a/Sources/Osiris/RequestBuilder.swift +++ b/Sources/Osiris/RequestBuilder.swift @@ -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 } } diff --git a/Tests/OsirisTests/FormEncoderTests.swift b/Tests/OsirisTests/FormEncoderTests.swift index efff177..d05fe6e 100644 --- a/Tests/OsirisTests/FormEncoderTests.swift +++ b/Tests/OsirisTests/FormEncoderTests.swift @@ -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 "" 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) } diff --git a/Tests/OsirisTests/HTTPRequestErrorTests.swift b/Tests/OsirisTests/HTTPRequestErrorTests.swift index dd98e07..2986db1 100644 --- a/Tests/OsirisTests/HTTPRequestErrorTests.swift +++ b/Tests/OsirisTests/HTTPRequestErrorTests.swift @@ -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) diff --git a/Tests/OsirisTests/HTTPRequestTests.swift b/Tests/OsirisTests/HTTPRequestTests.swift index 7dd2948..9c33eab 100644 --- a/Tests/OsirisTests/HTTPRequestTests.swift +++ b/Tests/OsirisTests/HTTPRequestTests.swift @@ -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 } diff --git a/Tests/OsirisTests/HTTPResponseTests.swift b/Tests/OsirisTests/HTTPResponseTests.swift index 2371ac5..e4155c3 100644 --- a/Tests/OsirisTests/HTTPResponseTests.swift +++ b/Tests/OsirisTests/HTTPResponseTests.swift @@ -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) diff --git a/Tests/OsirisTests/RequestBuilderTests.swift b/Tests/OsirisTests/RequestBuilderTests.swift index aa3175f..1ea1190 100644 --- a/Tests/OsirisTests/RequestBuilderTests.swift +++ b/Tests/OsirisTests/RequestBuilderTests.swift @@ -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") + } + }