From c2180f5050cb9be9f21c57bb941b427a726615fa Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Wed, 4 Jan 2023 16:43:26 -0500 Subject: [PATCH 01/13] feat: Add new health statuses and delay attempts --- Sources/ParseSwift/API/Responses.swift | 2 +- .../ParseSwift/Extensions/URLSession.swift | 77 ++++++++++++++++--- .../ParseSwift/Types/ParseHealth+async.swift | 2 +- .../Types/ParseHealth+combine.swift | 2 +- Sources/ParseSwift/Types/ParseHealth.swift | 20 ++++- .../ParseHealthAsyncTests.swift | 2 +- .../ParseHealthCombineTests.swift | 2 +- Tests/ParseSwiftTests/ParseHealthTests.swift | 4 +- .../ParseSwiftTests/ParsePushAsyncTests.swift | 2 +- .../ParsePushCombineTests.swift | 2 +- 10 files changed, 90 insertions(+), 25 deletions(-) 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..92fd0ced5 100644 --- a/Sources/ParseSwift/Extensions/URLSession.swift +++ b/Sources/ParseSwift/Extensions/URLSession.swift @@ -168,6 +168,27 @@ internal extension URLSession { message: "Unable to connect with parse-server: \(response).")) } + func computeDelay(_ seconds: Int) -> TimeInterval? { + Calendar.current.date(byAdding: .second, + value: seconds, + to: Date())?.timeIntervalSinceNow + } + + 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, @@ -187,19 +208,51 @@ 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 + 1, + var delayInterval = self.computeDelay(Self.reconnectInterval(2)) 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 let responseData = responseData { + completion(self.makeResult(request: request, + responseData: responseData, + urlResponse: urlResponse, + responseError: responseError, + mapper: mapper)) + } + + // 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 + } + + case 503: + + if let delayString = httpResponse.value(forHTTPHeaderField: "retry-after"), + let constantDelay = self.computeDelay(delayString) { + delayInterval = constantDelay + } + + default: + // Use random delay + delayInterval = delayInterval + } + + callbackQueue.asyncAfter(deadline: .now() + delayInterval) { self.dataTask(with: request, callbackQueue: callbackQueue, attempts: attempts, diff --git a/Sources/ParseSwift/Types/ParseHealth+async.swift b/Sources/ParseSwift/Types/ParseHealth+async.swift index 4d97e6297..5a034e8fb 100644 --- a/Sources/ParseSwift/Types/ParseHealth+async.swift +++ b/Sources/ParseSwift/Types/ParseHealth+async.swift @@ -19,7 +19,7 @@ public extension ParseHealth { - returns: Status of ParseServer. - throws: An error of type `ParseError`. */ - 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, completion: continuation.resume) diff --git a/Sources/ParseSwift/Types/ParseHealth+combine.swift b/Sources/ParseSwift/Types/ParseHealth+combine.swift index e4873fbab..a3bd6be9e 100644 --- a/Sources/ParseSwift/Types/ParseHealth+combine.swift +++ b/Sources/ParseSwift/Types/ParseHealth+combine.swift @@ -19,7 +19,7 @@ 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 { + static func checkPublisher(options: API.Options = []) -> Future { Future { promise in Self.check(options: options, completion: promise) diff --git a/Sources/ParseSwift/Types/ParseHealth.swift b/Sources/ParseSwift/Types/ParseHealth.swift index c41225481..304af4b17 100644 --- a/Sources/ParseSwift/Types/ParseHealth.swift +++ b/Sources/ParseSwift/Types/ParseHealth.swift @@ -13,6 +13,18 @@ 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. @@ -21,7 +33,7 @@ public struct ParseHealth: ParseTypeable { - 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) @@ -36,7 +48,7 @@ public struct ParseHealth: ParseTypeable { */ static public func check(options: API.Options = [], callbackQueue: DispatchQueue = .main, - completion: @escaping (Result) -> Void) { + completion: @escaping (Result) -> Void) { var options = options options.insert(.cachePolicy(.reloadIgnoringLocalCacheData)) healthCommand() @@ -45,9 +57,9 @@ public struct ParseHealth: ParseTypeable { 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/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..c3086c1b6 100644 --- a/Tests/ParseSwiftTests/ParseHealthCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseHealthCombineTests.swift @@ -40,7 +40,7 @@ class ParseHealthCombineTests: XCTestCase { var subscriptions = Set() let expectation1 = XCTestExpectation(description: "Save") - let healthOfServer = "ok" + let healthOfServer = ParseHealth.Status.ok let serverResponse = HealthResponse(status: healthOfServer) let encoded: Data! do { 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/ParsePushAsyncTests.swift b/Tests/ParseSwiftTests/ParsePushAsyncTests.swift index 1ed1527a9..1b9e58e42 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.ok) // BAKER: FIX to decoding 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..9a1b96f70 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.ok) // BAKER: Fix to decoding error MockURLProtocol.mockRequests { _ in do { let encoded = try ParseCoding.jsonEncoder().encode(results) From e42f8e5f2c40082e1ae036af2eb0502e408ba1b7 Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Wed, 4 Jan 2023 17:23:03 -0500 Subject: [PATCH 02/13] nits --- Sources/ParseSwift/Extensions/URLSession.swift | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Sources/ParseSwift/Extensions/URLSession.swift b/Sources/ParseSwift/Extensions/URLSession.swift index 92fd0ced5..984d73372 100644 --- a/Sources/ParseSwift/Extensions/URLSession.swift +++ b/Sources/ParseSwift/Extensions/URLSession.swift @@ -178,11 +178,9 @@ internal extension URLSession { 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) @@ -212,8 +210,7 @@ internal extension URLSession { let attempts = attempts + 1 // Retry if max attempts have not been reached. - guard attempts <= Parse.configuration.maxConnectionAttempts + 1, - var delayInterval = self.computeDelay(Self.reconnectInterval(2)) else { + guard attempts <= Parse.configuration.maxConnectionAttempts + 1 else { // If max attempts have been reached update the client now. completion(self.makeResult(request: request, responseData: responseData, @@ -232,24 +229,28 @@ internal extension URLSession { 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: - // Use random delay - delayInterval = delayInterval + delayInterval = self.computeDelay(Self.reconnectInterval(2)) } callbackQueue.asyncAfter(deadline: .now() + delayInterval) { From 36ff80cf567e88742b8dabb5ea02f555bd6e22a9 Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Wed, 11 Jan 2023 15:01:06 -0500 Subject: [PATCH 03/13] Switch ParseHealth combine from Future to PassthroughSubject --- Scripts/generate-documentation | 2 +- .../ParseSwift/Extensions/URLSession.swift | 2 +- .../Types/ParseHealth+combine.swift | 17 +++- Tests/ParseSwiftTests/APICommandTests.swift | 3 +- .../ParseHealthCombineTests.swift | 79 +++++++++++++++++-- 5 files changed, 90 insertions(+), 13 deletions(-) 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/Extensions/URLSession.swift b/Sources/ParseSwift/Extensions/URLSession.swift index 984d73372..899ec98e5 100644 --- a/Sources/ParseSwift/Extensions/URLSession.swift +++ b/Sources/ParseSwift/Extensions/URLSession.swift @@ -210,7 +210,7 @@ internal extension URLSession { let attempts = attempts + 1 // Retry if max attempts have not been reached. - guard attempts <= Parse.configuration.maxConnectionAttempts + 1 else { + guard attempts <= Parse.configuration.maxConnectionAttempts else { // If max attempts have been reached update the client now. completion(self.makeResult(request: request, responseData: responseData, diff --git a/Sources/ParseSwift/Types/ParseHealth+combine.swift b/Sources/ParseSwift/Types/ParseHealth+combine.swift index a3bd6be9e..a4977608e 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 { + subject.send(completion: .finished) + } + case .failure(let error): + subject.send(completion: .failure(error)) + } } + return subject.eraseToAnyPublisher() } } diff --git a/Tests/ParseSwiftTests/APICommandTests.swift b/Tests/ParseSwiftTests/APICommandTests.swift index d3ab942a5..2d5791086 100644 --- a/Tests/ParseSwiftTests/APICommandTests.swift +++ b/Tests/ParseSwiftTests/APICommandTests.swift @@ -38,6 +38,7 @@ class APICommandTests: XCTestCase { clientKey: "clientKey", primaryKey: "primaryKey", serverURL: url, + maxConnectionAttempts: 1, testing: true) } @@ -187,7 +188,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" diff --git a/Tests/ParseSwiftTests/ParseHealthCombineTests.swift b/Tests/ParseSwiftTests/ParseHealthCombineTests.swift index c3086c1b6..f1095710c 100644 --- a/Tests/ParseSwiftTests/ParseHealthCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseHealthCombineTests.swift @@ -36,7 +36,7 @@ class ParseHealthCombineTests: XCTestCase { try ParseStorage.shared.deleteAll() } - func testCheck() { + func testCheckOk() { var subscriptions = Set() let expectation1 = XCTestExpectation(description: "Save") @@ -54,18 +54,85 @@ 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) + } + expectation1.fulfill() + }, receiveValue: { health in + XCTAssertEqual(health, healthOfServer) + expectation1.fulfill() + }) + .store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testCheckInitialized() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + 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) - }, receiveValue: { health in - XCTAssertEqual(health, healthOfServer) - }) - publisher.store(in: &subscriptions) + wait(for: [expectation1], timeout: 20.0) + } + + func testCheckStarting() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + 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) } From ed570dbaaf46dbe09487498b563c99fc4ae0def3 Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Wed, 11 Jan 2023 18:14:32 -0500 Subject: [PATCH 04/13] improvements --- ParseSwift.xcodeproj/project.pbxproj | 12 +- .../ParseSwift/API/API+Command+async.swift | 2 + Sources/ParseSwift/API/API+Command.swift | 4 + .../API/API+NonParseBodyCommand+async.swift | 4 +- .../API/API+NonParseBodyCommand.swift | 5 +- .../ParseSwift/Extensions/URLSession.swift | 7 +- .../ParseSwift/Types/ParseHealth+async.swift | 5 + Sources/ParseSwift/Types/ParseHealth.swift | 5 + .../APICommandMultipleAttemptsTests.swift | 144 ++++++++++++++++++ Tests/ParseSwiftTests/APICommandTests.swift | 49 +++--- 10 files changed, 208 insertions(+), 29 deletions(-) create mode 100644 Tests/ParseSwiftTests/APICommandMultipleAttemptsTests.swift diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj index b6ef08025..a401d5cdc 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 */; }; @@ -1196,6 +1199,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 = ""; }; @@ -1597,6 +1601,7 @@ children = ( 4AA8076D1F794C1C008CD551 /* Info.plist */, 911DB12D24C4837E0027F3C7 /* APICommandTests.swift */, + 7031F355296F553200E077CC /* APICommandMultipleAttemptsTests.swift */, 7003957525A0EE770052CB31 /* BatchUtilsTests.swift */, 7023800E2747FCCD00EFC443 /* ExtensionsTests.swift */, 70DFEA892618E77800F8EB4B /* InitializeSDKTests.swift */, @@ -2955,6 +2960,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 */, @@ -3278,6 +3284,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 +3409,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 */, @@ -3967,7 +3975,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 +4037,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/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/Extensions/URLSession.swift b/Sources/ParseSwift/Extensions/URLSession.swift index 899ec98e5..509acab0c 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 @@ -191,6 +191,7 @@ internal extension URLSession { with request: URLRequest, callbackQueue: DispatchQueue, attempts: Int = 1, + allowIntermediateResponses: Bool, mapper: @escaping (Data) throws -> U, completion: @escaping(Result) -> Void ) { @@ -221,7 +222,8 @@ internal extension URLSession { } // If there is current response data, update the client now. - if let responseData = responseData { + if allowIntermediateResponses, + let responseData = responseData { completion(self.makeResult(request: request, responseData: responseData, urlResponse: urlResponse, @@ -257,6 +259,7 @@ internal extension URLSession { self.dataTask(with: request, callbackQueue: callbackQueue, attempts: attempts, + allowIntermediateResponses: allowIntermediateResponses, mapper: mapper, completion: completion) } diff --git a/Sources/ParseSwift/Types/ParseHealth+async.swift b/Sources/ParseSwift/Types/ParseHealth+async.swift index 5a034e8fb..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 -> Status { try await withCheckedThrowingContinuation { continuation in Self.check(options: options, + allowIntermediateResponses: false, completion: continuation.resume) } } diff --git a/Sources/ParseSwift/Types/ParseHealth.swift b/Sources/ParseSwift/Types/ParseHealth.swift index 304af4b17..21ac9f836 100644 --- a/Sources/ParseSwift/Types/ParseHealth.swift +++ b/Sources/ParseSwift/Types/ParseHealth.swift @@ -43,17 +43,22 @@ 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, + allowIntermediateResponses: Bool = true, completion: @escaping (Result) -> Void) { var options = options options.insert(.cachePolicy(.reloadIgnoringLocalCacheData)) healthCommand() .executeAsync(options: options, callbackQueue: callbackQueue, + allowIntermediateResponses: allowIntermediateResponses, completion: completion) } diff --git a/Tests/ParseSwiftTests/APICommandMultipleAttemptsTests.swift b/Tests/ParseSwiftTests/APICommandMultipleAttemptsTests.swift new file mode 100644 index 000000000..106859b63 --- /dev/null +++ b/Tests/ParseSwiftTests/APICommandMultipleAttemptsTests.swift @@ -0,0 +1,144 @@ +// +// 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 + +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 testErrorHTTP400JSON() async 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") + + 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") + case .failure(let error): + XCTAssertEqual(parseError.code, error.code) + Task { + await currentAttempts.incrementAttempts() + let current = await currentAttempts.attempts + if current == Parse.configuration.maxConnectionAttempts { + expectation1.fulfill() + } + } + } + } + wait(for: [expectation1], timeout: 20.0) + } + + func testErrorHTTPReturns400NoDataFromServer() { + Parse.configuration.maxConnectionAttempts = 2 + let currentAttempts = Result() + 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") + case .failure(let error): + XCTAssertEqual(originalError.code, error.code) + Task { + await currentAttempts.incrementAttempts() + let current = await currentAttempts.attempts + expectation1.fulfill() + } + } + } + wait(for: [expectation1], timeout: 20.0) + } +} +#endif diff --git a/Tests/ParseSwiftTests/APICommandTests.swift b/Tests/ParseSwiftTests/APICommandTests.swift index 2d5791086..9434bb2a6 100644 --- a/Tests/ParseSwiftTests/APICommandTests.swift +++ b/Tests/ParseSwiftTests/APICommandTests.swift @@ -38,7 +38,6 @@ class APICommandTests: XCTestCase { clientKey: "clientKey", primaryKey: "primaryKey", serverURL: url, - maxConnectionAttempts: 1, testing: true) } @@ -199,6 +198,7 @@ class APICommandTests: XCTestCase { errorKey: errorValue, codeKey: codeValue ] + Parse.configuration.maxConnectionAttempts = 1 MockURLProtocol.mockRequests { _ in do { @@ -228,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" @@ -268,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) From 0336447df07b4f303e37f2321e6ba177830eb326 Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Wed, 11 Jan 2023 23:59:53 -0500 Subject: [PATCH 05/13] add tests and changelog --- CHANGELOG.md | 5 + ParseSwift.xcodeproj/project.pbxproj | 10 - .../ParseSwift/Extensions/URLSession.swift | 14 +- .../ParseSwift/LiveQuery/ParseLiveQuery.swift | 4 +- .../LiveQuery/ParseLiveQueryConstants.swift | 13 - Sources/ParseSwift/Parse.swift | 6 + .../ParseSwift/Types/ParseConfiguration.swift | 20 +- Sources/ParseSwift/Types/ParseHealth.swift | 4 + .../APICommandMultipleAttemptsTests.swift | 330 +++++++++++++++++- Tests/ParseSwiftTests/APICommandTests.swift | 12 + .../ParseSwiftTests/ParseLiveQueryTests.swift | 8 +- 11 files changed, 384 insertions(+), 42 deletions(-) delete mode 100644 Sources/ParseSwift/LiveQuery/ParseLiveQueryConstants.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 3134af59a..f7595c7d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ ### 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 6. 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). diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj index a401d5cdc..d5164f409 100644 --- a/ParseSwift.xcodeproj/project.pbxproj +++ b/ParseSwift.xcodeproj/project.pbxproj @@ -591,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 */; }; @@ -1327,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 = ""; }; @@ -1945,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 */, @@ -2795,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 */, @@ -3109,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 */, @@ -3558,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 */, @@ -3748,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 */, diff --git a/Sources/ParseSwift/Extensions/URLSession.swift b/Sources/ParseSwift/Extensions/URLSession.swift index 509acab0c..e59e2e1f1 100644 --- a/Sources/ParseSwift/Extensions/URLSession.swift +++ b/Sources/ParseSwift/Extensions/URLSession.swift @@ -168,13 +168,13 @@ internal extension URLSession { message: "Unable to connect with parse-server: \(response).")) } - func computeDelay(_ seconds: Int) -> TimeInterval? { + static func computeDelay(_ seconds: Int) -> TimeInterval? { Calendar.current.date(byAdding: .second, value: seconds, to: Date())?.timeIntervalSinceNow } - func computeDelay(_ delayString: String) -> TimeInterval? { + 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" @@ -237,22 +237,22 @@ internal extension URLSession { switch statusCode { case 429: if let delayString = httpResponse.value(forHTTPHeaderField: "x-rate-limit-reset"), - let constantDelay = self.computeDelay(delayString) { + let constantDelay = Self.computeDelay(delayString) { delayInterval = constantDelay } else { - delayInterval = self.computeDelay(Self.reconnectInterval(2)) + delayInterval = Self.computeDelay(Self.reconnectInterval(2)) } case 503: if let delayString = httpResponse.value(forHTTPHeaderField: "retry-after"), - let constantDelay = self.computeDelay(delayString) { + let constantDelay = Self.computeDelay(delayString) { delayInterval = constantDelay } else { - delayInterval = self.computeDelay(Self.reconnectInterval(2)) + delayInterval = Self.computeDelay(Self.reconnectInterval(2)) } default: - delayInterval = self.computeDelay(Self.reconnectInterval(2)) + delayInterval = Self.computeDelay(Self.reconnectInterval(2)) } callbackQueue.asyncAfter(deadline: .now() + delayInterval) { diff --git a/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift b/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift index 930556670..07e24f6b1 100644 --- a/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift +++ b/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift @@ -63,11 +63,11 @@ public final class ParseLiveQuery: NSObject { var clientId: String! var attempts: Int = 1 { willSet { - if newValue >= ParseLiveQueryConstants.maxConnectionAttempts + 1 { + if newValue >= Parse.configuration.liveQueryMaxConnectionAttempts + 1 { 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..f2fa045ec 100644 --- a/Sources/ParseSwift/Parse.swift +++ b/Sources/ParseSwift/Parse.swift @@ -30,6 +30,7 @@ internal func initialize(applicationId: String, deletingKeychainIfNeeded: Bool = false, httpAdditionalHeaders: [AnyHashable: Any]? = nil, maxConnectionAttempts: Int = 5, + liveQueryMaxConnectionAttempts: Int = 20, testing: Bool = false, authentication: ((URLAuthenticationChallenge, (URLSession.AuthChallengeDisposition, @@ -51,6 +52,7 @@ internal func initialize(applicationId: String, deletingKeychainIfNeeded: deletingKeychainIfNeeded, httpAdditionalHeaders: httpAdditionalHeaders, maxConnectionAttempts: maxConnectionAttempts, + liveQueryMaxConnectionAttempts: liveQueryMaxConnectionAttempts, authentication: authentication) configuration.isMigratingFromObjcSDK = migratingFromObjcSDK configuration.isTestingSDK = testing @@ -208,6 +210,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 +243,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 +266,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..8368713c0 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. @@ -116,8 +120,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 +144,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 +179,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 +204,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.swift b/Sources/ParseSwift/Types/ParseHealth.swift index 21ac9f836..83917faca 100644 --- a/Sources/ParseSwift/Types/ParseHealth.swift +++ b/Sources/ParseSwift/Types/ParseHealth.swift @@ -30,6 +30,10 @@ public struct ParseHealth: ParseTypeable { - 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`. */ diff --git a/Tests/ParseSwiftTests/APICommandMultipleAttemptsTests.swift b/Tests/ParseSwiftTests/APICommandMultipleAttemptsTests.swift index 106859b63..0715923fd 100644 --- a/Tests/ParseSwiftTests/APICommandMultipleAttemptsTests.swift +++ b/Tests/ParseSwiftTests/APICommandMultipleAttemptsTests.swift @@ -14,6 +14,7 @@ import FoundationNetworking import XCTest @testable import ParseSwift +// swiftlint:disable:next type_body_length class APICommandMultipleAttemptsTests: XCTestCase { struct Level: ParseObject { var objectId: String? @@ -111,7 +112,6 @@ class APICommandMultipleAttemptsTests: XCTestCase { func testErrorHTTPReturns400NoDataFromServer() { Parse.configuration.maxConnectionAttempts = 2 - let currentAttempts = Result() let originalError = ParseError(code: .otherCause, message: "Could not decode") MockURLProtocol.mockRequests { _ in return MockURLResponse(error: originalError) // Status code defaults to 400 @@ -131,10 +131,336 @@ class APICommandMultipleAttemptsTests: XCTestCase { XCTFail("Should have thrown an error") case .failure(let error): XCTAssertEqual(originalError.code, error.code) + expectation1.fulfill() + } + } + wait(for: [expectation1], timeout: 20.0) + } + + func testErrorHTTP429JSONInterval() async 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") + case .failure(let error): + XCTAssertEqual(parseError.code, error.code) Task { await currentAttempts.incrementAttempts() let current = await currentAttempts.attempts - expectation1.fulfill() + if current == Parse.configuration.maxConnectionAttempts { + expectation1.fulfill() + } + } + } + } + wait(for: [expectation1], timeout: 20.0) + } + + // swiftlint:disable:next function_body_length + func testErrorHTTP429JSONDate() async 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") + case .failure(let error): + XCTAssertEqual(parseError.code, error.code) + Task { + await currentAttempts.incrementAttempts() + let current = await currentAttempts.attempts + if current == Parse.configuration.maxConnectionAttempts { + expectation1.fulfill() + } + } + } + } + wait(for: [expectation1], timeout: 20.0) + } + + func testErrorHTTP429JSONNoHeader() async 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") + case .failure(let error): + XCTAssertEqual(parseError.code, error.code) + Task { + await currentAttempts.incrementAttempts() + let current = await currentAttempts.attempts + if current == Parse.configuration.maxConnectionAttempts { + expectation1.fulfill() + } + } + } + } + wait(for: [expectation1], timeout: 20.0) + } + + func testErrorHTTP503JSONInterval() async 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") + case .failure(let error): + XCTAssertEqual(parseError.code, error.code) + Task { + await currentAttempts.incrementAttempts() + let current = await currentAttempts.attempts + if current == Parse.configuration.maxConnectionAttempts { + expectation1.fulfill() + } + } + } + } + wait(for: [expectation1], timeout: 20.0) + } + + // swiftlint:disable:next function_body_length + func testErrorHTTP503JSONDate() async 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") + case .failure(let error): + XCTAssertEqual(parseError.code, error.code) + Task { + await currentAttempts.incrementAttempts() + let current = await currentAttempts.attempts + if current == Parse.configuration.maxConnectionAttempts { + expectation1.fulfill() + } + } + } + } + wait(for: [expectation1], timeout: 20.0) + } + + func testErrorHTTP503JSONNoHeader() async 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") + case .failure(let error): + XCTAssertEqual(parseError.code, error.code) + Task { + await currentAttempts.incrementAttempts() + let current = await currentAttempts.attempts + if current == Parse.configuration.maxConnectionAttempts { + expectation1.fulfill() + } } } } diff --git a/Tests/ParseSwiftTests/APICommandTests.swift b/Tests/ParseSwiftTests/APICommandTests.swift index 9434bb2a6..6695a338f 100644 --- a/Tests/ParseSwiftTests/APICommandTests.swift +++ b/Tests/ParseSwiftTests/APICommandTests.swift @@ -801,4 +801,16 @@ class APICommandTests: XCTestCase { XCTFail(error.localizedDescription) } } + + 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) + } } diff --git a/Tests/ParseSwiftTests/ParseLiveQueryTests.swift b/Tests/ParseSwiftTests/ParseLiveQueryTests.swift index 322ceb4d2..f0a344f40 100644 --- a/Tests/ParseSwiftTests/ParseLiveQueryTests.swift +++ b/Tests/ParseSwiftTests/ParseLiveQueryTests.swift @@ -57,6 +57,7 @@ class ParseLiveQueryTests: XCTestCase { clientKey: "clientKey", primaryKey: "primaryKey", serverURL: url, + liveQueryMaxConnectionAttempts: 1, testing: true) ParseLiveQuery.defaultClient = try ParseLiveQuery() } @@ -364,14 +365,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.isConnecting, false) XCTAssertEqual(client.clientId, "yolo") - XCTAssertEqual(client.attempts, ParseLiveQueryConstants.maxConnectionAttempts + 1) + XCTAssertEqual(client.attempts, Parse.configuration.liveQueryMaxConnectionAttempts + 1) } func testDisconnectedState() throws { @@ -681,6 +682,7 @@ class ParseLiveQueryTests: XCTestCase { } func testSubscribeConnected() throws { + Parse.configuration.liveQueryMaxConnectionAttempts = 2 let query = GameScore.query("points" > 9) guard let subscription = query.subscribe else { XCTFail("Should create subscription") @@ -1247,6 +1249,7 @@ class ParseLiveQueryTests: XCTestCase { } func testSubscriptionUpdate() throws { + Parse.configuration.liveQueryMaxConnectionAttempts = 2 let query = GameScore.query("points" > 9) guard let subscription = query.subscribe else { XCTFail("Should create subscription") @@ -1335,6 +1338,7 @@ class ParseLiveQueryTests: XCTestCase { } func testResubscribing() throws { + Parse.configuration.liveQueryMaxConnectionAttempts = 2 let query = GameScore.query("points" > 9) guard let subscription = query.subscribe else { XCTFail("Should create subscription") From 8acdf66e6b7babc046fee0791c3780707f109cc2 Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Thu, 12 Jan 2023 00:11:53 -0500 Subject: [PATCH 06/13] nits --- Tests/ParseSwiftTests/ParsePushAsyncTests.swift | 2 +- Tests/ParseSwiftTests/ParsePushCombineTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/ParseSwiftTests/ParsePushAsyncTests.swift b/Tests/ParseSwiftTests/ParsePushAsyncTests.swift index 1b9e58e42..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: ParseHealth.Status.ok) // BAKER: FIX to decoding error + 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 9a1b96f70..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: ParseHealth.Status.ok) // BAKER: Fix to decoding error + let results = HealthResponse(status: ParseHealth.Status.error) MockURLProtocol.mockRequests { _ in do { let encoded = try ParseCoding.jsonEncoder().encode(results) From fd0121dd6da7ea82295ef3aa7b6f4a06580c6a72 Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Thu, 12 Jan 2023 09:44:09 -0500 Subject: [PATCH 07/13] improve added tests and changelog --- CHANGELOG.md | 10 ++++----- .../APICommandMultipleAttemptsTests.swift | 22 +++++++++++++------ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7595c7d4..b8e8559f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,13 @@ [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 6. 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). +* (Breaking Change) Added a new ParseHealth.Status enum to support Parse Server 6. 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/Tests/ParseSwiftTests/APICommandMultipleAttemptsTests.swift b/Tests/ParseSwiftTests/APICommandMultipleAttemptsTests.swift index 0715923fd..10ee75288 100644 --- a/Tests/ParseSwiftTests/APICommandMultipleAttemptsTests.swift +++ b/Tests/ParseSwiftTests/APICommandMultipleAttemptsTests.swift @@ -96,12 +96,13 @@ class APICommandMultipleAttemptsTests: XCTestCase { 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 - if current == Parse.configuration.maxConnectionAttempts { + if current >= Parse.configuration.maxConnectionAttempts { expectation1.fulfill() } } @@ -129,6 +130,7 @@ class APICommandMultipleAttemptsTests: XCTestCase { switch result { case .success: XCTFail("Should have thrown an error") + expectation1.fulfill() case .failure(let error): XCTAssertEqual(originalError.code, error.code) expectation1.fulfill() @@ -176,12 +178,13 @@ class APICommandMultipleAttemptsTests: XCTestCase { 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 - if current == Parse.configuration.maxConnectionAttempts { + if current >= Parse.configuration.maxConnectionAttempts { expectation1.fulfill() } } @@ -238,12 +241,13 @@ class APICommandMultipleAttemptsTests: XCTestCase { 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 - if current == Parse.configuration.maxConnectionAttempts { + if current >= Parse.configuration.maxConnectionAttempts { expectation1.fulfill() } } @@ -288,12 +292,13 @@ class APICommandMultipleAttemptsTests: XCTestCase { 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 - if current == Parse.configuration.maxConnectionAttempts { + if current >= Parse.configuration.maxConnectionAttempts { expectation1.fulfill() } } @@ -341,12 +346,13 @@ class APICommandMultipleAttemptsTests: XCTestCase { 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 - if current == Parse.configuration.maxConnectionAttempts { + if current >= Parse.configuration.maxConnectionAttempts { expectation1.fulfill() } } @@ -403,12 +409,13 @@ class APICommandMultipleAttemptsTests: XCTestCase { 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 - if current == Parse.configuration.maxConnectionAttempts { + if current >= Parse.configuration.maxConnectionAttempts { expectation1.fulfill() } } @@ -453,12 +460,13 @@ class APICommandMultipleAttemptsTests: XCTestCase { 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 - if current == Parse.configuration.maxConnectionAttempts { + if current >= Parse.configuration.maxConnectionAttempts { expectation1.fulfill() } } From 1be2d58b854a623702ee50282d1421128f1241df Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Thu, 12 Jan 2023 10:39:26 -0500 Subject: [PATCH 08/13] fix flakey LiveQuery tests --- Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift | 3 ++- Sources/ParseSwift/Parse.swift | 2 ++ Sources/ParseSwift/Types/ParseConfiguration.swift | 3 ++- Tests/ParseSwiftTests/ParseLiveQueryTests.swift | 8 +++----- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift b/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift index 07e24f6b1..b2b179938 100644 --- a/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift +++ b/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift @@ -63,7 +63,8 @@ public final class ParseLiveQuery: NSObject { var clientId: String! var attempts: Int = 1 { willSet { - if newValue >= Parse.configuration.liveQueryMaxConnectionAttempts + 1 { + if newValue >= Parse.configuration.liveQueryMaxConnectionAttempts + 1 && + !Parse.configuration.isTestingLiveQueryDontCloseSocket { let error = ParseError(code: .otherCause, message: """ ParseLiveQuery Error: Reached max attempts of diff --git a/Sources/ParseSwift/Parse.swift b/Sources/ParseSwift/Parse.swift index f2fa045ec..dccddddf9 100644 --- a/Sources/ParseSwift/Parse.swift +++ b/Sources/ParseSwift/Parse.swift @@ -32,6 +32,7 @@ internal func initialize(applicationId: String, maxConnectionAttempts: Int = 5, liveQueryMaxConnectionAttempts: Int = 20, testing: Bool = false, + testLiveQueryDontCloseSocket: Bool = false, authentication: ((URLAuthenticationChallenge, (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) -> Void)? = nil) { @@ -56,6 +57,7 @@ internal func initialize(applicationId: String, authentication: authentication) configuration.isMigratingFromObjcSDK = migratingFromObjcSDK configuration.isTestingSDK = testing + configuration.isTestingLiveQueryDontCloseSocket = testLiveQueryDontCloseSocket initialize(configuration: configuration) } diff --git a/Sources/ParseSwift/Types/ParseConfiguration.swift b/Sources/ParseSwift/Types/ParseConfiguration.swift index 8368713c0..96eef740d 100644 --- a/Sources/ParseSwift/Types/ParseConfiguration.swift +++ b/Sources/ParseSwift/Types/ParseConfiguration.swift @@ -109,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 diff --git a/Tests/ParseSwiftTests/ParseLiveQueryTests.swift b/Tests/ParseSwiftTests/ParseLiveQueryTests.swift index f0a344f40..14d414d29 100644 --- a/Tests/ParseSwiftTests/ParseLiveQueryTests.swift +++ b/Tests/ParseSwiftTests/ParseLiveQueryTests.swift @@ -58,7 +58,8 @@ class ParseLiveQueryTests: XCTestCase { primaryKey: "primaryKey", serverURL: url, liveQueryMaxConnectionAttempts: 1, - testing: true) + testing: true, + testLiveQueryDontCloseSocket: true) ParseLiveQuery.defaultClient = try ParseLiveQuery() } @@ -369,7 +370,7 @@ class ParseLiveQueryTests: XCTestCase { 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, Parse.configuration.liveQueryMaxConnectionAttempts + 1) @@ -682,7 +683,6 @@ class ParseLiveQueryTests: XCTestCase { } func testSubscribeConnected() throws { - Parse.configuration.liveQueryMaxConnectionAttempts = 2 let query = GameScore.query("points" > 9) guard let subscription = query.subscribe else { XCTFail("Should create subscription") @@ -1249,7 +1249,6 @@ class ParseLiveQueryTests: XCTestCase { } func testSubscriptionUpdate() throws { - Parse.configuration.liveQueryMaxConnectionAttempts = 2 let query = GameScore.query("points" > 9) guard let subscription = query.subscribe else { XCTFail("Should create subscription") @@ -1338,7 +1337,6 @@ class ParseLiveQueryTests: XCTestCase { } func testResubscribing() throws { - Parse.configuration.liveQueryMaxConnectionAttempts = 2 let query = GameScore.query("points" > 9) guard let subscription = query.subscribe else { XCTFail("Should create subscription") From d0d458c782810243a03037ba9eeb8324dad138d3 Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Thu, 12 Jan 2023 10:52:52 -0500 Subject: [PATCH 09/13] fix failing linux/windows tests --- .github/workflows/ci.yml | 2 +- .../APICommandMultipleAttemptsTests.swift | 12 ++++++++++++ Tests/ParseSwiftTests/APICommandTests.swift | 12 ------------ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index edd7fa255..8d8d68b2d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -232,7 +232,7 @@ jobs: tag: 5.7.1-RELEASE - name: Build run: | - swift build -v + swift test --enable-test-discovery --enable-code-coverage -v - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: diff --git a/Tests/ParseSwiftTests/APICommandMultipleAttemptsTests.swift b/Tests/ParseSwiftTests/APICommandMultipleAttemptsTests.swift index 10ee75288..51d68a83b 100644 --- a/Tests/ParseSwiftTests/APICommandMultipleAttemptsTests.swift +++ b/Tests/ParseSwiftTests/APICommandMultipleAttemptsTests.swift @@ -60,6 +60,18 @@ class APICommandMultipleAttemptsTests: XCTestCase { } } + 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() async throws { let parseError = ParseError(code: .connectionFailed, message: "Connection failed") let errorKey = "error" diff --git a/Tests/ParseSwiftTests/APICommandTests.swift b/Tests/ParseSwiftTests/APICommandTests.swift index 6695a338f..9434bb2a6 100644 --- a/Tests/ParseSwiftTests/APICommandTests.swift +++ b/Tests/ParseSwiftTests/APICommandTests.swift @@ -801,16 +801,4 @@ class APICommandTests: XCTestCase { XCTFail(error.localizedDescription) } } - - 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) - } } From 92ffbb13f5183c5921ce45b9d710b4be523bf62b Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Thu, 12 Jan 2023 11:06:07 -0500 Subject: [PATCH 10/13] don't lint markdown files --- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 23 ++++++++++++++++++++--- CONTRIBUTING.md | 1 + README.md | 1 + 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8d8d68b2d..a90de8fa9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -214,7 +214,7 @@ jobs: tag: 5.5.1-RELEASE - name: Build and Test run: | - swift test --enable-test-discovery --enable-code-coverage -v + swift test --enable-test-discovery -v - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index b8e8559f5..6c03c06dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ + # Parse-Swift Changelog ### main @@ -8,13 +9,29 @@ [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 6. 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). + +* (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/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

From 734af1d478a1832eb56724cfc8e2a3f5eced42b5 Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Thu, 12 Jan 2023 11:16:41 -0500 Subject: [PATCH 11/13] nit CI --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a90de8fa9..067b28142 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -214,7 +214,7 @@ jobs: tag: 5.5.1-RELEASE - name: Build and Test run: | - swift test --enable-test-discovery -v + swift test --enable-test-discovery --enable-code-coverage -v - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: @@ -232,7 +232,7 @@ jobs: tag: 5.7.1-RELEASE - name: Build run: | - swift test --enable-test-discovery --enable-code-coverage -v + swift test --enable-test-discovery -v - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: From d411546ed6db921c8e7742cb0dda5da46053cfbc Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Thu, 12 Jan 2023 11:25:30 -0500 Subject: [PATCH 12/13] revert running tests on windows --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 067b28142..edd7fa255 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -232,7 +232,7 @@ jobs: tag: 5.7.1-RELEASE - name: Build run: | - swift test --enable-test-discovery -v + swift build -v - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: From 948335f5b29ece205b9b7d5483dda54ccf60d1f5 Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Thu, 12 Jan 2023 13:52:22 -0500 Subject: [PATCH 13/13] improve tests --- .../Types/ParseHealth+combine.swift | 2 +- .../APICommandMultipleAttemptsTests.swift | 62 ++++++++++++------- .../ParseHealthCombineTests.swift | 43 +++++++++++-- .../ParseLiveQueryAsyncTests.swift | 4 +- .../ParseLiveQueryCombineTests.swift | 4 +- 5 files changed, 84 insertions(+), 31 deletions(-) diff --git a/Sources/ParseSwift/Types/ParseHealth+combine.swift b/Sources/ParseSwift/Types/ParseHealth+combine.swift index a4977608e..d406631d7 100644 --- a/Sources/ParseSwift/Types/ParseHealth+combine.swift +++ b/Sources/ParseSwift/Types/ParseHealth+combine.swift @@ -25,7 +25,7 @@ public extension ParseHealth { switch result { case .success(let status): subject.send(status) - if status == .ok { + if status == .ok || status == .error { subject.send(completion: .finished) } case .failure(let error): diff --git a/Tests/ParseSwiftTests/APICommandMultipleAttemptsTests.swift b/Tests/ParseSwiftTests/APICommandMultipleAttemptsTests.swift index 51d68a83b..5c5bb25b9 100644 --- a/Tests/ParseSwiftTests/APICommandMultipleAttemptsTests.swift +++ b/Tests/ParseSwiftTests/APICommandMultipleAttemptsTests.swift @@ -14,6 +14,8 @@ import FoundationNetworking import XCTest @testable import ParseSwift +// swiftlint:disable function_body_length + // swiftlint:disable:next type_body_length class APICommandMultipleAttemptsTests: XCTestCase { struct Level: ParseObject { @@ -72,7 +74,7 @@ class APICommandMultipleAttemptsTests: XCTestCase { XCTAssertLessThan(date.timeIntervalSinceNow - computedDate, 1) } - func testErrorHTTP400JSON() async throws { + func testErrorHTTP400JSON() throws { let parseError = ParseError(code: .connectionFailed, message: "Connection failed") let errorKey = "error" let errorValue = "yarr" @@ -95,7 +97,7 @@ class APICommandMultipleAttemptsTests: XCTestCase { } } - let expectation1 = XCTestExpectation(description: "Wait") + let expectation1 = XCTestExpectation(description: "Wait 1") API.NonParseBodyCommand(method: .GET, path: .login, @@ -114,8 +116,10 @@ class APICommandMultipleAttemptsTests: XCTestCase { Task { await currentAttempts.incrementAttempts() let current = await currentAttempts.attempts - if current >= Parse.configuration.maxConnectionAttempts { - expectation1.fulfill() + DispatchQueue.main.async { + if current >= Parse.configuration.maxConnectionAttempts { + expectation1.fulfill() + } } } } @@ -151,7 +155,7 @@ class APICommandMultipleAttemptsTests: XCTestCase { wait(for: [expectation1], timeout: 20.0) } - func testErrorHTTP429JSONInterval() async throws { + func testErrorHTTP429JSONInterval() throws { let parseError = ParseError(code: .connectionFailed, message: "Connection failed") let errorKey = "error" let errorValue = "yarr" @@ -196,8 +200,10 @@ class APICommandMultipleAttemptsTests: XCTestCase { Task { await currentAttempts.incrementAttempts() let current = await currentAttempts.attempts - if current >= Parse.configuration.maxConnectionAttempts { - expectation1.fulfill() + DispatchQueue.main.async { + if current >= Parse.configuration.maxConnectionAttempts { + expectation1.fulfill() + } } } } @@ -205,8 +211,7 @@ class APICommandMultipleAttemptsTests: XCTestCase { wait(for: [expectation1], timeout: 20.0) } - // swiftlint:disable:next function_body_length - func testErrorHTTP429JSONDate() async throws { + func testErrorHTTP429JSONDate() throws { let parseError = ParseError(code: .connectionFailed, message: "Connection failed") let errorKey = "error" let errorValue = "yarr" @@ -259,8 +264,10 @@ class APICommandMultipleAttemptsTests: XCTestCase { Task { await currentAttempts.incrementAttempts() let current = await currentAttempts.attempts - if current >= Parse.configuration.maxConnectionAttempts { - expectation1.fulfill() + DispatchQueue.main.async { + if current >= Parse.configuration.maxConnectionAttempts { + expectation1.fulfill() + } } } } @@ -268,7 +275,7 @@ class APICommandMultipleAttemptsTests: XCTestCase { wait(for: [expectation1], timeout: 20.0) } - func testErrorHTTP429JSONNoHeader() async throws { + func testErrorHTTP429JSONNoHeader() throws { let parseError = ParseError(code: .connectionFailed, message: "Connection failed") let errorKey = "error" let errorValue = "yarr" @@ -310,8 +317,10 @@ class APICommandMultipleAttemptsTests: XCTestCase { Task { await currentAttempts.incrementAttempts() let current = await currentAttempts.attempts - if current >= Parse.configuration.maxConnectionAttempts { - expectation1.fulfill() + DispatchQueue.main.async { + if current >= Parse.configuration.maxConnectionAttempts { + expectation1.fulfill() + } } } } @@ -319,7 +328,7 @@ class APICommandMultipleAttemptsTests: XCTestCase { wait(for: [expectation1], timeout: 20.0) } - func testErrorHTTP503JSONInterval() async throws { + func testErrorHTTP503JSONInterval() throws { let parseError = ParseError(code: .connectionFailed, message: "Connection failed") let errorKey = "error" let errorValue = "yarr" @@ -364,8 +373,10 @@ class APICommandMultipleAttemptsTests: XCTestCase { Task { await currentAttempts.incrementAttempts() let current = await currentAttempts.attempts - if current >= Parse.configuration.maxConnectionAttempts { - expectation1.fulfill() + DispatchQueue.main.async { + if current >= Parse.configuration.maxConnectionAttempts { + expectation1.fulfill() + } } } } @@ -373,8 +384,7 @@ class APICommandMultipleAttemptsTests: XCTestCase { wait(for: [expectation1], timeout: 20.0) } - // swiftlint:disable:next function_body_length - func testErrorHTTP503JSONDate() async throws { + func testErrorHTTP503JSONDate() throws { let parseError = ParseError(code: .connectionFailed, message: "Connection failed") let errorKey = "error" let errorValue = "yarr" @@ -427,8 +437,10 @@ class APICommandMultipleAttemptsTests: XCTestCase { Task { await currentAttempts.incrementAttempts() let current = await currentAttempts.attempts - if current >= Parse.configuration.maxConnectionAttempts { - expectation1.fulfill() + DispatchQueue.main.async { + if current >= Parse.configuration.maxConnectionAttempts { + expectation1.fulfill() + } } } } @@ -436,7 +448,7 @@ class APICommandMultipleAttemptsTests: XCTestCase { wait(for: [expectation1], timeout: 20.0) } - func testErrorHTTP503JSONNoHeader() async throws { + func testErrorHTTP503JSONNoHeader() throws { let parseError = ParseError(code: .connectionFailed, message: "Connection failed") let errorKey = "error" let errorValue = "yarr" @@ -478,8 +490,10 @@ class APICommandMultipleAttemptsTests: XCTestCase { Task { await currentAttempts.incrementAttempts() let current = await currentAttempts.attempts - if current >= Parse.configuration.maxConnectionAttempts { - expectation1.fulfill() + DispatchQueue.main.async { + if current >= Parse.configuration.maxConnectionAttempts { + expectation1.fulfill() + } } } } diff --git a/Tests/ParseSwiftTests/ParseHealthCombineTests.swift b/Tests/ParseSwiftTests/ParseHealthCombineTests.swift index f1095710c..ce5c98866 100644 --- a/Tests/ParseSwiftTests/ParseHealthCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseHealthCombineTests.swift @@ -38,7 +38,8 @@ class ParseHealthCombineTests: XCTestCase { func testCheckOk() { var subscriptions = Set() - let expectation1 = XCTestExpectation(description: "Save") + let expectation1 = XCTestExpectation(description: "Received Value") + let expectation2 = XCTestExpectation(description: "Received Complete") let healthOfServer = ParseHealth.Status.ok let serverResponse = HealthResponse(status: healthOfServer) @@ -59,19 +60,53 @@ class ParseHealthCombineTests: XCTestCase { 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], timeout: 20.0) + wait(for: [expectation1, expectation2], timeout: 20.0) } func testCheckInitialized() { var subscriptions = Set() - let expectation1 = XCTestExpectation(description: "Save") + let expectation1 = XCTestExpectation(description: "Received Value") let healthOfServer = ParseHealth.Status.initialized let serverResponse = HealthResponse(status: healthOfServer) @@ -105,7 +140,7 @@ class ParseHealthCombineTests: XCTestCase { func testCheckStarting() { var subscriptions = Set() - let expectation1 = XCTestExpectation(description: "Save") + let expectation1 = XCTestExpectation(description: "Received Value") let healthOfServer = ParseHealth.Status.starting let serverResponse = HealthResponse(status: healthOfServer) 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) }