IDPROD-2742 part 4: Upload ID Document Images (#492)
Now Identity document images are uploaded to our file upload endpoint as soon as the images are captured. When the user hits "continue" to finish the final step in document capture, the ViewController will: 1. Wait until the images are finished uploading (if they haven't finished yet) 2. Update the data store with the images and the resulting file ID 3. Save the IDs of the uploaded files to the `VerificationSessionData` endpoint #### Saving Images When testing on the simulator, I observed that it takes ~7 seconds for the files to finish uploading. We likely will need to downscale the images prior to uploading so it doesn't take so long (part of IDPROD-2482). We begin to upload the image as soon as we get a valid image capture from the camera feed, before the user confirms the image. In the event that the images are not done uploading by the time the user hits the final "Continue" button to go to the next screen, I added a `.saving` state to the view controller that will disable the button until it's done uploading and saving the data. We'll need to add a better loading state when we cleanup the UI to match the design (IDPROD-2756). #### Misc - Also added a `StripeFileMock` to `StripeCoreTestingUtils` to mock a file upload response. - Updated `VerificationSheetControllerTest` to load JSON mocks during setup as opposed to at the top of the file, since this effects our test env
This commit is contained in:
parent
8fd90116ca
commit
c85696c593
|
@ -56,6 +56,7 @@
|
|||
E66784B126980677005F7CC8 /* StripeCoreBundleLocator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E66784AF26980677005F7CC8 /* StripeCoreBundleLocator.swift */; };
|
||||
E66784B226980677005F7CC8 /* BundleLocatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E66784B026980677005F7CC8 /* BundleLocatorProtocol.swift */; };
|
||||
E6752D7826F413A00062B821 /* String+StripeCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6752D7726F413A00062B821 /* String+StripeCore.swift */; };
|
||||
E67A1E632740525500977F63 /* File_IdentityDocument.json in Resources */ = {isa = PBXBuildFile; fileRef = E67A1E6027404FD500977F63 /* File_IdentityDocument.json */; };
|
||||
E681E5662698EC9700692E45 /* NSError+StripeCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E681E5652698EC9700692E45 /* NSError+StripeCore.swift */; };
|
||||
E69D640526855B260090B43D /* StripeCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E69D63FB26855B250090B43D /* StripeCore.framework */; };
|
||||
E69D640C26855B260090B43D /* StripeCore.h in Headers */ = {isa = PBXBuildFile; fileRef = E69D63FE26855B250090B43D /* StripeCore.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
|
@ -192,6 +193,7 @@
|
|||
E66784AF26980677005F7CC8 /* StripeCoreBundleLocator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StripeCoreBundleLocator.swift; sourceTree = "<group>"; };
|
||||
E66784B026980677005F7CC8 /* BundleLocatorProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BundleLocatorProtocol.swift; sourceTree = "<group>"; };
|
||||
E6752D7726F413A00062B821 /* String+StripeCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+StripeCore.swift"; sourceTree = "<group>"; };
|
||||
E67A1E6027404FD500977F63 /* File_IdentityDocument.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = File_IdentityDocument.json; sourceTree = "<group>"; };
|
||||
E681E5652698EC9700692E45 /* NSError+StripeCore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSError+StripeCore.swift"; sourceTree = "<group>"; };
|
||||
E69D63FB26855B250090B43D /* StripeCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = StripeCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
E69D63FE26855B250090B43D /* StripeCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = StripeCore.h; sourceTree = "<group>"; };
|
||||
|
@ -393,6 +395,14 @@
|
|||
name = "Recovered References";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E67A1E5F27404FB500977F63 /* Mock Files */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E67A1E6027404FD500977F63 /* File_IdentityDocument.json */,
|
||||
);
|
||||
path = "Mock Files";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E69D63F126855B250090B43D = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -493,9 +503,10 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
E6548EDE2728AEBE00F399B2 /* APIStubbedTestCase.swift */,
|
||||
E6FB9BA9268EA7BD000FDB4F /* Mocks */,
|
||||
E61ADAA2270B925D004ED998 /* Categories */,
|
||||
E6FB9BBB268EA95F000FDB4F /* Info.plist */,
|
||||
E67A1E5F27404FB500977F63 /* Mock Files */,
|
||||
E6FB9BA9268EA7BD000FDB4F /* Mocks */,
|
||||
E6FB9BBA268EA95F000FDB4F /* StripeCoreTestUtils.h */,
|
||||
E6548EE52728AFB400F399B2 /* TestConstants.swift */,
|
||||
E6548F7E27339FB100F399B2 /* XCTestCase+Stripe.swift */,
|
||||
|
@ -720,6 +731,7 @@
|
|||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E67A1E632740525500977F63 /* File_IdentityDocument.json in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
|
@ -93,12 +93,16 @@
|
|||
}
|
||||
|
||||
@_spi(STP) public class Promise<Value>: Future<Value> {
|
||||
public init(value: Value? = nil) {
|
||||
public override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
public convenience init(value: Value) {
|
||||
self.init()
|
||||
|
||||
// If the value was already known at the time the promise
|
||||
// was constructed, we can report it directly:
|
||||
result = value.map(Result.success)
|
||||
result = .success(value)
|
||||
}
|
||||
|
||||
public func resolve(with value: Value) {
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"created": 1636833390,
|
||||
"id": "file_id",
|
||||
"purpose": "identity_document",
|
||||
"size": 100,
|
||||
"type": "jpg"
|
||||
}
|
|
@ -33,3 +33,13 @@ public extension MockData {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dummy class to determine this bundle
|
||||
private class ClassForBundle { }
|
||||
|
||||
@_spi(STP) public enum FileMock: String, MockData {
|
||||
public typealias ResponseType = StripeFile
|
||||
public var bundle: Bundle { return Bundle(for: ClassForBundle.self) }
|
||||
|
||||
case identityDocument = "File_IdentityDocument"
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
@_spi(STP) import StripeCore
|
||||
|
||||
protocol IdentityAPIClient {
|
||||
|
@ -18,6 +19,11 @@ protocol IdentityAPIClient {
|
|||
updating verificationData: VerificationSessionDataUpdate,
|
||||
ephemeralKeySecret: String
|
||||
) -> Promise<VerificationSessionData>
|
||||
|
||||
func uploadImage(
|
||||
_ image: UIImage,
|
||||
purpose: StripeFile.Purpose
|
||||
) -> Promise<StripeFile>
|
||||
}
|
||||
|
||||
extension STPAPIClient: IdentityAPIClient {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
@_spi(STP) import StripeCore
|
||||
|
||||
/**
|
||||
|
@ -188,4 +189,11 @@ extension MockIdentityAPIClient: IdentityAPIClient {
|
|||
}
|
||||
)
|
||||
}
|
||||
|
||||
func uploadImage(
|
||||
_ image: UIImage,
|
||||
purpose: StripeFile.Purpose
|
||||
) -> Promise<StripeFile> {
|
||||
return STPAPIClient.shared.uploadImage(image, purpose: purpose)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ struct VerificationSheetAPIContent {
|
|||
/// Server response from the last time the user's data was saved
|
||||
private(set) var sessionData: VerificationSessionData? = nil
|
||||
|
||||
private(set) var lastError: Error? = nil
|
||||
var lastError: Error? = nil
|
||||
|
||||
/// Status of the associated VerificationSession.
|
||||
var status: VerificationPage.Status? {
|
||||
|
|
|
@ -17,6 +17,8 @@ protocol VerificationSheetControllerProtocol: AnyObject {
|
|||
clientSecret: String
|
||||
)
|
||||
|
||||
func uploadDocument(image: UIImage) -> Future<String>
|
||||
|
||||
func saveData(
|
||||
completion: @escaping (VerificationSheetAPIContent) -> Void
|
||||
)
|
||||
|
@ -105,4 +107,12 @@ final class VerificationSheetController: VerificationSheetControllerProtocol {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Uploads a document image and returns a Future containing the ID of the uploaded file
|
||||
func uploadDocument(image: UIImage) -> Future<String> {
|
||||
// TODO(mludowise|IDPROD-2482): Crop and downscale image for faster upload times
|
||||
return apiClient.uploadImage(image, purpose: .identityDocument).chained { file in
|
||||
return Promise(value: file.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,8 @@ final class DocumentCaptureViewController: IdentityFlowViewController {
|
|||
case scanning(DocumentScanner.Classification)
|
||||
/// Successfully scanned the camera feed for the specified classification
|
||||
case scanned(DocumentScanner.Classification, UIImage)
|
||||
/// Saving the captured data
|
||||
case saving(lastImage: UIImage)
|
||||
}
|
||||
|
||||
private(set) var state: State {
|
||||
|
@ -79,7 +81,9 @@ final class DocumentCaptureViewController: IdentityFlowViewController {
|
|||
state: .videoPreview,
|
||||
instructionalText: "Position your passport in the center of the frame"
|
||||
)
|
||||
case .scanned(_, let image):
|
||||
case .scanned(_, let image),
|
||||
.saving(let image):
|
||||
// TODO(mludowise|IDPROD-2756): Display some sort of loading indicator during "Saving" while we wait for the files to finish uploading
|
||||
return .init(
|
||||
state: .staticImage(image, contentMode: .scaleAspectFill),
|
||||
instructionalText: "✓ Scanned"
|
||||
|
@ -107,6 +111,8 @@ final class DocumentCaptureViewController: IdentityFlowViewController {
|
|||
return true
|
||||
case .scanned:
|
||||
return false
|
||||
case .saving:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -133,9 +139,8 @@ final class DocumentCaptureViewController: IdentityFlowViewController {
|
|||
|
||||
// The captured front document images to be saved to the API when continuing
|
||||
// from this screen
|
||||
|
||||
var frontDocument: UIImage?
|
||||
var backDocument: UIImage?
|
||||
var frontUploadFuture: Future<VerificationSessionDataStore.DocumentImage?> = Promise(value: nil)
|
||||
var backUploadFuture: Future<VerificationSessionDataStore.DocumentImage?> = Promise(value: nil)
|
||||
|
||||
// MARK: Init
|
||||
|
||||
|
@ -208,22 +213,35 @@ extension DocumentCaptureViewController {
|
|||
}
|
||||
}
|
||||
|
||||
/// Starts uploading an image as soon as it's been scanned
|
||||
func handleScannedImage(pixelBuffer: CVPixelBuffer) {
|
||||
let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
|
||||
let uiImage = UIImage(ciImage: ciImage)
|
||||
|
||||
switch state {
|
||||
case .scanning(let classification):
|
||||
if classification.isFront {
|
||||
frontDocument = uiImage
|
||||
} else {
|
||||
backDocument = uiImage
|
||||
}
|
||||
state = .scanned(classification, uiImage)
|
||||
default:
|
||||
guard case let .scanning(classification) = state else {
|
||||
assertionFailure("state is '\(state)' but expected 'scanning'")
|
||||
return
|
||||
}
|
||||
|
||||
// Set state back to scanned when we're done
|
||||
defer {
|
||||
state = .scanned(classification, uiImage)
|
||||
}
|
||||
|
||||
guard let sheetController = sheetController else {
|
||||
return
|
||||
}
|
||||
|
||||
// Transform Future to return a `DocumentImage` containing the file ID and UIImage
|
||||
let imageUploadFuture: Future<VerificationSessionDataStore.DocumentImage?> = sheetController.uploadDocument(image: uiImage).chained { fileId in
|
||||
return Promise(value: .init(image: uiImage, fileId: fileId))
|
||||
}
|
||||
|
||||
if classification.isFront {
|
||||
frontUploadFuture = imageUploadFuture
|
||||
} else {
|
||||
backUploadFuture = imageUploadFuture
|
||||
}
|
||||
}
|
||||
|
||||
func didTapButton() {
|
||||
|
@ -231,26 +249,39 @@ extension DocumentCaptureViewController {
|
|||
case .interstitial(let classification):
|
||||
// TODO(mludowise|IDPROD-2775): Check camera permissions
|
||||
state = .scanning(classification)
|
||||
case .scanning:
|
||||
assertionFailure("Button should be disabled in state 'scanning'.")
|
||||
case .scanned(let classification, _):
|
||||
case .scanning,
|
||||
.saving:
|
||||
assertionFailure("Button should be disabled in state '\(state)'.")
|
||||
case .scanned(let classification, let image):
|
||||
if let nextClassification = classification.nextClassification {
|
||||
state = .interstitial(nextClassification)
|
||||
} else {
|
||||
saveDataAndTransition()
|
||||
state = .saving(lastImage: image)
|
||||
saveDataAndTransition(lastClassification: classification, lastImage: image)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func saveDataAndTransition() {
|
||||
// TODO: save image to uploads.stripe.com and use returned FileID
|
||||
// Blocked by https://github.com/stripe-ios/stripe-ios/pull/479
|
||||
sheetController?.dataStore.frontDocumentImage = frontDocument.map { .init(image: $0, fileId: "") }
|
||||
sheetController?.dataStore.backDocumentImage = backDocument.map { .init(image: $0, fileId: "") }
|
||||
sheetController?.saveData(completion: { [weak sheetController] apiContent in
|
||||
guard let sheetController = sheetController else { return }
|
||||
sheetController.flowController.transitionToNextScreen(apiContent: apiContent, sheetController: sheetController)
|
||||
})
|
||||
func saveDataAndTransition(lastClassification: DocumentScanner.Classification, lastImage: UIImage) {
|
||||
frontUploadFuture.chained { [weak self] frontImage in
|
||||
// Front upload is complete, update dataStore
|
||||
self?.sheetController?.dataStore.frontDocumentImage = frontImage
|
||||
return self?.backUploadFuture ?? Promise(value: nil)
|
||||
}.chained { [weak sheetController] (backImage: VerificationSessionDataStore.DocumentImage?) -> Future<()> in
|
||||
// Back upload is complete, update dataStore
|
||||
sheetController?.dataStore.backDocumentImage = backImage
|
||||
return Promise(value: ())
|
||||
}.observe { [weak self] _ in
|
||||
// Both front & back uploads are complete, save data
|
||||
guard let sheetController = self?.sheetController else { return }
|
||||
sheetController.saveData { apiContent in
|
||||
self?.state = .scanned(lastClassification, lastImage)
|
||||
sheetController.flowController.transitionToNextScreen(
|
||||
apiContent: apiContent,
|
||||
sheetController: sheetController
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
@_spi(STP) import StripeCore
|
||||
@testable import StripeIdentity
|
||||
|
||||
|
@ -13,6 +14,7 @@ final class IdentityAPIClientTestMock: IdentityAPIClient {
|
|||
|
||||
let verificationPage = MockAPIRequests<String, VerificationPage>()
|
||||
let verificationSessionData = MockAPIRequests<(id: String, data: VerificationSessionDataUpdate, ephemeralKey: String), VerificationSessionData>()
|
||||
let imageUpload = MockAPIRequests<(image: UIImage, purpose: StripeFile.Purpose), StripeFile>()
|
||||
|
||||
func postIdentityVerificationPage(clientSecret: String) -> Promise<VerificationPage> {
|
||||
return verificationPage.makeRequest(with: clientSecret)
|
||||
|
@ -29,6 +31,10 @@ final class IdentityAPIClientTestMock: IdentityAPIClient {
|
|||
ephemeralKey: ephemeralKeySecret
|
||||
))
|
||||
}
|
||||
|
||||
func uploadImage(_ image: UIImage, purpose: StripeFile.Purpose) -> Promise<StripeFile> {
|
||||
return imageUpload.makeRequest(with: (image, purpose))
|
||||
}
|
||||
}
|
||||
|
||||
class MockAPIRequests<ParamsType, ResponseType> {
|
||||
|
|
|
@ -6,6 +6,9 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
import UIKit
|
||||
@_spi(STP) import StripeCore
|
||||
@testable import StripeIdentity
|
||||
|
||||
final class VerificationSheetControllerMock: VerificationSheetControllerProtocol {
|
||||
|
@ -13,7 +16,9 @@ final class VerificationSheetControllerMock: VerificationSheetControllerProtocol
|
|||
let dataStore: VerificationSessionDataStore
|
||||
|
||||
private(set) var didLoadAndUpdateUI = false
|
||||
private(set) var didSaveData = false
|
||||
private(set) var didRequestSaveData = false
|
||||
private(set) var didFinishSaveDataExp = XCTestExpectation(description: "Saved data")
|
||||
private(set) var numUploadedImages = 0
|
||||
|
||||
init(
|
||||
flowController: VerificationSheetFlowControllerMock,
|
||||
|
@ -28,7 +33,15 @@ final class VerificationSheetControllerMock: VerificationSheetControllerProtocol
|
|||
}
|
||||
|
||||
func saveData(completion: @escaping (VerificationSheetAPIContent) -> Void) {
|
||||
didSaveData = true
|
||||
didRequestSaveData = true
|
||||
didFinishSaveDataExp.fulfill()
|
||||
completion(VerificationSheetAPIContent())
|
||||
}
|
||||
|
||||
func uploadDocument(
|
||||
image: UIImage
|
||||
) -> Future<String> {
|
||||
numUploadedImages += 1
|
||||
return Promise(value: "")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,17 +6,28 @@
|
|||
//
|
||||
|
||||
import XCTest
|
||||
import UIKit
|
||||
@_spi(STP) import StripeCore
|
||||
@_spi(STP) import StripeCoreTestUtils
|
||||
@testable import StripeIdentity
|
||||
|
||||
final class VerificationSheetControllerTest: XCTestCase {
|
||||
|
||||
let mockSecret = "secret_123"
|
||||
let mockStaticContent = try! VerificationPageMock.response200.make()
|
||||
static var mockStaticContent: VerificationPage!
|
||||
|
||||
private var controller: VerificationSheetController!
|
||||
private var mockAPIClient: IdentityAPIClientTestMock!
|
||||
private var exp: XCTestExpectation!
|
||||
|
||||
override class func setUp() {
|
||||
super.setUp()
|
||||
guard let mockStaticContent = try? VerificationPageMock.response200.make() else {
|
||||
return XCTFail("Could not load mock data")
|
||||
}
|
||||
self.mockStaticContent = mockStaticContent
|
||||
}
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
|
@ -26,7 +37,7 @@ final class VerificationSheetControllerTest: XCTestCase {
|
|||
exp = expectation(description: "Finished API call")
|
||||
}
|
||||
|
||||
func testValidVerificationPageResponse() throws {
|
||||
func testLoadValidResponse() throws {
|
||||
let mockResponse = try VerificationPageMock.response200.make()
|
||||
|
||||
// Load
|
||||
|
@ -53,7 +64,7 @@ final class VerificationSheetControllerTest: XCTestCase {
|
|||
XCTAssertNil(controller.apiContent.lastError)
|
||||
}
|
||||
|
||||
func testErrorVerificationPageResponse() throws {
|
||||
func testLoadErrorResponse() throws {
|
||||
let mockError = NSError(domain: "", code: 0, userInfo: nil)
|
||||
|
||||
// Load
|
||||
|
@ -72,14 +83,9 @@ final class VerificationSheetControllerTest: XCTestCase {
|
|||
XCTAssertNotNil(controller.apiContent.lastError)
|
||||
}
|
||||
|
||||
func testValidVerificationSessionDataResponse() throws {
|
||||
func testSaveDataValidResponse() throws {
|
||||
let mockResponse = try VerificationSessionDataMock.response200.make()
|
||||
|
||||
// Mock that a VerificationPage response has already been received
|
||||
controller.apiContent.setStaticContent(result: .success(mockStaticContent))
|
||||
|
||||
// Mock that the user has entered data
|
||||
controller.dataStore.biometricConsent = true
|
||||
setUpForSaveData()
|
||||
|
||||
// Save data
|
||||
controller.saveData { mutatedApiContent in
|
||||
|
@ -90,8 +96,8 @@ final class VerificationSheetControllerTest: XCTestCase {
|
|||
|
||||
// Verify 1 request made with Id, EAK, and collected data
|
||||
XCTAssertEqual(mockAPIClient.verificationSessionData.requestHistory.count, 1)
|
||||
XCTAssertEqual(mockAPIClient.verificationSessionData.requestHistory.first?.id, mockStaticContent.id)
|
||||
XCTAssertEqual(mockAPIClient.verificationSessionData.requestHistory.first?.ephemeralKey, mockStaticContent.ephemeralApiKey)
|
||||
XCTAssertEqual(mockAPIClient.verificationSessionData.requestHistory.first?.id, VerificationSheetControllerTest.mockStaticContent.id)
|
||||
XCTAssertEqual(mockAPIClient.verificationSessionData.requestHistory.first?.ephemeralKey, VerificationSheetControllerTest.mockStaticContent.ephemeralApiKey)
|
||||
XCTAssertEqual(mockAPIClient.verificationSessionData.requestHistory.first?.data, controller.dataStore.toAPIModel)
|
||||
|
||||
// Verify response & error are nil until API responds to request
|
||||
|
@ -109,14 +115,9 @@ final class VerificationSheetControllerTest: XCTestCase {
|
|||
XCTAssertNil(controller.apiContent.lastError)
|
||||
}
|
||||
|
||||
func testErrorVerificationSessionDataResponse() throws {
|
||||
func testSaveDataErrorResponse() throws {
|
||||
let mockError = NSError(domain: "", code: 0, userInfo: nil)
|
||||
|
||||
// Mock that a VerificationPage response has already been received
|
||||
controller.apiContent.setStaticContent(result: .success(mockStaticContent))
|
||||
|
||||
// Mock that the user has entered data
|
||||
controller.dataStore.biometricConsent = true
|
||||
setUpForSaveData()
|
||||
|
||||
// Save data
|
||||
controller.saveData { mutatedApiContent in
|
||||
|
@ -125,7 +126,7 @@ final class VerificationSheetControllerTest: XCTestCase {
|
|||
self.exp.fulfill()
|
||||
}
|
||||
|
||||
// Respond to request with success
|
||||
// Respond to request with failure
|
||||
mockAPIClient.verificationSessionData.respondToRequests(with: .failure(mockError))
|
||||
|
||||
// Verify completion block is called
|
||||
|
@ -135,4 +136,42 @@ final class VerificationSheetControllerTest: XCTestCase {
|
|||
XCTAssertNil(controller.apiContent.sessionData)
|
||||
XCTAssertNotNil(controller.apiContent.lastError)
|
||||
}
|
||||
|
||||
func testUploadDocument() throws {
|
||||
let mockImage = UIImage()
|
||||
let mockResponse = try FileMock.identityDocument.make()
|
||||
|
||||
let uploadPromise = controller.uploadDocument(image: mockImage)
|
||||
|
||||
uploadPromise.observe { [weak self] result in
|
||||
switch result {
|
||||
case .success(let response):
|
||||
XCTAssertEqual(response, mockResponse.id)
|
||||
case .failure(let error):
|
||||
XCTFail("Expected success but instead found error \(error)")
|
||||
}
|
||||
self?.exp.fulfill()
|
||||
}
|
||||
|
||||
// Verify API request is made
|
||||
XCTAssertEqual(mockAPIClient.imageUpload.requestHistory.count, 1)
|
||||
XCTAssertEqual(mockAPIClient.imageUpload.requestHistory.first?.image, mockImage)
|
||||
XCTAssertEqual(mockAPIClient.imageUpload.requestHistory.first?.purpose, .identityDocument)
|
||||
|
||||
// Respond to request with success
|
||||
mockAPIClient.imageUpload.respondToRequests(with: .success(mockResponse))
|
||||
|
||||
// Verify completion block is called
|
||||
wait(for: [exp], timeout: 1)
|
||||
}
|
||||
}
|
||||
|
||||
private extension VerificationSheetControllerTest {
|
||||
func setUpForSaveData() {
|
||||
// Mock that a VerificationPage response has already been received
|
||||
controller.apiContent.setStaticContent(result: .success(VerificationSheetControllerTest.mockStaticContent))
|
||||
|
||||
// Mock that the user has entered data
|
||||
controller.dataStore.biometricConsent = true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -81,6 +81,8 @@ final class DocumentCaptureViewControllerTest: XCTestCase {
|
|||
expectedState: .scanned(.idCardFront, UIImage()),
|
||||
isButtonDisabled: false
|
||||
)
|
||||
// Verify image started uploading
|
||||
XCTAssertNotNil(vc.frontUploadFuture)
|
||||
}
|
||||
|
||||
func testTransitionFromScannedCardFront() {
|
||||
|
@ -116,12 +118,32 @@ final class DocumentCaptureViewControllerTest: XCTestCase {
|
|||
expectedState: .scanned(.idCardBack, UIImage()),
|
||||
isButtonDisabled: false
|
||||
)
|
||||
// Verify image started uploading
|
||||
XCTAssertNotNil(vc.backUploadFuture)
|
||||
}
|
||||
|
||||
func testTransitionFromScannedCardBack() {
|
||||
// NOTE: Setting mock upload promises so that we can test the VC state
|
||||
// before `saveDataAndTransition` finishes and the state is reset to
|
||||
// `scanned`, otherwise the promises will resolve immediately
|
||||
let mockFrontUploadFuture = Promise<VerificationSessionDataStore.DocumentImage?>()
|
||||
let mockBackUploadFuture = Promise<VerificationSessionDataStore.DocumentImage?>()
|
||||
|
||||
let vc = makeViewController(state: .scanned(.idCardBack, UIImage()))
|
||||
vc.frontUploadFuture = mockFrontUploadFuture
|
||||
vc.backUploadFuture = mockBackUploadFuture
|
||||
vc.didTapButton()
|
||||
wait(for: [mockFlowController.didTransitionToNextScreenExp], timeout: 1)
|
||||
verify(
|
||||
vc,
|
||||
expectedState: .saving(lastImage: UIImage()),
|
||||
isButtonDisabled: true
|
||||
)
|
||||
// Mock that upload finishes
|
||||
mockFrontUploadFuture.resolve(with: nil)
|
||||
mockBackUploadFuture.resolve(with: nil)
|
||||
|
||||
wait(for: [mockSheetController.didFinishSaveDataExp], timeout: 1)
|
||||
XCTAssertTrue(mockSheetController.didRequestSaveData)
|
||||
}
|
||||
|
||||
func testInitialStatePassport() {
|
||||
|
@ -156,30 +178,58 @@ final class DocumentCaptureViewControllerTest: XCTestCase {
|
|||
expectedState: .scanned(.passport, UIImage()),
|
||||
isButtonDisabled: false
|
||||
)
|
||||
// Verify image started uploading
|
||||
XCTAssertNotNil(vc.frontUploadFuture)
|
||||
}
|
||||
|
||||
func testTransitionFromScannedPassport() {
|
||||
// NOTE: Setting mock upload promise so that we can test the VC state
|
||||
// before `saveDataAndTransition` finishes and the state is reset to
|
||||
// `scanned`, otherwise the promises will resolve immediately
|
||||
let mockFrontUploadFuture = Promise<VerificationSessionDataStore.DocumentImage?>()
|
||||
|
||||
let vc = makeViewController(state: .scanned(.passport, UIImage()))
|
||||
vc.frontUploadFuture = mockFrontUploadFuture
|
||||
vc.didTapButton()
|
||||
wait(for: [mockFlowController.didTransitionToNextScreenExp], timeout: 1)
|
||||
verify(
|
||||
vc,
|
||||
expectedState: .saving(lastImage: UIImage()),
|
||||
isButtonDisabled: true
|
||||
)
|
||||
// Mock that upload finishes
|
||||
mockFrontUploadFuture.resolve(with: nil)
|
||||
|
||||
wait(for: [mockSheetController.didFinishSaveDataExp], timeout: 1)
|
||||
XCTAssertTrue(mockSheetController.didRequestSaveData)
|
||||
}
|
||||
|
||||
func testSaveDataAndTransition() {
|
||||
// TODO: Test that image is uploaded
|
||||
// Blocked by https://github.com/stripe-ios/stripe-ios/pull/479
|
||||
|
||||
let mockFrontImage = UIImage()
|
||||
let mockBackImage = UIImage()
|
||||
let mockFrontImage = VerificationSessionDataStore.DocumentImage(image: UIImage(), fileId: "front_id")
|
||||
let mockBackImage = VerificationSessionDataStore.DocumentImage(image: UIImage(), fileId: "back_id")
|
||||
let mockLastClassification = DocumentScanner.Classification.idCardBack
|
||||
|
||||
// Mock that file has been captured and upload has begun
|
||||
let vc = makeViewController(documentType: .drivingLicense)
|
||||
vc.frontDocument = mockFrontImage
|
||||
vc.backDocument = mockBackImage
|
||||
vc.frontUploadFuture = Promise(value: mockFrontImage)
|
||||
vc.backUploadFuture = Promise(value: mockBackImage)
|
||||
|
||||
vc.saveDataAndTransition()
|
||||
XCTAssertEqual(dataStore.frontDocumentImage, .init(image: mockFrontImage, fileId: ""))
|
||||
XCTAssertEqual(dataStore.backDocumentImage, .init(image: mockBackImage, fileId: ""))
|
||||
XCTAssertTrue(mockSheetController.didSaveData)
|
||||
wait(for: [mockFlowController.didTransitionToNextScreenExp], timeout: 1)
|
||||
// Request to save data
|
||||
vc.saveDataAndTransition(lastClassification: mockLastClassification, lastImage: mockBackImage.image)
|
||||
|
||||
// Verify data saved and transitioned to next screen
|
||||
wait(for: [mockSheetController.didFinishSaveDataExp, mockFlowController.didTransitionToNextScreenExp], timeout: 1)
|
||||
|
||||
// Verify dataStore updated
|
||||
XCTAssertEqual(dataStore.frontDocumentImage, mockFrontImage)
|
||||
XCTAssertEqual(dataStore.backDocumentImage, mockBackImage)
|
||||
|
||||
// Verify state
|
||||
verify(
|
||||
vc,
|
||||
expectedState: .scanned(mockLastClassification, mockBackImage.image),
|
||||
isButtonDisabled: false,
|
||||
isScanning: false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -230,6 +280,8 @@ extension DocumentCaptureViewController.State: Equatable {
|
|||
(.scanning(let left), .scanning(let right)),
|
||||
(.scanned(let left, _), .scanned(let right, _)):
|
||||
return left == right
|
||||
case (.saving, .saving):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -67,9 +67,8 @@ final class DocumentTypeSelectViewControllerTest: XCTestCase {
|
|||
// Verify that dataStore is updated
|
||||
XCTAssertEqual(dataStore.idDocumentType, .passport)
|
||||
// Verify that saveData was called
|
||||
XCTAssertTrue(mockSheetController.didSaveData)
|
||||
// Verify user was transitioned to next screen
|
||||
wait(for: [mockFlowController.didTransitionToNextScreenExp], timeout: 1)
|
||||
wait(for: [mockSheetController.didFinishSaveDataExp, mockFlowController.didTransitionToNextScreenExp], timeout: 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue