diff --git a/CHANGELOG.md b/CHANGELOG.md index d7f5b4c515..a030866bec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,8 @@ x.y.z Release notes (yyyy-MM-dd) ### Fixed * ([#????](https://github.com/realm/realm-swift/issues/????), since v?.?.?) -* None. +* Fixed a crash that would occur when an http error 401 or 403 is returned upon + opening a watch stream for a MongoDB collection. ([#8519](https://github.com/realm/realm-swift/issues/8519)) diff --git a/Realm/ObjectServerTests/RealmServer.swift b/Realm/ObjectServerTests/RealmServer.swift index e5c1264e93..ea68afc070 100644 --- a/Realm/ObjectServerTests/RealmServer.swift +++ b/Realm/ObjectServerTests/RealmServer.swift @@ -1187,6 +1187,14 @@ public class RealmServer: NSObject { return session.apps[appServerId].users[userId].delete() } + public func revokeUserSessions(_ appId: String, userId: String) -> Result { + guard let appServerId = try? RealmServer.shared.retrieveAppServerId(appId), + let session = session else { + return .failure(URLError(.unknown)) + } + return session.apps[appServerId].users[userId].logout.put([:]) + } + public func retrieveSchemaProperties(_ appId: String, className: String, _ completion: @escaping (Result<[String], Error>) -> Void) { guard let appServerId = try? RealmServer.shared.retrieveAppServerId(appId), let session = session else { diff --git a/Realm/ObjectServerTests/SwiftObjectServerTests.swift b/Realm/ObjectServerTests/SwiftObjectServerTests.swift index cdd0b5ab92..fc5b7b5477 100644 --- a/Realm/ObjectServerTests/SwiftObjectServerTests.swift +++ b/Realm/ObjectServerTests/SwiftObjectServerTests.swift @@ -1322,6 +1322,35 @@ class SwiftObjectServerTests: SwiftSyncTestCase { let objectCol = (obj!.anyCol.dynamicObject?.objectCol as? Object) XCTAssertEqual((objectCol?["firstName"] as? String), "Morty") } + + func testRevokeUserSessions() { + let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" + let password = randomString(10) + + app.emailPasswordAuth.registerUser(email: email, password: password).await(self) + + let syncUser = app.login(credentials: Credentials.emailPassword(email: email, password: password)).await(self) + + // Should succeed refreshing custom data + syncUser.refreshCustomData().await(self) + + _ = try? RealmServer.shared.revokeUserSessions(appId, userId: syncUser.id).get() + + // Should fail refreshing custom data. This verifies we're correctly handling the error in RLMSessionDelegate + syncUser.refreshCustomData().awaitFailure(self) + + // This verifies that we don't crash in RLMEventSessionDelegate when creating a watch stream + // with a revoked user. See https://github.com/realm/realm-swift/issues/8519 + let watchTestUtility = WatchTestUtility(testCase: self, expectError: true) + _ = syncUser.collection(for: Dog.self, app: app).watch(delegate: watchTestUtility) + watchTestUtility.waitForOpen() + watchTestUtility.waitForClose() + + let didCloseError = watchTestUtility.didCloseError! as NSError + XCTAssertNotNil(didCloseError) + XCTAssertEqual(didCloseError.localizedDescription, "URLSession HTTP error code: 403") + XCTAssertNil(didCloseError.userInfo[NSUnderlyingErrorKey]) + } } #endif // os(macOS) diff --git a/Realm/ObjectServerTests/WatchTestUtility.swift b/Realm/ObjectServerTests/WatchTestUtility.swift index c707e9e614..3bca8465cc 100644 --- a/Realm/ObjectServerTests/WatchTestUtility.swift +++ b/Realm/ObjectServerTests/WatchTestUtility.swift @@ -26,10 +26,13 @@ final public class WatchTestUtility: ChangeEventDelegate { private let openExpectation: XCTestExpectation private let closeExpectation: XCTestExpectation private var changeExpectation: XCTestExpectation? + private let expectError: Bool + public var didCloseError: Error? - public init(testCase: XCTestCase, matchingObjectId: ObjectId? = nil) { + public init(testCase: XCTestCase, matchingObjectId: ObjectId? = nil, expectError: Bool = false) { self.testCase = testCase self.matchingObjectId = matchingObjectId + self.expectError = expectError openExpectation = testCase.expectation(description: "Open watch stream") closeExpectation = testCase.expectation(description: "Close watch stream") } @@ -57,7 +60,13 @@ final public class WatchTestUtility: ChangeEventDelegate { } public func changeStreamDidClose(with error: Error?) { - XCTAssertNil(error) + if expectError { + XCTAssertNotNil(error) + } else { + XCTAssertNil(error) + } + + didCloseError = error closeExpectation.fulfill() } diff --git a/Realm/RLMNetworkTransport.mm b/Realm/RLMNetworkTransport.mm index edd12cf56e..d1af28e9d0 100644 --- a/Realm/RLMNetworkTransport.mm +++ b/Realm/RLMNetworkTransport.mm @@ -226,12 +226,14 @@ - (void)URLSession:(__unused NSURLSession *)session NSString *errorStatus = [NSString stringWithFormat:@"URLSession HTTP error code: %ld", (long)httpResponse.statusCode]; + + // error may be nil when the http status code is 401/403 - replace it with [NSNull null] in that case NSError *wrappedError = [NSError errorWithDomain:RLMAppErrorDomain code:RLMAppErrorHttpRequestFailed userInfo:@{NSLocalizedDescriptionKey: errorStatus, - RLMHTTPStatusCodeKey: @(httpResponse.statusCode), + RLMHTTPStatusCodeKey: @(httpResponse.statusCode), NSURLErrorFailingURLErrorKey: task.currentRequest.URL, - NSUnderlyingErrorKey: error}]; + NSUnderlyingErrorKey: error?: [NSNull null]}]; return [_subscriber didCloseWithError:wrappedError]; }