From d2e063a98fba09bf2b742d81cad33e380f32f91f Mon Sep 17 00:00:00 2001 From: Okhan Okbay Date: Fri, 2 Aug 2024 15:23:42 +0100 Subject: [PATCH] Implement timeout for Risk SDK --- .../Tokenisation/CheckoutAPIService.swift | 62 ++++++++++++++---- CheckoutTests/Stubs/StubRisk.swift | 15 ++++- .../CheckoutAPIServiceTests.swift | 64 +++++++++++++++++++ 3 files changed, 125 insertions(+), 16 deletions(-) diff --git a/Checkout/Source/Tokenisation/CheckoutAPIService.swift b/Checkout/Source/Tokenisation/CheckoutAPIService.swift index 6699e2ce4..ed581dfed 100644 --- a/Checkout/Source/Tokenisation/CheckoutAPIService.swift +++ b/Checkout/Source/Tokenisation/CheckoutAPIService.swift @@ -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) -> Void) { @@ -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() @@ -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, paymentType: TokenRequest.TokenType, httpURLResponse: HTTPURLResponse?) { diff --git a/CheckoutTests/Stubs/StubRisk.swift b/CheckoutTests/Stubs/StubRisk.swift index b7985603c..d2103a696 100644 --- a/CheckoutTests/Stubs/StubRisk.swift +++ b/CheckoutTests/Stubs/StubRisk.swift @@ -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) { configureCalledCount += 1 - completion(.success(())) + if shouldConfigureFunctionCallCompletion { + completion(.success(())) + } } func publishData (cardToken: String? = nil, completion: @escaping (Result) -> Void) { publishDataCalledCount += 1 - completion(.success(PublishRiskData(deviceSessionId: "dsid_testDeviceSessionId"))) + if shouldPublishFunctionCallCompletion { + completion(.success(PublishRiskData(deviceSessionId: "dsid_testDeviceSessionId"))) + } } } diff --git a/CheckoutTests/Tokenisation/CheckoutAPIServiceTests.swift b/CheckoutTests/Tokenisation/CheckoutAPIServiceTests.swift index 98780039e..a33ec53d8 100644 --- a/CheckoutTests/Tokenisation/CheckoutAPIServiceTests.swift +++ b/CheckoutTests/Tokenisation/CheckoutAPIServiceTests.swift @@ -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? + 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? + 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) + } +}