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

PP-138 alternative token source #703

Merged
merged 7 commits into from
Nov 22, 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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@

import Foundation

public final class Token {

public final class Token: Hashable {
var expiration: Date
var scope: String?
var type: String?
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand All @@ -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
}
ValentinaIancu-Gini marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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?,
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Token, GiniError>) -> 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<OnboardingScreenEventType>) {
}

func onCameraScreenEvent(event: Event<CameraScreenEventType>) {
}

func onReviewScreenEvent(event: Event<ReviewScreenEventType>) {
}

func onAnalysisScreenEvent(event: Event<AnalysisScreenEventType>) {
}
}

final class NetworkingScreenApiCoordinatorTests: XCTestCase {
ValentinaIancu-Gini marked this conversation as resolved.
Show resolved Hide resolved
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
}
}
Loading