diff --git a/Adyen.xcodeproj/project.pbxproj b/Adyen.xcodeproj/project.pbxproj index e372a04a14..4e7c3a4898 100644 --- a/Adyen.xcodeproj/project.pbxproj +++ b/Adyen.xcodeproj/project.pbxproj @@ -151,6 +151,8 @@ 8109FF4F2AD84496000748C8 /* MapkitAddressLookupProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81AA3B372AD040D800F5719D /* MapkitAddressLookupProvider.swift */; }; 8109FF502AD84498000748C8 /* MapkitAddressLookupProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81AA3B372AD040D800F5719D /* MapkitAddressLookupProvider.swift */; }; 81129AE62A4EEF8600E63EBE /* SearchViewController+InterfaceState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81129AE52A4EEF8600E63EBE /* SearchViewController+InterfaceState.swift */; }; + 8122B9BB2B9A09D0002FC4D6 /* TwintSDKActionTests+Flows.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8122B9BA2B9A09D0002FC4D6 /* TwintSDKActionTests+Flows.swift */; }; + 8122B9BD2B9A0FF3002FC4D6 /* ErrorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8122B9BC2B9A0FF3002FC4D6 /* ErrorMock.swift */; }; 813BF1122B2365400096940E /* XCTestCase+FirstResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 813BF1102B2364E00096940E /* XCTestCase+FirstResponder.swift */; }; 813BF1132B2365400096940E /* XCTestCase+FirstResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 813BF1102B2364E00096940E /* XCTestCase+FirstResponder.swift */; }; 813EF9DE2A5DA0BC00C65D15 /* FormPickerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 813EF9DD2A5DA0BC00C65D15 /* FormPickerItem.swift */; }; @@ -166,7 +168,6 @@ 8140A3762A3327B500896403 /* AdyenComponents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F9175E55259393E800D653BE /* AdyenComponents.framework */; }; 814276622A7145F50081E896 /* AddressInputFormViewController+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 814276612A7145F50081E896 /* AddressInputFormViewController+ViewModel.swift */; }; 8149CCB62B0B855F007235E2 /* ThreeDS2PlusDACoreActionHandlerTests+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8149CCB52B0B855F007235E2 /* ThreeDS2PlusDACoreActionHandlerTests+Constants.swift */; }; - 815175612B986871008A9601 /* TwintSDKActionTests+Success.swift in Sources */ = {isa = PBXBuildFile; fileRef = 815175602B986871008A9601 /* TwintSDKActionTests+Success.swift */; }; 815175632B986B6D008A9601 /* TwintSDKActionTests+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 815175622B986B6D008A9601 /* TwintSDKActionTests+Convenience.swift */; }; 8169B9EC2A0506CC00AAC9F8 /* Adyen.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E2C0E03322097917008616F6 /* Adyen.framework */; }; 817F6D8C2B62A6110010D248 /* (null) in Sources */ = {isa = PBXBuildFile; }; @@ -1488,6 +1489,8 @@ 8109FF4B2AD5AD0C000748C8 /* OpenExternalAppDetector+DependencyKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenExternalAppDetector+DependencyKey.swift"; sourceTree = ""; }; 8109FF4D2AD6D8A9000748C8 /* MockAddressLookupProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAddressLookupProvider.swift; sourceTree = ""; }; 81129AE52A4EEF8600E63EBE /* SearchViewController+InterfaceState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewController+InterfaceState.swift"; sourceTree = ""; }; + 8122B9BA2B9A09D0002FC4D6 /* TwintSDKActionTests+Flows.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TwintSDKActionTests+Flows.swift"; sourceTree = ""; }; + 8122B9BC2B9A0FF3002FC4D6 /* ErrorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMock.swift; sourceTree = ""; }; 813BF1102B2364E00096940E /* XCTestCase+FirstResponder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+FirstResponder.swift"; sourceTree = ""; }; 813EF9DD2A5DA0BC00C65D15 /* FormPickerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormPickerItem.swift; sourceTree = ""; }; 813EF9E12A5DA2D400C65D15 /* FormPickerItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormPickerItemView.swift; sourceTree = ""; }; @@ -1501,7 +1504,6 @@ 814044302B62BDBB00EB7FBA /* SecuredViewControllerUITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecuredViewControllerUITests.swift; sourceTree = ""; }; 814276612A7145F50081E896 /* AddressInputFormViewController+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AddressInputFormViewController+ViewModel.swift"; sourceTree = ""; }; 8149CCB52B0B855F007235E2 /* ThreeDS2PlusDACoreActionHandlerTests+Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThreeDS2PlusDACoreActionHandlerTests+Constants.swift"; sourceTree = ""; }; - 815175602B986871008A9601 /* TwintSDKActionTests+Success.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TwintSDKActionTests+Success.swift"; sourceTree = ""; }; 815175622B986B6D008A9601 /* TwintSDKActionTests+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TwintSDKActionTests+Convenience.swift"; sourceTree = ""; }; 81825CB72AC59C3300F91912 /* UIView+Search.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Search.swift"; sourceTree = ""; }; 81825CBA2AC59C4000F91912 /* UIViewController+Search.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+Search.swift"; sourceTree = ""; }; @@ -2853,7 +2855,7 @@ isa = PBXGroup; children = ( 8182AAB82B95FEA10087568E /* TwintSDKActionTests.swift */, - 815175602B986871008A9601 /* TwintSDKActionTests+Success.swift */, + 8122B9BA2B9A09D0002FC4D6 /* TwintSDKActionTests+Flows.swift */, 815175622B986B6D008A9601 /* TwintSDKActionTests+Convenience.swift */, 8182AABA2B974D160087568E /* Twint+Spy.swift */, ); @@ -3130,6 +3132,7 @@ 813BF1102B2364E00096940E /* XCTestCase+FirstResponder.swift */, 81B505782A7BE209009B4CB3 /* UIBarButtonItem+XCTest.swift */, 819CC3332B14C53200D2EEE9 /* PaymentMethods+Equatable.swift */, + 8122B9BC2B9A0FF3002FC4D6 /* ErrorMock.swift */, ); path = Helpers; sourceTree = ""; @@ -6982,6 +6985,7 @@ F99F08582383EA2A00EBB948 /* ValidatorMock.swift in Sources */, F9EDB78B2395608D00CFB3C9 /* SEPADirectDebitComponentTests.swift in Sources */, E74D918325AF3FE600743B0C /* CardBrandProviderTests.swift in Sources */, + 8122B9BB2B9A09D0002FC4D6 /* TwintSDKActionTests+Flows.swift in Sources */, 813BF1122B2365400096940E /* XCTestCase+FirstResponder.swift in Sources */, E773EA3C2523432B00119499 /* CardTypeProviderMock.swift in Sources */, C97C16AC280702B200534419 /* AnalyticsFlavorTests.swift in Sources */, @@ -7018,6 +7022,7 @@ E7768BDC26B416C3000851B3 /* DateValidationTests.swift in Sources */, F97BC2CE2668EB4500F1D242 /* VoucherViewDelegateMock.swift in Sources */, F9AC61C2243751A70062A00D /* ActionComponentDelegateMock.swift in Sources */, + 8122B9BD2B9A0FF3002FC4D6 /* ErrorMock.swift in Sources */, C9C0005F280474DF00CE2EEC /* AnalyticsProviderTests.swift in Sources */, F9F1A98726CCFD1F0005CB1D /* JWAA256CBCHS512AlgorithmTests.swift in Sources */, 81EA6C122A44836E0071A141 /* FormAddressItemTests.swift in Sources */, @@ -7049,7 +7054,6 @@ F9D5750E237475DB009C18B5 /* CardEncryptorCardTests.swift in Sources */, F9C5F0DD25CAB8FC005A6E54 /* VoucherComponentTests.swift in Sources */, 5AD40F4F263157D50090E01C /* UIButtonHelpersTests.swift in Sources */, - 815175612B986871008A9601 /* TwintSDKActionTests+Success.swift in Sources */, E7E8343C24F3C81700EC3844 /* PublicKeyProviderMock.swift in Sources */, F9838D0126318EBD00963483 /* ReadyToSubmitPaymentComponentDelegateMock.swift in Sources */, F919DF9924EA64680027976E /* CardPublicKeyProviderTests.swift in Sources */, diff --git a/Adyen/Core/Payment Methods/Abstract/AnyPaymentMethod.swift b/Adyen/Core/Payment Methods/Abstract/AnyPaymentMethod.swift index 31defb957a..b8a988cded 100644 --- a/Adyen/Core/Payment Methods/Abstract/AnyPaymentMethod.swift +++ b/Adyen/Core/Payment Methods/Abstract/AnyPaymentMethod.swift @@ -78,9 +78,11 @@ internal enum AnyPaymentMethod: Codable { case let .onlineBanking(paymentMethod): return paymentMethod case let .upi(paymentMethod): return paymentMethod case let .cashAppPay(paymentMethod): return paymentMethod + // swiftlint:disable switch_case_alignment #if canImport(TwintSDK) case let .twint(paymentMethod): return paymentMethod #endif + // swiftlint:enable switch_case_alignment case .none: return nil } } diff --git a/Adyen/Core/ToolBar/CancellableToolBar.swift b/Adyen/Core/ToolBar/CancellableToolBar.swift index 07fe23915d..9f34a1816b 100644 --- a/Adyen/Core/ToolBar/CancellableToolBar.swift +++ b/Adyen/Core/ToolBar/CancellableToolBar.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2022 Adyen N.V. +// Copyright (c) 2024 Adyen N.V. // // This file is open source and available under the MIT license. See the LICENSE file for more info. // @@ -40,4 +40,3 @@ public final class CancellingToolBar: ModalToolbar { } } } - diff --git a/AdyenActions/Actions/SDKAction.swift b/AdyenActions/Actions/SDKAction.swift index 4415631bcf..c78019e6ff 100644 --- a/AdyenActions/Actions/SDKAction.swift +++ b/AdyenActions/Actions/SDKAction.swift @@ -24,10 +24,12 @@ public enum SDKAction: Decodable { switch type { case .weChatPay: self = try .weChatPay(WeChatPaySDKAction(from: decoder)) + // swiftlint:disable switch_case_alignment #if canImport(TwintSDK) case .twint: self = try .twint(TwintSDKAction(from: decoder)) #endif + // swiftlint:enable switch_case_alignment } } diff --git a/AdyenActions/Actions/TwintSDKAction.swift b/AdyenActions/Actions/TwintSDKAction.swift index 6891679afc..55ff78b37b 100644 --- a/AdyenActions/Actions/TwintSDKAction.swift +++ b/AdyenActions/Actions/TwintSDKAction.swift @@ -21,7 +21,7 @@ public final class TwintSDKAction: Decodable { // The payment method subtype public let type: String - enum CodingKeys: CodingKey { + internal enum CodingKeys: CodingKey { case sdkData case paymentData case paymentMethodType diff --git a/AdyenActions/AdyenActionComponent.swift b/AdyenActions/AdyenActionComponent.swift index 8d15b879d2..baec815e4d 100644 --- a/AdyenActions/AdyenActionComponent.swift +++ b/AdyenActions/AdyenActionComponent.swift @@ -80,7 +80,7 @@ public final class AdyenActionComponent: ActionComponent, ActionHandlingComponen /// without a host/path/... (e.g. "my-app", not a url "my-app://...") public init(callbackAppScheme: String) { if !Self.isCallbackSchemeValid(callbackAppScheme) { - assertionFailure("Format of provided callbackAppScheme '\(callbackAppScheme)' is incorrect.") + AdyenAssertion.assertionFailure(message: "Format of provided callbackAppScheme '\(callbackAppScheme)' is incorrect.") } self.callbackAppScheme = callbackAppScheme @@ -213,10 +213,12 @@ public final class AdyenActionComponent: ActionComponent, ActionHandlingComponen switch sdkAction { case let .weChatPay(weChatPaySDKAction): handle(weChatPaySDKAction) + // swiftlint:disable switch_case_alignment #if canImport(TwintSDK) case let .twint(twintSDKAction): handle(twintSDKAction) #endif + // swiftlint:enable switch_case_alignment } } diff --git a/AdyenActions/Components/SDK/TwintSDKActionComponent.swift b/AdyenActions/Components/SDK/TwintSDKActionComponent.swift index 8c696c97c3..7cdcd55331 100644 --- a/AdyenActions/Components/SDK/TwintSDKActionComponent.swift +++ b/AdyenActions/Components/SDK/TwintSDKActionComponent.swift @@ -11,7 +11,7 @@ import Foundation #endif #if canImport(TwintSDK) - struct TwintActionDetails: AdditionalDetails {} + internal struct TwintActionDetails: AdditionalDetails {} /// A component that handles Twint SDK action's. public final class TwintSDKActionComponent: ActionComponent { @@ -38,7 +38,8 @@ import Foundation /// The callback app scheme invoked once the Twint app is done with the payment /// - /// - Important: This value is required to only provide the scheme, without a host/path/.... (e.g. "my-app", not a url "my-app://...") + /// - Important: This value is required to only provide the scheme, + /// without a host/path/.... (e.g. "my-app", not a url "my-app://...") public let callbackAppScheme: String /// Initializes an instance of `Configuration` @@ -48,7 +49,8 @@ import Foundation /// - callbackAppScheme: The callback app scheme invoked once the Twint app is done with the payment /// - localizationParameters: The localization parameters, leave it nil to use the default parameters. /// - /// - Important: The value of ``callbackAppScheme`` is required to only provide the scheme, without a host/path/... (e.g. "my-app", not a url "my-app://...") + /// - Important: The value of ``callbackAppScheme`` is required to only provide the scheme, + /// without a host/path/... (e.g. "my-app", not a url "my-app://...") public init( style: AwaitComponentStyle = .init(), callbackAppScheme: String, diff --git a/AdyenDropIn/DropInComponent.swift b/AdyenDropIn/DropInComponent.swift index 6b4c180788..788facef66 100644 --- a/AdyenDropIn/DropInComponent.swift +++ b/AdyenDropIn/DropInComponent.swift @@ -238,10 +238,12 @@ public final class DropInComponent: NSObject, navigationController.present(asModal: component) case let component as InstantPaymentComponent: component.initiatePayment() + // swiftlint:disable switch_case_alignment #if canImport(AdyenTwint) case let component as TwintComponent: component.initiatePayment() #endif + // swiftlint:enable switch_case_alignment default: break } diff --git a/AdyenDropIn/Models/DropInConfiguration.swift b/AdyenDropIn/Models/DropInConfiguration.swift index 4deb4f39af..06023c1145 100644 --- a/AdyenDropIn/Models/DropInConfiguration.swift +++ b/AdyenDropIn/Models/DropInConfiguration.swift @@ -87,7 +87,7 @@ public extension DropInComponent { public var threeDS: AdyenActionComponent.Configuration.ThreeDS = .init() /// Twint configurations - public var twint: AdyenActionComponent.Configuration.Twint? = nil + public var twint: AdyenActionComponent.Configuration.Twint? } /// Boleto component configuration. diff --git a/Tests/Actions Tests/ActionComponent/Twint/TwintSDKActionTests+Convenience.swift b/Tests/Actions Tests/ActionComponent/Twint/TwintSDKActionTests+Convenience.swift index b74d375b53..f53259d23d 100644 --- a/Tests/Actions Tests/ActionComponent/Twint/TwintSDKActionTests+Convenience.swift +++ b/Tests/Actions Tests/ActionComponent/Twint/TwintSDKActionTests+Convenience.swift @@ -8,20 +8,44 @@ import XCTest @testable @_spi(AdyenInternal) import Adyen @testable @_spi(AdyenInternal) import AdyenActions -extension TwintSDKActionTests { - - /// PresentationDelegateMock that fails when `doPresent` is called - static func failingPresentationDelegateMock() -> PresentationDelegateMock { - let presentationDelegateMock = PresentationDelegateMock() - presentationDelegateMock.doPresent = { _ in - XCTFail("Nothing should have been displayed") - } - return presentationDelegateMock +#if canImport(TwintSDK) +import TwintSDK +#endif + +#if canImport(TwintSDK) + +extension TWAppConfiguration { + static var dummy: TWAppConfiguration { + let twintAppConfiguration = TWAppConfiguration() + twintAppConfiguration.appDisplayName = "Test App" + twintAppConfiguration.appURLScheme = "scheme://" + return twintAppConfiguration } +} + +extension TwintSDKActionComponent.Configuration { + static var dummy: Self { + .init(callbackAppScheme: "ui-host") + } +} + +extension TwintSDKAction { + static var dummy: TwintSDKAction { + .init( + sdkData: .init(token: "token"), + paymentData: "paymentData", + paymentMethodType: "paymentMethodType", + type: "type" + ) + } +} + +extension TwintSDKActionTests { static func actionComponent( with twintSpy: TwintSpy, - presentationDelegate: PresentationDelegate? + presentationDelegate: PresentationDelegate?, + delegate: ActionComponentDelegate? ) -> TwintSDKActionComponent { let component = TwintSDKActionComponent( @@ -31,7 +55,53 @@ extension TwintSDKActionTests { ) component.presentationDelegate = presentationDelegate + component.delegate = delegate return component } + + // MARK: PresentationDelegateMock + + /// PresentationDelegateMock that fails when `doPresent` is called + static func failingPresentationDelegateMock() -> PresentationDelegateMock { + let presentationDelegateMock = PresentationDelegateMock() + presentationDelegateMock.doPresent = { _ in + XCTFail("Nothing should have been displayed") + } + return presentationDelegateMock + } + + /// ActionComponentDelegateMock that fails when `onDidFail` is called + static func successFlowActionComponentDelegateMock( + onProvide: @escaping (ActionComponentData) -> Void + ) -> ActionComponentDelegateMock { + + let actonComponentDelegateMock = ActionComponentDelegateMock() + actonComponentDelegateMock.onDidFail = { error, component in + XCTFail("delegate.onDidFail should not have been called") + } + actonComponentDelegateMock.onDidProvide = { data, component in + onProvide(data) + } + + return actonComponentDelegateMock + } + + /// ActionComponentDelegateMock that fails when `onDidFail` is called + static func failureFlowActionComponentDelegateMock( + onDidFail: @escaping (Error) -> Void + ) -> ActionComponentDelegateMock { + + let actonComponentDelegateMock = ActionComponentDelegateMock() + actonComponentDelegateMock.onDidFail = { error, component in + onDidFail(error) + } + actonComponentDelegateMock.onDidProvide = { data, component in + XCTFail("delegate.onDidProvide should not have been called") + } + + return actonComponentDelegateMock + } } + +#endif diff --git a/Tests/Actions Tests/ActionComponent/Twint/TwintSDKActionTests+SuccessFlow.swift b/Tests/Actions Tests/ActionComponent/Twint/TwintSDKActionTests+Flows.swift similarity index 58% rename from Tests/Actions Tests/ActionComponent/Twint/TwintSDKActionTests+SuccessFlow.swift rename to Tests/Actions Tests/ActionComponent/Twint/TwintSDKActionTests+Flows.swift index 0abe1b2a57..0c3fb28213 100644 --- a/Tests/Actions Tests/ActionComponent/Twint/TwintSDKActionTests+SuccessFlow.swift +++ b/Tests/Actions Tests/ActionComponent/Twint/TwintSDKActionTests+Flows.swift @@ -16,30 +16,65 @@ import TwintSDK extension TwintSDKActionTests { - /// ActionComponentDelegateMock that fails when `onDidFail` is called - private static func actonComponentDelegateMock( - onProvide: @escaping (ActionComponentData) -> Void - ) -> ActionComponentDelegateMock { - - let actonComponentDelegateMock = ActionComponentDelegateMock() - actonComponentDelegateMock.onDidFail = { error, component in - XCTFail("delegate.onDidFail should not have been called") + func testSuccessFlow() throws { + + let expectedError = TWErrorCode.B_SUCCESS + let handleOnDidProvideExpectation = expectation(description: "delegate.onDidProvide was called") + + let delegate = Self.successFlowActionComponentDelegateMock { + XCTAssertTrue($0.details is TwintActionDetails) + XCTAssertEqual($0.paymentData, TwintSDKAction.dummy.paymentData) + handleOnDidProvideExpectation.fulfill() } - actonComponentDelegateMock.onDidProvide = { data, component in - XCTAssertTrue(data.details is TwintActionDetails) - XCTAssertEqual(data.paymentData, TwintSDKAction.dummy.paymentData) - onProvide(data) + + try singleAppFlow( + delegate: delegate, + expectedTwintError: expectedError + ) + + wait( + for: [handleOnDidProvideExpectation], + timeout: 1, + enforceOrder: true + ) + } + + func testFailureFlow() throws { + + let expectedError = TWErrorCode.B_ERROR + let handleOnDidProvideExpectation = expectation(description: "delegate.onDidProvide was called") + + let delegate = Self.failureFlowActionComponentDelegateMock { + let errorCode = ($0 as NSError).code + XCTAssertEqual(errorCode, expectedError.rawValue) + handleOnDidProvideExpectation.fulfill() } - return actonComponentDelegateMock + try singleAppFlow( + delegate: delegate, + expectedTwintError: expectedError + ) + + wait( + for: [handleOnDidProvideExpectation], + timeout: 1, + enforceOrder: true + ) } +} + +// MARK: - Flow + +private extension TwintSDKActionTests { - func testSuccessFlow() throws { + func singleAppFlow( + delegate: ActionComponentDelegate, + expectedTwintError: TWErrorCode + ) throws { let fetchBlockExpectation = expectation(description: "Fetch was called") let payBlockExpectation = expectation(description: "Pay was called") let handleOpenBlockExpectation = expectation(description: "Handle open was called") - let handleOnDidProvideExpectation = expectation(description: "delegate.onDidProvide was called") let expectedRedirectUrl = URL(string: "ui-host://payment")! var twintResponseHandler: ((Error?) -> Void)? = nil @@ -59,10 +94,13 @@ extension TwintSDKActionTests { handleOpenBlockExpectation.fulfill() return true } + + let presentationDelegate = Self.failingPresentationDelegateMock() let twintActionComponent = Self.actionComponent( with: twintSpy, - presentationDelegate: Self.failingPresentationDelegateMock() + presentationDelegate: presentationDelegate, + delegate: delegate ) // When @@ -77,7 +115,7 @@ extension TwintSDKActionTests { enforceOrder: true ) - _ = try RedirectListener.applicationDidOpen(from: expectedRedirectUrl) + try XCTAssertTrue(RedirectListener.applicationDidOpen(from: expectedRedirectUrl)) wait( for: [handleOpenBlockExpectation], @@ -85,13 +123,7 @@ extension TwintSDKActionTests { ) let responseHandler = try XCTUnwrap(twintResponseHandler) - responseHandler(NSError(domain: "", code: TWErrorCode.B_SUCCESS.rawValue)) - - wait( - for: [handleOnDidProvideExpectation], - timeout: 1, - enforceOrder: true - ) + responseHandler(NSError(domain: "", code: expectedTwintError.rawValue)) } } diff --git a/Tests/Actions Tests/ActionComponent/Twint/TwintSDKActionTests.swift b/Tests/Actions Tests/ActionComponent/Twint/TwintSDKActionTests.swift index 39cf5cc66a..50ab0f1775 100644 --- a/Tests/Actions Tests/ActionComponent/Twint/TwintSDKActionTests.swift +++ b/Tests/Actions Tests/ActionComponent/Twint/TwintSDKActionTests.swift @@ -16,12 +16,48 @@ import TwintSDK final class TwintSDKActionTests: XCTestCase { + override func tearDownWithError() throws { + AdyenAssertion.listener = nil + } + + func testConfiguration() throws { + + let validSchemes = [ + "scheme" + ] + + let invalidSchemes = [ + "scheme:", + "scheme://", + "scheme://host" + ] + + // Valid Configuration + + validSchemes.forEach { scheme in + AdyenAssertion.listener = { message in + XCTFail("No assertion should have been raised") + } + + let _ = AdyenActionComponent.Configuration.Twint(callbackAppScheme: scheme) + } + + // Invalid Configuration + + invalidSchemes.forEach { scheme in + AdyenAssertion.listener = { message in + XCTAssertEqual(message, "Format of provided callbackAppScheme '\(scheme)' is incorrect.") + } + + let _ = AdyenActionComponent.Configuration.Twint(callbackAppScheme: scheme) + } + } + func testNoAppFound() throws { let fetchBlockExpectation = expectation(description: "Fetch was called") let noAppFoundAlertExpectation = expectation(description: "No app found alert was shown") - // TODO: Get the message from the localizations let expectedAlertMessage = "No or an outdated version of TWINT is installed on this device. Please update or install the TWINT app." let twintSpy = TwintSpy { configurationsBlock in @@ -93,10 +129,12 @@ final class TwintSDKActionTests: XCTestCase { XCTFail("Handle open should not have been called") return false } + + let presentationDelegate = Self.failingPresentationDelegateMock() let twintActionComponent = Self.actionComponent( with: twintSpy, - presentationDelegate: Self.failingPresentationDelegateMock(), + presentationDelegate: presentationDelegate, delegate: nil ) @@ -153,7 +191,7 @@ final class TwintSDKActionTests: XCTestCase { let presentationDelegateMock = PresentationDelegateMock() presentationDelegateMock.doPresent = { component in let alertController = try XCTUnwrap(component.viewController as? UIAlertController) - XCTAssertEqual(alertController, expectedAppPicker) + XCTAssertTrue(alertController === expectedAppPicker) pickerExpectation.fulfill() } @@ -197,11 +235,51 @@ final class TwintSDKActionTests: XCTestCase { } func testPayError() throws { - // TODO: handlePay returns an error - } - - func testFailureFlow() throws { - // TODO: responseHandler returns an !B_SUCCESS error + + let fetchBlockExpectation = expectation(description: "Fetch was called") + let payBlockExpectation = expectation(description: "Pay was called") + let alertExpectation = expectation(description: "Alert was shown") + + let expectedAlertMessage = "Error Message" + + let twintSpy = TwintSpy { configurationsBlock in + fetchBlockExpectation.fulfill() + configurationsBlock([.dummy]) + } handlePay: { code, appConfiguration, callbackAppScheme in + payBlockExpectation.fulfill() + return MockError(errorDescription: expectedAlertMessage) + } handleController: { installedAppConfigurations, selectionHandler, cancelHandler in + XCTFail("Twint controller should not have been shown") + return nil + } handleOpenUrl: { url, responseHandler in + XCTFail("Handle open should not have been called") + return false + } + + let presentationDelegate = PresentationDelegateMock() + presentationDelegate.doPresent = { component in + let alertController = try XCTUnwrap(component.viewController as? UIAlertController) + XCTAssertEqual(alertController.message, expectedAlertMessage) + alertExpectation.fulfill() + } + + let twintActionComponent = Self.actionComponent( + with: twintSpy, + presentationDelegate: presentationDelegate, + delegate: nil + ) + + // When + + twintActionComponent.handle(.dummy) + + // Then + + wait( + for: [fetchBlockExpectation, payBlockExpectation, alertExpectation], + timeout: 1, + enforceOrder: true + ) } } diff --git a/Tests/Actions Tests/ActionComponent/Twint/TwintSDKActionTests+Configuration.swift b/Tests/Helpers/ErrorMock.swift similarity index 69% rename from Tests/Actions Tests/ActionComponent/Twint/TwintSDKActionTests+Configuration.swift rename to Tests/Helpers/ErrorMock.swift index c67d8b06c2..beb45b1c0b 100644 --- a/Tests/Actions Tests/ActionComponent/Twint/TwintSDKActionTests+Configuration.swift +++ b/Tests/Helpers/ErrorMock.swift @@ -5,3 +5,7 @@ // import Foundation + +struct MockError: LocalizedError { + var errorDescription: String? +}