Skip to content

Commit

Permalink
Merge 4a98272 into 507414a
Browse files Browse the repository at this point in the history
  • Loading branch information
paultopher committed Jul 3, 2019
2 parents 507414a + 4a98272 commit b106b52
Show file tree
Hide file tree
Showing 7 changed files with 435 additions and 50 deletions.
12 changes: 12 additions & 0 deletions ButtonMerchant.xcodeproj/project.pbxproj
Expand Up @@ -63,6 +63,9 @@
DA4AF05720869966002C3E0E /* NotificationCenterExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4AF05620869966002C3E0E /* NotificationCenterExtensions.swift */; };
DA4AF059208699CA002C3E0E /* NotificationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4AF058208699CA002C3E0E /* NotificationExtensions.swift */; };
DA4AF05B20869E34002C3E0E /* TestNotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4AF05A20869E34002C3E0E /* TestNotificationCenter.swift */; };
DA4C8C7322CD093F000E15A9 /* RequestCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4C8C7222CD093F000E15A9 /* RequestCoordinator.swift */; };
DA4C8C7522CD0C48000E15A9 /* TestRequestCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4C8C7422CD0C48000E15A9 /* TestRequestCoordinator.swift */; };
DA4C8C7722CD1654000E15A9 /* RequestCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4C8C7622CD1654000E15A9 /* RequestCoordinatorTests.swift */; };
DA756B6A22B2B6DF003397E3 /* AmazonRootCA4.cer in Resources */ = {isa = PBXBuildFile; fileRef = DA756B6622B2B6DF003397E3 /* AmazonRootCA4.cer */; };
DA756B6B22B2B6DF003397E3 /* AmazonRootCA2.cer in Resources */ = {isa = PBXBuildFile; fileRef = DA756B6722B2B6DF003397E3 /* AmazonRootCA2.cer */; };
DA756B6C22B2B6DF003397E3 /* AmazonRootCA3.cer in Resources */ = {isa = PBXBuildFile; fileRef = DA756B6822B2B6DF003397E3 /* AmazonRootCA3.cer */; };
Expand Down Expand Up @@ -197,6 +200,9 @@
DA4AF05620869966002C3E0E /* NotificationCenterExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationCenterExtensions.swift; sourceTree = "<group>"; };
DA4AF058208699CA002C3E0E /* NotificationExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationExtensions.swift; sourceTree = "<group>"; };
DA4AF05A20869E34002C3E0E /* TestNotificationCenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNotificationCenter.swift; sourceTree = "<group>"; };
DA4C8C7222CD093F000E15A9 /* RequestCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestCoordinator.swift; sourceTree = "<group>"; };
DA4C8C7422CD0C48000E15A9 /* TestRequestCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestRequestCoordinator.swift; sourceTree = "<group>"; };
DA4C8C7622CD1654000E15A9 /* RequestCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestCoordinatorTests.swift; sourceTree = "<group>"; };
DA756B6622B2B6DF003397E3 /* AmazonRootCA4.cer */ = {isa = PBXFileReference; lastKnownFileType = file; path = AmazonRootCA4.cer; sourceTree = "<group>"; };
DA756B6722B2B6DF003397E3 /* AmazonRootCA2.cer */ = {isa = PBXFileReference; lastKnownFileType = file; path = AmazonRootCA2.cer; sourceTree = "<group>"; };
DA756B6822B2B6DF003397E3 /* AmazonRootCA3.cer */ = {isa = PBXFileReference; lastKnownFileType = file; path = AmazonRootCA3.cer; sourceTree = "<group>"; };
Expand Down Expand Up @@ -374,6 +380,7 @@
9E4C496620616B040053E4CA /* ButtonDefaultsTests.swift */,
9E0DBB1B207BB55A0066A35D /* SystemTests.swift */,
9E5475E1206D7E0C00947A1C /* ClientTests.swift */,
DA4C8C7622CD1654000E15A9 /* RequestCoordinatorTests.swift */,
DA756B7822B2DB67003397E3 /* SessionDelegateTests.swift */,
DA756B8622B3ECE9003397E3 /* TrustEvaluatorTests.swift */,
DA756B6E22B2B717003397E3 /* PEMCertificateTests.swift */,
Expand Down Expand Up @@ -494,6 +501,7 @@
9EAA60C2207AC92B00D21601 /* TestSystem.swift */,
9E4C496A20616B130053E4CA /* TestButtonDefaults.swift */,
9E6F4340206C160C004242A1 /* TestClient.swift */,
DA4C8C7422CD0C48000E15A9 /* TestRequestCoordinator.swift */,
9E76FEB22090EEC700BDC844 /* TestUserAgent.swift */,
DA756B7A22B2DB72003397E3 /* TestTrustEvaluator.swift */,
9EAA60C8207ACA3800D21601 /* Foundation */,
Expand Down Expand Up @@ -582,6 +590,7 @@
9E77202120605506005F740B /* ButtonDefaults.swift */,
9EB1B09F207AB43E00BE0A1A /* System.swift */,
9E2B4310206C1275009F2886 /* Client.swift */,
DA4C8C7222CD093F000E15A9 /* RequestCoordinator.swift */,
DA2F911F22A710E3000054D6 /* SessionDelegate.swift */,
DAE8B96D22AF5F0400D11AF9 /* TrustEvaluator.swift */,
DA98C00522B29BA0002D1823 /* PEMCertificate.swift */,
Expand Down Expand Up @@ -1123,6 +1132,7 @@
DA2F912222A83FEE000054D6 /* SessionDelegate.swift in Sources */,
DA756B8322B33762003397E3 /* URLAuthenticationChallengeExtensions.swift in Sources */,
9EFF2B5A2065965000250269 /* UserDefaultsExtensions.swift in Sources */,
DA4C8C7322CD093F000E15A9 /* RequestCoordinator.swift in Sources */,
9E2B4314206C12BC009F2886 /* URLSessionDataTaskExtensions.swift in Sources */,
DA0FA2A0205C1B3A008296A6 /* Core.swift in Sources */,
DC1F008122CA85F000E789D0 /* Configurable.swift in Sources */,
Expand Down Expand Up @@ -1167,10 +1177,12 @@
DA756B6F22B2B717003397E3 /* PEMCertificateTests.swift in Sources */,
DA756B7B22B2DB72003397E3 /* TestTrustEvaluator.swift in Sources */,
9E6F4343206C171F004242A1 /* TestURLSession.swift in Sources */,
DA4C8C7722CD1654000E15A9 /* RequestCoordinatorTests.swift in Sources */,
9EAA60C7207AC9FF00D21601 /* TestCalendar.swift in Sources */,
DA4AF05B20869E34002C3E0E /* TestNotificationCenter.swift in Sources */,
DADE90C2209B9A630073144B /* TestError.swift in Sources */,
DE175A2120A0AFF6005C97B9 /* VersionTests.generated.swift in Sources */,
DA4C8C7522CD0C48000E15A9 /* TestRequestCoordinator.swift in Sources */,
9E5475E4206D91A900947A1C /* TestURLSessionDataTask.swift in Sources */,
DE1706E5208563D7009FF30B /* TestLocale.swift in Sources */,
9EDED112208FA4D70049A56A /* TestBundle.swift in Sources */,
Expand Down
21 changes: 15 additions & 6 deletions Source/Client.swift
Expand Up @@ -43,18 +43,22 @@ internal protocol ClientType: class {
var userAgent: UserAgentType { get }
func fetchPostInstallURL(parameters: [String: Any], _ completion: @escaping (URL?, String?) -> Void)
func trackOrder(parameters: [String: Any], _ completion: ((Error?) -> Void)?)
func reportOrder(parameters: [String: Any], encodedApplicationId: String, _ completion: ((Error?) -> Void)?)
func reportOrder(parameters: [String: Any],
encodedApplicationId: String,
_ completion: ((Error?) -> Void)?)
init(session: URLSessionType, userAgent: UserAgentType)
}

internal final class Client: ClientType {

var session: URLSessionType
var userAgent: UserAgentType
var requestCoordinator: RequestCoordinatorType

init(session: URLSessionType, userAgent: UserAgentType) {
self.session = session
self.userAgent = userAgent
requestCoordinator = RequestCoordinator(session: session)
}

func fetchPostInstallURL(parameters: [String: Any], _ completion: @escaping (URL?, String?) -> Void) {
Expand All @@ -81,13 +85,18 @@ internal final class Client: ClientType {
}
}

func reportOrder(parameters: [String: Any], encodedApplicationId: String, _ completion: ((Error?) -> Void)?) {
func reportOrder(parameters: [String: Any],
encodedApplicationId: String,
_ completion: ((Error?) -> Void)?) {
var request = urlRequest(url: Service.order.url, parameters: parameters)
request.setValue("Basic \(encodedApplicationId):", forHTTPHeaderField: "Authorization")
enqueueRequest(request: request) { _, error in
if let completion = completion {
completion(error)
}
requestCoordinator.enqueueRetriableRequest(request: request,
attempt: 0,
maxRetries: 3,
retryIntervalInMS: 100) { _, error in
if let completion = completion {
completion(error)
}
}
}

Expand Down
78 changes: 78 additions & 0 deletions Source/RequestCoordinator.swift
@@ -0,0 +1,78 @@
//
// RequestCoordinator.swift
//
// Copyright © 2019 Button, Inc. All rights reserved. (https://usebutton.com)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//

import Foundation

internal protocol RequestCoordinatorType {
func enqueueRetriableRequest(request: URLRequest,
attempt: Int,
maxRetries: Int,
retryIntervalInMS: Int,
_ completion: @escaping (Data?, Error?) -> Void)
}

internal final class RequestCoordinator: RequestCoordinatorType {

var session: URLSessionType

required init(session: URLSessionType) {
self.session = session
}

func enqueueRetriableRequest(request: URLRequest,
attempt: Int,
maxRetries: Int,
retryIntervalInMS: Int,
_ completion: @escaping (Data?, Error?) -> Void) {
let task = session.dataTask(with: request) { data, response, error in

var shouldRetry = true
if let response = response as? HTTPURLResponse,
data != nil {
switch response.statusCode {
case 500...599:
shouldRetry = true
default:
shouldRetry = false
}
}

guard shouldRetry, attempt + 1 <= maxRetries else {
completion(data, error)
return
}

var delay = retryIntervalInMS
delay *= attempt == 0 ? 1 : 2 << (attempt - 1)
DispatchQueue.main.asyncAfter(deadline: .now() + Double(delay) / 1000.0, execute: {
self.enqueueRetriableRequest(request: request,
attempt: attempt + 1,
maxRetries: maxRetries,
retryIntervalInMS: retryIntervalInMS,
completion)
})
}
task.resume()
}
}
74 changes: 30 additions & 44 deletions Tests/UnitTests/ClientTests.swift
Expand Up @@ -38,6 +38,7 @@ class ClientTests: XCTestCase {
// Assert
XCTAssertEqualReferences(client.session as AnyObject, expectedURLSession)
XCTAssertEqualReferences(client.userAgent as AnyObject, expectedUserAgent)
XCTAssertNotNil(client.requestCoordinator as? RequestCoordinator)
}

func testURLRequestCreatedWithParameters() {
Expand Down Expand Up @@ -304,38 +305,46 @@ class ClientTests: XCTestCase {
self.wait(for: [expectation], timeout: 2.0)
}

func testReportOrderEnqueuesRequests() {
func testReportOrder_enqueuesRequestWithCoordinator() {
// Arrange
let testURLSession = TestURLSession()
let client = Client(session: testURLSession, userAgent: TestUserAgent())
let expectedParameters = ["blargh": "blergh"]
let testCoordinator = TestRequestCoordinator()
let client = Client(session: TestURLSession(), userAgent: TestUserAgent())
client.requestCoordinator = testCoordinator
let expectedParameters = ["key": "value"]
let expectedURL = URL(string: "https://api.usebutton.com/v1/mobile-order")!

let expectedAuthHeader = "Basic encoded_app_id:"

// Act
client.reportOrder(parameters: expectedParameters, encodedApplicationId: "") { _ in }
let request = (testURLSession.lastDataTask?.originalRequest)!
client.reportOrder(parameters: expectedParameters,
encodedApplicationId: "encoded_app_id") { _ in }

let request = testCoordinator.actualRequest!
let requestParameters = try? JSONSerialization.jsonObject(with: request.httpBody!)
let parameters = requestParameters as? [String: String]

let headers = request.allHTTPHeaderFields!

// Assert
XCTAssertTrue(testURLSession.didCallDataTaskWithRequest)
XCTAssertEqual(testURLSession.lastDataTask?.originalRequest!.url, expectedURL)
XCTAssertTrue(testCoordinator.didCallEnqueueRetriableRequest)
XCTAssertEqual(request.url!, expectedURL)
XCTAssertEqual(parameters!, expectedParameters)
XCTAssertEqual(headers["Authorization"], expectedAuthHeader)
}

func testReportOrder_addsAuthorizationHeader() {
func testReportOrder_enqueuesRequestWithCoordinator_withProperRetryParameters() {
// Arrange
let testURLSession = TestURLSession()
let client = Client(session: testURLSession, userAgent: TestUserAgent())
let expectedAuthHeader = "Basic encoded_app_id:"

let testCoordinator = TestRequestCoordinator()
let client = Client(session: TestURLSession(), userAgent: TestUserAgent())
client.requestCoordinator = testCoordinator
// Act
client.reportOrder(parameters: ["blargh": "blergh"], encodedApplicationId: "encoded_app_id") { _ in }
let request = (testURLSession.lastDataTask?.originalRequest)!
let authHeaders = request.allHTTPHeaderFields

client.reportOrder(parameters: [:], encodedApplicationId: "") { _ in }

// Assert
XCTAssertEqual(authHeaders?["Authorization"], expectedAuthHeader)
XCTAssertTrue(testCoordinator.didCallEnqueueRetriableRequest)
XCTAssertEqual(testCoordinator.actualAttempt, 0)
XCTAssertEqual(testCoordinator.actualMaxRetries, 3)
XCTAssertEqual(testCoordinator.actualRetryIntervalInMS, 100)
XCTAssertNotNil(testCoordinator.actualCompletion)
}

func testReportOrderSuccess() {
Expand All @@ -358,27 +367,4 @@ class ClientTests: XCTestCase {

self.wait(for: [expectation], timeout: 2.0)
}

func testReportOrderFails() {
// Arrange
let expectation = XCTestExpectation(description: "report order fails")
let testURLSession = TestURLSession()
let client = Client(session: testURLSession, userAgent: TestUserAgent())
let url = URL(string: "https://api.usebutton.com/v1/mobile-order")!
let expectedError = TestError.known

// Act
client.reportOrder(parameters: [:], encodedApplicationId: "") { error in

// Assert
XCTAssertNotNil(error)
XCTAssertEqual(error as? TestError, expectedError)

expectation.fulfill()
}
let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)
testURLSession.lastDataTask?.completion(nil, response, expectedError)

self.wait(for: [expectation], timeout: 2.0)
}
}

0 comments on commit b106b52

Please sign in to comment.