diff --git a/IONFileTransferLib/Delegates/IONFLTRUploadDelegate.swift b/IONFileTransferLib/Delegates/IONFLTRUploadDelegate.swift index 39d9cdb..dfe5bd8 100644 --- a/IONFileTransferLib/Delegates/IONFLTRUploadDelegate.swift +++ b/IONFileTransferLib/Delegates/IONFLTRUploadDelegate.swift @@ -10,6 +10,10 @@ class IONFLTRUploadDelegate: IONFLTRBaseDelegate { /// The total number of bytes sent during the upload. private var totalBytesSent: Int = 0 + + /// The data received from the server during the upload. + private lazy var receivedData: Data = Data() + /// Initializes a new instance of the `IONFLTRUploadDelegate` class. /// @@ -43,7 +47,36 @@ extension IONFLTRUploadDelegate: URLSessionDataDelegate { /// - task: The `URLSessionTask` that completed. /// - error: The error that occurred, if any. func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: (any Error)?) { - super.handleCompletion(task: task, error: error) + if let error = error { + super.handleCompletion(task: task, error: error) + return + } + + let response = task.response as? HTTPURLResponse + let statusCode = response?.statusCode ?? 0 + let responseData = String(data: receivedData, encoding: .utf8) + let headers = response?.allHeaderFields.reduce(into: [String: String]()) { result, element in + if let key = element.key as? String, let value = element.value as? String { + result[key] = value + } + } ?? [:] + + if (200...299).contains(statusCode) { + publisher.sendSuccess( + totalBytes: totalBytesSent, + responseCode: statusCode, + responseBody: responseData, + headers: headers + ) + } else { + publisher.sendFailure( + IONFLTRException.httpError( + responseCode: statusCode, + responseBody: nil, + headers: headers + ) + ) + } } /// Handles HTTP redirection for a task. @@ -65,20 +98,6 @@ extension IONFLTRUploadDelegate: URLSessionDataDelegate { /// - dataTask: The `URLSessionDataTask` that received the data. /// - data: The data received from the server. func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { - let response = dataTask.response as? HTTPURLResponse - let statusCode = response?.statusCode ?? 0 - let responseData = String(data: data, encoding: .utf8) - let headers = response?.allHeaderFields.reduce(into: [String: String]()) { result, element in - if let key = element.key as? String, let value = element.value as? String { - result[key] = value - } - } ?? [:] - - publisher.sendSuccess( - totalBytes: totalBytesSent, - responseCode: statusCode, - responseBody: responseData, - headers: headers - ) + receivedData.append(data) } } diff --git a/IONFileTransferLibTests/Delegates/IONFLTRUploadDelegateTests.swift b/IONFileTransferLibTests/Delegates/IONFLTRUploadDelegateTests.swift index cf89b67..c54760d 100644 --- a/IONFileTransferLibTests/Delegates/IONFLTRUploadDelegateTests.swift +++ b/IONFileTransferLibTests/Delegates/IONFLTRUploadDelegateTests.swift @@ -93,35 +93,36 @@ final class IONFLTRUploadDelegateTests: XCTestCase { waitForExpectations(timeout: 1) } - func testDidReceiveData_shouldCallSendSuccess() { - class MockDataTask: URLSessionDataTask, @unchecked Sendable { - private let mockResponse: URLResponse? - - override var response: URLResponse? { - return mockResponse - } - - init(response: URLResponse?) { - self.mockResponse = response - super.init() - } - } + func testDidReceiveData_shouldAccumulateData() { + let data1 = "Part1".data(using: .utf8)! + let data2 = "Part2".data(using: .utf8)! - let expectedResponseBody = "Success!" - let data = expectedResponseBody.data(using: .utf8)! let task = URLSession.shared.dataTask(with: URL(string: "https://example.com")!) - let urlResponse = HTTPURLResponse(url: task.originalRequest!.url!, statusCode: 200, httpVersion: nil, headerFields: ["Content-Type": "text/plain"]) - let dataTask = MockDataTask(response: urlResponse) + delegate.urlSession(URLSession.shared, dataTask: task, didReceive: data1) + delegate.urlSession(URLSession.shared, dataTask: task, didReceive: data2) + + let response = HTTPURLResponse( + url: task.originalRequest!.url!, + statusCode: 200, + httpVersion: nil, + headerFields: ["Content-Type": "text/plain"] + ) + + class MockTask: URLSessionTask, @unchecked Sendable { + let mockResponse: URLResponse? + override var response: URLResponse? { return mockResponse } + init(response: URLResponse?) { self.mockResponse = response } + } - delegate.urlSession(URLSession.shared, dataTask: dataTask, didReceive: data) + let mockTask = MockTask(response: response) + delegate.urlSession(URLSession.shared, task: mockTask, didCompleteWithError: nil) let result = mockPublisher.successCalled - XCTAssertEqual(result?.0, 0) // totalBytesSent should still be 0 - XCTAssertEqual(result?.1, 200) - XCTAssertEqual(result?.2, expectedResponseBody) + XCTAssertEqual(result?.2, "Part1Part2") XCTAssertEqual(result?.3["Content-Type"], "text/plain") } + func testDidCompleteWithError_Non2xxStatusCode() { class MockURLSessionTask: URLSessionTask, @unchecked Sendable { @@ -152,9 +153,32 @@ final class IONFLTRUploadDelegateTests: XCTestCase { ) } - func testDidCompleteWithError_withoutError_shouldNotSendFailure() { - delegate.urlSession(URLSession.shared, task: URLSessionDataTask(), didCompleteWithError: nil) + func testDidCompleteWithSuccess_shouldSendSuccess() { + let responseBody = "Uploaded!" + let data = responseBody.data(using: .utf8)! - XCTAssertNil(mockPublisher.failureCalled) + let task = URLSession.shared.dataTask(with: URL(string: "https://example.com")!) + delegate.urlSession(URLSession.shared, dataTask: task, didReceive: data) + + class MockTask: URLSessionTask { + let mockResponse: URLResponse? + override var response: URLResponse? { return mockResponse } + init(response: URLResponse?) { self.mockResponse = response } + } + + let mockResponse = HTTPURLResponse( + url: URL(string: "https://example.com")!, + statusCode: 201, + httpVersion: nil, + headerFields: ["X-Custom": "value"] + ) + let mockTask = MockTask(response: mockResponse) + + delegate.urlSession(URLSession.shared, task: mockTask, didCompleteWithError: nil) + + XCTAssertEqual(mockPublisher.successCalled?.0, 0) // totalBytesSent is default 0 + XCTAssertEqual(mockPublisher.successCalled?.1, 201) + XCTAssertEqual(mockPublisher.successCalled?.2, responseBody) + XCTAssertEqual(mockPublisher.successCalled?.3["X-Custom"], "value") } } diff --git a/scripts/build.sh b/scripts/build.sh index 72bda82..74951db 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -1,26 +1,37 @@ -cd .. +BUILD_FOLDER="build" +BUILD_SCHEME="IONFileTransferLib" +FRAMEWORK_NAME="IONFileTransferLib" +SIMULATOR_ARCHIVE_PATH="${BUILD_FOLDER}/iphonesimulator.xcarchive" +IOS_DEVICE_ARCHIVE_PATH="${BUILD_FOLDER}/iphoneos.xcarchive" -rm -rf build +rm -rf "${FRAMEWORK_NAME}.zip" +rm -rf ${BUILD_FOLDER} xcodebuild archive \ --scheme IONFileTransferLib \ --configuration Release \ --destination 'generic/platform=iOS Simulator' \ --archivePath './scripts/build/IONFileTransferLib.framework-iphonesimulator.xcarchive' \ -SKIP_INSTALL=NO \ -BUILD_LIBRARIES_FOR_DISTRIBUTION=YES - + -scheme ${BUILD_SCHEME} \ + -configuration Release \ + -destination 'generic/platform=iOS Simulator' \ + -archivePath "./${SIMULATOR_ARCHIVE_PATH}/" \ + SKIP_INSTALL=NO \ + BUILD_LIBRARIES_FOR_DISTRIBUTION=YES xcodebuild archive \ --scheme IONFileTransferLib \ --configuration Release \ --destination 'generic/platform=iOS' \ --archivePath './scripts/build/IONFileTransferLib.framework-iphoneos.xcarchive' \ -SKIP_INSTALL=NO \ -BUILD_LIBRARIES_FOR_DISTRIBUTION=YES - + -scheme ${BUILD_SCHEME} \ + -configuration Release \ + -destination 'generic/platform=iOS' \ + -archivePath "./${IOS_DEVICE_ARCHIVE_PATH}/" \ + SKIP_INSTALL=NO \ + BUILD_LIBRARIES_FOR_DISTRIBUTION=YES xcodebuild -create-xcframework \ --framework './scripts/build/IONFileTransferLib.framework-iphonesimulator.xcarchive/Products/Library/Frameworks/IONFileTransferLib.framework' \ --framework './scripts/build/IONFileTransferLib.framework-iphoneos.xcarchive/Products/Library/Frameworks/IONFileTransferLib.framework' \ --output './scripts/build/IONFileTransferLib.xcframework' + -framework "./${SIMULATOR_ARCHIVE_PATH}/Products/Library/Frameworks/${FRAMEWORK_NAME}.framework" \ + -debug-symbols "${PWD}/${SIMULATOR_ARCHIVE_PATH}/dSYMs/${FRAMEWORK_NAME}.framework.dSYM" \ + -framework "./${IOS_DEVICE_ARCHIVE_PATH}/Products/Library/Frameworks/${FRAMEWORK_NAME}.framework" \ + -debug-symbols "${PWD}/${IOS_DEVICE_ARCHIVE_PATH}/dSYMs/${FRAMEWORK_NAME}.framework.dSYM" \ + -output "./${BUILD_FOLDER}/${FRAMEWORK_NAME}.xcframework" + +cp LICENSE ${BUILD_FOLDER} + +cd "./${BUILD_FOLDER}" + +zip -r "${FRAMEWORK_NAME}.zip" "${FRAMEWORK_NAME}.xcframework" LICENSE