diff --git a/Sources/ForageSDK/Foundation/Network/Provider.swift b/Sources/ForageSDK/Foundation/Network/Provider.swift index 88872a35..c35afd31 100644 --- a/Sources/ForageSDK/Foundation/Network/Provider.swift +++ b/Sources/ForageSDK/Foundation/Network/Provider.swift @@ -45,45 +45,6 @@ class Provider { } } - func execute(endpoint: ServiceProtocol, completion: @escaping (Result) -> Void) throws { - do { - let request = try endpoint.urlRequest() - task = urlSession.dataTask(with: request) { data, response, error in - self.processResponse(response: response) { result in - DispatchQueue.main.async { - switch result { - case .success: - completion(.success(data)) - case .failure: - if let data = data { - self.processVaultData( - model: ForageServiceError.self, - code: nil, - data: data, - response: response - ) { errorResult in - switch errorResult { - case let .success(errorParsed): - return completion(.failure(errorParsed)) - case let .failure(error): - return completion(.failure(error)) - } - } - } else if let error = error { - return completion(.failure(error)) - } else { - return completion(.failure(ServiceError.emptyError)) - } - } - } - } - } - task?.resume() - } catch { - throw error - } - } - func stopRequestOnGoing() { if let task = task { task.cancel() @@ -91,31 +52,22 @@ class Provider { } private func middleware(model: T.Type, data: Data?, response: URLResponse?, error: Error?, completion: @escaping (Result) -> Void) { - var httpResponse: HTTPURLResponse? - - processResponse(response: response) { result in - switch result { - case let .success(response): - self.logger?.info( - "Received \(response.statusCode) response from \(self.getResponseUrlPath(response))", - attributes: ["endpoint": httpResponse?.url?.path] - ) - httpResponse = response - case let .failure(error): - self.logger?.error("Failed to process response", error: error, attributes: nil) - return completion(.failure(error)) - } + if let error = error { + let httpResponse = response as? HTTPURLResponse + let wrappedError = NSError(domain: "Error: \(error)", code: httpResponse?.statusCode ?? 500, userInfo: nil) + self.logger?.error("Failed to process error for \(self.getResponseUrlPath(httpResponse))", error: wrappedError, attributes: nil) + return completion(.failure(wrappedError)) } - processError(error: error, response: httpResponse) { result in - switch result { - case .success: - break - case let .failure(error): - self.logger?.error("Failed to process error for \(self.getResponseUrlPath(httpResponse))", error: error, attributes: nil) - completion(.failure(error)) - } + guard let httpResponse = response as? HTTPURLResponse else { + let wrappedError = CommonErrors.UNKNOWN_SERVER_ERROR + self.logger?.error("Failed to process response", error: wrappedError, attributes: nil) + return completion(.failure(wrappedError)) } + self.logger?.info( + "Received \(httpResponse.statusCode) response from \(self.getResponseUrlPath(httpResponse))", + attributes: ["endpoint": httpResponse.url?.path] + ) processData(model: model, data: data, response: httpResponse, error: error) { result in switch result { @@ -128,22 +80,6 @@ class Provider { } } - private func processResponse(response: URLResponse?, completion: @escaping (Result) -> Void) { - guard let httpResponse = response as? HTTPURLResponse else { - return completion(.failure(CommonErrors.UNKNOWN_SERVER_ERROR)) - } - - return completion(.success(httpResponse)) - } - - private func processError(error: Error?, response: HTTPURLResponse?, completion: @escaping (Result) -> Void) { - guard let error = error else { - return completion(.success(())) - } - - completion(.failure(NSError(domain: "Error: \(error)", code: response?.statusCode ?? 500, userInfo: nil))) - } - private func processData(model: T.Type, data: Data?, response: HTTPURLResponse?, error: Error?, completion: @escaping (Result) -> Void) { guard let data = data else { return completion(.failure(ForageError.create( @@ -181,45 +117,4 @@ class Provider { return "\(host)\(path)" } - func processVaultData(model: T.Type, code: Int?, data: Data?, response: URLResponse?, completion: @escaping (Result) -> Void) { - var httpResponse: HTTPURLResponse? - - processResponse(response: response) { result in - switch result { - case let .success(response): - httpResponse = response - case let .failure(error): - return completion(.failure(error)) - } - } - - guard let data = data else { - return completion(.failure(ForageError.create( - code: "invalid_input_data", - httpStatusCode: httpResponse?.statusCode ?? 500, - message: "Double check the reference documentation to validate the request body, and scan your implementation for any other errors." - ))) - } - - guard let result = try? JSONDecoder().decode(T.self, from: data) else { - // NOW TRY TO DECODE IT AS AN ERROR! - guard let forageServiceError = try? JSONDecoder().decode(ForageServiceError.self, from: data) else { - return completion(.failure(ForageError.create( - code: "unknown_server_error", - httpStatusCode: httpResponse?.statusCode ?? 500, - message: "Could not decode payload - \(String(decoding: data, as: UTF8.self))" - ))) - } - - let code = forageServiceError.errors[0].code - let message = forageServiceError.errors[0].message - return completion(.failure(ForageError.create( - code: code, - httpStatusCode: httpResponse?.statusCode ?? 500, - message: message - ))) - } - - return completion(.success(result)) - } } diff --git a/Tests/ForageSDKTests/ForagePublicSubmitMethodTests.swift b/Tests/ForageSDKTests/ForagePublicSubmitMethodTests.swift index 40edf875..fce312b1 100644 --- a/Tests/ForageSDKTests/ForagePublicSubmitMethodTests.swift +++ b/Tests/ForageSDKTests/ForagePublicSubmitMethodTests.swift @@ -136,6 +136,7 @@ final class ForagePublicSubmitMethodTests: XCTestCase { mockService.doesCheckBalanceThrow = doesThrow let mockPinTextField = createMockPinTextField(isComplete: pinComplete) let expectation = XCTestExpectation(description: description) + expectation.assertForOverFulfill = true MockForageSDK.shared.checkBalance( foragePinTextField: mockPinTextField, @@ -158,6 +159,7 @@ final class ForagePublicSubmitMethodTests: XCTestCase { mockService.doesCapturePaymentThrow = doesThrow let mockPinTextField = createMockPinTextField(isComplete: pinComplete) let expectation = XCTestExpectation(description: description) + expectation.assertForOverFulfill = true MockForageSDK.shared.capturePayment( foragePinTextField: mockPinTextField, @@ -179,6 +181,7 @@ final class ForagePublicSubmitMethodTests: XCTestCase { mockService.doesCollectPinThrow = doesThrow let mockPinTextField = createMockPinTextField(isComplete: pinComplete) let expectation = XCTestExpectation(description: description) + expectation.assertForOverFulfill = true MockForageSDK.shared.deferPaymentCapture( foragePinTextField: mockPinTextField, @@ -195,6 +198,7 @@ final class ForagePublicSubmitMethodTests: XCTestCase { func testTokenizeEBTCard_Success() { let expectation = XCTestExpectation(description: "Returns PaymentMethod response") + expectation.assertForOverFulfill = true let mockPanTextField = ForagePANTextField(frame: .zero) MockForageSDK.shared.tokenizeEBTCard( @@ -221,6 +225,7 @@ final class ForagePublicSubmitMethodTests: XCTestCase { func testTokenizeEBTCard_Throws_DoesRejectWithError() { let expectation = XCTestExpectation(description: "tokenizeEBTCard rejects with ForageError") + expectation.assertForOverFulfill = true let mockPanTextField = ForagePANTextField(frame: .zero) (MockForageSDK.shared.service as! MockForageService).doesTokenizeEBTCardThrow = true diff --git a/Tests/ForageSDKTests/ForageServiceTests.swift b/Tests/ForageSDKTests/ForageServiceTests.swift index dd162072..096912aa 100644 --- a/Tests/ForageSDKTests/ForageServiceTests.swift +++ b/Tests/ForageSDKTests/ForageServiceTests.swift @@ -37,6 +37,7 @@ final class ForageServiceTests: XCTestCase { ) let expectation = XCTestExpectation(description: "Tokenize EBT Card - should succeed") + expectation.assertForOverFulfill = true service.tokenizeEBTCard(request: foragePANRequestModel) { result in switch result { case let .success(response): @@ -69,6 +70,7 @@ final class ForageServiceTests: XCTestCase { ) let expectation = XCTestExpectation(description: "Tokenize EBT Card - result should be failure") + expectation.assertForOverFulfill = true service.tokenizeEBTCard(request: foragePANRequestModel) { result in switch result { case .success: @@ -88,6 +90,7 @@ final class ForageServiceTests: XCTestCase { let service = createTestService(mockSession) let expectation = XCTestExpectation(description: "Get the Payment Method - should succeed") + expectation.assertForOverFulfill = true service.getPaymentMethod(sessionToken: "auth1234", merchantID: "1234567", paymentMethodRef: "ca29d3443f") { result in switch result { case let .success(paymentMethod): @@ -112,6 +115,7 @@ final class ForageServiceTests: XCTestCase { let service = createTestService(mockSession) let expectation = XCTestExpectation(description: "Get the Payment Method - result should be failure") + expectation.assertForOverFulfill = true service.getPaymentMethod(sessionToken: "auth1234", merchantID: "1234567", paymentMethodRef: "ca29d3443f") { result in switch result { case .success: @@ -124,6 +128,75 @@ final class ForageServiceTests: XCTestCase { wait(for: [expectation], timeout: 1.0) } + func test_getPaymentMethod_onNetworkErrorFailure_shouldReturnFailure() { + let mockSession = URLSessionMock() + mockSession.error = forageMocks.networkError + let service = createTestService(mockSession) + + let expectation = XCTestExpectation(description: "Get the Payment Method - result should be failure") + expectation.assertForOverFulfill = true + service.getPaymentMethod(sessionToken: "auth1234", merchantID: "1234567", paymentMethodRef: "ca29d3443f") { result in + switch result { + case .success: + XCTFail("Expected failure") + case let .failure(error): + XCTAssertNotNil(error) + expectation.fulfill() + } + } + wait(for: [expectation], timeout: 1.0) + } + + func test_getPaymentMethod_onNilResponse_shouldReturnUnknownServerError() { + let mockSession = URLSessionMock() + mockSession.response = nil // This will trigger the guard statement + let service = createTestService(mockSession) + + let expectation = XCTestExpectation(description: "Get the Payment Method - result should be unknown server error") + expectation.assertForOverFulfill = true + + service.getPaymentMethod(sessionToken: "auth1234", merchantID: "1234567", paymentMethodRef: "ca29d3443f") { result in + switch result { + case .success: + XCTFail("Expected unknown server error") + case let .failure(error): + let forageError = error as! ForageError + XCTAssertEqual(forageError.code, "unknown_server_error") + XCTAssertEqual(forageError.httpStatusCode, 500) + XCTAssertEqual(forageError.message, "Unknown error. This is a problem on Forage’s end.") + expectation.fulfill() + } + } + wait(for: [expectation], timeout: 1.0) + } + + func test_getPaymentMethod_onNonHTTPResponse_shouldReturnUnknownServerError() { + let mockSession = URLSessionMock() + // Create a non-HTTP URLResponse + mockSession.response = URLResponse(url: URL(string: "https://forage.com/tests")!, + mimeType: nil, + expectedContentLength: 0, + textEncodingName: nil) + let service = createTestService(mockSession) + + let expectation = XCTestExpectation(description: "Get the Payment Method - result should be unknown server error") + expectation.assertForOverFulfill = true + + service.getPaymentMethod(sessionToken: "auth1234", merchantID: "1234567", paymentMethodRef: "ca29d3443f") { result in + switch result { + case .success: + XCTFail("Expected unknown server error") + case let .failure(error): + let forageError = error as! ForageError + XCTAssertEqual(forageError.code, "unknown_server_error") + XCTAssertEqual(forageError.httpStatusCode, 500) + XCTAssertEqual(forageError.message, "Unknown error. This is a problem on Forage’s end.") + expectation.fulfill() + } + } + wait(for: [expectation], timeout: 1.0) + } + func test_getPayment_onSuccess_checkExpectedPayload() { let mockSession = URLSessionMock() mockSession.data = forageMocks.capturePaymentSuccess @@ -131,6 +204,7 @@ final class ForageServiceTests: XCTestCase { let service = createTestService(mockSession) let expectation = XCTestExpectation(description: "Get the Payment - should succeed") + expectation.assertForOverFulfill = true service.getPayment(sessionToken: "auth1234", merchantID: "1234567", paymentRef: "11767381fd") { (result: Result) in switch result { case let .success(payment): @@ -154,6 +228,7 @@ final class ForageServiceTests: XCTestCase { let service = createTestService(mockSession) let expectation = XCTestExpectation(description: "Get the Payment - should succeed") + expectation.assertForOverFulfill = true service.getPayment(sessionToken: "auth1234", merchantID: "1234567", paymentRef: "11767381fd") { (result: Result) in switch result { case let .success(payment): @@ -173,6 +248,7 @@ final class ForageServiceTests: XCTestCase { let service = createTestService(mockSession) let expectation = XCTestExpectation(description: "Get the Payment - result should be failure") + expectation.assertForOverFulfill = true service.getPayment(sessionToken: "auth1234", merchantID: "1234567", paymentRef: "11767381fd") { (result: Result) in switch result { case .success: @@ -200,4 +276,92 @@ final class ForageServiceTests: XCTestCase { func test_capturePayment_onFailure_shouldReturnFailure() { _ = XCTSkip("Need to clean up and decouple capturePayment before we can test it properly") } + + func test_getPaymentMethod_onNilData_shouldReturnInvalidInputDataError() { + let mockSession = URLSessionMock() + mockSession.data = nil + mockSession.response = forageMocks.mockSuccessResponse + let service = createTestService(mockSession) + + let expectation = XCTestExpectation(description: "Get the Payment Method - result should be invalid input data error") + expectation.assertForOverFulfill = true + + service.getPaymentMethod(sessionToken: "auth1234", merchantID: "1234567", paymentMethodRef: "ca29d3443f") { result in + switch result { + case .success: + XCTFail("Expected invalid input data error") + case let .failure(error): + let forageError = error as! ForageError + XCTAssertEqual(forageError.code, "invalid_input_data") + XCTAssertEqual(forageError.httpStatusCode, 200) // Using mockSuccessResponse's status code + XCTAssertEqual(forageError.message, "Double check the reference documentation to validate the request body, and scan your implementation for any other errors.") + expectation.fulfill() + } + } + wait(for: [expectation], timeout: 1.0) + } + + func test_getPaymentMethod_onInvalidJSONData_shouldReturnUnknownServerError() { + let mockSession = URLSessionMock() + // Create invalid JSON data that can't be decoded + mockSession.data = "Invalid JSON Data".data(using: .utf8) + mockSession.response = forageMocks.mockSuccessResponse + let service = createTestService(mockSession) + + let expectation = XCTestExpectation(description: "Get the Payment Method - result should be unknown server error") + expectation.assertForOverFulfill = true + + service.getPaymentMethod(sessionToken: "auth1234", merchantID: "1234567", paymentMethodRef: "ca29d3443f") { result in + switch result { + case .success: + XCTFail("Expected unknown server error") + case let .failure(error): + let forageError = error as! ForageError + XCTAssertEqual(forageError.code, "unknown_server_error") + XCTAssertEqual(forageError.httpStatusCode, 200) // Using mockSuccessResponse's status code + XCTAssertEqual(forageError.message, "Could not decode payload - Invalid JSON Data") + expectation.fulfill() + } + } + wait(for: [expectation], timeout: 1.0) + } + + func test_getPaymentMethod_onForageServiceError_shouldReturnForageError() { + let mockSession = URLSessionMock() + let errorJSON = """ + { + "path": "/api/payment_methods/test123/", + "errors": [ + { + "code": "custom_error_code", + "message": "Custom error message", + "source": { + "resource": "Payment_Methods", + "ref": "test123" + } + } + ] + } + """ + mockSession.data = errorJSON.data(using: .utf8) + mockSession.response = forageMocks.mockSuccessResponse + let service = createTestService(mockSession) + + let expectation = XCTestExpectation(description: "Get the Payment Method - result should be forage service error") + expectation.assertForOverFulfill = true + + service.getPaymentMethod(sessionToken: "auth1234", merchantID: "1234567", paymentMethodRef: "test123") { result in + switch result { + case .success: + XCTFail("Expected forage service error") + case let .failure(error): + let forageError = error as! ForageError + XCTAssertEqual(forageError.code, "custom_error_code") + XCTAssertEqual(forageError.httpStatusCode, 200) // Using mockSuccessResponse's status code + XCTAssertEqual(forageError.message, "Custom error message") + expectation.fulfill() + } + } + wait(for: [expectation], timeout: 1.0) + } } diff --git a/Tests/ForageSDKTests/Mock/ForageMocks.swift b/Tests/ForageSDKTests/Mock/ForageMocks.swift index 597f161a..f420c08d 100644 --- a/Tests/ForageSDKTests/Mock/ForageMocks.swift +++ b/Tests/ForageSDKTests/Mock/ForageMocks.swift @@ -37,6 +37,11 @@ class ForageMocks { return NSError(domain: response, code: 400, userInfo: nil) } + var networkError: Error { + // Mock SSL error + return NSError(domain: "NSURLErrorDomain", code: -1200, userInfo: nil) + } + var tokenizeSuccess: Data { let response = """ { diff --git a/Tests/ForageSDKTests/Mock/URLSessionDataTaskMock.swift b/Tests/ForageSDKTests/Mock/URLSessionDataTaskMock.swift index a0b7b76b..0b80740e 100644 --- a/Tests/ForageSDKTests/Mock/URLSessionDataTaskMock.swift +++ b/Tests/ForageSDKTests/Mock/URLSessionDataTaskMock.swift @@ -20,7 +20,7 @@ class URLSessionMock: URLSessionProtocol { // data and error can be set to provide data or an error var data: Data? var error: Error? - var response: HTTPURLResponse? = nil + var response: URLResponse? = nil var lastRequest: URLRequest? func dataTask(with request: URLRequest, completionHandler: @escaping CompletionHandler) -> URLSessionDataTask {