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

Implement timeout for Risk SDK #544

Merged
merged 1 commit into from
Aug 6, 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
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()
ehab-al-cko marked this conversation as resolved.
Show resolved Hide resolved
self.logManager.queue(event: .riskSDKTimeOut)
}

self.riskSDK.configure { [weak self] configurationResult in
guard let self else { return }
switch configurationResult {
ehab-al-cko marked this conversation as resolved.
Show resolved Hide resolved
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
Loading