diff --git a/Sources/Certificate Pinning/Hasher.swift b/Sources/Certificate Pinning/Hasher.swift index 0eddee7..a5bf7aa 100644 --- a/Sources/Certificate Pinning/Hasher.swift +++ b/Sources/Certificate Pinning/Hasher.swift @@ -27,8 +27,4 @@ public class CKSHA256Hasher: Hasher { extension Digest { var bytes: [UInt8] { Array(makeIterator()) } var data: Data { Data(bytes) } - - var hexStr: String { - bytes.map { String(format: "%02X", $0) }.joined() - } } diff --git a/Sources/ParameterEncoding/EncodingError.swift b/Sources/ParameterEncoding/EncodingError.swift new file mode 100644 index 0000000..432b630 --- /dev/null +++ b/Sources/ParameterEncoding/EncodingError.swift @@ -0,0 +1,21 @@ +// +// EncodingError.swift +// QuickHatch +// +// Created by Daniel Koster on 10/25/17. +// Copyright © 2019 DaVinci Labs. All rights reserved. +// + +import Foundation + +public enum ParameterEncodingFailureReason : Sendable { + case missingURL + case jsonEncodingFailed(error: Error) + case propertyListEncodingFailed(error: Error) +} + +public enum EncodingError: Error { + case stringDecodingFailed + case parameterEncodingFailed(reason: ParameterEncodingFailureReason) + case invalidURL(url: any URLConvertible) +} diff --git a/Sources/ParameterEncoding/EncodingHelpers.swift b/Sources/ParameterEncoding/EncodingHelpers.swift new file mode 100644 index 0000000..cf84195 --- /dev/null +++ b/Sources/ParameterEncoding/EncodingHelpers.swift @@ -0,0 +1,41 @@ +// +// EncodingHelpers.swift +// QuickHatch +// +// Created by Daniel Koster on 8/14/19. +// Copyright © 2019 DaVinci Labs. All rights reserved. +// + +import Foundation + +public struct EncodingHelpers { + public static func queryComponents(fromKey key: String, value: Any) -> [(String, String)] { + var components: [(String, String)] = [] + + if let dictionary = value as? [String: Any] { + for (nestedKey, value) in dictionary { + components += queryComponents(fromKey: "\(key)[\(nestedKey)]", value: value) + } + } else if let array = value as? [Any] { + for value in array { + components += queryComponents(fromKey: "\(key)", value: value) + } + } + else if let bool = value as? Bool { + components.append((escape(key), escape((bool ? "1" : "0")))) + } else { + components.append((escape(key), escape("\(value)"))) + } + + return components + } + + public static func escape(_ string: String) -> String { + let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4 + let subDelimitersToEncode = "!$&'()*+,;=" + + var allowedCharacterSet = CharacterSet.urlQueryAllowed + allowedCharacterSet.remove(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)") + return string.addingPercentEncoding(withAllowedCharacters: allowedCharacterSet) ?? string + } +} diff --git a/Sources/ParameterEncoding/JSONEncoding.swift b/Sources/ParameterEncoding/JSONEncoding.swift new file mode 100644 index 0000000..34d209b --- /dev/null +++ b/Sources/ParameterEncoding/JSONEncoding.swift @@ -0,0 +1,42 @@ +// +// JSONEncoding.swift +// QuickHatch +// +// Created by Daniel Koster on 8/16/19. +// Copyright © 2019 DaVinci Labs. All rights reserved. +// + +import Foundation + +public struct JSONEncoding: ParameterEncoding { + + public static var `default`: JSONEncoding { return JSONEncoding() } + + public static var prettyPrinted: JSONEncoding { return JSONEncoding(options: .prettyPrinted) } + + public let options: JSONSerialization.WritingOptions + + public init(options: JSONSerialization.WritingOptions = []) { + self.options = options + } + + public func encode(_ urlRequest: URLRequestProtocol, with parameters: Parameters?) throws -> URLRequest { + var urlRequest = try urlRequest.asURLRequest() + + guard let parameters = parameters else { return urlRequest } + + do { + let data = try JSONSerialization.data(withJSONObject: parameters, options: options) + + if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil { + urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") + } + + urlRequest.httpBody = data + } catch { + throw EncodingError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error)) + } + + return urlRequest + } +} diff --git a/Sources/ParameterEncoding/ParameterEncoding.swift b/Sources/ParameterEncoding/ParameterEncoding.swift new file mode 100644 index 0000000..8282403 --- /dev/null +++ b/Sources/ParameterEncoding/ParameterEncoding.swift @@ -0,0 +1,46 @@ +// +// ParameterEncoding.swift +// QuickHatch +// +// Created by Daniel Koster on 10/25/17. +// Copyright © 2019 DaVinci Labs. All rights reserved. +// + +import Foundation + +public enum HTTPMethod: String { + case options = "OPTIONS" + case get = "GET" + case head = "HEAD" + case post = "POST" + case put = "PUT" + case patch = "PATCH" + case delete = "DELETE" + case trace = "TRACE" + case connect = "CONNECT" +} + +public typealias Parameters = [String: Any] + +public protocol ParameterEncoding { + func encode(_ urlRequest: URLRequestProtocol, with parameters: Parameters?) throws -> URLRequest +} + +public enum ParamDestination { + case methodDependent + case queryString + case httpBody +} + +public extension NSNumber { + var isBool: Bool { return CFBooleanGetTypeID() == CFGetTypeID(self) } +} + +public extension Bool { + var stringValue: String { + return self ? "true" : "false" + } + var intValue: Int { + return self ? 1 : 0 + } +} diff --git a/Sources/ParameterEncoding/ParameterTransformer.swift b/Sources/ParameterEncoding/ParameterTransformer.swift new file mode 100644 index 0000000..df9ece9 --- /dev/null +++ b/Sources/ParameterEncoding/ParameterTransformer.swift @@ -0,0 +1,26 @@ +// +// ParameterTransformer.swift +// QuickHatchHTTP +// +// Created by Daniel Koster on 8/29/25. +// +import Foundation + +public protocol ParameterTransformer { + func transform(parameters: [String: any Sendable]) -> String +} + +public struct DefaultParameterTransformer: ParameterTransformer { + + public init() { + + } + + public func transform(parameters: [String : any Sendable]) -> String { + let parameters = parameters + .flatMap { (key, value) in EncodingHelpers.queryComponents(fromKey: key, value: value) } + .map { "\($0)=\($1)" } + .sorted(by: >) + return parameters.joined(separator: "&") + } +} diff --git a/Sources/ParameterEncoding/StringEncoding.swift b/Sources/ParameterEncoding/StringEncoding.swift new file mode 100644 index 0000000..bf92916 --- /dev/null +++ b/Sources/ParameterEncoding/StringEncoding.swift @@ -0,0 +1,71 @@ +// +// StringEncoding.swift +// QuickHatch +// +// Created by Daniel Koster on 8/14/19. +// Copyright © 2019 DaVinci Labs. All rights reserved. +// + +import Foundation + +public class StringEncoding: ParameterEncoding { + + public init(destination: ParamDestination = .queryString) { + self.paramDest = destination + } + private var paramDest: ParamDestination + + public class var bodyEncoding: StringEncoding { + return StringEncoding(destination: .httpBody) + } + + public class var urlEncoding: StringEncoding { + return StringEncoding() + } + + public func encode(_ urlRequest: URLRequestProtocol, with parameters: Parameters?) throws -> URLRequest { + var request: URLRequest = try urlRequest.asURLRequest() + guard let url = request.url else { + throw EncodingError.parameterEncodingFailed(reason: ParameterEncodingFailureReason.missingURL) + } + if paramDest == .queryString { + urlEncoding(url: url, params: parameters, urlRequest: &request) + return request + } + bodyEncoding(params: parameters, urlRequest: &request) + return request + } + + private func urlEncoding(url: URL, params: Parameters?, urlRequest: inout URLRequest) { + guard let validParams = params, !validParams.isEmpty else { return } + var urlString = url.absoluteString + validParams.forEach({ (key, value) in + let escapedParameter = EncodingHelpers.escape("{\(key)}") + urlString = urlString.replacingOccurrences(of: escapedParameter, with: EncodingHelpers.queryComponents(fromKey: key, value: value)[0].1) + }) + urlRequest.url = URL(string: urlString) + } + + private func bodyEncoding(params: Parameters?, urlRequest: inout URLRequest) { + guard let validParams = params, !validParams.isEmpty else { return } + var paramsEncoded: String = "" + validParams.forEach({ (key, value) in + paramsEncoded += EncodingHelpers.queryComponents(fromKey: key, value: value)[0].1 + "&" + }) + paramsEncoded.removeLast() + urlRequest.httpBody = paramsEncoded.data(using: .utf8, allowLossyConversion: false) + } + +} + +public struct StringURLTransformer: URLTransformer { + public func transform(url: String, parameters: [String : any Sendable]) -> String { + guard !parameters.isEmpty else { return url } + var urlString = url + for (key, value) in parameters { + let escapedKey = EncodingHelpers.escape("{\(key)}") + urlString = urlString.replacingOccurrences(of: escapedKey, with: EncodingHelpers.queryComponents(fromKey: key, value: value)[0].1) + } + return urlString + } +} diff --git a/Sources/ParameterEncoding/URLEncoding.swift b/Sources/ParameterEncoding/URLEncoding.swift new file mode 100644 index 0000000..9f6c722 --- /dev/null +++ b/Sources/ParameterEncoding/URLEncoding.swift @@ -0,0 +1,80 @@ +// +// URLEncoding.swift +// QuickHatch +// +// Created by Daniel Koster on 8/16/19. +// Copyright © 2019 DaVinci Labs. All rights reserved. +// + +import Foundation + +public struct URLEncoding: ParameterEncoding { + + public static var `default`: URLEncoding { return URLEncoding() } + + public static var methodDependent: URLEncoding { return URLEncoding() } + + public static var queryString: URLEncoding { return URLEncoding(destination: .queryString) } + + public static var httpBody: URLEncoding { return URLEncoding(destination: .httpBody) } + + public let destination: ParamDestination + + public init(destination: ParamDestination = .methodDependent) { + self.destination = destination + } + + public func encode(_ urlRequest: URLRequestProtocol, with parameters: Parameters?) throws -> URLRequest { + var urlRequest = try urlRequest.asURLRequest() + + guard let parameters = parameters else { return urlRequest } + + if let method = HTTPMethod(rawValue: urlRequest.httpMethod ?? "GET"), encodeParametersInURL(with: method) { + guard let url = urlRequest.url else { + throw EncodingError.parameterEncodingFailed(reason: .missingURL) + } + + if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), !parameters.isEmpty { + let percentEncodedQuery = (urlComponents.percentEncodedQuery.map { $0 + "&" } ?? "") + query(parameters) + urlComponents.percentEncodedQuery = percentEncodedQuery + urlRequest.url = urlComponents.url + } + } else { + if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil { + urlRequest.setValue("application/x-www-form-urlencoded; charset=utf-8", forHTTPHeaderField: "Content-Type") + } + + urlRequest.httpBody = query(parameters).data(using: .utf8, allowLossyConversion: false) + } + + return urlRequest + } + + private func query(_ parameters: [String: Any]) -> String { + var components: [(String, String)] = [] + + for key in parameters.keys.sorted(by: <) { + let value = parameters[key]! + components += EncodingHelpers.queryComponents(fromKey: key, value: value) + } + return components.map { "\($0)=\($1)" }.joined(separator: "&") + } + + private func encodeParametersInURL(with method: HTTPMethod) -> Bool { + switch destination { + case .queryString: + return true + case .httpBody: + return false + default: + break + } + + switch method { + case .get, .head, .delete: + return true + default: + return false + } + } +} diff --git a/Sources/ParameterEncoding/URLRequestProtocol.swift b/Sources/ParameterEncoding/URLRequestProtocol.swift new file mode 100644 index 0000000..c9fa057 --- /dev/null +++ b/Sources/ParameterEncoding/URLRequestProtocol.swift @@ -0,0 +1,62 @@ +// +// URLConvertible.swift +// QuickHatch +// +// Created by Daniel Koster on 10/25/17. +// Copyright © 2019 DaVinci Labs. All rights reserved. +// + +import Foundation + +public typealias HTTPHeaders = [String: String] + +public protocol URLConvertible: Sendable { + func asURL() throws -> URL +} + +extension String: URLConvertible { + public func asURL() throws -> URL { + guard let url = URL(string: self) else { throw EncodingError.invalidURL(url: self) } + return url + } +} + +extension URL: URLConvertible { + public func asURL() throws -> URL { return self } +} + +extension URLComponents: URLConvertible { + + public func asURL() throws -> URL { + guard let url = url else { throw EncodingError.invalidURL(url: self) } + return url + } +} + +public protocol URLRequestProtocol { + func asURLRequest() throws -> URLRequest +} + +extension URLRequestProtocol { + public var urlRequest: URLRequest? { return try? asURLRequest() } +} + +extension URLRequest: URLRequestProtocol { + public func asURLRequest() throws -> URLRequest { return self } +} + +extension URLRequest { + public init(url: URLConvertible, method: HTTPMethod, headers: HTTPHeaders? = nil) throws { + let url = try url.asURL() + + self.init(url: url) + + httpMethod = method.rawValue + + if let headers = headers { + for (headerField, headerValue) in headers { + setValue(headerValue, forHTTPHeaderField: headerField) + } + } + } +} diff --git a/Sources/ParameterEncoding/URLTransformer.swift b/Sources/ParameterEncoding/URLTransformer.swift new file mode 100644 index 0000000..89b6fe7 --- /dev/null +++ b/Sources/ParameterEncoding/URLTransformer.swift @@ -0,0 +1,49 @@ +// +// URLTransformer.swift +// QuickHatchHTTP +// +// Created by Daniel Koster on 8/29/25. +// +import Foundation + +public protocol URLTransformer { + func transform(url: String, parameters: [String: any Sendable]) -> String +} + +public struct DefaultURLTransformer: URLTransformer { + private let parameterTransformer: ParameterTransformer + + public init(parameterTransformer: ParameterTransformer) { + self.parameterTransformer = parameterTransformer + } + + public func transform(url: String, parameters: [String : any Sendable]) -> String { + if url.isEmpty { return "" } + let params = parameterTransformer.transform(parameters: parameters) + return params.isEmpty ? url : url + "?" + params + } +} + +/// MARK: Use this transformer for parameter mapping +/// Example: Input -> https://quickhatch.com/{user_id}|/{age} +/// +/// Example: ParameterMappingURLTransformer().transform("https://quickhatch.com/{user_id}|/{age}", ["user_id": "ABCD1234", "age": 20]) +/// +/// Example: Output -> https://quickhatch.com/ABCD1234/20 +/// +public struct ParameterMappingURLTransformer: URLTransformer { + + public init() {} + + public func transform(url: String, parameters: [String : any Sendable]) -> String { + guard !url.isEmpty else { return url } + let parameters = parameters.flatMap { (key, value) in EncodingHelpers.queryComponents(fromKey: key, value: value) } + guard !parameters.isEmpty else { return url } + var urlResult = EncodingHelpers.escape(url) + for (key, value) in parameters { + let escapedKey = EncodingHelpers.escape("{\(key)}") + urlResult = urlResult.replacingOccurrences(of: escapedKey, with: value) + } + return urlResult + } +} diff --git a/Tests/TestCases/ParameterEncoding/EncodingHelpersTests.swift b/Tests/TestCases/ParameterEncoding/EncodingHelpersTests.swift new file mode 100644 index 0000000..bb3757e --- /dev/null +++ b/Tests/TestCases/ParameterEncoding/EncodingHelpersTests.swift @@ -0,0 +1,82 @@ +// +// EncodingHelpersTests.swift +// QuickHatchTests +// +// Created by Daniel Koster on 8/14/19. +// Copyright © 2019 DaVinci Labs. All rights reserved. +// + +import Testing +import QuickHatchHTTP + +struct EncodingHelpersTests { + + @Test(arguments: [("boolValue", true), ("boolValue", false)]) + func queryComponents_whenBool(key: String, boolValue: Bool) { + let result = EncodingHelpers.queryComponents(fromKey: key, value: boolValue) + print(result) + #expect(result[0].0 == "boolValue") + #expect(result[0].1 == boolValue.intValue.description) + } + + @Test + func queryComponentsWithInt() { + let int = 2 + let result = EncodingHelpers.queryComponents(fromKey: "intValue", value: int) + #expect(result[0].0 == "intValue") + #expect(result[0].1 == "2") + } + + @Test + func queryComponentsWithString() { + let string = "quickhatch" + let result = EncodingHelpers.queryComponents(fromKey: "stringValue", value: string) + #expect(result[0].0 == "stringValue") + #expect(result[0].1 == "quickhatch") + } + + @Test + func queryComponentsWithArray() { + let string = "quickhatch" + let int = 2 + let array = [string,int] as [Any] + let result = EncodingHelpers.queryComponents(fromKey: "arrayValue", value: array) + #expect(result[0].0 == "arrayValue") + #expect(result[0].1 == "quickhatch") + #expect(result[1].0 == "arrayValue") + #expect(result[1].1 == "2") + } + + @Test + func queryComponentsWithDic() { + let string = "quickhatch" + let int = 2 + let dic = ["string": string,"int": int] as [String: Any] + + let result = EncodingHelpers.queryComponents(fromKey: "dicValue", value: dic) + + let containsString = !result.filter({ + return $0 == EncodingHelpers.escape("dicValue[string]") && $1 == "quickhatch" + }).isEmpty + let containsInt = !result.filter({ + return $0 == EncodingHelpers.escape("dicValue[int]") && $1 == "2" + }).isEmpty + #expect(containsString && containsInt) + } + + @Test + func escapedString() { + let expected = "%5Bint%5D" + #expect(EncodingHelpers.escape("[int]") == expected) + } + + @Test(arguments: [(false, "false"), (true, "true")]) + func boolStringValue(value: Bool, expectedResult: String) { + #expect(value.stringValue == expectedResult) + } + + @Test(arguments: [(false, 0), (true, 1)]) + func testBoolIntValue(value: Bool, expectedResult: Int) { + #expect(value.intValue == expectedResult) + } +} diff --git a/Tests/TestCases/ParameterEncoding/JSONEncodingTests.swift b/Tests/TestCases/ParameterEncoding/JSONEncodingTests.swift new file mode 100644 index 0000000..bcdb73b --- /dev/null +++ b/Tests/TestCases/ParameterEncoding/JSONEncodingTests.swift @@ -0,0 +1,24 @@ +// +// JSONEncodingTests.swift +// QuickHatchHTTP +// +// Created by Daniel Koster on 8/29/25. +// +import Foundation +import QuickHatchHTTP +import Testing + +struct JSONEncodingTests { + + @Test + func jsonEncoding() throws { + let sut = JSONEncoding.default + let urlRequest = URLRequest(url: try #require(URL(string: "quickhatch.com"))) + + let result = try sut.encode(urlRequest, with: ["user_id": "ABCD12345", "age": 20]) + let body = try #require(result.httpBody) + let decoded = try #require(try JSONSerialization.jsonObject(with: body) as? [String: any Sendable]) + + #expect(NSDictionary(dictionary: decoded) == ["user_id": "ABCD12345", "age": 20]) + } +} diff --git a/Tests/TestCases/ParameterEncoding/StringEncodingTests.swift b/Tests/TestCases/ParameterEncoding/StringEncodingTests.swift new file mode 100644 index 0000000..814b926 --- /dev/null +++ b/Tests/TestCases/ParameterEncoding/StringEncodingTests.swift @@ -0,0 +1,122 @@ +// +// StringEncodingTests.swift +// QuickHatchTests +// +// Created by Daniel Koster on 8/14/19. +// Copyright © 2019 DaVinci Labs. All rights reserved. +// + +import XCTest +import QuickHatchHTTP +// swiftlint:disable force_try + +final class StringEncodingTests: XCTestCase { + + func test_encode_whenURLValidAndParams_expectParamsEncoded() throws { + let sut = StringEncoding() + let escapedUrlString = EncodingHelpers.escape("www.quickhatch.com/{name}") + let url = try XCTUnwrap(URL(string: escapedUrlString)) + let urlRequest = try URLRequest(url: url, method: .get) + let requestResult = try sut.encode(urlRequest, + with: ["name": "dani"]) + + let result = try XCTUnwrap(requestResult.url).absoluteString + + XCTAssertEqual("www.quickhatch.com/dani", result) + } + + func test_encode_whenURLValidAndNoParams_expectNoChange() throws { + let sut = StringEncoding() + let escapedUrlString = EncodingHelpers.escape("www.quickhatch.com/{name}") + let url = try XCTUnwrap(URL(string: escapedUrlString)) + let urlRequest = try URLRequest(url: url, method: .get) + let requestResult = try sut.encode(urlRequest, + with: [:]) + + let result = try XCTUnwrap(requestResult.url).absoluteString + + XCTAssertEqual(escapedUrlString, result) + } + + func test_encode_whenValidURLAndManyParams_expect_parametersEncoded() throws { + let sut = StringEncoding() + let escapedUrlString = EncodingHelpers.escape("www.quickhatch.com/{name}/{age}") + let url = try XCTUnwrap(URL(string: escapedUrlString)) + let urlRequest = try URLRequest(url: url, method: .get) + let requestResult = try sut.encode(urlRequest, + with: ["name": "dani","age": 13]) + + let result = try XCTUnwrap(requestResult.url).absoluteString + + XCTAssertEqual("www.quickhatch.com/dani/13", result) + } + + func testStringEncodingManyParametersAndHeaders() throws { + let urlString = EncodingHelpers.escape("www.quickhatch.com/{name}/{age}") + print(urlString) + var urlRequest = try URLRequest(url: URL(string: urlString)!, method: .get) + urlRequest.addValue("header", forHTTPHeaderField: "header") + let stringEncoding = StringEncoding.urlEncoding + let requestResult = try! stringEncoding.encode(urlRequest, + with: ["name": "dani","age": 13]) + XCTAssertTrue(requestResult.url!.absoluteString == "www.quickhatch.com/dani/13") + XCTAssertTrue(requestResult.allHTTPHeaderFields!["header"] == "header") + } + + func testStringEncodingError() { + var urlRequest = URLRequest(url: URL(string: "www.quickhatch.com")!) + urlRequest.url = nil + let stringEncoding = StringEncoding() + do { + _ = try stringEncoding.encode(urlRequest, + with: ["name": "dani","age": 13]) + } catch _ { + XCTAssertTrue(true) + } + + } + + func testStringEncodingManyBodyParametersAndHeaders() throws { + let urlString = EncodingHelpers.escape("www.quickhatch.com/{name}/{age}") + print(urlString) + var urlRequest = try URLRequest(url: URL(string: urlString)!, method: .get) + urlRequest.addValue("header", forHTTPHeaderField: "header") + let stringEncoding = StringEncoding.bodyEncoding + let requestResult = try! stringEncoding.encode(urlRequest, + with: ["name": "dani","age": 13]) + let body = String(data: requestResult.httpBody!, encoding: .utf8) + let split = body!.split(separator: "&") + XCTAssertTrue(split.count == 2) + XCTAssertTrue(split.contains("dani")) + XCTAssertTrue(split.contains("13")) + XCTAssertTrue(requestResult.allHTTPHeaderFields!["header"] == "header") + } + + func testStringEncodingBodyParametersAndHeaders() throws { + let urlString = EncodingHelpers.escape("www.quickhatch.com/{name}/{age}") + print(urlString) + var urlRequest = try URLRequest(url: URL(string: urlString)!, method: .get) + urlRequest.addValue("header", forHTTPHeaderField: "header") + let stringEncoding = StringEncoding.bodyEncoding + let requestResult = try! stringEncoding.encode(urlRequest, + with: ["age": 13]) + let body = String(data: requestResult.httpBody!, encoding: .utf8) + let split = body!.split(separator: "&") + XCTAssertTrue(split.count == 1) + XCTAssertTrue(split.contains("13")) + XCTAssertTrue(requestResult.allHTTPHeaderFields!["header"] == "header") + } + + func testStringEncodingBodyNoParametersAndHeaders() throws { + let urlString = EncodingHelpers.escape("www.quickhatch.com/{name}/{age}") + print(urlString) + var urlRequest = try URLRequest(url: URL(string: urlString)!, method: .get) + urlRequest.addValue("header", forHTTPHeaderField: "header") + let stringEncoding = StringEncoding.bodyEncoding + let requestResult = try! stringEncoding.encode(urlRequest, + with: [:]) + XCTAssertNil(requestResult.httpBody) + XCTAssertTrue(requestResult.allHTTPHeaderFields!["header"] == "header") + } + +} diff --git a/Tests/TestCases/ParameterEncoding/URLEncodingTests.swift b/Tests/TestCases/ParameterEncoding/URLEncodingTests.swift new file mode 100644 index 0000000..c1b87cd --- /dev/null +++ b/Tests/TestCases/ParameterEncoding/URLEncodingTests.swift @@ -0,0 +1,32 @@ +// +// URLEncodingTests.swift +// QuickHatchHTTP +// +// Created by Daniel Koster on 8/29/25. +// +import Testing +import QuickHatchHTTP +import Foundation + +struct URLEncodingTests { + + @Test + func urlEncoding_queryEncoding() throws { + let sut = URLEncoding.queryString + let urlRequest = URLRequest(url: try #require(URL(string: "quickhatch.com"))) + + let result = try sut.encode(urlRequest, with: ["user_id": "ABCD1234"]) + + #expect(result.url?.absoluteString == "quickhatch.com?user_id=ABCD1234") + } + + @Test + func urlEncoding_bodyEncoding() throws { + let sut = URLEncoding.httpBody + let urlRequest = URLRequest(url: try #require(URL(string: "quickhatch.com"))) + + let result = try sut.encode(urlRequest, with: ["user_id": "ABCD1234"]) + let body = String(decoding: try #require(result.httpBody), as: UTF8.self) + #expect(body == "user_id=ABCD1234") + } +} diff --git a/Tests/TestCases/ParameterEncoding/URLTransformerTests.swift b/Tests/TestCases/ParameterEncoding/URLTransformerTests.swift new file mode 100644 index 0000000..09c867d --- /dev/null +++ b/Tests/TestCases/ParameterEncoding/URLTransformerTests.swift @@ -0,0 +1,65 @@ +// +// URLTransformerTests.swift +// QuickHatchHTTP +// +// Created by Daniel Koster on 8/29/25. +// + +import Testing +import QuickHatchHTTP +import Foundation + +struct URLTransformerTests { + + struct URLTransformerTestMetadata : Sendable { + let url: String + let parameters: [String: any Sendable] + let urlResult: String + } + + private class DefaultURLTransformerTestCases { + static let whenEmptyURLExpectSameURL = URLTransformerTestMetadata(url: "", + parameters: [:], + urlResult: "") + static let whenNoParametersExpectSameURL = URLTransformerTestMetadata(url: "https://quickhatch.com", + parameters: [:], + urlResult: "https://quickhatch.com") + static let whenParametersExpectURLTransformed = URLTransformerTestMetadata(url: "https://quickhatch.com", + parameters: ["id": 12, "OtherId": "ABCDE12345"], + urlResult: "https://quickhatch.com?id=12&OtherId=ABCDE12345") + } + + private class ParameterMappingURLTransformerTestCases { + static let whenEmptyURLExpectSameURL = URLTransformerTestMetadata(url: "", + parameters: [:], + urlResult: "") + static let whenNoParametersExpectSameURL = URLTransformerTestMetadata(url: "https://quickhatch.com", + parameters: [:], + urlResult: "https://quickhatch.com") + static let whenParametersExpectURLTransformed = URLTransformerTestMetadata(url: "https://quickhatch.com/{name}/{age}", + parameters: ["name": "quickhatch", "age": 20], + urlResult: EncodingHelpers.escape("https://quickhatch.com/quickhatch/20")) + } + + @Test(arguments: [DefaultURLTransformerTestCases.whenEmptyURLExpectSameURL, + DefaultURLTransformerTestCases.whenNoParametersExpectSameURL, + DefaultURLTransformerTestCases.whenParametersExpectURLTransformed]) + func defaultURLTransformer_transform(metadata: URLTransformerTestMetadata) { + let sut = DefaultURLTransformer(parameterTransformer: DefaultParameterTransformer()) + + let result = sut.transform(url: metadata.url, parameters: metadata.parameters) + + #expect(result == metadata.urlResult) + } + + @Test(arguments: [ParameterMappingURLTransformerTestCases.whenEmptyURLExpectSameURL, + ParameterMappingURLTransformerTestCases.whenParametersExpectURLTransformed, + ParameterMappingURLTransformerTestCases.whenNoParametersExpectSameURL]) + func parameterMappingURLTransformer_transform(metadata: URLTransformerTestMetadata) { + let sut = ParameterMappingURLTransformer() + + let result = sut.transform(url: metadata.url, parameters: metadata.parameters) + + #expect(result == metadata.urlResult) + } +}