diff --git a/StripeApplePay/StripeApplePayTests/STPPaymentMethodFunctionalTest.swift b/StripeApplePay/StripeApplePayTests/STPPaymentMethodFunctionalTest.swift index 43d411ce623..ac51c56a293 100644 --- a/StripeApplePay/StripeApplePayTests/STPPaymentMethodFunctionalTest.swift +++ b/StripeApplePay/StripeApplePayTests/STPPaymentMethodFunctionalTest.swift @@ -79,6 +79,7 @@ class STPPaymentMethodModernTest: XCTestCase { StripeAPI.PaymentMethod.create(apiClient: apiClient, params: params) { result in do { _ = try result.get() + XCTFail("This request should fail") } catch { let stripeError = error as? StripeError diff --git a/StripeApplePay/StripeApplePayTests/TelemetryInjectionTest.swift b/StripeApplePay/StripeApplePayTests/TelemetryInjectionTest.swift index 550e797fb01..7ded88bd351 100644 --- a/StripeApplePay/StripeApplePayTests/TelemetryInjectionTest.swift +++ b/StripeApplePay/StripeApplePayTests/TelemetryInjectionTest.swift @@ -49,12 +49,13 @@ class TelemetryInjectionTest: APIStubbedTestCase { return HTTPStubsResponse() } - let params = StripeAPI.PaymentMethodParams(type: .card) + var params = StripeAPI.PaymentMethodParams(type: .card) var card = StripeAPI.PaymentMethodParams.Card() card.number = "4242424242424242" card.expYear = 28 card.expMonth = 12 card.cvc = "100" + params.card = card // Set up telemetry data StripeAPI.advancedFraudSignalsEnabled = true diff --git a/StripeCore/StripeCore.xcodeproj/project.pbxproj b/StripeCore/StripeCore.xcodeproj/project.pbxproj index c1f8773a39c..cda2405ba4c 100644 --- a/StripeCore/StripeCore.xcodeproj/project.pbxproj +++ b/StripeCore/StripeCore.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 3126570327B4852600D2F8A8 /* URLRequest+StripeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3126570227B4852600D2F8A8 /* URLRequest+StripeTest.swift */; }; 31337A4926E04B6A005C7E02 /* URLSession+Retry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31337A4826E04B6A005C7E02 /* URLSession+Retry.swift */; }; 315BDBDF2788E2B4007BD11F /* STPDispatchFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 315BDBDE2788E2B4007BD11F /* STPDispatchFunctions.swift */; }; + 3160673F2804DA6C0082B148 /* StripeJSONShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3160673E2804DA6C0082B148 /* StripeJSONShared.swift */; }; 317C4FE0275FF44D003771D7 /* InstallMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317C4FDF275FF44D003771D7 /* InstallMethod.swift */; }; 319E36582719EBF700460867 /* ServerErrorMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319E36572719EBF700460867 /* ServerErrorMapper.swift */; }; 31A5269226C46D9600F8AB59 /* STPAppInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A5269126C46D9500F8AB59 /* STPAppInfo.swift */; }; @@ -118,6 +119,7 @@ 3126570227B4852600D2F8A8 /* URLRequest+StripeTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URLRequest+StripeTest.swift"; sourceTree = ""; }; 31337A4826E04B6A005C7E02 /* URLSession+Retry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSession+Retry.swift"; sourceTree = ""; }; 315BDBDE2788E2B4007BD11F /* STPDispatchFunctions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = STPDispatchFunctions.swift; sourceTree = ""; }; + 3160673E2804DA6C0082B148 /* StripeJSONShared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StripeJSONShared.swift; sourceTree = ""; }; 317C4FDF275FF44D003771D7 /* InstallMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallMethod.swift; sourceTree = ""; }; 319E36572719EBF700460867 /* ServerErrorMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerErrorMapper.swift; sourceTree = ""; }; 31A5268D26C46CFE00F8AB59 /* StripeCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StripeCodable.swift; sourceTree = ""; }; @@ -284,6 +286,7 @@ isa = PBXGroup; children = ( 31AEABAE27BED97A000FB845 /* StripeJSONDecoder.swift */, + 3160673E2804DA6C0082B148 /* StripeJSONShared.swift */, 31AEABB327BEDB49000FB845 /* StripeJSONEncoder.swift */, ); path = Coder; @@ -798,6 +801,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3160673F2804DA6C0082B148 /* StripeJSONShared.swift in Sources */, F338B9832748809200E9323D /* EmptyResponse.swift in Sources */, 31E6D9642744451B00A89B6D /* NSCharacterSet+StripeCore.swift in Sources */, E6752D7826F413A00062B821 /* String+StripeCore.swift in Sources */, diff --git a/StripeCore/StripeCore/Source/Coder/StripeJSONDecoder.swift b/StripeCore/StripeCore/Source/Coder/StripeJSONDecoder.swift index 6537972bf44..c13309911ee 100644 --- a/StripeCore/StripeCore/Source/Coder/StripeJSONDecoder.swift +++ b/StripeCore/StripeCore/Source/Coder/StripeJSONDecoder.swift @@ -2,8 +2,502 @@ // StripeJSONDecoder.swift // StripeCore // +// This is a bridge between NSJSONSerialization and Decoder, including some Stripe-specific behavior. // import Foundation -// TODO: Build StripeJSONDecoder +internal class StripeJSONDecoder { + var userInfo: [CodingUserInfoKey : Any] = [:] + + var inputFormatting: JSONSerialization.ReadingOptions = [] + + func decode(_ type: T.Type, from data: Data) throws -> T where T : Decodable { + var inputFormatting = self.inputFormatting + // We always allow fragments. (Though we mostly only use these for tests.) + inputFormatting.insert(.fragmentsAllowed) + let jsonObject: Any + do { + jsonObject = try JSONSerialization.jsonObject(with: data, options: inputFormatting) + } catch let error { + throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: error)) + } + guard let object = jsonObject as? NSObject else { + throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "The given data could not be decoded from JSON.", underlyingError: nil)) + } + let decoder = _stpinternal_JSONDecoder(jsonObject: object) + return try decoder.castFromNSObject() + } +} + +fileprivate class _stpinternal_JSONDecoder: Decoder, STPDecodingContainerProtocol { + var userInfo: [CodingUserInfoKey : Any] = [:] + var codingPath: [CodingKey] = [] + var jsonObject: NSObject + + init(jsonObject: NSObject) { + self.jsonObject = jsonObject + } + + func castFromNSObject() throws -> T where T : Decodable{ + return try castFromNSObject(codingPath: codingPath, T.self, jsonObject) + } + + func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer where Key : CodingKey { + guard let dict = jsonObject as? NSDictionary else { + throw DecodingError.typeMismatch(NSDictionary.self, .init(codingPath: codingPath, debugDescription: "KeyedContainer is not a dictionary", underlyingError: nil)) + } + return KeyedDecodingContainer(STPKeyedDecodingContainer(codingPath: codingPath, dict: dict, allKeys: [], userInfo: userInfo)) + } + + func unkeyedContainer() throws -> UnkeyedDecodingContainer { + guard let array = jsonObject as? NSArray else { + throw DecodingError.typeMismatch(NSArray.self, .init(codingPath: codingPath, debugDescription: "UnkeyedContainer is not an array", underlyingError: nil)) + } + return STPUnkeyedDecodingContainer(userInfo: userInfo, array: array, codingPath: codingPath) + } + + func singleValueContainer() throws -> SingleValueDecodingContainer { + return STPSingleValueDecodingContainer(codingPath: codingPath, userInfo: userInfo, object: jsonObject) + } +} + +fileprivate protocol STPDecodingContainerProtocol { + var userInfo: [CodingUserInfoKey: Any] { + get set + } +} + +fileprivate struct STPKeyedDecodingContainer: STPDecodingContainerProtocol, KeyedDecodingContainerProtocol where K: CodingKey { + var codingPath: [CodingKey] + + var dict: NSDictionary + var allKeys: [K] + + var userInfo: [CodingUserInfoKey : Any] + + typealias Key = K + + func _dictionaryKey(from key: K) -> String { + let maintainExistingCase = userInfo[STPMaintainExistingCase] as? Bool ?? false + var key = key.stringValue + + if !maintainExistingCase { + key = URLEncoder.convertToSnakeCase(camelCase: key) + } + + return key + } + + func contains(_ key: K) -> Bool { + let key = _dictionaryKey(from: key) + return dict[key] != nil + } + + func _objectForKey(_ key: K) throws -> NSObject { + if !contains(key) { + throw DecodingError.keyNotFound(key, .init(codingPath: codingPath, debugDescription: "Key \(key) not found in \(codingPath)", underlyingError: nil)) + } + + let key = _dictionaryKey(from: key) + return dict[key] as! NSObject + } + + func _decode(_ type: T.Type, forKey key: K) throws -> T where T: Decodable { + return try castFromNSObject(codingPath: codingPath + [key], type, _objectForKey(key)) + } + + func decodeNil(forKey key: K) throws -> Bool { + return (try _objectForKey(key) is NSNull) + } + + func decode(_ type: Bool.Type, forKey key: K) throws -> Bool { + return try _decode(type, forKey: key) + } + + func decode(_ type: String.Type, forKey key: K) throws -> String { + return try _decode(type, forKey: key) + } + + func decode(_ type: Double.Type, forKey key: K) throws -> Double { + return try _decode(type, forKey: key) + } + + func decode(_ type: Float.Type, forKey key: K) throws -> Float { + return try _decode(type, forKey: key) + } + + func decode(_ type: Int.Type, forKey key: K) throws -> Int { + return try _decode(type, forKey: key) + } + + func decode(_ type: Int8.Type, forKey key: K) throws -> Int8 { + return try _decode(type, forKey: key) + } + + func decode(_ type: Int16.Type, forKey key: K) throws -> Int16 { + return try _decode(type, forKey: key) + } + + func decode(_ type: Int32.Type, forKey key: K) throws -> Int32 { + return try _decode(type, forKey: key) + } + + func decode(_ type: Int64.Type, forKey key: K) throws -> Int64 { + return try _decode(type, forKey: key) + } + + func decode(_ type: UInt.Type, forKey key: K) throws -> UInt { + return try _decode(type, forKey: key) + } + + func decode(_ type: UInt8.Type, forKey key: K) throws -> UInt8 { + return try _decode(type, forKey: key) + } + + func decode(_ type: UInt16.Type, forKey key: K) throws -> UInt16 { + return try _decode(type, forKey: key) + } + + func decode(_ type: UInt32.Type, forKey key: K) throws -> UInt32 { + return try _decode(type, forKey: key) + } + + func decode(_ type: UInt64.Type, forKey key: K) throws -> UInt64 { + return try _decode(type, forKey: key) + } + + func decode(_ type: T.Type, forKey key: K) throws -> T where T : Decodable { + return try _decode(type, forKey: key) + } + + func nestedContainer(keyedBy type: NestedKey.Type, forKey key: K) throws -> KeyedDecodingContainer where NestedKey : CodingKey { + assertionFailure("nestedContainer(keyedBy:forKey:) is not implemented.") + return KeyedDecodingContainer(STPKeyedDecodingContainer(codingPath: [], dict: NSMutableDictionary(), allKeys: [], userInfo: [:])) + } + + func nestedUnkeyedContainer(forKey key: K) throws -> UnkeyedDecodingContainer { + assertionFailure("nestedUnkeyedContainer(forKey:) is not implemented.") + return STPUnkeyedDecodingContainer(userInfo: [:], array: NSArray(), codingPath: [], currentIndex: 0) + } + + func superDecoder() throws -> Decoder { + assertionFailure("superDecoder() is not implemented.") + return _stpinternal_JSONDecoder(jsonObject: NSNull()) + } + + func superDecoder(forKey key: K) throws -> Decoder { + assertionFailure("superDecoder(forKey:) is not implemented.") + return _stpinternal_JSONDecoder(jsonObject: NSNull()) + } + + +} + +fileprivate struct STPUnkeyedDecodingContainer: UnkeyedDecodingContainer, STPDecodingContainerProtocol { + var userInfo: [CodingUserInfoKey : Any] + + var array: NSArray + + var codingPath: [CodingKey] + + var count: Int? { + return array.count + } + + var isAtEnd: Bool { + return currentIndex >= count ?? 0 + } + + var currentIndex: Int = 0 + + mutating func _popObject() -> NSObject { + assert(!isAtEnd, "Tried to read past the end of the container.") + let object = array[currentIndex] as! NSObject + currentIndex += 1 + return object + } + + mutating func _decode(_ type: T.Type) throws -> T where T: Decodable { + let codingPath = codingPath + [STPCodingKey(intValue: currentIndex)!] + return try castFromNSObject(codingPath: codingPath, type, _popObject()) + } + + mutating func decodeNil() throws -> Bool { + let object = _popObject() + return (object is NSNull) + } + + mutating func decode(_ type: Bool.Type) throws -> Bool { + return try _decode(type) + } + + mutating func decode(_ type: String.Type) throws -> String { + return try _decode(type) + } + + mutating func decode(_ type: Double.Type) throws -> Double { + return try _decode(type) + } + + mutating func decode(_ type: Float.Type) throws -> Float { + return try _decode(type) + } + + mutating func decode(_ type: Int.Type) throws -> Int { + return try _decode(type) + } + + mutating func decode(_ type: Int8.Type) throws -> Int8 { + return try _decode(type) + } + + mutating func decode(_ type: Int16.Type) throws -> Int16 { + return try _decode(type) + } + + mutating func decode(_ type: Int32.Type) throws -> Int32 { + return try _decode(type) + } + + mutating func decode(_ type: Int64.Type) throws -> Int64 { + return try _decode(type) + } + + mutating func decode(_ type: UInt.Type) throws -> UInt { + return try _decode(type) + } + + mutating func decode(_ type: UInt8.Type) throws -> UInt8 { + return try _decode(type) + } + + mutating func decode(_ type: UInt16.Type) throws -> UInt16 { + return try _decode(type) + } + + mutating func decode(_ type: UInt32.Type) throws -> UInt32 { + return try _decode(type) + } + + mutating func decode(_ type: UInt64.Type) throws -> UInt64 { + return try _decode(type) + } + + mutating func decode(_ type: T.Type) throws -> T where T : Decodable { + return try _decode(type) + } + + + mutating func nestedContainer(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer where NestedKey : CodingKey { + assertionFailure("nestedContainer(keyedBy:) is not implemented.") + return KeyedDecodingContainer(STPKeyedDecodingContainer(codingPath: [], dict: NSMutableDictionary(), allKeys: [], userInfo: [:])) + } + + mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { + assertionFailure("nestedUnkeyedContainer(forKey:) is not implemented.") + return STPUnkeyedDecodingContainer(userInfo: [:], array: NSArray(), codingPath: []) + } + + mutating func superDecoder() throws -> Decoder { + assertionFailure("superDecoder() is not implemented.") + return _stpinternal_JSONDecoder(jsonObject: NSNull()) + } + +} + +fileprivate struct STPSingleValueDecodingContainer: SingleValueDecodingContainer, STPDecodingContainerProtocol { + var codingPath: [CodingKey] + + var userInfo: [CodingUserInfoKey : Any] + + var object: NSObject + + func _decode(_ type: T.Type) throws -> T where T: Decodable { + return try castFromNSObject(codingPath: codingPath, type, object) + } + + func decodeNil() -> Bool { + return object == NSNull() + } + + func decode(_ type: Bool.Type) throws -> Bool { + return try _decode(type) + } + + func decode(_ type: String.Type) throws -> String { + return try _decode(type) + } + + func decode(_ type: Double.Type) throws -> Double { + return try _decode(type) + } + + func decode(_ type: Float.Type) throws -> Float { + return try _decode(type) + } + + func decode(_ type: Int.Type) throws -> Int { + return try _decode(type) + } + + func decode(_ type: Int8.Type) throws -> Int8 { + return try _decode(type) + } + + func decode(_ type: Int16.Type) throws -> Int16 { + return try _decode(type) + } + + func decode(_ type: Int32.Type) throws -> Int32 { + return try _decode(type) + } + + func decode(_ type: Int64.Type) throws -> Int64 { + return try _decode(type) + } + + func decode(_ type: UInt.Type) throws -> UInt { + return try _decode(type) + } + + func decode(_ type: UInt8.Type) throws -> UInt8 { + return try _decode(type) + } + + func decode(_ type: UInt16.Type) throws -> UInt16 { + return try _decode(type) + } + + func decode(_ type: UInt32.Type) throws -> UInt32 { + return try _decode(type) + } + + func decode(_ type: UInt64.Type) throws -> UInt64 { + return try _decode(type) + } + + func decode(_ type: T.Type) throws -> T where T : Decodable { + return try _decode(type) + } + +} + +// MARK: Casting logic + +// These extensions help us maintain the type information for Arrays and Dictionaries within castFromNSObject, so that +// inner calls to castFromNSObject call the templated function without erasing the underlying type. +// I'm not sure if there's a cleaner way to do this... +fileprivate protocol _STPDecodableIsArray { + static var valueType: Decodable.Type { get } +} +extension Array: _STPDecodableIsArray where Element: Decodable { + static var valueType: Decodable.Type { return Element.self } +} +fileprivate protocol _STPDecodableIsDictionary { + static var valueType: Decodable.Type { get } +} +extension Dictionary: _STPDecodableIsDictionary where Key == String, Value: Decodable { + static var valueType: Decodable.Type { return Value.self } +} +fileprivate extension Decodable { + static func _castFromNSObject(codingPath: [CodingKey] = [], decodingContainer: STPDecodingContainerProtocol, object: NSObject) throws -> Self { + return try decodingContainer.castFromNSObject(codingPath: codingPath, Self.self, object) + } +} + +extension STPDecodingContainerProtocol { + func castFromNSObject(codingPath: [CodingKey] = [], _ type: T.Type, _ object: NSObject) throws -> T where T: Decodable { + switch type { + case is Double.Type: + switch object as? String { + case "Inf": + return Double.infinity as! T + case "-Inf": + return -Double.infinity as! T + case "nan": + return Double.nan as! T + case .none, .some(_): + guard let value = object as? Double, let returnValue = value as? T else { + throw DecodingError.dataCorrupted(.init(codingPath: codingPath, debugDescription: "Parsed JSON number <\(object)> does not fit in \(type).", underlyingError: nil)) + } + return returnValue + } + case is Float.Type: + switch object as? String { + case "Inf": + return Float.infinity as! T + case "-Inf": + return -Float.infinity as! T + case "nan": + return Float.nan as! T + case .none, .some(_): + guard let value = object as? Float, let returnValue = value as? T else { + throw DecodingError.dataCorrupted(.init(codingPath: codingPath, debugDescription: "Parsed JSON number <\(object)> does not fit in \(type).", underlyingError: nil)) + } + return returnValue + } + case is Bool.Type, + is Int.Type, + is Int8.Type, + is Int16.Type, + is Int32.Type, + is Int64.Type, + is UInt.Type, + is UInt8.Type, + is UInt16.Type, + is UInt32.Type, + is UInt64.Type, + is String.Type: + guard let value = object as? T else { + throw DecodingError.dataCorrupted(.init(codingPath: codingPath, debugDescription: "Parsed JSON number <\(object)> does not fit in \(type).", underlyingError: nil)) + } + return value + case is Decimal.Type: + guard let decimal = (object as? NSNumber)?.decimalValue else { + throw DecodingError.dataCorrupted(.init(codingPath: codingPath, debugDescription: "Could not convert <\(object)> to \(type).", underlyingError: nil)) + } + return decimal as! T + case is URL.Type: + guard let string = object as? String, let url = URL(string: string) else { + throw DecodingError.dataCorrupted(.init(codingPath: codingPath, debugDescription: "Could not convert <\(object)> to \(type).", underlyingError: nil)) + } + return url as! T + case is Data.Type: + guard let string = object as? String, let data = Data(base64Encoded: string) else { + throw DecodingError.dataCorrupted(.init(codingPath: codingPath, debugDescription: "Could not convert <\(object)> to \(type).", underlyingError: nil)) + } + return data as! T + case is Date.Type: + guard let ti = object as? TimeInterval else { + throw DecodingError.dataCorrupted(.init(codingPath: codingPath, debugDescription: "Could not convert <\(object)> to \(type).", underlyingError: nil)) + } + return Date(timeIntervalSince1970: ti) as! T + case is _STPDecodableIsDictionary.Type: + guard let dict = object as? [String: Any] else { + throw DecodingError.dataCorrupted(.init(codingPath: codingPath, debugDescription: "Could not convert <\(object)> to \(type).", underlyingError: nil)) + } + var convertedDict: [String: Any] = [:] + for (k, v) in dict { + let dictType = T.self as! (_STPDecodableIsDictionary.Type) + convertedDict[k] = try dictType.valueType._castFromNSObject(codingPath: codingPath, decodingContainer: self, object: v as! NSObject) + } + return convertedDict as! T + case is _STPDecodableIsArray.Type: + guard let array = object as? [Any] else { + throw DecodingError.dataCorrupted(.init(codingPath: codingPath, debugDescription: "Could not convert <\(object)> to \(type).", underlyingError: nil)) + } + var convertedArray: [Any] = [] + for i in array { + let arrayType = T.self as! (_STPDecodableIsArray.Type) + convertedArray.append(try arrayType.valueType._castFromNSObject(codingPath: codingPath, decodingContainer: self, object: i as! NSObject)) + } + return convertedArray as! T + default: + let decoder = _stpinternal_JSONDecoder(jsonObject: object) + decoder.userInfo = userInfo + decoder.codingPath = codingPath + return try T(from: decoder) + } + } +} diff --git a/StripeCore/StripeCore/Source/Coder/StripeJSONEncoder.swift b/StripeCore/StripeCore/Source/Coder/StripeJSONEncoder.swift index b096cbb6a05..d45fc022385 100644 --- a/StripeCore/StripeCore/Source/Coder/StripeJSONEncoder.swift +++ b/StripeCore/StripeCore/Source/Coder/StripeJSONEncoder.swift @@ -465,25 +465,3 @@ extension StripeEncodingContainer { } } } - -// Constants -fileprivate let STPMaintainExistingCase = CodingUserInfoKey(rawValue: "_STPMaintainExistingCase")! - -fileprivate struct STPCodingKey: CodingKey { - init?(stringValue: String) { - self.stringValue = stringValue - } - - init?(intValue: Int) { - self.intValue = intValue - self.stringValue = intValue.description - } - - init(stringValue: String, intValue: Int?) { - self.intValue = intValue - self.stringValue = stringValue - } - - var stringValue: String - var intValue: Int? -} diff --git a/StripeCore/StripeCore/Source/Coder/StripeJSONShared.swift b/StripeCore/StripeCore/Source/Coder/StripeJSONShared.swift new file mode 100644 index 00000000000..4cd378d0197 --- /dev/null +++ b/StripeCore/StripeCore/Source/Coder/StripeJSONShared.swift @@ -0,0 +1,28 @@ +// +// StripeJSONShared.swift +// StripeCore +// + +import Foundation + +// Constants +internal let STPMaintainExistingCase = CodingUserInfoKey(rawValue: "_STPMaintainExistingCase")! + +internal struct STPCodingKey: CodingKey { + init?(stringValue: String) { + self.stringValue = stringValue + } + + init?(intValue: Int) { + self.intValue = intValue + self.stringValue = intValue.description + } + + init(stringValue: String, intValue: Int?) { + self.intValue = intValue + self.stringValue = stringValue + } + + var stringValue: String + var intValue: Int? +} diff --git a/StripeCore/StripeCoreTests/External/TestJSONEncoder.swift b/StripeCore/StripeCoreTests/External/TestJSONEncoder.swift index be090ee33c4..96ccca0424a 100644 --- a/StripeCore/StripeCoreTests/External/TestJSONEncoder.swift +++ b/StripeCore/StripeCoreTests/External/TestJSONEncoder.swift @@ -28,11 +28,9 @@ struct TopLevelObjectWrapper: Codable, Equatable { class TestJSONEncoder : XCTestCase { // MARK: - Encoding Top-Level fragments + // JSON fragments are only supported by JSONDecoder in iOS 13 or later + @available(iOS 13, *) func test_encodingTopLevelFragments() { - // JSON fragments are only supported by JSONDecoder in iOS 13 or later - guard #available(iOS 13.0, *) else { - return - } func _testFragment(value: T, fragment: String) { let data: Data @@ -47,7 +45,7 @@ class TestJSONEncoder : XCTestCase { return } do { - let decodedValue = try JSONDecoder().decode(T.self, from: data) + let decodedValue = try StripeJSONDecoder().decode(T.self, from: data) XCTAssertEqual(value, decodedValue) } catch { XCTFail("Failed to decode \(payload) to \(T.self): \(error)") @@ -63,7 +61,7 @@ class TestJSONEncoder : XCTestCase { let v: Int? = nil _testFragment(value: v, fragment: "null") } - + // MARK: - Encoding Top-Level Empty Types func test_encodingTopLevelEmptyStruct() { let empty = EmptyStruct() @@ -76,12 +74,9 @@ class TestJSONEncoder : XCTestCase { } // MARK: - Encoding Top-Level Single-Value Types + // JSON fragments are only supported by JSONDecoder in iOS 13 or later + @available(iOS 13, *) func test_encodingTopLevelSingleValueEnum() { - // JSON fragments are only supported by JSONDecoder in iOS 13 or later - guard #available(iOS 13.0, *) else { - return - } - _testRoundTrip(of: Switch.off) _testRoundTrip(of: Switch.on) @@ -89,20 +84,16 @@ class TestJSONEncoder : XCTestCase { _testRoundTrip(of: TopLevelArrayWrapper(Switch.on)) } + // JSON fragments are only supported by JSONDecoder in iOS 13 or later + @available(iOS 13, *) func test_encodingTopLevelSingleValueStruct() { - // JSON fragments are only supported by JSONDecoder in iOS 13 or later - guard #available(iOS 13.0, *) else { - return - } _testRoundTrip(of: Timestamp(3141592653)) _testRoundTrip(of: TopLevelArrayWrapper(Timestamp(3141592653))) } + // JSON fragments are only supported by JSONDecoder in iOS 13 or later + @available(iOS 13, *) func test_encodingTopLevelSingleValueClass() { - // JSON fragments are only supported by JSONDecoder in iOS 13 or later - guard #available(iOS 13.0, *) else { - return - } _testRoundTrip(of: Counter()) _testRoundTrip(of: TopLevelArrayWrapper(Counter())) } @@ -368,12 +359,6 @@ class TestJSONEncoder : XCTestCase { test_codingOf(value: Bool(true), toAndFrom: "true") test_codingOf(value: Bool(false), toAndFrom: "false") - do { - _ = try JSONDecoder().decode([Bool].self, from: "[1]".data(using: .utf8)!) - XCTFail("Coercing non-boolean numbers into Bools was expected to fail") - } catch { } - - // Check that a Bool false or true isn't converted to 0 or 1 struct Foo: Decodable { var intValue: Int? @@ -395,12 +380,12 @@ class TestJSONEncoder : XCTestCase { func testValue(_ valueName: String) { do { let jsonData = "{ \"\(valueName)\": false }".data(using: .utf8)! - _ = try JSONDecoder().decode(Foo.self, from: jsonData) + _ = try StripeJSONDecoder().decode(Foo.self, from: jsonData) XCTFail("Decoded 'false' as non Bool for \(valueName)") } catch {} do { let jsonData = "{ \"\(valueName)\": true }".data(using: .utf8)! - _ = try JSONDecoder().decode(Foo.self, from: jsonData) + _ = try StripeJSONDecoder().decode(Foo.self, from: jsonData) XCTFail("Decoded 'true' as non Bool for \(valueName)") } catch {} } @@ -418,15 +403,15 @@ class TestJSONEncoder : XCTestCase { testValue("floatValue") testValue("doubleValue") testValue("decimalValue") - let falseJsonData = "{ \"boolValue\": false }".data(using: .utf8)! - if let falseFoo = try? JSONDecoder().decode(Foo.self, from: falseJsonData) { + let falseJsonData = "{ \"bool_value\": false }".data(using: .utf8)! + if let falseFoo = try? StripeJSONDecoder().decode(Foo.self, from: falseJsonData) { XCTAssertFalse(falseFoo.boolValue) } else { XCTFail("Could not decode 'false' as a Bool") } - let trueJsonData = "{ \"boolValue\": true }".data(using: .utf8)! - if let trueFoo = try? JSONDecoder().decode(Foo.self, from: trueJsonData) { + let trueJsonData = "{ \"bool_value\": true }".data(using: .utf8)! + if let trueFoo = try? StripeJSONDecoder().decode(Foo.self, from: trueJsonData) { XCTAssertTrue(trueFoo.boolValue) } else { XCTFail("Could not decode 'true' as a Bool") @@ -510,14 +495,14 @@ class TestJSONEncoder : XCTestCase { test_codingOf(value: Float(1.5), toAndFrom: "1.5") // Check value too large fails to decode. - XCTAssertThrowsError(try JSONDecoder().decode(Float.self, from: "1e100".data(using: .utf8)!)) + XCTAssertThrowsError(try StripeJSONDecoder().decode(Float.self, from: "1e100".data(using: .utf8)!)) } func test_codingOfDouble() { test_codingOf(value: Double(1.5), toAndFrom: "1.5") // Check value too large fails to decode. - XCTAssertThrowsError(try JSONDecoder().decode(Double.self, from: "100e323".data(using: .utf8)!)) + XCTAssertThrowsError(try StripeJSONDecoder().decode(Double.self, from: "100e323".data(using: .utf8)!)) } func test_codingOfDecimal() { @@ -583,8 +568,8 @@ class TestJSONEncoder : XCTestCase { func decode(_ type: String, _ value: String) throws { var key = type.lowercased() - key.append("Value") - _ = try JSONDecoder().decode(DataStruct.self, from: "{ \"\(key)\": \(value) }".data(using: .utf8)!) + key.append("_value") + _ = try StripeJSONDecoder().decode(DataStruct.self, from: "{ \"\(key)\": \(value) }".data(using: .utf8)!) } func testGoodValue(_ type: String, _ value: String) { @@ -733,16 +718,13 @@ class TestJSONEncoder : XCTestCase { XCTAssertEqual(jsonObject["this_is_an_array"] as? [Int], [1, 2, 3]) XCTAssertEqual(jsonObject["this_is_a_dictionary"] as? [String: Bool], ["trueValue": true, "falseValue": false ]) - let decoder = JSONDecoder() - decoder.keyDecodingStrategy = .convertFromSnakeCase - decoder.dateDecodingStrategy = .secondsSince1970 + let decoder = StripeJSONDecoder() let decodedData = try decoder.decode(MyTestData.self, from: encodedData) XCTAssertEqual(data, decodedData) } func test_dictionary_snake_case_decoding() throws { - let decoder = JSONDecoder() - decoder.keyDecodingStrategy = .convertFromSnakeCase + let decoder = StripeJSONDecoder() let snakeCaseJSONData = """ { "snake_case_key": { @@ -795,9 +777,28 @@ class TestJSONEncoder : XCTestCase { let toEncode = Something(dict: [:]) let data = try StripeJSONEncoder().encode(toEncode) - let result = try JSONDecoder().decode(Something.self, from: data) + let result = try StripeJSONDecoder().decode(Something.self, from: data) XCTAssertEqual(result.dict.count, 0) } + + func testIncorrectArrayType() throws { + struct PaymentMethod: Decodable { + let type: String + } + + let json = """ + { + "type": "card" + } + """ + + let decoder = StripeJSONDecoder() + do { + let _ = try decoder.decode(Array.self, from: json.data(using: .utf8)!) + } catch DecodingError.dataCorrupted(let context) { + XCTAssert(context.debugDescription.hasPrefix("Could not convert")) + } + } // MARK: - Helper Functions private var _jsonEmptyDictionary: Data { @@ -858,11 +859,7 @@ class TestJSONEncoder : XCTestCase { } do { - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = dateDecodingStrategy - decoder.dataDecodingStrategy = dataDecodingStrategy - decoder.nonConformingFloatDecodingStrategy = nonConformingFloatDecodingStrategy - decoder.keyDecodingStrategy = .convertFromSnakeCase + let decoder = StripeJSONDecoder() let decoded = try decoder.decode(T.self, from: payload) XCTAssertEqual(decoded, value, "\(T.self) did not round-trip to an equal value.") } catch {