diff --git a/CHANGELOG.md b/CHANGELOG.md index b6276c051..b66ea0255 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,23 @@ # Parse-Swift Changelog ### main -[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.12.0...main) +[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.13.0...main) * _Contributing to this repo? Add info about your change here to be included in the next release_ +### 4.13.0 +[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.12.0...4.13.0) + +__New features__ +- Add helper methods to ParseFileTransferable protocol to assist with creating propper responses to file uploads ([#411](https://github.com/parse-community/Parse-Swift/pull/411)), thanks to [Corey Baker](https://github.com/cbaker6). + +__Fixes__ +- Remove cached error responses when decoding errors occur ([#411](https://github.com/parse-community/Parse-Swift/pull/411)), thanks to [Corey Baker](https://github.com/cbaker6). + ### 4.12.0 [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.11.0...4.12.0) __New features__ -- Add the ParseFileTransferable protocol for overriding the default transfer behavior for ParseFile's. Allows for direct uploads to other file storage - providers ([#408](https://github.com/parse-community/Parse-Swift/pull/408)), thanks to [Corey Baker](https://github.com/cbaker6). +- Add the ParseFileTransferable protocol for overriding the default transfer behavior for ParseFile's. Allows for direct uploads to other file storage providers ([#410](https://github.com/parse-community/Parse-Swift/pull/410)), thanks to [Corey Baker](https://github.com/cbaker6). - Add the become method to ParseInstallation which allows any ParseInstallation to be copied to the current installation. This method can be used to migrate any ParseInstallation to the current installation in the Swift SDK ([#407](https://github.com/parse-community/Parse-Swift/pull/407)), thanks to [Corey Baker](https://github.com/cbaker6). __Fixes__ diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj index 564e74b4a..8b2a3133d 100644 --- a/ParseSwift.xcodeproj/project.pbxproj +++ b/ParseSwift.xcodeproj/project.pbxproj @@ -327,6 +327,9 @@ 704E781D28CFFAF80075F952 /* ParseFileDefaultTransfer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 704E781B28CFFAF80075F952 /* ParseFileDefaultTransfer.swift */; }; 704E781E28CFFAF80075F952 /* ParseFileDefaultTransfer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 704E781B28CFFAF80075F952 /* ParseFileDefaultTransfer.swift */; }; 704E781F28CFFAF80075F952 /* ParseFileDefaultTransfer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 704E781B28CFFAF80075F952 /* ParseFileDefaultTransfer.swift */; }; + 704E782128D0D73E0075F952 /* ParseFileTransferableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 704E782028D0D73E0075F952 /* ParseFileTransferableTests.swift */; }; + 704E782228D0D73E0075F952 /* ParseFileTransferableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 704E782028D0D73E0075F952 /* ParseFileTransferableTests.swift */; }; + 704E782328D0D73E0075F952 /* ParseFileTransferableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 704E782028D0D73E0075F952 /* ParseFileTransferableTests.swift */; }; 705025992842FD3B008D6624 /* ParseCLPTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 705025982842FD3B008D6624 /* ParseCLPTests.swift */; }; 7050259A2842FD3B008D6624 /* ParseCLPTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 705025982842FD3B008D6624 /* ParseCLPTests.swift */; }; 7050259B2842FD3B008D6624 /* ParseCLPTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 705025982842FD3B008D6624 /* ParseCLPTests.swift */; }; @@ -1244,6 +1247,7 @@ 704C886B28BE69A8008E6B01 /* ParseConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseConfiguration.swift; sourceTree = ""; }; 704E781628CFD8A00075F952 /* ParseFileTransferable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseFileTransferable.swift; sourceTree = ""; }; 704E781B28CFFAF80075F952 /* ParseFileDefaultTransfer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseFileDefaultTransfer.swift; sourceTree = ""; }; + 704E782028D0D73E0075F952 /* ParseFileTransferableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseFileTransferableTests.swift; sourceTree = ""; }; 705025982842FD3B008D6624 /* ParseCLPTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseCLPTests.swift; sourceTree = ""; }; 7050259C2843F0CF008D6624 /* ParseSchemaAsyncTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseSchemaAsyncTests.swift; sourceTree = ""; }; 705025A02843F0E7008D6624 /* ParseSchemaCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseSchemaCombineTests.swift; sourceTree = ""; }; @@ -1620,6 +1624,7 @@ 7044C1F825C5CFAB0011F6E7 /* ParseFileCombineTests.swift */, 705A99F8259807F900B3547F /* ParseFileManagerTests.swift */, 705727882593FF8000F0ADD5 /* ParseFileTests.swift */, + 704E782028D0D73E0075F952 /* ParseFileTransferableTests.swift */, 70BC0B32251903D1001556DB /* ParseGeoPointTests.swift */, 70F03A592780EAB000E5AFB4 /* ParseGitHubCombineTests.swift */, 70F03A5D2780EAC700E5AFB4 /* ParseGitHubTests.swift */, @@ -2926,6 +2931,7 @@ 7044C20625C5D6780011F6E7 /* ParseQueryCombineTests.swift in Sources */, 70C5508525B4A68700B5DBC2 /* ParseOperationTests.swift in Sources */, 7023800F2747FCCD00EFC443 /* ExtensionsTests.swift in Sources */, + 704E782128D0D73E0075F952 /* ParseFileTransferableTests.swift in Sources */, 917BA43E2703E84000F8D747 /* ParseOperationAsyncTests.swift in Sources */, 7037DAB226384DE1005D7E62 /* TestParseEncoder.swift in Sources */, 7004C24D25B69207005E0AD9 /* ParseRoleTests.swift in Sources */, @@ -3246,6 +3252,7 @@ 7044C20825C5D6780011F6E7 /* ParseQueryCombineTests.swift in Sources */, 70C5508725B4A68700B5DBC2 /* ParseOperationTests.swift in Sources */, 702380112747FCCD00EFC443 /* ExtensionsTests.swift in Sources */, + 704E782328D0D73E0075F952 /* ParseFileTransferableTests.swift in Sources */, 917BA4402703E84000F8D747 /* ParseOperationAsyncTests.swift in Sources */, 7037DAB426384DE1005D7E62 /* TestParseEncoder.swift in Sources */, 7004C26125B6920B005E0AD9 /* ParseRoleTests.swift in Sources */, @@ -3369,6 +3376,7 @@ 7044C20725C5D6780011F6E7 /* ParseQueryCombineTests.swift in Sources */, 70C5508625B4A68700B5DBC2 /* ParseOperationTests.swift in Sources */, 702380102747FCCD00EFC443 /* ExtensionsTests.swift in Sources */, + 704E782228D0D73E0075F952 /* ParseFileTransferableTests.swift in Sources */, 917BA43F2703E84000F8D747 /* ParseOperationAsyncTests.swift in Sources */, 7037DAB326384DE1005D7E62 /* TestParseEncoder.swift in Sources */, 7004C25725B6920A005E0AD9 /* ParseRoleTests.swift in Sources */, diff --git a/Sources/ParseSwift/API/Responses.swift b/Sources/ParseSwift/API/Responses.swift index 12afa2687..59c245bf7 100644 --- a/Sources/ParseSwift/API/Responses.swift +++ b/Sources/ParseSwift/API/Responses.swift @@ -137,7 +137,7 @@ internal struct LoginSignupResponse: Codable { } // MARK: ParseFile -internal struct FileUploadResponse: Decodable { +internal struct FileUploadResponse: Codable { let name: String let url: URL diff --git a/Sources/ParseSwift/Extensions/URLSession.swift b/Sources/ParseSwift/Extensions/URLSession.swift index 9691c80fe..9d296df13 100644 --- a/Sources/ParseSwift/Extensions/URLSession.swift +++ b/Sources/ParseSwift/Extensions/URLSession.swift @@ -95,6 +95,7 @@ internal extension URLSession { do { responseData = try ParseCoding.jsonEncoder().encode(pushStatus) } catch { + URLSession.parse.configuration.urlCache?.removeCachedResponse(for: request) return .failure(ParseError(code: .unknownError, message: error.localizedDescription)) } } @@ -102,6 +103,7 @@ internal extension URLSession { do { return try .success(mapper(responseData)) } catch { + URLSession.parse.configuration.urlCache?.removeCachedResponse(for: request) guard let parseError = error as? ParseError else { guard JSONSerialization.isValidJSONObject(responseData), let json = try? JSONSerialization @@ -226,26 +228,42 @@ internal extension URLSession { ) { var task: URLSessionTask? if let data = data { - task = ParseSwift - .configuration - .parseFileTransfer - .upload(with: request, from: data) { (responseData, urlResponse, responseError) in - completion(self.makeResult(request: request, - responseData: responseData, - urlResponse: urlResponse, - responseError: responseError, - mapper: mapper)) + do { + task = try ParseSwift + .configuration + .parseFileTransfer + .upload(with: request, + from: data) { (responseData, urlResponse, updatedRequest, responseError) in + completion(self.makeResult(request: updatedRequest ?? request, + responseData: responseData, + urlResponse: urlResponse, + responseError: responseError, + mapper: mapper)) + } + } catch { + let defaultError = ParseError(code: .unknownError, + message: "Error uploading file: \(String(describing: error))") + let parseError = error as? ParseError ?? defaultError + completion(.failure(parseError)) } } else if let file = file { - task = ParseSwift - .configuration - .parseFileTransfer - .upload(with: request, fromFile: file) { (responseData, urlResponse, responseError) in - completion(self.makeResult(request: request, - responseData: responseData, - urlResponse: urlResponse, - responseError: responseError, - mapper: mapper)) + do { + task = try ParseSwift + .configuration + .parseFileTransfer + .upload(with: request, + fromFile: file) { (responseData, urlResponse, updatedRequest, responseError) in + completion(self.makeResult(request: updatedRequest ?? request, + responseData: responseData, + urlResponse: urlResponse, + responseError: responseError, + mapper: mapper)) + } + } catch { + let defaultError = ParseError(code: .unknownError, + message: "Error uploading file: \(String(describing: error))") + let parseError = error as? ParseError ?? defaultError + completion(.failure(parseError)) } } else { completion(.failure(ParseError(code: .unknownError, message: "data and file both cannot be nil"))) diff --git a/Sources/ParseSwift/Protocols/ParseFileTransferable.swift b/Sources/ParseSwift/Protocols/ParseFileTransferable.swift index 6171e3caf..dab038c3a 100644 --- a/Sources/ParseSwift/Protocols/ParseFileTransferable.swift +++ b/Sources/ParseSwift/Protocols/ParseFileTransferable.swift @@ -23,11 +23,14 @@ public protocol ParseFileTransferable: AnyObject { request type, and so on. - parameter fileURL: The URL of the file to upload. - parameter completion: The completion handler to call when the load request - is complete. This handler is executed on the delegate queue. + is complete. Should be in the form `(Data?, URLResponse?, URLRequest?, Error?)`. + `Data` and `URLResponse` should be created using `makeSuccessfulUploadResponse()`. + `URLRequest` is the request used to upload the file if available. `Error` is any error that occured + that prevented the file upload. */ func upload(with request: URLRequest, fromFile fileURL: URL, - completion: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionUploadTask + completion: @escaping (Data?, URLResponse?, URLRequest?, Error?) -> Void) throws -> URLSessionUploadTask /** Creates a task that performs an HTTP request for the specified URL request @@ -36,23 +39,77 @@ public protocol ParseFileTransferable: AnyObject { request type, and so on. - parameter bodyData: The body data for the request. - parameter completion: The completion handler to call when the load request - is complete. This handler is executed on the delegate queue. + is complete. Should be in the form `(Data?, URLResponse?, URLRequest?, Error?)`. + `Data` and `URLResponse` should be created using `makeSuccessfulUploadResponse()`. + `URLRequest` is the request used to upload the file if available. `Error` is any error that occured + that prevented the file upload. */ func upload(with request: URLRequest, from bodyData: Data?, - completion: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionUploadTask + completion: @escaping (Data?, URLResponse?, URLRequest?, Error?) -> Void) throws -> URLSessionUploadTask + + /** + Compose a valid file upload response with a name and url. + + Use this method after uploading a file to any file storage to + respond to the Swift SDK upload request. + - parameter name: The name of the file. + - parameter url: The url of the file that was stored. + - returns: A tuple of `(Data, HTTPURLResponse?)` where `Data` is the + JSON encoded file upload response and `HTTPURLResponse` is the metadata + associated with the response to the load request. + */ + func makeSuccessfulUploadResponse(_ name: String, url: URL) throws -> (Data, HTTPURLResponse?) + + /** + Compose a dummy upload task. + + Use this method if you do not need the Parse Swift SDK to start + your upload task for you. + - returns: A dummy upload task that starts an upload of zero bytes + to localhost. + - throws: An error of type `ParseError`. + */ + func makeDummyUploadTask() throws -> URLSessionUploadTask } +// MARK: Default Implementation - Internal extension ParseFileTransferable { func upload(with request: URLRequest, fromFile fileURL: URL, - completion: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionUploadTask { - URLSession.parse.uploadTask(with: request, fromFile: fileURL, completionHandler: completion) + // swiftlint:disable:next line_length + completion: @escaping (Data?, URLResponse?, URLRequest?, Error?) -> Void) throws -> URLSessionUploadTask { + URLSession.parse.uploadTask(with: request, fromFile: fileURL) { (data, response, error) in + completion(data, response, request, error) + } } func upload(with request: URLRequest, from bodyData: Data?, - completion: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionUploadTask { - URLSession.parse.uploadTask(with: request, from: bodyData, completionHandler: completion) + // swiftlint:disable:next line_length + completion: @escaping (Data?, URLResponse?, URLRequest?, Error?) -> Void) throws -> URLSessionUploadTask { + URLSession.parse.uploadTask(with: request, from: bodyData) { (data, response, error) in + completion(data, response, request, error) + } + } +} + +// MARK: Default Implementation - Public +public extension ParseFileTransferable { + func makeSuccessfulUploadResponse(_ name: String, url: URL) throws -> (Data, HTTPURLResponse?) { + let responseData = FileUploadResponse(name: name, url: url) + let response = HTTPURLResponse(url: url, + statusCode: 200, + httpVersion: nil, + headerFields: nil) + let encodedResponseData = try ParseCoding.jsonEncoder().encode(responseData) + return (encodedResponseData, response) + } + + func makeDummyUploadTask() throws -> URLSessionUploadTask { + guard let url = URL(string: "http://localhost") else { + throw ParseError(code: .unknownError, message: "Could not create URL") + } + return URLSession.shared.uploadTask(with: .init(url: url), from: Data()) } } diff --git a/Tests/ParseSwiftTests/ParseFileTransferableTests.swift b/Tests/ParseSwiftTests/ParseFileTransferableTests.swift new file mode 100644 index 000000000..6f5eb50f2 --- /dev/null +++ b/Tests/ParseSwiftTests/ParseFileTransferableTests.swift @@ -0,0 +1,305 @@ +// +// ParseFileTransferableTests.swift +// ParseSwift +// +// Created by Corey Baker on 9/13/22. +// Copyright © 2022 Parse Community. All rights reserved. +// + +import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif +import XCTest +@testable import ParseSwift + +class ParseFileTransferableTests: XCTestCase { + let temporaryDirectory = "\(NSTemporaryDirectory())test/" + + struct FileUploadResponse: Codable, Equatable { + let name: String + let url: URL + } + + class TestFileTransfer: ParseFileTransferable { + let name: String + let url: URL + + init(name: String, url: URL?) throws { + guard let url = url else { + throw ParseError(code: .unknownError, + message: "URL should not be nil") + } + self.url = url + self.name = name + } + + func upload(with request: URLRequest, + from bodyData: Data?, + // swiftlint:disable:next line_length + completion: @escaping (Data?, URLResponse?, URLRequest?, Error?) -> Void) throws -> URLSessionUploadTask { + try fakeUpload(completion: completion) + } + + func upload(with request: URLRequest, + fromFile fileURL: URL, + // swiftlint:disable:next line_length + completion: @escaping (Data?, URLResponse?, URLRequest?, Error?) -> Void) throws -> URLSessionUploadTask { + try fakeUpload(completion: completion) + } + + func fakeUpload(completion: @escaping (Data?, + URLResponse?, + URLRequest?, + Error?) -> Void) throws -> URLSessionUploadTask { + let response = try makeSuccessfulUploadResponse(name, url: url) + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + completion(response.0, response.1, nil, nil) + } + return try makeDummyUploadTask() + } + } + + class TestFileTransferThrowError: ParseFileTransferable { + let name: String + let url: URL + + init(name: String, url: URL?) throws { + guard let url = url else { + throw ParseError(code: .unknownError, + message: "URL should not be nil") + } + self.url = url + self.name = name + } + + func upload(with request: URLRequest, + from bodyData: Data?, + // swiftlint:disable:next line_length + completion: @escaping (Data?, URLResponse?, URLRequest?, Error?) -> Void) throws -> URLSessionUploadTask { + throw ParseError(code: .unknownError, message: "Thrown on purpose") + } + + func upload(with request: URLRequest, + fromFile fileURL: URL, + // swiftlint:disable:next line_length + completion: @escaping (Data?, URLResponse?, URLRequest?, Error?) -> Void) throws -> URLSessionUploadTask { + throw ParseError(code: .unknownError, message: "Thrown on purpose") + } + } + + override func setUpWithError() throws { + try super.setUpWithError() + guard let url = URL(string: "http://localhost:1337/1") else { + XCTFail("Should create valid URL") + return + } + ParseSwift.initialize(applicationId: "applicationId", + clientKey: "clientKey", + masterKey: "masterKey", + serverURL: url, + testing: true) + + guard let fileManager = ParseFileManager() else { + throw ParseError(code: .unknownError, message: "Should have initialized file manage") + } + try fileManager.createDirectoryIfNeeded(temporaryDirectory) + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + MockURLProtocol.removeAll() + URLSession.parse.configuration.urlCache?.removeAllCachedResponses() + #if !os(Linux) && !os(Android) && !os(Windows) + try KeychainStore.shared.deleteAll() + #endif + try ParseStorage.shared.deleteAll() + + guard let fileManager = ParseFileManager(), + let defaultDirectoryPath = fileManager.defaultDataDirectoryPath else { + throw ParseError(code: .unknownError, message: "Should have initialized file manage") + } + let directory = URL(fileURLWithPath: temporaryDirectory, isDirectory: true) + let expectation1 = XCTestExpectation(description: "Delete files1") + fileManager.removeDirectoryContents(directory) { error in + guard let error = error else { + expectation1.fulfill() + return + } + XCTFail(error.localizedDescription) + expectation1.fulfill() + } + let directory2 = defaultDirectoryPath + .appendingPathComponent(ParseConstants.fileDownloadsDirectory, isDirectory: true) + let expectation2 = XCTestExpectation(description: "Delete files2") + fileManager.removeDirectoryContents(directory2) { _ in + expectation2.fulfill() + } + wait(for: [expectation1, expectation2], timeout: 20.0) + } + + func testSDKInitializers() throws { + guard let url = URL(string: "http://localhost:1337/1") else { + XCTFail("Should create valid URL") + return + } + let fileTransferAdapter = try TestFileTransfer(name: "test", url: url) + let fileTransferAdapterOther = try TestFileTransfer(name: "test", url: url) + XCTAssertTrue(fileTransferAdapter !== fileTransferAdapterOther) + XCTAssertTrue(ParseSwift.configuration.parseFileTransfer !== fileTransferAdapter) + ParseSwift.initialize(applicationId: "applicationId", + clientKey: "clientKey", + masterKey: "masterKey", + serverURL: url, + parseFileTransfer: fileTransferAdapter) + XCTAssertTrue(ParseSwift.configuration.parseFileTransfer === fileTransferAdapter) + ParseSwift.initialize(configuration: .init(applicationId: "applicationId", + clientKey: "clientKey", + masterKey: "masterKey", + serverURL: url, + parseFileTransfer: fileTransferAdapterOther)) + XCTAssertTrue(ParseSwift.configuration.parseFileTransfer === fileTransferAdapterOther) + } + + func testMakeSuccessfulUploadResponse() throws { + guard let url = URL(string: "http://localhost:1337/1") else { + XCTFail("Should create valid URL") + return + } + let fileTransferAdapter = try TestFileTransfer(name: "test", url: url) + let originalUploadResponse = FileUploadResponse(name: "hello", url: url) + let fileUploadResponseData = try fileTransferAdapter.makeSuccessfulUploadResponse("hello", + url: url) + let decodedUploadResponse = try ParseCoding.jsonDecoder().decode(FileUploadResponse.self, + from: fileUploadResponseData.0) + XCTAssertEqual(originalUploadResponse, decodedUploadResponse) + XCTAssertEqual(fileUploadResponseData.1?.url, url) + XCTAssertEqual(fileUploadResponseData.1?.statusCode, 200) + } + + func testMakeDummyUploadTask() throws { + XCTAssertNoThrow(try ParseSwift.configuration.parseFileTransfer.makeDummyUploadTask()) + } + + func testSaveFromData() throws { + guard let sampleData = "Hello World".data(using: .utf8) else { + throw ParseError(code: .unknownError, message: "Should have converted to data") + } + let fileName = "sampleData.txt" + let parseFile = ParseFile(name: "sampleData.txt", + data: sampleData, + metadata: ["Testing": "123"], + tags: ["Hey": "now"]) + + // swiftlint:disable:next line_length + guard let url = URL(string: "http://localhost:1337/1/files/applicationId/89d74fcfa4faa5561799e5076593f67c_sampleData.txt") else { + XCTFail("Should create URL") + return + } + let fileTransferAdapter = try TestFileTransfer(name: fileName, url: url) + Parse.configuration.parseFileTransfer = fileTransferAdapter + + let expectation1 = XCTestExpectation(description: "ParseFile save") + parseFile.save { result in + switch result { + case .success(let savedFile): + XCTAssertEqual(savedFile.name, fileName) + XCTAssertEqual(savedFile.url, url) + case .failure(let error): + XCTFail("\(error)") + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testSaveFromFile() throws { + let tempFilePath = URL(fileURLWithPath: "\(temporaryDirectory)sampleData.txt") + guard let sampleData = "Hello World".data(using: .utf8) else { + throw ParseError(code: .unknownError, message: "Should have converted to data") + } + try sampleData.write(to: tempFilePath) + let fileName = "sampleData.txt" + let parseFile = ParseFile(name: fileName, localURL: tempFilePath) + // swiftlint:disable:next line_length + guard let url = URL(string: "http://localhost:1337/1/files/applicationId/89d74fcfa4faa5561799e5076593f67c_sampleData.txt") else { + XCTFail("Should create URL") + return + } + let fileTransferAdapter = try TestFileTransfer(name: fileName, url: url) + Parse.configuration.parseFileTransfer = fileTransferAdapter + + let expectation1 = XCTestExpectation(description: "ParseFile save") + parseFile.save { result in + switch result { + case .success(let savedFile): + XCTAssertEqual(savedFile.name, fileName) + XCTAssertEqual(savedFile.url, url) + case .failure(let error): + XCTFail("\(error)") + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testSaveFromDataThrowError() throws { + guard let sampleData = "Hello World".data(using: .utf8) else { + throw ParseError(code: .unknownError, message: "Should have converted to data") + } + let fileName = "sampleData.txt" + let parseFile = ParseFile(name: "sampleData.txt", + data: sampleData, + metadata: ["Testing": "123"], + tags: ["Hey": "now"]) + + // swiftlint:disable:next line_length + guard let url = URL(string: "http://localhost:1337/1/files/applicationId/89d74fcfa4faa5561799e5076593f67c_sampleData.txt") else { + XCTFail("Should create URL") + return + } + let fileTransferAdapter = try TestFileTransferThrowError(name: fileName, url: url) + Parse.configuration.parseFileTransfer = fileTransferAdapter + + let expectation1 = XCTestExpectation(description: "ParseFile save") + parseFile.save { result in + switch result { + case .success: + XCTFail("Should have failed") + case .failure(let error): + XCTAssertTrue(error.message.contains("purpose")) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testSaveFromFileThrowError() throws { + let tempFilePath = URL(fileURLWithPath: "\(temporaryDirectory)sampleData.txt") + guard let sampleData = "Hello World".data(using: .utf8) else { + throw ParseError(code: .unknownError, message: "Should have converted to data") + } + try sampleData.write(to: tempFilePath) + let fileName = "sampleData.txt" + let parseFile = ParseFile(name: fileName, localURL: tempFilePath) + // swiftlint:disable:next line_length + guard let url = URL(string: "http://localhost:1337/1/files/applicationId/89d74fcfa4faa5561799e5076593f67c_sampleData.txt") else { + XCTFail("Should create URL") + return + } + let fileTransferAdapter = try TestFileTransferThrowError(name: fileName, url: url) + Parse.configuration.parseFileTransfer = fileTransferAdapter + + let expectation1 = XCTestExpectation(description: "ParseFile save") + parseFile.save { result in + switch result { + case .success: + XCTFail("Should have failed") + case .failure(let error): + XCTAssertTrue(error.message.contains("purpose")) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } +}