Skip to content

Commit

Permalink
Merge pull request #544 from checkout/feature/risk-sdk-timeout-recovery
Browse files Browse the repository at this point in the history
Implement timeout for Risk SDK
  • Loading branch information
okhan-okbay-cko authored Aug 6, 2024
2 parents b4fd428 + 7743d72 commit df79dd5
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 22 deletions.
3 changes: 2 additions & 1 deletion Checkout/Samples/CocoapodsSample/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ target 'CheckoutCocoapodsSample' do
use_frameworks!

# Pods for CheckoutSDKCocoapodsSample
pod 'Checkout', '4.3.6'
# pod 'Checkout', '4.3.7'
pod 'Checkout', :git => 'https://github.com/checkout/frames-ios.git', :branch => 'feature/risk-sdk-timeout-recovery'

end
9 changes: 7 additions & 2 deletions Checkout/Source/Logging/CheckoutLogEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ enum CheckoutLogEvent: Equatable {
case cvvRequested(SecurityCodeTokenRequestData)
case cvvResponse(SecurityCodeTokenRequestData, TokenResponseData)
case riskSDKCompletion
case riskSDKTimeOut

func event(date: Date) -> Event {
Event(
Expand Down Expand Up @@ -58,6 +59,8 @@ enum CheckoutLogEvent: Equatable {
return "card_validator_cvv"
case .riskSDKCompletion:
return "risk_sdk_completion"
case .riskSDKTimeOut:
return "risk_sdk_time_out"
}
}

Expand All @@ -70,7 +73,8 @@ enum CheckoutLogEvent: Equatable {
.validateExpiryInteger,
.validateCVV,
.cvvRequested,
.riskSDKCompletion:
.riskSDKCompletion,
.riskSDKTimeOut:
return .info
case .tokenResponse(_, let tokenResponseData),
.cvvResponse(_, let tokenResponseData):
Expand All @@ -93,7 +97,8 @@ enum CheckoutLogEvent: Equatable {
.validateExpiryString,
.validateExpiryInteger,
.validateCVV,
.riskSDKCompletion:
.riskSDKCompletion,
.riskSDKTimeOut:
return [:]
case let .tokenRequested(tokenRequestData):
return [
Expand Down
63 changes: 50 additions & 13 deletions Checkout/Source/Tokenisation/CheckoutAPIService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ final public class CheckoutAPIService: CheckoutAPIProtocol {
}
}

let timeoutInterval: TimeInterval = 5.0
private let taskCompletionQueue = DispatchQueue(label: "taskCompletionQueue", qos: .userInitiated)
private var isTaskCompleted = false

private func createToken(requestParameters: NetworkManager.RequestParameters,
paymentType: TokenRequest.TokenType,
completion: @escaping (Result<TokenDetails, TokenisationError.TokenRequest>) -> Void) {
Expand All @@ -164,19 +168,10 @@ final public class CheckoutAPIService: CheckoutAPIProtocol {
return
}

self.riskSDK.configure { configurationResult in
switch configurationResult {
case .failure:
completion(.success(tokenDetails))
logManager.resetCorrelationID()
case .success():
self.riskSDK.publishData(cardToken: tokenDetails.token) { _ in
logManager.queue(event: .riskSDKCompletion)
completion(.success(tokenDetails))
logManager.resetCorrelationID()
}
}
}
self.callRiskSDK(tokenDetails: tokenDetails) {
completion(.success(tokenDetails))
}

case .errorResponse(let errorResponse):
completion(.failure(.serverError(errorResponse)))
logManager.resetCorrelationID()
Expand All @@ -187,6 +182,48 @@ final public class CheckoutAPIService: CheckoutAPIProtocol {
}
}

private func callRiskSDK(tokenDetails: TokenDetails,
completion: @escaping () -> Void) {

/* Risk SDK calls can be finalised in 3 different ways
1. When Risk SDK's configure(...) function completed successfully and publishData(...) completed successfully or not
2. When Risk SDK's configure(...) function completed with failure
3. When Risk SDK's configure(...) or publishData(...) functions hang and don't call their completion blocks.
In this case, we wait for `self.timeoutInterval` amount of time and call the completion block anyway.
All these operations are done synchronously to avoid the completion closure getting called multiple times.
*/

let finaliseRiskSDKCalls = {
self.taskCompletionQueue.sync {
if !self.isTaskCompleted {
self.isTaskCompleted = true
completion()
}
}
}

DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + timeoutInterval) {
finaliseRiskSDKCalls()
self.logManager.queue(event: .riskSDKTimeOut)
}

self.riskSDK.configure { [weak self] configurationResult in
guard let self else { return }
switch configurationResult {
case .failure:
finaliseRiskSDKCalls()
self.logManager.resetCorrelationID()
case .success():
self.riskSDK.publishData(cardToken: tokenDetails.token) { _ in
self.logManager.queue(event: .riskSDKCompletion)
finaliseRiskSDKCalls()
self.logManager.resetCorrelationID()
}
}
}
}

private func logTokenResponse(tokenResponseResult: NetworkRequestResult<TokenResponse, TokenisationError.ServerError>,
paymentType: TokenRequest.TokenType,
httpURLResponse: HTTPURLResponse?) {
Expand Down
15 changes: 12 additions & 3 deletions CheckoutTests/Stubs/StubRisk.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,24 @@ class StubRisk: RiskProtocol {

var configureCalledCount = 0
var publishDataCalledCount = 0


// If set to false, Risk SDK will hang and not call the completion block for that specific function.
// It will mimic the behaviour of a bug we have. We need to call Frames's completion block after the defined timeout period in that case.
var shouldConfigureFunctionCallCompletion: Bool = true
var shouldPublishFunctionCallCompletion: Bool = true

func configure(completion: @escaping (Result<Void, RiskError.Configuration>) -> Void) {
configureCalledCount += 1
completion(.success(()))
if shouldConfigureFunctionCallCompletion {
completion(.success(()))
}
}

func publishData (cardToken: String? = nil, completion: @escaping (Result<PublishRiskData, RiskError.Publish>) -> Void) {
publishDataCalledCount += 1
completion(.success(PublishRiskData(deviceSessionId: "dsid_testDeviceSessionId")))
if shouldPublishFunctionCallCompletion {
completion(.success(PublishRiskData(deviceSessionId: "dsid_testDeviceSessionId")))
}
}
}

64 changes: 64 additions & 0 deletions CheckoutTests/Tokenisation/CheckoutAPIServiceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -354,3 +354,67 @@ extension CheckoutAPIServiceTests {
}
}
}

// Risk SDK Timeout Recovery Tests
extension CheckoutAPIServiceTests {
func testWhenRiskSDKCallsCompletionThenFramesReturnsSuccess() {
let card = StubProvider.createCard()
let tokenRequest = StubProvider.createTokenRequest()
let requestParameters = StubProvider.createRequestParameters()
let tokenResponse = StubProvider.createTokenResponse()
let tokenDetails = StubProvider.createTokenDetails()

stubTokenRequestFactory.createToReturn = .success(tokenRequest)
stubRequestFactory.createToReturn = .success(requestParameters)
stubTokenDetailsFactory.createToReturn = tokenDetails

var result: Result<TokenDetails, TokenisationError.TokenRequest>?
subject.createToken(.card(card)) { result = $0 }
stubRequestExecutor.executeCalledWithCompletion?(.response(tokenResponse), HTTPURLResponse())

XCTAssertEqual(stubRisk.configureCalledCount, 1)
XCTAssertEqual(stubRisk.publishDataCalledCount, 1)
XCTAssertEqual(result, .success(tokenDetails))
}

func testWhenRiskSDKConfigureHangsThenFramesSDKCancelsWaitingRiskSDKAndCallsCompletionBlockAnywayAfterTimeout() {
stubRisk.shouldConfigureFunctionCallCompletion = false // Configure function will hang forever before it calls its completion closure
verifyRiskSDKTimeoutRecovery(timeoutAddition: 1, expectedConfigureCallCount: 1, expectedPublishDataCallCount: 0)
}

func testWhenRiskSDKPublishHangsThenFramesSDKCancelsWaitingRiskSDKAndCallsCompletionBlockAnywayAfterTimeout() {
stubRisk.shouldPublishFunctionCallCompletion = false // Publish data function will hang forever before it calls its completion closure
verifyRiskSDKTimeoutRecovery(timeoutAddition: 1, expectedConfigureCallCount: 1, expectedPublishDataCallCount: 1)
}

func verifyRiskSDKTimeoutRecovery(timeoutAddition: Double,
expectedConfigureCallCount: Int,
expectedPublishDataCallCount: Int,
file: StaticString = #file,
line: UInt = #line) {
let card = StubProvider.createCard()
let tokenRequest = StubProvider.createTokenRequest()
let tokenResponse = StubProvider.createTokenResponse()
let requestParameters = StubProvider.createRequestParameters()
let tokenDetails = StubProvider.createTokenDetails()

stubTokenRequestFactory.createToReturn = .success(tokenRequest)
stubRequestFactory.createToReturn = .success(requestParameters)
stubTokenDetailsFactory.createToReturn = tokenDetails

let expectation = self.expectation(description: "Frames will time out awaiting Risk SDK result")

var _: Result<TokenDetails, TokenisationError.TokenRequest>?
subject.createToken(.card(card)) {

XCTAssertEqual(self.stubRisk.configureCalledCount, expectedConfigureCallCount, file: file, line: line)
XCTAssertEqual(self.stubRisk.publishDataCalledCount, expectedPublishDataCallCount, file: file, line: line)
XCTAssertEqual($0, .success(tokenDetails), file: file, line: line)

expectation.fulfill()
}
stubRequestExecutor.executeCalledWithCompletion?(.response(tokenResponse), HTTPURLResponse())

waitForExpectations(timeout: subject.timeoutInterval + timeoutAddition)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1238,8 +1238,8 @@
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/checkout/frames-ios";
requirement = {
kind = exactVersion;
version = 4.3.6;
branch = "feature/risk-sdk-timeout-recovery";
kind = branch;
};
};
16C3F83E2A7927ED00690639 /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */ = {
Expand Down
3 changes: 2 additions & 1 deletion iOS Example Frame/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ target 'iOS Example Frame' do
use_frameworks!

# Pods for iOS Example Custom
pod 'Frames', '4.3.6'
# pod 'Frames', '4.3.6'
pod 'Frames', :git => 'https://github.com/checkout/frames-ios.git', :branch => 'feature/risk-sdk-timeout-recovery'

end

Expand Down

0 comments on commit df79dd5

Please sign in to comment.