diff --git a/BankAPILibrary/GiniBankAPILibrary/Sources/GiniBankAPILibrary/Documents/APIResource.swift b/BankAPILibrary/GiniBankAPILibrary/Sources/GiniBankAPILibrary/Documents/APIResource.swift index 952fb6e79..5cd4e0188 100644 --- a/BankAPILibrary/GiniBankAPILibrary/Sources/GiniBankAPILibrary/Documents/APIResource.swift +++ b/BankAPILibrary/GiniBankAPILibrary/Sources/GiniBankAPILibrary/Documents/APIResource.swift @@ -13,7 +13,7 @@ public enum APIDomain { /// A custom domain with optional path and custom token source case custom(domain: String, path: String? = nil, tokenSource: AlternativeTokenSource?) - var domainString: String { + public var domainString: String { switch self { case .default: return "pay-api.gini.net" case .custom(let domain, _, _): return domain diff --git a/BankAPILibrary/GiniBankAPILibrary/Sources/GiniBankAPILibrary/Documents/Core/Auth/Token.swift b/BankAPILibrary/GiniBankAPILibrary/Sources/GiniBankAPILibrary/Documents/Core/Auth/Token.swift index 34e177a9f..e002bba6e 100644 --- a/BankAPILibrary/GiniBankAPILibrary/Sources/GiniBankAPILibrary/Documents/Core/Auth/Token.swift +++ b/BankAPILibrary/GiniBankAPILibrary/Sources/GiniBankAPILibrary/Documents/Core/Auth/Token.swift @@ -7,8 +7,7 @@ import Foundation -public final class Token { - +public final class Token: Hashable { var expiration: Date var scope: String? var type: String? @@ -28,6 +27,16 @@ public final class Token { case accessToken = "access_token" } + public static func == (lhs: Token, rhs: Token) -> Bool { + lhs.hashValue == rhs.hashValue + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(expiration) + hasher.combine(scope) + hasher.combine(type) + hasher.combine(accessToken) + } } extension Token: Decodable { diff --git a/BankAPILibrary/GiniBankAPILibrary/Sources/GiniBankAPILibrary/Documents/Core/GiniBankAPI.swift b/BankAPILibrary/GiniBankAPILibrary/Sources/GiniBankAPILibrary/Documents/Core/GiniBankAPI.swift index e1014049f..b199b2ff5 100644 --- a/BankAPILibrary/GiniBankAPILibrary/Sources/GiniBankAPILibrary/Documents/Core/GiniBankAPI.swift +++ b/BankAPILibrary/GiniBankAPILibrary/Sources/GiniBankAPILibrary/Documents/Core/GiniBankAPI.swift @@ -92,7 +92,7 @@ extension GiniBankAPI { /** * Creates a Gini Bank API Library to be used with a transparent proxy and a custom api access token source. */ - public init(customApiDomain: String, + public init(customApiDomain: String = APIDomain.default.domainString, alternativeTokenSource: AlternativeTokenSource, logLevel: LogLevel = .none, sessionDelegate: URLSessionDelegate? = nil) { @@ -134,6 +134,7 @@ extension GiniBankAPI { } private func save(_ client: Client) { + guard !runningUnitTests() else { return } do { try KeychainStore().save(item: KeychainManagerItem(key: .clientId, value: client.id, @@ -149,5 +150,12 @@ extension GiniBankAPI { "Check that the Keychain capability is enabled in your project") } } + + private func runningUnitTests() -> Bool { + #if canImport(XCTest) + return ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil + #endif + return false + } } } diff --git a/BankAPILibrary/GiniBankAPILibraryExample/GiniBankAPILibraryExampleTests/IntegrationTests.swift b/BankAPILibrary/GiniBankAPILibraryExample/GiniBankAPILibraryExampleTests/IntegrationTests.swift index caf3a6696..7cc7c5daf 100644 --- a/BankAPILibrary/GiniBankAPILibraryExample/GiniBankAPILibraryExampleTests/IntegrationTests.swift +++ b/BankAPILibrary/GiniBankAPILibraryExample/GiniBankAPILibraryExampleTests/IntegrationTests.swift @@ -75,16 +75,16 @@ class IntegrationTests: XCTestCase { } func testResolvePaymentRequest(){ - let expect = expectation(description: "it resolves the payment request") + let message = "You can't resolve the previously resolved payment request" + let expect = expectation(description: message) let paymentService = giniBankAPILib.paymentService() paymentService.resolvePaymentRequest(id: paymentRequestID, recipient: "Dr. med. Hackler", iban: "DE13760700120500154000", bic: "", amount: "335.50:EUR", purpose: "ReNr AZ356789Z"){ result in switch result { case .success(let resolvedRequest): - XCTAssertEqual(resolvedRequest.links?.payment, "https://pay-api.gini.net/paymentRequests/a6466506-acf1-4896-94c8-9b398d4e0ee1/payment") - expect.fulfill() + XCTFail(message) case .failure(let error): - XCTFail(String(describing: error)) + expect.fulfill() } } wait(for: [expect], timeout: 10) diff --git a/BankAPILibrary/GiniBankAPILibraryPinningExample/GiniBankAPILibraryPinningExampleTests/GiniBankAPILibraryPinningIntegrationTests.swift b/BankAPILibrary/GiniBankAPILibraryPinningExample/GiniBankAPILibraryPinningExampleTests/GiniBankAPILibraryPinningIntegrationTests.swift index 2e4d17423..faca539f4 100644 --- a/BankAPILibrary/GiniBankAPILibraryPinningExample/GiniBankAPILibraryPinningExampleTests/GiniBankAPILibraryPinningIntegrationTests.swift +++ b/BankAPILibrary/GiniBankAPILibraryPinningExample/GiniBankAPILibraryPinningExampleTests/GiniBankAPILibraryPinningIntegrationTests.swift @@ -89,16 +89,16 @@ class PinningIntegrationTests: XCTestCase { } func testResolvePaymentRequest(){ - let expect = expectation(description: "it resolves the payment request") + let message = "You can't resolve the previously resolved payment request" + let expect = expectation(description: message) let paymentService = giniBankAPILib.paymentService() paymentService.resolvePaymentRequest(id: paymentRequestID, recipient: "Dr. med. Hackler", iban: "DE13760700120500154000", bic: "", amount: "335.50:EUR", purpose: "ReNr AZ356789Z"){ result in switch result { - case .success(let resolvedRequest): - XCTAssertEqual(resolvedRequest.links?.payment, "https://pay-api.gini.net/paymentRequests/a6466506-acf1-4896-94c8-9b398d4e0ee1/payment") - expect.fulfill() - case .failure(let error): - XCTFail(String(describing: error)) + case .success(let resolvedRequest): + XCTFail(message) + case .failure(let error): + expect.fulfill() } } wait(for: [expect], timeout: 10) diff --git a/BankSDK/GiniBankSDK/Sources/GiniBankSDK/Core/GiniBankNetworkingScreenApiCoordinator.swift b/BankSDK/GiniBankSDK/Sources/GiniBankSDK/Core/GiniBankNetworkingScreenApiCoordinator.swift index 178a366a7..2dcb5d8bb 100644 --- a/BankSDK/GiniBankSDK/Sources/GiniBankSDK/Core/GiniBankNetworkingScreenApiCoordinator.swift +++ b/BankSDK/GiniBankSDK/Sources/GiniBankSDK/Core/GiniBankNetworkingScreenApiCoordinator.swift @@ -151,6 +151,24 @@ open class GiniBankNetworkingScreenApiCoordinator: GiniScreenAPICoordinator, Gin self.trackingDelegate = trackingDelegate } + private init(resultsDelegate: GiniCaptureResultsDelegate, + configuration: GiniBankConfiguration, + documentMetadata: Document.Metadata?, + trackingDelegate: GiniCaptureTrackingDelegate?, + lib: GiniBankAPI) { + documentService = DocumentService(lib: lib, metadata: documentMetadata) + configurationService = lib.configurationService() + let captureConfiguration = configuration.captureConfiguration() + super.init(withDelegate: nil, giniConfiguration: captureConfiguration) + + visionDelegate = self + GiniBank.setConfiguration(configuration) + giniBankConfiguration = configuration + giniBankConfiguration.documentService = documentService + self.resultsDelegate = resultsDelegate + self.trackingDelegate = trackingDelegate + } + public init(resultsDelegate: GiniCaptureResultsDelegate, configuration: GiniBankConfiguration, documentMetadata: Document.Metadata?, @@ -193,6 +211,22 @@ open class GiniBankNetworkingScreenApiCoordinator: GiniScreenAPICoordinator, Gin lib: lib) } + convenience init(alternativeTokenSource tokenSource: AlternativeTokenSource, + resultsDelegate: GiniCaptureResultsDelegate, + configuration: GiniBankConfiguration, + documentMetadata: Document.Metadata?, + trackingDelegate: GiniCaptureTrackingDelegate?) { + let lib = GiniBankAPI + .Builder(alternativeTokenSource: tokenSource) + .build() + + self.init(resultsDelegate: resultsDelegate, + configuration: configuration, + documentMetadata: documentMetadata, + trackingDelegate: trackingDelegate, + lib: lib) + } + private func deliver(result: ExtractionResult, analysisDelegate: AnalysisDelegate) { let hasExtractions = result.extractions.count > 0 diff --git a/BankSDK/GiniBankSDK/Sources/GiniBankSDK/Core/Networking/GiniBank+Networking.swift b/BankSDK/GiniBankSDK/Sources/GiniBankSDK/Core/Networking/GiniBank+Networking.swift index 566927e8b..e3f5d28ca 100644 --- a/BankSDK/GiniBankSDK/Sources/GiniBankSDK/Core/Networking/GiniBank+Networking.swift +++ b/BankSDK/GiniBankSDK/Sources/GiniBankSDK/Core/Networking/GiniBank+Networking.swift @@ -19,6 +19,7 @@ extension GiniBank { all screens and transitions out of the box, including the networking. - parameter client: `GiniClient` with the information needed to enable document analysis + - parameter importedDocuments: There should be either images or one PDF, and they should be validated before calling this method. - parameter resultsDelegate: Results delegate object where you can get the results of the analysis. - parameter configuration: The configuration to set. - parameter documentMetadata: Additional HTTP headers to send when uploading documents @@ -46,6 +47,34 @@ extension GiniBank { return screenCoordinator.startSDK(withDocuments: importedDocuments) } + /** + Returns a view controller which will handle the analysis process. + It's the easiest way to get started with the Gini Bank SDK as it comes pre-configured and handles + all screens and transitions out of the box, including the networking. + + - parameter tokenSource: Alternative token source + - parameter importedDocuments: There should be either images or one PDF, and they should be validated before calling this method. + - parameter resultsDelegate: Results delegate object where you can get the results of the analysis. + - parameter configuration: The configuration to set. + - parameter documentMetadata: Additional HTTP headers to send when uploading documents + - parameter trackingDelegate: A delegate object to receive user events + + - returns: A presentable view controller. + */ + public class func viewController(withAlternativeTokenSource tokenSource: AlternativeTokenSource, + importedDocuments: [GiniCaptureDocument]? = nil, + configuration: GiniBankConfiguration, + resultsDelegate: GiniCaptureResultsDelegate, + documentMetadata: Document.Metadata? = nil, + trackingDelegate: GiniCaptureTrackingDelegate? = nil) -> UIViewController { + let screenCoordinator = GiniBankNetworkingScreenApiCoordinator(alternativeTokenSource: tokenSource, + resultsDelegate: resultsDelegate, + configuration: configuration, + documentMetadata: documentMetadata, + trackingDelegate: trackingDelegate) + return screenCoordinator.startSDK(withDocuments: importedDocuments) + } + // MARK: - Screen API with Custom Networking - Initializers for 'UIViewController' /** diff --git a/BankSDK/GiniBankSDK/Tests/GiniBankSDKTests/NetworkingScreenApiCoordinatorTests.swift b/BankSDK/GiniBankSDK/Tests/GiniBankSDKTests/NetworkingScreenApiCoordinatorTests.swift new file mode 100644 index 000000000..b0f61e951 --- /dev/null +++ b/BankSDK/GiniBankSDK/Tests/GiniBankSDKTests/NetworkingScreenApiCoordinatorTests.swift @@ -0,0 +1,186 @@ +// +// NetworkingScreenApiCoordinatorTests.swift +// +// Copyright © 2024 Gini GmbH. All rights reserved. +// + +@testable import GiniBankAPILibrary +@testable import GiniBankSDK +@testable import GiniCaptureSDK +import XCTest + +private class MockTokenSource: AlternativeTokenSource { + var token: Token? + init(token: Token? = nil) { + self.token = token + } + func fetchToken(completion: @escaping (Result) -> Void) { + if let token { + completion(.success(token)) + } else { + completion(.failure(.requestCancelled)) + } + } +} + +private class MockCaptureResultsDelegate: GiniCaptureResultsDelegate { + func giniCaptureAnalysisDidFinishWith(result: AnalysisResult) { + } + + func giniCaptureDidCancelAnalysis() { + } + + func giniCaptureDidEnterManually() { + } +} + +private class MockTrackingDelegate: GiniCaptureTrackingDelegate { + func onOnboardingScreenEvent(event: Event) { + } + + func onCameraScreenEvent(event: Event) { + } + + func onReviewScreenEvent(event: Event) { + } + + func onAnalysisScreenEvent(event: Event) { + } +} + +final class NetworkingScreenApiCoordinatorTests: XCTestCase { + private var tokenSource: MockTokenSource! + private var resultsDelegate: MockCaptureResultsDelegate! + private var configuration: GiniBankConfiguration! + private var metadata: Document.Metadata! + private var trackingDelegate: MockTrackingDelegate! + + override func setUp() { + tokenSource = makeTokenSource() + resultsDelegate = MockCaptureResultsDelegate() + configuration = GiniBankConfiguration() + metadata = Document.Metadata(branchId: "branch") + trackingDelegate = MockTrackingDelegate() + } + + func testInitWithAlternativeTokenSource() throws { + let (coordinator, service) = try makeCoordinatorAndService() + + // check domain + XCTAssertEqual(service.apiDomain.domainString, "pay-api.gini.net", "Service api domain should match our default") + + // check token + let receivedToken = try XCTUnwrap( + login(service: service), + "Should log in successfully" + ) + XCTAssertEqual(receivedToken, tokenSource.token, "Received token should match the expected token") + + // check for delegates/configs + XCTAssertNotNil( + coordinator.resultsDelegate as? MockCaptureResultsDelegate, + "Coordinator should have correct results delegate instance" + ) + XCTAssertEqual(coordinator.giniBankConfiguration, configuration, "Coordinator should have correct configuration instance") + XCTAssertNotNil( + coordinator.trackingDelegate as? MockTrackingDelegate, + "Coordinator should have correct tracking delegate instance" + ) + XCTAssertEqual(coordinator.documentService.metadata?.headers, metadata.headers, "Metadata headers should match") + } + + func testViewControllerWithAlternativeTokenSource() throws { + let (coordinator, service) = try makeCoordinatorAndService(fromViewController: true) + + // check domain + XCTAssertEqual(service.apiDomain.domainString, "pay-api.gini.net", "Service api domain should match our default") + + // check token + let receivedToken = try XCTUnwrap( + login(service: service), + "Should log in successfully" + ) + XCTAssertEqual(receivedToken, tokenSource.token, "Received token should match the expected token") + + // check for delegates/configs + XCTAssertNotNil( + coordinator.resultsDelegate as? MockCaptureResultsDelegate, + "Coordinator should have correct results delegate instance" + ) + XCTAssertEqual(coordinator.giniBankConfiguration, configuration, "Coordinator should have correct configuration instance") + XCTAssertNotNil( + coordinator.trackingDelegate as? MockTrackingDelegate, + "Coordinator should have correct tracking delegate instance" + ) + XCTAssertEqual(coordinator.documentService.metadata?.headers, metadata.headers, "Metadata headers should match") + } +} + +private extension NetworkingScreenApiCoordinatorTests { + func makeTokenSource() -> MockTokenSource { + MockTokenSource( + token: + Token( + expiration: .init(), + scope: "the_scope", + type: "the_type", + accessToken: "some_totally_random_gibberish" + ) + ) + } + + func makeCoordinatorAndService(fromViewController: Bool = false) throws -> (GiniBankNetworkingScreenApiCoordinator, DefaultDocumentService) { + let coordinator: GiniBankNetworkingScreenApiCoordinator + if fromViewController { + let viewController = try XCTUnwrap( + GiniBank.viewController( + withAlternativeTokenSource: tokenSource, + configuration: configuration, + resultsDelegate: resultsDelegate, + documentMetadata: metadata, + trackingDelegate: trackingDelegate + ) as? ContainerNavigationController, + "There should be an instance of `ContainerNavigationController`" + ) + coordinator = try XCTUnwrap( + viewController.coordinator as? GiniBankNetworkingScreenApiCoordinator, + "The instance of `ContainerNavigationController` should have a coordinator of type `GiniBankNetworkingScreenApiCoordinator" + ) + } else { + coordinator = GiniBankNetworkingScreenApiCoordinator( + alternativeTokenSource: tokenSource, + resultsDelegate: resultsDelegate, + configuration: configuration, + documentMetadata: metadata, + trackingDelegate: trackingDelegate + ) + } + let documentService = try XCTUnwrap( + coordinator.documentService as? GiniCaptureSDK.DocumentService, + "The coordinator should have a document service of type `GiniCaptureSDK.DocumentService" + ) + let captureNetworkService = try XCTUnwrap( + documentService.captureNetworkService as? DefaultCaptureNetworkService, + "The document service should have a capture network service of type `DefaultCaptureNetworkService" + ) + + + return (coordinator, captureNetworkService.documentService) + } + + func login(service: DefaultDocumentService) throws -> Token? { + let logInExpectation = self.expectation(description: "login") + var receivedToken: Token? + service.sessionManager.logIn { result in + switch result { + case .success(let token): + receivedToken = token + logInExpectation.fulfill() + case .failure(let error): + XCTFail("Failure: \(error.localizedDescription)") + } + } + wait(for: [logInExpectation], timeout: 1) + return receivedToken + } +}