Skip to content

Commit

Permalink
Implement timeout for Risk SDK
Browse files Browse the repository at this point in the history
  • Loading branch information
okhan-okbay-cko committed Aug 5, 2024
1 parent b4fd428 commit d2e063a
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 16 deletions.
62 changes: 49 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 = 3
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,47 @@ 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.riskSDK.configure { [weak self] configurationResult in
guard let self else { return }
switch configurationResult {
case .failure:
finaliseRiskSDKCalls()
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)
}
}

0 comments on commit d2e063a

Please sign in to comment.