Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Require Encodable body for all GET requests #1166

Merged
merged 5 commits into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion Braintree.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,16 @@
80482F8629D3A498007E5F50 /* BTThreeDSecureShippingMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80482F8529D3A498007E5F50 /* BTThreeDSecureShippingMethod.swift */; };
80482F8829D3A571007E5F50 /* BTThreeDSecureRequestedExemptionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80482F8729D3A571007E5F50 /* BTThreeDSecureRequestedExemptionType.swift */; };
804B5712275AAF8500E51A52 /* BTAmericanExpressRewardsBalance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 804B5711275AAF8500E51A52 /* BTAmericanExpressRewardsBalance.swift */; };
804DC45B2B2CF3DC00F17A15 /* BTAmexRewardsBalanceRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 804DC45A2B2CF3DC00F17A15 /* BTAmexRewardsBalanceRequest.swift */; };
804DC45D2B2D08FF00F17A15 /* BTConfigurationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 804DC45C2B2D08FF00F17A15 /* BTConfigurationRequest.swift */; };
8053F05429FAB6B00076F988 /* FPTIBatchData_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8053F05329FAB6B00076F988 /* FPTIBatchData_Tests.swift */; };
8053F05729FAD4790076F988 /* Encodable+Dictionary_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8053F05629FAD4790076F988 /* Encodable+Dictionary_Tests.swift */; };
8053F05929FB2F700076F988 /* URL+IsPayPal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8053F05829FB2F700076F988 /* URL+IsPayPal.swift */; };
80581A8C25531D0A00006F53 /* BTConfiguration+ThreeDSecure_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80581A8B25531D0A00006F53 /* BTConfiguration+ThreeDSecure_Tests.swift */; };
80581B1D2553319C00006F53 /* BTGraphQLHTTP_SSLPinning_IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80581B1C2553319C00006F53 /* BTGraphQLHTTP_SSLPinning_IntegrationTests.swift */; };
8075CBEE2B1B735200CA6265 /* BTAPIRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8075CBED2B1B735200CA6265 /* BTAPIRequest.swift */; };
80BA3C292B23892700900BBB /* FakeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80BA3C282B23892700900BBB /* FakeRequest.swift */; };
80A6C6192B21205900416D50 /* UIApplication+URLOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80A6C6182B21205900416D50 /* UIApplication+URLOpener.swift */; };
80BA3C292B23892700900BBB /* FakeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80BA3C282B23892700900BBB /* FakeRequest.swift */; };
80BA64AC29D788E000E15264 /* BTLocalPaymentResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80BA64AB29D788E000E15264 /* BTLocalPaymentResult.swift */; };
80BA64B229D7937E00E15264 /* BTLocalPaymentRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80BA64B129D7937E00E15264 /* BTLocalPaymentRequest.swift */; };
80BA64B429D795D000E15264 /* BTLocalPaymentRequestDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80BA64B329D795D000E15264 /* BTLocalPaymentRequestDelegate.swift */; };
Expand Down Expand Up @@ -701,6 +703,8 @@
80482F8529D3A498007E5F50 /* BTThreeDSecureShippingMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTThreeDSecureShippingMethod.swift; sourceTree = "<group>"; };
80482F8729D3A571007E5F50 /* BTThreeDSecureRequestedExemptionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTThreeDSecureRequestedExemptionType.swift; sourceTree = "<group>"; };
804B5711275AAF8500E51A52 /* BTAmericanExpressRewardsBalance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTAmericanExpressRewardsBalance.swift; sourceTree = "<group>"; };
804DC45A2B2CF3DC00F17A15 /* BTAmexRewardsBalanceRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTAmexRewardsBalanceRequest.swift; sourceTree = "<group>"; };
804DC45C2B2D08FF00F17A15 /* BTConfigurationRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTConfigurationRequest.swift; sourceTree = "<group>"; };
8053F05329FAB6B00076F988 /* FPTIBatchData_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPTIBatchData_Tests.swift; sourceTree = "<group>"; };
8053F05629FAD4790076F988 /* Encodable+Dictionary_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Encodable+Dictionary_Tests.swift"; sourceTree = "<group>"; };
8053F05829FB2F700076F988 /* URL+IsPayPal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+IsPayPal.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1128,6 +1132,7 @@
9CE8C7F9275ABDE700FB11F9 /* BTAmericanExpressClient.swift */,
9C3F55EE275E80DA00C21C8A /* BTAmericanExpressError.swift */,
804B5711275AAF8500E51A52 /* BTAmericanExpressRewardsBalance.swift */,
804DC45A2B2CF3DC00F17A15 /* BTAmexRewardsBalanceRequest.swift */,
);
path = BraintreeAmericanExpress;
sourceTree = "<group>";
Expand Down Expand Up @@ -1188,6 +1193,7 @@
BED00CB128A57AD400D74AEC /* BTClientTokenError.swift */,
BE2F98CF28A2BCCD008EF189 /* BTConfiguration.swift */,
BE5C8C0228A2C183004F9130 /* BTConfiguration+Core.swift */,
804DC45C2B2D08FF00F17A15 /* BTConfigurationRequest.swift */,
BE698EA328AD2C10001D9B10 /* BTCoreConstants.swift */,
BC17F9BD28D25054004B18CC /* BTGraphQLErrorNode.swift */,
BC17F9BB28D24C9E004B18CC /* BTGraphQLErrorTree.swift */,
Expand Down Expand Up @@ -2705,6 +2711,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
804DC45B2B2CF3DC00F17A15 /* BTAmexRewardsBalanceRequest.swift in Sources */,
804B5712275AAF8500E51A52 /* BTAmericanExpressRewardsBalance.swift in Sources */,
9C3F55EF275E80DA00C21C8A /* BTAmericanExpressError.swift in Sources */,
3B0EF89429B28C0800456973 /* BTAmericanExpressAnalytics.swift in Sources */,
Expand Down Expand Up @@ -2768,6 +2775,7 @@
57CBBCE828B033760037F4EE /* BTGraphQLHTTP.swift in Sources */,
BE63A3A7288F3026001936DA /* BTPostalAddress.swift in Sources */,
BE2F98D028A2BCCD008EF189 /* BTConfiguration.swift in Sources */,
804DC45D2B2D08FF00F17A15 /* BTConfigurationRequest.swift in Sources */,
BED00CB228A57AD400D74AEC /* BTClientTokenError.swift in Sources */,
BE24C67328E73E810067B11A /* BTAPIClientHTTPType.swift in Sources */,
BE9EC0982899CF040022EC63 /* BTAPIPinnedCertificates.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import BraintreeCore
/// - Note: If the nonce is associated with an ineligible card or a card with insufficient points, the rewardsBalance will contain this information as `errorMessage` and `errorCode`.
@objc(getRewardsBalanceForNonce:currencyIsoCode:completion:)
public func getRewardsBalance(forNonce nonce: String, currencyISOCode: String, completion: @escaping (BTAmericanExpressRewardsBalance?, Error?) -> Void) {
let parameters = ["currencyIsoCode": currencyISOCode, "paymentMethodNonce": nonce]
let parameters = BTAmexRewardsBalanceRequest(currencyIsoCode: currencyISOCode, paymentMethodNonce: nonce)
apiClient.sendAnalyticsEvent(BTAmericanExpressAnalytics.started)

apiClient.get("v1/payment_methods/amex_rewards_balance", parameters: parameters) { [weak self] body, response, error in
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Foundation

/// The GET parameters for `v1/payment_methods/amex_rewards_balance`
struct BTAmexRewardsBalanceRequest: Encodable {

private let currencyIsoCode: String
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit for a future PR but we should update this to currencyISOCode to match our style guide preferences. Same in the init

private let paymentMethodNonce: String

init(currencyIsoCode: String, paymentMethodNonce: String) {
self.currencyIsoCode = currencyIsoCode
self.paymentMethodNonce = paymentMethodNonce
}
}
13 changes: 5 additions & 8 deletions Sources/BraintreeCore/BTAPIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,7 @@ import Foundation
configPath = clientToken.configURL.absoluteString
}

let parameters: [String: Any] = ["configVersion": "3"]

configurationHTTP?.get(configPath, parameters: parameters, shouldCache: true) { [weak self] body, response, error in
configurationHTTP?.get(configPath, parameters: BTConfigurationRequest(), shouldCache: true) { [weak self] body, response, error in
guard let self else {
completion(nil, BTAPIClientError.deallocated)
return
Expand Down Expand Up @@ -235,9 +233,9 @@ import Foundation
completion(paymentMethodNonces, nil)
}
}

/// :nodoc: This method is exposed for internal Braintree use only. Do not use. It is not covered by Semantic Versioning and may change or be removed at any time.
///
///
/// Perfom an HTTP GET on a URL composed of the configured from environment and the given path.
/// - Parameters:
/// - path: The endpoint URI path.
Expand All @@ -248,11 +246,10 @@ import Foundation
/// HTTP response and `error` will be `nil`; on failure, `body` and `response` will be
/// `nil` and `error` will contain the error that occurred.
@_documentation(visibility: private)
@objc(GET:parameters:httpType:completion:)
public func get(
_ path: String,
parameters: [String: String]? = nil,
httpType: BTAPIClientHTTPService = .gateway,
parameters: Encodable? = nil,
httpType: BTAPIClientHTTPService = .gateway,
completion: @escaping RequestCompletion
) {
fetchOrReturnRemoteConfiguration { [weak self] configuration, error in
Expand Down
5 changes: 5 additions & 0 deletions Sources/BraintreeCore/BTConfigurationRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// The POST body for `v1/configuration`
struct BTConfigurationRequest: Encodable {

private let configVersion = "3"
}
2 changes: 1 addition & 1 deletion Sources/BraintreeCore/BTGraphQLHTTP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class BTGraphQLHTTP: BTHTTP {

// MARK: - Overrides

override func get(_ path: String, parameters: [String: Any]? = nil, shouldCache: Bool = false, completion: @escaping RequestCompletion) {
override func get(_ path: String, parameters: Encodable? = nil, shouldCache: Bool = false, completion: @escaping RequestCompletion) {
NSException(name: exceptionName, reason: "GET is unsupported").raise()
}

Expand Down
16 changes: 11 additions & 5 deletions Sources/BraintreeCore/BTHTTP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,17 @@ class BTHTTP: NSObject, NSCopying, URLSessionDelegate {

// MARK: - HTTP Methods

func get(_ path: String, parameters: [String: Any]? = nil, shouldCache: Bool = false, completion: @escaping RequestCompletion) {
if shouldCache {
httpRequestWithCaching(method: "GET", path: path, parameters: parameters, completion: completion)
} else {
httpRequest(method: "GET", path: path, parameters: parameters, completion: completion)
func get(_ path: String, parameters: Encodable? = nil, shouldCache: Bool = false, completion: @escaping RequestCompletion) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a future PR, we can make parameters non-optional, since there are no get() calls in the SDK that don't include this value.

do {
let dict = try parameters?.toDictionary()

if shouldCache {
httpRequestWithCaching(method: "GET", path: path, parameters: dict, completion: completion)
} else {
httpRequest(method: "GET", path: path, parameters: dict, completion: completion)
}
} catch let error {
completion(nil, nil, error)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,20 @@ class BTAmericanExpressClient_Tests: XCTestCase {
super.setUp()
mockAPIClient = MockAPIClient(authorization: "development_tokenization_key")!
amexClient = BTAmericanExpressClient(apiClient: mockAPIClient)
}
}

func testGetRewardsBalance_formatsGETRequest() async {
let result = try? await amexClient!.getRewardsBalance(forNonce: "fake-nonce", currencyISOCode: "fake-code")

XCTAssertEqual(mockAPIClient.lastGETPath, "v1/payment_methods/amex_rewards_balance")

guard let lastGetParameters = mockAPIClient.lastGETParameters else {
XCTFail("Expected GET parameters")
return
}
XCTAssertEqual(lastGetParameters["currencyIsoCode"] as! String, "fake-code")
XCTAssertEqual(lastGetParameters["paymentMethodNonce"] as! String, "fake-nonce")
Comment on lines +26 to +27
Copy link
Contributor Author

@scannillo scannillo Dec 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This work has been a good exercise, since it reveals that we aren't testing the formatting of most our JSON bodies for both GET & POST requests right now 😬 (as also evidenced in #1151)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't completely understand the example that reveals we aren't testing the formatting of JSON bodies. Maybe understanding it would give me the answer to my question.

Q: Why do we care about the formatting of the JSON as long as the key value pairs are the same?

}

func testGetRewardsBalance_returnsSendsAnalyticsEventOnSuccess() {
let responseBody = [
Expand Down
36 changes: 22 additions & 14 deletions UnitTests/BraintreeCoreTests/BTHTTP_Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,6 @@ final class BTHTTP_Tests: XCTestCase {
return URL(string: dataURLString)!
}

var parameterDictionary: [String: Any] {
[
"stringParameter": "value",
"crazyStringParameter[]": "crazy%20and&value",
"numericParameter": 42,
"trueBooleanParameter": true,
"falseBooleanParameter": false,
"dictionaryParameter": [ "dictionaryKey": "dictionaryValue" ],
"arrayParameter": ["arrayItem1", "arrayItem2"]
]
}

var testURLSession: URLSession {
let testConfiguration: URLSessionConfiguration = URLSessionConfiguration.ephemeral
testConfiguration.protocolClasses = [BTHTTPTestProtocol.self]
Expand Down Expand Up @@ -650,6 +638,26 @@ final class BTHTTP_Tests: XCTestCase {
}

// MARK: - Parameters tests

struct SampleRequest: Encodable {
enum CodingKeys: String, CodingKey {
case stringParameter
case crazyStringParameter = "crazyStringParameter[]"
case numericParameter
case trueBooleanParameter
case falseBooleanParameter
case dictionaryParameter
case arrayParameter
}

let stringParameter = "value"
let crazyStringParameter = "crazy%20and&value"
let numericParameter = 42
let trueBooleanParameter = true
let falseBooleanParameter = false
let dictionaryParameter = ["dictionaryKey": "dictionaryValue"]
let arrayParameter = ["arrayItem1", "arrayItem2"]
}

func testTransmitsTheParametersAsURLEncodedQueryParameters() {
let expectation = expectation(description: "GET request")
Expand All @@ -664,7 +672,7 @@ final class BTHTTP_Tests: XCTestCase {
"arrayParameter%5B%5D=arrayItem2"
]

http?.get("200.json", parameters: parameterDictionary) { body, response, error in
http?.get("200.json", parameters: SampleRequest()) { body, response, error in
XCTAssertNotNil(body)
XCTAssertNotNil(response)
XCTAssertNil(error)
Expand Down Expand Up @@ -695,7 +703,7 @@ final class BTHTTP_Tests: XCTestCase {
"authorization_fingerprint": "test-authorization-fingerprint"
]

http?.post("200.json", parameters: parameterDictionary) { body, response, error in
http?.post("200.json", parameters: SampleRequest()) { body, response, error in
XCTAssertNotNil(body)
XCTAssertNotNil(response)
XCTAssertNil(error)
Expand Down
4 changes: 2 additions & 2 deletions UnitTests/BraintreeTestShared/FakeHTTP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ import Foundation
cannedError = error
}

public override func get(_ path: String, parameters: [String: Any]? = nil, shouldCache: Bool, completion: BTHTTP.RequestCompletion?) {
public override func get(_ path: String, parameters: Encodable? = nil, shouldCache: Bool, completion: BTHTTP.RequestCompletion?) {
GETRequestCount += 1
lastRequestEndpoint = path
lastRequestParameters = parameters
lastRequestParameters = try? parameters?.toDictionary()
lastRequestMethod = "GET"

if cannedError != nil {
Expand Down
6 changes: 3 additions & 3 deletions UnitTests/BraintreeTestShared/MockAPIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public class MockAPIClient: BTAPIClient {
public var lastPOSTAPIClientHTTPType: BTAPIClientHTTPService?

public var lastGETPath = ""
public var lastGETParameters = [:] as [String : String]?
public var lastGETParameters = [:] as [String: Any]?
public var lastGETAPIClientHTTPType: BTAPIClientHTTPService?

public var postedAnalyticsEvents : [String] = []
Expand All @@ -27,9 +27,9 @@ public class MockAPIClient: BTAPIClient {
super.init(authorization: authorization, sendAnalyticsEvent: sendAnalyticsEvent)
}

public override func get(_ path: String, parameters: [String: String]?, httpType: BTAPIClientHTTPService, completion completionBlock: ((BTJSON?, HTTPURLResponse?, Error?) -> Void)? = nil) {
public override func get(_ path: String, parameters: Encodable?, httpType: BTAPIClientHTTPService, completion completionBlock: ((BTJSON?, HTTPURLResponse?, Error?) -> Void)? = nil) {
lastGETPath = path
lastGETParameters = parameters
lastGETParameters = try? parameters?.toDictionary()
lastGETAPIClientHTTPType = httpType

guard let completionBlock = completionBlock else {
Expand Down