diff --git a/Example/TDOAuth.xcodeproj/project.pbxproj b/Example/TDOAuth.xcodeproj/project.pbxproj index ca031b3..330514b 100644 --- a/Example/TDOAuth.xcodeproj/project.pbxproj +++ b/Example/TDOAuth.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ 607FACEC1AFB9204008FA782 /* OAuth1Spec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACEB1AFB9204008FA782 /* OAuth1Spec.swift */; }; 9810E2CF42C71CB54FC9817B /* Pods_TDOAuth_watchOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C9AE6DB313043BFD421E116D /* Pods_TDOAuth_watchOS.framework */; }; ED3586181D3AC6A5F479F62A /* Pods_TDOAuth_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1468F75DCB0D1331A62E245C /* Pods_TDOAuth_iOS.framework */; }; + F2E921622907D5640093DBB4 /* TestTDOQueryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2E921612907D5640093DBB4 /* TestTDOQueryItem.swift */; }; FC71E7F42416E7A200CCC34C /* Compat.m in Sources */ = {isa = PBXBuildFile; fileRef = FC71E7F32416E7A200CCC34C /* Compat.m */; }; FCC067C9241C1940004997C8 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC067C8241C1940004997C8 /* AppDelegate.swift */; }; FCC067CB241C1940004997C8 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC067CA241C1940004997C8 /* ViewController.swift */; }; @@ -134,6 +135,7 @@ E5897B5C42516E19A5C8C497 /* Pods_TDOAuth_macOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_TDOAuth_macOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; E6AC1FB932F461059AB20B60 /* Pods-TDOAuth_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TDOAuth_Example.release.xcconfig"; path = "Target Support Files/Pods-TDOAuth_Example/Pods-TDOAuth_Example.release.xcconfig"; sourceTree = ""; }; ECC894BE67C3786829385505 /* Pods-TDOAuth-TDOAuth-iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TDOAuth-TDOAuth-iOS.release.xcconfig"; path = "Target Support Files/Pods-TDOAuth-TDOAuth-iOS/Pods-TDOAuth-TDOAuth-iOS.release.xcconfig"; sourceTree = ""; }; + F2E921612907D5640093DBB4 /* TestTDOQueryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTDOQueryItem.swift; sourceTree = ""; }; F2EA75221BBDF7E5162DF034 /* Pods-TDOAuth_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TDOAuth_iOS.release.xcconfig"; path = "Target Support Files/Pods-TDOAuth_iOS/Pods-TDOAuth_iOS.release.xcconfig"; sourceTree = ""; }; FC71E7F22416E7A100CCC34C /* TDOAuth_Tests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "TDOAuth_Tests-Bridging-Header.h"; sourceTree = ""; }; FC71E7F32416E7A200CCC34C /* Compat.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Compat.m; sourceTree = ""; }; @@ -310,6 +312,7 @@ FCCE3C4421FAE14200522CBD /* HMACSpec.swift */, FCCE3C4621FAE1E000522CBD /* PlaintextSpec.swift */, FCCE3C4821FAE23F00522CBD /* Utils.swift */, + F2E921612907D5640093DBB4 /* TestTDOQueryItem.swift */, FC71E7F32416E7A200CCC34C /* Compat.m */, FC71E7F22416E7A100CCC34C /* TDOAuth_Tests-Bridging-Header.h */, 607FACE91AFB9204008FA782 /* Supporting Files */, @@ -820,6 +823,7 @@ 607FACEC1AFB9204008FA782 /* OAuth1Spec.swift in Sources */, FCCE3C4921FAE23F00522CBD /* Utils.swift in Sources */, FC71E7F42416E7A200CCC34C /* Compat.m in Sources */, + F2E921622907D5640093DBB4 /* TestTDOQueryItem.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Example/Tests/TestTDOQueryItem.swift b/Example/Tests/TestTDOQueryItem.swift new file mode 100644 index 0000000..8f19729 --- /dev/null +++ b/Example/Tests/TestTDOQueryItem.swift @@ -0,0 +1,92 @@ +// Copyright 2022, Yahoo Inc. +// Licensed under the terms of the MIT license. See LICENSE file in https://github.com/yahoo/TDOAuth for terms. + +import XCTest +@testable import TDOAuth + +class TestTDOQueryItem: XCTestCase { + + static var paramDictionary: [AnyHashable: Any] { + return [ + "key_string": "value_string", + "key_positive_int": Int(808), + "key_negative_int": Int(-394), + "key_double": Double(3.141592653589793), + "key_float": Float(3.1415925), + "key_dictionary": [ + "dic_string": "dictionary", + "dic_number": Int(761) + ], + "key_array": ["array1", "array2", "array3"], + "key_bool": true + ] + } + + func testStringParam() { + guard let queryItems = TDOQueryItem.getItems(from: Self.paramDictionary, isCollectionValuesSupported: true) else { + assertionFailure("TDOQueryItem: parse parameters failed.") + return + } + XCTAssert(queryItems.count == 8) + + if let stringItem = queryItems.first(where: { $0.name == "key_string" }) { + XCTAssert(stringItem.stringValue == "value_string") + XCTAssert(stringItem.rawValue as? String == "value_string") + } else { + assertionFailure("TDOQueryItem: parse string item failed.") + } + + if let positiveIntItem = queryItems.first(where: { $0.name == "key_positive_int" }) { + XCTAssert(positiveIntItem.stringValue == "808") + XCTAssert(positiveIntItem.rawValue as? Int == Int(808)) + } else { + assertionFailure("TDOQueryItem: parse positive int item failed.") + } + + if let negativeIntItem = queryItems.first(where: { $0.name == "key_negative_int" }) { + XCTAssert(negativeIntItem.stringValue == "-394") + XCTAssert(negativeIntItem.rawValue as? Int == Int(-394)) + } else { + assertionFailure("TDOQueryItem: parse negative int item failed.") + } + + if let doubleItem = queryItems.first(where: { $0.name == "key_double" }) { + XCTAssert(doubleItem.stringValue == "3.141592653589793") + XCTAssert(doubleItem.rawValue as? Double == Double(3.141592653589793)) + } else { + assertionFailure("TDOQueryItem: parse double item failed.") + } + + if let floatItem = queryItems.first(where: { $0.name == "key_float" }) { + XCTAssert(floatItem.stringValue == "3.1415925") + XCTAssert(floatItem.rawValue as? Float == Float(3.1415925)) + } else { + assertionFailure("TDOQueryItem: parse float item failed.") + } + + if let dictionaryItem = queryItems.first(where: { $0.name == "key_dictionary" }) { + XCTAssert(dictionaryItem.stringValue == "[\"dic_string\": \"dictionary\", \"dic_number\": 761]" || dictionaryItem.stringValue == "[\"dic_number\": 761, \"dic_string\": \"dictionary\"]") + XCTAssert((dictionaryItem.rawValue as? Dictionary)?["dic_string"] as? String == "dictionary") + XCTAssert((dictionaryItem.rawValue as? Dictionary)?["dic_number"] as? Int == 761) + } else { + assertionFailure("TDOQueryItem: parse dictionary item failed.") + } + + if let arrayItem = queryItems.first(where: { $0.name == "key_array" }) { + XCTAssert(arrayItem.stringValue == "[\"array1\", \"array2\", \"array3\"]") + XCTAssert((arrayItem.rawValue as? Array)?[0] == "array1") + XCTAssert((arrayItem.rawValue as? Array)?[1] == "array2") + XCTAssert((arrayItem.rawValue as? Array)?[2] == "array3") + } else { + assertionFailure("TDOQueryItem: parse array item failed.") + } + + if let boolItem = queryItems.first(where: { $0.name == "key_bool" }) { + XCTAssert(boolItem.stringValue == "true") + XCTAssert(boolItem.rawValue as? Bool == true) + } else { + assertionFailure("TDOQueryItem: parse bool item failed.") + } + } + +} diff --git a/Source/compat/TDOAuth.swift b/Source/compat/TDOAuth.swift index ea4e1bd..7d80587 100644 --- a/Source/compat/TDOAuth.swift +++ b/Source/compat/TDOAuth.swift @@ -52,12 +52,58 @@ let TDOAuthURLRequestTimeout = 30.0 // MARK: - internal class TDOQueryItem : NSObject { - var name : String - var value: String - - init(name: String, value: String) { + var name: String + var rawValue: Any + var stringValue: String? + + init(name: String, rawValue: Any) { self.name = name - self.value = value + self.rawValue = rawValue + self.stringValue = Self.getStringValue(by: rawValue) + } + + private class func getStringValue(by rawValue: Any, isCollectionValuesSupported: Bool = true) -> String? { + if !isCollectionValuesSupported, + (rawValue is Array || rawValue is Dictionary) { + return nil + } + var formattedValue: String? + switch rawValue { + case let losslessString as CustomStringConvertible: + formattedValue = losslessString.description + case let nsObject as NSObjectProtocol: + formattedValue = nsObject.description + case let arrayValue as Array: + formattedValue = String(describing: arrayValue) + case let arrayValue as Array: + formattedValue = String(describing: arrayValue) + case let dictionaryValue as Dictionary: + formattedValue = String(describing: dictionaryValue) + default: + /// `value` is not a valid type - skipping + assertionFailure("TDOAuth: failed to casting the parameter: \(rawValue)") + } + return formattedValue + } + + class func getItems(from dictionary: [AnyHashable: Any]?, isCollectionValuesSupported: Bool = true) -> [TDOQueryItem]? { + guard let dic = dictionary else { return nil } + var queryItems = [TDOQueryItem]() + + for (key, value) in dic { + guard let key = key as? String else { continue } + if Self.getStringValue(by: value, isCollectionValuesSupported: isCollectionValuesSupported) == nil { + if isCollectionValuesSupported { + /// `value` is not a valid type - skipping + assertionFailure("TDOAuth: failed to casting the parameter: \(value) for the key: \(key)") + } + continue + } + let queryItem = TDOQueryItem(name: key, rawValue: value) + queryItems.append(queryItem) + } + + return queryItems } } @@ -99,7 +145,7 @@ internal class TDOQueryItem : NSObject { headerValues:nil, signatureMethod:.hmacSha1) } - + /** Some services insist on HTTPS. Or maybe you don't want the data to be sniffed. You can pass @"https" via the scheme parameter. @@ -172,7 +218,7 @@ internal class TDOQueryItem : NSObject { if let items = urlComponents.queryItems { items.forEach { item in if let value = item.value { - let queryItem = TDOQueryItem(name: item.name, value: value) + let queryItem = TDOQueryItem(name: item.name, rawValue: value) queryItems.append(queryItem) } } @@ -191,11 +237,11 @@ internal class TDOQueryItem : NSObject { headerValues:nil, signatureMethod:.hmacSha1) } - + /** This method allows the caller to specify particular values for many different parameters such as scheme, method, header values and alternate signature hash algorithms. - + @p scheme may be any string value, generally "http" or "https". @p requestMethod may be any string value. There is no validation, so remember that all currently-defined HTTP methods are uppercase and the RFC specifies that the method @@ -231,31 +277,9 @@ internal class TDOQueryItem : NSObject { dataEncoding: TDOAuthContentType, headerValues: [AnyHashable : Any]?, signatureMethod: TDOAuthSignatureMethod) -> URLRequest! { - - var queryItems = [TDOQueryItem]() - - if let unencodedParameters = unencodedParameters { - for (key, value) in unencodedParameters { - guard let key = key as? String else { continue } - let formattedValue: String - switch value { - case let stringValue as String: - formattedValue = stringValue - case let intValue as Int: - formattedValue = String(intValue) - case let boolValue as Bool: - formattedValue = String(boolValue) - default: - /// `value` is not a valid type - skipping - continue - } - let queryItem = TDOQueryItem(name: key, value: formattedValue) - queryItems.append(queryItem) - } - } return self.urlRequest(forPath: unencodedPathWithoutQuery, - queryItems: queryItems, + queryItems: TDOQueryItem.getItems(from: unencodedParameters, isCollectionValuesSupported: method == "POST") ?? [], host: host, consumerKey: consumerKey, consumerSecret: consumerSecret, @@ -313,7 +337,7 @@ internal class TDOQueryItem : NSObject { guard let host = host, let unencodedPathWithoutQuery = unencodedPathWithoutQuery, let scheme = scheme, let method = method else { return nil } - + // We don't use pcen as we don't want to percent encode eg. /, this is perhaps // not the most all encompassing solution, but in practice it seems to work // everywhere and means that programmer error is *much* less likely. @@ -359,9 +383,9 @@ internal class TDOQueryItem : NSObject { else if (dataEncoding == .jsonObject) { // This falls back to dictionary as not sure what's the proper action here. - var unencodedParameters = [String:String]() + var unencodedParameters = [String: Any]() for queryItem in queryItems { - unencodedParameters[queryItem.name] = queryItem.value; + unencodedParameters[queryItem.name] = queryItem.rawValue } do { let postbody = try JSONSerialization.data(withJSONObject: unencodedParameters) @@ -379,11 +403,11 @@ internal class TDOQueryItem : NSObject { } else // invalid type { - return nil; + return nil } } - return rq; + return rq } // METHOD ADAPTED FROM LEGACY OAUTH1 CLIENT @@ -393,19 +417,19 @@ internal class TDOQueryItem : NSObject { var queryString = String("") var encodedParameters = [TDOQueryItem]() for queryItem in unencodedParameters { - let enkey = TDPCEN(queryItem.name) - let envalue = TDPCEN(queryItem.value) - if let enkey = enkey, let envalue = envalue { + if let enkey = TDPCEN(queryItem.name), + let stringValue = queryItem.stringValue, + let envalue = TDPCEN(stringValue) { if queryString.count > 0 { queryString.append("&") } - encodedParameters.append(TDOQueryItem(name: enkey, value: envalue)) + encodedParameters.append(TDOQueryItem(name: enkey, rawValue: envalue)) queryString.append(enkey) queryString.append("=") queryString.append(envalue) } } - return queryString; + return queryString } // METHOD ADAPTED FROM LEGACY OAUTH1 CLIENT @@ -433,7 +457,7 @@ internal class TDOQueryItem : NSObject { } /** - + OAuth requires the UTC timestamp we send to be accurate. The user's device may not be, and often isn't. To work around this you should set this to the UTC timestamp that you get back in HTTP headers from OAuth servers. diff --git a/TDOAuth.podspec b/TDOAuth.podspec index e22f44d..a8b0126 100644 --- a/TDOAuth.podspec +++ b/TDOAuth.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'TDOAuth' - s.version = '1.6.1' + s.version = '1.6.2' s.summary = 'Elegant, simple and compliant OAuth 1.x solution.' s.description = <<-DESC