diff --git a/CHANGELOG.md b/CHANGELOG.md index 3134af59a..6c03c06dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ + # Parse-Swift Changelog ### main @@ -7,9 +8,30 @@ ### 5.0.0 [Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/4.16.2...5.0.0), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/5.0.0/documentation/parseswift) +__New features__ + +* (Breaking Change) Added a new ParseHealth.Status enum to support Parse Server. +Developers can access the new status values (Status.initialized, Status.starting) +using the ParseHealth.check callback or Combine methods. The new status values +are not available for async/await and synchounous methods. Connecting to Parse +Servers < 6.0.0, using async/await, or synchronous methods only returns +Status.ok or throws an error +([#43](https://github.com/netreconlab/Parse-Swift/pull/43)), +thanks to [Corey Baker](https://github.com/cbaker6). + +* The Swift SDK can now properly handle HTTP Status codes 429 and 503 and will retry after the delay specified in the respective header ([#43](https://github.com/netreconlab/Parse-Swift/pull/43)), thanks to [Corey Baker](https://github.com/cbaker6). + +* The max connection attempts for LiveQuery can now be changed when initializing the SDK ([#43](https://github.com/netreconlab/Parse-Swift/pull/43)), thanks to [Corey Baker](https://github.com/cbaker6). + __Fixes__ -- (Breaking Change) Add and update ParseError codes. unknownError has been renamed to otherCause. invalidImageData now has the error code of 150. webhookError has the error code of 143 ([#23](https://github.com/netreconlab/Parse-Swift/pull/23)), thanks to [Corey Baker](https://github.com/cbaker6). -- (Breaking Change) Remove deprecated code ([#23](https://github.com/netreconlab/Parse-Swift/pull/23)), thanks to [Corey Baker](https://github.com/cbaker6). +* (Breaking Change) Add and update ParseError codes. unknownError has been renamed +to otherCause. invalidImageData now has the error code of 150. webhookError has +the error code of 143 ([#23](https://github.com/netreconlab/Parse-Swift/pull/23)), +thanks to [Corey Baker](https://github.com/cbaker6). + +* (Breaking Change) Remove deprecated code +([#23](https://github.com/netreconlab/Parse-Swift/pull/23)), thanks +to [Corey Baker](https://github.com/cbaker6). ### 4.16.2 [Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/4.16.1...4.16.2), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/4.16.2/documentation/parseswift) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 74fc851ec..371358886 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,4 @@ + # Contributing to the ParseSwift SDK ## Table of Contents diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj index b6ef08025..d5164f409 100644 --- a/ParseSwift.xcodeproj/project.pbxproj +++ b/ParseSwift.xcodeproj/project.pbxproj @@ -131,6 +131,9 @@ 7028373A26BD8C89007688C9 /* ParseUser+async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7028373826BD8C89007688C9 /* ParseUser+async.swift */; }; 7028373B26BD8C89007688C9 /* ParseUser+async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7028373826BD8C89007688C9 /* ParseUser+async.swift */; }; 7028373C26BD8C89007688C9 /* ParseUser+async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7028373826BD8C89007688C9 /* ParseUser+async.swift */; }; + 7031F356296F553200E077CC /* APICommandMultipleAttemptsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7031F355296F553200E077CC /* APICommandMultipleAttemptsTests.swift */; }; + 7031F357296F553200E077CC /* APICommandMultipleAttemptsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7031F355296F553200E077CC /* APICommandMultipleAttemptsTests.swift */; }; + 7031F358296F553200E077CC /* APICommandMultipleAttemptsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7031F355296F553200E077CC /* APICommandMultipleAttemptsTests.swift */; }; 7033ECB325584A83009770F3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7033ECB225584A83009770F3 /* AppDelegate.swift */; }; 7033ECB525584A83009770F3 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7033ECB425584A83009770F3 /* ViewController.swift */; }; 7033ECB825584A83009770F3 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7033ECB625584A83009770F3 /* Main.storyboard */; }; @@ -588,10 +591,6 @@ 70C550A125B4A9F600B5DBC2 /* RemoveRelation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5509F25B4A9F600B5DBC2 /* RemoveRelation.swift */; }; 70C550A225B4A9F600B5DBC2 /* RemoveRelation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5509F25B4A9F600B5DBC2 /* RemoveRelation.swift */; }; 70C550A325B4A9F600B5DBC2 /* RemoveRelation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5509F25B4A9F600B5DBC2 /* RemoveRelation.swift */; }; - 70C5655925AA147B00BDD57F /* ParseLiveQueryConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5655825AA147B00BDD57F /* ParseLiveQueryConstants.swift */; }; - 70C5655A25AA147B00BDD57F /* ParseLiveQueryConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5655825AA147B00BDD57F /* ParseLiveQueryConstants.swift */; }; - 70C5655B25AA147B00BDD57F /* ParseLiveQueryConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5655825AA147B00BDD57F /* ParseLiveQueryConstants.swift */; }; - 70C5655C25AA147B00BDD57F /* ParseLiveQueryConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5655825AA147B00BDD57F /* ParseLiveQueryConstants.swift */; }; 70C7DC1E24D20E530050419B /* ParseUserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C7DC1D24D20E530050419B /* ParseUserTests.swift */; }; 70C7DC2124D20F190050419B /* ParseQueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C7DC1F24D20F180050419B /* ParseQueryTests.swift */; }; 70C7DC2224D20F190050419B /* ParseObjectBatchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C7DC2024D20F190050419B /* ParseObjectBatchTests.swift */; }; @@ -1196,6 +1195,7 @@ 7023800E2747FCCD00EFC443 /* ExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionsTests.swift; sourceTree = ""; }; 7028373326BD8883007688C9 /* ParseObject+async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseObject+async.swift"; sourceTree = ""; }; 7028373826BD8C89007688C9 /* ParseUser+async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseUser+async.swift"; sourceTree = ""; }; + 7031F355296F553200E077CC /* APICommandMultipleAttemptsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APICommandMultipleAttemptsTests.swift; sourceTree = ""; }; 7033ECB025584A83009770F3 /* TestHostTV.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TestHostTV.app; sourceTree = BUILT_PRODUCTS_DIR; }; 7033ECB225584A83009770F3 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7033ECB425584A83009770F3 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; @@ -1323,7 +1323,6 @@ 70C5508425B4A68700B5DBC2 /* ParseOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseOperationTests.swift; sourceTree = ""; }; 70C5509125B4A99100B5DBC2 /* AddRelation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddRelation.swift; sourceTree = ""; }; 70C5509F25B4A9F600B5DBC2 /* RemoveRelation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveRelation.swift; sourceTree = ""; }; - 70C5655825AA147B00BDD57F /* ParseLiveQueryConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseLiveQueryConstants.swift; sourceTree = ""; }; 70C7DC1D24D20E530050419B /* ParseUserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseUserTests.swift; sourceTree = ""; }; 70C7DC1F24D20F180050419B /* ParseQueryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseQueryTests.swift; sourceTree = ""; }; 70C7DC2024D20F190050419B /* ParseObjectBatchTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseObjectBatchTests.swift; sourceTree = ""; }; @@ -1597,6 +1596,7 @@ children = ( 4AA8076D1F794C1C008CD551 /* Info.plist */, 911DB12D24C4837E0027F3C7 /* APICommandTests.swift */, + 7031F355296F553200E077CC /* APICommandMultipleAttemptsTests.swift */, 7003957525A0EE770052CB31 /* BatchUtilsTests.swift */, 7023800E2747FCCD00EFC443 /* ExtensionsTests.swift */, 70DFEA892618E77800F8EB4B /* InitializeSDKTests.swift */, @@ -1940,7 +1940,6 @@ 7003960825A184EF0052CB31 /* ParseLiveQuery.swift */, 703B091526BD99BC005A112F /* ParseLiveQuery+async.swift */, 918CED582684C74000CFDC83 /* ParseLiveQuery+combine.swift */, - 70C5655825AA147B00BDD57F /* ParseLiveQueryConstants.swift */, 700395B925A1470F0052CB31 /* Subscription.swift */, 705D950725BE4C08003EF6F8 /* SubscriptionCallback.swift */, 700395DE25A147C40052CB31 /* Protocols */, @@ -2790,7 +2789,6 @@ 7004C22025B63C7A005E0AD9 /* ParseRelation.swift in Sources */, 7003959525A10DFC0052CB31 /* Messages.swift in Sources */, 703B091126BD992E005A112F /* ParseOperation+async.swift in Sources */, - 70C5655925AA147B00BDD57F /* ParseLiveQueryConstants.swift in Sources */, 91F346BE269B77B5005727B6 /* CloudObservable.swift in Sources */, F97B462F24D9C74400F4A88B /* BatchUtils.swift in Sources */, 70385E802858EAA90084D306 /* ParseHookFunctionRequest.swift in Sources */, @@ -2955,6 +2953,7 @@ 70E6B01E28612FF00043EC4A /* ParseHookTriggerTests.swift in Sources */, 705025A12843F0E7008D6624 /* ParseSchemaCombineTests.swift in Sources */, 7044C1F925C5CFAB0011F6E7 /* ParseFileCombineTests.swift in Sources */, + 7031F356296F553200E077CC /* APICommandMultipleAttemptsTests.swift in Sources */, 70C5502225B3D8F700B5DBC2 /* ParseAppleTests.swift in Sources */, 917BA4322703E36800F8D747 /* ParseConfigAsyncTests.swift in Sources */, 89899DB526045DC4002E2043 /* ParseFacebookCombineTests.swift in Sources */, @@ -3103,7 +3102,6 @@ 7004C22125B63C7A005E0AD9 /* ParseRelation.swift in Sources */, 7003959625A10DFC0052CB31 /* Messages.swift in Sources */, 703B091226BD992E005A112F /* ParseOperation+async.swift in Sources */, - 70C5655A25AA147B00BDD57F /* ParseLiveQueryConstants.swift in Sources */, 91F346BF269B77B5005727B6 /* CloudObservable.swift in Sources */, F97B463024D9C74400F4A88B /* BatchUtils.swift in Sources */, 4AFDA72A1F26DAE1002AE4FC /* Parse.swift in Sources */, @@ -3278,6 +3276,7 @@ 70E6B02028612FF00043EC4A /* ParseHookTriggerTests.swift in Sources */, 705025A32843F0E7008D6624 /* ParseSchemaCombineTests.swift in Sources */, 7044C1FB25C5CFAB0011F6E7 /* ParseFileCombineTests.swift in Sources */, + 7031F358296F553200E077CC /* APICommandMultipleAttemptsTests.swift in Sources */, 70C5502425B3D8F700B5DBC2 /* ParseAppleTests.swift in Sources */, 917BA4342703E36800F8D747 /* ParseConfigAsyncTests.swift in Sources */, 89899DB726045DC4002E2043 /* ParseFacebookCombineTests.swift in Sources */, @@ -3402,6 +3401,7 @@ 70E6B01F28612FF00043EC4A /* ParseHookTriggerTests.swift in Sources */, 705025A22843F0E7008D6624 /* ParseSchemaCombineTests.swift in Sources */, 7044C1FA25C5CFAB0011F6E7 /* ParseFileCombineTests.swift in Sources */, + 7031F357296F553200E077CC /* APICommandMultipleAttemptsTests.swift in Sources */, 70C5502325B3D8F700B5DBC2 /* ParseAppleTests.swift in Sources */, 917BA4332703E36800F8D747 /* ParseConfigAsyncTests.swift in Sources */, 89899DB626045DC4002E2043 /* ParseFacebookCombineTests.swift in Sources */, @@ -3550,7 +3550,6 @@ 7004C22325B63C7A005E0AD9 /* ParseRelation.swift in Sources */, 7003959825A10DFC0052CB31 /* Messages.swift in Sources */, 703B091426BD992E005A112F /* ParseOperation+async.swift in Sources */, - 70C5655C25AA147B00BDD57F /* ParseLiveQueryConstants.swift in Sources */, 91F346C1269B77B5005727B6 /* CloudObservable.swift in Sources */, 70BDA2B6250536FF00FC2237 /* ParseInstallation.swift in Sources */, F97B465924D9C78C00F4A88B /* Remove.swift in Sources */, @@ -3740,7 +3739,6 @@ 7004C22225B63C7A005E0AD9 /* ParseRelation.swift in Sources */, 7003959725A10DFC0052CB31 /* Messages.swift in Sources */, 703B091326BD992E005A112F /* ParseOperation+async.swift in Sources */, - 70C5655B25AA147B00BDD57F /* ParseLiveQueryConstants.swift in Sources */, 91F346C0269B77B5005727B6 /* CloudObservable.swift in Sources */, 70BDA2B5250536FF00FC2237 /* ParseInstallation.swift in Sources */, F97B465824D9C78C00F4A88B /* Remove.swift in Sources */, @@ -3967,7 +3965,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -4029,7 +4027,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/README.md b/README.md index 12c345c45..21494c088 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ + ![parse-swift](https://user-images.githubusercontent.com/8621344/204069535-e1882bb0-bbcb-4178-87e6-58fd1bed96d1.png)

iOS · macOS · watchOS · tvOS · Linux · Android · Windows

diff --git a/Scripts/generate-documentation b/Scripts/generate-documentation index d71e77a8f..6fb6435b2 100755 --- a/Scripts/generate-documentation +++ b/Scripts/generate-documentation @@ -131,7 +131,7 @@ xcrun docc $DOCC_CMD \ --additional-symbol-graph-dir "$SGFS_DIR" \ --output-path "$OUTPUT_PATH" $EXTRA_DOCC_FLAGS \ --fallback-display-name ParseSwift \ - --fallback-bundle-identifier com.parse.ParseSwift \ + --fallback-bundle-identifier edu.uky.cs.netreconlab.ParseSwift \ --fallback-bundle-version 1.0.0 if [[ "$DOCC_CMD" == "convert"* ]]; then diff --git a/Sources/ParseSwift/API/API+Command+async.swift b/Sources/ParseSwift/API/API+Command+async.swift index f35686d02..da26bd6ad 100644 --- a/Sources/ParseSwift/API/API+Command+async.swift +++ b/Sources/ParseSwift/API/API+Command+async.swift @@ -20,6 +20,7 @@ internal extension API.Command { notificationQueue: DispatchQueue? = nil, childObjects: [String: PointerType]? = nil, childFiles: [UUID: ParseFile]? = nil, + allowIntermediateResponses: Bool = false, uploadProgress: ((URLSessionTask, Int64, Int64, Int64) -> Void)? = nil, downloadProgress: ((URLSessionDownloadTask, Int64, Int64, Int64) -> Void)? = nil) async throws -> U { try await withCheckedThrowingContinuation { continuation in @@ -29,6 +30,7 @@ internal extension API.Command { notificationQueue: notificationQueue, childObjects: childObjects, childFiles: childFiles, + allowIntermediateResponses: allowIntermediateResponses, uploadProgress: uploadProgress, downloadProgress: downloadProgress, completion: continuation.resume) diff --git a/Sources/ParseSwift/API/API+Command.swift b/Sources/ParseSwift/API/API+Command.swift index 165b0ca5b..302044f60 100644 --- a/Sources/ParseSwift/API/API+Command.swift +++ b/Sources/ParseSwift/API/API+Command.swift @@ -104,6 +104,7 @@ internal extension API { notificationQueue: notificationQueue, childObjects: childObjects, childFiles: childFiles, + allowIntermediateResponses: false, uploadProgress: uploadProgress, downloadProgress: downloadProgress) { result in responseResult = result @@ -126,6 +127,7 @@ internal extension API { notificationQueue: DispatchQueue? = nil, childObjects: [String: PointerType]? = nil, childFiles: [UUID: ParseFile]? = nil, + allowIntermediateResponses: Bool = false, uploadProgress: ((URLSessionTask, Int64, Int64, Int64) -> Void)? = nil, downloadProgress: ((URLSessionDownloadTask, Int64, Int64, Int64) -> Void)? = nil, completion: @escaping(Result) -> Void) { @@ -144,6 +146,7 @@ internal extension API { case .success(let urlRequest): URLSession.parse.dataTask(with: urlRequest, callbackQueue: callbackQueue, + allowIntermediateResponses: allowIntermediateResponses, mapper: mapper) { result in callbackQueue.async { switch result { @@ -202,6 +205,7 @@ internal extension API { case .success(let urlRequest): URLSession.parse.dataTask(with: urlRequest, callbackQueue: callbackQueue, + allowIntermediateResponses: allowIntermediateResponses, mapper: mapper) { result in callbackQueue.async { switch result { diff --git a/Sources/ParseSwift/API/API+NonParseBodyCommand+async.swift b/Sources/ParseSwift/API/API+NonParseBodyCommand+async.swift index fb336d4ad..8da115953 100644 --- a/Sources/ParseSwift/API/API+NonParseBodyCommand+async.swift +++ b/Sources/ParseSwift/API/API+NonParseBodyCommand+async.swift @@ -15,10 +15,12 @@ import FoundationNetworking extension API.NonParseBodyCommand { // MARK: Asynchronous Execution func execute(options: API.Options, - callbackQueue: DispatchQueue) async throws -> U { + callbackQueue: DispatchQueue, + allowIntermediateResponses: Bool = false) async throws -> U { try await withCheckedThrowingContinuation { continuation in self.executeAsync(options: options, callbackQueue: callbackQueue, + allowIntermediateResponses: allowIntermediateResponses, completion: continuation.resume) } } diff --git a/Sources/ParseSwift/API/API+NonParseBodyCommand.swift b/Sources/ParseSwift/API/API+NonParseBodyCommand.swift index 4f59c5e76..7022ecb56 100644 --- a/Sources/ParseSwift/API/API+NonParseBodyCommand.swift +++ b/Sources/ParseSwift/API/API+NonParseBodyCommand.swift @@ -43,7 +43,8 @@ internal extension API { let group = DispatchGroup() group.enter() self.executeAsync(options: options, - callbackQueue: synchronizationQueue) { result in + callbackQueue: synchronizationQueue, + allowIntermediateResponses: false) { result in responseResult = result group.leave() } @@ -59,12 +60,14 @@ internal extension API { // MARK: Asynchronous Execution func executeAsync(options: API.Options, callbackQueue: DispatchQueue, + allowIntermediateResponses: Bool = false, completion: @escaping(Result) -> Void) { switch self.prepareURLRequest(options: options) { case .success(let urlRequest): URLSession.parse.dataTask(with: urlRequest, callbackQueue: callbackQueue, + allowIntermediateResponses: allowIntermediateResponses, mapper: mapper) { result in callbackQueue.async { switch result { diff --git a/Sources/ParseSwift/API/Responses.swift b/Sources/ParseSwift/API/Responses.swift index 1198bba2a..c6cb91bfb 100644 --- a/Sources/ParseSwift/API/Responses.swift +++ b/Sources/ParseSwift/API/Responses.swift @@ -174,7 +174,7 @@ internal struct BooleanResponse: Codable { // MARK: HealthResponse internal struct HealthResponse: Codable { - let status: String + let status: ParseHealth.Status } // MARK: PushResponse diff --git a/Sources/ParseSwift/Extensions/URLSession.swift b/Sources/ParseSwift/Extensions/URLSession.swift index 35f2adf44..e59e2e1f1 100644 --- a/Sources/ParseSwift/Extensions/URLSession.swift +++ b/Sources/ParseSwift/Extensions/URLSession.swift @@ -16,7 +16,7 @@ internal extension URLSession { #if !os(Linux) && !os(Android) && !os(Windows) static var parse = URLSession.shared #else - static var parse: URLSession = /* URLSession.shared */ { + static var parse: URLSession = { if !Parse.configuration.isTestingSDK { let configuration = URLSessionConfiguration.default configuration.urlCache = URLCache.parse @@ -168,10 +168,30 @@ internal extension URLSession { message: "Unable to connect with parse-server: \(response).")) } + static func computeDelay(_ seconds: Int) -> TimeInterval? { + Calendar.current.date(byAdding: .second, + value: seconds, + to: Date())?.timeIntervalSinceNow + } + + static func computeDelay(_ delayString: String) -> TimeInterval? { + guard let seconds = Int(delayString) else { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "E, d MMM yyyy HH:mm:ss z" + guard let delayUntil = dateFormatter.date(from: delayString) else { + return nil + } + return delayUntil.timeIntervalSinceNow + } + return computeDelay(seconds) + } + + // swiftlint:disable:next function_body_length func dataTask( with request: URLRequest, callbackQueue: DispatchQueue, attempts: Int = 1, + allowIntermediateResponses: Bool, mapper: @escaping (Data) throws -> U, completion: @escaping(Result) -> Void ) { @@ -187,22 +207,59 @@ internal extension URLSession { } let statusCode = httpResponse.statusCode guard (200...299).contains(statusCode) else { - guard statusCode >= 500, - attempts <= Parse.configuration.maxConnectionAttempts + 1, - responseData == nil else { - completion(self.makeResult(request: request, - responseData: responseData, - urlResponse: urlResponse, - responseError: responseError, - mapper: mapper)) - return - } + let attempts = attempts + 1 - callbackQueue.asyncAfter(deadline: .now() + DispatchTimeInterval - .seconds(Self.reconnectInterval(2))) { + + // Retry if max attempts have not been reached. + guard attempts <= Parse.configuration.maxConnectionAttempts else { + // If max attempts have been reached update the client now. + completion(self.makeResult(request: request, + responseData: responseData, + urlResponse: urlResponse, + responseError: responseError, + mapper: mapper)) + return + } + + // If there is current response data, update the client now. + if allowIntermediateResponses, + let responseData = responseData { + completion(self.makeResult(request: request, + responseData: responseData, + urlResponse: urlResponse, + responseError: responseError, + mapper: mapper)) + } + + let delayInterval: TimeInterval! + + // Check for constant delays in header information. + switch statusCode { + case 429: + if let delayString = httpResponse.value(forHTTPHeaderField: "x-rate-limit-reset"), + let constantDelay = Self.computeDelay(delayString) { + delayInterval = constantDelay + } else { + delayInterval = Self.computeDelay(Self.reconnectInterval(2)) + } + + case 503: + if let delayString = httpResponse.value(forHTTPHeaderField: "retry-after"), + let constantDelay = Self.computeDelay(delayString) { + delayInterval = constantDelay + } else { + delayInterval = Self.computeDelay(Self.reconnectInterval(2)) + } + + default: + delayInterval = Self.computeDelay(Self.reconnectInterval(2)) + } + + callbackQueue.asyncAfter(deadline: .now() + delayInterval) { self.dataTask(with: request, callbackQueue: callbackQueue, attempts: attempts, + allowIntermediateResponses: allowIntermediateResponses, mapper: mapper, completion: completion) } diff --git a/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift b/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift index 930556670..b2b179938 100644 --- a/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift +++ b/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift @@ -63,11 +63,12 @@ public final class ParseLiveQuery: NSObject { var clientId: String! var attempts: Int = 1 { willSet { - if newValue >= ParseLiveQueryConstants.maxConnectionAttempts + 1 { + if newValue >= Parse.configuration.liveQueryMaxConnectionAttempts + 1 && + !Parse.configuration.isTestingLiveQueryDontCloseSocket { let error = ParseError(code: .otherCause, message: """ ParseLiveQuery Error: Reached max attempts of -\(ParseLiveQueryConstants.maxConnectionAttempts). +\(Parse.configuration.liveQueryMaxConnectionAttempts). Not attempting to open ParseLiveQuery socket anymore """) notificationQueue.async { diff --git a/Sources/ParseSwift/LiveQuery/ParseLiveQueryConstants.swift b/Sources/ParseSwift/LiveQuery/ParseLiveQueryConstants.swift deleted file mode 100644 index c93cd6d0f..000000000 --- a/Sources/ParseSwift/LiveQuery/ParseLiveQueryConstants.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// ParseLiveQueryConstants.swift -// ParseSwift -// -// Created by Corey Baker on 1/9/21. -// Copyright © 2021 Parse Community. All rights reserved. -// - -import Foundation - -enum ParseLiveQueryConstants { - static let maxConnectionAttempts = 20 -} diff --git a/Sources/ParseSwift/Parse.swift b/Sources/ParseSwift/Parse.swift index c46bd8454..dccddddf9 100644 --- a/Sources/ParseSwift/Parse.swift +++ b/Sources/ParseSwift/Parse.swift @@ -30,7 +30,9 @@ internal func initialize(applicationId: String, deletingKeychainIfNeeded: Bool = false, httpAdditionalHeaders: [AnyHashable: Any]? = nil, maxConnectionAttempts: Int = 5, + liveQueryMaxConnectionAttempts: Int = 20, testing: Bool = false, + testLiveQueryDontCloseSocket: Bool = false, authentication: ((URLAuthenticationChallenge, (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) -> Void)? = nil) { @@ -51,9 +53,11 @@ internal func initialize(applicationId: String, deletingKeychainIfNeeded: deletingKeychainIfNeeded, httpAdditionalHeaders: httpAdditionalHeaders, maxConnectionAttempts: maxConnectionAttempts, + liveQueryMaxConnectionAttempts: liveQueryMaxConnectionAttempts, authentication: authentication) configuration.isMigratingFromObjcSDK = migratingFromObjcSDK configuration.isTestingSDK = testing + configuration.isTestingLiveQueryDontCloseSocket = testLiveQueryDontCloseSocket initialize(configuration: configuration) } @@ -208,6 +212,8 @@ public func initialize(configuration: ParseConfiguration) { // swiftlint:disable for more info. - parameter maxConnectionAttempts: Maximum number of times to try to connect to Parse Server. Defaults to 5. + - parameter liveQueryMaxConnectionAttempts: Maximum number of times to try to connect to a Parse + LiveQuery Server. Defaults to 20. - parameter parseFileTransfer: Override the default transfer behavior for `ParseFile`'s. Allows for direct uploads to other file storage providers. - parameter authentication: A callback block that will be used to receive/accept/decline network challenges. @@ -239,6 +245,7 @@ public func initialize( deletingKeychainIfNeeded: Bool = false, httpAdditionalHeaders: [AnyHashable: Any]? = nil, maxConnectionAttempts: Int = 5, + liveQueryMaxConnectionAttempts: Int = 20, parseFileTransfer: ParseFileTransferable? = nil, authentication: ((URLAuthenticationChallenge, (URLSession.AuthChallengeDisposition, @@ -261,6 +268,7 @@ public func initialize( deletingKeychainIfNeeded: deletingKeychainIfNeeded, httpAdditionalHeaders: httpAdditionalHeaders, maxConnectionAttempts: maxConnectionAttempts, + liveQueryMaxConnectionAttempts: liveQueryMaxConnectionAttempts, parseFileTransfer: parseFileTransfer, authentication: authentication) initialize(configuration: configuration) diff --git a/Sources/ParseSwift/Types/ParseConfiguration.swift b/Sources/ParseSwift/Types/ParseConfiguration.swift index 2742ebe84..96eef740d 100644 --- a/Sources/ParseSwift/Types/ParseConfiguration.swift +++ b/Sources/ParseSwift/Types/ParseConfiguration.swift @@ -35,10 +35,10 @@ public struct ParseConfiguration { /// The client key for your Parse application. public internal(set) var clientKey: String? - /// The server URL to connect to Parse Server. + /// The server URL to connect to a Parse Server. public internal(set) var serverURL: URL - /// The live query server URL to connect to Parse Server. + /// The live query server URL to connect to a Parse LiveQuery Server. public internal(set) var liveQuerysServerURL: URL? /// Requires `objectId`'s to be created on the client. @@ -91,10 +91,14 @@ public struct ParseConfiguration { /// apps do not have credentials to setup a Keychain. public internal(set) var isUsingDataProtectionKeychain: Bool = false - /// Maximum number of times to try to connect to Parse Server. + /// Maximum number of times to try to connect to a Parse Server. /// Defaults to 5. public internal(set) var maxConnectionAttempts: Int = 5 + /// Maximum number of times to try to connect to a Parse LiveQuery Server. + /// Defaults to 20. + public internal(set) var liveQueryMaxConnectionAttempts: Int = 20 + /** Override the default transfer behavior for `ParseFile`'s. Allows for direct uploads to other file storage providers. @@ -105,7 +109,8 @@ public struct ParseConfiguration { (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) -> Void)? internal var mountPath: String - internal var isTestingSDK = false // Enable this only for certain tests like ParseFile + internal var isTestingSDK = false + internal var isTestingLiveQueryDontCloseSocket = false #if !os(Linux) && !os(Android) && !os(Windows) internal var keychainAccessGroup = ParseKeychainAccessGroup() #endif @@ -116,8 +121,8 @@ public struct ParseConfiguration { - parameter clientKey: The client key for your Parse application. - parameter primaryKey: The master key for your Parse application. This key should only be specified when using the SDK on a server. - - parameter serverURL: The server URL to connect to Parse Server. - - parameter liveQueryServerURL: The live query server URL to connect to Parse Server. + - parameter serverURL: The server URL to connect to a Parse Server. + - parameter liveQueryServerURL: The live query server URL to connect to a Parse LiveQuery Server. - parameter requiringCustomObjectIds: Requires `objectId`'s to be created on the client side for each object. Must be enabled on the server to work. - parameter usingTransactions: Use transactions when saving/updating multiple objects. @@ -140,8 +145,10 @@ public struct ParseConfiguration { - parameter httpAdditionalHeaders: A dictionary of additional headers to send with requests. See Apple's [documentation](https://developer.apple.com/documentation/foundation/urlsessionconfiguration/1411532-httpadditionalheaders) for more info. - - parameter maxConnectionAttempts: Maximum number of times to try to connect to Parse Server. + - parameter maxConnectionAttempts: Maximum number of times to try to connect to a Parse Server. Defaults to 5. + - parameter liveQueryMaxConnectionAttempts: Maximum number of times to try to connect to a Parse + LiveQuery Server. Defaults to 20. - parameter parseFileTransfer: Override the default transfer behavior for `ParseFile`'s. Allows for direct uploads to other file storage providers. - parameter authentication: A callback block that will be used to receive/accept/decline network challenges. @@ -173,6 +180,7 @@ public struct ParseConfiguration { deletingKeychainIfNeeded: Bool = false, httpAdditionalHeaders: [AnyHashable: Any]? = nil, maxConnectionAttempts: Int = 5, + liveQueryMaxConnectionAttempts: Int = 20, parseFileTransfer: ParseFileTransferable? = nil, authentication: ((URLAuthenticationChallenge, (URLSession.AuthChallengeDisposition, @@ -197,6 +205,7 @@ public struct ParseConfiguration { self.isDeletingKeychainIfNeeded = deletingKeychainIfNeeded self.httpAdditionalHeaders = httpAdditionalHeaders self.maxConnectionAttempts = maxConnectionAttempts + self.liveQueryMaxConnectionAttempts = liveQueryMaxConnectionAttempts self.parseFileTransfer = parseFileTransfer ?? ParseFileDefaultTransfer() ParseStorage.shared.use(primitiveStore ?? InMemoryKeyValueStore()) } diff --git a/Sources/ParseSwift/Types/ParseHealth+async.swift b/Sources/ParseSwift/Types/ParseHealth+async.swift index 4d97e6297..9a5b57638 100644 --- a/Sources/ParseSwift/Types/ParseHealth+async.swift +++ b/Sources/ParseSwift/Types/ParseHealth+async.swift @@ -18,10 +18,15 @@ public extension ParseHealth { - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: Status of ParseServer. - throws: An error of type `ParseError`. + - important: Calls to this method will only return `Status.ok` or throw a `ParseError`. + Other status values such as `Status.initialized` or `Status.starting` will never + be produced. If you desire other statuses, either use the completion handler or publisher version of + this method. */ - static func check(options: API.Options = []) async throws -> String { + static func check(options: API.Options = []) async throws -> Status { try await withCheckedThrowingContinuation { continuation in Self.check(options: options, + allowIntermediateResponses: false, completion: continuation.resume) } } diff --git a/Sources/ParseSwift/Types/ParseHealth+combine.swift b/Sources/ParseSwift/Types/ParseHealth+combine.swift index e4873fbab..d406631d7 100644 --- a/Sources/ParseSwift/Types/ParseHealth+combine.swift +++ b/Sources/ParseSwift/Types/ParseHealth+combine.swift @@ -19,11 +19,20 @@ public extension ParseHealth { - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: A publisher that eventually produces a single value and then finishes or fails. */ - static func checkPublisher(options: API.Options = []) -> Future { - Future { promise in - Self.check(options: options, - completion: promise) + static func checkPublisher(options: API.Options = []) -> AnyPublisher { + let subject = PassthroughSubject() + Self.check(options: options) { result in + switch result { + case .success(let status): + subject.send(status) + if status == .ok || status == .error { + subject.send(completion: .finished) + } + case .failure(let error): + subject.send(completion: .failure(error)) + } } + return subject.eraseToAnyPublisher() } } diff --git a/Sources/ParseSwift/Types/ParseHealth.swift b/Sources/ParseSwift/Types/ParseHealth.swift index c41225481..83917faca 100644 --- a/Sources/ParseSwift/Types/ParseHealth.swift +++ b/Sources/ParseSwift/Types/ParseHealth.swift @@ -13,15 +13,31 @@ import Foundation */ public struct ParseHealth: ParseTypeable { + /// The health status value of a Parse Server. + public enum Status: String, Codable { + /// The server started and is running. + case ok + /// The server has been created but the start method has not been called yet. + case initialized + /// The server is starting up. + case starting + /// There was a startup error, see the logs for details. + case error + } + /** Calls the health check function *synchronously* and returns a result of it is execution. - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: Returns the status of the server. - throws: An error of type `ParseError`. + - important: Calls to this method will only return `Status.ok` or throw a `ParseError`. + Other status values such as `Status.initialized` or `Status.starting` will never + be produced. If you desire other statuses, either use the completion handler or publisher version of + this method. - note: The default cache policy for this method is `.reloadIgnoringLocalCacheData`. If a developer desires a different policy, it should be inserted in `options`. */ - static public func check(options: API.Options = []) throws -> String { + static public func check(options: API.Options = []) throws -> Status { var options = options options.insert(.cachePolicy(.reloadIgnoringLocalCacheData)) return try healthCommand().execute(options: options) @@ -31,23 +47,28 @@ public struct ParseHealth: ParseTypeable { Calls the health check function *asynchronously* and returns a result of it is execution. - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. + - parameter allowIntermediateResponses: If *true*, this method will continue to update `Status` + until the server returns `Status.ok`. Otherwise, calling this method will only return `Status.ok` + or throw a `ParseError`. - parameter completion: A block that will be called when the health check completes or fails. It should have the following argument signature: `(Result)`. */ static public func check(options: API.Options = [], callbackQueue: DispatchQueue = .main, - completion: @escaping (Result) -> Void) { + allowIntermediateResponses: Bool = true, + completion: @escaping (Result) -> Void) { var options = options options.insert(.cachePolicy(.reloadIgnoringLocalCacheData)) healthCommand() .executeAsync(options: options, callbackQueue: callbackQueue, + allowIntermediateResponses: allowIntermediateResponses, completion: completion) } - internal static func healthCommand() -> API.Command { + internal static func healthCommand() -> API.Command { return API.Command(method: .POST, - path: .health) { (data) -> String in + path: .health) { (data) -> Status in return try ParseCoding.jsonDecoder().decode(HealthResponse.self, from: data).status } } diff --git a/Tests/ParseSwiftTests/APICommandMultipleAttemptsTests.swift b/Tests/ParseSwiftTests/APICommandMultipleAttemptsTests.swift new file mode 100644 index 000000000..5c5bb25b9 --- /dev/null +++ b/Tests/ParseSwiftTests/APICommandMultipleAttemptsTests.swift @@ -0,0 +1,504 @@ +// +// APICommandMultipleAttemptsTests.swift +// ParseSwift +// +// Created by Corey Baker on 1/11/23. +// Copyright © 2023 Network Reconnaissance Lab. All rights reserved. +// + +#if compiler(>=5.5.2) && canImport(_Concurrency) +import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif +import XCTest +@testable import ParseSwift + +// swiftlint:disable function_body_length + +// swiftlint:disable:next type_body_length +class APICommandMultipleAttemptsTests: XCTestCase { + struct Level: ParseObject { + var objectId: String? + + var createdAt: Date? + + var updatedAt: Date? + + var ACL: ParseACL? + + var name = "First" + + var originalData: Data? + } + + 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", + primaryKey: "primaryKey", + serverURL: url, + testing: true) + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + MockURLProtocol.removeAll() + #if !os(Linux) && !os(Android) && !os(Windows) + try KeychainStore.shared.deleteAll() + #endif + try ParseStorage.shared.deleteAll() + } + + actor Result: Sendable { + var attempts = 0 + + func incrementAttempts() { + attempts += 1 + } + } + + func testComputeDelayFromString() { + let dateString = "Wed, 21 Oct 2015 07:28:00 GMT" + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "E, d MMM yyyy HH:mm:ss z" + guard let date = dateFormatter.date(from: dateString), + let computedDate = URLSession.computeDelay(dateString) else { + XCTFail("Should have produced date") + return + } + XCTAssertLessThan(date.timeIntervalSinceNow - computedDate, 1) + } + + func testErrorHTTP400JSON() throws { + let parseError = ParseError(code: .connectionFailed, message: "Connection failed") + let errorKey = "error" + let errorValue = "yarr" + let codeKey = "code" + let codeValue = 100 + let responseDictionary: [String: Any] = [ + errorKey: errorValue, + codeKey: codeValue + ] + Parse.configuration.maxConnectionAttempts = 2 + let currentAttempts = Result() + + MockURLProtocol.mockRequests { _ in + do { + let json = try JSONSerialization.data(withJSONObject: responseDictionary, options: []) + return MockURLResponse(data: json, statusCode: 400, delay: 0.0) + } catch { + XCTFail(error.localizedDescription) + return nil + } + } + + let expectation1 = XCTestExpectation(description: "Wait 1") + + API.NonParseBodyCommand(method: .GET, + path: .login, + params: nil, + mapper: { (_) -> NoBody in + throw parseError + }).executeAsync(options: [], + callbackQueue: .main, + allowIntermediateResponses: true) { result in + switch result { + case .success: + XCTFail("Should have thrown an error") + expectation1.fulfill() + case .failure(let error): + XCTAssertEqual(parseError.code, error.code) + Task { + await currentAttempts.incrementAttempts() + let current = await currentAttempts.attempts + DispatchQueue.main.async { + if current >= Parse.configuration.maxConnectionAttempts { + expectation1.fulfill() + } + } + } + } + } + wait(for: [expectation1], timeout: 20.0) + } + + func testErrorHTTPReturns400NoDataFromServer() { + Parse.configuration.maxConnectionAttempts = 2 + let originalError = ParseError(code: .otherCause, message: "Could not decode") + MockURLProtocol.mockRequests { _ in + return MockURLResponse(error: originalError) // Status code defaults to 400 + } + let expectation1 = XCTestExpectation(description: "Wait") + + API.NonParseBodyCommand(method: .GET, + path: .login, + params: nil, + mapper: { (_) -> NoBody in + throw originalError + }).executeAsync(options: [], + callbackQueue: .main, + allowIntermediateResponses: true) { result in + switch result { + case .success: + XCTFail("Should have thrown an error") + expectation1.fulfill() + case .failure(let error): + XCTAssertEqual(originalError.code, error.code) + expectation1.fulfill() + } + } + wait(for: [expectation1], timeout: 20.0) + } + + func testErrorHTTP429JSONInterval() throws { + let parseError = ParseError(code: .connectionFailed, message: "Connection failed") + let errorKey = "error" + let errorValue = "yarr" + let codeKey = "code" + let codeValue = 100 + let responseDictionary: [String: Any] = [ + errorKey: errorValue, + codeKey: codeValue + ] + let headerKey = "x-rate-limit-reset" + let headerValue = "2" + let headerFields = [headerKey: headerValue] + Parse.configuration.maxConnectionAttempts = 2 + let currentAttempts = Result() + + MockURLProtocol.mockRequests { _ in + do { + let json = try JSONSerialization.data(withJSONObject: responseDictionary, options: []) + return MockURLResponse(data: json, statusCode: 429, delay: 0.0, headerFields: headerFields) + } catch { + XCTFail(error.localizedDescription) + return nil + } + } + + let expectation1 = XCTestExpectation(description: "Wait") + + API.NonParseBodyCommand(method: .GET, + path: .login, + params: nil, + mapper: { (_) -> NoBody in + throw parseError + }).executeAsync(options: [], + callbackQueue: .main, + allowIntermediateResponses: true) { result in + switch result { + case .success: + XCTFail("Should have thrown an error") + expectation1.fulfill() + case .failure(let error): + XCTAssertEqual(parseError.code, error.code) + Task { + await currentAttempts.incrementAttempts() + let current = await currentAttempts.attempts + DispatchQueue.main.async { + if current >= Parse.configuration.maxConnectionAttempts { + expectation1.fulfill() + } + } + } + } + } + wait(for: [expectation1], timeout: 20.0) + } + + func testErrorHTTP429JSONDate() throws { + let parseError = ParseError(code: .connectionFailed, message: "Connection failed") + let errorKey = "error" + let errorValue = "yarr" + let codeKey = "code" + let codeValue = 100 + let responseDictionary: [String: Any] = [ + errorKey: errorValue, + codeKey: codeValue + ] + let headerKey = "x-rate-limit-reset" + guard let date = Calendar.current.date(byAdding: .second, + value: 2, + to: Date()) else { + XCTFail("Should have produced date") + return + } + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "E, d MMM yyyy HH:mm:ss z" + let headerValue = dateFormatter.string(from: date) + let headerFields = [headerKey: headerValue] + Parse.configuration.maxConnectionAttempts = 2 + let currentAttempts = Result() + + MockURLProtocol.mockRequests { _ in + do { + let json = try JSONSerialization.data(withJSONObject: responseDictionary, options: []) + return MockURLResponse(data: json, statusCode: 429, delay: 0.0, headerFields: headerFields) + } catch { + XCTFail(error.localizedDescription) + return nil + } + } + + let expectation1 = XCTestExpectation(description: "Wait") + + API.NonParseBodyCommand(method: .GET, + path: .login, + params: nil, + mapper: { (_) -> NoBody in + throw parseError + }).executeAsync(options: [], + callbackQueue: .main, + allowIntermediateResponses: true) { result in + switch result { + case .success: + XCTFail("Should have thrown an error") + expectation1.fulfill() + case .failure(let error): + XCTAssertEqual(parseError.code, error.code) + Task { + await currentAttempts.incrementAttempts() + let current = await currentAttempts.attempts + DispatchQueue.main.async { + if current >= Parse.configuration.maxConnectionAttempts { + expectation1.fulfill() + } + } + } + } + } + wait(for: [expectation1], timeout: 20.0) + } + + func testErrorHTTP429JSONNoHeader() throws { + let parseError = ParseError(code: .connectionFailed, message: "Connection failed") + let errorKey = "error" + let errorValue = "yarr" + let codeKey = "code" + let codeValue = 100 + let responseDictionary: [String: Any] = [ + errorKey: errorValue, + codeKey: codeValue + ] + Parse.configuration.maxConnectionAttempts = 2 + let currentAttempts = Result() + + MockURLProtocol.mockRequests { _ in + do { + let json = try JSONSerialization.data(withJSONObject: responseDictionary, options: []) + return MockURLResponse(data: json, statusCode: 429, delay: 0.0) + } catch { + XCTFail(error.localizedDescription) + return nil + } + } + + let expectation1 = XCTestExpectation(description: "Wait") + + API.NonParseBodyCommand(method: .GET, + path: .login, + params: nil, + mapper: { (_) -> NoBody in + throw parseError + }).executeAsync(options: [], + callbackQueue: .main, + allowIntermediateResponses: true) { result in + switch result { + case .success: + XCTFail("Should have thrown an error") + expectation1.fulfill() + case .failure(let error): + XCTAssertEqual(parseError.code, error.code) + Task { + await currentAttempts.incrementAttempts() + let current = await currentAttempts.attempts + DispatchQueue.main.async { + if current >= Parse.configuration.maxConnectionAttempts { + expectation1.fulfill() + } + } + } + } + } + wait(for: [expectation1], timeout: 20.0) + } + + func testErrorHTTP503JSONInterval() throws { + let parseError = ParseError(code: .connectionFailed, message: "Connection failed") + let errorKey = "error" + let errorValue = "yarr" + let codeKey = "code" + let codeValue = 100 + let responseDictionary: [String: Any] = [ + errorKey: errorValue, + codeKey: codeValue + ] + let headerKey = "retry-after" + let headerValue = "2" + let headerFields = [headerKey: headerValue] + Parse.configuration.maxConnectionAttempts = 2 + let currentAttempts = Result() + + MockURLProtocol.mockRequests { _ in + do { + let json = try JSONSerialization.data(withJSONObject: responseDictionary, options: []) + return MockURLResponse(data: json, statusCode: 503, delay: 0.0, headerFields: headerFields) + } catch { + XCTFail(error.localizedDescription) + return nil + } + } + + let expectation1 = XCTestExpectation(description: "Wait") + + API.NonParseBodyCommand(method: .GET, + path: .login, + params: nil, + mapper: { (_) -> NoBody in + throw parseError + }).executeAsync(options: [], + callbackQueue: .main, + allowIntermediateResponses: true) { result in + switch result { + case .success: + XCTFail("Should have thrown an error") + expectation1.fulfill() + case .failure(let error): + XCTAssertEqual(parseError.code, error.code) + Task { + await currentAttempts.incrementAttempts() + let current = await currentAttempts.attempts + DispatchQueue.main.async { + if current >= Parse.configuration.maxConnectionAttempts { + expectation1.fulfill() + } + } + } + } + } + wait(for: [expectation1], timeout: 20.0) + } + + func testErrorHTTP503JSONDate() throws { + let parseError = ParseError(code: .connectionFailed, message: "Connection failed") + let errorKey = "error" + let errorValue = "yarr" + let codeKey = "code" + let codeValue = 100 + let responseDictionary: [String: Any] = [ + errorKey: errorValue, + codeKey: codeValue + ] + let headerKey = "retry-after" + guard let date = Calendar.current.date(byAdding: .second, + value: 2, + to: Date()) else { + XCTFail("Should have produced date") + return + } + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "E, d MMM yyyy HH:mm:ss z" + let headerValue = dateFormatter.string(from: date) + let headerFields = [headerKey: headerValue] + Parse.configuration.maxConnectionAttempts = 2 + let currentAttempts = Result() + + MockURLProtocol.mockRequests { _ in + do { + let json = try JSONSerialization.data(withJSONObject: responseDictionary, options: []) + return MockURLResponse(data: json, statusCode: 503, delay: 0.0, headerFields: headerFields) + } catch { + XCTFail(error.localizedDescription) + return nil + } + } + + let expectation1 = XCTestExpectation(description: "Wait") + + API.NonParseBodyCommand(method: .GET, + path: .login, + params: nil, + mapper: { (_) -> NoBody in + throw parseError + }).executeAsync(options: [], + callbackQueue: .main, + allowIntermediateResponses: true) { result in + switch result { + case .success: + XCTFail("Should have thrown an error") + expectation1.fulfill() + case .failure(let error): + XCTAssertEqual(parseError.code, error.code) + Task { + await currentAttempts.incrementAttempts() + let current = await currentAttempts.attempts + DispatchQueue.main.async { + if current >= Parse.configuration.maxConnectionAttempts { + expectation1.fulfill() + } + } + } + } + } + wait(for: [expectation1], timeout: 20.0) + } + + func testErrorHTTP503JSONNoHeader() throws { + let parseError = ParseError(code: .connectionFailed, message: "Connection failed") + let errorKey = "error" + let errorValue = "yarr" + let codeKey = "code" + let codeValue = 100 + let responseDictionary: [String: Any] = [ + errorKey: errorValue, + codeKey: codeValue + ] + Parse.configuration.maxConnectionAttempts = 2 + let currentAttempts = Result() + + MockURLProtocol.mockRequests { _ in + do { + let json = try JSONSerialization.data(withJSONObject: responseDictionary, options: []) + return MockURLResponse(data: json, statusCode: 503, delay: 0.0) + } catch { + XCTFail(error.localizedDescription) + return nil + } + } + + let expectation1 = XCTestExpectation(description: "Wait") + + API.NonParseBodyCommand(method: .GET, + path: .login, + params: nil, + mapper: { (_) -> NoBody in + throw parseError + }).executeAsync(options: [], + callbackQueue: .main, + allowIntermediateResponses: true) { result in + switch result { + case .success: + XCTFail("Should have thrown an error") + expectation1.fulfill() + case .failure(let error): + XCTAssertEqual(parseError.code, error.code) + Task { + await currentAttempts.incrementAttempts() + let current = await currentAttempts.attempts + DispatchQueue.main.async { + if current >= Parse.configuration.maxConnectionAttempts { + expectation1.fulfill() + } + } + } + } + } + wait(for: [expectation1], timeout: 20.0) + } +} +#endif diff --git a/Tests/ParseSwiftTests/APICommandTests.swift b/Tests/ParseSwiftTests/APICommandTests.swift index d3ab942a5..9434bb2a6 100644 --- a/Tests/ParseSwiftTests/APICommandTests.swift +++ b/Tests/ParseSwiftTests/APICommandTests.swift @@ -187,7 +187,7 @@ class APICommandTests: XCTestCase { } } - // This is how errors HTTP errors should typically come in + // This is how HTTP errors should typically come in func testErrorHTTP400JSON() { let parseError = ParseError(code: .connectionFailed, message: "Connection failed") let errorKey = "error" @@ -198,6 +198,7 @@ class APICommandTests: XCTestCase { errorKey: errorValue, codeKey: codeValue ] + Parse.configuration.maxConnectionAttempts = 1 MockURLProtocol.mockRequests { _ in do { @@ -227,8 +228,32 @@ class APICommandTests: XCTestCase { } } + func testErrorHTTPReturns400NoDataFromServer() { + Parse.configuration.maxConnectionAttempts = 1 + let originalError = ParseError(code: .otherCause, message: "Could not decode") + MockURLProtocol.mockRequests { _ in + return MockURLResponse(error: originalError) // Status code defaults to 400 + } + do { + _ = try API.NonParseBodyCommand(method: .GET, + path: .login, + params: nil, + mapper: { (_) -> NoBody in + throw originalError + }).execute(options: []) + XCTFail("Should have thrown an error") + } catch { + guard let error = error as? ParseError else { + XCTFail("should be able unwrap final error to ParseError") + return + } + XCTAssertEqual(originalError.code, error.code) + } + } + // This is how errors HTTP errors should typically come in func testErrorHTTP500JSON() { + Parse.configuration.maxConnectionAttempts = 1 let parseError = ParseError(code: .connectionFailed, message: "Connection failed") let errorKey = "error" let errorValue = "yarr" @@ -267,29 +292,8 @@ class APICommandTests: XCTestCase { } } - func testErrorHTTPReturns400NoDataFromServer() { - let originalError = ParseError(code: .otherCause, message: "Could not decode") - MockURLProtocol.mockRequests { _ in - return MockURLResponse(error: originalError) // Status code defaults to 400 - } - do { - _ = try API.NonParseBodyCommand(method: .GET, - path: .login, - params: nil, - mapper: { (_) -> NoBody in - throw originalError - }).execute(options: []) - XCTFail("Should have thrown an error") - } catch { - guard let error = error as? ParseError else { - XCTFail("should be able unwrap final error to ParseError") - return - } - XCTAssertEqual(originalError.code, error.code) - } - } - func testErrorHTTPReturns500NoDataFromServer() { + Parse.configuration.maxConnectionAttempts = 1 let originalError = ParseError(code: .otherCause, message: "Could not decode") MockURLProtocol.mockRequests { _ in var response = MockURLResponse(error: originalError) diff --git a/Tests/ParseSwiftTests/ParseHealthAsyncTests.swift b/Tests/ParseSwiftTests/ParseHealthAsyncTests.swift index 0786c0c2f..b1bf39280 100644 --- a/Tests/ParseSwiftTests/ParseHealthAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParseHealthAsyncTests.swift @@ -40,7 +40,7 @@ class ParseHealthAsyncTests: XCTestCase { @MainActor func testCheck() async throws { - let healthOfServer = "ok" + let healthOfServer = ParseHealth.Status.ok let serverResponse = HealthResponse(status: healthOfServer) let encoded: Data! do { diff --git a/Tests/ParseSwiftTests/ParseHealthCombineTests.swift b/Tests/ParseSwiftTests/ParseHealthCombineTests.swift index 58f117a1a..ce5c98866 100644 --- a/Tests/ParseSwiftTests/ParseHealthCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseHealthCombineTests.swift @@ -36,11 +36,12 @@ class ParseHealthCombineTests: XCTestCase { try ParseStorage.shared.deleteAll() } - func testCheck() { + func testCheckOk() { var subscriptions = Set() - let expectation1 = XCTestExpectation(description: "Save") + let expectation1 = XCTestExpectation(description: "Received Value") + let expectation2 = XCTestExpectation(description: "Received Complete") - let healthOfServer = "ok" + let healthOfServer = ParseHealth.Status.ok let serverResponse = HealthResponse(status: healthOfServer) let encoded: Data! do { @@ -54,18 +55,119 @@ class ParseHealthCombineTests: XCTestCase { return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } - let publisher = ParseHealth.checkPublisher() + ParseHealth.checkPublisher() .sink(receiveCompletion: { result in + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation2.fulfill() + }, receiveValue: { health in + XCTAssertEqual(health, healthOfServer) + expectation1.fulfill() + }) + .store(in: &subscriptions) + + wait(for: [expectation1, expectation2], timeout: 20.0) + } + + func testCheckError() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Received Value") + let expectation2 = XCTestExpectation(description: "Received Complete") + + let healthOfServer = ParseHealth.Status.error + let serverResponse = HealthResponse(status: healthOfServer) + 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) + } + + ParseHealth.checkPublisher() + .sink(receiveCompletion: { result in + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation2.fulfill() + }, receiveValue: { health in + XCTAssertEqual(health, healthOfServer) + expectation1.fulfill() + }) + .store(in: &subscriptions) + + wait(for: [expectation1, expectation2], timeout: 20.0) + } + + func testCheckInitialized() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Received Value") + + let healthOfServer = ParseHealth.Status.initialized + let serverResponse = HealthResponse(status: healthOfServer) + 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) + } + ParseHealth.checkPublisher() + .sink(receiveCompletion: { result in if case let .failure(error) = result { XCTFail(error.localizedDescription) } + XCTFail("Should not have received completion") + expectation1.fulfill() + }, receiveValue: { health in + XCTAssertEqual(health, healthOfServer) expectation1.fulfill() + }) + .store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } - }, receiveValue: { health in - XCTAssertEqual(health, healthOfServer) - }) - publisher.store(in: &subscriptions) + func testCheckStarting() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Received Value") + + let healthOfServer = ParseHealth.Status.starting + let serverResponse = HealthResponse(status: healthOfServer) + 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) + } + + ParseHealth.checkPublisher() + .sink(receiveCompletion: { result in + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + XCTFail("Should not have received completion") + expectation1.fulfill() + }, receiveValue: { health in + XCTAssertEqual(health, healthOfServer) + expectation1.fulfill() + }) + .store(in: &subscriptions) wait(for: [expectation1], timeout: 20.0) } diff --git a/Tests/ParseSwiftTests/ParseHealthTests.swift b/Tests/ParseSwiftTests/ParseHealthTests.swift index 89f18bfa1..e2f136742 100644 --- a/Tests/ParseSwiftTests/ParseHealthTests.swift +++ b/Tests/ParseSwiftTests/ParseHealthTests.swift @@ -43,7 +43,7 @@ class ParseHealthTests: XCTestCase { func testCheck() { - let healthOfServer = "ok" + let healthOfServer = ParseHealth.Status.ok let serverResponse = HealthResponse(status: healthOfServer) let encoded: Data! do { @@ -66,7 +66,7 @@ class ParseHealthTests: XCTestCase { } func testCheckAsync() { - let healthOfServer = "ok" + let healthOfServer = ParseHealth.Status.ok let serverResponse = HealthResponse(status: healthOfServer) let encoded: Data! do { diff --git a/Tests/ParseSwiftTests/ParseLiveQueryAsyncTests.swift b/Tests/ParseSwiftTests/ParseLiveQueryAsyncTests.swift index 129a143fd..561f8f5fc 100644 --- a/Tests/ParseSwiftTests/ParseLiveQueryAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParseLiveQueryAsyncTests.swift @@ -25,7 +25,9 @@ class ParseLiveQueryAsyncTests: XCTestCase { clientKey: "clientKey", primaryKey: "primaryKey", serverURL: url, - testing: true) + liveQueryMaxConnectionAttempts: 1, + testing: true, + testLiveQueryDontCloseSocket: true) ParseLiveQuery.defaultClient = try ParseLiveQuery(isDefault: true) } diff --git a/Tests/ParseSwiftTests/ParseLiveQueryCombineTests.swift b/Tests/ParseSwiftTests/ParseLiveQueryCombineTests.swift index 0be69bc49..74dc3142d 100644 --- a/Tests/ParseSwiftTests/ParseLiveQueryCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseLiveQueryCombineTests.swift @@ -26,7 +26,9 @@ class ParseLiveQueryCombineTests: XCTestCase { clientKey: "clientKey", primaryKey: "primaryKey", serverURL: url, - testing: true) + liveQueryMaxConnectionAttempts: 1, + testing: true, + testLiveQueryDontCloseSocket: true) ParseLiveQuery.defaultClient = try ParseLiveQuery(isDefault: true) } diff --git a/Tests/ParseSwiftTests/ParseLiveQueryTests.swift b/Tests/ParseSwiftTests/ParseLiveQueryTests.swift index 322ceb4d2..14d414d29 100644 --- a/Tests/ParseSwiftTests/ParseLiveQueryTests.swift +++ b/Tests/ParseSwiftTests/ParseLiveQueryTests.swift @@ -57,7 +57,9 @@ class ParseLiveQueryTests: XCTestCase { clientKey: "clientKey", primaryKey: "primaryKey", serverURL: url, - testing: true) + liveQueryMaxConnectionAttempts: 1, + testing: true, + testLiveQueryDontCloseSocket: true) ParseLiveQuery.defaultClient = try ParseLiveQuery() } @@ -364,14 +366,14 @@ class ParseLiveQueryTests: XCTestCase { client.isSocketEstablished = true // Socket needs to be true client.isConnecting = true client.isConnected = true - client.attempts = ParseLiveQueryConstants.maxConnectionAttempts + 1 + client.attempts = Parse.configuration.liveQueryMaxConnectionAttempts + 1 client.clientId = "yolo" client.isDisconnectedByUser = false - XCTAssertEqual(client.isSocketEstablished, false) + XCTAssertEqual(client.isSocketEstablished, true) XCTAssertEqual(client.isConnecting, false) XCTAssertEqual(client.clientId, "yolo") - XCTAssertEqual(client.attempts, ParseLiveQueryConstants.maxConnectionAttempts + 1) + XCTAssertEqual(client.attempts, Parse.configuration.liveQueryMaxConnectionAttempts + 1) } func testDisconnectedState() throws { diff --git a/Tests/ParseSwiftTests/ParsePushAsyncTests.swift b/Tests/ParseSwiftTests/ParsePushAsyncTests.swift index 1ed1527a9..4043aeb6e 100644 --- a/Tests/ParseSwiftTests/ParsePushAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParsePushAsyncTests.swift @@ -190,7 +190,7 @@ class ParsePushAsyncTests: XCTestCase { let appleAlert = ParsePushAppleAlert(body: "hello world") let headers = ["X-Parse-Push-Status-Id": objectId] - let results = HealthResponse(status: "peace") + let results = HealthResponse(status: ParseHealth.Status.error) MockURLProtocol.mockRequests { _ in do { let encoded = try ParseCoding.jsonEncoder().encode(results) diff --git a/Tests/ParseSwiftTests/ParsePushCombineTests.swift b/Tests/ParseSwiftTests/ParsePushCombineTests.swift index ddd532f0b..f78d97dff 100644 --- a/Tests/ParseSwiftTests/ParsePushCombineTests.swift +++ b/Tests/ParseSwiftTests/ParsePushCombineTests.swift @@ -233,7 +233,7 @@ class ParsePushCombineTests: XCTestCase { let appleAlert = ParsePushAppleAlert(body: "hello world") let headers = ["X-Parse-Push-Status-Id": objectId] - let results = HealthResponse(status: "peace") + let results = HealthResponse(status: ParseHealth.Status.error) MockURLProtocol.mockRequests { _ in do { let encoded = try ParseCoding.jsonEncoder().encode(results)