diff --git a/.codecov.yml b/.codecov.yml index 728915380..a26c023fb 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -6,7 +6,7 @@ coverage: status: patch: default: - target: auto + target: 78 changes: false project: default: diff --git a/CHANGELOG.md b/CHANGELOG.md index 16d6b70d3..515fdef76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ### main -[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/3.1.2...main) +[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.0.0...main) * _Contributing to this repo? Add info about your change here to be included in the next release_ ### 4.0.0 @@ -32,6 +32,7 @@ __Improvements__ - (Breaking Change) Change the following method parameter names: isUsingTransactions -> usingTransactions, isAllowingCustomObjectIds -> allowingCustomObjectIds, isUsingEqualQueryConstraint -> usingEqualQueryConstraint, isMigratingFromObjcSDK -> migratingFromObjcSDK, isDeletingKeychainIfNeeded -> deletingKeychainIfNeeded ([#323](https://github.com/parse-community/Parse-Swift/pull/323)), thanks to [Corey Baker](https://github.com/cbaker6). __Fixes__ +- Async/await methods that return void would no throw errors received from server ([#334](https://github.com/parse-community/Parse-Swift/pull/334)), thanks to [Corey Baker](https://github.com/cbaker6). - Always check for ParseError first when decoding responses from the server. Before this fix, this could cause issues depending on how calls are made from the Swift SDK ([#332](https://github.com/parse-community/Parse-Swift/pull/332)), thanks to [Corey Baker](https://github.com/cbaker6). ### 3.1.2 diff --git a/README.md b/README.md index ddc5d4e5a..1a286dbb8 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ import PackageDescription let package = Package( name: "YOUR_PROJECT_NAME", dependencies: [ - .package(url: "https://github.com/parse-community/Parse-Swift", from: "3.1.2"), + .package(url: "https://github.com/parse-community/Parse-Swift", .upToNextMajor(from: "4.0.0")), ] ) ``` diff --git a/Sources/ParseSwift/Objects/ParseInstallation+async.swift b/Sources/ParseSwift/Objects/ParseInstallation+async.swift index f3213fefa..f3cdc29a9 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation+async.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation+async.swift @@ -114,9 +114,12 @@ public extension ParseInstallation { - important: If an object deleted has the same objectId as current, it will automatically update the current. */ func delete(options: API.Options = []) async throws { - _ = try await withCheckedThrowingContinuation { continuation in + let result = try await withCheckedThrowingContinuation { continuation in self.delete(options: options, completion: continuation.resume) } + if case let .failure(error) = result { + throw error + } } } diff --git a/Sources/ParseSwift/Objects/ParseInstallation.swift b/Sources/ParseSwift/Objects/ParseInstallation.swift index ce5cd4c58..a0a2fc6ac 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation.swift @@ -406,11 +406,12 @@ extension ParseInstallation { var foundCurrentInstallationObjects = results.filter { $0.hasSameInstallationId(as: currentInstallation) } foundCurrentInstallationObjects = try foundCurrentInstallationObjects.sorted(by: { - if $0.updatedAt == nil || $1.updatedAt == nil { + guard let firstUpdatedAt = $0.updatedAt, + let secondUpdatedAt = $1.updatedAt else { throw ParseError(code: .unknownError, - message: "Objects from the server should always have an 'updatedAt'") + message: "Objects from the server should always have an \"updatedAt\"") } - return $0.updatedAt!.compare($1.updatedAt!) == .orderedDescending + return firstUpdatedAt.compare(secondUpdatedAt) == .orderedDescending }) if let foundCurrentInstallation = foundCurrentInstallationObjects.first { if !deleting { diff --git a/Sources/ParseSwift/Objects/ParseObject+async.swift b/Sources/ParseSwift/Objects/ParseObject+async.swift index 142c4779f..d2d15cfb3 100644 --- a/Sources/ParseSwift/Objects/ParseObject+async.swift +++ b/Sources/ParseSwift/Objects/ParseObject+async.swift @@ -96,10 +96,13 @@ public extension ParseObject { - throws: An error of type `ParseError`. */ func delete(options: API.Options = []) async throws { - _ = try await withCheckedThrowingContinuation { continuation in + let result = try await withCheckedThrowingContinuation { continuation in self.delete(options: options, completion: continuation.resume) } + if case let .failure(error) = result { + throw error + } } } diff --git a/Sources/ParseSwift/Objects/ParseUser+async.swift b/Sources/ParseSwift/Objects/ParseUser+async.swift index c837fcd32..46e7aebfc 100644 --- a/Sources/ParseSwift/Objects/ParseUser+async.swift +++ b/Sources/ParseSwift/Objects/ParseUser+async.swift @@ -101,9 +101,12 @@ public extension ParseUser { - throws: An error of type `ParseError`. */ static func logout(options: API.Options = []) async throws { - _ = try await withCheckedThrowingContinuation { continuation in + let result = try await withCheckedThrowingContinuation { continuation in Self.logout(options: options, completion: continuation.resume) } + if case let .failure(error) = result { + throw error + } } /** @@ -115,9 +118,12 @@ public extension ParseUser { */ static func passwordReset(email: String, options: API.Options = []) async throws { - _ = try await withCheckedThrowingContinuation { continuation in + let result = try await withCheckedThrowingContinuation { continuation in Self.passwordReset(email: email, options: options, completion: continuation.resume) } + if case let .failure(error) = result { + throw error + } } /** @@ -147,9 +153,12 @@ public extension ParseUser { */ static func verificationEmail(email: String, options: API.Options = []) async throws { - _ = try await withCheckedThrowingContinuation { continuation in + let result = try await withCheckedThrowingContinuation { continuation in Self.verificationEmail(email: email, options: options, completion: continuation.resume) } + if case let .failure(error) = result { + throw error + } } /** @@ -252,9 +261,12 @@ public extension ParseUser { - important: If an object deleted has the same objectId as current, it will automatically update the current. */ func delete(options: API.Options = []) async throws { - _ = try await withCheckedThrowingContinuation { continuation in + let result = try await withCheckedThrowingContinuation { continuation in self.delete(options: options, completion: continuation.resume) } + if case let .failure(error) = result { + throw error + } } } diff --git a/Sources/ParseSwift/Objects/ParseUser.swift b/Sources/ParseSwift/Objects/ParseUser.swift index 525e90750..e2d084571 100644 --- a/Sources/ParseSwift/Objects/ParseUser.swift +++ b/Sources/ParseSwift/Objects/ParseUser.swift @@ -494,17 +494,8 @@ extension ParseUser { completion: @escaping (Result) -> Void) { var options = options options.insert(.cachePolicy(.reloadIgnoringLocalCacheData)) - let username: String! - if let current = BaseParseUser.current, - let currentUsername = current.username { - username = currentUsername - } else { - username = "" - } - var method: API.Method = .POST - if !usingPost { - method = .GET - } + let username = BaseParseUser.current?.username ?? "" + let method: API.Method = usingPost ? .POST : .GET verifyPasswordCommand(username: username, password: password, method: method) @@ -532,10 +523,7 @@ extension ParseUser { path: .verifyPassword, params: params, body: loginBody) { (data) -> Self in - var sessionToken = "" - if let currentSessionToken = BaseParseUser.current?.sessionToken { - sessionToken = currentSessionToken - } + var sessionToken = BaseParseUser.current?.sessionToken ?? "" if let decodedSessionToken = try? ParseCoding.jsonDecoder() .decode(LoginSignupResponse.self, from: data).sessionToken { sessionToken = decodedSessionToken @@ -826,11 +814,12 @@ extension ParseUser { var foundCurrentUserObjects = results.filter { $0.hasSameObjectId(as: currentUser) } foundCurrentUserObjects = try foundCurrentUserObjects.sorted(by: { - if $0.updatedAt == nil || $1.updatedAt == nil { + guard let firstUpdatedAt = $0.updatedAt, + let secondUpdatedAt = $1.updatedAt else { throw ParseError(code: .unknownError, - message: "Objects from the server should always have an 'updatedAt'") + message: "Objects from the server should always have an \"updatedAt\"") } - return $0.updatedAt!.compare($1.updatedAt!) == .orderedDescending + return firstUpdatedAt.compare(secondUpdatedAt) == .orderedDescending }) if let foundCurrentUser = foundCurrentUserObjects.first { if !deleting { diff --git a/Sources/ParseSwift/Types/ParseAnalytics+async.swift b/Sources/ParseSwift/Types/ParseAnalytics+async.swift index 20417f306..31e8d94ae 100644 --- a/Sources/ParseSwift/Types/ParseAnalytics+async.swift +++ b/Sources/ParseSwift/Types/ParseAnalytics+async.swift @@ -34,12 +34,15 @@ public extension ParseAnalytics { static func trackAppOpened(launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil, at date: Date? = nil, options: API.Options = []) async throws { - _ = try await withCheckedThrowingContinuation { continuation in + let result = try await withCheckedThrowingContinuation { continuation in Self.trackAppOpened(launchOptions: launchOptions, at: date, options: options, completion: continuation.resume) } + if case let .failure(error) = result { + throw error + } } #endif @@ -58,12 +61,15 @@ public extension ParseAnalytics { static func trackAppOpened(dimensions: [String: String]? = nil, at date: Date? = nil, options: API.Options = []) async throws { - _ = try await withCheckedThrowingContinuation { continuation in + let result = try await withCheckedThrowingContinuation { continuation in Self.trackAppOpened(dimensions: dimensions, at: date, options: options, completion: continuation.resume) } + if case let .failure(error) = result { + throw error + } } /** @@ -73,10 +79,13 @@ public extension ParseAnalytics { - throws: An error of type `ParseError`. */ func track(options: API.Options = []) async throws { - _ = try await withCheckedThrowingContinuation { continuation in + let result = try await withCheckedThrowingContinuation { continuation in self.track(options: options, completion: continuation.resume) } + if case let .failure(error) = result { + throw error + } } /** diff --git a/Sources/ParseSwift/Types/ParseFile+async.swift b/Sources/ParseSwift/Types/ParseFile+async.swift index 9126d9c7c..1092d8e23 100644 --- a/Sources/ParseSwift/Types/ParseFile+async.swift +++ b/Sources/ParseSwift/Types/ParseFile+async.swift @@ -96,9 +96,12 @@ public extension ParseFile { - throws: An error of type `ParseError`. */ func delete(options: API.Options = []) async throws { - _ = try await withCheckedThrowingContinuation { continuation in + let result = try await withCheckedThrowingContinuation { continuation in self.delete(options: options, completion: continuation.resume) } + if case let .failure(error) = result { + throw error + } } } diff --git a/Tests/ParseSwiftTests/APICommandTests.swift b/Tests/ParseSwiftTests/APICommandTests.swift index 31c7a5218..3e25b2314 100644 --- a/Tests/ParseSwiftTests/APICommandTests.swift +++ b/Tests/ParseSwiftTests/APICommandTests.swift @@ -185,7 +185,7 @@ class APICommandTests: XCTestCase { } } - //This is how errors HTTP errors should typically come in + // This is how errors HTTP errors should typically come in func testErrorHTTP400JSON() { let parseError = ParseError(code: .connectionFailed, message: "Connection failed") let errorKey = "error" diff --git a/Tests/ParseSwiftTests/ParseAnanlyticsAsyncTests.swift b/Tests/ParseSwiftTests/ParseAnanlyticsAsyncTests.swift index 10fc6d683..0eb8786de 100644 --- a/Tests/ParseSwiftTests/ParseAnanlyticsAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParseAnanlyticsAsyncTests.swift @@ -56,11 +56,29 @@ class ParseAnanlyticsAsyncTests: XCTestCase { // swiftlint:disable:this type_bod let options = [UIApplication.LaunchOptionsKey.remoteNotification: ["stop": "drop"]] _ = try await ParseAnalytics.trackAppOpened(launchOptions: options) } + + func testTrackAppOpenedUIKitError() async throws { + + let serverResponse = NoBody() + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(serverResponse) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + let options = [UIApplication.LaunchOptionsKey.remoteNotification: ["stop": "drop"]] + _ = try await ParseAnalytics.trackAppOpened(launchOptions: options) + } #endif @MainActor func testTrackAppOpened() async throws { - let serverResponse = NoBody() + let serverResponse = ParseError(code: .internalServer, message: "none") let encoded: Data! do { @@ -73,7 +91,45 @@ class ParseAnanlyticsAsyncTests: XCTestCase { // swiftlint:disable:this type_bod return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } - _ = try await ParseAnalytics.trackAppOpened(dimensions: ["stop": "drop"]) + do { + _ = try await ParseAnalytics.trackAppOpened(dimensions: ["stop": "drop"]) + XCTFail("Should have thrown error") + } catch { + + guard let error = error as? ParseError else { + XCTFail("Should be ParseError") + return + } + XCTAssertEqual(error.message, serverResponse.message) + } + } + + @MainActor + func testTrackAppOpenedError() async throws { + let serverResponse = ParseError(code: .internalServer, message: "none") + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(serverResponse) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + do { + _ = try await ParseAnalytics.trackAppOpened(dimensions: ["stop": "drop"]) + XCTFail("Should have thrown error") + } catch { + + guard let error = error as? ParseError else { + XCTFail("Should be ParseError") + return + } + XCTAssertEqual(error.message, serverResponse.message) + } } @MainActor @@ -94,6 +150,35 @@ class ParseAnanlyticsAsyncTests: XCTestCase { // swiftlint:disable:this type_bod _ = try await event.track() } + @MainActor + func testTrackEventError() async throws { + let serverResponse = ParseError(code: .internalServer, message: "none") + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(serverResponse) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + let event = ParseAnalytics(name: "hello") + + do { + _ = try await event.track() + XCTFail("Should have thrown error") + } catch { + + guard let error = error as? ParseError else { + XCTFail("Should be ParseError") + return + } + XCTAssertEqual(error.message, serverResponse.message) + } + } + func testTrackEventMutated() async throws { let serverResponse = NoBody() @@ -110,5 +195,32 @@ class ParseAnanlyticsAsyncTests: XCTestCase { // swiftlint:disable:this type_bod let event = ParseAnalytics(name: "hello") _ = try await event.track(dimensions: ["stop": "drop"]) } + + func testTrackEventMutatedError() async throws { + let serverResponse = ParseError(code: .internalServer, message: "none") + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(serverResponse) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + let event = ParseAnalytics(name: "hello") + do { + _ = try await event.track(dimensions: ["stop": "drop"]) + XCTFail("Should have thrown error") + } catch { + + guard let error = error as? ParseError else { + XCTFail("Should be ParseError") + return + } + XCTAssertEqual(error.message, serverResponse.message) + } + } } #endif diff --git a/Tests/ParseSwiftTests/ParseFileAsyncTests.swift b/Tests/ParseSwiftTests/ParseFileAsyncTests.swift index 1031f677d..de6eb9b18 100644 --- a/Tests/ParseSwiftTests/ParseFileAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParseFileAsyncTests.swift @@ -225,5 +225,41 @@ class ParseFileAsyncTests: XCTestCase { // swiftlint:disable:this type_body_leng _ = try await parseFile.delete(options: [.useMasterKey]) } + + @MainActor + func testDeleteError () async throws { + // swiftlint:disable:next line_length + guard let parseFileURL = URL(string: "http://localhost:1337/1/files/applicationId/d3a37aed0672a024595b766f97133615_logo.svg") else { + XCTFail("Should create URL") + return + } + var parseFile = ParseFile(name: "d3a37aed0672a024595b766f97133615_logo.svg", cloudURL: parseFileURL) + parseFile.url = parseFileURL + + let serverResponse = ParseError(code: .fileDeleteFailure, message: "not found") + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(serverResponse) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + do { + _ = try await parseFile.delete(options: [.useMasterKey]) + XCTFail("Should have thrown error") + } catch { + + guard let error = error as? ParseError else { + XCTFail("Should be ParseError") + return + } + XCTAssertEqual(error.message, serverResponse.message) + } + } } #endif diff --git a/Tests/ParseSwiftTests/ParseInstallationAsyncTests.swift b/Tests/ParseSwiftTests/ParseInstallationAsyncTests.swift index 06e2ac03d..3442edd37 100644 --- a/Tests/ParseSwiftTests/ParseInstallationAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParseInstallationAsyncTests.swift @@ -663,6 +663,45 @@ class ParseInstallationAsyncTests: XCTestCase { // swiftlint:disable:this type_b } } + @MainActor + func testDeleteError() async throws { + try saveCurrentInstallation() + MockURLProtocol.removeAll() + + guard let installation = Installation.current, + let savedObjectId = installation.objectId else { + XCTFail("Should unwrap") + return + } + XCTAssertEqual(savedObjectId, self.testInstallationObjectId) + + let serverResponse = ParseError(code: .objectNotFound, message: "not found") + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(serverResponse) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + do { + _ = try await installation.delete() + XCTFail("Should have thrown error") + } catch { + guard let error = error as? ParseError else { + XCTFail("Should be ParseError") + return + } + XCTAssertEqual(error.message, serverResponse.message) + } + + if let newInstallation = Installation.current { + XCTAssertTrue(installation.hasSameInstallationId(as: newInstallation)) + } + } + @MainActor func testFetchAll() async throws { try saveCurrentInstallation() diff --git a/Tests/ParseSwiftTests/ParseObjectAsyncTests.swift b/Tests/ParseSwiftTests/ParseObjectAsyncTests.swift index 74b421728..34c8e81d1 100644 --- a/Tests/ParseSwiftTests/ParseObjectAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectAsyncTests.swift @@ -484,6 +484,39 @@ class ParseObjectAsyncTests: XCTestCase { // swiftlint:disable:this type_body_le try await score2.delete() } + @MainActor + func testDeleteError() async throws { + var score = GameScore(points: 10) + score.objectId = "yarr" + let score2 = score + + let serverResponse = ParseError(code: .objectNotFound, message: "not found") + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(serverResponse) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + do { + try await score2.delete() + XCTFail("Should have thrown error") + } catch { + + guard let error = error as? ParseError else { + XCTFail("Should be ParseError") + return + } + XCTAssertEqual(error.message, serverResponse.message) + } + } + @MainActor func testFetchAll() async throws { let score = GameScore(points: 10) diff --git a/Tests/ParseSwiftTests/ParseUserAsyncTests.swift b/Tests/ParseSwiftTests/ParseUserAsyncTests.swift index c146a2dd6..ce530f02b 100644 --- a/Tests/ParseSwiftTests/ParseUserAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParseUserAsyncTests.swift @@ -423,7 +423,17 @@ class ParseUserAsyncTests: XCTestCase { // swiftlint:disable:this type_body_leng return } - _ = try await User.logout() + do { + _ = try await User.logout() + XCTFail("Should have thrown error") + } catch { + guard let error = error as? ParseError else { + XCTFail("Should be ParseError") + return + } + XCTAssertEqual(error.message, serverResponse.message) + } + if let userFromKeychain = BaseParseUser.current { XCTFail("\(userFromKeychain) wasn't deleted from Keychain during logout") } @@ -639,6 +649,7 @@ class ParseUserAsyncTests: XCTestCase { // swiftlint:disable:this type_body_leng } do { _ = try await User.verifyPassword(password: "blue") + XCTFail("Should have thrown error") } catch { guard let error = error as? ParseError else { XCTFail("Should be ParseError") @@ -678,6 +689,7 @@ class ParseUserAsyncTests: XCTestCase { // swiftlint:disable:this type_body_leng } do { _ = try await User.verificationEmail(email: "hello@parse.org") + XCTFail("Should have thrown error") } catch { guard let error = error as? ParseError else { XCTFail("Should be ParseError") @@ -1092,6 +1104,41 @@ class ParseUserAsyncTests: XCTestCase { // swiftlint:disable:this type_body_leng } } + @MainActor + func testDeleteError() async throws { + login() + MockURLProtocol.removeAll() + XCTAssertNotNil(User.current?.objectId) + + guard let user = User.current else { + XCTFail("Should unwrap") + return + } + + let serverResponse = ParseError(code: .objectNotFound, message: "Not found") + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(serverResponse) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + do { + _ = try await user.delete() + XCTFail("Should have thrown error") + } catch { + guard let error = error as? ParseError else { + XCTFail("Should be ParseError") + return + } + XCTAssertEqual(error.message, serverResponse.message) + } + XCTAssertNotNil(BaseParseUser.current) + } + @MainActor func testFetchAll() async throws { login() diff --git a/Tests/ParseSwiftTests/ParseUserCombineTests.swift b/Tests/ParseSwiftTests/ParseUserCombineTests.swift index 2f0c7b3eb..67114111e 100644 --- a/Tests/ParseSwiftTests/ParseUserCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseUserCombineTests.swift @@ -649,8 +649,8 @@ class ParseUserCombineTests: XCTestCase { // swiftlint:disable:this type_body_le let publisher = User.verificationEmailPublisher(email: "hello@parse.org") .sink(receiveCompletion: { result in - if case .finished = result { - XCTFail("Should have thrown ParseError") + if case .failure(let error) = result { + XCTAssertEqual(error.message, parseError.message) } expectation1.fulfill()