From 535097ab963710cf4137f858e262f198521ff72f Mon Sep 17 00:00:00 2001 From: Thomas Goyne Date: Thu, 14 Dec 2023 14:13:28 -0800 Subject: [PATCH 01/10] Allow creating notifiers inside write transactions before the first change Core has allowed this for a while, but we had our own validation which made it not work. --- CHANGELOG.md | 20 ++++++++++++++++++++ Realm/RLMCollection.mm | 7 ++++++- Realm/RLMObjectBase.mm | 9 +++++++-- Realm/RLMRealm.mm | 3 --- Realm/Tests/AsyncTests.mm | 14 +++++++++----- RealmSwift/Tests/ObjectTests.swift | 2 -- 6 files changed, 42 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d81a82d72..daedb02600 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +x.y.z Release notes (yyyy-MM-dd) +============================================================= +### Enhancements +* None. + +### Fixed +* Registering new notifications inside write transactions before actually + making any changes is now actually allowed. This was supposed to be allowed + in 10.39.1, but it did not actually work due to some redundant validation. + +### Compatibility +* Realm Studio: 14.0.1 or later. +* APIs are backwards compatible with all previous releases in the 10.x.y series. +* Carthage release for Swift is built with Xcode 15.1.0. +* CocoaPods: 1.10 or later. +* Xcode: 14.1-15.1.0. + +### Internal +* Upgraded realm-core from ? to ? + 10.45.1 Release notes (2023-12-18) ============================================================= diff --git a/Realm/RLMCollection.mm b/Realm/RLMCollection.mm index aebaf1eb63..6826e302db 100644 --- a/Realm/RLMCollection.mm +++ b/Realm/RLMCollection.mm @@ -506,7 +506,12 @@ - (bool)invalidate { RLMClassInfo *info = collection.objectInfo; if (!queue) { [realm verifyNotificationsAreSupported:true]; - token->_token = [collection addNotificationCallback:block keyPaths:info->keyPathArrayFromStringArray(keyPaths)]; + try { + token->_token = [collection addNotificationCallback:block keyPaths:info->keyPathArrayFromStringArray(keyPaths)]; + } + catch (const realm::Exception& e) { + @throw RLMException(e); + } return token; } diff --git a/Realm/RLMObjectBase.mm b/Realm/RLMObjectBase.mm index 8dbf6f48b9..a31310fe43 100644 --- a/Realm/RLMObjectBase.mm +++ b/Realm/RLMObjectBase.mm @@ -752,8 +752,13 @@ - (void)observe:(RLMObjectBase *)obj completion(); } }; - _token = _object.add_notification_callback(ObjectChangeCallbackWrapper{block, obj, completion}, - obj->_info->keyPathArrayFromStringArray(keyPaths)); + try { + _token = _object.add_notification_callback(ObjectChangeCallbackWrapper{block, obj, completion}, + obj->_info->keyPathArrayFromStringArray(keyPaths)); + } + catch (const realm::Exception& e) { + @throw RLMException(e); + } } - (void)registrationComplete:(void (^)())completion { diff --git a/Realm/RLMRealm.mm b/Realm/RLMRealm.mm index 9109d0e0ba..268a65ad6f 100644 --- a/Realm/RLMRealm.mm +++ b/Realm/RLMRealm.mm @@ -550,9 +550,6 @@ - (void)verifyNotificationsAreSupported:(bool)isCollection { if (_realm->config().automatic_change_notifications && !_realm->can_deliver_notifications()) { @throw RLMException(@"Can only add notification blocks from within runloops."); } - if (isCollection && _realm->is_in_transaction()) { - @throw RLMException(@"Cannot register notification blocks from within write transactions."); - } } - (RLMNotificationToken *)addNotificationBlock:(RLMNotificationBlock)block { diff --git a/Realm/Tests/AsyncTests.mm b/Realm/Tests/AsyncTests.mm index 819c46b45b..854297da09 100644 --- a/Realm/Tests/AsyncTests.mm +++ b/Realm/Tests/AsyncTests.mm @@ -859,11 +859,15 @@ - (void)testAsyncNotSupportedForReadOnlyRealms { }]); } -- (void)testAsyncNotSupportedInWriteTransactions { - [RLMRealm.defaultRealm transactionWithBlock:^{ - XCTAssertThrows([IntObject.allObjects addNotificationBlock:^(RLMResults *results, RLMCollectionChange *change, NSError *error) { - XCTFail(@"should not be called"); - }]); +- (void)testAsyncNotSupportedAfterMakingChangesInWriteTransactions { + RLMRealm *realm = [RLMRealm defaultRealm]; + [realm transactionWithBlock:^{ + XCTAssertNoThrow([IntObject.allObjects addNotificationBlock:^(RLMResults *, RLMCollectionChange *, NSError *) {}]); + [IntObject createInRealm:realm withValue:@[@0]]; + RLMAssertThrowsWithReason([IntObject.allObjects addNotificationBlock:^(RLMResults *, RLMCollectionChange *, NSError *) {}], + @"Cannot create asynchronous query after making changes in a write transaction."); + RLMAssertThrowsWithReason([IntObject.allObjects[0] addNotificationBlock:^(BOOL, NSArray *, NSError *) {}], + @"Cannot create asynchronous query after making changes in a write transaction."); }]; } diff --git a/RealmSwift/Tests/ObjectTests.swift b/RealmSwift/Tests/ObjectTests.swift index 06918f6964..a29697d519 100644 --- a/RealmSwift/Tests/ObjectTests.swift +++ b/RealmSwift/Tests/ObjectTests.swift @@ -1580,7 +1580,6 @@ class ObjectTests: TestCase { // This test consistently crashes inside the Swift runtime when building // with SPM. - #if !SWIFT_PACKAGE @MainActor @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) func testAsyncSequenceObserve() async throws { @@ -1608,7 +1607,6 @@ class ObjectTests: TestCase { task.cancel() _ = try await task.value } - #endif @MainActor @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) From 0b16b60f505d0bde33335a53b2997dcf60315674 Mon Sep 17 00:00:00 2001 From: Thomas Goyne Date: Mon, 2 Oct 2023 16:22:23 -0700 Subject: [PATCH 02/10] Release resources in RLMAsyncSubscriptionTask earlier The RLMSyncSubscriptionSet was retained until the task was deallocated even after the task completed, which is not strictly incorrect but made a test unreliable. --- .../SwiftFlexibleSyncServerTests.swift | 31 ++++++------------- Realm/RLMAsyncTask.mm | 18 +++++------ Realm/RLMResults.mm | 17 +++++----- 3 files changed, 27 insertions(+), 39 deletions(-) diff --git a/Realm/ObjectServerTests/SwiftFlexibleSyncServerTests.swift b/Realm/ObjectServerTests/SwiftFlexibleSyncServerTests.swift index 9616cdb3f4..b250b052ac 100644 --- a/Realm/ObjectServerTests/SwiftFlexibleSyncServerTests.swift +++ b/Realm/ObjectServerTests/SwiftFlexibleSyncServerTests.swift @@ -1675,30 +1675,19 @@ XCTAssertNotNil(realm.subscriptions.first(ofType: SwiftPerson.self) { $0.age > 5 let realm = try openFlexibleSyncRealm() realm.syncSession!.suspend() - let expectation = XCTestExpectation(description: "doesn't wait longer than expected") - Task { - let timeout = 2.0 - do { - _ = try await realm.objects(SwiftPerson.self).where { $0.age >= 8 }.subscribe(waitForSync: .always, timeout: timeout) - } catch let error as NSError { - expectation.fulfill() - XCTAssertEqual(error.code, Int(ETIMEDOUT)) - XCTAssertEqual(error.domain, NSPOSIXErrorDomain) - XCTAssertEqual(error.localizedDescription, "Waiting for update timed out after \(timeout) seconds.") - } - } - await fulfillment(of: [expectation], timeout: 3.0) - - // resume sync session and wait for subscription otherwise tear - // down can't complete successfully - realm.syncSession!.resume() - let start = Date() - while realm.subscriptions.state != .complete && start.timeIntervalSinceNow > -10.0 { - sleep(1) + let timeout = 1.0 + do { + _ = try await realm.objects(SwiftPerson.self).where { $0.age >= 8 } + .subscribe(waitForSync: .always, timeout: timeout) + XCTFail("subscribe did not time out") + } catch let error as NSError { + XCTAssertEqual(error.code, Int(ETIMEDOUT)) + XCTAssertEqual(error.domain, NSPOSIXErrorDomain) + XCTAssertEqual(error.localizedDescription, "Waiting for update timed out after \(timeout) seconds.") } - XCTAssertEqual(realm.subscriptions.state, .complete) } + @MainActor func testSubscribeTimeoutSucceeds() async throws { try await populateSwiftPerson() diff --git a/Realm/RLMAsyncTask.mm b/Realm/RLMAsyncTask.mm index f32d341271..fe5833c99f 100644 --- a/Realm/RLMAsyncTask.mm +++ b/Realm/RLMAsyncTask.mm @@ -504,9 +504,8 @@ - (instancetype)initWithSubscriptionSet:(RLMSyncSubscriptionSet *)subscriptionSe } - (void)waitForSubscription { - std::lock_guard lock(_mutex); - if (_timeout != 0) { + std::lock_guard lock(_mutex); // Setup timer dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_timeout * NSEC_PER_SEC)); // If the call below doesn't return after `time` seconds, the internal completion is called with an error. @@ -522,7 +521,8 @@ - (void)waitForSubscription { [self waitForSync]; } --(void)waitForSync { +- (void)waitForSync { + std::lock_guard lock(_mutex); if (_completion) { _subscriptionSet->_subscriptionSet->get_state_change_notification(realm::sync::SubscriptionSet::State::Complete) .get_async([self](realm::StatusWith state) noexcept { @@ -532,16 +532,16 @@ -(void)waitForSync { } } --(void)invokeCompletionWithError:(NSError * _Nullable)error { +- (void)invokeCompletionWithError:(NSError * _Nullable)error { void (^completion)(NSError *); { std::lock_guard lock(_mutex); std::swap(completion, _completion); - } - - if (_worker) { - dispatch_block_cancel(_worker); - _worker = nil; + if (_worker) { + dispatch_block_cancel(_worker); + _worker = nil; + } + _subscriptionSet = nil; } if (completion) { diff --git a/Realm/RLMResults.mm b/Realm/RLMResults.mm index 0be42e6d03..699eac7bf8 100644 --- a/Realm/RLMResults.mm +++ b/Realm/RLMResults.mm @@ -574,18 +574,17 @@ - (void)completionWithThreadSafeReference:(RLMThreadSafeReference * _Nullable)re confinedTo:(RLMScheduler *)confinement completion:(RLMResultsCompletionBlock)completion error:(NSError *_Nullable)error { - auto tsr = (error != nil) ? nil : reference; RLMRealmConfiguration *configuration = _realm.configuration; [confinement invoke:^{ - if (tsr) { - NSError *err; - RLMRealm *realm = [RLMRealm realmWithConfiguration:configuration error:&err]; - RLMResults *collection = [realm resolveThreadSafeReference:tsr]; - collection.associatedSubscriptionId = self.associatedSubscriptionId; - completion(collection, err); - } else { - completion(nil, error); + if (error) { + return completion(nil, error); } + + NSError *err; + RLMRealm *realm = [RLMRealm realmWithConfiguration:configuration error:&err]; + RLMResults *collection = [realm resolveThreadSafeReference:reference]; + collection.associatedSubscriptionId = self.associatedSubscriptionId; + completion(collection, err); }]; } From c830376454480c000e40f5635414b5141197ccc7 Mon Sep 17 00:00:00 2001 From: Thomas Goyne Date: Tue, 3 Oct 2023 09:22:41 -0700 Subject: [PATCH 03/10] Eliminate a bunch of calls to XCTAssertNotNil on non-optional types This compiles because the value can be implicitly converted to an optional, but it isn't actually checking anything. Even if the non-optional has an invalid nil value, it ends up as `.some(invalid)` and will pass the test. --- .../SwiftAsymmetricSyncServerTests.swift | 4 -- .../SwiftFlexibleSyncServerTests.swift | 43 +------------------ .../SwiftObjectServerTests.swift | 3 +- .../SwiftUIServerTests.swift | 22 ---------- 4 files changed, 4 insertions(+), 68 deletions(-) diff --git a/Realm/ObjectServerTests/SwiftAsymmetricSyncServerTests.swift b/Realm/ObjectServerTests/SwiftAsymmetricSyncServerTests.swift index 34c6eda94a..87bb5415c6 100644 --- a/Realm/ObjectServerTests/SwiftAsymmetricSyncServerTests.swift +++ b/Realm/ObjectServerTests/SwiftAsymmetricSyncServerTests.swift @@ -227,7 +227,6 @@ extension SwiftAsymmetricSyncTests { @MainActor func testCreateAsymmetricObject() async throws { let realm = try await realm() - XCTAssertNotNil(realm) // Create Asymmetric Objects try realm.write { @@ -248,7 +247,6 @@ extension SwiftAsymmetricSyncTests { func testPropertyTypesAsymmetricObject() async throws { let collection = try await setupCollection("SwiftObjectAsymmetric") let realm = try await realm() - XCTAssertNotNil(realm) // Create Asymmetric Objects try realm.write { @@ -276,7 +274,6 @@ extension SwiftAsymmetricSyncTests { @MainActor func testCreateHugeAsymmetricObject() async throws { let realm = try await realm() - XCTAssertNotNil(realm) // Create Asymmetric Objects try realm.write { @@ -293,7 +290,6 @@ extension SwiftAsymmetricSyncTests { func testCreateCustomAsymmetricObject() async throws { let collection = try await setupCollection("SwiftCustomColumnAsymmetricObject") let realm = try await realm() - XCTAssertNotNil(realm) let objectId = ObjectId.generate() let valuesDictionary: [String: Any] = ["id": objectId, diff --git a/Realm/ObjectServerTests/SwiftFlexibleSyncServerTests.swift b/Realm/ObjectServerTests/SwiftFlexibleSyncServerTests.swift index b250b052ac..d17e9ff7df 100644 --- a/Realm/ObjectServerTests/SwiftFlexibleSyncServerTests.swift +++ b/Realm/ObjectServerTests/SwiftFlexibleSyncServerTests.swift @@ -39,8 +39,7 @@ class SwiftFlexibleSyncTests: SwiftSyncTestCase { } func testFlexibleSyncOpenRealm() throws { - let realm = try openFlexibleSyncRealm() - XCTAssertNotNil(realm) + _ = try openFlexibleSyncRealm() } func testGetSubscriptionsWhenLocalRealm() throws { @@ -72,8 +71,7 @@ class SwiftFlexibleSyncTests: SwiftSyncTestCase { func testWriteEmptyBlock() throws { let realm = try openFlexibleSyncRealm() let subscriptions = realm.subscriptions - subscriptions.update { - } + subscriptions.update {} XCTAssertEqual(subscriptions.count, 0) } @@ -654,11 +652,9 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { } let realm = try flexibleSyncRealm() - XCTAssertNotNil(realm) checkCount(expected: 0, realm, SwiftPerson.self) let subscriptions = realm.subscriptions - XCTAssertNotNil(subscriptions) XCTAssertEqual(subscriptions.count, 0) waitForDownloads(for: realm) @@ -676,11 +672,9 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { } let realm = try flexibleSyncRealm() - XCTAssertNotNil(realm) checkCount(expected: 0, realm, SwiftPerson.self) let subscriptions = realm.subscriptions - XCTAssertNotNil(subscriptions) XCTAssertEqual(subscriptions.count, 0) let ex = expectation(description: "state change complete") @@ -716,11 +710,9 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { } let realm = try flexibleSyncRealm() - XCTAssertNotNil(realm) checkCount(expected: 0, realm, SwiftPerson.self) let subscriptions = realm.subscriptions - XCTAssertNotNil(subscriptions) XCTAssertEqual(subscriptions.count, 0) let ex = expectation(description: "state change complete") @@ -759,11 +751,9 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { } let realm = try flexibleSyncRealm() - XCTAssertNotNil(realm) checkCount(expected: 0, realm, SwiftPerson.self) let subscriptions = realm.subscriptions - XCTAssertNotNil(subscriptions) XCTAssertEqual(subscriptions.count, 0) let ex = expectation(description: "state change complete") @@ -818,11 +808,9 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { } let realm = try flexibleSyncRealm() - XCTAssertNotNil(realm) checkCount(expected: 0, realm, SwiftPerson.self) let subscriptions = realm.subscriptions - XCTAssertNotNil(subscriptions) XCTAssertEqual(subscriptions.count, 0) let ex = expectation(description: "state change complete") @@ -881,11 +869,9 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { } let realm = try flexibleSyncRealm() - XCTAssertNotNil(realm) checkCount(expected: 0, realm, SwiftPerson.self) let subscriptions = realm.subscriptions - XCTAssertNotNil(subscriptions) XCTAssertEqual(subscriptions.count, 0) let ex = expectation(description: "state change complete") @@ -941,11 +927,9 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { } let realm = try flexibleSyncRealm() - XCTAssertNotNil(realm) checkCount(expected: 0, realm, SwiftPerson.self) let subscriptions = realm.subscriptions - XCTAssertNotNil(subscriptions) XCTAssertEqual(subscriptions.count, 0) let ex = expectation(description: "state change complete") @@ -1008,7 +992,6 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { } let realm = try Realm(configuration: config) let subscriptions = realm.subscriptions - XCTAssertNotNil(subscriptions) XCTAssertEqual(subscriptions.count, 1) checkCount(expected: 0, realm, SwiftPerson.self) @@ -1111,11 +1094,9 @@ extension SwiftFlexibleSyncServerTests { } let realm = try await flexibleSyncRealm() - XCTAssertNotNil(realm) checkCount(expected: 0, realm, SwiftPerson.self) let subscriptions = realm.subscriptions - XCTAssertNotNil(subscriptions) XCTAssertEqual(subscriptions.count, 0) try await subscriptions.update { @@ -1131,8 +1112,6 @@ extension SwiftFlexibleSyncServerTests { @MainActor func testStates() async throws { let realm = try await flexibleSyncRealm() - XCTAssertNotNil(realm) - let subscriptions = realm.subscriptions XCTAssertEqual(subscriptions.count, 0) @@ -1172,11 +1151,9 @@ extension SwiftFlexibleSyncServerTests { } let realm = try await flexibleSyncRealm() - XCTAssertNotNil(realm) checkCount(expected: 0, realm, SwiftPerson.self) let subscriptions = realm.subscriptions - XCTAssertNotNil(subscriptions) XCTAssertEqual(subscriptions.count, 0) try await subscriptions.update { @@ -1191,8 +1168,6 @@ extension SwiftFlexibleSyncServerTests { func testFlexibleSyncNotInitialSubscriptions() async throws { let config = try await flexibleSyncConfig() let realm = try await Realm(configuration: config, downloadBeforeOpen: .always) - XCTAssertNotNil(realm) - XCTAssertEqual(realm.subscriptions.count, 0) } @@ -1218,8 +1193,6 @@ extension SwiftFlexibleSyncServerTests { config.objectTypes = [SwiftPerson.self] } let realm = try await Realm(configuration: config, downloadBeforeOpen: .once) - XCTAssertNotNil(realm) - XCTAssertEqual(realm.subscriptions.count, 1) checkCount(expected: 10, realm, SwiftPerson.self) } @@ -1237,7 +1210,6 @@ extension SwiftFlexibleSyncServerTests { config.objectTypes = [SwiftPerson.self] } let realm = try await Realm(configuration: config, downloadBeforeOpen: .once) - XCTAssertNotNil(realm) XCTAssertEqual(realm.subscriptions.count, 1) let realm2 = try await Realm(configuration: config, downloadBeforeOpen: .once) @@ -1260,7 +1232,6 @@ extension SwiftFlexibleSyncServerTests { config.objectTypes = [SwiftPerson.self] } let realm = try await Realm(configuration: config, downloadBeforeOpen: .once) - XCTAssertNotNil(realm) XCTAssertEqual(realm.subscriptions.count, 1) let realm2 = try await Realm(configuration: config, downloadBeforeOpen: .once) @@ -1302,14 +1273,12 @@ extension SwiftFlexibleSyncServerTests { let c = config _ = try await Task { @MainActor in let realm = try await Realm(configuration: c, downloadBeforeOpen: .always) - XCTAssertNotNil(realm) XCTAssertEqual(realm.subscriptions.count, 1) checkCount(expected: 9, realm, SwiftTypesSyncObject.self) }.value _ = try await Task { @MainActor in let realm = try await Realm(configuration: c, downloadBeforeOpen: .always) - XCTAssertNotNil(realm) XCTAssertEqual(realm.subscriptions.count, 2) checkCount(expected: 19, realm, SwiftTypesSyncObject.self) }.value @@ -1744,7 +1713,6 @@ XCTAssertNotNil(realm.subscriptions.first(ofType: SwiftPerson.self) { $0.age > 5 }) config.objectTypes = [SwiftCustomColumnObject.self] let realm = try await Realm(configuration: config, downloadBeforeOpen: .once) - XCTAssertNotNil(realm) XCTAssertEqual(realm.subscriptions.count, 1) let foundObject = realm.object(ofType: SwiftCustomColumnObject.self, forPrimaryKey: objectId) @@ -1783,7 +1751,6 @@ XCTAssertNotNil(realm.subscriptions.first(ofType: SwiftPerson.self) { $0.age > 5 }) config.objectTypes = [SwiftCustomColumnObject.self] let realm = try await Realm(configuration: config, downloadBeforeOpen: .once) - XCTAssertNotNil(realm) XCTAssertEqual(realm.subscriptions.count, 1) checkCount(expected: 2, realm, SwiftCustomColumnObject.self) @@ -1823,7 +1790,6 @@ XCTAssertNotNil(realm.subscriptions.first(ofType: SwiftPerson.self) { $0.age > 5 }) config.objectTypes = [SwiftCustomColumnObject.self] let realm = try await Realm(configuration: config, downloadBeforeOpen: .once) - XCTAssertNotNil(realm) XCTAssertEqual(realm.subscriptions.count, 1) checkCount(expected: 2, realm, SwiftCustomColumnObject.self) @@ -1865,7 +1831,6 @@ XCTAssertNotNil(realm.subscriptions.first(ofType: SwiftPerson.self) { $0.age > 5 }) config.objectTypes = [SwiftCustomColumnObject.self] let realm = try await Realm(configuration: config, downloadBeforeOpen: .once) - XCTAssertNotNil(realm) XCTAssertEqual(realm.subscriptions.count, 1) checkCount(expected: 2, realm, SwiftCustomColumnObject.self) @@ -1903,11 +1868,9 @@ extension SwiftFlexibleSyncServerTests { } let realm = try flexibleSyncRealm() - XCTAssertNotNil(realm) checkCount(expected: 0, realm, SwiftPerson.self) let subscriptions = realm.subscriptions - XCTAssertNotNil(subscriptions) XCTAssertEqual(subscriptions.count, 0) let ex = expectation(description: "state change complete") @@ -1928,11 +1891,9 @@ extension SwiftFlexibleSyncServerTests { #if false // FIXME: this is no longer an error and needs to be updated to something which is func testFlexibleSyncCombineWriteFails() throws { let realm = try flexibleSyncRealm() - XCTAssertNotNil(realm) checkCount(expected: 0, realm, SwiftPerson.self) let subscriptions = realm.subscriptions - XCTAssertNotNil(subscriptions) XCTAssertEqual(subscriptions.count, 0) let ex = expectation(description: "state change error") diff --git a/Realm/ObjectServerTests/SwiftObjectServerTests.swift b/Realm/ObjectServerTests/SwiftObjectServerTests.swift index eb76354999..e09cfaf4c3 100644 --- a/Realm/ObjectServerTests/SwiftObjectServerTests.swift +++ b/Realm/ObjectServerTests/SwiftObjectServerTests.swift @@ -393,7 +393,8 @@ class SwiftObjectServerTests: SwiftSyncTestCase { XCTAssertEqual(error.code, .clientResetError) let resetInfo = try XCTUnwrap(error.clientResetInfo()) XCTAssertTrue(resetInfo.0.contains("mongodb-realm/\(self.appId)/recovered-realms/recovered_realm")) - XCTAssertNotNil(realm) + + realm.invalidate() // ensure realm is kept alive to here } func testClientResetManualInitiation() throws { diff --git a/Realm/ObjectServerTests/SwiftUIServerTests.swift b/Realm/ObjectServerTests/SwiftUIServerTests.swift index e5b6d94639..8304087604 100644 --- a/Realm/ObjectServerTests/SwiftUIServerTests.swift +++ b/Realm/ObjectServerTests/SwiftUIServerTests.swift @@ -67,7 +67,6 @@ class SwiftUIServerTests: SwiftSyncTestCase { let ex = expectation(description: "download-realm-async-open") asyncOpen(user: user, appId: appId, partitionValue: #function) { asyncOpenState in if case let .open(realm) = asyncOpenState { - XCTAssertNotNil(realm) ex.fulfill() } } @@ -83,7 +82,6 @@ class SwiftUIServerTests: SwiftSyncTestCase { let ex = expectation(description: "download-populated-realm-async-open") asyncOpen(user: user, appId: appId, partitionValue: #function) { asyncOpenState in if case let .open(realm) = asyncOpenState { - XCTAssertNotNil(realm) self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) ex.fulfill() } @@ -158,7 +156,6 @@ class SwiftUIServerTests: SwiftSyncTestCase { let ex = expectation(description: "download-cached-app-async-open") asyncOpen(user: user, partitionValue: #function) { asyncOpenState in if case let .open(realm) = asyncOpenState { - XCTAssertNotNil(realm) self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) ex.fulfill() } @@ -191,7 +188,6 @@ class SwiftUIServerTests: SwiftSyncTestCase { let ex = expectation(description: "download-partition-value-async-open") asyncOpen(user: user, partitionValue: partitionValueA) { asyncOpenState in if case let .open(realm) = asyncOpenState { - XCTAssertNotNil(realm) self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) ex.fulfill() } @@ -200,7 +196,6 @@ class SwiftUIServerTests: SwiftSyncTestCase { let ex2 = expectation(description: "download-other-partition-value-async-open") asyncOpen(user: user, partitionValue: partitionValueB) { asyncOpenState in if case let .open(realm) = asyncOpenState { - XCTAssertNotNil(realm) self.checkCount(expected: 0, realm, SwiftHugeSyncObject.self) ex2.fulfill() } @@ -224,7 +219,6 @@ class SwiftUIServerTests: SwiftSyncTestCase { let ex = expectation(description: "test-multiuser1-app-async-open") asyncOpen(user: syncUser2, appId: appId, partitionValue: partitionValueB) { asyncOpenState in if case let .open(realm) = asyncOpenState { - XCTAssertNotNil(realm) self.checkCount(expected: 0, realm, SwiftHugeSyncObject.self) ex.fulfill() } @@ -237,7 +231,6 @@ class SwiftUIServerTests: SwiftSyncTestCase { let ex2 = expectation(description: "test-multiuser2-app-async-open") asyncOpen(user: syncUser2, appId: appId, partitionValue: partitionValueA) { asyncOpenState in if case let .open(realm) = asyncOpenState { - XCTAssertNotNil(realm) self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) ex2.fulfill() } @@ -258,7 +251,6 @@ class SwiftUIServerTests: SwiftSyncTestCase { let ex = expectation(description: "download-realm-anonymous-user-async-open") asyncOpen(user: anonymousUser, appId: appId, partitionValue: partitionValueA) { asyncOpenState in if case let .open(realm) = asyncOpenState { - XCTAssertNotNil(realm) self.checkCount(expected: 0, realm, SwiftHugeSyncObject.self) ex.fulfill() } @@ -269,7 +261,6 @@ class SwiftUIServerTests: SwiftSyncTestCase { let ex2 = expectation(description: "download-realm-after-logout-async-open") asyncOpen(user: user, appId: appId, partitionValue: partitionValueB) { asyncOpenState in if case let .open(realm) = asyncOpenState { - XCTAssertNotNil(realm) self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) ex2.fulfill() } @@ -297,7 +288,6 @@ class SwiftUIServerTests: SwiftSyncTestCase { let ex = expectation(description: "download-realm-auto-open") autoOpen(user: user, appId: appId, partitionValue: #function) { autoOpenState in if case let .open(realm) = autoOpenState { - XCTAssertNotNil(realm) ex.fulfill() } } @@ -314,7 +304,6 @@ class SwiftUIServerTests: SwiftSyncTestCase { let ex = expectation(description: "download-populated-realm-auto-open") autoOpen(user: user, appId: appId, partitionValue: #function) { autoOpenState in if case let .open(realm) = autoOpenState { - XCTAssertNotNil(realm) self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) ex.fulfill() } @@ -437,7 +426,6 @@ class SwiftUIServerTests: SwiftSyncTestCase { let ex = expectation(description: "download-cached-app-auto-open") autoOpen(user: user, partitionValue: #function) { autoOpenState in if case let .open(realm) = autoOpenState { - XCTAssertNotNil(realm) self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) ex.fulfill() } @@ -476,7 +464,6 @@ class SwiftUIServerTests: SwiftSyncTestCase { let ex = expectation(description: "test-multiuser1-app-auto-open") autoOpen(user: syncUser2, appId: appId, partitionValue: partitionValueB) { autoOpenState in if case let .open(realm) = autoOpenState { - XCTAssertNotNil(realm) self.checkCount(expected: 0, realm, SwiftHugeSyncObject.self) ex.fulfill() } @@ -489,7 +476,6 @@ class SwiftUIServerTests: SwiftSyncTestCase { let ex2 = expectation(description: "test-multiuser2-app-auto-open") autoOpen(user: syncUser1, appId: appId, partitionValue: partitionValueA) { autoOpenState in if case let .open(realm) = autoOpenState { - XCTAssertNotNil(realm) self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) ex2.fulfill() } @@ -504,7 +490,6 @@ class SwiftUIServerTests: SwiftSyncTestCase { let ex = expectation(description: "download-realm-anonymous-user-auto-open") autoOpen(user: anonymousUser, appId: appId, partitionValue: partitionValueA) { autoOpenState in if case let .open(realm) = autoOpenState { - XCTAssertNotNil(realm) self.checkCount(expected: 0, realm, SwiftHugeSyncObject.self) ex.fulfill() } @@ -521,7 +506,6 @@ class SwiftUIServerTests: SwiftSyncTestCase { let ex2 = expectation(description: "download-realm-after-logout-auto-open") autoOpen(user: user, appId: appId, partitionValue: partitionValueB) { autoOpenState in if case let .open(realm) = autoOpenState { - XCTAssertNotNil(realm) self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) ex2.fulfill() } @@ -541,7 +525,6 @@ class SwiftUIServerTests: SwiftSyncTestCase { let ex = expectation(description: "download-partition-value-auto-open") autoOpen(user: user, partitionValue: partitionValueA) { autoOpenState in if case let .open(realm) = autoOpenState { - XCTAssertNotNil(realm) self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) ex.fulfill() } @@ -550,7 +533,6 @@ class SwiftUIServerTests: SwiftSyncTestCase { let ex2 = expectation(description: "download-other-partition-value-auto-open") autoOpen(user: user, partitionValue: partitionValueB) { autoOpenState in if case let .open(realm) = autoOpenState { - XCTAssertNotNil(realm) self.checkCount(expected: 0, realm, SwiftHugeSyncObject.self) ex2.fulfill() } @@ -571,7 +553,6 @@ class SwiftUIServerTests: SwiftSyncTestCase { let ex = expectation(description: "download-partition-value-async-open-mixed") asyncOpen(user: user, partitionValue: partitionValueA) { asyncOpenState in if case let .open(realm) = asyncOpenState { - XCTAssertNotNil(realm) self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) ex.fulfill() } @@ -580,7 +561,6 @@ class SwiftUIServerTests: SwiftSyncTestCase { let ex2 = expectation(description: "download-partition-value-auto-open-mixed") autoOpen(user: user, partitionValue: partitionValueB) { autoOpenState in if case let .open(realm) = autoOpenState { - XCTAssertNotNil(realm) self.checkCount(expected: 0, realm, SwiftHugeSyncObject.self) ex2.fulfill() } @@ -604,7 +584,6 @@ class SwiftUIServerTests: SwiftSyncTestCase { let ex = expectation(description: "test-combine-multiuser1-app-auto-open") autoOpen(user: syncUser2, appId: appId, partitionValue: partitionValueB) { autoOpenState in if case let .open(realm) = autoOpenState { - XCTAssertNotNil(realm) self.checkCount(expected: 0, realm, SwiftHugeSyncObject.self) ex.fulfill() } @@ -617,7 +596,6 @@ class SwiftUIServerTests: SwiftSyncTestCase { let ex2 = expectation(description: "test-combine-multiuser2-app-auto-open") asyncOpen(user: syncUser1, appId: appId, partitionValue: partitionValueA) { asyncOpenState in if case let .open(realm) = asyncOpenState { - XCTAssertNotNil(realm) self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) ex2.fulfill() } From 5695a787424dfd8f4cd2a75cc805d75d26b3e8dc Mon Sep 17 00:00:00 2001 From: Thomas Goyne Date: Tue, 3 Oct 2023 10:00:38 -0700 Subject: [PATCH 04/10] Fulfill expectations even if an error occurred We'll never get a success after an error, so not fulfilling the expectation just pointless waits for a timeout. --- Realm/ObjectServerTests/RLMSyncTestCase.mm | 7 +- .../SwiftFlexibleSyncServerTests.swift | 70 ++++++------------- 2 files changed, 22 insertions(+), 55 deletions(-) diff --git a/Realm/ObjectServerTests/RLMSyncTestCase.mm b/Realm/ObjectServerTests/RLMSyncTestCase.mm index 46dd2eb5ec..6210ee1c29 100644 --- a/Realm/ObjectServerTests/RLMSyncTestCase.mm +++ b/Realm/ObjectServerTests/RLMSyncTestCase.mm @@ -767,11 +767,8 @@ - (void)writeQueryAndCompleteForRealm:(RLMRealm *)realm block:(void (^)(RLMSyncS [subs update:^{ block(subs); } onComplete:^(NSError* error) { - if (error == nil) { - [ex fulfill]; - } else { - XCTFail(); - } + XCTAssertNil(error); + [ex fulfill]; }]; XCTAssertNotNil(subs); [self waitForExpectationsWithTimeout:20.0 handler:nil]; diff --git a/Realm/ObjectServerTests/SwiftFlexibleSyncServerTests.swift b/Realm/ObjectServerTests/SwiftFlexibleSyncServerTests.swift index d17e9ff7df..63585ed626 100644 --- a/Realm/ObjectServerTests/SwiftFlexibleSyncServerTests.swift +++ b/Realm/ObjectServerTests/SwiftFlexibleSyncServerTests.swift @@ -683,11 +683,8 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { $0.age > 15 && $0.firstName == "\(#function)" }) }, onComplete: { error in - if error == nil { - ex.fulfill() - } else { - XCTFail("Subscription Set could not complete with \(error!)") - } + XCTAssertNil(error) + ex.fulfill() }) waitForExpectations(timeout: 20.0, handler: nil) @@ -724,11 +721,8 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { $0.intCol == 1 && $0.stringCol == "\(#function)" }) }, onComplete: { error in - if error == nil { - ex.fulfill() - } else { - XCTFail("Subscription Set could not complete with \(error!)") - } + XCTAssertNil(error) + ex.fulfill() }) waitForExpectations(timeout: 20.0, handler: nil) @@ -765,11 +759,8 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { $0.intCol == 1 && $0.stringCol == "\(#function)" }) }, onComplete: { error in - if error == nil { - ex.fulfill() - } else { - XCTFail("Subscription Set could not complete with \(error!)") - } + XCTAssertNil(error) + ex.fulfill() }) waitForExpectations(timeout: 20.0, handler: nil) @@ -781,11 +772,8 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { subscriptions.update({ subscriptions.remove(named: "person_age_5") }, onComplete: { error in - if error == nil { - ex2.fulfill() - } else { - XCTFail("Subscription Set could not complete with \(error!)") - } + XCTAssertNil(error) + ex2.fulfill() }) waitForExpectations(timeout: 20.0, handler: nil) @@ -822,11 +810,8 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { $0.intCol == 1 && $0.stringCol == "\(#function)" }) }, onComplete: { error in - if error == nil { - ex.fulfill() - } else { - XCTFail("Subscription Set could not complete with \(error!)") - } + XCTAssertNil(error) + ex.fulfill() }) waitForExpectations(timeout: 20.0, handler: nil) @@ -842,11 +827,8 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { $0.age > 20 && $0.firstName == "\(#function)" }) }, onComplete: { error in - if error == nil { - ex2.fulfill() - } else { - XCTFail("Subscription Set could not complete with \(error!)") - } + XCTAssertNil(error) + ex2.fulfill() }) waitForExpectations(timeout: 20.0, handler: nil) @@ -887,11 +869,8 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { $0.intCol == 1 && $0.stringCol == "\(#function)" }) }, onComplete: { error in - if error == nil { - ex.fulfill() - } else { - XCTFail("Subscription Set could not complete with \(error!)") - } + XCTAssertNil(error) + ex.fulfill() }) waitForExpectations(timeout: 20.0, handler: nil) @@ -903,11 +882,8 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { subscriptions.update({ subscriptions.removeAll(ofType: SwiftPerson.self) }, onComplete: { error in - if error == nil { - ex2.fulfill() - } else { - XCTFail("Subscription Set could not complete with \(error!)") - } + XCTAssertNil(error) + ex2.fulfill() }) waitForExpectations(timeout: 20.0, handler: nil) @@ -938,11 +914,8 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { $0.age > 20 && $0.firstName == "\(#function)" }) }, onComplete: { error in - if error == nil { - ex.fulfill() - } else { - XCTFail("Subscription Set could not complete with \(error!)") - } + XCTAssertNil(error) + ex.fulfill() }) waitForExpectations(timeout: 20.0, handler: nil) @@ -958,11 +931,8 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { $0.age > 5 && $0.firstName == "\(#function)" }) }, onComplete: { error in - if error == nil { - ex2.fulfill() - } else { - XCTFail("Subscription Set could not complete with \(error!)") - } + XCTAssertNil(error) + ex2.fulfill() }) waitForExpectations(timeout: 20.0, handler: nil) From 63b7e5c8c17accc75e6cc1e9676795ee805f399a Mon Sep 17 00:00:00 2001 From: Thomas Goyne Date: Mon, 27 Nov 2023 11:04:35 -0800 Subject: [PATCH 05/10] Don't use developer mode in sync tests --- Realm/ObjectServerTests/RLMSyncTestCase.mm | 2 +- Realm/ObjectServerTests/RealmServer.swift | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/Realm/ObjectServerTests/RLMSyncTestCase.mm b/Realm/ObjectServerTests/RLMSyncTestCase.mm index 6210ee1c29..1874d4acfb 100644 --- a/Realm/ObjectServerTests/RLMSyncTestCase.mm +++ b/Realm/ObjectServerTests/RLMSyncTestCase.mm @@ -661,7 +661,7 @@ - (NSString *)flexibleSyncAppId { } else { NSError *error; - _flexibleSyncAppId = [RealmServer.shared createAppWithQueryableFields:@[@"age", @"breed", @"partition", @"firstName", @"boolCol", @"intCol", @"stringCol", @"dateCol", @"lastName", @"_id"] error:&error]; + _flexibleSyncAppId = [RealmServer.shared createAppWithQueryableFields:@[@"age", @"breed", @"partition", @"firstName", @"boolCol", @"intCol", @"stringCol", @"dateCol", @"lastName", @"_id", @"uuidCol"] error:&error]; if (error) { NSLog(@"Failed to create app: %@", error); abort(); diff --git a/Realm/ObjectServerTests/RealmServer.swift b/Realm/ObjectServerTests/RealmServer.swift index e109bbaa17..d0f8a98574 100644 --- a/Realm/ObjectServerTests/RealmServer.swift +++ b/Realm/ObjectServerTests/RealmServer.swift @@ -920,10 +920,6 @@ public class RealmServer: NSObject { } _ = try app.services[serviceId].config.patch(serviceConfig).get() - app.sync.config.put(on: group, data: [ - "development_mode_enabled": true - ], failOnError) - app.functions.post(on: group, [ "name": "sum", "private": false, From bc93a88b8b60e913cc08f16c1e905d3b62210b64 Mon Sep 17 00:00:00 2001 From: Thomas Goyne Date: Mon, 27 Nov 2023 11:10:15 -0800 Subject: [PATCH 06/10] Upgrade to a newer baas version --- .../ObjectServerTests/RLMFlexibleSyncServerTests.mm | 9 ++------- Realm/ObjectServerTests/setup_baas.rb | 2 +- Realm/RLMMongoCollection.mm | 12 ++++++++---- dependencies.list | 2 +- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/Realm/ObjectServerTests/RLMFlexibleSyncServerTests.mm b/Realm/ObjectServerTests/RLMFlexibleSyncServerTests.mm index 45c2baddb9..f6f84f191f 100644 --- a/Realm/ObjectServerTests/RLMFlexibleSyncServerTests.mm +++ b/Realm/ObjectServerTests/RLMFlexibleSyncServerTests.mm @@ -938,13 +938,13 @@ - (void)testFlexibleSyncAddObjectOutsideQuery { auto ex = [self expectationWithDescription:@"should revert write"]; self.flexibleSyncApp.syncManager.errorHandler = ^(NSError *error, RLMSyncSession *) { RLMValidateError(error, RLMSyncErrorDomain, RLMSyncErrorWriteRejected, - @"Client attempted a write that is outside of permissions or query filters; it has been reverted"); + @"Client attempted a write that is not allowed; it has been reverted"); NSArray *info = error.userInfo[RLMCompensatingWriteInfoKey]; XCTAssertEqual(info.count, 1U); XCTAssertEqualObjects(info[0].objectType, @"Person"); XCTAssertEqualObjects(info[0].primaryKey, invalidObjectPK); XCTAssertEqualObjects(info[0].reason, - ([NSString stringWithFormat:@"write to \"%@\" in table \"Person\" not allowed; object is outside of the current query view", invalidObjectPK])); + ([NSString stringWithFormat:@"write to ObjectID(\"%@\") in table \"Person\" not allowed; object is outside of the current query view", invalidObjectPK])); [ex fulfill]; }; [realm transactionWithBlock:^{ @@ -1048,11 +1048,6 @@ - (void)testFlexibleSyncInitialSubscriptionRerunOnOpen { XCTAssertNotNil(realm); XCTAssertNil(error); XCTAssertEqual(realm.subscriptions.count, 1UL); - // Adding this sleep, because there seems to be a timing issue after this commit in baas - // https://github.com/10gen/baas/commit/64e75b3f1fe8a6f8704d1597de60f9dda401ccce, - // data take a little longer to be downloaded to the realm even though the - // sync client changed the subscription state to completed. - sleep(1); CHECK_COUNT(11, Person, realm); [ex fulfill]; }]; diff --git a/Realm/ObjectServerTests/setup_baas.rb b/Realm/ObjectServerTests/setup_baas.rb index 2a591f4dfc..95fb2ad7cd 100644 --- a/Realm/ObjectServerTests/setup_baas.rb +++ b/Realm/ObjectServerTests/setup_baas.rb @@ -18,7 +18,7 @@ }.to_h MONGODB_VERSION='5.0.6' -GO_VERSION='1.19.5' +GO_VERSION='1.21.4' NODE_VERSION='16.13.1' STITCH_VERSION=DEPENDENCIES["STITCH_VERSION"] diff --git a/Realm/RLMMongoCollection.mm b/Realm/RLMMongoCollection.mm index ee3bfa4473..7a3110d80d 100644 --- a/Realm/RLMMongoCollection.mm +++ b/Realm/RLMMongoCollection.mm @@ -216,13 +216,17 @@ - (void)aggregateWithPipeline:(NSArray> *> - (void)countWhere:(NSDictionary> *)document limit:(NSInteger)limit completion:(RLMMongoCountBlock)completion { - self.collection.count(toBsonDocument(document), limit, - [completion](uint64_t count, - std::optional error) { + self.collection.count_bson(toBsonDocument(document), limit, + [completion](std::optional&& value, + std::optional&& error) { if (error) { return completion(0, makeError(*error)); } - completion(static_cast(count), nil); + if (value->type() == realm::bson::Bson::Type::Int64) { + return completion(static_cast(static_cast(*value)), nil); + } + // If the collection does not exist the call returns undefined + return completion(0, nil); }); } diff --git a/dependencies.list b/dependencies.list index 60094e00fd..2a8941f432 100755 --- a/dependencies.list +++ b/dependencies.list @@ -1,3 +1,3 @@ VERSION=10.45.1 REALM_CORE_VERSION=13.25.0 -STITCH_VERSION=911b8db03b852f664af13880c42eb8178d4fb5f4 +STITCH_VERSION=8bf8ebcff6e804586c30a6ccbadb060753071a42 From 95eb341e588d6052e12a04324fc8f4f4c4025bdf Mon Sep 17 00:00:00 2001 From: Thomas Goyne Date: Mon, 27 Nov 2023 11:19:59 -0800 Subject: [PATCH 07/10] Fix some unused variable warnings --- Realm/ObjectServerTests/SwiftUIServerTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Realm/ObjectServerTests/SwiftUIServerTests.swift b/Realm/ObjectServerTests/SwiftUIServerTests.swift index 8304087604..23e091e7eb 100644 --- a/Realm/ObjectServerTests/SwiftUIServerTests.swift +++ b/Realm/ObjectServerTests/SwiftUIServerTests.swift @@ -66,7 +66,7 @@ class SwiftUIServerTests: SwiftSyncTestCase { let ex = expectation(description: "download-realm-async-open") asyncOpen(user: user, appId: appId, partitionValue: #function) { asyncOpenState in - if case let .open(realm) = asyncOpenState { + if case .open = asyncOpenState { ex.fulfill() } } @@ -287,7 +287,7 @@ class SwiftUIServerTests: SwiftSyncTestCase { let ex = expectation(description: "download-realm-auto-open") autoOpen(user: user, appId: appId, partitionValue: #function) { autoOpenState in - if case let .open(realm) = autoOpenState { + if case .open = autoOpenState { ex.fulfill() } } From a5636c1ca2a5b4f7109869d3d78551c468d3ab93 Mon Sep 17 00:00:00 2001 From: Thomas Goyne Date: Wed, 29 Nov 2023 13:38:24 -0800 Subject: [PATCH 08/10] Add a few missing Sendable conformances --- CHANGELOG.md | 2 ++ .../ObjectServerTests/SwiftFlexibleSyncServerTests.swift | 2 +- Realm/ObjectServerTests/SwiftObjectServerTests.swift | 8 +++++--- RealmSwift/Sync.swift | 4 ++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index daedb02600..3612461eac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ x.y.z Release notes (yyyy-MM-dd) * Registering new notifications inside write transactions before actually making any changes is now actually allowed. This was supposed to be allowed in 10.39.1, but it did not actually work due to some redundant validation. +* `SyncSession.ProgressDirection` and `SyncSession.ProgressMode` were missing + `Sendable` annotations. ### Compatibility * Realm Studio: 14.0.1 or later. diff --git a/Realm/ObjectServerTests/SwiftFlexibleSyncServerTests.swift b/Realm/ObjectServerTests/SwiftFlexibleSyncServerTests.swift index 63585ed626..5a9d086702 100644 --- a/Realm/ObjectServerTests/SwiftFlexibleSyncServerTests.swift +++ b/Realm/ObjectServerTests/SwiftFlexibleSyncServerTests.swift @@ -1895,7 +1895,7 @@ extension SwiftFlexibleSyncServerTests { @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) @globalActor actor CustomGlobalActor: GlobalActor { - static var shared = CustomGlobalActor() + static let shared = CustomGlobalActor() } #endif // canImport(Combine) diff --git a/Realm/ObjectServerTests/SwiftObjectServerTests.swift b/Realm/ObjectServerTests/SwiftObjectServerTests.swift index e09cfaf4c3..f626adcabc 100644 --- a/Realm/ObjectServerTests/SwiftObjectServerTests.swift +++ b/Realm/ObjectServerTests/SwiftObjectServerTests.swift @@ -3047,9 +3047,11 @@ class AsyncAwaitObjectServerTests: SwiftSyncTestCase { return super.defaultTestSuite } - func assertThrowsError(_ expression: @autoclosure () async throws -> T, - file: StaticString = #filePath, line: UInt = #line, - _ errorHandler: (_ error: E) -> Void) async { + func assertThrowsError( + _ expression: @autoclosure () async throws -> T, + file: StaticString = #filePath, line: UInt = #line, + _ errorHandler: (_ error: E) -> Void + ) async { do { _ = try await expression() XCTFail("Expression should have thrown an error", file: file, line: line) diff --git a/RealmSwift/Sync.swift b/RealmSwift/Sync.swift index 17d5a9cb4d..b3a5162d64 100644 --- a/RealmSwift/Sync.swift +++ b/RealmSwift/Sync.swift @@ -801,7 +801,7 @@ public extension SyncSession { Progress notification blocks can be registered on sessions if your app wishes to be informed how many bytes have been uploaded or downloaded, for example to show progress indicator UIs. */ - enum ProgressDirection { + enum ProgressDirection: Sendable { /// For monitoring upload progress. case upload /// For monitoring download progress. @@ -814,7 +814,7 @@ public extension SyncSession { Progress notification blocks can be registered on sessions if your app wishes to be informed how many bytes have been uploaded or downloaded, for example to show progress indicator UIs. */ - enum ProgressMode { + enum ProgressMode: Sendable { /** The block will be called forever, or until it is unregistered by calling `ProgressNotificationToken.invalidate()`. From 39c83d05e410287bd30a2cec46d42118c7d4d2df Mon Sep 17 00:00:00 2001 From: Thomas Goyne Date: Tue, 14 Nov 2023 18:06:21 -0800 Subject: [PATCH 09/10] Rework the sync tests This unfortunately turned into a giant unreviewable mess, but I don't really know how to split it up. An overview of the changes: Each XCTestCase subclass now creates a server-side App which is used for most of the tests in that test case. Apps are now never shared between test cases, and do not share Mongo collections with other apps. Together this means that state from one test case should never bleed over to other test cases. This required rearranging many of the tests, as we had test cases with both PBS and FLX tests. While doing this I also discovered several tests which were just plain in the wrong test case due to files having multiple multi-thousand-line test cases and people thought they were adding tests to the one defined at the top. I split these files into multiple files, which I think makes things much more managable but unfortunately results in the diff being unhelpful. Each test case now explicitly defines which set of classes it uses, and only Rules for those classes is created in the server app. This cuts the time required to create apps roughly in half and helps offset the fact that we're now creating more apps. It also gets rid of the weird things like the hardcoded list of flx-compatible types in RealmServer. Creating an app now waits for the initial sync to complete. With PBS this is required to avoid some very strange bugs. With FLX it mostly ensures that if this times out for some reason we get a test failure there, rather than later in some very confusing place in the middle of a test. Client reset tests now use the API endpoint for FLX in addition to PBS. This makes them dramatically faster (several seconds instead of 30+). FLX tests now consistently follow the pattern of using one of the object fields as a partition key to query on rather than querying for all objects of a type. Some tests already did this, while others tried to clear the data first (which did not always work if the server was in the middle of processing old requests), and some just plain broke if tests were run in the wrong order. In the very early days of sync, opening the same Realm URL twice required two different processes, so all our sync tests spawned child processes. That hasn't been true for a very long time, but the tests stuck around and some more were written in that style due to mimicking the existing tests. I've ported almost all of them over to operating in a single process, which makes them both simpler and much faster (5s to .5s in many cases). The tests are now run with developer mode off. This was initially required due to the change where opening with a class subset is now considered a breaking change in developer mode, but now that test cases explicitly specify their types that isn't a problem any more. However, it does let us once again test subscriptions failing due to an unqueryable field, and that test revealed that we were using the wrong error domain for that error. I added some new helper functions for the things I discovered I was going to have to change in literally hundreds of places. Creating a temporary user is now just `self.createUser()` rather than separate steps of creating credentials and logging in. `self.name` is now used as the tag value for partitions and user names and such rather than `#function` or `NSStringFromSelector(_cmd)`, which makes it so that it doesn't have to be explicitly passed into helper functions. There were a number of places where this previously was done incorrectly and `#function` was used inside helper functions, which didn't achieve the desired effect. --- CHANGELOG.md | 4 +- Package.swift | 29 +- Realm.xcodeproj/project.pbxproj | 28 +- Realm/ObjectServerTests/AsyncSyncTests.swift | 1260 +++++ .../ObjectServerTests/ClientResetTests.swift | 715 +++ .../ObjectServerTests/CombineSyncTests.swift | 713 +++ Realm/ObjectServerTests/EventTests.swift | 56 +- .../RLMAsymmetricSyncServerTests.mm | 88 +- .../RLMCollectionSyncTests.mm | 129 +- .../RLMFlexibleSyncServerTests.mm | 865 +--- .../ObjectServerTests/RLMMongoClientTests.mm | 800 ++++ .../RLMObjectServerPartitionTests.mm | 39 +- .../ObjectServerTests/RLMObjectServerTests.mm | 1549 ++----- .../ObjectServerTests/RLMSubscriptionTests.mm | 668 +++ Realm/ObjectServerTests/RLMSyncTestCase.h | 69 +- Realm/ObjectServerTests/RLMSyncTestCase.mm | 330 +- .../RLMUser+ObjectServerTests.h | 2 - .../RLMUser+ObjectServerTests.mm | 30 - Realm/ObjectServerTests/RealmServer.swift | 183 +- .../SwiftAsymmetricSyncServerTests.swift | 100 +- .../SwiftCollectionSyncTests.swift | 20 +- .../SwiftFlexibleSyncServerTests.swift | 1150 +---- .../SwiftMongoClientTests.swift | 36 +- .../SwiftObjectServerPartitionTests.swift | 31 +- .../SwiftObjectServerTests.swift | 4078 ++++------------- .../ObjectServerTests/SwiftSyncTestCase.swift | 235 +- .../SwiftUIServerTests.swift | 287 +- .../TimeoutProxyServer.swift | 2 +- Realm/RLMError.mm | 13 +- Realm/RLMRealm.mm | 20 + Realm/RLMRealm_Private.h | 2 + Realm/RLMSyncSession.mm | 2 +- .../SwiftUISyncTestHost/ContentView.swift | 126 +- .../Tests/SwiftUISyncTestHost/TestType.swift | 30 + .../SwiftUISyncTestHostUITests.swift | 352 +- RealmSwift/Combine.swift | 17 +- RealmSwift/SyncSubscription.swift | 32 +- 37 files changed, 6597 insertions(+), 7493 deletions(-) create mode 100644 Realm/ObjectServerTests/AsyncSyncTests.swift create mode 100644 Realm/ObjectServerTests/ClientResetTests.swift create mode 100644 Realm/ObjectServerTests/CombineSyncTests.swift create mode 100644 Realm/ObjectServerTests/RLMMongoClientTests.mm create mode 100644 Realm/ObjectServerTests/RLMSubscriptionTests.mm create mode 100644 Realm/Tests/SwiftUISyncTestHost/TestType.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 3612461eac..38d252cf2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,9 @@ x.y.z Release notes (yyyy-MM-dd) making any changes is now actually allowed. This was supposed to be allowed in 10.39.1, but it did not actually work due to some redundant validation. * `SyncSession.ProgressDirection` and `SyncSession.ProgressMode` were missing - `Sendable` annotations. + `Sendable` annotations ([PR #8435](https://github.com/realm/realm-swift/pull/8435)). +* `Realm.Error.subscriptionFailed` was reported with the incorrect error + domain, making it impossible to catch (since v10.42.2, [PR #8435](https://github.com/realm/realm-swift/pull/8435)). ### Compatibility * Realm Studio: 14.0.1 or later. diff --git a/Package.swift b/Package.swift index 80ce0c5295..c48de3c2c2 100644 --- a/Package.swift +++ b/Package.swift @@ -38,22 +38,28 @@ let testCxxSettings: [CXXSetting] = cxxSettings + [ // SPM requires all targets to explicitly include or exclude every file, which // gets very awkward when we have four targets building from a single directory let objectServerTestSources = [ + "AsyncSyncTests.swift", + "ClientResetTests.swift", + "CombineSyncTests.swift", + "EventTests.swift", "Object-Server-Tests-Bridging-Header.h", "ObjectServerTests-Info.plist", "RLMAsymmetricSyncServerTests.mm", "RLMBSONTests.mm", "RLMCollectionSyncTests.mm", "RLMFlexibleSyncServerTests.mm", + "RLMMongoClientTests.mm", "RLMObjectServerPartitionTests.mm", "RLMObjectServerTests.mm", + "RLMServerTestObjects.h", "RLMServerTestObjects.m", + "RLMSubscriptionTests.mm", "RLMSyncTestCase.h", "RLMSyncTestCase.mm", "RLMUser+ObjectServerTests.h", "RLMUser+ObjectServerTests.mm", "RLMWatchTestUtility.h", "RLMWatchTestUtility.m", - "EventTests.swift", "RealmServer.swift", "SwiftAsymmetricSyncServerTests.swift", "SwiftCollectionSyncTests.swift", @@ -324,24 +330,30 @@ let package = Package( objectServerTestSupportTarget( name: "RealmSyncTestSupport", dependencies: ["Realm", "RealmSwift", "RealmTestSupport"], - sources: ["RLMSyncTestCase.mm", - "RLMUser+ObjectServerTests.mm", - "RLMServerTestObjects.m"] + sources: [ + "RLMServerTestObjects.m", + "RLMSyncTestCase.mm", + "RLMUser+ObjectServerTests.mm", + "RLMWatchTestUtility.m", + ] ), objectServerTestSupportTarget( name: "RealmSwiftSyncTestSupport", dependencies: ["RealmSwift", "RealmTestSupport", "RealmSyncTestSupport", "RealmSwiftTestSupport"], sources: [ + "RealmServer.swift", + "SwiftServerObjects.swift", "SwiftSyncTestCase.swift", "TimeoutProxyServer.swift", "WatchTestUtility.swift", - "RealmServer.swift", - "SwiftServerObjects.swift" ] ), objectServerTestTarget( name: "SwiftObjectServerTests", sources: [ + "AsyncSyncTests.swift", + "ClientResetTests.swift", + "CombineSyncTests.swift", "EventTests.swift", "SwiftAsymmetricSyncServerTests.swift", "SwiftCollectionSyncTests.swift", @@ -349,7 +361,7 @@ let package = Package( "SwiftMongoClientTests.swift", "SwiftObjectServerPartitionTests.swift", "SwiftObjectServerTests.swift", - "SwiftUIServerTests.swift" + "SwiftUIServerTests.swift", ] ), objectServerTestTarget( @@ -359,9 +371,10 @@ let package = Package( "RLMBSONTests.mm", "RLMCollectionSyncTests.mm", "RLMFlexibleSyncServerTests.mm", + "RLMMongoClientTests.mm", "RLMObjectServerPartitionTests.mm", "RLMObjectServerTests.mm", - "RLMWatchTestUtility.m" + "RLMSubscriptionTests.mm", ] ) ], diff --git a/Realm.xcodeproj/project.pbxproj b/Realm.xcodeproj/project.pbxproj index 0e386449d7..d640c259c4 100644 --- a/Realm.xcodeproj/project.pbxproj +++ b/Realm.xcodeproj/project.pbxproj @@ -98,6 +98,11 @@ 3F1F47821B9612B300CD99A3 /* KVOTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3F0F029D1B6FFE610046A4D5 /* KVOTests.mm */; }; 3F222C4E1E26F51300CA0713 /* ThreadSafeReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F222C4D1E26F51300CA0713 /* ThreadSafeReference.swift */; }; 3F2633C31E9D630000B32D30 /* PrimitiveListTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F2633C21E9D630000B32D30 /* PrimitiveListTests.swift */; }; + 3F2B40CA2B2900DA00E30319 /* CombineSyncTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F2B40C52B2900DA00E30319 /* CombineSyncTests.swift */; }; + 3F2B40CB2B2900DA00E30319 /* AsyncSyncTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F2B40C62B2900DA00E30319 /* AsyncSyncTests.swift */; }; + 3F2B40CC2B2900DA00E30319 /* RLMSubscriptionTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3F2B40C72B2900DA00E30319 /* RLMSubscriptionTests.mm */; }; + 3F2B40CD2B2900DA00E30319 /* RLMMongoClientTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3F2B40C82B2900DA00E30319 /* RLMMongoClientTests.mm */; }; + 3F2B40CE2B2900DA00E30319 /* ClientResetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F2B40C92B2900DA00E30319 /* ClientResetTests.swift */; }; 3F2E66641CA0BA11004761D5 /* NotificationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3F2E66611CA0B9D5004761D5 /* NotificationTests.m */; }; 3F336E8B1DA2FA15006CB5A0 /* RLMSyncConfiguration_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A36236A1D83868F00945A54 /* RLMSyncConfiguration_Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; 3F3411A6273433B300EC9D25 /* ObjcBridgeable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F3411A5273433B300EC9D25 /* ObjcBridgeable.swift */; }; @@ -109,6 +114,8 @@ 3F4F3AD723F71C790048DB43 /* RLMObjectId.h in Headers */ = {isa = PBXBuildFile; fileRef = 3F4F3AD023F71C790048DB43 /* RLMObjectId.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3F4F3ADB23F71C790048DB43 /* RLMObjectId.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3F4F3AD223F71C790048DB43 /* RLMObjectId.mm */; }; 3F4F3ADD23F71C790048DB43 /* RLMDecimal128.h in Headers */ = {isa = PBXBuildFile; fileRef = 3F4F3AD323F71C790048DB43 /* RLMDecimal128.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 3F4FD2FB2B2A389A003E3DFD /* TestType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F4FD2FA2B2A389A003E3DFD /* TestType.swift */; }; + 3F4FD2FC2B2A389A003E3DFD /* TestType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F4FD2FA2B2A389A003E3DFD /* TestType.swift */; }; 3F558C8722C29A03002F0F30 /* TestUtils.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3F558C7E22C29A02002F0F30 /* TestUtils.mm */; }; 3F558C8822C29A03002F0F30 /* TestUtils.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3F558C7E22C29A02002F0F30 /* TestUtils.mm */; }; 3F558C8A22C29A03002F0F30 /* TestUtils.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3F558C7E22C29A02002F0F30 /* TestUtils.mm */; }; @@ -333,7 +340,6 @@ AC05380C2885B25A00CE27C4 /* SwiftAsymmetricSyncServerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC0538062885B1DF00CE27C4 /* SwiftAsymmetricSyncServerTests.swift */; }; AC23487E26FC8619009129F2 /* RLMUser+ObjectServerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1AF64DD11DA304A90081EB15 /* RLMUser+ObjectServerTests.mm */; }; AC2C2A40268E1B0200B4DA33 /* SwiftServerObjects.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC2C2A3E268E1ACE00B4DA33 /* SwiftServerObjects.swift */; }; - AC2C2A41268E1B0700B4DA33 /* SwiftServerObjects.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC2C2A3E268E1ACE00B4DA33 /* SwiftServerObjects.swift */; }; AC320BAE268E1F2D0043D484 /* SwiftServerObjects.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC2C2A3E268E1ACE00B4DA33 /* SwiftServerObjects.swift */; }; AC3B33AE29DC6CEE0042F3A0 /* RLMLogger.h in Headers */ = {isa = PBXBuildFile; fileRef = AC3B33AB29DC6CEE0042F3A0 /* RLMLogger.h */; settings = {ATTRIBUTES = (Public, ); }; }; AC3B33AF29DC6CEE0042F3A0 /* RLMLogger.mm in Sources */ = {isa = PBXBuildFile; fileRef = AC3B33AC29DC6CEE0042F3A0 /* RLMLogger.mm */; }; @@ -678,6 +684,11 @@ 3F222C4D1E26F51300CA0713 /* ThreadSafeReference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThreadSafeReference.swift; sourceTree = ""; }; 3F2633C21E9D630000B32D30 /* PrimitiveListTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrimitiveListTests.swift; sourceTree = ""; }; 3F275EBD2433A5DA00161E7F /* RLMApp_Private.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = RLMApp_Private.hpp; sourceTree = ""; }; + 3F2B40C52B2900DA00E30319 /* CombineSyncTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CombineSyncTests.swift; path = Realm/ObjectServerTests/CombineSyncTests.swift; sourceTree = ""; }; + 3F2B40C62B2900DA00E30319 /* AsyncSyncTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AsyncSyncTests.swift; path = Realm/ObjectServerTests/AsyncSyncTests.swift; sourceTree = ""; }; + 3F2B40C72B2900DA00E30319 /* RLMSubscriptionTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = RLMSubscriptionTests.mm; path = Realm/ObjectServerTests/RLMSubscriptionTests.mm; sourceTree = ""; }; + 3F2B40C82B2900DA00E30319 /* RLMMongoClientTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = RLMMongoClientTests.mm; path = Realm/ObjectServerTests/RLMMongoClientTests.mm; sourceTree = ""; }; + 3F2B40C92B2900DA00E30319 /* ClientResetTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ClientResetTests.swift; path = Realm/ObjectServerTests/ClientResetTests.swift; sourceTree = ""; }; 3F2E66611CA0B9D5004761D5 /* NotificationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NotificationTests.m; sourceTree = ""; }; 3F3411A5273433B300EC9D25 /* ObjcBridgeable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjcBridgeable.swift; sourceTree = ""; }; 3F4071342A57472F00D9C4A3 /* PrivateSymbols.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = PrivateSymbols.txt; sourceTree = ""; }; @@ -693,6 +704,7 @@ 3F4F3AD223F71C790048DB43 /* RLMObjectId.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RLMObjectId.mm; sourceTree = ""; }; 3F4F3AD323F71C790048DB43 /* RLMDecimal128.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RLMDecimal128.h; sourceTree = ""; }; 3F4F3AD423F71C790048DB43 /* RLMObjectId_Private.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = RLMObjectId_Private.hpp; sourceTree = ""; }; + 3F4FD2FA2B2A389A003E3DFD /* TestType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestType.swift; sourceTree = ""; }; 3F558C7E22C29A02002F0F30 /* TestUtils.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = TestUtils.mm; path = Realm/TestUtils/TestUtils.mm; sourceTree = ""; }; 3F558C7F22C29A02002F0F30 /* RLMTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RLMTestCase.h; path = Realm/TestUtils/include/RLMTestCase.h; sourceTree = ""; }; 3F558C8022C29A02002F0F30 /* RLMMultiProcessTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RLMMultiProcessTestCase.h; path = Realm/TestUtils/include/RLMMultiProcessTestCase.h; sourceTree = ""; }; @@ -1434,6 +1446,7 @@ AC8846772686573B00DF4A65 /* ContentView.swift */, AC8847472687E0AA00DF4A65 /* Info.plist */, AC8846752686573B00DF4A65 /* SwiftUISyncTestHostApp.swift */, + 3F4FD2FA2B2A389A003E3DFD /* TestType.swift */, ); path = SwiftUISyncTestHost; sourceTree = ""; @@ -1566,6 +1579,9 @@ isa = PBXGroup; children = ( 1AA5AEA21D98CA5300ED8C27 /* Utility */, + 3F2B40C62B2900DA00E30319 /* AsyncSyncTests.swift */, + 3F2B40C92B2900DA00E30319 /* ClientResetTests.swift */, + 3F2B40C52B2900DA00E30319 /* CombineSyncTests.swift */, 536B7C0B24A4C223006B535D /* dependencies.list */, 3FEE4F44281C439D009194C7 /* EventTests.swift */, AC0538052885B1DF00CE27C4 /* RLMAsymmetricSyncServerTests.mm */, @@ -1573,10 +1589,12 @@ CFA3A23D260B8427002C3266 /* RLMCollectionSyncTests.mm */, CF08757C260B98E100B9BE60 /* RLMCollectionSyncTests.mm */, ACB6FD30273C60920009712F /* RLMFlexibleSyncServerTests.mm */, + 3F2B40C82B2900DA00E30319 /* RLMMongoClientTests.mm */, AC7D182B261F2F560080E1D2 /* RLMObjectServerPartitionTests.mm */, E8267FF01D90B8E700E001C7 /* RLMObjectServerTests.mm */, 531F956F279070A800E497F1 /* RLMServerTestObjects.h */, 531F956E279070A800E497F1 /* RLMServerTestObjects.m */, + 3F2B40C72B2900DA00E30319 /* RLMSubscriptionTests.mm */, 1AA5AE9D1D98A6D800ED8C27 /* RLMSyncTestCase.h */, 1AA5AE9B1D98A68E00ED8C27 /* RLMSyncTestCase.mm */, 49D9DFC4246C8E48003AD31D /* setup_baas.rb */, @@ -2606,8 +2624,8 @@ buildActionMask = 2147483647; files = ( AC8846782686573B00DF4A65 /* ContentView.swift in Sources */, - AC2C2A41268E1B0700B4DA33 /* SwiftServerObjects.swift in Sources */, AC8846762686573B00DF4A65 /* SwiftUISyncTestHostApp.swift in Sources */, + 3F4FD2FB2B2A389A003E3DFD /* TestType.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2625,6 +2643,7 @@ AC320BAE268E1F2D0043D484 /* SwiftServerObjects.swift in Sources */, ACBD595328364DB0009A664E /* SwiftSyncTestCase.swift in Sources */, AC88478626888CEE00DF4A65 /* SwiftUISyncTestHostUITests.swift in Sources */, + 3F4FD2FC2B2A389A003E3DFD /* TestType.swift in Sources */, ACBD595B2836541F009A664E /* TestUtils.mm in Sources */, 3F95F4A2295B8488006BC287 /* TestUtils.swift in Sources */, ); @@ -2634,6 +2653,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3F2B40CB2B2900DA00E30319 /* AsyncSyncTests.swift in Sources */, + 3F2B40CE2B2900DA00E30319 /* ClientResetTests.swift in Sources */, + 3F2B40CA2B2900DA00E30319 /* CombineSyncTests.swift in Sources */, 3FEE4F45281C439D009194C7 /* EventTests.swift in Sources */, 537130C824A9E417001FDBBC /* RealmServer.swift in Sources */, AC05380B2885B23E00CE27C4 /* RLMAsymmetricSyncServerTests.mm in Sources */, @@ -2641,10 +2663,12 @@ 530BA61626DFA1CB008FC550 /* RLMChildProcessEnvironment.m in Sources */, CF08757D260B98E100B9BE60 /* RLMCollectionSyncTests.mm in Sources */, ACB6FD37273C61040009712F /* RLMFlexibleSyncServerTests.mm in Sources */, + 3F2B40CD2B2900DA00E30319 /* RLMMongoClientTests.mm in Sources */, 3F558C9422C29A03002F0F30 /* RLMMultiProcessTestCase.m in Sources */, AC7D182D261F2F560080E1D2 /* RLMObjectServerPartitionTests.mm in Sources */, E8267FF11D90B8E700E001C7 /* RLMObjectServerTests.mm in Sources */, 531F9570279070A900E497F1 /* RLMServerTestObjects.m in Sources */, + 3F2B40CC2B2900DA00E30319 /* RLMSubscriptionTests.mm in Sources */, 1AA5AE9C1D98A68E00ED8C27 /* RLMSyncTestCase.mm in Sources */, 3F558C9022C29A03002F0F30 /* RLMTestCase.m in Sources */, 1A1536481DB0408A00C0EC93 /* RLMUser+ObjectServerTests.mm in Sources */, diff --git a/Realm/ObjectServerTests/AsyncSyncTests.swift b/Realm/ObjectServerTests/AsyncSyncTests.swift new file mode 100644 index 0000000000..64b6d734bc --- /dev/null +++ b/Realm/ObjectServerTests/AsyncSyncTests.swift @@ -0,0 +1,1260 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2016 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +#if os(macOS) && swift(>=5.8) + +import Realm +import Realm.Private +import RealmSwift +import XCTest + +#if canImport(RealmTestSupport) +import RealmSwiftSyncTestSupport +import RealmSyncTestSupport +import RealmTestSupport +import RealmSwiftTestSupport +#endif + +// SE-0392 exposes this functionality directly, but for now we have to call the +// internal standard library function +@_silgen_name("swift_job_run") +private func _swiftJobRun(_ job: UnownedJob, _ executor: UnownedSerialExecutor) + +@available(macOS 13, *) +class AsyncAwaitSyncTests: SwiftSyncTestCase { + override class var defaultTestSuite: XCTestSuite { + // async/await is currently incompatible with thread sanitizer and will + // produce many false positives + // https://bugs.swift.org/browse/SR-15444 + if RLMThreadSanitizerEnabled() { + return XCTestSuite(name: "\(type(of: self))") + } + return super.defaultTestSuite + } + + override var objectTypes: [ObjectBase.Type] { + [ + SwiftCustomColumnObject.self, + SwiftHugeSyncObject.self, + SwiftPerson.self, + SwiftTypesSyncObject.self, + ] + } + + @MainActor func populateRealm() async throws { + try await write { realm in + realm.add(SwiftHugeSyncObject.create()) + realm.add(SwiftHugeSyncObject.create()) + } + } + + func assertThrowsError( + _ expression: @autoclosure () async throws -> T, + file: StaticString = #filePath, line: UInt = #line, + _ errorHandler: (_ error: E) -> Void + ) async { + do { + _ = try await expression() + XCTFail("Expression should have thrown an error", file: file, line: line) + } catch let error as E { + errorHandler(error) + } catch { + XCTFail("Expected error of type \(E.self) but got \(error)") + } + } + + @MainActor func testAsyncOpenStandalone() async throws { + try autoreleasepool { + let configuration = Realm.Configuration(objectTypes: [SwiftPerson.self]) + let realm = try Realm(configuration: configuration) + try realm.write { + (0..<10).forEach { _ in realm.add(SwiftPerson(firstName: "Charlie", lastName: "Bucket")) } + } + } + let configuration = Realm.Configuration(objectTypes: [SwiftPerson.self]) + let realm = try await Realm(configuration: configuration) + XCTAssertEqual(realm.objects(SwiftPerson.self).count, 10) + } + + @MainActor func testAsyncOpenSync() async throws { + try await populateRealm() + let realm = try await openRealm() + XCTAssertEqual(realm.objects(SwiftHugeSyncObject.self).count, 2) + } + + @MainActor func testAsyncOpenDownloadBehaviorNever() async throws { + try await populateRealm() + let config = try configuration() + + // Should not have any objects as it just opens immediately without waiting + let realm = try await Realm(configuration: config, downloadBeforeOpen: .never) + XCTAssertEqual(realm.objects(SwiftHugeSyncObject.self).count, 0) + waitForDownloads(for: realm) + XCTAssertEqual(realm.objects(SwiftHugeSyncObject.self).count, 2) + } + + @MainActor func testAsyncOpenDownloadBehaviorOnce() async throws { + try await populateRealm() + let config = try configuration() + + // Should have the objects + try await Task { + let realm = try await Realm(configuration: config, downloadBeforeOpen: .once) + XCTAssertEqual(realm.objects(SwiftHugeSyncObject.self).count, 2) + }.value + + // Add some more objects + try await write { realm in + realm.add(SwiftHugeSyncObject.create()) + realm.add(SwiftHugeSyncObject.create()) + } + + // Will not wait for the new objects to download + try await Task { + let realm = try await Realm(configuration: config, downloadBeforeOpen: .once) + XCTAssertEqual(realm.objects(SwiftHugeSyncObject.self).count, 2) + try XCTUnwrap(realm.syncSession).suspend() + }.value + } + + @MainActor func testAsyncOpenDownloadBehaviorAlwaysWithCachedRealm() async throws { + try await populateRealm() + let config = try configuration() + + // Should have the objects + let realm = try await Realm(configuration: config, downloadBeforeOpen: .always) + XCTAssertEqual(realm.objects(SwiftHugeSyncObject.self).count, 2) + try XCTUnwrap(realm.syncSession).suspend() + + + // Add some more objects + try await populateRealm() + + // Should resume the session and wait for the new objects to download + _ = try await Realm(configuration: config, downloadBeforeOpen: .always) + XCTAssertEqual(realm.objects(SwiftHugeSyncObject.self).count, 4) + } + + @MainActor func testAsyncOpenDownloadBehaviorAlwaysWithFreshRealm() async throws { + try await populateRealm() + let config = try configuration() + + // Open in a Task so that the Realm is closed and re-opened later + _ = try await Task { + let realm = try await Realm(configuration: config, downloadBeforeOpen: .always) + XCTAssertEqual(realm.objects(SwiftHugeSyncObject.self).count, 2) + }.value + + // Add some more objects + try await populateRealm() + + // Should wait for the new objects to download + let realm = try await Realm(configuration: config, downloadBeforeOpen: .always) + XCTAssertEqual(realm.objects(SwiftHugeSyncObject.self).count, 4) + } + + @MainActor func testDownloadPBSRealmCustomColumnNames() async throws { + let objectId = ObjectId.generate() + let linkedObjectId = ObjectId.generate() + + try await write { realm in + let object = SwiftCustomColumnObject() + object.id = objectId + object.binaryCol = "string".data(using: String.Encoding.utf8)! + let linkedObject = SwiftCustomColumnObject() + linkedObject.id = linkedObjectId + object.objectCol = linkedObject + realm.add(object) + } + + // Should have the objects + let realm = try await openRealm() + XCTAssertEqual(realm.objects(SwiftCustomColumnObject.self).count, 2) + + let object = try XCTUnwrap(realm.object(ofType: SwiftCustomColumnObject.self, forPrimaryKey: objectId)) + XCTAssertEqual(object.id, objectId) + XCTAssertEqual(object.boolCol, true) + XCTAssertEqual(object.intCol, 1) + XCTAssertEqual(object.doubleCol, 1.1) + XCTAssertEqual(object.stringCol, "string") + XCTAssertEqual(object.binaryCol, "string".data(using: String.Encoding.utf8)!) + XCTAssertEqual(object.dateCol, Date(timeIntervalSince1970: -1)) + XCTAssertEqual(object.longCol, 1) + XCTAssertEqual(object.decimalCol, Decimal128(1)) + XCTAssertEqual(object.uuidCol, UUID(uuidString: "85d4fbee-6ec6-47df-bfa1-615931903d7e")!) + XCTAssertNil(object.objectIdCol) + XCTAssertEqual(object.objectCol!.id, linkedObjectId) + } + + // A custom executor which cancels the task after the requested number of + // invocations. This is a very naive executor which just synchronously + // invokes jobs, which generally is not a legal thing to do + final class CancellingExecutor: SerialExecutor, @unchecked Sendable { + private var remaining: Locked + private var pendingJob: UnownedJob? + var task: Task? { + didSet { + if let pendingJob = pendingJob { + self.pendingJob = nil + enqueue(pendingJob) + } + } + } + + init(cancelAfter: Int) { + remaining = Locked(cancelAfter) + } + + func enqueue(_ job: UnownedJob) { + // The body of the task is enqueued before the task variable is + // set, so we need to defer invoking the very first job + guard let task = task else { + precondition(pendingJob == nil) + pendingJob = job + return + } + + remaining.withLock { remaining in + if remaining == 0 { + task.cancel() + } + remaining -= 1 + + // S#-0392 exposes all the stuff we need for this in the public + // API (Which hopefully will arrive in Swift 5.9), but for now + // invoking a job requires some private things. + _swiftJobRun(job, self.asUnownedSerialExecutor()) + } + } + + func asUnownedSerialExecutor() -> UnownedSerialExecutor { + UnownedSerialExecutor(ordinary: self) + } + } + + // An actor that does nothing other than have a custom executor + actor CustomExecutorActor { + nonisolated let executor: UnownedSerialExecutor + init(_ executor: UnownedSerialExecutor) { + self.executor = executor + } + nonisolated var unownedExecutor: UnownedSerialExecutor { + executor + } + } + + @MainActor func testAsyncOpenTaskCancellation() async throws { + try await populateRealm() + + let configuration = try configuration() + func isolatedOpen(_ actor: isolated CustomExecutorActor) async throws { + _ = try await Realm(configuration: configuration, actor: actor, downloadBeforeOpen: .always) + } + + // Try opening the Realm with the Task being cancelled at every possible + // point between executor invocations. This doesn't really test that + // cancellation is *correct*; just that cancellation never results in + // a hang or crash. + for i in 0 ..< .max { + RLMWaitForRealmToClose(configuration.fileURL!.path) + _ = try Realm.deleteFiles(for: configuration) + + let executor = CancellingExecutor(cancelAfter: i) + executor.task = Task { + try await isolatedOpen(.init(executor.asUnownedSerialExecutor())) + } + do { + try await executor.task!.value + break + } catch is CancellationError { + // pass + } catch { + XCTFail("Expected CancellationError but got \(error)") + } + } + + // Repeat the above, but with a cached Realm so that we hit that code path instead + let cachedRealm = try await Realm(configuration: configuration, downloadBeforeOpen: .always) + for i in 0 ..< .max { + let executor = CancellingExecutor(cancelAfter: i) + executor.task = Task { + try await isolatedOpen(.init(executor.asUnownedSerialExecutor())) + } + do { + try await executor.task!.value + break + } catch is CancellationError { + // pass + } catch { + XCTFail("Expected CancellationError but got \(error)") + } + } + cachedRealm.invalidate() + } + + func testCallResetPasswordAsyncAwait() async throws { + let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" + let password = randomString(10) + try await app.emailPasswordAuth.registerUser(email: email, password: password) + let auth = app.emailPasswordAuth + await assertThrowsError(try await auth.callResetPasswordFunction(email: email, + password: randomString(10), + args: [[:]])) { + assertAppError($0, .unknown, "failed to reset password for user \"\(email)\"") + } + } + + func testAppLinkUserAsyncAwait() async throws { + let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" + let password = randomString(10) + try await app.emailPasswordAuth.registerUser(email: email, password: password) + + let syncUser = try await self.app.login(credentials: Credentials.anonymous) + + let credentials = Credentials.emailPassword(email: email, password: password) + let linkedUser = try await syncUser.linkUser(credentials: credentials) + XCTAssertEqual(linkedUser.id, app.currentUser?.id) + XCTAssertEqual(linkedUser.identities.count, 2) + } + + func testUserCallFunctionAsyncAwait() async throws { + let user = try await self.app.login(credentials: basicCredentials()) + guard case let .int32(sum) = try await user.functions.sum([1, 2, 3, 4, 5]) else { + return XCTFail("Should be int32") + } + XCTAssertEqual(sum, 15) + } + + // MARK: - Objective-C async await + func testPushRegistrationAsyncAwait() async throws { + let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" + let password = randomString(10) + try await app.emailPasswordAuth.registerUser(email: email, password: password) + + _ = try await app.login(credentials: Credentials.emailPassword(email: email, password: password)) + + let client = app.pushClient(serviceName: "gcm") + try await client.registerDevice(token: "some-token", user: app.currentUser!) + try await client.deregisterDevice(user: app.currentUser!) + } + + func testEmailPasswordProviderClientAsyncAwait() async throws { + let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" + let password = randomString(10) + let auth = app.emailPasswordAuth + try await auth.registerUser(email: email, password: password) + + await assertThrowsError(try await auth.confirmUser("atoken", tokenId: "atokenid")) { + assertAppError($0, .badRequest, "invalid token data") + } + await assertThrowsError(try await auth.resendConfirmationEmail(email)) { + assertAppError($0, .userAlreadyConfirmed, "already confirmed") + } + await assertThrowsError(try await auth.retryCustomConfirmation(email)) { + assertAppError($0, .unknown, + "cannot run confirmation for \(email): automatic confirmation is enabled") + } + await assertThrowsError(try await auth.sendResetPasswordEmail("atoken")) { + assertAppError($0, .userNotFound, "user not found") + } + } + + func testUserAPIKeyProviderClientAsyncAwait() async throws { + let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" + let password = randomString(10) + try await app.emailPasswordAuth.registerUser(email: email, password: password) + + let credentials = Credentials.emailPassword(email: email, password: password) + let syncUser = try await self.app.login(credentials: credentials) + let apiKey = try await syncUser.apiKeysAuth.createAPIKey(named: "my-api-key") + XCTAssertNotNil(apiKey) + + let fetchedApiKey = try await syncUser.apiKeysAuth.fetchAPIKey(apiKey.objectId) + XCTAssertNotNil(fetchedApiKey) + + let fetchedApiKeys = try await syncUser.apiKeysAuth.fetchAPIKeys() + XCTAssertNotNil(fetchedApiKeys) + XCTAssertEqual(fetchedApiKeys.count, 1) + + try await syncUser.apiKeysAuth.disableAPIKey(apiKey.objectId) + try await syncUser.apiKeysAuth.enableAPIKey(apiKey.objectId) + try await syncUser.apiKeysAuth.deleteAPIKey(apiKey.objectId) + + let newFetchedApiKeys = try await syncUser.apiKeysAuth.fetchAPIKeys() + XCTAssertNotNil(newFetchedApiKeys) + XCTAssertEqual(newFetchedApiKeys.count, 0) + } + + func testCustomUserDataAsyncAwait() async throws { + let user = try await createUser() + _ = try await user.functions.updateUserData([ + ["favourite_colour": "green", "apples": 10] + ]) + + try await user.refreshCustomData() + XCTAssertEqual(user.customData["favourite_colour"], .string("green")) + XCTAssertEqual(user.customData["apples"], .int64(10)) + } + + func testDeleteUserAsyncAwait() async throws { + let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" + let password = randomString(10) + let credentials: Credentials = .emailPassword(email: email, password: password) + try await app.emailPasswordAuth.registerUser(email: email, password: password) + + let user = try await self.app.login(credentials: credentials) + + XCTAssertNotNil(app.currentUser) + try await user.delete() + + XCTAssertNil(app.currentUser) + XCTAssertEqual(app.allUsers.count, 0) + } + + func testSwiftAddObjectsAsync() async throws { + let realm = try await openRealm() + checkCount(expected: 0, realm, SwiftPerson.self) + checkCount(expected: 0, realm, SwiftTypesSyncObject.self) + + try await write { realm in + realm.add(SwiftPerson(firstName: "Ringo", lastName: "Starr")) + realm.add(SwiftPerson(firstName: "John", lastName: "Lennon")) + realm.add(SwiftPerson(firstName: "Paul", lastName: "McCartney")) + realm.add(SwiftTypesSyncObject(person: SwiftPerson(firstName: "George", lastName: "Harrison"))) + } + + try await realm.syncSession?.wait(for: .download) + checkCount(expected: 4, realm, SwiftPerson.self) + checkCount(expected: 1, realm, SwiftTypesSyncObject.self) + + let obj = realm.objects(SwiftTypesSyncObject.self).first! + XCTAssertEqual(obj.boolCol, true) + XCTAssertEqual(obj.intCol, 1) + XCTAssertEqual(obj.doubleCol, 1.1) + XCTAssertEqual(obj.stringCol, "string") + XCTAssertEqual(obj.binaryCol, "string".data(using: String.Encoding.utf8)!) + XCTAssertEqual(obj.decimalCol, Decimal128(1)) + XCTAssertEqual(obj.dateCol, Date(timeIntervalSince1970: -1)) + XCTAssertEqual(obj.longCol, Int64(1)) + XCTAssertEqual(obj.uuidCol, UUID(uuidString: "85d4fbee-6ec6-47df-bfa1-615931903d7e")!) + XCTAssertEqual(obj.anyCol.intValue, 1) + XCTAssertEqual(obj.objectCol!.firstName, "George") + } +} + +@available(macOS 13, *) +class AsyncFlexibleSyncTests: SwiftSyncTestCase { + override class var defaultTestSuite: XCTestSuite { + // async/await is currently incompatible with thread sanitizer and will + // produce many false positives + // https://bugs.swift.org/browse/SR-15444 + if RLMThreadSanitizerEnabled() || true { + return XCTestSuite(name: "\(type(of: self))") + } + return super.defaultTestSuite + } + + override var objectTypes: [ObjectBase.Type] { + [SwiftCustomColumnObject.self, SwiftPerson.self, SwiftTypesSyncObject.self] + } + + override func configuration(user: User) -> Realm.Configuration { + user.flexibleSyncConfiguration() + } + + override func createApp() throws -> String { + try createFlexibleSyncApp() + } + + @MainActor + func populateSwiftPerson(_ count: Int = 10) async throws { + try await write { realm in + for i in 1...count { + let person = SwiftPerson(firstName: "\(self.name)", + lastName: "lastname_\(i)", + age: i) + realm.add(person) + } + } + } + + @MainActor + func testFlexibleSyncAppAddQueryAsyncAwait() async throws { + try await populateSwiftPerson(25) + + let realm = try await openRealm() + checkCount(expected: 0, realm, SwiftPerson.self) + + let subscriptions = realm.subscriptions + XCTAssertEqual(subscriptions.count, 0) + + try await subscriptions.update { + subscriptions.append(QuerySubscription(name: "person_age_15") { + $0.age > 15 && $0.firstName == "\(name)" + }) + } + XCTAssertEqual(subscriptions.state, .complete) + XCTAssertEqual(subscriptions.count, 1) + + checkCount(expected: 10, realm, SwiftPerson.self) + } + + @MainActor + func testStates() async throws { + let realm = try await openRealm() + let subscriptions = realm.subscriptions + XCTAssertEqual(subscriptions.count, 0) + + // should complete + try await subscriptions.update { + subscriptions.append(QuerySubscription(name: "person_age_15") { + $0.age > 15 && $0.firstName == "\(name)" + }) + } + XCTAssertEqual(subscriptions.state, .complete) + // should error + do { + try await subscriptions.update { + subscriptions.append(QuerySubscription(name: "swiftObject_longCol") { + $0.longCol == Int64(1) + }) + } + XCTFail("Invalid query should have failed") + } catch Realm.Error.subscriptionFailed { + guard case .error = subscriptions.state else { + return XCTFail("Adding a query for a not queryable field should change the subscription set state to error") + } + } + } + + @MainActor + func testFlexibleSyncNotInitialSubscriptions() async throws { + let realm = try await openRealm() + XCTAssertEqual(realm.subscriptions.count, 0) + } + + @MainActor + func testFlexibleSyncInitialSubscriptionsAsync() async throws { + try await write { realm in + for i in 1...20 { + realm.add(SwiftPerson(firstName: "\(self.name)", + lastName: "lastname_\(i)", + age: i)) + } + } + + let user = try await createUser() + var config = user.flexibleSyncConfiguration(initialSubscriptions: { subscriptions in + subscriptions.append(QuerySubscription(name: "person_age_10") { + $0.age > 10 && $0.firstName == "\(self.name)" + }) + }) + config.objectTypes = [SwiftPerson.self] + let realm = try await Realm(configuration: config, downloadBeforeOpen: .once) + XCTAssertEqual(realm.subscriptions.count, 1) + checkCount(expected: 10, realm, SwiftPerson.self) + } + + @MainActor + func testFlexibleSyncInitialSubscriptionsNotRerunOnOpen() async throws { + let user = try await createUser() + var config = user.flexibleSyncConfiguration(initialSubscriptions: { subscriptions in + subscriptions.append(QuerySubscription(name: "person_age_10") { + $0.age > 10 && $0.firstName == "\(self.name)" + }) + }) + config.objectTypes = [SwiftPerson.self] + let realm = try await Realm(configuration: config, downloadBeforeOpen: .once) + XCTAssertEqual(realm.subscriptions.count, 1) + + let realm2 = try await Realm(configuration: config, downloadBeforeOpen: .once) + XCTAssertNotNil(realm2) + XCTAssertEqual(realm.subscriptions.count, 1) + } + + @MainActor + func testFlexibleSyncInitialSubscriptionsRerunOnOpenNamedQuery() async throws { + let user = try await createUser() + var config = user.flexibleSyncConfiguration(initialSubscriptions: { subscriptions in + if subscriptions.first(named: "person_age_10") == nil { + subscriptions.append(QuerySubscription(name: "person_age_10") { + $0.age > 20 && $0.firstName == "\(self.name)" + }) + } + }, rerunOnOpen: true) + config.objectTypes = [SwiftPerson.self] + try await Task { + let realm = try await Realm(configuration: config, downloadBeforeOpen: .once) + XCTAssertEqual(realm.subscriptions.count, 1) + }.value + + try await Task { + let realm2 = try await Realm(configuration: config, downloadBeforeOpen: .once) + XCTAssertNotNil(realm2) + XCTAssertEqual(realm2.subscriptions.count, 1) + }.value + } + + @MainActor + func testFlexibleSyncInitialSubscriptionsRerunOnOpenUnnamedQuery() async throws { + try await write { realm in + for i in 1...30 { + let object = SwiftTypesSyncObject() + object.stringCol = self.name + object.dateCol = Calendar.current.date( + byAdding: .hour, + value: -i, + to: Date())! + realm.add(object) + } + } + + let user = try await createUser() + let isFirstOpen = Locked(true) + var config = user.flexibleSyncConfiguration(initialSubscriptions: { subscriptions in + subscriptions.append(QuerySubscription(query: { + let date = isFirstOpen.wrappedValue ? Calendar.current.date( + byAdding: .hour, + value: -10, + to: Date()) : Calendar.current.date( + byAdding: .hour, + value: -20, + to: Date()) + isFirstOpen.wrappedValue = false + return $0.stringCol == self.name && $0.dateCol < Date() && $0.dateCol > date! + })) + }, rerunOnOpen: true) + config.objectTypes = [SwiftTypesSyncObject.self, SwiftPerson.self] + let c = config + try await Task { + let realm = try await Realm(configuration: c, downloadBeforeOpen: .always) + XCTAssertEqual(realm.subscriptions.count, 1) + checkCount(expected: 9, realm, SwiftTypesSyncObject.self) + }.value + + try await Task { + let realm = try await Realm(configuration: c, downloadBeforeOpen: .always) + XCTAssertEqual(realm.subscriptions.count, 2) + checkCount(expected: 19, realm, SwiftTypesSyncObject.self) + }.value + } + + @MainActor + func testFlexibleSyncInitialSubscriptionsThrows() async throws { + let user = try await createUser() + var config = user.flexibleSyncConfiguration(initialSubscriptions: { subscriptions in + subscriptions.append(QuerySubscription(query: { + $0.uuidCol == UUID() + })) + }) + config.objectTypes = [SwiftTypesSyncObject.self, SwiftPerson.self] + do { + _ = try await Realm(configuration: config, downloadBeforeOpen: .once) + } catch let error as Realm.Error { + XCTAssertEqual(error.code, .subscriptionFailed) + } + } + + @MainActor + func testFlexibleSyncInitialSubscriptionsDefaultConfiguration() async throws { + let user = try await createUser() + var config = user.flexibleSyncConfiguration(initialSubscriptions: { subscriptions in + subscriptions.append(QuerySubscription()) + }) + config.objectTypes = [SwiftTypesSyncObject.self, SwiftPerson.self] + Realm.Configuration.defaultConfiguration = config + + let realm = try await Realm(downloadBeforeOpen: .once) + XCTAssertEqual(realm.subscriptions.count, 1) + } + + // MARK: Subscribe + + @MainActor + func testSubscribe() async throws { + try await populateSwiftPerson() + + let realm = try await openRealm() + let results0 = try await realm.objects(SwiftPerson.self).where { $0.age >= 6 }.subscribe() + XCTAssertEqual(results0.count, 5) + XCTAssertEqual(realm.subscriptions.count, 1) + let results1 = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == self.name && $0.lastName == "lastname_3" } + .subscribe() + XCTAssertEqual(results1.count, 1) + XCTAssertEqual(results0.count, 5) + XCTAssertEqual(realm.subscriptions.count, 2) + let results2 = realm.objects(SwiftPerson.self) + XCTAssertEqual(results2.count, 6) + } + + @MainActor + func testSubscribeReassign() async throws { + try await populateSwiftPerson() + let realm = try await openRealm() + + var results0 = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == self.name && $0.age >= 8 } + .subscribe() + XCTAssertEqual(results0.count, 3) + XCTAssertEqual(realm.subscriptions.count, 1) + // results0 local query is { $0.age >= 8 AND $0.age < 8 } + results0 = try await results0.where { $0.age < 8 }.subscribe() + XCTAssertEqual(results0.count, 0) // no matches because local query is impossible + // two subscriptions: "$0.age >= 8 AND $0.age < 8" and "$0.age >= 8" + XCTAssertEqual(realm.subscriptions.count, 2) + let results1 = realm.objects(SwiftPerson.self) + XCTAssertEqual(results1.count, 3) // three objects from "$0.age >= 8". None "$0.age >= 8 AND $0.age < 8". + } + + @MainActor + func testSubscribeSameQueryNoName() async throws { + try await populateSwiftPerson() + let realm = try await openRealm() + + let results0 = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age >= 8 } + .subscribe() + let ex = XCTestExpectation(description: "no attempt to re-create subscription, returns immediately") + realm.syncSession!.suspend() + let task = Task { + _ = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age >= 8 }.subscribe() + _ = try await results0.subscribe() + ex.fulfill() + } + await fulfillment(of: [ex], timeout: 1.0) + try await task.value + XCTAssertEqual(realm.subscriptions.count, 1) + } + + @MainActor + func testSubscribeSameQuerySameName() async throws { + try await populateSwiftPerson() + let realm = try await openRealm() + + let results0 = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age >= 8 } + .subscribe(name: "8 or older") + realm.syncSession!.suspend() + let ex = XCTestExpectation(description: "no attempt to re-create subscription, returns immediately") + Task { + _ = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age >= 8 } + .subscribe(name: "8 or older") + _ = try await results0.subscribe(name: "8 or older") + XCTAssertEqual(realm.subscriptions.count, 1) + ex.fulfill() + } + await fulfillment(of: [ex], timeout: 5.0) + XCTAssertEqual(realm.subscriptions.count, 1) + } + + @MainActor + func testSubscribeSameQueryDifferentName() async throws { + try await populateSwiftPerson() + let realm = try await openRealm() + + let results0 = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age >= 8 }.subscribe() + _ = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age >= 8 }.subscribe(name: "8 or older") + _ = try await results0.subscribe(name: "older than 7") + XCTAssertEqual(realm.subscriptions.count, 3) + let subscriptions = realm.subscriptions + XCTAssertNil(subscriptions[0]!.name) + XCTAssertEqual(subscriptions[1]!.name, "8 or older") + XCTAssertEqual(subscriptions[2]!.name, "older than 7") + } + + @MainActor + func testSubscribeDifferentQuerySameName() async throws { + try await populateSwiftPerson() + let realm = try await openRealm() + + _ = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age > 8 }.subscribe(name: "group1") + _ = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age > 5 }.subscribe(name: "group1") + XCTAssertEqual(realm.subscriptions.count, 1) + XCTAssertNotNil(realm.subscriptions.first(ofType: SwiftPerson.self) { $0.firstName == name && $0.age > 5 }) + } + + @MainActor + func testSubscribeOnRealmConfinedActor() async throws { + try await populateSwiftPerson() + try await populateSwiftPerson() + + let user = try await createUser() + var config = user.flexibleSyncConfiguration() + config.objectTypes = [SwiftPerson.self] + let realm = try await Realm(configuration: config, actor: MainActor.shared) + let results1 = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age > 8 }.subscribe(waitForSync: .onCreation) + XCTAssertEqual(results1.count, 2) + let results2 = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age > 6 }.subscribe(waitForSync: .always) + XCTAssertEqual(results2.count, 4) + let results3 = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age > 4 }.subscribe(waitForSync: .never) + XCTAssertEqual(results3.count, 4) + XCTAssertEqual(realm.subscriptions.count, 3) + } + + @CustomGlobalActor + func testSubscribeOnRealmConfinedCustomActor() async throws { + try await populateSwiftPerson() + + let user = try await createUser() + var config = user.flexibleSyncConfiguration() + config.objectTypes = [SwiftPerson.self] + let realm = try await Realm(configuration: config, actor: CustomGlobalActor.shared) + let results1 = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age > 8 }.subscribe(waitForSync: .onCreation) + XCTAssertEqual(results1.count, 2) + let results2 = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age > 6 }.subscribe(waitForSync: .always) + XCTAssertEqual(results2.count, 4) + let results3 = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age > 4 }.subscribe(waitForSync: .never) + XCTAssertEqual(results3.count, 4) + XCTAssertEqual(realm.subscriptions.count, 3) + } + + @MainActor + func testUnsubscribe() async throws { + try await populateSwiftPerson() + let realm = try await openRealm() + + let results1 = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.lastName == "lastname_3" }.subscribe() + XCTAssertEqual(realm.subscriptions.count, 1) + results1.unsubscribe() + XCTAssertEqual(realm.subscriptions.count, 0) + } + + @MainActor + func testUnsubscribeAfterReassign() async throws { + try await populateSwiftPerson() + let realm = try await openRealm() + + var results0 = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age >= 8 }.subscribe() + XCTAssertEqual(results0.count, 3) + XCTAssertEqual(realm.subscriptions.count, 1) + results0 = try await results0 + .where { $0.firstName == name && $0.age < 8 }.subscribe() // subscribes to "age >= 8 && age < 8" because that's the local query + XCTAssertEqual(results0.count, 0) + XCTAssertEqual(realm.subscriptions.count, 2) // Two subs present:1) "age >= 8" 2) "age >= 8 && age < 8" + let results1 = realm.objects(SwiftPerson.self) + XCTAssertEqual(results1.count, 3) + results0.unsubscribe() // unsubscribes from "age >= 8 && age < 8" + XCTAssertEqual(realm.subscriptions.count, 1) + XCTAssertNotNil(realm.subscriptions.first(ofType: SwiftPerson.self) { $0.firstName == name && $0.age >= 8 }) + XCTAssertEqual(results0.count, 0) // local query is still "age >= 8 && age < 8". + XCTAssertEqual(results1.count, 3) + } + + @MainActor + func testUnsubscribeWithoutSubscriptionExistingNamed() async throws { + try await populateSwiftPerson() + let realm = try await openRealm() + + _ = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age >= 8 }.subscribe(name: "sub1") + XCTAssertEqual(realm.subscriptions.count, 1) + let results = realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age >= 8 } + results.unsubscribe() + XCTAssertEqual(realm.subscriptions.count, 1) + XCTAssertEqual(realm.subscriptions.first!.name, "sub1") + } + + func testUnsubscribeNoExistingMatch() async throws { + try await populateSwiftPerson() + let realm = try await openRealm() + + XCTAssertEqual(realm.subscriptions.count, 0) + _ = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age >= 8 }.subscribe(name: "age_older_8") + let results0 = realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age >= 8 } + XCTAssertEqual(realm.subscriptions.count, 1) + XCTAssertEqual(results0.count, 3) + results0.unsubscribe() + XCTAssertEqual(realm.subscriptions.count, 1) + XCTAssertEqual(results0.count, 3) // Results are not modified because there is no subscription associated to the unsubscribed result + } + + @MainActor + func testUnsubscribeNamed() async throws { + try await populateSwiftPerson() + let realm = try await openRealm() + + _ = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age >= 8 }.subscribe() + _ = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age >= 8 }.subscribe(name: "first_named") + let results = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age >= 8 }.subscribe(name: "second_named") + XCTAssertEqual(realm.subscriptions.count, 3) + + results.unsubscribe() + XCTAssertEqual(realm.subscriptions.count, 2) + XCTAssertEqual(realm.subscriptions[0]!.name, nil) + XCTAssertEqual(realm.subscriptions[1]!.name, "first_named") + results.unsubscribe() // check again for case when subscription doesn't exist + XCTAssertEqual(realm.subscriptions.count, 2) + XCTAssertEqual(realm.subscriptions[0]!.name, nil) + XCTAssertEqual(realm.subscriptions[1]!.name, "first_named") + } + + @MainActor + func testUnsubscribeReassign() async throws { + try await populateSwiftPerson() + let realm = try await openRealm() + + _ = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age >= 8 }.subscribe(name: "first_named") + var results = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age >= 8 }.subscribe(name: "second_named") + // expect `results` associated subscription to be reassigned to the id which matches the unnamed subscription + results = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age >= 8 }.subscribe() + XCTAssertEqual(realm.subscriptions.count, 3) + + results.unsubscribe() + // so the two named subscriptions remain. + XCTAssertEqual(realm.subscriptions.count, 2) + XCTAssertEqual(realm.subscriptions[0]!.name, "first_named") + XCTAssertEqual(realm.subscriptions[1]!.name, "second_named") + } + + @MainActor + func testUnsubscribeSameQueryDifferentName() async throws { + try await populateSwiftPerson() + let realm = try await openRealm() + + _ = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age >= 8 }.subscribe() + let results2 = realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age >= 8 } + XCTAssertEqual(realm.subscriptions.count, 1) + results2.unsubscribe() + XCTAssertEqual(realm.subscriptions.count, 0) + } + + @MainActor + func testSubscribeNameAcrossTypes() async throws { + try await populateSwiftPerson() + let realm = try await openRealm() + + let results = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age >= 8 }.subscribe(name: "sameName") + XCTAssertEqual(realm.subscriptions.count, 1) + XCTAssertEqual(results.count, 3) + _ = try await realm.objects(SwiftTypesSyncObject.self).subscribe(name: "sameName") + XCTAssertEqual(realm.subscriptions.count, 1) + XCTAssertEqual(results.count, 0) + } + + @MainActor + func testSubscribeOnCreation() async throws { + try await populateSwiftPerson() + let realm = try await openRealm() + + var results = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age >= 8 }.subscribe(waitForSync: .onCreation) + XCTAssertEqual(results.count, 3) + let expectation = XCTestExpectation(description: "method doesn't hang") + realm.syncSession!.suspend() + let task = Task { + results = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age >= 8 } + .subscribe(waitForSync: .onCreation) + XCTAssertEqual(results.count, 3) // expect method to return immediately, and not hang while no connection + XCTAssertEqual(realm.subscriptions.count, 1) + expectation.fulfill() + } + await fulfillment(of: [expectation], timeout: 2.0) + try await task.value + } + + @MainActor + func testSubscribeAlways() async throws { + let collection = anonymousUser.collection(for: SwiftPerson.self, app: app) + try await populateSwiftPerson() + let realm = try await openRealm() + + var results = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age >= 9 }.subscribe(waitForSync: .always) + XCTAssertEqual(results.count, 2) + + // suspend session on client. Add a document that isn't on the client. + realm.syncSession!.suspend() + let serverObject: Document = [ + "_id": .objectId(ObjectId.generate()), + "firstName": .string(name), + "lastName": .string("M"), + "age": .int32(30) + ] + collection.insertOne(serverObject).await(self, timeout: 10.0) + + // Resume the client session. + realm.syncSession!.resume() + XCTAssertEqual(results.count, 2) + results = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age >= 9 }.subscribe(waitForSync: .always) + // Expect this subscribe call to wait for sync downloads, even though the subscription already existed + XCTAssertEqual(results.count, 3) // Count is 3 because it includes the object/document that was created while offline. + XCTAssertEqual(realm.subscriptions.count, 1) + } + + @MainActor + func testSubscribeNever() async throws { + try await populateSwiftPerson() + let realm = try await openRealm() + + let expectation = XCTestExpectation(description: "test doesn't hang") + Task { + let results = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age >= 8 }.subscribe(waitForSync: .never) + XCTAssertEqual(results.count, 0) // expect no objects to be able to sync because of immediate return + XCTAssertEqual(realm.subscriptions.count, 1) + expectation.fulfill() + } + await fulfillment(of: [expectation], timeout: 1) + } + + @MainActor + func testSubscribeTimeout() async throws { + try await populateSwiftPerson() + let realm = try await openRealm() + + realm.syncSession!.suspend() + let timeout = 1.0 + do { + _ = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age >= 8 } + .subscribe(waitForSync: .always, timeout: timeout) + XCTFail("subscribe did not time out") + } catch let error as NSError { + XCTAssertEqual(error.code, Int(ETIMEDOUT)) + XCTAssertEqual(error.domain, NSPOSIXErrorDomain) + XCTAssertEqual(error.localizedDescription, "Waiting for update timed out after \(timeout) seconds.") + } + } + + @MainActor + func testSubscribeTimeoutSucceeds() async throws { + try await populateSwiftPerson() + + let realm = try await openRealm() + let results0 = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.age >= 6 }.subscribe(timeout: 2.0) + XCTAssertEqual(results0.count, 5) + XCTAssertEqual(realm.subscriptions.count, 1) + let results1 = try await realm.objects(SwiftPerson.self) + .where { $0.firstName == name && $0.lastName == "lastname_3" }.subscribe(timeout: 2.0) + XCTAssertEqual(results1.count, 1) + XCTAssertEqual(results0.count, 5) + XCTAssertEqual(realm.subscriptions.count, 2) + + let results2 = realm.objects(SwiftPerson.self) + XCTAssertEqual(results2.count, 6) + } + + // MARK: - Custom Column + + func testCustomColumnFlexibleSyncSchema() throws { + let realm = try openRealm() + for property in realm.schema.objectSchema.first(where: { $0.className == "SwiftCustomColumnObject" })!.properties { + XCTAssertEqual(customColumnPropertiesMapping[property.name], property.columnName) + } + } + + @MainActor + func testCreateCustomColumnFlexibleSyncSubscription() async throws { + let objectId = ObjectId.generate() + try await write { realm in + let valuesDictionary: [String: Any] = ["id": objectId, + "boolCol": true, + "intCol": 365, + "doubleCol": 365.365, + "stringCol": "@#¢∞¬÷÷", + "binaryCol": "string".data(using: String.Encoding.utf8)!, + "dateCol": Date(timeIntervalSince1970: -365), + "longCol": 365, + "decimalCol": Decimal128(365), + "uuidCol": UUID(uuidString: "629bba42-97dc-4fee-97ff-78af054952ec")!, + "objectIdCol": ObjectId.generate()] + + realm.create(SwiftCustomColumnObject.self, value: valuesDictionary) + } + + let user = try await createUser() + var config = user.flexibleSyncConfiguration(initialSubscriptions: { subscriptions in + subscriptions.append(QuerySubscription()) + }) + config.objectTypes = [SwiftCustomColumnObject.self] + let realm = try await Realm(configuration: config, downloadBeforeOpen: .once) + XCTAssertEqual(realm.subscriptions.count, 1) + + let foundObject = realm.object(ofType: SwiftCustomColumnObject.self, forPrimaryKey: objectId) + XCTAssertNotNil(foundObject) + XCTAssertEqual(foundObject!.id, objectId) + XCTAssertEqual(foundObject!.boolCol, true) + XCTAssertEqual(foundObject!.intCol, 365) + XCTAssertEqual(foundObject!.doubleCol, 365.365) + XCTAssertEqual(foundObject!.stringCol, "@#¢∞¬÷÷") + XCTAssertEqual(foundObject!.binaryCol, "string".data(using: String.Encoding.utf8)!) + XCTAssertEqual(foundObject!.dateCol, Date(timeIntervalSince1970: -365)) + XCTAssertEqual(foundObject!.longCol, 365) + XCTAssertEqual(foundObject!.decimalCol, Decimal128(365)) + XCTAssertEqual(foundObject!.uuidCol, UUID(uuidString: "629bba42-97dc-4fee-97ff-78af054952ec")!) + XCTAssertNotNil(foundObject?.objectIdCol) + XCTAssertNil(foundObject?.objectCol) + } + + @MainActor + func testCustomColumnFlexibleSyncSubscriptionNSPredicate() async throws { + let objectId = ObjectId.generate() + let linkedObjectId = ObjectId.generate() + try await write { realm in + let object = SwiftCustomColumnObject() + object.id = objectId + object.binaryCol = "string".data(using: String.Encoding.utf8)! + let linkedObject = SwiftCustomColumnObject() + linkedObject.id = linkedObjectId + object.objectCol = linkedObject + realm.add(object) + } + let user = try await createUser() + + var config = user.flexibleSyncConfiguration(initialSubscriptions: { subscriptions in + subscriptions.append(QuerySubscription(where: NSPredicate(format: "id == %@ || id == %@", objectId, linkedObjectId))) + }) + config.objectTypes = [SwiftCustomColumnObject.self] + let realm = try await Realm(configuration: config, downloadBeforeOpen: .once) + XCTAssertEqual(realm.subscriptions.count, 1) + checkCount(expected: 2, realm, SwiftCustomColumnObject.self) + + let foundObject = realm.objects(SwiftCustomColumnObject.self).where { $0.id == objectId }.first + XCTAssertNotNil(foundObject) + XCTAssertEqual(foundObject!.id, objectId) + XCTAssertEqual(foundObject!.boolCol, true) + XCTAssertEqual(foundObject!.intCol, 1) + XCTAssertEqual(foundObject!.doubleCol, 1.1) + XCTAssertEqual(foundObject!.stringCol, "string") + XCTAssertEqual(foundObject!.binaryCol, "string".data(using: String.Encoding.utf8)!) + XCTAssertEqual(foundObject!.dateCol, Date(timeIntervalSince1970: -1)) + XCTAssertEqual(foundObject!.longCol, 1) + XCTAssertEqual(foundObject!.decimalCol, Decimal128(1)) + XCTAssertEqual(foundObject!.uuidCol, UUID(uuidString: "85d4fbee-6ec6-47df-bfa1-615931903d7e")!) + XCTAssertNil(foundObject?.objectIdCol) + XCTAssertEqual(foundObject!.objectCol!.id, linkedObjectId) + } + + @MainActor + func testCustomColumnFlexibleSyncSubscriptionFilter() async throws { + let objectId = ObjectId.generate() + let linkedObjectId = ObjectId.generate() + try await write { realm in + let object = SwiftCustomColumnObject() + object.id = objectId + object.binaryCol = "string".data(using: String.Encoding.utf8)! + let linkedObject = SwiftCustomColumnObject() + linkedObject.id = linkedObjectId + object.objectCol = linkedObject + realm.add(object) + } + let user = try await createUser() + + var config = user.flexibleSyncConfiguration(initialSubscriptions: { subscriptions in + subscriptions.append(QuerySubscription(where: "id == %@ || id == %@", objectId, linkedObjectId)) + }) + config.objectTypes = [SwiftCustomColumnObject.self] + let realm = try await Realm(configuration: config, downloadBeforeOpen: .once) + XCTAssertEqual(realm.subscriptions.count, 1) + checkCount(expected: 2, realm, SwiftCustomColumnObject.self) + + let foundObject = realm.objects(SwiftCustomColumnObject.self).where { $0.id == objectId }.first + XCTAssertNotNil(foundObject) + XCTAssertEqual(foundObject!.id, objectId) + XCTAssertEqual(foundObject!.boolCol, true) + XCTAssertEqual(foundObject!.intCol, 1) + XCTAssertEqual(foundObject!.doubleCol, 1.1) + XCTAssertEqual(foundObject!.stringCol, "string") + XCTAssertEqual(foundObject!.binaryCol, "string".data(using: String.Encoding.utf8)!) + XCTAssertEqual(foundObject!.dateCol, Date(timeIntervalSince1970: -1)) + XCTAssertEqual(foundObject!.longCol, 1) + XCTAssertEqual(foundObject!.decimalCol, Decimal128(1)) + XCTAssertEqual(foundObject!.uuidCol, UUID(uuidString: "85d4fbee-6ec6-47df-bfa1-615931903d7e")!) + XCTAssertNil(foundObject?.objectIdCol) + XCTAssertEqual(foundObject!.objectCol!.id, linkedObjectId) + } + + @MainActor + func testCustomColumnFlexibleSyncSubscriptionQuery() async throws { + let objectId = ObjectId.generate() + let linkedObjectId = ObjectId.generate() + try await write { realm in + let object = SwiftCustomColumnObject() + object.id = objectId + object.binaryCol = "string".data(using: String.Encoding.utf8)! + let linkedObject = SwiftCustomColumnObject() + linkedObject.id = linkedObjectId + object.objectCol = linkedObject + realm.add(object) + } + let user = try await createUser() + + var config = user.flexibleSyncConfiguration(initialSubscriptions: { subscriptions in + subscriptions.append(QuerySubscription { + $0.id == objectId || $0.id == linkedObjectId + }) + }) + config.objectTypes = [SwiftCustomColumnObject.self] + let realm = try await Realm(configuration: config, downloadBeforeOpen: .once) + XCTAssertEqual(realm.subscriptions.count, 1) + checkCount(expected: 2, realm, SwiftCustomColumnObject.self) + + let foundObject = realm.objects(SwiftCustomColumnObject.self).where { $0.id == objectId }.first + + XCTAssertNotNil(foundObject) + XCTAssertEqual(foundObject!.id, objectId) + XCTAssertEqual(foundObject!.boolCol, true) + XCTAssertEqual(foundObject!.intCol, 1) + XCTAssertEqual(foundObject!.doubleCol, 1.1) + XCTAssertEqual(foundObject!.stringCol, "string") + XCTAssertEqual(foundObject!.binaryCol, "string".data(using: String.Encoding.utf8)!) + XCTAssertEqual(foundObject!.dateCol, Date(timeIntervalSince1970: -1)) + XCTAssertEqual(foundObject!.longCol, 1) + XCTAssertEqual(foundObject!.decimalCol, Decimal128(1)) + XCTAssertEqual(foundObject!.uuidCol, UUID(uuidString: "85d4fbee-6ec6-47df-bfa1-615931903d7e")!) + XCTAssertNil(foundObject?.objectIdCol) + XCTAssertEqual(foundObject!.objectCol!.id, linkedObjectId) + } +} + +@available(macOS 13, *) +@globalActor actor CustomGlobalActor: GlobalActor { + static let shared = CustomGlobalActor() +} + +#endif // os(macOS) && swift(>=5.8) diff --git a/Realm/ObjectServerTests/ClientResetTests.swift b/Realm/ObjectServerTests/ClientResetTests.swift new file mode 100644 index 0000000000..0a05a2aba5 --- /dev/null +++ b/Realm/ObjectServerTests/ClientResetTests.swift @@ -0,0 +1,715 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2023 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +#if os(macOS) + +import Realm +import Realm.Private +import RealmSwift +import XCTest + +#if canImport(RealmTestSupport) +import RealmSwiftSyncTestSupport +import RealmSyncTestSupport +import RealmTestSupport +import RealmSwiftTestSupport +#endif + +// Uses admin API to toggle recovery mode on the baas server +func waitForEditRecoveryMode(flexibleSync: Bool = false, appId: String, disable: Bool) throws { + // Retrieve server IDs + let appServerId = try RealmServer.shared.retrieveAppServerId(appId) + let syncServiceId = try RealmServer.shared.retrieveSyncServiceId(appServerId: appServerId) + guard let syncServiceConfig = try RealmServer.shared.getSyncServiceConfiguration(appServerId: appServerId, syncServiceId: syncServiceId) else { fatalError("precondition failure: no sync service configuration found") } + + _ = try RealmServer.shared.patchRecoveryMode( + flexibleSync: flexibleSync, disable: disable, appServerId, + syncServiceId, syncServiceConfig).get() +} + +@available(macOS 13, *) +class ClientResetTests: SwiftSyncTestCase { + func prepareClientReset(app: App? = nil) throws -> User { + let app = app ?? self.app + let config = try configuration(app: app) + let user = config.syncConfiguration!.user + try autoreleasepool { + let realm = try openRealm(configuration: config) + realm.syncSession!.suspend() + + try RealmServer.shared.triggerClientReset(app.appId, realm) + + // Add an object to the local realm that won't be synced due to the suspend + try realm.write { + realm.add(SwiftPerson(firstName: "John", lastName: name)) + } + } + + // Add an object which should be present post-reset + try write(app: app) { realm in + realm.add(SwiftPerson(firstName: "Paul", lastName: self.name)) + } + + return user + } + + func expectSyncError(_ fn: () -> Void) -> SyncError? { + let error = Locked(SyncError?.none) + let ex = expectation(description: "Waiting for error handler to be called...") + app.syncManager.errorHandler = { @Sendable (e, _) in + if let e = e as? SyncError { + error.value = e + } else { + XCTFail("Error \(e) was not a sync error. Something is wrong.") + } + ex.fulfill() + } + + fn() + + waitForExpectations(timeout: 10, handler: nil) + XCTAssertNotNil(error.value) + return error.value + } + + func assertManualClientReset(_ user: User, app: App) -> ErrorReportingBlock { + let ex = self.expectation(description: "get client reset error") + return { error, session in + guard let error = error as? SyncError else { + return XCTFail("Bad error type: \(error)") + } + XCTAssertEqual(error.code, .clientResetError) + XCTAssertEqual(session?.state, .inactive) + XCTAssertEqual(session?.connectionState, .disconnected) + XCTAssertEqual(session?.parentUser()?.id, user.id) + guard let (path, token) = error.clientResetInfo() else { + return XCTAssertNotNil(error.clientResetInfo()) + } + XCTAssertTrue(path.contains("mongodb-realm/\(app.appId)/recovered-realms/recovered_realm")) + XCTAssertFalse(FileManager.default.fileExists(atPath: path)) + SyncSession.immediatelyHandleError(token, syncManager: app.syncManager) + XCTAssertTrue(FileManager.default.fileExists(atPath: path)) + ex.fulfill() + } + } + + func assertDiscardLocal() -> (@Sendable (Realm) -> Void, @Sendable (Realm, Realm) -> Void) { + let beforeCallbackEx = expectation(description: "before reset callback") + @Sendable func beforeClientReset(_ before: Realm) { + let results = before.objects(SwiftPerson.self) + XCTAssertEqual(results.count, 1) + XCTAssertEqual(results.filter("firstName == 'John'").count, 1) + + beforeCallbackEx.fulfill() + } + let afterCallbackEx = expectation(description: "before reset callback") + @Sendable func afterClientReset(_ before: Realm, _ after: Realm) { + let results = before.objects(SwiftPerson.self) + XCTAssertEqual(results.count, 1) + XCTAssertEqual(results.filter("firstName == 'John'").count, 1) + + let results2 = after.objects(SwiftPerson.self) + XCTAssertEqual(results2.count, 1) + XCTAssertEqual(results2.filter("firstName == 'Paul'").count, 1) + + // Fulfill on the main thread to make it harder to hit a race + // condition where the test completes before the client reset finishes + // unwinding. This does not fully fix the problem. + DispatchQueue.main.async { + afterCallbackEx.fulfill() + } + } + return (beforeClientReset, afterClientReset) + } + + func assertRecover() -> (@Sendable (Realm) -> Void, @Sendable (Realm, Realm) -> Void) { + let beforeCallbackEx = expectation(description: "before reset callback") + @Sendable func beforeClientReset(_ before: Realm) { + let results = before.objects(SwiftPerson.self) + XCTAssertEqual(results.count, 1) + XCTAssertEqual(results.filter("firstName == 'John'").count, 1) + beforeCallbackEx.fulfill() + } + let afterCallbackEx = expectation(description: "after reset callback") + @Sendable func afterClientReset(_ before: Realm, _ after: Realm) { + let results = before.objects(SwiftPerson.self) + XCTAssertEqual(results.count, 1) + XCTAssertEqual(results.filter("firstName == 'John'").count, 1) + + let results2 = after.objects(SwiftPerson.self) + XCTAssertEqual(results2.count, 2) + XCTAssertEqual(results2.filter("firstName == 'John'").count, 1) + XCTAssertEqual(results2.filter("firstName == 'Paul'").count, 1) + + // Fulfill on the main thread to make it harder to hit a race + // condition where the test completes before the client reset finishes + // unwinding. This does not fully fix the problem. + DispatchQueue.main.async { + afterCallbackEx.fulfill() + } + } + return (beforeClientReset, afterClientReset) + } + + func verifyClientResetDiscardedLocalChanges(_ user: User) throws { + try autoreleasepool { + var configuration = user.configuration(partitionValue: name) + configuration.objectTypes = [SwiftPerson.self] + + let realm = try Realm(configuration: configuration) + waitForDownloads(for: realm) + // After reopening, the old Realm file should have been moved aside + // and we should now have the data from the server + XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) + XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "Paul") + } + } +} + +@available(macOS 13, *) +class PBSClientResetTests: ClientResetTests { + func testClientResetManual() throws { + let user = try prepareClientReset() + try autoreleasepool { + var configuration = user.configuration(partitionValue: name, clientResetMode: .manual()) + configuration.objectTypes = [SwiftPerson.self] + + let syncManager = app.syncManager + syncManager.errorHandler = assertManualClientReset(user, app: app) + + try autoreleasepool { + let realm = try Realm(configuration: configuration) + waitForExpectations(timeout: 15.0) + realm.refresh() + // The locally created object should still be present as we didn't + // actually handle the client reset + XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) + XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "John") + } + } + app.syncManager.waitForSessionTermination() + try verifyClientResetDiscardedLocalChanges(user) + } + + func testClientResetManualWithEnumCallback() throws { + let user = try prepareClientReset() + try autoreleasepool { + var configuration = user.configuration(partitionValue: name, clientResetMode: .manual(errorHandler: assertManualClientReset(user, app: app))) + configuration.objectTypes = [SwiftPerson.self] + + switch configuration.syncConfiguration!.clientResetMode { + case .manual(let block): + XCTAssertNotNil(block) + default: + XCTFail("Should be set to manual") + } + + try autoreleasepool { + let realm = try Realm(configuration: configuration) + waitForExpectations(timeout: 15.0) + realm.refresh() + // The locally created object should still be present as we didn't + // actually handle the client reset + XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) + XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "John") + } + } + try verifyClientResetDiscardedLocalChanges(user) + } + + func testClientResetManualManagerFallback() throws { + let user = try prepareClientReset() + + try autoreleasepool { + // No callback is passed into enum `.manual`, but a syncManager.errorHandler exists, + // so expect that to be used instead. + var configuration = user.configuration(partitionValue: name, clientResetMode: .manual()) + configuration.objectTypes = [SwiftPerson.self] + + let syncManager = self.app.syncManager + syncManager.errorHandler = assertManualClientReset(user, app: app) + + try autoreleasepool { + let realm = try Realm(configuration: configuration) + waitForExpectations(timeout: 15.0) // Wait for expectations in asssertManualClientReset + // The locally created object should still be present as we didn't + // actually handle the client reset + XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) + XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "John") + } + } + + try verifyClientResetDiscardedLocalChanges(user) + } + + // If the syncManager.ErrorHandler and manual enum callback + // are both set, use the enum callback. + func testClientResetManualEnumCallbackNotManager() throws { + let user = try prepareClientReset() + + try autoreleasepool { + var configuration = user.configuration(partitionValue: name, clientResetMode: .manual(errorHandler: assertManualClientReset(user, app: app))) + configuration.objectTypes = [SwiftPerson.self] + + switch configuration.syncConfiguration!.clientResetMode { + case .manual(let block): + XCTAssertNotNil(block) + default: + XCTFail("Should be set to manual") + } + + let syncManager = self.app.syncManager + syncManager.errorHandler = { error, _ in + guard nil != error as? SyncError else { + return XCTFail("Bad error type: \(error)") + } + XCTFail("Expected the syncManager.ErrorHandler to not be called") + } + + try autoreleasepool { + let realm = try Realm(configuration: configuration) + waitForExpectations(timeout: 15.0) + // The locally created object should still be present as we didn't + // actually handle the client reset + XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) + XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "John") + } + } + + try verifyClientResetDiscardedLocalChanges(user) + } + + func testClientResetManualWithoutLiveRealmInstance() throws { + let user = try prepareClientReset() + + var configuration = user.configuration(partitionValue: name, clientResetMode: .manual()) + configuration.objectTypes = [SwiftPerson.self] + + let syncManager = app.syncManager + syncManager.errorHandler = assertManualClientReset(user, app: app) + + try autoreleasepool { + _ = try Realm(configuration: configuration) + // We have to wait for the error to arrive (or the session will just + // transition to inactive without calling the error handler), but we + // need to ensure the Realm is deallocated before the error handler + // is invoked on the main thread. + sleep(1) + } + waitForExpectations(timeout: 15.0) + syncManager.waitForSessionTermination() + resetSyncManager() + } + + @available(*, deprecated) // .discardLocal + func testClientResetDiscardLocal() throws { + let user = try prepareClientReset() + + let (assertBeforeBlock, assertAfterBlock) = assertDiscardLocal() + var configuration = user.configuration(partitionValue: name, + clientResetMode: .discardLocal(beforeReset: assertBeforeBlock, afterReset: assertAfterBlock)) + configuration.objectTypes = [SwiftPerson.self] + + let syncConfig = try XCTUnwrap(configuration.syncConfiguration) + switch syncConfig.clientResetMode { + case .discardUnsyncedChanges(let before, let after): + XCTAssertNotNil(before) + XCTAssertNotNil(after) + default: + XCTFail("Should be set to discardLocal") + } + + try autoreleasepool { + let realm = try Realm(configuration: configuration) + let results = realm.objects(SwiftPerson.self) + XCTAssertEqual(results.count, 1) + waitForExpectations(timeout: 15.0) + realm.refresh() // expectation is potentially fulfilled before autorefresh + // The Person created locally ("John") should have been discarded, + // while the one from the server ("Paul") should be present + XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) + XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "Paul") + } + } + + func testClientResetDiscardUnsyncedChanges() throws { + let user = try prepareClientReset() + + let (assertBeforeBlock, assertAfterBlock) = assertDiscardLocal() + var configuration = user.configuration(partitionValue: name, + clientResetMode: .discardUnsyncedChanges(beforeReset: assertBeforeBlock, afterReset: assertAfterBlock)) + configuration.objectTypes = [SwiftPerson.self] + + guard let syncConfig = configuration.syncConfiguration else { fatalError("Test condition failure. SyncConfiguration not set.") } + switch syncConfig.clientResetMode { + case .discardUnsyncedChanges(let before, let after): + XCTAssertNotNil(before) + XCTAssertNotNil(after) + default: + XCTFail("Should be set to discardUnsyncedChanges") + } + + try autoreleasepool { + let realm = try Realm(configuration: configuration) + waitForExpectations(timeout: 15.0) + realm.refresh() + // The Person created locally ("John") should have been discarded, + // while the one from the server ("Paul") should be present + XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) + XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "Paul") + } + } + + @available(*, deprecated) // .discardLocal + func testClientResetDiscardLocalAsyncOpen() throws { + let user = try prepareClientReset() + + let (assertBeforeBlock, assertAfterBlock) = assertDiscardLocal() + var configuration = user.configuration(partitionValue: name, clientResetMode: .discardLocal(beforeReset: assertBeforeBlock, afterReset: assertAfterBlock)) + configuration.objectTypes = [SwiftPerson.self] + + let asyncOpenEx = expectation(description: "async open") + Realm.asyncOpen(configuration: configuration) { result in + let realm = try! result.get() + XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) + XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "Paul") + asyncOpenEx.fulfill() + } + waitForExpectations(timeout: 15.0) + } + + func testClientResetRecover() throws { + let user = try prepareClientReset() + + let (assertBeforeBlock, assertAfterBlock) = assertRecover() + var configuration = user.configuration(partitionValue: name, clientResetMode: .recoverUnsyncedChanges(beforeReset: assertBeforeBlock, afterReset: assertAfterBlock)) + configuration.objectTypes = [SwiftPerson.self] + + let syncConfig = try XCTUnwrap(configuration.syncConfiguration) + switch syncConfig.clientResetMode { + case .recoverUnsyncedChanges(let before, let after): + XCTAssertNotNil(before) + XCTAssertNotNil(after) + default: + XCTFail("Should be set to recover") + } + try autoreleasepool { + let realm = try Realm(configuration: configuration) + waitForExpectations(timeout: 15.0) + XCTAssertEqual(realm.objects(SwiftPerson.self).count, 2) + // The object created locally (John) and the object created on the server (Paul) + // should both be integrated into the new realm file. + XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "John") + XCTAssertEqual(realm.objects(SwiftPerson.self)[1].firstName, "Paul") + } + } + + func testClientResetRecoverAsyncOpen() throws { + let user = try prepareClientReset() + + let (assertBeforeBlock, assertAfterBlock) = assertRecover() + var configuration = user.configuration(partitionValue: name, clientResetMode: .recoverUnsyncedChanges(beforeReset: assertBeforeBlock, afterReset: assertAfterBlock)) + configuration.objectTypes = [SwiftPerson.self] + + let syncConfig = try XCTUnwrap(configuration.syncConfiguration) + switch syncConfig.clientResetMode { + case .recoverUnsyncedChanges(let before, let after): + XCTAssertNotNil(before) + XCTAssertNotNil(after) + default: + XCTFail("Should be set to recover") + } + autoreleasepool { + let realm = Realm.asyncOpen(configuration: configuration).await(self) + XCTAssertEqual(realm.objects(SwiftPerson.self).count, 2) + // The object created locally (John) and the object created on the server (Paul) + // should both be integrated into the new realm file. + XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "John") + XCTAssertEqual(realm.objects(SwiftPerson.self)[1].firstName, "Paul") + waitForExpectations(timeout: 15.0) + } + } + + func testClientResetRecoverWithSchemaChanges() throws { + let user = try prepareClientReset() + + let beforeCallbackEx = expectation(description: "before reset callback") + @Sendable func beforeClientReset(_ before: Realm) { + let person = before.objects(SwiftPersonWithAdditionalProperty.self).first! + XCTAssertEqual(person.objectSchema.properties.map(\.name), + ["_id", "firstName", "lastName", "age", "newProperty"]) + XCTAssertEqual(person.newProperty, 0) + beforeCallbackEx.fulfill() + } + let afterCallbackEx = expectation(description: "after reset callback") + @Sendable func afterClientReset(_ before: Realm, _ after: Realm) { + let beforePerson = before.objects(SwiftPersonWithAdditionalProperty.self).first! + XCTAssertEqual(beforePerson.objectSchema.properties.map(\.name), + ["_id", "firstName", "lastName", "age", "newProperty"]) + XCTAssertEqual(beforePerson.newProperty, 0) + let afterPerson = after.objects(SwiftPersonWithAdditionalProperty.self).first! + XCTAssertEqual(afterPerson.objectSchema.properties.map(\.name), + ["_id", "firstName", "lastName", "age", "newProperty"]) + XCTAssertEqual(afterPerson.newProperty, 0) + + // Fulfill on the main thread to make it harder to hit a race + // condition where the test completes before the client reset finishes + // unwinding. This does not fully fix the problem. + DispatchQueue.main.async { + afterCallbackEx.fulfill() + } + } + + var configuration = user.configuration(partitionValue: name, clientResetMode: .recoverUnsyncedChanges(beforeReset: beforeClientReset, afterReset: afterClientReset)) + configuration.objectTypes = [SwiftPersonWithAdditionalProperty.self] + + autoreleasepool { + _ = Realm.asyncOpen(configuration: configuration).await(self) + waitForExpectations(timeout: 15.0) + } + } + + func testClientResetRecoverOrDiscardLocalFailedRecovery() throws { + let appId = try RealmServer.shared.createApp(types: [SwiftPerson.self]) + // Disable recovery mode on the server. + // This attempts to simulate a case where recovery mode fails when + // using RecoverOrDiscardLocal + try waitForEditRecoveryMode(appId: appId, disable: true) + + let user = try prepareClientReset(app: self.app(id: appId)) + // Expect the recovery to fail back to discardLocal logic + let (assertBeforeBlock, assertAfterBlock) = assertDiscardLocal() + var configuration = user.configuration(partitionValue: name, clientResetMode: .recoverOrDiscardUnsyncedChanges(beforeReset: assertBeforeBlock, afterReset: assertAfterBlock)) + configuration.objectTypes = [SwiftPerson.self] + + let syncConfig = try XCTUnwrap(configuration.syncConfiguration) + switch syncConfig.clientResetMode { + case .recoverOrDiscardUnsyncedChanges(let before, let after): + XCTAssertNotNil(before) + XCTAssertNotNil(after) + default: + XCTFail("Should be set to recoverOrDiscard") + } + + // Expect the recovery to fail back to discardLocal logic + try autoreleasepool { + let realm = try Realm(configuration: configuration) + waitForExpectations(timeout: 15.0) + realm.refresh() + XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) + // The Person created locally ("John") should have been discarded, + // while the one from the server ("Paul") should be present. + XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "Paul") + } + } +} + +@available(macOS 13, *) +class FLXClientResetTests: ClientResetTests { + override func createApp() throws -> String { + try createFlexibleSyncApp() + } + + override func configuration(user: User) -> Realm.Configuration { + user.flexibleSyncConfiguration { subscriptions in + subscriptions.append(QuerySubscription { $0.lastName == self.name }) + } + } + + @available(*, deprecated) // .discardLocal + func testFlexibleSyncDiscardLocalClientReset() throws { + let user = try prepareClientReset() + + let (assertBeforeBlock, assertAfterBlock) = assertDiscardLocal() + var config = user.flexibleSyncConfiguration(clientResetMode: .discardLocal(beforeReset: assertBeforeBlock, afterReset: assertAfterBlock)) + config.objectTypes = [SwiftPerson.self] + let syncConfig = try XCTUnwrap(config.syncConfiguration) + switch syncConfig.clientResetMode { + case .discardUnsyncedChanges(let before, let after): + XCTAssertNotNil(before) + XCTAssertNotNil(after) + default: + XCTFail("Should be set to discardUnsyncedChanges") + } + + try autoreleasepool { + XCTAssertEqual(user.flexibleSyncConfiguration().fileURL, config.fileURL) + let realm = try Realm(configuration: config) + let subscriptions = realm.subscriptions + XCTAssertEqual(subscriptions.count, 1) // subscription created during prepareFlexibleSyncClientReset + + waitForExpectations(timeout: 15.0) + realm.refresh() + XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) + XCTAssertEqual(realm.objects(SwiftPerson.self).first?.firstName, "Paul") + } + } + + func testFlexibleSyncDiscardUnsyncedChangesClientReset() throws { + let user = try prepareClientReset() + + let (assertBeforeBlock, assertAfterBlock) = assertDiscardLocal() + var config = user.flexibleSyncConfiguration(clientResetMode: .discardUnsyncedChanges(beforeReset: assertBeforeBlock, afterReset: assertAfterBlock)) + config.objectTypes = [SwiftPerson.self] + let syncConfig = try XCTUnwrap(config.syncConfiguration) + switch syncConfig.clientResetMode { + case .discardUnsyncedChanges(let before, let after): + XCTAssertNotNil(before) + XCTAssertNotNil(after) + default: + XCTFail("Should be set to discardUnsyncedChanges") + } + + try autoreleasepool { + XCTAssertEqual(user.flexibleSyncConfiguration().fileURL, config.fileURL) + let realm = try Realm(configuration: config) + let subscriptions = realm.subscriptions + XCTAssertEqual(subscriptions.count, 1) // subscription created during prepareFlexibleSyncClientReset + + waitForExpectations(timeout: 15.0) + realm.refresh() + XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) + XCTAssertEqual(realm.objects(SwiftPerson.self).first?.firstName, "Paul") + } + } + + func testFlexibleSyncClientResetRecover() throws { + let user = try prepareClientReset() + + let (assertBeforeBlock, assertAfterBlock) = assertRecover() + var config = user.flexibleSyncConfiguration(clientResetMode: .recoverUnsyncedChanges(beforeReset: assertBeforeBlock, afterReset: assertAfterBlock)) + config.objectTypes = [SwiftPerson.self] + let syncConfig = try XCTUnwrap(config.syncConfiguration) + switch syncConfig.clientResetMode { + case .recoverUnsyncedChanges(let before, let after): + XCTAssertNotNil(before) + XCTAssertNotNil(after) + default: + XCTFail("Should be set to recover") + } + + try autoreleasepool { + XCTAssertEqual(user.flexibleSyncConfiguration().fileURL, config.fileURL) + let realm = try Realm(configuration: config) + let subscriptions = realm.subscriptions + XCTAssertEqual(subscriptions.count, 1) // subscription created during prepareFlexibleSyncClientReset + + waitForExpectations(timeout: 15.0) // wait for expectations in assertRecover + realm.refresh() + XCTAssertEqual(realm.objects(SwiftPerson.self).count, 2) + // The object created locally (John) and the object created on the server (Paul) + // should both be integrated into the new realm file. + XCTAssertEqual(realm.objects(SwiftPerson.self).filter("firstName == 'John'").count, 1) + XCTAssertEqual(realm.objects(SwiftPerson.self).filter("firstName == 'Paul'").count, 1) + } + } + + func testFlexibleSyncClientResetRecoverOrDiscardLocalFailedRecovery() throws { + let appId = try RealmServer.shared.createApp(fields: ["lastName"], types: [SwiftPerson.self]) + try waitForEditRecoveryMode(flexibleSync: true, appId: appId, disable: true) + let user = try prepareClientReset(app: app(id: appId)) + + // Expect the client reset process to discard the local changes + let (assertBeforeBlock, assertAfterBlock) = assertDiscardLocal() + var config = user.flexibleSyncConfiguration(clientResetMode: .recoverOrDiscardUnsyncedChanges(beforeReset: assertBeforeBlock, afterReset: assertAfterBlock)) + config.objectTypes = [SwiftPerson.self] + guard let syncConfig = config.syncConfiguration else { + fatalError("Test condition failure. SyncConfiguration not set.") + } + switch syncConfig.clientResetMode { + case .recoverOrDiscardUnsyncedChanges(let before, let after): + XCTAssertNotNil(before) + XCTAssertNotNil(after) + default: + XCTFail("Should be set to recoverOrDiscard") + } + + try autoreleasepool { + XCTAssertEqual(user.flexibleSyncConfiguration().fileURL, config.fileURL) + let realm = try Realm(configuration: config) + let subscriptions = realm.subscriptions + XCTAssertEqual(subscriptions.count, 1) // subscription created during prepareFlexibleSyncClientReset + + waitForExpectations(timeout: 15.0) + realm.refresh() + XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) + // The Person created locally ("John") should have been discarded, + // while the one from the server ("Paul") should be present. + XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "Paul") + } + } + + func testFlexibleClientResetManual() throws { + let user = try prepareClientReset() + try autoreleasepool { + var config = user.flexibleSyncConfiguration(clientResetMode: .manual(errorHandler: assertManualClientReset(user, app: app))) + config.objectTypes = [SwiftPerson.self] + + switch config.syncConfiguration!.clientResetMode { + case .manual(let block): + XCTAssertNotNil(block) + default: + XCTFail("Should be set to manual") + } + try autoreleasepool { + let realm = try Realm(configuration: config) + waitForExpectations(timeout: 15.0) + // The locally created object should still be present as we didn't + // actually handle the client reset + XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) + XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "John") + } + } + + var config = user.flexibleSyncConfiguration { subscriptions in + subscriptions.append(QuerySubscription { $0.lastName == self.name }) + } + config.objectTypes = [SwiftPerson.self] + + try autoreleasepool { + let realm = try openRealm(configuration: config) + XCTAssertEqual(realm.subscriptions.count, 1) + + // After reopening, the old Realm file should have been moved aside + // and we should now have the data from the server + XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) + XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "Paul") + } + } + + func testDefaultClientResetMode() throws { + let user = createUser() + let fConfig = user.flexibleSyncConfiguration() + let pConfig = user.configuration(partitionValue: name) + + switch fConfig.syncConfiguration!.clientResetMode { + case .recoverUnsyncedChanges: + return + default: + XCTFail("expected recover mode") + } + switch pConfig.syncConfiguration!.clientResetMode { + case .recoverUnsyncedChanges: + return + default: + XCTFail("expected recover mode") + } + } +} + +#endif // os(macOS) diff --git a/Realm/ObjectServerTests/CombineSyncTests.swift b/Realm/ObjectServerTests/CombineSyncTests.swift new file mode 100644 index 0000000000..25c151b75f --- /dev/null +++ b/Realm/ObjectServerTests/CombineSyncTests.swift @@ -0,0 +1,713 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2016 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +#if os(macOS) + +import Combine +import Realm +import Realm.Private +import RealmSwift +import XCTest + +#if canImport(RealmTestSupport) +import RealmSwiftSyncTestSupport +import RealmSyncTestSupport +import RealmTestSupport +import RealmSwiftTestSupport +#endif + +@available(macOS 13, *) +@objc(CombineSyncTests) +class CombineSyncTests: SwiftSyncTestCase { + override var objectTypes: [ObjectBase.Type] { + [Dog.self, SwiftPerson.self, SwiftHugeSyncObject.self] + } + + var subscriptions: Set = [] + override func tearDown() { + subscriptions.forEach { $0.cancel() } + subscriptions = [] + super.tearDown() + } + + // swiftlint:disable multiple_closures_with_trailing_closure + func testWatchCombine() throws { + let collection = try setupMongoCollection(for: Dog.self) + let document: Document = ["name": "fido", "breed": "cane corso"] + + let watchEx1 = Locked(expectation(description: "Main thread watch")) + let watchEx2 = Locked(expectation(description: "Background thread watch")) + + collection.watch() + .onOpen { + watchEx1.wrappedValue.fulfill() + } + .subscribe(on: DispatchQueue.global()) + .receive(on: DispatchQueue.global()) + .sink(receiveCompletion: { @Sendable _ in }) { @Sendable _ in + XCTAssertFalse(Thread.isMainThread) + watchEx1.wrappedValue.fulfill() + }.store(in: &subscriptions) + + collection.watch() + .onOpen { + watchEx2.wrappedValue.fulfill() + } + .subscribe(on: DispatchQueue.main) + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: { _ in }) { _ in + XCTAssertTrue(Thread.isMainThread) + watchEx2.wrappedValue.fulfill() + }.store(in: &subscriptions) + + for _ in 0..<3 { + wait(for: [watchEx1.wrappedValue, watchEx2.wrappedValue], timeout: 60.0) + watchEx1.wrappedValue = expectation(description: "Main thread watch") + watchEx2.wrappedValue = expectation(description: "Background thread watch") + collection.insertOne(document) { result in + if case .failure(let error) = result { + XCTFail("Failed to insert: \(error)") + } + } + } + wait(for: [watchEx1.wrappedValue, watchEx2.wrappedValue], timeout: 60.0) + } + + func testWatchCombineWithFilterIds() throws { + let collection = try setupMongoCollection(for: Dog.self) + let document: Document = ["name": "fido", "breed": "cane corso"] + let document2: Document = ["name": "rex", "breed": "cane corso"] + let document3: Document = ["name": "john", "breed": "cane corso"] + let document4: Document = ["name": "ted", "breed": "bullmastiff"] + + let objIds = collection.insertMany([document, document2, document3, document4]).await(self) + let objectIds = objIds.map { $0.objectIdValue! } + + let watchEx1 = Locked(expectation(description: "Main thread watch")) + let watchEx2 = Locked(expectation(description: "Background thread watch")) + collection.watch(filterIds: [objectIds[0]]) + .onOpen { + watchEx1.wrappedValue.fulfill() + } + .subscribe(on: DispatchQueue.main) + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: { _ in }) { changeEvent in + XCTAssertTrue(Thread.isMainThread) + guard let doc = changeEvent.documentValue else { + return + } + + let objectId = doc["fullDocument"]??.documentValue!["_id"]??.objectIdValue! + if objectId == objectIds[0] { + watchEx1.wrappedValue.fulfill() + } + }.store(in: &subscriptions) + + collection.watch(filterIds: [objectIds[1]]) + .onOpen { + watchEx2.wrappedValue.fulfill() + } + .subscribe(on: DispatchQueue.global()) + .receive(on: DispatchQueue.global()) + .sink(receiveCompletion: { _ in }) { @Sendable changeEvent in + XCTAssertFalse(Thread.isMainThread) + guard let doc = changeEvent.documentValue else { + return + } + + let objectId = doc["fullDocument"]??.documentValue!["_id"]??.objectIdValue! + if objectId == objectIds[1] { + watchEx2.wrappedValue.fulfill() + } + }.store(in: &subscriptions) + + for i in 0..<3 { + wait(for: [watchEx1.wrappedValue, watchEx2.wrappedValue], timeout: 60.0) + watchEx1.wrappedValue = expectation(description: "Main thread watch") + watchEx2.wrappedValue = expectation(description: "Background thread watch") + + let name: AnyBSON = .string("fido-\(i)") + collection.updateOneDocument(filter: ["_id": AnyBSON.objectId(objectIds[0])], + update: ["name": name, "breed": "king charles"]) { result in + if case .failure(let error) = result { + XCTFail("Failed to update: \(error)") + } + } + collection.updateOneDocument(filter: ["_id": AnyBSON.objectId(objectIds[1])], + update: ["name": name, "breed": "king charles"]) { result in + if case .failure(let error) = result { + XCTFail("Failed to update: \(error)") + } + } + } + wait(for: [watchEx1.wrappedValue, watchEx2.wrappedValue], timeout: 60.0) + } + + func testWatchCombineWithMatchFilter() throws { + let collection = try setupMongoCollection(for: Dog.self) + let document: Document = ["name": "fido", "breed": "cane corso"] + let document2: Document = ["name": "rex", "breed": "cane corso"] + let document3: Document = ["name": "john", "breed": "cane corso"] + let document4: Document = ["name": "ted", "breed": "bullmastiff"] + + let objIds = collection.insertMany([document, document2, document3, document4]).await(self) + XCTAssertEqual(objIds.count, 4) + let objectIds = objIds.map { $0.objectIdValue! } + + let watchEx1 = Locked(expectation(description: "Main thread watch")) + let watchEx2 = Locked(expectation(description: "Background thread watch")) + collection.watch(matchFilter: ["fullDocument._id": AnyBSON.objectId(objectIds[0])]) + .onOpen { + watchEx1.wrappedValue.fulfill() + } + .subscribe(on: DispatchQueue.main) + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: { _ in }) { changeEvent in + XCTAssertTrue(Thread.isMainThread) + guard let doc = changeEvent.documentValue else { + return + } + + let objectId = doc["fullDocument"]??.documentValue!["_id"]??.objectIdValue! + if objectId == objectIds[0] { + watchEx1.wrappedValue.fulfill() + } + }.store(in: &subscriptions) + + collection.watch(matchFilter: ["fullDocument._id": AnyBSON.objectId(objectIds[1])]) + .onOpen { + watchEx2.wrappedValue.fulfill() + } + .subscribe(on: DispatchQueue.global()) + .receive(on: DispatchQueue.global()) + .sink(receiveCompletion: { _ in }) { @Sendable changeEvent in + XCTAssertFalse(Thread.isMainThread) + guard let doc = changeEvent.documentValue else { + return + } + + let objectId = doc["fullDocument"]??.documentValue!["_id"]??.objectIdValue! + if objectId == objectIds[1] { + watchEx2.wrappedValue.fulfill() + } + }.store(in: &subscriptions) + + for i in 0..<3 { + wait(for: [watchEx1.wrappedValue, watchEx2.wrappedValue], timeout: 60.0) + watchEx1.wrappedValue = expectation(description: "Main thread watch") + watchEx2.wrappedValue = expectation(description: "Background thread watch") + + let name: AnyBSON = .string("fido-\(i)") + collection.updateOneDocument(filter: ["_id": AnyBSON.objectId(objectIds[0])], + update: ["name": name, "breed": "king charles"]) { result in + if case .failure(let error) = result { + XCTFail("Failed to update: \(error)") + } + } + collection.updateOneDocument(filter: ["_id": AnyBSON.objectId(objectIds[1])], + update: ["name": name, "breed": "king charles"]) { result in + if case .failure(let error) = result { + XCTFail("Failed to update: \(error)") + } + } + } + wait(for: [watchEx1.wrappedValue, watchEx2.wrappedValue], timeout: 60.0) + } + + // MARK: - Combine promises + + func testAppLoginCombine() { + let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" + let password = randomString(10) + + let loginEx = expectation(description: "Login user") + let appEx = expectation(description: "App changes triggered") + var triggered = 0 + app.objectWillChange.sink { _ in + triggered += 1 + if triggered == 2 { + appEx.fulfill() + } + }.store(in: &subscriptions) + + app.emailPasswordAuth.registerUser(email: email, password: password) + .flatMap { @Sendable in self.app.login(credentials: .emailPassword(email: email, password: password)) } + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: { result in + if case let .failure(error) = result { + XCTFail("Should have completed login chain: \(error.localizedDescription)") + } + }, receiveValue: { user in + user.objectWillChange.sink { @Sendable user in + XCTAssert(!user.isLoggedIn) + loginEx.fulfill() + }.store(in: &self.subscriptions) + XCTAssertEqual(user.id, self.app.currentUser?.id) + user.logOut { _ in } // logout user and make sure it is observed + }) + .store(in: &subscriptions) + wait(for: [loginEx, appEx], timeout: 30.0) + XCTAssertEqual(self.app.allUsers.count, 1) + XCTAssertEqual(triggered, 2) + } + + func testAsyncOpenCombine() { + let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" + let password = randomString(10) + app.emailPasswordAuth.registerUser(email: email, password: password) + .flatMap { @Sendable in self.app.login(credentials: .emailPassword(email: email, password: password)) } + .flatMap { @Sendable (user: User) in + var config = user.configuration(partitionValue: self.name) + config.objectTypes = [SwiftHugeSyncObject.self] + return Realm.asyncOpen(configuration: config) + } + .tryMap { realm in + try realm.write { + realm.add(SwiftHugeSyncObject.create()) + realm.add(SwiftHugeSyncObject.create()) + } + let progressEx = self.expectation(description: "Should upload") + let token = try XCTUnwrap(realm.syncSession).addProgressNotification(for: .upload, mode: .forCurrentlyOutstandingWork) { + if $0.isTransferComplete { + progressEx.fulfill() + } + } + self.wait(for: [progressEx], timeout: 30.0) + token?.invalidate() + } + .await(self, timeout: 30.0) + + let chainEx = expectation(description: "Should chain realm login => realm async open") + let progressEx = expectation(description: "Should receive progress notification") + app.login(credentials: .anonymous) + .flatMap { @Sendable user in + var config = user.configuration(partitionValue: self.name) + config.objectTypes = [SwiftHugeSyncObject.self] + return Realm.asyncOpen(configuration: config).onProgressNotification { + if $0.isTransferComplete { + progressEx.fulfill() + } + } + } + .expectValue(self, chainEx) { realm in + XCTAssertEqual(realm.objects(SwiftHugeSyncObject.self).count, 2) + }.store(in: &subscriptions) + wait(for: [chainEx, progressEx], timeout: 30.0) + } + + func testAsyncOpenStandaloneCombine() throws { + try autoreleasepool { + let realm = try Realm() + try realm.write { + (0..<10000).forEach { _ in realm.add(SwiftPerson(firstName: "Charlie", lastName: "Bucket")) } + } + } + + Realm.asyncOpen().await(self) { realm in + XCTAssertEqual(realm.objects(SwiftPerson.self).count, 10000) + } + } + + func testDeleteUserCombine() { + let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" + let password = randomString(10) + + let appEx = expectation(description: "App changes triggered") + var triggered = 0 + app.objectWillChange.sink { _ in + triggered += 1 + if triggered == 2 { + appEx.fulfill() + } + }.store(in: &subscriptions) + + app.emailPasswordAuth.registerUser(email: email, password: password) + .flatMap { @Sendable in self.app.login(credentials: .emailPassword(email: email, password: password)) } + .flatMap { @Sendable in $0.delete() } + .await(self) + wait(for: [appEx], timeout: 30.0) + XCTAssertEqual(self.app.allUsers.count, 0) + XCTAssertEqual(triggered, 2) + } + + func testMongoCollectionInsertCombine() throws { + let collection = try setupMongoCollection(for: Dog.self) + let document: Document = ["name": "fido", "breed": "cane corso"] + let document2: Document = ["name": "rex", "breed": "tibetan mastiff"] + + collection.insertOne(document).await(self) + collection.insertMany([document, document2]) + .await(self) { objectIds in + XCTAssertEqual(objectIds.count, 2) + } + collection.find(filter: [:]) + .await(self) { findResult in + XCTAssertEqual(findResult.map({ $0["name"]??.stringValue }), ["fido", "fido", "rex"]) + } + } + + func testMongoCollectionFindCombine() throws { + let collection = try setupMongoCollection(for: Dog.self) + let document: Document = ["name": "fido", "breed": "cane corso"] + let document2: Document = ["name": "rex", "breed": "tibetan mastiff"] + let document3: Document = ["name": "rex", "breed": "tibetan mastiff", "coat": ["fawn", "brown", "white"]] + let findOptions = FindOptions(1, nil) + + collection.find(filter: [:], options: findOptions) + .await(self) { findResult in + XCTAssertEqual(findResult.count, 0) + } + collection.insertMany([document, document2, document3]).await(self) + collection.find(filter: [:]) + .await(self) { findResult in + XCTAssertEqual(findResult.map({ $0["name"]??.stringValue }), ["fido", "rex", "rex"]) + } + collection.find(filter: [:], options: findOptions) + .await(self) { findResult in + XCTAssertEqual(findResult.count, 1) + XCTAssertEqual(findResult[0]["name"]??.stringValue, "fido") + } + collection.find(filter: document3, options: findOptions) + .await(self) { findResult in + XCTAssertEqual(findResult.count, 1) + } + collection.findOneDocument(filter: document).await(self) + + collection.findOneDocument(filter: document, options: findOptions).await(self) + } + + func testMongoCollectionCountAndAggregateCombine() throws { + let collection = try setupMongoCollection(for: Dog.self) + let document: Document = ["name": "fido", "breed": "cane corso"] + + collection.insertMany([document]).await(self) + collection.aggregate(pipeline: [["$match": ["name": "fido"]], ["$group": ["_id": "$name"]]]) + .await(self) + collection.count(filter: document).await(self) { count in + XCTAssertEqual(count, 1) + } + collection.count(filter: document, limit: 1).await(self) { count in + XCTAssertEqual(count, 1) + } + } + + func testMongoCollectionDeleteOneCombine() throws { + let collection = try setupMongoCollection(for: Dog.self) + let document: Document = ["name": "fido", "breed": "cane corso"] + let document2: Document = ["name": "rex", "breed": "cane corso"] + + collection.deleteOneDocument(filter: document).await(self) { count in + XCTAssertEqual(count, 0) + } + collection.insertMany([document, document2]).await(self) + collection.deleteOneDocument(filter: document).await(self) { count in + XCTAssertEqual(count, 1) + } + } + + func testMongoCollectionDeleteManyCombine() throws { + let collection = try setupMongoCollection(for: Dog.self) + let document: Document = ["name": "fido", "breed": "cane corso"] + let document2: Document = ["name": "rex", "breed": "cane corso"] + + collection.deleteManyDocuments(filter: document).await(self) { count in + XCTAssertEqual(count, 0) + } + collection.insertMany([document, document2]).await(self) + collection.deleteManyDocuments(filter: ["breed": "cane corso"]).await(self) { count in + XCTAssertEqual(count, 2) + } + } + + func testMongoCollectionUpdateOneCombine() throws { + let collection = try setupMongoCollection(for: Dog.self) + let document: Document = ["name": "fido", "breed": "cane corso"] + let document2: Document = ["name": "rex", "breed": "cane corso"] + let document3: Document = ["name": "john", "breed": "cane corso"] + let document4: Document = ["name": "ted", "breed": "bullmastiff"] + let document5: Document = ["name": "bill", "breed": "great dane"] + + collection.insertMany([document, document2, document3, document4]).await(self) + collection.updateOneDocument(filter: document, update: document2).await(self) { updateResult in + XCTAssertEqual(updateResult.matchedCount, 1) + XCTAssertEqual(updateResult.modifiedCount, 1) + XCTAssertNil(updateResult.documentId) + } + + collection.updateOneDocument(filter: document5, update: document2, upsert: true).await(self) { updateResult in + XCTAssertEqual(updateResult.matchedCount, 0) + XCTAssertEqual(updateResult.modifiedCount, 0) + XCTAssertNotNil(updateResult.documentId) + } + } + + func testMongoCollectionUpdateManyCombine() throws { + let collection = try setupMongoCollection(for: Dog.self) + let document: Document = ["name": "fido", "breed": "cane corso"] + let document2: Document = ["name": "rex", "breed": "cane corso"] + let document3: Document = ["name": "john", "breed": "cane corso"] + let document4: Document = ["name": "ted", "breed": "bullmastiff"] + let document5: Document = ["name": "bill", "breed": "great dane"] + + collection.insertMany([document, document2, document3, document4]).await(self) + collection.updateManyDocuments(filter: document, update: document2).await(self) { updateResult in + XCTAssertEqual(updateResult.matchedCount, 1) + XCTAssertEqual(updateResult.modifiedCount, 1) + XCTAssertNil(updateResult.documentId) + } + collection.updateManyDocuments(filter: document5, update: document2, upsert: true).await(self) { updateResult in + XCTAssertEqual(updateResult.matchedCount, 0) + XCTAssertEqual(updateResult.modifiedCount, 0) + XCTAssertNotNil(updateResult.documentId) + } + } + + func testMongoCollectionFindAndUpdateCombine() throws { + let collection = try setupMongoCollection(for: Dog.self) + let document: Document = ["name": "fido", "breed": "cane corso"] + let document2: Document = ["name": "rex", "breed": "cane corso"] + let document3: Document = ["name": "john", "breed": "cane corso"] + + collection.findOneAndUpdate(filter: document, update: document2).await(self) + + let options1 = FindOneAndModifyOptions(["name": 1], [["_id": 1]], true, true) + collection.findOneAndUpdate(filter: document2, update: document3, options: options1).await(self) { updateResult in + guard let updateResult = updateResult else { + XCTFail("Should find") + return + } + XCTAssertEqual(updateResult["name"]??.stringValue, "john") + } + + let options2 = FindOneAndModifyOptions(["name": 1], [["_id": 1]], true, true) + collection.findOneAndUpdate(filter: document, update: document2, options: options2).await(self) { updateResult in + guard let updateResult = updateResult else { + XCTFail("Should find") + return + } + XCTAssertEqual(updateResult["name"]??.stringValue, "rex") + } + } + + func testMongoCollectionFindAndReplaceCombine() throws { + let collection = try setupMongoCollection(for: Dog.self) + let document: Document = ["name": "fido", "breed": "cane corso"] + let document2: Document = ["name": "rex", "breed": "cane corso"] + let document3: Document = ["name": "john", "breed": "cane corso"] + + collection.findOneAndReplace(filter: document, replacement: document2).await(self) { updateResult in + XCTAssertNil(updateResult) + } + + let options1 = FindOneAndModifyOptions(["name": 1], [["_id": 1]], true, true) + collection.findOneAndReplace(filter: document2, replacement: document3, options: options1).await(self) { updateResult in + guard let updateResult = updateResult else { + XCTFail("Should find") + return + } + XCTAssertEqual(updateResult["name"]??.stringValue, "john") + } + + let options2 = FindOneAndModifyOptions(["name": 1], [["_id": 1]], true, false) + collection.findOneAndReplace(filter: document, replacement: document2, options: options2).await(self) { updateResult in + XCTAssertNil(updateResult) + } + } + + func testMongoCollectionFindAndDeleteCombine() throws { + let collection = try setupMongoCollection(for: Dog.self) + let document: Document = ["name": "fido", "breed": "cane corso"] + collection.insertMany([document]).await(self) + + collection.findOneAndDelete(filter: document).await(self) { updateResult in + XCTAssertNotNil(updateResult) + } + collection.findOneAndDelete(filter: document).await(self) { updateResult in + XCTAssertNil(updateResult) + } + + collection.insertMany([document]).await(self) + let options1 = FindOneAndModifyOptions(["name": 1], [["_id": 1]], false, false) + collection.findOneAndDelete(filter: document, options: options1).await(self) { deleteResult in + XCTAssertNotNil(deleteResult) + } + collection.findOneAndDelete(filter: document, options: options1).await(self) { deleteResult in + XCTAssertNil(deleteResult) + } + + collection.insertMany([document]).await(self) + let options2 = FindOneAndModifyOptions(["name": 1], [["_id": 1]]) + collection.findOneAndDelete(filter: document, options: options2).await(self) { deleteResult in + XCTAssertNotNil(deleteResult) + } + collection.findOneAndDelete(filter: document, options: options2).await(self) { deleteResult in + XCTAssertNil(deleteResult) + } + + collection.insertMany([document]).await(self) + collection.find(filter: [:]).await(self) { updateResult in + XCTAssertEqual(updateResult.count, 1) + } + } + + func testCallFunctionCombine() { + 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 credentials = Credentials.emailPassword(email: email, password: password) + app.login(credentials: credentials).await(self) { user in + XCTAssertNotNil(user) + } + + app.currentUser?.functions.sum([1, 2, 3, 4, 5]).await(self) { bson in + guard case let .int32(sum) = bson else { + XCTFail("Should be int32") + return + } + XCTAssertEqual(sum, 15) + } + + app.currentUser?.functions.updateUserData([["favourite_colour": "green", "apples": 10]]).await(self) { bson in + guard case let .bool(upd) = bson else { + XCTFail("Should be bool") + return + } + XCTAssertTrue(upd) + } + } + + func testAPIKeyAuthCombine() { + 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 user = app.login(credentials: Credentials.emailPassword(email: email, password: password)).await(self) + + let apiKey = user.apiKeysAuth.createAPIKey(named: "my-api-key").await(self) + user.apiKeysAuth.fetchAPIKey(apiKey.objectId).await(self) + user.apiKeysAuth.fetchAPIKeys().await(self) { userApiKeys in + XCTAssertEqual(userApiKeys.count, 1) + } + + user.apiKeysAuth.disableAPIKey(apiKey.objectId).await(self) + user.apiKeysAuth.enableAPIKey(apiKey.objectId).await(self) + user.apiKeysAuth.deleteAPIKey(apiKey.objectId).await(self) + } + + func testPushRegistrationCombine() { + let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" + let password = randomString(10) + + app.emailPasswordAuth.registerUser(email: email, password: password).await(self) + app.login(credentials: Credentials.emailPassword(email: email, password: password)).await(self) + + let client = app.pushClient(serviceName: "gcm") + client.registerDevice(token: "some-token", user: app.currentUser!).await(self) + client.deregisterDevice(user: app.currentUser!).await(self) + } +} + +@available(macOS 13, *) +class CombineFlexibleSyncTests: SwiftSyncTestCase { + override var objectTypes: [ObjectBase.Type] { + [SwiftPerson.self, SwiftTypesSyncObject.self] + } + + override func configuration(user: User) -> Realm.Configuration { + user.flexibleSyncConfiguration() + } + + override func createApp() throws -> String { + try createFlexibleSyncApp() + } + + var cancellables: Set = [] + override func tearDown() { + cancellables.forEach { $0.cancel() } + cancellables = [] + super.tearDown() + } + + func testFlexibleSyncCombineWrite() throws { + try write { realm in + for i in 1...25 { + let person = SwiftPerson(firstName: "\(self.name)", + lastName: "lastname_\(i)", + age: i) + realm.add(person) + } + } + + let realm = try openRealm() + checkCount(expected: 0, realm, SwiftPerson.self) + + let subscriptions = realm.subscriptions + XCTAssertEqual(subscriptions.count, 0) + + let ex = expectation(description: "state change complete") + subscriptions.updateSubscriptions { + subscriptions.append(QuerySubscription(name: "person_age_10") { + $0.age > 10 && $0.firstName == "\(self.name)" + }) + }.sink(receiveCompletion: { @Sendable _ in }, + receiveValue: { @Sendable _ in ex.fulfill() } + ).store(in: &cancellables) + + waitForExpectations(timeout: 20.0, handler: nil) + + waitForDownloads(for: realm) + checkCount(expected: 15, realm, SwiftPerson.self) + } + + func testFlexibleSyncCombineWriteFails() throws { + let realm = try openRealm() + checkCount(expected: 0, realm, SwiftPerson.self) + + let subscriptions = realm.subscriptions + XCTAssertEqual(subscriptions.count, 0) + + let ex = expectation(description: "state change error") + subscriptions.updateSubscriptions { + subscriptions.append(QuerySubscription(name: "swiftObject_longCol") { + $0.longCol == Int64(1) + }) + } + .sink(receiveCompletion: { result in + if case .failure(let error as Realm.Error) = result { + XCTAssertEqual(error.code, .subscriptionFailed) + guard case .error = subscriptions.state else { + return XCTFail("Adding a query for a not queryable field should change the subscription set state to error") + } + } else { + XCTFail("Expected an error but got \(result)") + } + ex.fulfill() + }, receiveValue: { _ in }) + .store(in: &cancellables) + + waitForExpectations(timeout: 20.0, handler: nil) + + waitForDownloads(for: realm) + checkCount(expected: 0, realm, SwiftPerson.self) + } +} + +#endif // os(macOS) diff --git a/Realm/ObjectServerTests/EventTests.swift b/Realm/ObjectServerTests/EventTests.swift index 058fdb4393..00ec763ef0 100644 --- a/Realm/ObjectServerTests/EventTests.swift +++ b/Realm/ObjectServerTests/EventTests.swift @@ -60,16 +60,15 @@ class AuditEvent: Object { var parsedData: NSDictionary? } +@available(macOS 13, *) class SwiftEventTests: SwiftSyncTestCase { var user: User! var collection: MongoCollection! var start: Date! override func setUp() { - user = try! logInUser(for: basicCredentials()) - let mongoClient = user.mongoClient("mongodb1") - let database = mongoClient.database(named: "test_data") - collection = database.collection(withName: "AuditEvent") + user = createUser() + collection = user.collection(for: AuditEvent.self, app: app) _ = collection.deleteManyDocuments(filter: [:]).await(self) // The server truncates date values to lower precision than we support, @@ -78,7 +77,7 @@ class SwiftEventTests: SwiftSyncTestCase { } override func tearDown() { - if let user = self.user { + if let user { while user.allSessions.count > 0 { RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.1)) } @@ -87,20 +86,14 @@ class SwiftEventTests: SwiftSyncTestCase { super.tearDown() } - func config(partition: String = UUID().uuidString) -> Realm.Configuration { - var config = user.configuration(partitionValue: partition) + override func configuration(user: User) -> Realm.Configuration { + var config = user.configuration(partitionValue: name) config.eventConfiguration = EventConfiguration() - config.objectTypes = [SwiftPerson.self, SwiftCustomEventRepresentation.self] return config } - func openRealm(_ configuration: Realm.Configuration? = nil) throws -> Realm { - let realm = try openRealm(configuration: configuration ?? self.config()) - // For some reason the server deletes and recreates our objects, which - // breaks our accessor objects. Work around this by just not syncing the - // main Realm after opening it. - realm.syncSession?.pause() - return realm + override var objectTypes: [ObjectBase.Type] { + [AuditEvent.self, SwiftPerson.self, SwiftCustomEventRepresentation.self, LinkToSwiftPerson.self] } func scope(_ events: Events, _ name: String, body: () throws -> T) rethrows -> T { @@ -113,10 +106,8 @@ class SwiftEventTests: SwiftSyncTestCase { } func getEvents(expectedCount: Int) -> [AuditEvent] { - let waitStart = Date() - while collection.count(filter: [:]).await(self) < expectedCount && waitStart.timeIntervalSinceNow > -600.0 { - sleep(5) - } + waitForCollectionCount(collection, expectedCount) + let docs = collection.find(filter: [:]).await(self) XCTAssertEqual(docs.count, expectedCount) return docs.map { doc in @@ -219,7 +210,7 @@ class SwiftEventTests: SwiftSyncTestCase { } func testBasicWithAsyncOpen() throws { - let realm = Realm.asyncOpen(configuration: self.config()).await(self) + let realm = Realm.asyncOpen(configuration: try configuration()).await(self) let events = try XCTUnwrap(realm.events) let personJson: NSDictionary = try scope(events, "create object") { @@ -267,9 +258,7 @@ class SwiftEventTests: SwiftSyncTestCase { } func testReadEvents() throws { - var config = self.config() - config.objectTypes = [SwiftPerson.self, LinkToSwiftPerson.self] - let realm = try openRealm(config) + let realm = try openRealm() let events = realm.events! let a = SwiftPerson(firstName: "A", lastName: "B") @@ -339,9 +328,7 @@ class SwiftEventTests: SwiftSyncTestCase { } func testLinkTracking() throws { - var config = self.config() - config.objectTypes = [SwiftPerson.self, LinkToSwiftPerson.self] - let realm = try openRealm(config) + let realm = try openRealm() let events = realm.events! let a = SwiftPerson(firstName: "A", lastName: "B") @@ -445,7 +432,7 @@ class SwiftEventTests: SwiftSyncTestCase { } func testMetadata() throws { - let realm = try Realm(configuration: self.config()) + let realm = try openRealm() let events = realm.events! func writeEvent(_ name: String) throws { @@ -474,7 +461,7 @@ class SwiftEventTests: SwiftSyncTestCase { func testCustomLogger() throws { let ex = expectation(description: "saw message with scope name") ex.assertForOverFulfill = false - var config = self.config() + var config = try configuration() config.eventConfiguration!.logger = { _, message in // Mostly just verify that the user-provided logger is wired up // correctly and not that the log messages are sensible @@ -489,7 +476,7 @@ class SwiftEventTests: SwiftSyncTestCase { } func testCustomEvent() throws { - let realm = try Realm(configuration: self.config()) + let realm = try openRealm() let events = realm.events! events.recordEvent(activity: "no event or data") @@ -547,17 +534,20 @@ class SwiftEventTests: SwiftSyncTestCase { } func testErrorHandler() throws { - var config = self.config() + var config = try configuration() let blockCalled = Locked(false) let ex = expectation(description: "Error callback called") - config.eventConfiguration?.errorHandler = { error in - assertSyncError(error, .clientUserError, "Unable to refresh the user access token: signature is invalid") + var eventConfiguration = config.eventConfiguration! + eventConfiguration.errorHandler = { error in + assertSyncError(error, .clientInternalError, + "Invalid schema change (UPLOAD): non-breaking schema change: adding \"String\" column at field \"invalid metadata field\" in schema \"AuditEvent\", schema changes from clients are restricted when developer mode is disabled") blockCalled.value = true ex.fulfill() } + eventConfiguration.metadata = ["invalid metadata field": "value"] + config.eventConfiguration = eventConfiguration let realm = try openRealm(configuration: config) let events = realm.events! - setInvalidTokensFor(user) // Recording the audit event should succeed, but we should get a sync // error when trying to actually upload it due to the user having diff --git a/Realm/ObjectServerTests/RLMAsymmetricSyncServerTests.mm b/Realm/ObjectServerTests/RLMAsymmetricSyncServerTests.mm index 4ccc61a6bb..74afda786a 100644 --- a/Realm/ObjectServerTests/RLMAsymmetricSyncServerTests.mm +++ b/Realm/ObjectServerTests/RLMAsymmetricSyncServerTests.mm @@ -113,66 +113,32 @@ + (bool)_realmIgnoreClass { @interface RLMAsymmetricSyncServerTests : RLMSyncTestCase @end -@implementation RLMAsymmetricSyncServerTests { - NSString *_asymmetricSyncAppId; - RLMApp *_asymmetricSyncApp; -} +@implementation RLMAsymmetricSyncServerTests #pragma mark Asymmetric Sync App -- (NSString *)asymmetricSyncAppId { - if (!_asymmetricSyncAppId) { - static NSString *s_appId; - if (s_appId) { - _asymmetricSyncAppId = s_appId; - } - else { - NSError *error; - NSArray *objectsSchema = @[[RLMObjectSchema schemaForObjectClass:PersonAsymmetric.class]]; - _asymmetricSyncAppId = [RealmServer.shared createAppForAsymmetricSchema:objectsSchema error:&error]; - if (error) { - NSLog(@"Failed to create asymmetric app: %@", error); - abort(); - } - s_appId = _asymmetricSyncAppId; - } - } - return _asymmetricSyncAppId; +- (NSString *)createAppWithError:(NSError **)error { + return [RealmServer.shared createAppWithFields:@[] + types:@[PersonAsymmetric.self] + persistent:true + error:error]; } -- (RLMApp *)asymmetricSyncApp { - if (!_asymmetricSyncApp) { - _asymmetricSyncApp = [self appWithId:self.asymmetricSyncAppId]; - RLMSyncManager *syncManager = self.asymmetricSyncApp.syncManager; - RLMLogger.defaultLogger.level = RLMLogLevelOff; - syncManager.userAgent = self.name; - } - return _asymmetricSyncApp; +- (NSArray *)defaultObjectTypes { + return @[PersonAsymmetric.self]; } -- (RLMUser *)userForSelector:(SEL)testSel { - return [self logInUserForCredentials:[self basicCredentialsWithName:NSStringFromSelector(testSel) - register:YES - app:self.asymmetricSyncApp] - app:self.asymmetricSyncApp]; +- (RLMRealmConfiguration *)configurationForUser:(RLMUser *)user { + return [user flexibleSyncConfiguration]; } - (void)tearDown { - RLMUser *user = [self logInUserForCredentials:[RLMCredentials anonymousCredentials] - app:self.asymmetricSyncApp]; - RLMMongoClient *client = [user mongoClientWithServiceName:@"mongodb1"]; - RLMMongoDatabase *database = [client databaseWithName:@"test_data"]; - RLMMongoCollection *collection = [database collectionWithName:@"PersonAsymmetric"]; - [self cleanupRemoteDocuments:collection]; + [self cleanupRemoteDocuments:[self.anonymousUser collectionForType:PersonAsymmetric.class app:self.app]]; [super tearDown]; } - (void)checkCountInMongo:(unsigned long)expectedCount { - RLMUser *user = [self logInUserForCredentials:[RLMCredentials anonymousCredentials] - app:self.asymmetricSyncApp]; - RLMMongoClient *client = [user mongoClientWithServiceName:@"mongodb1"]; - RLMMongoDatabase *database = [client databaseWithName:@"test_data"]; - RLMMongoCollection *collection = [database collectionWithName:@"PersonAsymmetric"]; + RLMMongoCollection *collection = [self.anonymousUser collectionForType:PersonAsymmetric.class app:self.app]; __block unsigned long count = 0; NSDate *waitStart = [NSDate date]; @@ -193,15 +159,12 @@ - (void)checkCountInMongo:(unsigned long)expectedCount { } - (void)testAsymmetricObjectSchema { - RLMUser *user = [self userForSelector:_cmd]; - RLMRealmConfiguration *configuration = [user flexibleSyncConfiguration]; - configuration.objectClasses = @[PersonAsymmetric.self]; - RLMRealm *realm = [RLMRealm realmWithConfiguration:configuration error:nil]; + RLMRealm *realm = [self openRealm]; XCTAssertTrue(realm.schema.objectSchema[0].isAsymmetric); } - (void)testUnsupportedAsymmetricLinkAsymmetricThrowsError { - RLMUser *user = [self userForSelector:_cmd]; + RLMUser *user = [self createUser]; RLMRealmConfiguration *configuration = [user flexibleSyncConfiguration]; configuration.objectClasses = @[UnsupportedLinkAsymmetric.self, PersonAsymmetric.self]; NSError *error; @@ -212,7 +175,7 @@ - (void)testUnsupportedAsymmetricLinkAsymmetricThrowsError { } - (void)testUnsupportedObjectLinksAsymmetricThrowsError { - RLMUser *user = [self userForSelector:_cmd]; + RLMUser *user = [self createUser]; RLMRealmConfiguration *configuration = [user flexibleSyncConfiguration]; configuration.objectClasses = @[UnsupportedObjectLinkAsymmetric.self, PersonAsymmetric.self]; NSError *error; @@ -234,8 +197,8 @@ - (void)testOpenLocalRealmWithAsymmetricObjectError { } - (void)testOpenPBSConfigurationWithAsymmetricObjectError { - RLMUser *user = [self userForTest:_cmd]; - RLMRealmConfiguration *configuration = [user configurationWithPartitionValue:NSStringFromSelector(_cmd)]; + RLMUser *user = [self createUser]; + RLMRealmConfiguration *configuration = [user configurationWithPartitionValue:self.name]; configuration.objectClasses = @[PersonAsymmetric.self]; NSError *error; RLMRealm *realm = [RLMRealm realmWithConfiguration:configuration error:&error]; @@ -245,16 +208,15 @@ - (void)testOpenPBSConfigurationWithAsymmetricObjectError { } - (void)testCreateAsymmetricObjects { - RLMUser *user = [self userForSelector:_cmd]; - RLMRealmConfiguration *configuration = [user flexibleSyncConfiguration]; - configuration.objectClasses = @[PersonAsymmetric.self]; - RLMRealm *realm = [RLMRealm realmWithConfiguration:configuration error:nil]; - XCTAssertNotNil(realm); + RLMRealm *realm = [self openRealm]; [realm beginWriteTransaction]; for (int i = 1; i <= 12; ++i) { RLMObjectId *oid = [RLMObjectId objectId]; - PersonAsymmetric *person = [PersonAsymmetric createInRealm:realm withValue:@[oid, [NSString stringWithFormat:@"firstname_%d", i], [NSString stringWithFormat:@"lastname_%d", i]]]; + PersonAsymmetric *person = [PersonAsymmetric createInRealm:realm withValue:@[ + oid, [NSString stringWithFormat:@"firstname_%d", i], + [NSString stringWithFormat:@"lastname_%d", i] + ]]; XCTAssertNil(person); } [realm commitWriteTransaction]; @@ -263,11 +225,7 @@ - (void)testCreateAsymmetricObjects { } - (void)testCreateAsymmetricSameObjectNotDuplicates { - RLMUser *user = [self userForSelector:_cmd]; - RLMRealmConfiguration *configuration = [user flexibleSyncConfiguration]; - configuration.objectClasses = @[PersonAsymmetric.self]; - RLMRealm *realm = [RLMRealm realmWithConfiguration:configuration error:nil]; - XCTAssertNotNil(realm); + RLMRealm *realm = [self openRealm]; RLMObjectId *oid = [RLMObjectId objectId]; [realm beginWriteTransaction]; diff --git a/Realm/ObjectServerTests/RLMCollectionSyncTests.mm b/Realm/ObjectServerTests/RLMCollectionSyncTests.mm index 82e7444c49..ba16655064 100644 --- a/Realm/ObjectServerTests/RLMCollectionSyncTests.mm +++ b/Realm/ObjectServerTests/RLMCollectionSyncTests.mm @@ -36,20 +36,17 @@ @interface RLMSetObjectServerTests : RLMSyncTestCase @end @implementation RLMSetObjectServerTests +- (NSArray *)defaultObjectTypes { + return @[RLMSetSyncObject.self, Person.self]; +} - (void)roundTripWithPropertyGetter:(RLMSet *(^)(id))propertyGetter values:(NSArray *)values otherPropertyGetter:(RLMSet *(^)(id))otherPropertyGetter otherValues:(NSArray *)otherValues - isObject:(BOOL)isObject - callerName:(NSString *)callerName { - RLMUser *readUser = [self logInUserForCredentials:[self basicCredentialsWithName:callerName - register:YES]]; - RLMUser *writeUser = [self logInUserForCredentials:[self basicCredentialsWithName:[callerName stringByAppendingString:@"Writer"] - register:YES]]; - - RLMRealm *readRealm = [self openRealmForPartitionValue:callerName user:readUser]; - RLMRealm *writeRealm = [self openRealmForPartitionValue:callerName user:writeUser]; + isObject:(BOOL)isObject { + RLMRealm *readRealm = [self openRealm]; + RLMRealm *writeRealm = [self openRealm]; auto write = [&](auto fn) { [writeRealm transactionWithBlock:^{ fn(); @@ -106,8 +103,7 @@ - (void)testIntSet { values:@[@123, @234, @345] otherPropertyGetter:^RLMSet *(RLMSetSyncObject *obj) { return obj.otherIntSet; } otherValues:@[@345, @567, @789] - isObject:NO - callerName:NSStringFromSelector(_cmd)]; + isObject:NO]; } - (void)testStringSet { @@ -115,8 +111,7 @@ - (void)testStringSet { values:@[@"Who", @"What", @"When"] otherPropertyGetter:^RLMSet *(RLMSetSyncObject *obj) { return obj.otherStringSet; } otherValues:@[@"When", @"Strings", @"Collide"] - isObject:NO - callerName:NSStringFromSelector(_cmd)]; + isObject:NO]; } - (void)testDataSet { @@ -131,8 +126,7 @@ - (void)testDataSet { values:@[duplicateData, createData(1024U), createData(1024U)] otherPropertyGetter:^RLMSet *(RLMSetSyncObject *obj) { return obj.otherDataSet; } otherValues:@[duplicateData, createData(1024U), createData(1024U)] - isObject:NO - callerName:NSStringFromSelector(_cmd)]; + isObject:NO]; } - (void)testDoubleSet { @@ -140,8 +134,7 @@ - (void)testDoubleSet { values:@[@123.456, @234.456, @345.567] otherPropertyGetter:^RLMSet *(RLMSetSyncObject *obj) { return obj.otherDoubleSet; } otherValues:@[@123.456, @434.456, @545.567] - isObject:NO - callerName:NSStringFromSelector(_cmd)]; + isObject:NO]; } - (void)testObjectIdSet { @@ -153,8 +146,7 @@ - (void)testObjectIdSet { otherValues:@[[[RLMObjectId alloc] initWithString:@"6058f12b957ba06156586a7c" error:nil], [[RLMObjectId alloc] initWithString:@"6058f12682b2fbb1f334ef1e" error:nil], [[RLMObjectId alloc] initWithString:@"6058f12d42e5a393e67538df" error:nil]] - isObject:NO - callerName:NSStringFromSelector(_cmd)]; + isObject:NO]; } - (void)testDecimalSet { @@ -166,8 +158,7 @@ - (void)testDecimalSet { otherValues:@[[[RLMDecimal128 alloc] initWithNumber:@123.456], [[RLMDecimal128 alloc] initWithNumber:@423.456], [[RLMDecimal128 alloc] initWithNumber:@523.456]] - isObject:NO - callerName:NSStringFromSelector(_cmd)]; + isObject:NO]; } - (void)testUUIDSet { @@ -179,8 +170,7 @@ - (void)testUUIDSet { otherValues:@[[[NSUUID alloc] initWithUUIDString:@"6b28ec45-b29a-4b0a-bd6a-343c7f6d90fd"], [[NSUUID alloc] initWithUUIDString:@"6b28ec45-b29a-4b0a-bd6a-343c7f6d90ae"], [[NSUUID alloc] initWithUUIDString:@"6b28ec45-b29a-4b0a-bd6a-343c7f6d90bf"]] - isObject:NO - callerName:NSStringFromSelector(_cmd)]; + isObject:NO]; } - (void)testObjectSet { @@ -188,8 +178,7 @@ - (void)testObjectSet { values:@[[Person john], [Person paul], [Person ringo]] otherPropertyGetter:^RLMSet *(RLMSetSyncObject *obj) { return obj.otherObjectSet; } otherValues:@[[Person john], [Person paul], [Person ringo]] - isObject:YES - callerName:NSStringFromSelector(_cmd)]; + isObject:YES]; } - (void)testAnySet { @@ -199,8 +188,7 @@ - (void)testAnySet { otherValues:@[[[NSUUID alloc] initWithUUIDString:@"6b28ec45-b29a-4b0a-bd6a-343c7f6d90fd"], @123, [[RLMObjectId alloc] initWithString:@"6058f12682b2fbb1f334ef1d" error:nil]] - isObject:NO - callerName:NSStringFromSelector(_cmd)]; + isObject:NO]; } @end @@ -211,18 +199,15 @@ @interface RLMArrayObjectServerTests : RLMSyncTestCase @end @implementation RLMArrayObjectServerTests +- (NSArray *)defaultObjectTypes { + return @[RLMArraySyncObject.self, Person.self]; +} - (void)roundTripWithPropertyGetter:(RLMArray *(^)(id))propertyGetter values:(NSArray *)values - isObject:(BOOL)isObject - callerName:(NSString *)callerName { - RLMUser *readUser = [self logInUserForCredentials:[self basicCredentialsWithName:callerName - register:YES]]; - RLMUser *writeUser = [self logInUserForCredentials:[self basicCredentialsWithName:[callerName stringByAppendingString:@"Writer"] - register:YES]]; - - RLMRealm *readRealm = [self openRealmForPartitionValue:callerName user:readUser]; - RLMRealm *writeRealm = [self openRealmForPartitionValue:callerName user:writeUser]; + isObject:(BOOL)isObject { + RLMRealm *readRealm = [self openRealm]; + RLMRealm *writeRealm = [self openRealm]; auto write = [&](auto fn) { [writeRealm transactionWithBlock:^{ fn(); @@ -269,22 +254,19 @@ - (void)roundTripWithPropertyGetter:(RLMArray *(^)(id))propertyGetter - (void)testIntArray { [self roundTripWithPropertyGetter:^RLMArray *(RLMArraySyncObject *obj) { return obj.intArray; } values:@[@123, @234, @345] - isObject:NO - callerName:NSStringFromSelector(_cmd)]; + isObject:NO]; } - (void)testBoolArray { [self roundTripWithPropertyGetter:^RLMArray *(RLMArraySyncObject *obj) { return obj.boolArray; } values:@[@YES, @NO, @YES] - isObject:NO - callerName:NSStringFromSelector(_cmd)]; + isObject:NO]; } - (void)testStringArray { [self roundTripWithPropertyGetter:^RLMArray *(RLMArraySyncObject *obj) { return obj.stringArray; } values:@[@"Hello...", @"It's", @"Me"] - isObject:NO - callerName:NSStringFromSelector(_cmd)]; + isObject:NO]; } - (void)testDataArray { @@ -295,15 +277,13 @@ - (void)testDataArray { length:1], [NSData dataWithBytes:(unsigned char[]){0x0c} length:1]] - isObject:NO - callerName:NSStringFromSelector(_cmd)]; + isObject:NO]; } - (void)testDoubleArray { [self roundTripWithPropertyGetter:^RLMArray *(RLMArraySyncObject *obj) { return obj.doubleArray; } values:@[@123.456, @789.456, @987.344] - isObject:NO - callerName:NSStringFromSelector(_cmd)]; + isObject:NO]; } - (void)testObjectIdArray { @@ -311,8 +291,7 @@ - (void)testObjectIdArray { values:@[[[RLMObjectId alloc] initWithString:@"6058f12b957ba06156586a7c" error:nil], [[RLMObjectId alloc] initWithString:@"6058f12682b2fbb1f334ef1d" error:nil], [[RLMObjectId alloc] initWithString:@"6058f12d42e5a393e67538d0" error:nil]] - isObject:NO - callerName:NSStringFromSelector(_cmd)]; + isObject:NO]; } - (void)testDecimalArray { @@ -320,8 +299,7 @@ - (void)testDecimalArray { values:@[[[RLMDecimal128 alloc] initWithNumber:@123.456], [[RLMDecimal128 alloc] initWithNumber:@456.456], [[RLMDecimal128 alloc] initWithNumber:@789.456]] - isObject:NO - callerName:NSStringFromSelector(_cmd)]; + isObject:NO]; } - (void)testUUIDArray { @@ -329,22 +307,19 @@ - (void)testUUIDArray { values:@[[[NSUUID alloc] initWithUUIDString:@"6b28ec45-b29a-4b0a-bd6a-343c7f6d90fd"], [[NSUUID alloc] initWithUUIDString:@"6b28ec45-b29a-4b0a-bd6a-343c7f6d90fe"], [[NSUUID alloc] initWithUUIDString:@"6b28ec45-b29a-4b0a-bd6a-343c7f6d90ff"]] - isObject:NO - callerName:NSStringFromSelector(_cmd)]; + isObject:NO]; } - (void)testObjectArray { [self roundTripWithPropertyGetter:^RLMArray *(RLMArraySyncObject *obj) { return obj.objectArray; } values:@[[Person john], [Person paul], [Person ringo]] - isObject:YES - callerName:NSStringFromSelector(_cmd)]; + isObject:YES]; } - (void)testAnyArray { [self roundTripWithPropertyGetter:^RLMArray *(RLMArraySyncObject *obj) { return obj.anyArray; } values:@[@1234, @"I'm a String", NSNull.null] - isObject:NO - callerName:NSStringFromSelector(_cmd)]; + isObject:NO]; } @end @@ -355,18 +330,15 @@ @interface RLMDictionaryObjectServerTests : RLMSyncTestCase @end @implementation RLMDictionaryObjectServerTests +- (NSArray *)defaultObjectTypes { + return @[RLMDictionarySyncObject.self, Person.self]; +} - (void)roundTripWithPropertyGetter:(RLMDictionary *(^)(id))propertyGetter values:(NSDictionary *)values - isObject:(BOOL)isObject - callerName:(NSString *)callerName { - RLMUser *readUser = [self logInUserForCredentials:[self basicCredentialsWithName:callerName - register:YES]]; - RLMUser *writeUser = [self logInUserForCredentials:[self basicCredentialsWithName:[callerName stringByAppendingString:@"Writer"] - register:YES]]; - - RLMRealm *readRealm = [self openRealmForPartitionValue:callerName user:readUser]; - RLMRealm *writeRealm = [self openRealmForPartitionValue:callerName user:writeUser]; + isObject:(BOOL)isObject { + RLMRealm *readRealm = [self openRealm]; + RLMRealm *writeRealm = [self openRealm]; auto write = [&](auto fn) { [writeRealm transactionWithBlock:^{ fn(); @@ -423,14 +395,12 @@ - (void)roundTripWithPropertyGetter:(RLMDictionary *(^)(id))propertyGetter - (void)testIntDictionary { [self roundTripWithPropertyGetter:^RLMDictionary *(RLMDictionarySyncObject *obj) { return obj.intDictionary; } values:@{@"0": @123, @"1": @234, @"2": @345, @"3": @567, @"4": @789} - isObject:NO - callerName:NSStringFromSelector(_cmd)]; + isObject:NO]; } - (void)testStringDictionary { [self roundTripWithPropertyGetter:^RLMDictionary *(RLMDictionarySyncObject *obj) { return obj.stringDictionary; } values:@{@"0": @"Who", @"1": @"What", @"2": @"When", @"3": @"Strings", @"4": @"Collide"} - isObject:NO - callerName:NSStringFromSelector(_cmd)]; + isObject:NO]; } - (void)testDataDictionary { @@ -440,15 +410,13 @@ - (void)testDataDictionary { @"2": [NSData dataWithBytes:(unsigned char[]){0x0c} length:1], @"3": [NSData dataWithBytes:(unsigned char[]){0x0d} length:1], @"4": [NSData dataWithBytes:(unsigned char[]){0x0e} length:1]} - isObject:NO - callerName:NSStringFromSelector(_cmd)]; + isObject:NO]; } - (void)testDoubleDictionary { [self roundTripWithPropertyGetter:^RLMDictionary *(RLMDictionarySyncObject *obj) { return obj.doubleDictionary; } values:@{@"0": @123.456, @"1": @234.456, @"2": @345.567, @"3": @434.456, @"4": @545.567} - isObject:NO - callerName:NSStringFromSelector(_cmd)]; + isObject:NO]; } - (void)testObjectIdDictionary { @@ -458,8 +426,7 @@ - (void)testObjectIdDictionary { @"2": [[RLMObjectId alloc] initWithString:@"6058f12d42e5a393e67538d0" error:nil], @"3": [[RLMObjectId alloc] initWithString:@"6058f12682b2fbb1f334ef1e" error:nil], @"4": [[RLMObjectId alloc] initWithString:@"6058f12d42e5a393e67538df" error:nil]} - isObject:NO - callerName:NSStringFromSelector(_cmd)]; + isObject:NO]; } - (void)testDecimalDictionary { @@ -469,8 +436,7 @@ - (void)testDecimalDictionary { @"2": [[RLMDecimal128 alloc] initWithNumber:@323.456], @"3": [[RLMDecimal128 alloc] initWithNumber:@423.456], @"4": [[RLMDecimal128 alloc] initWithNumber:@523.456]} - isObject:NO - callerName:NSStringFromSelector(_cmd)]; + isObject:NO]; } - (void)testUUIDDictionary { @@ -480,8 +446,7 @@ - (void)testUUIDDictionary { @"2": [[NSUUID alloc] initWithUUIDString:@"6b28ec45-b29a-4b0a-bd6a-343c7f6d90ff"], @"3": [[NSUUID alloc] initWithUUIDString:@"6b28ec45-b29a-4b0a-bd6a-343c7f6d90ae"], @"4": [[NSUUID alloc] initWithUUIDString:@"6b28ec45-b29a-4b0a-bd6a-343c7f6d90bf"]} - isObject:NO - callerName:NSStringFromSelector(_cmd)]; + isObject:NO]; } - (void)testObjectDictionary { @@ -491,8 +456,7 @@ - (void)testObjectDictionary { @"2": [Person ringo], @"3": [Person george], @"4": [Person stuart]} - isObject:YES - callerName:NSStringFromSelector(_cmd)]; + isObject:YES]; } - (void)testAnyDictionary { @@ -502,8 +466,7 @@ - (void)testAnyDictionary { @"2": NSNull.null, @"3": [[NSUUID alloc] initWithUUIDString:@"6b28ec45-b29a-4b0a-bd6a-343c7f6d90fd"], @"4": [[RLMObjectId alloc] initWithString:@"6058f12682b2fbb1f334ef1d" error:nil]} - isObject:NO - callerName:NSStringFromSelector(_cmd)]; + isObject:NO]; } @end diff --git a/Realm/ObjectServerTests/RLMFlexibleSyncServerTests.mm b/Realm/ObjectServerTests/RLMFlexibleSyncServerTests.mm index f6f84f191f..ca2d1fde26 100644 --- a/Realm/ObjectServerTests/RLMFlexibleSyncServerTests.mm +++ b/Realm/ObjectServerTests/RLMFlexibleSyncServerTests.mm @@ -31,671 +31,43 @@ @interface RLMFlexibleSyncTests : RLMSyncTestCase @end @implementation RLMFlexibleSyncTests -- (void)testCreateFlexibleSyncApp { - NSString *appId = [RealmServer.shared createAppWithQueryableFields:@[@"age", @"breed"] - error:nil]; - RLMApp *app = [self appWithId:appId]; - XCTAssertNotNil(app); - [RealmServer.shared deleteApp:appId error:nil]; +- (NSArray *)defaultObjectTypes { + return @[Dog.self, Person.self, UUIDPrimaryKeyObject.self]; } -- (void)testFlexibleSyncOpenRealm { - XCTAssertNotNil([self openFlexibleSyncRealm:_cmd]); +- (NSString *)createAppWithError:(NSError **)error { + return [self createFlexibleSyncAppWithError:error]; } -- (void)testGetSubscriptionsWhenLocalRealm { - RLMRealmConfiguration *configuration = [RLMRealmConfiguration defaultConfiguration]; - configuration.objectClasses = @[Person.self]; - RLMRealm *realm = [RLMRealm realmWithConfiguration:configuration error:nil]; - RLMAssertThrowsWithReason(realm.subscriptions, @"This Realm was not configured with flexible sync"); +- (RLMRealmConfiguration *)configurationForUser:(RLMUser *)user { + return [user flexibleSyncConfiguration]; } -- (void)testGetSubscriptionsWhenPbsRealm { - RLMRealm *realm = [self realmForTest:_cmd]; - RLMAssertThrowsWithReason(realm.subscriptions, @"This Realm was not configured with flexible sync"); -} - -- (void)testFlexibleSyncRealmFilePath { - RLMUser *user = [self logInUserForCredentials:[self basicCredentialsWithName:NSStringFromSelector(_cmd) - register:YES - app:self.flexibleSyncApp] - app:self.flexibleSyncApp]; - RLMRealmConfiguration *config = [user flexibleSyncConfiguration]; - NSString *expected = [NSString stringWithFormat:@"mongodb-realm/%@/%@/flx_sync_default.realm", self.flexibleSyncAppId, user.identifier]; - XCTAssertTrue([config.fileURL.path hasSuffix:expected]); -} - -- (void)testGetSubscriptionsWhenFlexibleSync { - RLMRealm *realm = [self openFlexibleSyncRealm:_cmd]; - RLMSyncSubscriptionSet *subs = realm.subscriptions; - XCTAssertNotNil(subs); - XCTAssertEqual(subs.version, 0UL); - XCTAssertEqual(subs.count, 0UL); -} - -- (void)testGetSubscriptionsWhenSameVersion { - RLMRealm *realm = [self openFlexibleSyncRealm:_cmd]; - RLMSyncSubscriptionSet *subs1 = realm.subscriptions; - RLMSyncSubscriptionSet *subs2 = realm.subscriptions; - XCTAssertEqual(subs1.version, 0UL); - XCTAssertEqual(subs2.version, 0UL); -} - -- (void)testCheckVersionAfterAddSubscription { - RLMRealm *realm = [self openFlexibleSyncRealm:_cmd]; - RLMSyncSubscriptionSet *subs = realm.subscriptions; - XCTAssertNotNil(subs); - XCTAssertEqual(subs.version, 0UL); - XCTAssertEqual(subs.count, 0UL); - - [subs update:^{ - [subs addSubscriptionWithClassName:Person.className - where:@"age > 15"]; - }]; - - XCTAssertEqual(subs.version, 1UL); - XCTAssertEqual(subs.count, 1UL); -} - -- (void)testEmptyWriteSubscriptions { - RLMRealm *realm = [self openFlexibleSyncRealm:_cmd]; - RLMSyncSubscriptionSet *subs = realm.subscriptions; - XCTAssertNotNil(subs); - XCTAssertEqual(subs.version, 0UL); - XCTAssertEqual(subs.count, 0UL); - - [subs update:^{ - }]; - - XCTAssertEqual(subs.version, 1UL); - XCTAssertEqual(subs.count, 0UL); -} - -- (void)testAddAndFindSubscriptionByQuery { - RLMRealm *realm = [self openFlexibleSyncRealm:_cmd]; - RLMSyncSubscriptionSet *subs = realm.subscriptions; - - [subs update:^{ - [subs addSubscriptionWithClassName:Person.className - where:@"age > 15"]; - }]; - - RLMSyncSubscription *foundSubscription = [subs subscriptionWithClassName:Person.className - where:@"age > 15"]; - XCTAssertNotNil(foundSubscription); - XCTAssertNil(foundSubscription.name); - XCTAssert(foundSubscription.queryString, @"age > 15"); -} - -- (void)testAddAndFindSubscriptionWithCompoundQuery { - RLMRealm *realm = [self openFlexibleSyncRealm:_cmd]; - RLMSyncSubscriptionSet *subs = realm.subscriptions; - XCTAssertNotNil(subs); - XCTAssertEqual(subs.version, 0UL); - XCTAssertEqual(subs.count, 0UL); - - [subs update:^{ - [subs addSubscriptionWithClassName:Person.className - where:@"firstName == %@ and lastName == %@", @"John", @"Doe"]; - }]; - - XCTAssertEqual(subs.version, 1UL); - XCTAssertEqual(subs.count, 1UL); - - RLMSyncSubscription *foundSubscription = [subs subscriptionWithClassName:Person.className - where:@"firstName == %@ and lastName == %@", @"John", @"Doe"]; - XCTAssertNotNil(foundSubscription); - XCTAssertNil(foundSubscription.name); - XCTAssert(foundSubscription.queryString, @"firstName == 'John' and lastName == 'Doe'"); -} - -- (void)testAddAndFindSubscriptionWithPredicate { - RLMRealm *realm = [self openFlexibleSyncRealm:_cmd]; - RLMSyncSubscriptionSet *subs = realm.subscriptions; - XCTAssertNotNil(subs); - XCTAssertEqual(subs.version, 0UL); - XCTAssertEqual(subs.count, 0UL); - - [subs update:^{ - [subs addSubscriptionWithClassName:Person.className - predicate:[NSPredicate predicateWithFormat:@"age == %d", 20]]; - }]; - - XCTAssertEqual(subs.version, 1UL); - XCTAssertEqual(subs.count, 1UL); - - RLMSyncSubscription *foundSubscription = [subs subscriptionWithClassName:Person.className - predicate:[NSPredicate predicateWithFormat:@"age == %d", 20]]; - XCTAssertNotNil(foundSubscription); - XCTAssertNil(foundSubscription.name); - XCTAssert(foundSubscription.queryString, @"age == 20"); -} - -- (void)testAddSubscriptionWithoutWriteThrow { - RLMRealm *realm = [self openFlexibleSyncRealm:_cmd]; - RLMSyncSubscriptionSet *subs = realm.subscriptions; - RLMAssertThrowsWithReason([subs addSubscriptionWithClassName:Person.className where:@"age > 15"], - @"Can only add, remove, or update subscriptions within a write subscription block."); -} - -- (void)testAddAndFindSubscriptionByName { - RLMRealm *realm = [self openFlexibleSyncRealm:_cmd]; - RLMSyncSubscriptionSet *subs = realm.subscriptions; - XCTAssertNotNil(realm.subscriptions); - XCTAssertEqual(realm.subscriptions.version, 0UL); - XCTAssertEqual(realm.subscriptions.count, 0UL); - - [subs update:^{ - [subs addSubscriptionWithClassName:Person.className - subscriptionName:@"person_older_15" - where:@"age > 15"]; - }]; - - RLMSyncSubscription *foundSubscription = [subs subscriptionWithName:@"person_older_15"]; - XCTAssertNotNil(foundSubscription); - XCTAssert(foundSubscription.name, @"person_older_15"); - XCTAssert(foundSubscription.queryString, @"age > 15"); -} - -- (void)testAddDuplicateSubscription { - RLMRealm *realm = [self openFlexibleSyncRealm:_cmd]; - RLMSyncSubscriptionSet *subs = realm.subscriptions; - - [subs update:^{ - [subs addSubscriptionWithClassName:Person.className - where:@"age > 15"]; - [subs addSubscriptionWithClassName:Person.className - where:@"age > 15"]; - }]; - - XCTAssertEqual(subs.version, 1UL); - XCTAssertEqual(subs.count, 1UL); -} - -- (void)testAddDuplicateNamedSubscriptionWillThrow { - RLMRealm *realm = [self openFlexibleSyncRealm:_cmd]; - RLMSyncSubscriptionSet *subs = realm.subscriptions; - - [subs update:^{ - [subs addSubscriptionWithClassName:Person.className - subscriptionName:@"person_age" - where:@"age > 15"]; - RLMAssertThrowsWithReason([subs addSubscriptionWithClassName:Person.className - subscriptionName:@"person_age" - where:@"age > 20"], - @"A subscription named 'person_age' already exists. If you meant to update the existing subscription please use the `update` method."); - }]; - - XCTAssertEqual(subs.version, 1UL); - XCTAssertEqual(subs.count, 1UL); - - RLMSyncSubscription *foundSubscription = [subs subscriptionWithName:@"person_age"]; - XCTAssertNotNil(foundSubscription); - - XCTAssertEqualObjects(foundSubscription.name, @"person_age"); - XCTAssertEqualObjects(foundSubscription.queryString, @"age > 15"); - XCTAssertEqualObjects(foundSubscription.objectClassName, @"Person"); -} - -- (void)testAddDuplicateSubscriptionWithPredicate { - RLMRealm *realm = [self openFlexibleSyncRealm:_cmd]; - RLMSyncSubscriptionSet *subs = realm.subscriptions; - - [subs update:^{ - [subs addSubscriptionWithClassName:Person.className - where:@"age > 15"]; - [subs addSubscriptionWithClassName:Person.className - predicate:[NSPredicate predicateWithFormat:@"age > %d", 15]]; - }]; - - XCTAssertEqual(subs.version, 1UL); - XCTAssertEqual(subs.count, 1UL); -} - -- (void)testAddDuplicateSubscriptionWithDifferentName { - RLMRealm *realm = [self openFlexibleSyncRealm:_cmd]; - RLMSyncSubscriptionSet *subs = realm.subscriptions; - - [subs update:^{ - [subs addSubscriptionWithClassName:Person.className - subscriptionName:@"person_age_1" - where:@"age > 15"]; - [subs addSubscriptionWithClassName:Person.className - subscriptionName:@"person_age_2" - predicate:[NSPredicate predicateWithFormat:@"age > %d", 15]]; - }]; - - XCTAssertEqual(subs.version, 1UL); - XCTAssertEqual(subs.count, 2UL); - - RLMSyncSubscription *foundSubscription = [subs subscriptionWithName:@"person_age_1"]; - XCTAssertNotNil(foundSubscription); - - RLMSyncSubscription *foundSubscription2 = [subs subscriptionWithName:@"person_age_2"]; - XCTAssertNotNil(foundSubscription2); - - XCTAssertNotEqualObjects(foundSubscription.name, foundSubscription2.name); - XCTAssertEqualObjects(foundSubscription.queryString, foundSubscription2.queryString); - XCTAssertEqualObjects(foundSubscription.objectClassName, foundSubscription2.objectClassName); -} - -- (void)testOverrideNamedWithUnnamedSubscription { - RLMRealm *realm = [self openFlexibleSyncRealm:_cmd]; - RLMSyncSubscriptionSet *subs = realm.subscriptions; - - [subs update:^{ - [subs addSubscriptionWithClassName:Person.className - subscriptionName:@"person_age_1" - where:@"age > 15"]; - [subs addSubscriptionWithClassName:Person.className - predicate:[NSPredicate predicateWithFormat:@"age > %d", 15]]; - }]; - - XCTAssertEqual(subs.version, 1UL); - XCTAssertEqual(subs.count, 2UL); -} - -- (void)testOverrideUnnamedWithNamedSubscription { - RLMRealm *realm = [self openFlexibleSyncRealm:_cmd]; - RLMSyncSubscriptionSet *subs = realm.subscriptions; - - [subs update:^{ - [subs addSubscriptionWithClassName:Person.className - predicate:[NSPredicate predicateWithFormat:@"age > %d", 15]]; - [subs addSubscriptionWithClassName:Person.className - subscriptionName:@"person_age_1" - where:@"age > 15"]; - }]; - - XCTAssertEqual(subs.version, 1UL); - XCTAssertEqual(subs.count, 2UL); -} - -- (void)testAddSubscriptionInDifferentWriteBlocks { - RLMRealm *realm = [self openFlexibleSyncRealm:_cmd]; - RLMSyncSubscriptionSet *subs = realm.subscriptions; - - [subs update:^{ - [subs addSubscriptionWithClassName:Person.className - subscriptionName:@"person_age_1" - where:@"age > 15"]; - }]; - - [subs update:^{ - [subs addSubscriptionWithClassName:Person.className - subscriptionName:@"person_age_2" - predicate:[NSPredicate predicateWithFormat:@"age > %d", 20]]; - }]; - - XCTAssertEqual(realm.subscriptions.version, 2UL); - XCTAssertEqual(realm.subscriptions.count, 2UL); - - RLMSyncSubscription *foundSubscription = [subs subscriptionWithName:@"person_age_1"]; - XCTAssertNotNil(foundSubscription); - - RLMSyncSubscription *foundSubscription2 = [subs subscriptionWithName:@"person_age_2"]; - XCTAssertNotNil(foundSubscription2); -} - -- (void)testRemoveSubscriptionByName { - RLMRealm *realm = [self openFlexibleSyncRealm:_cmd]; - RLMSyncSubscriptionSet *subs = realm.subscriptions; - - [subs update:^{ - [subs addSubscriptionWithClassName:Person.className - subscriptionName:@"person_age_1" - where:@"age > 15"]; - [subs addSubscriptionWithClassName:Person.className - subscriptionName:@"person_age_2" - predicate:[NSPredicate predicateWithFormat:@"age > %d", 20]]; - }]; - - XCTAssertEqual(subs.version, 1UL); - XCTAssertEqual(subs.count, 2UL); - - [subs update:^{ - [subs removeSubscriptionWithName:@"person_age_1"]; - }]; - - XCTAssertEqual(subs.version, 2UL); - XCTAssertEqual(subs.count, 1UL); - - RLMSyncSubscription *foundSubscription = [subs subscriptionWithName:@"person_age_1"]; - XCTAssertNil(foundSubscription); - - RLMSyncSubscription *foundSubscription2 = [subs subscriptionWithName:@"person_age_2"]; - XCTAssertNotNil(foundSubscription2); -} - -- (void)testRemoveSubscriptionWithoutWriteThrow { - RLMRealm *realm = [self openFlexibleSyncRealm:_cmd]; - RLMSyncSubscriptionSet *subs = realm.subscriptions; - - [subs update:^{ - [subs addSubscriptionWithClassName:Person.className - subscriptionName:@"person_age_1" - where:@"age > 15"]; - }]; - - XCTAssertEqual(subs.version, 1UL); - XCTAssertEqual(subs.count, 1UL); - RLMAssertThrowsWithReason([subs removeSubscriptionWithName:@"person_age_1"], @"Can only add, remove, or update subscriptions within a write subscription block."); -} - -- (void)testRemoveSubscriptionByQuery { - RLMRealm *realm = [self openFlexibleSyncRealm:_cmd]; - RLMSyncSubscriptionSet *subs = realm.subscriptions; - - [subs update:^{ - [subs addSubscriptionWithClassName:Person.className - subscriptionName:@"person_age" - where:@"age > 15"]; - [subs addSubscriptionWithClassName:Person.className - subscriptionName:@"person_firstname" - where:@"firstName == %@", @"John"]; - [subs addSubscriptionWithClassName:Person.className - subscriptionName:@"person_lastname" - predicate:[NSPredicate predicateWithFormat:@"lastName == %@", @"Doe"]]; - }]; - - XCTAssertEqual(subs.version, 1UL); - XCTAssertEqual(subs.count, 3UL); - - [subs update:^{ - [subs removeSubscriptionWithClassName:Person.className where:@"firstName == %@", @"John"]; - [subs removeSubscriptionWithClassName:Person.className predicate:[NSPredicate predicateWithFormat:@"lastName == %@", @"Doe"]]; - }]; - - XCTAssertEqual(subs.version, 2UL); - XCTAssertEqual(subs.count, 1UL); - - RLMSyncSubscription *foundSubscription = [subs subscriptionWithName:@"person_age"]; - XCTAssertNotNil(foundSubscription); - - RLMSyncSubscription *foundSubscription2 = [subs subscriptionWithName:@"person_firstname"]; - XCTAssertNil(foundSubscription2); - - RLMSyncSubscription *foundSubscription3 = [subs subscriptionWithName:@"person_lastname"]; - XCTAssertNil(foundSubscription3); -} - -- (void)testRemoveSubscription { - RLMRealm *realm = [self openFlexibleSyncRealm:_cmd]; - RLMSyncSubscriptionSet *subs = realm.subscriptions; - - [subs update:^{ - [subs addSubscriptionWithClassName:Person.className - subscriptionName:@"person_age" - where:@"age > 15"]; - [subs addSubscriptionWithClassName:Person.className - subscriptionName:@"person_firstname" - where:@"firstName == '%@'", @"John"]; - [subs addSubscriptionWithClassName:Person.className - subscriptionName:@"person_lastname" - predicate:[NSPredicate predicateWithFormat:@"lastName == %@", @"Doe"]]; - }]; - - XCTAssertEqual(subs.version, 1UL); - XCTAssertEqual(subs.count, 3UL); - - RLMSyncSubscription *foundSubscription = [subs subscriptionWithName:@"person_age"]; - XCTAssertNotNil(foundSubscription); - - [subs update:^{ - [subs removeSubscription:foundSubscription]; - }]; - - XCTAssertEqual(subs.version, 2UL); - XCTAssertEqual(subs.count, 2UL); - - RLMSyncSubscription *foundSubscription2 = [subs subscriptionWithName:@"person_firstname"]; - XCTAssertNotNil(foundSubscription2); - - [subs update:^{ - [subs removeSubscription:foundSubscription2]; - }]; - - XCTAssertEqual(subs.version, 3UL); - XCTAssertEqual(subs.count, 1UL); -} - -- (void)testRemoveAllSubscription { - RLMRealm *realm = [self openFlexibleSyncRealm:_cmd]; - RLMSyncSubscriptionSet *subs = realm.subscriptions; - - [subs update:^{ - [subs addSubscriptionWithClassName:Person.className - subscriptionName:@"person_age" - where:@"age > 15"]; - [subs addSubscriptionWithClassName:Person.className - subscriptionName:@"person_firstname" - where:@"firstName == '%@'", @"John"]; - [subs addSubscriptionWithClassName:Person.className - subscriptionName:@"person_lastname" - predicate:[NSPredicate predicateWithFormat:@"lastName == %@", @"Doe"]]; - }]; - - XCTAssertEqual(subs.version, 1UL); - XCTAssertEqual(subs.count, 3UL); - - [subs update:^{ - [subs removeAllSubscriptions]; - }]; - - XCTAssertEqual(subs.version, 2UL); - XCTAssertEqual(subs.count, 0UL); - - RLMSyncSubscription *foundSubscription = [subs subscriptionWithName:@"person_age_3"]; - XCTAssertNil(foundSubscription); -} - -- (void)testRemoveAllSubscriptionForType { - RLMRealm *realm = [self openFlexibleSyncRealm:_cmd]; - RLMSyncSubscriptionSet *subs = realm.subscriptions; - - [subs update:^{ - [subs addSubscriptionWithClassName:Person.className - subscriptionName:@"person_age" - where:@"age > 15"]; - [subs addSubscriptionWithClassName:Dog.className - subscriptionName:@"dog_name" - where:@"name == '%@'", @"Tomas"]; - [subs addSubscriptionWithClassName:Person.className - subscriptionName:@"person_lastname" - predicate:[NSPredicate predicateWithFormat:@"lastName == %@", @"Doe"]]; - }]; - - XCTAssertEqual(subs.version, 1UL); - XCTAssertEqual(subs.count, 3UL); - - [subs update:^{ - [subs removeAllSubscriptionsWithClassName:Person.className]; - }]; - - XCTAssertEqual(subs.version, 2UL); - XCTAssertEqual(subs.count, 1UL); - - RLMSyncSubscription *foundSubscription = [subs subscriptionWithName:@"dog_name"]; - XCTAssertNotNil(foundSubscription); - - [subs update:^{ - [subs removeAllSubscriptionsWithClassName:Dog.className]; - }]; - - XCTAssertEqual(subs.version, 3UL); - XCTAssertEqual(subs.count, 0UL); -} - -- (void)testUpdateSubscriptionQuery { - RLMRealm *realm = [self openFlexibleSyncRealm:_cmd]; - RLMSyncSubscriptionSet *subs = realm.subscriptions; - - [subs update:^{ - [subs addSubscriptionWithClassName:Person.className - subscriptionName:@"person_age" - where:@"age > 15"]; - }]; - - XCTAssertEqual(subs.version, 1UL); - XCTAssertEqual(subs.count, 1UL); - - RLMSyncSubscription *foundSubscription = [subs subscriptionWithName:@"person_age"]; - XCTAssertNotNil(foundSubscription); - - [subs update:^{ - [foundSubscription updateSubscriptionWhere:@"age > 20"]; - }]; - - XCTAssertEqual(subs.version, 2UL); - XCTAssertEqual(subs.count, 1UL); - - RLMSyncSubscription *foundSubscription2 = [subs subscriptionWithName:@"person_age"]; - XCTAssertNotNil(foundSubscription2); - XCTAssertEqualObjects(foundSubscription2.queryString, @"age > 20"); - XCTAssertEqualObjects(foundSubscription2.objectClassName, @"Person"); -} - -- (void)testUpdateSubscriptionQueryWithoutWriteThrow { - RLMRealm *realm = [self openFlexibleSyncRealm:_cmd]; - RLMSyncSubscriptionSet *subs = realm.subscriptions; - - [subs update:^{ - [subs addSubscriptionWithClassName:Person.className - subscriptionName:@"subscription_1" - where:@"age > 15"]; - }]; - - XCTAssertEqual(subs.version, 1UL); - XCTAssertEqual(subs.count, 1UL); - - RLMSyncSubscription *foundSubscription = [subs subscriptionWithName:@"subscription_1"]; - XCTAssertNotNil(foundSubscription); - - RLMAssertThrowsWithReason([foundSubscription updateSubscriptionWithPredicate:[NSPredicate predicateWithFormat:@"name == 'Tomas'"]], @"Can only add, remove, or update subscriptions within a write subscription block."); -} - -- (void)testSubscriptionSetIterate { - RLMRealm *realm = [self openFlexibleSyncRealm:_cmd]; - RLMSyncSubscriptionSet *subs = realm.subscriptions; - - double numberOfSubs = 100; - [subs update:^{ - for (int i = 0; i < numberOfSubs; ++i) { - [subs addSubscriptionWithClassName:Person.className - subscriptionName:[NSString stringWithFormat:@"person_age_%d", i] - where:[NSString stringWithFormat:@"age > %d", i]]; - } - }]; - - XCTAssertEqual(subs.version, 1UL); - XCTAssertEqual(subs.count, (unsigned long)numberOfSubs); - - __weak id objects[(unsigned long)pow(numberOfSubs, 2.0) + (unsigned long)numberOfSubs]; - NSInteger count = 0; - for (RLMSyncSubscription *sub in subs) { - XCTAssertNotNil(sub); - objects[count++] = sub; - for (RLMSyncSubscription *sub in subs) { - objects[count++] = sub; - } - } - XCTAssertEqual(count, pow(numberOfSubs, 2) + numberOfSubs); -} - -- (void)testSubscriptionSetFirstAndLast { - RLMRealm *realm = [self openFlexibleSyncRealm:_cmd]; - RLMSyncSubscriptionSet *subs = realm.subscriptions; - - XCTAssertNil(subs.firstObject); - XCTAssertNil(subs.lastObject); - - int numberOfSubs = 20; - [subs update:^{ - for (int i = 1; i <= numberOfSubs; ++i) { - [subs addSubscriptionWithClassName:Person.className - subscriptionName:[NSString stringWithFormat:@"person_age_%d", i] - where:[NSString stringWithFormat:@"age > %d", i]]; - } - }]; - - XCTAssertEqual(subs.version, 1UL); - XCTAssertEqual(subs.count, (unsigned long)numberOfSubs); - - RLMSyncSubscription *firstSubscription = subs.firstObject; - XCTAssertEqualObjects(firstSubscription.name, @"person_age_1"); - XCTAssertEqualObjects(firstSubscription.queryString, @"age > 1"); - - RLMSyncSubscription *lastSubscription = subs.lastObject; - XCTAssertEqualObjects(lastSubscription.name, ([NSString stringWithFormat:@"person_age_%d", numberOfSubs])); - XCTAssertEqualObjects(lastSubscription.queryString, ([NSString stringWithFormat:@"age > %d", numberOfSubs])); -} - -- (void)testSubscriptionSetSubscript { - RLMRealm *realm = [self openFlexibleSyncRealm:_cmd]; - RLMSyncSubscriptionSet *subs = realm.subscriptions; - - XCTAssertEqual(subs.count, 0UL); - - int numberOfSubs = 20; - [subs update:^{ - for (int i = 1; i <= numberOfSubs; ++i) { - [subs addSubscriptionWithClassName:Person.className - subscriptionName:[NSString stringWithFormat:@"person_age_%d", i] - where:[NSString stringWithFormat:@"age > %d", i]]; - } - }]; - - XCTAssertEqual(subs.version, 1UL); - XCTAssertEqual(subs.count, (unsigned long)numberOfSubs); - - RLMSyncSubscription *firstSubscription = subs[0]; - XCTAssertEqualObjects(firstSubscription.name, @"person_age_1"); - XCTAssertEqualObjects(firstSubscription.queryString, @"age > 1"); - - RLMSyncSubscription *lastSubscription = subs[numberOfSubs-1]; - XCTAssertEqualObjects(lastSubscription.name, ([NSString stringWithFormat:@"person_age_%d", numberOfSubs])); - XCTAssertEqualObjects(lastSubscription.queryString, ([NSString stringWithFormat:@"age > %d", numberOfSubs])); - - int index = (numberOfSubs/2); - RLMSyncSubscription *objectAtIndexSubscription = [subs objectAtIndex:index]; - XCTAssertEqualObjects(objectAtIndexSubscription.name, ([NSString stringWithFormat:@"person_age_%d", index+1])); - XCTAssertEqualObjects(objectAtIndexSubscription.queryString, ([NSString stringWithFormat:@"age > %d", index+1])); -} -@end - -@interface RLMFlexibleSyncServerTests : RLMSyncTestCase -@end - -@implementation RLMFlexibleSyncServerTests -- (void)createPeople:(RLMRealm *)realm partition:(SEL)cmd { +- (void)createPeople:(RLMRealm *)realm { const int numberOfSubs = 21; for (int i = 1; i <= numberOfSubs; ++i) { Person *person = [[Person alloc] initWithPrimaryKey:[RLMObjectId objectId] age:i firstName:[NSString stringWithFormat:@"firstname_%d", i] lastName:[NSString stringWithFormat:@"lastname_%d", i]]; - person.partition = NSStringFromSelector(cmd); + person.partition = self.name; [realm addObject:person]; } } -- (void)createDog:(RLMRealm *)realm partition:(SEL)cmd { +- (void)createDog:(RLMRealm *)realm { Dog *dog = [[Dog alloc] initWithPrimaryKey:[RLMObjectId objectId] breed:@"Labradoodle" name:@"Tom"]; - dog.partition = NSStringFromSelector(cmd); + dog.partition = self.name; [realm addObject:dog]; } - (void)testFlexibleSyncWithoutQuery { - bool didPopulate = [self populateData:^(RLMRealm *realm) { - [self createPeople:realm partition:_cmd]; + [self populateData:^(RLMRealm *realm) { + [self createPeople:realm]; }]; - if (!didPopulate) { - return; - } - RLMRealm *realm = [self getFlexibleSyncRealm:_cmd]; - XCTAssertNotNil(realm); + RLMRealm *realm = [self openRealm]; CHECK_COUNT(0, Person, realm); RLMSyncSubscriptionSet *subs = realm.subscriptions; @@ -709,21 +81,17 @@ - (void)testFlexibleSyncWithoutQuery { } - (void)testFlexibleSyncAddQuery { - bool didPopulate = [self populateData:^(RLMRealm *realm) { - [self createPeople:realm partition:_cmd]; + [self populateData:^(RLMRealm *realm) { + [self createPeople:realm]; }]; - if (!didPopulate) { - return; - } - RLMRealm *realm = [self getFlexibleSyncRealm:_cmd]; - XCTAssertNotNil(realm); + RLMRealm *realm = [self openRealm]; CHECK_COUNT(0, Person, realm); [self writeQueryAndCompleteForRealm:realm block:^(RLMSyncSubscriptionSet *subs) { [subs addSubscriptionWithClassName:Person.className subscriptionName:@"person_age" - where:@"age > 15 and partition == %@", NSStringFromSelector(_cmd)]; + where:@"age > 15 and partition == %@", self.name]; }]; CHECK_COUNT(6, Person, realm); @@ -731,25 +99,21 @@ - (void)testFlexibleSyncAddQuery { } - (void)testFlexibleSyncAddMultipleQuery { - bool didPopulate = [self populateData:^(RLMRealm *realm) { - [self createPeople:realm partition:_cmd]; - [self createDog:realm partition:_cmd]; + [self populateData:^(RLMRealm *realm) { + [self createPeople:realm]; + [self createDog:realm]; }]; - if (!didPopulate) { - return; - } - RLMRealm *realm = [self getFlexibleSyncRealm:_cmd]; - XCTAssertNotNil(realm); + RLMRealm *realm = [self openRealm]; CHECK_COUNT(0, Person, realm); [self writeQueryAndCompleteForRealm:realm block:^(RLMSyncSubscriptionSet *subs) { [subs addSubscriptionWithClassName:Person.className subscriptionName:@"person_age" - where:@"age > 10 and partition == %@", NSStringFromSelector(_cmd)]; + where:@"age > 10 and partition == %@", self.name]; [subs addSubscriptionWithClassName:Dog.className subscriptionName:@"dog_breed_labradoodle" - where:@"breed == 'Labradoodle' and partition == %@", NSStringFromSelector(_cmd)]; + where:@"breed == 'Labradoodle' and partition == %@", self.name]; }]; CHECK_COUNT(11, Person, realm); @@ -757,25 +121,21 @@ - (void)testFlexibleSyncAddMultipleQuery { } - (void)testFlexibleSyncRemoveQuery { - bool didPopulate = [self populateData:^(RLMRealm *realm) { - [self createPeople:realm partition:_cmd]; - [self createDog:realm partition:_cmd]; + [self populateData:^(RLMRealm *realm) { + [self createPeople:realm]; + [self createDog:realm]; }]; - if (!didPopulate) { - return; - } - RLMRealm *realm = [self getFlexibleSyncRealm:_cmd]; - XCTAssertNotNil(realm); + RLMRealm *realm = [self openRealm]; CHECK_COUNT(0, Person, realm); [self writeQueryAndCompleteForRealm:realm block:^(RLMSyncSubscriptionSet *subs) { [subs addSubscriptionWithClassName:Person.className subscriptionName:@"person_age" - where:@"age > 5 and partition == %@", NSStringFromSelector(_cmd)]; + where:@"age > 5 and partition == %@", self.name]; [subs addSubscriptionWithClassName:Dog.className subscriptionName:@"dog_breed_labradoodle" - where:@"breed == 'Labradoodle' and partition == %@", NSStringFromSelector(_cmd)]; + where:@"breed == 'Labradoodle' and partition == %@", self.name]; }]; CHECK_COUNT(16, Person, realm); CHECK_COUNT(1, Dog, realm); @@ -788,25 +148,21 @@ - (void)testFlexibleSyncRemoveQuery { } - (void)testFlexibleSyncRemoveAllQueries { - bool didPopulate = [self populateData:^(RLMRealm *realm) { - [self createPeople:realm partition:_cmd]; - [self createDog:realm partition:_cmd]; + [self populateData:^(RLMRealm *realm) { + [self createPeople:realm]; + [self createDog:realm]; }]; - if (!didPopulate) { - return; - } - RLMRealm *realm = [self getFlexibleSyncRealm:_cmd]; - XCTAssertNotNil(realm); + RLMRealm *realm = [self openRealm]; CHECK_COUNT(0, Person, realm); [self writeQueryAndCompleteForRealm:realm block:^(RLMSyncSubscriptionSet *subs) { [subs addSubscriptionWithClassName:Person.className subscriptionName:@"person_age" - where:@"age > 5 and partition == %@", NSStringFromSelector(_cmd)]; + where:@"age > 5 and partition == %@", self.name]; [subs addSubscriptionWithClassName:Dog.className subscriptionName:@"dog_breed_labradoodle" - where:@"breed == 'Labradoodle' and partition == %@", NSStringFromSelector(_cmd)]; + where:@"breed == 'Labradoodle' and partition == %@", self.name]; }]; CHECK_COUNT(16, Person, realm); CHECK_COUNT(1, Dog, realm); @@ -815,35 +171,31 @@ - (void)testFlexibleSyncRemoveAllQueries { [subs removeAllSubscriptions]; [subs addSubscriptionWithClassName:Person.className subscriptionName:@"person_age" - where:@"age > 0 and partition == %@", NSStringFromSelector(_cmd)]; + where:@"age > 0 and partition == %@", self.name]; }]; CHECK_COUNT(21, Person, realm); CHECK_COUNT(0, Dog, realm); } - (void)testFlexibleSyncRemoveAllQueriesForType { - bool didPopulate = [self populateData:^(RLMRealm *realm) { - [self createPeople:realm partition:_cmd]; - [self createDog:realm partition:_cmd]; + [self populateData:^(RLMRealm *realm) { + [self createPeople:realm]; + [self createDog:realm]; }]; - if (!didPopulate) { - return; - } - RLMRealm *realm = [self getFlexibleSyncRealm:_cmd]; - XCTAssertNotNil(realm); + RLMRealm *realm = [self openRealm]; CHECK_COUNT(0, Person, realm); [self writeQueryAndCompleteForRealm:realm block:^(RLMSyncSubscriptionSet *subs) { [subs addSubscriptionWithClassName:Person.className subscriptionName:@"person_age" - where:@"age > 20 and partition == %@", NSStringFromSelector(_cmd)]; + where:@"age > 20 and partition == %@", self.name]; [subs addSubscriptionWithClassName:Person.className subscriptionName:@"person_age_2" - where:@"firstName == 'firstname_1' and partition == %@", NSStringFromSelector(_cmd)]; + where:@"firstName == 'firstname_1' and partition == %@", self.name]; [subs addSubscriptionWithClassName:Dog.className subscriptionName:@"dog_breed_labradoodle" - where:@"breed == 'Labradoodle' and partition == %@", NSStringFromSelector(_cmd)]; + where:@"breed == 'Labradoodle' and partition == %@", self.name]; }]; CHECK_COUNT(2, Person, realm); CHECK_COUNT(1, Dog, realm); @@ -856,26 +208,22 @@ - (void)testFlexibleSyncRemoveAllQueriesForType { } - (void)testRemoveAllUnnamedSubscriptions { - bool didPopulate = [self populateData:^(RLMRealm *realm) { - [self createPeople:realm partition:_cmd]; - [self createDog:realm partition:_cmd]; + [self populateData:^(RLMRealm *realm) { + [self createPeople:realm]; + [self createDog:realm]; }]; - if (!didPopulate) { - return; - } - RLMRealm *realm = [self getFlexibleSyncRealm:_cmd]; - XCTAssertNotNil(realm); + RLMRealm *realm = [self openRealm]; CHECK_COUNT(0, Person, realm); [self writeQueryAndCompleteForRealm:realm block:^(RLMSyncSubscriptionSet *subs) { [subs addSubscriptionWithClassName:Person.className - where:@"age > 20 and partition == %@", NSStringFromSelector(_cmd)]; + where:@"age > 20 and partition == %@", self.name]; [subs addSubscriptionWithClassName:Person.className subscriptionName:@"person_age_2" - where:@"firstName == 'firstname_1' and partition == %@", NSStringFromSelector(_cmd)]; + where:@"firstName == 'firstname_1' and partition == %@", self.name]; [subs addSubscriptionWithClassName:Dog.className - where:@"breed == 'Labradoodle' and partition == %@", NSStringFromSelector(_cmd)]; + where:@"breed == 'Labradoodle' and partition == %@", self.name]; }]; XCTAssertEqual(realm.subscriptions.count, 3U); CHECK_COUNT(2, Person, realm); @@ -890,53 +238,45 @@ - (void)testRemoveAllUnnamedSubscriptions { } - (void)testFlexibleSyncUpdateQuery { - bool didPopulate = [self populateData:^(RLMRealm *realm) { - [self createPeople:realm partition:_cmd]; + [self populateData:^(RLMRealm *realm) { + [self createPeople:realm]; }]; - if (!didPopulate) { - return; - } - RLMRealm *realm = [self getFlexibleSyncRealm:_cmd]; - XCTAssertNotNil(realm); + RLMRealm *realm = [self openRealm]; CHECK_COUNT(0, Person, realm); [self writeQueryAndCompleteForRealm:realm block:^(RLMSyncSubscriptionSet *subs) { [subs addSubscriptionWithClassName:Person.className subscriptionName:@"person_age" - where:@"age > 0 and partition == %@", NSStringFromSelector(_cmd)]; + where:@"age > 0 and partition == %@", self.name]; }]; CHECK_COUNT(21, Person, realm); [self writeQueryAndCompleteForRealm:realm block:^(RLMSyncSubscriptionSet *subs) { RLMSyncSubscription *foundSub = [subs subscriptionWithName:@"person_age"]; - [foundSub updateSubscriptionWhere:@"age > 20 and partition == %@", NSStringFromSelector(_cmd)]; + [foundSub updateSubscriptionWhere:@"age > 20 and partition == %@", self.name]; }]; CHECK_COUNT(1, Person, realm); } - (void)testFlexibleSyncAddObjectOutsideQuery { - bool didPopulate = [self populateData:^(RLMRealm *realm) { - [self createPeople:realm partition:_cmd]; + [self populateData:^(RLMRealm *realm) { + [self createPeople:realm]; }]; - if (!didPopulate) { - return; - } - RLMRealm *realm = [self getFlexibleSyncRealm:_cmd]; - XCTAssertNotNil(realm); + RLMRealm *realm = [self openRealm]; CHECK_COUNT(0, Person, realm); [self writeQueryAndCompleteForRealm:realm block:^(RLMSyncSubscriptionSet *subs) { [subs addSubscriptionWithClassName:Person.className subscriptionName:@"person_age" - where:@"age > 18 and partition == %@", NSStringFromSelector(_cmd)]; + where:@"age > 18 and partition == %@", self.name]; }]; CHECK_COUNT(3, Person, realm); RLMObjectId *invalidObjectPK = [RLMObjectId objectId]; auto ex = [self expectationWithDescription:@"should revert write"]; - self.flexibleSyncApp.syncManager.errorHandler = ^(NSError *error, RLMSyncSession *) { + self.app.syncManager.errorHandler = ^(NSError *error, RLMSyncSession *) { RLMValidateError(error, RLMSyncErrorDomain, RLMSyncErrorWriteRejected, @"Client attempted a write that is not allowed; it has been reverted"); NSArray *info = error.userInfo[RLMCompensatingWriteInfoKey]; @@ -956,7 +296,7 @@ - (void)testFlexibleSyncAddObjectOutsideQuery { } - (void)testFlexibleSyncInitialSubscription { - RLMUser *user = [self flexibleSyncUser:_cmd]; + RLMUser *user = [self createUser]; RLMRealmConfiguration *config = [user flexibleSyncConfigurationWithInitialSubscriptions:^(RLMSyncSubscriptionSet *subscriptions) { [subscriptions addSubscriptionWithClassName:Person.className subscriptionName:@"person_age" @@ -968,18 +308,15 @@ - (void)testFlexibleSyncInitialSubscription { } - (void)testFlexibleSyncInitialSubscriptionAwait { - bool didPopulate = [self populateData:^(RLMRealm *realm) { - [self createPeople:realm partition:_cmd]; + [self populateData:^(RLMRealm *realm) { + [self createPeople:realm]; }]; - if (!didPopulate) { - return; - } - RLMUser *user = [self flexibleSyncUser:_cmd]; + RLMUser *user = [self createUser]; RLMRealmConfiguration *config = [user flexibleSyncConfigurationWithInitialSubscriptions:^(RLMSyncSubscriptionSet *subscriptions) { [subscriptions addSubscriptionWithClassName:Person.className subscriptionName:@"person_age" - where:@"age > 10 and partition == %@", NSStringFromSelector(_cmd)]; + where:@"age > 10 and partition == %@", self.name]; } rerunOnOpen:false]; config.objectClasses = @[Person.self]; XCTestExpectation *ex = [self expectationWithDescription:@"download-realm"]; @@ -988,11 +325,6 @@ - (void)testFlexibleSyncInitialSubscriptionAwait { callback:^(RLMRealm *realm, NSError *error) { XCTAssertNil(error); XCTAssertEqual(realm.subscriptions.count, 1UL); - // Adding this sleep, because there seems to be a timing issue after this commit in baas - // https://github.com/10gen/baas/commit/64e75b3f1fe8a6f8704d1597de60f9dda401ccce, - // data take a little longer to be downloaded to the realm even though the - // sync client changed the subscription state to completed. - sleep(1); CHECK_COUNT(11, Person, realm); [ex fulfill]; }]; @@ -1000,11 +332,11 @@ - (void)testFlexibleSyncInitialSubscriptionAwait { } - (void)testFlexibleSyncInitialSubscriptionDoNotRerunOnOpen { - RLMUser *user = [self flexibleSyncUser:_cmd]; + RLMUser *user = [self createUser]; RLMRealmConfiguration *config = [user flexibleSyncConfigurationWithInitialSubscriptions:^(RLMSyncSubscriptionSet *subscriptions) { [subscriptions addSubscriptionWithClassName:Person.className subscriptionName:@"person_age" - where:@"age > 10 and partition == %@", NSStringFromSelector(_cmd)]; + where:@"age > 10 and partition == %@", self.name]; } rerunOnOpen:false]; config.objectClasses = @[Person.self]; @@ -1023,21 +355,18 @@ - (void)testFlexibleSyncInitialSubscriptionDoNotRerunOnOpen { } - (void)testFlexibleSyncInitialSubscriptionRerunOnOpen { - bool didPopulate = [self populateData:^(RLMRealm *realm) { - [self createPeople:realm partition:_cmd]; + [self populateData:^(RLMRealm *realm) { + [self createPeople:realm]; }]; - if (!didPopulate) { - return; - } - RLMUser *user = [self flexibleSyncUser:_cmd]; + RLMUser *user = [self createUser]; __block int openCount = 0; RLMRealmConfiguration *config = [user flexibleSyncConfigurationWithInitialSubscriptions:^(RLMSyncSubscriptionSet *subscriptions) { XCTAssertLessThan(openCount, 2); int age = openCount == 0 ? 10 : 5; [subscriptions addSubscriptionWithClassName:Person.className - where:@"age > %i and partition == %@", age, NSStringFromSelector(_cmd)]; + where:@"age > %i and partition == %@", age, self.name]; ++openCount; } rerunOnOpen:true]; config.objectClasses = @[Person.self]; @@ -1089,12 +418,12 @@ - (void)testFlexibleSyncInitialOnConnectionTimeout { RLMSyncTimeoutOptions *timeoutOptions = [RLMSyncTimeoutOptions new]; timeoutOptions.connectTimeout = 1000.0; appConfig.syncTimeouts = timeoutOptions; - RLMApp *app = [RLMApp appWithId:self.flexibleSyncAppId configuration:appConfig]; + RLMApp *app = [RLMApp appWithId:self.appId configuration:appConfig]; RLMUser *user = [self logInUserForCredentials:[RLMCredentials anonymousCredentials] app:app]; RLMRealmConfiguration *config = [user flexibleSyncConfigurationWithInitialSubscriptions:^(RLMSyncSubscriptionSet *subscriptions) { [subscriptions addSubscriptionWithClassName:Person.className - where:@"age > 10 and partition == %@", NSStringFromSelector(_cmd)]; + where:@"age > 10 and partition == %@", self.name]; } rerunOnOpen:true]; config.objectClasses = @[Person.class]; RLMSyncConfiguration *syncConfig = config.syncConfiguration; @@ -1119,20 +448,16 @@ - (void)testFlexibleSyncInitialOnConnectionTimeout { } - (void)testSubscribeWithName { - bool didPopulate = [self populateData:^(RLMRealm *realm) { + [self populateData:^(RLMRealm *realm) { Person *person = [[Person alloc] initWithPrimaryKey:[RLMObjectId objectId] age:30 firstName:@"Brian" lastName:@"Epstein"]; - person.partition = NSStringFromSelector(_cmd); + person.partition = self.name; [realm addObject:person]; }]; - if (!didPopulate) { - return; - } - RLMRealm *realm = [self getFlexibleSyncRealm:_cmd]; - XCTAssertNotNil(realm); + RLMRealm *realm = [self openRealm]; CHECK_COUNT(0, Person, realm); XCTestExpectation *ex = [self expectationWithDescription:@"wait for download"]; @@ -1145,24 +470,22 @@ - (void)testSubscribeWithName { } - (void)testUnsubscribeWithinBlock { - bool didPopulate = [self populateData:^(RLMRealm *realm) { + [self populateData:^(RLMRealm *realm) { Person *person = [[Person alloc] initWithPrimaryKey:[RLMObjectId objectId] age:30 firstName:@"Joe" lastName:@"Doe"]; - person.partition = NSStringFromSelector(_cmd); + person.partition = self.name; [realm addObject:person]; }]; - if (!didPopulate) { - return; - } - RLMRealm *realm = [self getFlexibleSyncRealm:_cmd]; - XCTAssertNotNil(realm); + RLMRealm *realm = [self openRealm]; CHECK_COUNT(0, Person, realm); XCTestExpectation *ex = [self expectationWithDescription:@"wait for download"]; - [[[Person allObjectsInRealm:realm] objectsWhere:@"lastName == 'Doe'"] subscribeWithName:@"unknown" onQueue:dispatch_get_main_queue() completion:^(RLMResults *results, NSError *error) { + [[Person objectsInRealm:realm where:@"lastName == 'Doe' AND partition == %@", self.name] + subscribeWithName:@"unknown" onQueue:dispatch_get_main_queue() + completion:^(RLMResults *results, NSError *error) { XCTAssertNil(error); XCTAssertEqual(results.count, 1U); [results unsubscribe]; @@ -1173,19 +496,16 @@ - (void)testUnsubscribeWithinBlock { } - (void)testSubscribeOnQueue { - bool didPopulate = [self populateData:^(RLMRealm *realm) { + [self populateData:^(RLMRealm *realm) { Person *person = [[Person alloc] initWithPrimaryKey:[RLMObjectId objectId] age:30 firstName:@"Sophia" lastName:@"Loren"]; - person.partition = NSStringFromSelector(_cmd); + person.partition = self.name; [realm addObject:person]; }]; - if (!didPopulate) { - return; - } - RLMUser *user = [self flexibleSyncUser:_cmd]; + RLMUser *user = [self createUser]; RLMRealmConfiguration *config = [user flexibleSyncConfiguration]; config.objectClasses = @[Person.self]; @@ -1213,14 +533,11 @@ - (void)testSubscribeOnQueue { } - (void)testSubscribeWithNameAndTimeout { - bool didPopulate = [self populateData:^(RLMRealm *realm) { - [self createPeople:realm partition:_cmd]; + [self populateData:^(RLMRealm *realm) { + [self createPeople:realm]; }]; - if (!didPopulate) { - return; - } - RLMRealm *realm = [self getFlexibleSyncRealm:_cmd]; + RLMRealm *realm = [self openRealm]; XCTAssertNotNil(realm); CHECK_COUNT(0, Person, realm); @@ -1248,9 +565,8 @@ - (void)testSubscribeWithNameAndTimeout { XCTAssertEqual(realm.subscriptions.state, RLMSyncSubscriptionStateComplete); } -#if 0 // FIXME: this is no longer an error and needs to be updated to something which is - (void)testFlexibleSyncInitialSubscriptionThrowsError { - RLMUser *user = [self flexibleSyncUser:_cmd]; + RLMUser *user = [self createUser]; RLMRealmConfiguration *config = [user flexibleSyncConfigurationWithInitialSubscriptions:^(RLMSyncSubscriptionSet *subscriptions) { [subscriptions addSubscriptionWithClassName:UUIDPrimaryKeyObject.className where:@"strCol == %@", @"Tom"]; @@ -1261,11 +577,10 @@ - (void)testFlexibleSyncInitialSubscriptionThrowsError { callbackQueue:dispatch_get_main_queue() callback:^(RLMRealm *realm, NSError *error) { RLMValidateError(error, RLMErrorDomain, RLMErrorSubscriptionFailed, - @"Client provided query with bad syntax: unsupported query for table \"UUIDPrimaryKeyObject\": key \"strCol\" is not a queryable field"); + @"Invalid query: unsupported query for table \"UUIDPrimaryKeyObject\": key \"strCol\" is not a queryable field"); XCTAssertNil(realm); [ex fulfill]; }]; [self waitForExpectationsWithTimeout:30.0 handler:nil]; } -#endif @end diff --git a/Realm/ObjectServerTests/RLMMongoClientTests.mm b/Realm/ObjectServerTests/RLMMongoClientTests.mm new file mode 100644 index 0000000000..640e5ce0d1 --- /dev/null +++ b/Realm/ObjectServerTests/RLMMongoClientTests.mm @@ -0,0 +1,800 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2016 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +#import "RLMSyncTestCase.h" +#import "RLMUser+ObjectServerTests.h" +#import "RLMWatchTestUtility.h" +#import "RLMBSON_Private.hpp" +#import "RLMUser_Private.hpp" + +#import +#import + +#import + +#if TARGET_OS_OSX + +@interface RLMMongoClientTests : RLMSyncTestCase +@end + +@implementation RLMMongoClientTests +- (NSArray *)defaultObjectTypes { + return @[Dog.self]; +} + +- (void)tearDown { + [self cleanupRemoteDocuments:[self.anonymousUser collectionForType:Dog.class app:self.app]]; + [super tearDown]; +} + +- (void)testFindOneAndModifyOptions { + NSDictionary> *projection = @{@"name": @1, @"breed": @1}; + NSArray> *sorting = @[@{@"age": @1}, @{@"coat": @1}]; + + RLMFindOneAndModifyOptions *findOneAndModifyOptions1 = [[RLMFindOneAndModifyOptions alloc] init]; + XCTAssertNil(findOneAndModifyOptions1.projection); + XCTAssertEqual(findOneAndModifyOptions1.sorting.count, 0U); + XCTAssertFalse(findOneAndModifyOptions1.shouldReturnNewDocument); + XCTAssertFalse(findOneAndModifyOptions1.upsert); + + RLMFindOneAndModifyOptions *findOneAndModifyOptions2 = [[RLMFindOneAndModifyOptions alloc] init]; + findOneAndModifyOptions2.projection = projection; + findOneAndModifyOptions2.sorting = sorting; + findOneAndModifyOptions2.shouldReturnNewDocument = YES; + findOneAndModifyOptions2.upsert = YES; + XCTAssertNotNil(findOneAndModifyOptions2.projection); + XCTAssertEqual(findOneAndModifyOptions2.sorting.count, 2U); + XCTAssertTrue(findOneAndModifyOptions2.shouldReturnNewDocument); + XCTAssertTrue(findOneAndModifyOptions2.upsert); + XCTAssertFalse([findOneAndModifyOptions2.projection isEqual:@{}]); + XCTAssertTrue([findOneAndModifyOptions2.projection isEqual:projection]); + XCTAssertFalse([findOneAndModifyOptions2.sorting isEqual:@{}]); + XCTAssertTrue([findOneAndModifyOptions2.sorting isEqual:sorting]); + + RLMFindOneAndModifyOptions *findOneAndModifyOptions3 = [[RLMFindOneAndModifyOptions alloc] + initWithProjection:projection + sorting:sorting + upsert:YES + shouldReturnNewDocument:YES]; + XCTAssertNotNil(findOneAndModifyOptions3.projection); + XCTAssertEqual(findOneAndModifyOptions3.sorting.count, 2U); + XCTAssertTrue(findOneAndModifyOptions3.shouldReturnNewDocument); + XCTAssertTrue(findOneAndModifyOptions3.upsert); + XCTAssertFalse([findOneAndModifyOptions3.projection isEqual:@{}]); + XCTAssertTrue([findOneAndModifyOptions3.projection isEqual:projection]); + XCTAssertFalse([findOneAndModifyOptions3.sorting isEqual:@{}]); + XCTAssertTrue([findOneAndModifyOptions3.sorting isEqual:sorting]); + + findOneAndModifyOptions3.projection = nil; + findOneAndModifyOptions3.sorting = @[]; + XCTAssertNil(findOneAndModifyOptions3.projection); + XCTAssertEqual(findOneAndModifyOptions3.sorting.count, 0U); + XCTAssertTrue([findOneAndModifyOptions3.sorting isEqual:@[]]); + + RLMFindOneAndModifyOptions *findOneAndModifyOptions4 = [[RLMFindOneAndModifyOptions alloc] + initWithProjection:nil + sorting:@[] + upsert:NO + shouldReturnNewDocument:NO]; + XCTAssertNil(findOneAndModifyOptions4.projection); + XCTAssertEqual(findOneAndModifyOptions4.sorting.count, 0U); + XCTAssertFalse(findOneAndModifyOptions4.upsert); + XCTAssertFalse(findOneAndModifyOptions4.shouldReturnNewDocument); +} + +- (void)testFindOptions { + NSDictionary> *projection = @{@"name": @1, @"breed": @1}; + NSArray> *sorting = @[@{@"age": @1}, @{@"coat": @1}]; + + RLMFindOptions *findOptions1 = [[RLMFindOptions alloc] init]; + XCTAssertNil(findOptions1.projection); + XCTAssertEqual(findOptions1.sorting.count, 0U); + XCTAssertEqual(findOptions1.limit, 0); + + findOptions1.limit = 37; + findOptions1.projection = projection; + findOptions1.sorting = sorting; + XCTAssertEqual(findOptions1.limit, 37); + XCTAssertTrue([findOptions1.projection isEqual:projection]); + XCTAssertEqual(findOptions1.sorting.count, 2U); + XCTAssertTrue([findOptions1.sorting isEqual:sorting]); + + RLMFindOptions *findOptions2 = [[RLMFindOptions alloc] initWithProjection:projection + sorting:sorting]; + XCTAssertTrue([findOptions2.projection isEqual:projection]); + XCTAssertEqual(findOptions2.sorting.count, 2U); + XCTAssertEqual(findOptions2.limit, 0); + XCTAssertTrue([findOptions2.sorting isEqual:sorting]); + + RLMFindOptions *findOptions3 = [[RLMFindOptions alloc] initWithLimit:37 + projection:projection + sorting:sorting]; + XCTAssertTrue([findOptions3.projection isEqual:projection]); + XCTAssertEqual(findOptions3.sorting.count, 2U); + XCTAssertEqual(findOptions3.limit, 37); + XCTAssertTrue([findOptions3.sorting isEqual:sorting]); + + findOptions3.projection = nil; + findOptions3.sorting = @[]; + XCTAssertNil(findOptions3.projection); + XCTAssertEqual(findOptions3.sorting.count, 0U); + + RLMFindOptions *findOptions4 = [[RLMFindOptions alloc] initWithProjection:nil + sorting:@[]]; + XCTAssertNil(findOptions4.projection); + XCTAssertEqual(findOptions4.sorting.count, 0U); + XCTAssertEqual(findOptions4.limit, 0); +} + +- (void)testMongoInsert { + RLMMongoCollection *collection = [self.anonymousUser collectionForType:Dog.class app:self.app]; + + XCTestExpectation *insertOneExpectation = [self expectationWithDescription:@"should insert one document"]; + [collection insertOneDocument:@{@"name": @"fido", @"breed": @"cane corso"} completion:^(id objectId, NSError *error) { + XCTAssertEqual(objectId.bsonType, RLMBSONTypeObjectId); + XCTAssertNotEqualObjects(((RLMObjectId *)objectId).stringValue, @""); + XCTAssertNil(error); + [insertOneExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:60.0 handler:nil]; + + XCTestExpectation *insertManyExpectation = [self expectationWithDescription:@"should insert one document"]; + [collection insertManyDocuments:@[ + @{@"name": @"fido", @"breed": @"cane corso"}, + @{@"name": @"fido", @"breed": @"cane corso"}, + @{@"name": @"rex", @"breed": @"tibetan mastiff"}] + completion:^(NSArray> *objectIds, NSError *error) { + XCTAssertGreaterThan(objectIds.count, 0U); + XCTAssertNil(error); + [insertManyExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:60.0 handler:nil]; + + XCTestExpectation *findExpectation = [self expectationWithDescription:@"should find documents"]; + RLMFindOptions *options = [[RLMFindOptions alloc] initWithLimit:0 projection:nil sorting:@[]]; + [collection findWhere:@{@"name": @"fido", @"breed": @"cane corso"} + options:options + completion:^(NSArray *documents, NSError *error) { + XCTAssertEqual(documents.count, 3U); + XCTAssertNil(error); + [findExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:60.0 handler:nil]; +} + +- (void)testMongoFind { + RLMMongoCollection *collection = [self.anonymousUser collectionForType:Dog.class app:self.app]; + + XCTestExpectation *insertManyExpectation = [self expectationWithDescription:@"should insert one document"]; + [collection insertManyDocuments:@[ + @{@"name": @"fido", @"breed": @"cane corso"}, + @{@"name": @"fido", @"breed": @"cane corso"}, + @{@"name": @"rex", @"breed": @"tibetan mastiff"}] + completion:^(NSArray> *objectIds, NSError *error) { + XCTAssertGreaterThan(objectIds.count, 0U); + XCTAssertNil(error); + [insertManyExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:60.0 handler:nil]; + + XCTestExpectation *findExpectation = [self expectationWithDescription:@"should find documents"]; + RLMFindOptions *options = [[RLMFindOptions alloc] initWithLimit:0 projection:nil sorting:@[]]; + [collection findWhere:@{@"name": @"fido", @"breed": @"cane corso"} + options:options + completion:^(NSArray *documents, NSError *error) { + XCTAssertEqual(documents.count, 2U); + XCTAssertNil(error); + [findExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:60.0 handler:nil]; + + XCTestExpectation *findExpectation2 = [self expectationWithDescription:@"should find documents"]; + [collection findWhere:@{@"name": @"fido", @"breed": @"cane corso"} + completion:^(NSArray *documents, NSError *error) { + XCTAssertEqual(documents.count, 2U); + XCTAssertNil(error); + [findExpectation2 fulfill]; + }]; + [self waitForExpectationsWithTimeout:60.0 handler:nil]; + + XCTestExpectation *findExpectation3 = [self expectationWithDescription:@"should not find documents"]; + [collection findWhere:@{@"name": @"should not exist", @"breed": @"should not exist"} + completion:^(NSArray *documents, NSError *error) { + XCTAssertEqual(documents.count, NSUInteger(0)); + XCTAssertNil(error); + [findExpectation3 fulfill]; + }]; + [self waitForExpectationsWithTimeout:60.0 handler:nil]; + + XCTestExpectation *findExpectation4 = [self expectationWithDescription:@"should not find documents"]; + [collection findWhere:@{} + completion:^(NSArray *documents, NSError *error) { + XCTAssertGreaterThan(documents.count, 0U); + XCTAssertNil(error); + [findExpectation4 fulfill]; + }]; + [self waitForExpectationsWithTimeout:60.0 handler:nil]; + + XCTestExpectation *findOneExpectation1 = [self expectationWithDescription:@"should find documents"]; + [collection findOneDocumentWhere:@{@"name": @"fido", @"breed": @"cane corso"} + completion:^(NSDictionary *document, NSError *error) { + XCTAssertTrue([document[@"name"] isEqualToString:@"fido"]); + XCTAssertTrue([document[@"breed"] isEqualToString:@"cane corso"]); + XCTAssertNil(error); + [findOneExpectation1 fulfill]; + }]; + [self waitForExpectationsWithTimeout:60.0 handler:nil]; + + XCTestExpectation *findOneExpectation2 = [self expectationWithDescription:@"should find documents"]; + [collection findOneDocumentWhere:@{@"name": @"fido", @"breed": @"cane corso"} + options:options + completion:^(NSDictionary *document, NSError *error) { + XCTAssertTrue([document[@"name"] isEqualToString:@"fido"]); + XCTAssertTrue([document[@"breed"] isEqualToString:@"cane corso"]); + XCTAssertNil(error); + [findOneExpectation2 fulfill]; + }]; + [self waitForExpectationsWithTimeout:60.0 handler:nil]; +} + +- (void)testMongoAggregateAndCount { + RLMMongoCollection *collection = [self.anonymousUser collectionForType:Dog.class app:self.app]; + + XCTestExpectation *insertManyExpectation = [self expectationWithDescription:@"should insert one document"]; + [collection insertManyDocuments:@[ + @{@"name": @"fido", @"breed": @"cane corso"}, + @{@"name": @"fido", @"breed": @"cane corso"}, + @{@"name": @"rex", @"breed": @"tibetan mastiff"}] + completion:^(NSArray> *objectIds, NSError *error) { + XCTAssertEqual(objectIds.count, 3U); + XCTAssertNil(error); + [insertManyExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:60.0 handler:nil]; + + XCTestExpectation *aggregateExpectation1 = [self expectationWithDescription:@"should aggregate documents"]; + [collection aggregateWithPipeline:@[@{@"name" : @"fido"}] + completion:^(NSArray *documents, NSError *error) { + RLMValidateErrorContains(error, RLMAppErrorDomain, RLMAppErrorMongoDBError, + @"Unrecognized pipeline stage name: 'name'"); + XCTAssertNil(documents); + [aggregateExpectation1 fulfill]; + }]; + [self waitForExpectationsWithTimeout:60.0 handler:nil]; + + XCTestExpectation *aggregateExpectation2 = [self expectationWithDescription:@"should aggregate documents"]; + [collection aggregateWithPipeline:@[@{@"$match" : @{@"name" : @"fido"}}, @{@"$group" : @{@"_id" : @"$name"}}] + completion:^(NSArray *documents, NSError *error) { + XCTAssertNil(error); + XCTAssertNotNil(documents); + XCTAssertGreaterThan(documents.count, 0U); + [aggregateExpectation2 fulfill]; + }]; + [self waitForExpectationsWithTimeout:60.0 handler:nil]; + + XCTestExpectation *countExpectation1 = [self expectationWithDescription:@"should aggregate documents"]; + [collection countWhere:@{@"name" : @"fido"} + completion:^(NSInteger count, NSError *error) { + XCTAssertGreaterThan(count, 0); + XCTAssertNil(error); + [countExpectation1 fulfill]; + }]; + [self waitForExpectationsWithTimeout:60.0 handler:nil]; + + XCTestExpectation *countExpectation2 = [self expectationWithDescription:@"should aggregate documents"]; + [collection countWhere:@{@"name" : @"fido"} + limit:1 + completion:^(NSInteger count, NSError *error) { + XCTAssertEqual(count, 1); + XCTAssertNil(error); + [countExpectation2 fulfill]; + }]; + [self waitForExpectationsWithTimeout:60.0 handler:nil]; +} + +- (void)testMongoUpdate { + RLMMongoCollection *collection = [self.anonymousUser collectionForType:Dog.class app:self.app]; + + XCTestExpectation *updateExpectation1 = [self expectationWithDescription:@"should update document"]; + [collection updateOneDocumentWhere:@{@"name" : @"scrabby doo"} + updateDocument:@{@"name" : @"scooby"} + upsert:YES + completion:^(RLMUpdateResult *result, NSError *error) { + XCTAssertNotNil(result); + XCTAssertNotNil(result.documentId); + XCTAssertEqual(result.modifiedCount, (NSUInteger)0); + XCTAssertEqual(result.matchedCount, (NSUInteger)0); + XCTAssertNil(error); + [updateExpectation1 fulfill]; + }]; + [self waitForExpectationsWithTimeout:60.0 handler:nil]; + + XCTestExpectation *updateExpectation2 = [self expectationWithDescription:@"should update document"]; + [collection updateOneDocumentWhere:@{@"name" : @"scooby"} + updateDocument:@{@"name" : @"fred"} + upsert:NO + completion:^(RLMUpdateResult *result, NSError *error) { + XCTAssertNotNil(result); + XCTAssertNil(result.documentId); + XCTAssertEqual(result.modifiedCount, (NSUInteger)1); + XCTAssertEqual(result.matchedCount, (NSUInteger)1); + XCTAssertNil(error); + [updateExpectation2 fulfill]; + }]; + [self waitForExpectationsWithTimeout:60.0 handler:nil]; + + XCTestExpectation *updateExpectation3 = [self expectationWithDescription:@"should update document"]; + [collection updateOneDocumentWhere:@{@"name" : @"fred"} + updateDocument:@{@"name" : @"scrabby"} + completion:^(RLMUpdateResult *result, NSError *error) { + XCTAssertNotNil(result); + XCTAssertNil(result.documentId); + XCTAssertEqual(result.modifiedCount, (NSUInteger)1); + XCTAssertEqual(result.matchedCount, (NSUInteger)1); + XCTAssertNil(error); + [updateExpectation3 fulfill]; + }]; + [self waitForExpectationsWithTimeout:60.0 handler:nil]; + + XCTestExpectation *updateManyExpectation1 = [self expectationWithDescription:@"should update many documents"]; + [collection updateManyDocumentsWhere:@{@"name" : @"scrabby"} + updateDocument:@{@"name" : @"fred"} + completion:^(RLMUpdateResult *result, NSError *error) { + XCTAssertNotNil(result); + XCTAssertNil(result.documentId); + XCTAssertEqual(result.modifiedCount, (NSUInteger)1); + XCTAssertEqual(result.matchedCount, (NSUInteger)1); + XCTAssertNil(error); + [updateManyExpectation1 fulfill]; + }]; + [self waitForExpectationsWithTimeout:60.0 handler:nil]; + + XCTestExpectation *updateManyExpectation2 = [self expectationWithDescription:@"should update many documents"]; + [collection updateManyDocumentsWhere:@{@"name" : @"john"} + updateDocument:@{@"name" : @"alex"} + upsert:YES + completion:^(RLMUpdateResult *result, NSError *error) { + XCTAssertNotNil(result); + XCTAssertNotNil(result.documentId); + XCTAssertEqual(result.modifiedCount, (NSUInteger)0); + XCTAssertEqual(result.matchedCount, (NSUInteger)0); + XCTAssertNil(error); + [updateManyExpectation2 fulfill]; + }]; + [self waitForExpectationsWithTimeout:60.0 handler:nil]; +} + +- (void)testMongoFindAndModify { + RLMMongoCollection *collection = [self.anonymousUser collectionForType:Dog.class app:self.app]; + + NSArray> *sorting = @[@{@"name": @1}, @{@"breed": @1}]; + RLMFindOneAndModifyOptions *findAndModifyOptions = [[RLMFindOneAndModifyOptions alloc] + initWithProjection:@{@"name" : @1, @"breed" : @1} + sorting:sorting + upsert:YES + shouldReturnNewDocument:YES]; + + XCTestExpectation *findOneAndUpdateExpectation1 = [self expectationWithDescription:@"should find one document and update"]; + [collection findOneAndUpdateWhere:@{@"name" : @"alex"} + updateDocument:@{@"name" : @"max"} + options:findAndModifyOptions + completion:^(NSDictionary *document, NSError *error) { + XCTAssertTrue([document[@"name"] isEqualToString:@"max"]); + XCTAssertNil(error); + [findOneAndUpdateExpectation1 fulfill]; + }]; + [self waitForExpectationsWithTimeout:60.0 handler:nil]; + + XCTestExpectation *findOneAndUpdateExpectation2 = [self expectationWithDescription:@"should find one document and update"]; + [collection findOneAndUpdateWhere:@{@"name" : @"max"} + updateDocument:@{@"name" : @"john"} + completion:^(NSDictionary *document, NSError *error) { + XCTAssertTrue([document[@"name"] isEqualToString:@"max"]); + XCTAssertNil(error); + [findOneAndUpdateExpectation2 fulfill]; + }]; + [self waitForExpectationsWithTimeout:60.0 handler:nil]; + + XCTestExpectation *findOneAndReplaceExpectation1 = [self expectationWithDescription:@"should find one document and replace"]; + [collection findOneAndReplaceWhere:@{@"name" : @"alex"} + replacementDocument:@{@"name" : @"max"} + options:findAndModifyOptions + completion:^(NSDictionary *document, NSError *error) { + XCTAssertTrue([document[@"name"] isEqualToString:@"max"]); + XCTAssertNil(error); + [findOneAndReplaceExpectation1 fulfill]; + }]; + [self waitForExpectationsWithTimeout:60.0 handler:nil]; + + XCTestExpectation *findOneAndReplaceExpectation2 = [self expectationWithDescription:@"should find one document and replace"]; + [collection findOneAndReplaceWhere:@{@"name" : @"max"} + replacementDocument:@{@"name" : @"john"} + completion:^(NSDictionary *document, NSError *error) { + XCTAssertTrue([document[@"name"] isEqualToString:@"max"]); + XCTAssertNil(error); + [findOneAndReplaceExpectation2 fulfill]; + }]; + [self waitForExpectationsWithTimeout:60.0 handler:nil]; +} + +- (void)testMongoDelete { + RLMMongoCollection *collection = [self.anonymousUser collectionForType:Dog.class app:self.app]; + + NSArray *objectIds = [self prepareDogDocumentsIn:collection]; + RLMObjectId *rexObjectId = objectIds[1]; + + XCTestExpectation *deleteOneExpectation1 = [self expectationWithDescription:@"should delete first document in collection"]; + [collection deleteOneDocumentWhere:@{@"_id" : rexObjectId} + completion:^(NSInteger count, NSError *error) { + XCTAssertEqual(count, 1); + XCTAssertNil(error); + [deleteOneExpectation1 fulfill]; + }]; + [self waitForExpectationsWithTimeout:60.0 handler:nil]; + + XCTestExpectation *findExpectation1 = [self expectationWithDescription:@"should find documents"]; + [collection findWhere:@{} + completion:^(NSArray *documents, NSError *error) { + XCTAssertEqual(documents.count, 2U); + XCTAssertTrue([documents[0][@"name"] isEqualToString:@"fido"]); + XCTAssertNil(error); + [findExpectation1 fulfill]; + }]; + [self waitForExpectationsWithTimeout:60.0 handler:nil]; + + XCTestExpectation *deleteManyExpectation1 = [self expectationWithDescription:@"should delete many documents"]; + [collection deleteManyDocumentsWhere:@{@"name" : @"rex"} + completion:^(NSInteger count, NSError *error) { + XCTAssertEqual(count, 0U); + XCTAssertNil(error); + [deleteManyExpectation1 fulfill]; + }]; + [self waitForExpectationsWithTimeout:60.0 handler:nil]; + + XCTestExpectation *deleteManyExpectation2 = [self expectationWithDescription:@"should delete many documents"]; + [collection deleteManyDocumentsWhere:@{@"breed" : @"cane corso"} + completion:^(NSInteger count, NSError *error) { + XCTAssertEqual(count, 1); + XCTAssertNil(error); + [deleteManyExpectation2 fulfill]; + }]; + [self waitForExpectationsWithTimeout:60.0 handler:nil]; + + XCTestExpectation *findOneAndDeleteExpectation1 = [self expectationWithDescription:@"should find one and delete"]; + [collection findOneAndDeleteWhere:@{@"name": @"john"} + completion:^(NSDictionary> *document, NSError *error) { + XCTAssertNotNil(document); + NSString *name = (NSString *)document[@"name"]; + XCTAssertTrue([name isEqualToString:@"john"]); + XCTAssertNil(error); + [findOneAndDeleteExpectation1 fulfill]; + }]; + [self waitForExpectationsWithTimeout:60.0 handler:nil]; + + XCTestExpectation *findOneAndDeleteExpectation2 = [self expectationWithDescription:@"should find one and delete"]; + NSDictionary> *projection = @{@"name": @1, @"breed": @1}; + NSArray> *sortDescriptors = @[@{@"_id": @1}, @{@"breed": @1}]; + RLMFindOneAndModifyOptions *findOneAndModifyOptions = [[RLMFindOneAndModifyOptions alloc] + initWithProjection:projection + sorting:sortDescriptors + upsert:YES + shouldReturnNewDocument:YES]; + + [collection findOneAndDeleteWhere:@{@"name": @"john"} + options:findOneAndModifyOptions + completion:^(NSDictionary> *document, NSError *error) { + XCTAssertNil(document); + // FIXME: when a projection is used, the server reports the error + // "expected pre-image to match projection matcher" when there are no + // matches, rather than simply doing nothing like when there is no projection +// XCTAssertNil(error); + (void)error; + [findOneAndDeleteExpectation2 fulfill]; + }]; + [self waitForExpectationsWithTimeout:60.0 handler:nil]; +} + +#pragma mark - Watch + +- (void)testWatch { + [self performWatchTest:nil]; +} + +- (void)testWatchAsync { + auto asyncQueue = dispatch_queue_create("io.realm.watchQueue", DISPATCH_QUEUE_CONCURRENT); + [self performWatchTest:asyncQueue]; +} + +- (void)performWatchTest:(nullable dispatch_queue_t)delegateQueue { + XCTestExpectation *expectation = [self expectationWithDescription:@"watch collection and receive change event 3 times"]; + + RLMMongoCollection *collection = [self.anonymousUser collectionForType:Dog.class app:self.app]; + + RLMWatchTestUtility *testUtility = + [[RLMWatchTestUtility alloc] initWithChangeEventCount:3 + expectation:expectation]; + + RLMChangeStream *changeStream = [collection watchWithDelegate:testUtility delegateQueue:delegateQueue]; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + WAIT_FOR_SEMAPHORE(testUtility.isOpenSemaphore, 30.0); + for (int i = 0; i < 3; i++) { + [collection insertOneDocument:@{@"name": @"fido"} completion:^(id objectId, NSError *error) { + XCTAssertNil(error); + XCTAssertNotNil(objectId); + }]; + WAIT_FOR_SEMAPHORE(testUtility.semaphore, 30.0); + } + [changeStream close]; + }); + + [self waitForExpectations:@[expectation] timeout:60.0]; +} + +- (void)testWatchWithMatchFilter { + [self performWatchWithMatchFilterTest:nil]; +} + +- (void)testWatchWithMatchFilterAsync { + auto asyncQueue = dispatch_queue_create("io.realm.watchQueue", DISPATCH_QUEUE_CONCURRENT); + [self performWatchWithMatchFilterTest:asyncQueue]; +} + +- (NSArray *)prepareDogDocumentsIn:(RLMMongoCollection *)collection { + __block NSArray *objectIds; + XCTestExpectation *ex = [self expectationWithDescription:@"delete existing documents"]; + [collection deleteManyDocumentsWhere:@{} completion:^(NSInteger, NSError *error) { + XCTAssertNil(error); + [ex fulfill]; + }]; + [self waitForExpectations:@[ex] timeout:60.0]; + + XCTestExpectation *insertManyExpectation = [self expectationWithDescription:@"should insert documents"]; + [collection insertManyDocuments:@[ + @{@"name": @"fido", @"breed": @"cane corso"}, + @{@"name": @"rex", @"breed": @"tibetan mastiff"}, + @{@"name": @"john", @"breed": @"tibetan mastiff"}] + completion:^(NSArray> *ids, NSError *error) { + XCTAssertEqual(ids.count, 3U); + for (id objectId in ids) { + XCTAssertEqual(objectId.bsonType, RLMBSONTypeObjectId); + } + XCTAssertNil(error); + objectIds = (NSArray *)ids; + [insertManyExpectation fulfill]; + }]; + [self waitForExpectations:@[insertManyExpectation] timeout:60.0]; + return objectIds; +} + +- (void)performWatchWithMatchFilterTest:(nullable dispatch_queue_t)delegateQueue { + RLMMongoCollection *collection = [self.anonymousUser collectionForType:Dog.class app:self.app]; + NSArray *objectIds = [self prepareDogDocumentsIn:collection]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"watch collection and receive change event 3 times"]; + + RLMWatchTestUtility *testUtility = + [[RLMWatchTestUtility alloc] initWithChangeEventCount:3 + matchingObjectId:objectIds[0] + expectation:expectation]; + + RLMChangeStream *changeStream = [collection watchWithMatchFilter:@{@"fullDocument._id": objectIds[0]} + delegate:testUtility + delegateQueue:delegateQueue]; + + dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + WAIT_FOR_SEMAPHORE(testUtility.isOpenSemaphore, 30.0); + for (int i = 0; i < 3; i++) { + [collection updateOneDocumentWhere:@{@"_id": objectIds[0]} + updateDocument:@{@"breed": @"king charles", @"name": [NSString stringWithFormat:@"fido-%d", i]} + completion:^(RLMUpdateResult *, NSError *error) { + XCTAssertNil(error); + }]; + + [collection updateOneDocumentWhere:@{@"_id": objectIds[1]} + updateDocument:@{@"breed": @"french bulldog", @"name": [NSString stringWithFormat:@"fido-%d", i]} + completion:^(RLMUpdateResult *, NSError *error) { + XCTAssertNil(error); + }]; + WAIT_FOR_SEMAPHORE(testUtility.semaphore, 30.0); + } + [changeStream close]; + }); + [self waitForExpectations:@[expectation] timeout:60.0]; +} + +- (void)testWatchWithFilterIds { + [self performWatchWithFilterIdsTest:nil]; +} + +- (void)testWatchWithFilterIdsAsync { + auto asyncQueue = dispatch_queue_create("io.realm.watchQueue", DISPATCH_QUEUE_CONCURRENT); + [self performWatchWithFilterIdsTest:asyncQueue]; +} + +- (void)performWatchWithFilterIdsTest:(nullable dispatch_queue_t)delegateQueue { + RLMMongoCollection *collection = [self.anonymousUser collectionForType:Dog.class app:self.app]; + NSArray *objectIds = [self prepareDogDocumentsIn:collection]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"watch collection and receive change event 3 times"]; + + RLMWatchTestUtility *testUtility = + [[RLMWatchTestUtility alloc] initWithChangeEventCount:3 + matchingObjectId:objectIds[0] + expectation:expectation]; + + RLMChangeStream *changeStream = [collection watchWithFilterIds:@[objectIds[0]] + delegate:testUtility + delegateQueue:delegateQueue]; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + WAIT_FOR_SEMAPHORE(testUtility.isOpenSemaphore, 30.0); + for (int i = 0; i < 3; i++) { + [collection updateOneDocumentWhere:@{@"_id": objectIds[0]} + updateDocument:@{@"breed": @"king charles", @"name": [NSString stringWithFormat:@"fido-%d", i]} + completion:^(RLMUpdateResult *, NSError *error) { + XCTAssertNil(error); + }]; + + [collection updateOneDocumentWhere:@{@"_id": objectIds[1]} + updateDocument:@{@"breed": @"french bulldog", @"name": [NSString stringWithFormat:@"fido-%d", i]} + completion:^(RLMUpdateResult *, NSError *error) { + XCTAssertNil(error); + }]; + WAIT_FOR_SEMAPHORE(testUtility.semaphore, 30.0); + } + [changeStream close]; + }); + + [self waitForExpectations:@[expectation] timeout:60.0]; +} + +- (void)testMultipleWatchStreams { + auto asyncQueue = dispatch_queue_create("io.realm.watchQueue", DISPATCH_QUEUE_CONCURRENT); + [self performMultipleWatchStreamsTest:asyncQueue]; +} + +- (void)testMultipleWatchStreamsAsync { + [self performMultipleWatchStreamsTest:nil]; +} + +- (void)performMultipleWatchStreamsTest:(nullable dispatch_queue_t)delegateQueue { + RLMMongoCollection *collection = [self.anonymousUser collectionForType:Dog.class app:self.app]; + NSArray *objectIds = [self prepareDogDocumentsIn:collection]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"watch collection and receive change event 3 times"]; + expectation.expectedFulfillmentCount = 2; + + RLMWatchTestUtility *testUtility1 = + [[RLMWatchTestUtility alloc] initWithChangeEventCount:3 + matchingObjectId:objectIds[0] + expectation:expectation]; + + RLMWatchTestUtility *testUtility2 = + [[RLMWatchTestUtility alloc] initWithChangeEventCount:3 + matchingObjectId:objectIds[1] + expectation:expectation]; + + RLMChangeStream *changeStream1 = [collection watchWithFilterIds:@[objectIds[0]] + delegate:testUtility1 + delegateQueue:delegateQueue]; + + RLMChangeStream *changeStream2 = [collection watchWithFilterIds:@[objectIds[1]] + delegate:testUtility2 + delegateQueue:delegateQueue]; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + WAIT_FOR_SEMAPHORE(testUtility1.isOpenSemaphore, 30.0); + WAIT_FOR_SEMAPHORE(testUtility2.isOpenSemaphore, 30.0); + for (int i = 0; i < 3; i++) { + [collection updateOneDocumentWhere:@{@"_id": objectIds[0]} + updateDocument:@{@"breed": @"king charles", @"name": [NSString stringWithFormat:@"fido-%d", i]} + completion:^(RLMUpdateResult *, NSError *error) { + XCTAssertNil(error); + }]; + + [collection updateOneDocumentWhere:@{@"_id": objectIds[1]} + updateDocument:@{@"breed": @"french bulldog", @"name": [NSString stringWithFormat:@"fido-%d", i]} + completion:^(RLMUpdateResult *, NSError *error) { + XCTAssertNil(error); + }]; + + [collection updateOneDocumentWhere:@{@"_id": objectIds[2]} + updateDocument:@{@"breed": @"german shepard", @"name": [NSString stringWithFormat:@"fido-%d", i]} + completion:^(RLMUpdateResult *, NSError *error) { + XCTAssertNil(error); + }]; + WAIT_FOR_SEMAPHORE(testUtility1.semaphore, 30.0); + WAIT_FOR_SEMAPHORE(testUtility2.semaphore, 30.0); + } + [changeStream1 close]; + [changeStream2 close]; + }); + + [self waitForExpectations:@[expectation] timeout:60.0]; +} + +#pragma mark - File paths + +static NSString *newPathForPartitionValue(RLMUser *user, id partitionValue) { + std::stringstream s; + s << RLMConvertRLMBSONToBson(partitionValue); + // Intentionally not passing the correct partition value here as we (accidentally?) + // don't use the filename generated from the partition value + realm::SyncConfig config(user._syncUser, "null"); + return @(user._syncUser->sync_manager()->path_for_realm(config, s.str()).c_str()); +} + +- (void)testSyncFilePaths { + RLMUser *user = self.anonymousUser; + auto configuration = [user configurationWithPartitionValue:@"abc"]; + XCTAssertTrue([configuration.fileURL.path + hasSuffix:([NSString stringWithFormat:@"mongodb-realm/%@/%@/%%22abc%%22.realm", + self.appId, user.identifier])]); + configuration = [user configurationWithPartitionValue:@123]; + XCTAssertTrue([configuration.fileURL.path + hasSuffix:([NSString stringWithFormat:@"mongodb-realm/%@/%@/%@.realm", + self.appId, user.identifier, @"%7B%22%24numberInt%22%3A%22123%22%7D"])]); + configuration = [user configurationWithPartitionValue:nil]; + XCTAssertTrue([configuration.fileURL.path + hasSuffix:([NSString stringWithFormat:@"mongodb-realm/%@/%@/null.realm", + self.appId, user.identifier])]); + + XCTAssertEqualObjects([user configurationWithPartitionValue:@"abc"].fileURL.path, + newPathForPartitionValue(user, @"abc")); + XCTAssertEqualObjects([user configurationWithPartitionValue:@123].fileURL.path, + newPathForPartitionValue(user, @123)); + XCTAssertEqualObjects([user configurationWithPartitionValue:nil].fileURL.path, + newPathForPartitionValue(user, nil)); +} + +static NSString *oldPathForPartitionValue(RLMUser *user, NSString *oldName) { + realm::SyncConfig config(user._syncUser, "null"); + return [NSString stringWithFormat:@"%@/%s%@.realm", + [@(user._syncUser->sync_manager()->path_for_realm(config).c_str()) stringByDeletingLastPathComponent], + user._syncUser->identity().c_str(), oldName]; +} + +- (void)testLegacyFilePathsAreUsedIfFilesArePresent { + RLMUser *user = self.anonymousUser; + + auto testPartitionValue = [&](id partitionValue, NSString *oldName) { + NSURL *url = [NSURL fileURLWithPath:oldPathForPartitionValue(user, oldName)]; + @autoreleasepool { + auto configuration = [user configurationWithPartitionValue:partitionValue]; + configuration.fileURL = url; + configuration.objectClasses = @[Person.class]; + RLMRealm *realm = [RLMRealm realmWithConfiguration:configuration error:nil]; + [realm beginWriteTransaction]; + [Person createInRealm:realm withValue:[Person george]]; + [realm commitWriteTransaction]; + } + + auto configuration = [user configurationWithPartitionValue:partitionValue]; + configuration.objectClasses = @[Person.class]; + XCTAssertEqualObjects(configuration.fileURL, url); + RLMRealm *realm = [RLMRealm realmWithConfiguration:configuration error:nil]; + XCTAssertEqual([Person allObjectsInRealm:realm].count, 1U); + }; + + testPartitionValue(@"abc", @"%2F%2522abc%2522"); + testPartitionValue(@123, @"%2F%257B%2522%24numberInt%2522%253A%2522123%2522%257D"); + testPartitionValue(nil, @"%2Fnull"); +} +@end + +#endif // TARGET_OS_OSX diff --git a/Realm/ObjectServerTests/RLMObjectServerPartitionTests.mm b/Realm/ObjectServerTests/RLMObjectServerPartitionTests.mm index d6c9a185f3..61978c2053 100644 --- a/Realm/ObjectServerTests/RLMObjectServerPartitionTests.mm +++ b/Realm/ObjectServerTests/RLMObjectServerPartitionTests.mm @@ -29,32 +29,26 @@ @interface RLMObjectServerPartitionTests : RLMSyncTestCase @implementation RLMObjectServerPartitionTests -- (void)roundTripForPartitionValue:(id)value testName:(SEL)callerName { +- (void)roundTripForPartitionValue:(id)value { NSError *error; - NSString *appId = [RealmServer.shared createAppForBSONType:[self partitionBsonType:value] error:&error]; - + NSString *appId = [RealmServer.shared + createAppWithPartitionKeyType:[self partitionBsonType:value] + types:@[Person.self] persistent:false error:&error]; if (error) { - XCTFail(@"Could not create app for partition value %@d", value); - return; + return XCTFail(@"Could not create app for partition value %@d", value); } - NSString *name = NSStringFromSelector(callerName); RLMApp *app = [self appWithId:appId]; - RLMCredentials *credentials = [self basicCredentialsWithName:name register:YES app:app]; - RLMUser *user = [self logInUserForCredentials:credentials app:app]; + RLMUser *user = [self createUserForApp:app]; RLMRealm *realm = [self openRealmForPartitionValue:value user:user]; CHECK_COUNT(0, Person, realm); - RLMCredentials *writeCredentials = [self basicCredentialsWithName:[name stringByAppendingString:@"Writer"] - register:YES app:app]; - RLMUser *writeUser = [self logInUserForCredentials:writeCredentials app:app]; + RLMUser *writeUser = [self createUserForApp:app]; RLMRealm *writeRealm = [self openRealmForPartitionValue:value user:writeUser]; - auto write = [&]() { + auto write = [&] { [self addPersonsToRealm:writeRealm - persons:@[[Person john], - [Person paul], - [Person ringo]]]; + persons:@[[Person john], [Person paul], [Person ringo]]]; [self waitForUploadsForRealm:writeRealm]; [self waitForDownloadsForRealm:realm]; }; @@ -66,29 +60,22 @@ - (void)roundTripForPartitionValue:(id)value testName:(SEL)callerName { write(); CHECK_COUNT(6, Person, realm); XCTAssertEqual([Person objectsInRealm:realm where:@"firstName = 'John'"].count, 2UL); - - [RealmServer.shared deleteApp:appId error:&error]; - XCTAssertNil(error); } - (void)testRoundTripForObjectIdPartitionValue { - [self roundTripForPartitionValue:[[RLMObjectId alloc] initWithString:@"1234567890ab1234567890ab" error:nil] - testName:_cmd]; + [self roundTripForPartitionValue:[[RLMObjectId alloc] initWithString:@"1234567890ab1234567890ab" error:nil]]; } - (void)testRoundTripForUUIDPartitionValue { - [self roundTripForPartitionValue:[[NSUUID alloc] initWithUUIDString:@"85d4fbee-6ec6-47df-bfa1-615931903d7e"] - testName:_cmd]; + [self roundTripForPartitionValue:[[NSUUID alloc] initWithUUIDString:@"85d4fbee-6ec6-47df-bfa1-615931903d7e"]]; } - (void)testRoundTripForStringPartitionValue { - [self roundTripForPartitionValue:@"1234567890ab1234567890ab" - testName:_cmd]; + [self roundTripForPartitionValue:@"1234567890ab1234567890ab"]; } - (void)testRoundTripForIntPartitionValue { - [self roundTripForPartitionValue:@1234567890 - testName:_cmd]; + [self roundTripForPartitionValue:@1234567890]; } @end diff --git a/Realm/ObjectServerTests/RLMObjectServerTests.mm b/Realm/ObjectServerTests/RLMObjectServerTests.mm index 55ae06dfc5..997699123e 100644 --- a/Realm/ObjectServerTests/RLMObjectServerTests.mm +++ b/Realm/ObjectServerTests/RLMObjectServerTests.mm @@ -52,21 +52,22 @@ - (void)stop; @property (nonatomic) double delay; @end -@interface RLMUser (Test) -@end -@implementation RLMUser (Test) -- (RLMRealmConfiguration *)configurationWithTestSelector:(SEL)sel { - auto config = [self configurationWithPartitionValue:NSStringFromSelector(sel)]; - config.objectClasses = @[Person.class, HugeSyncObject.class]; - return config; -} - -@end - @interface RLMObjectServerTests : RLMSyncTestCase @end @implementation RLMObjectServerTests +- (NSArray *)defaultObjectTypes { + return @[ + AllTypesSyncObject.class, + HugeSyncObject.class, + IntPrimaryKeyObject.class, + Person.class, + RLMSetSyncObject.class, + StringPrimaryKeyObject.class, + UUIDPrimaryKeyObject.class, + ]; +} + #pragma mark - App Tests static NSString *generateRandomString(int num) { @@ -119,10 +120,8 @@ - (void)testLogoutCurrentUser { } - (void)testLogoutSpecificUser { - RLMUser *firstUser = [self logInUserForCredentials:[self basicCredentialsWithName:NSStringFromSelector(_cmd) - register:YES]]; - RLMUser *secondUser = [self logInUserForCredentials:[self basicCredentialsWithName:@"test1@10gen.com" - register:YES]]; + RLMUser *firstUser = [self createUser]; + RLMUser *secondUser = [self createUser]; XCTAssertEqualObjects(self.app.currentUser.identifier, secondUser.identifier); // `[app currentUser]` will now be `secondUser`, so let's logout firstUser and ensure @@ -139,22 +138,18 @@ - (void)testLogoutSpecificUser { } - (void)testSwitchUser { - RLMApp *app = self.app; - - RLMUser *syncUserA = [self anonymousUser]; - RLMUser *syncUserB = [self userForTest:_cmd]; + RLMUser *syncUserA = [self createUser]; + RLMUser *syncUserB = [self createUser]; XCTAssertNotEqualObjects(syncUserA.identifier, syncUserB.identifier); - XCTAssertEqualObjects(app.currentUser.identifier, syncUserB.identifier); + XCTAssertEqualObjects(self.app.currentUser.identifier, syncUserB.identifier); - XCTAssertEqualObjects([app switchToUser:syncUserA].identifier, syncUserA.identifier); + XCTAssertEqualObjects([self.app switchToUser:syncUserA].identifier, syncUserA.identifier); } - (void)testRemoveUser { - RLMUser *firstUser = [self logInUserForCredentials:[self basicCredentialsWithName:NSStringFromSelector(_cmd) - register:YES]]; - RLMUser *secondUser = [self logInUserForCredentials:[self basicCredentialsWithName:@"test2@10gen.com" - register:YES]]; + RLMUser *firstUser = [self createUser]; + RLMUser *secondUser = [self createUser]; XCTAssert([self.app.currentUser.identifier isEqualToString:secondUser.identifier]); @@ -171,10 +166,8 @@ - (void)testRemoveUser { } - (void)testDeleteUser { - [self logInUserForCredentials:[self basicCredentialsWithName:NSStringFromSelector(_cmd) - register:YES]]; - RLMUser *secondUser = [self logInUserForCredentials:[self basicCredentialsWithName:@"test3@10gen.com" - register:YES]]; + [self createUser]; + RLMUser *secondUser = [self createUser]; XCTAssert([self.app.currentUser.identifier isEqualToString:secondUser.identifier]); @@ -485,19 +478,17 @@ - (void)testFunctionCredential { /// Valid email/password credentials should be able to log in a user. Using the same credentials should return the /// same user object. - (void)testEmailPasswordAuthentication { - RLMUser *firstUser = [self logInUserForCredentials:[self basicCredentialsWithName:NSStringFromSelector(_cmd) - register:YES]]; - RLMUser *secondUser = [self logInUserForCredentials:[self basicCredentialsWithName:NSStringFromSelector(_cmd) - register:NO]]; + RLMCredentials *credentials = [self basicCredentialsWithName:self.name register:YES]; + RLMUser *firstUser = [self logInUserForCredentials:credentials]; + RLMUser *secondUser = [self logInUserForCredentials:credentials]; // Two users created with the same credential should resolve to the same actual user. XCTAssertTrue([firstUser.identifier isEqualToString:secondUser.identifier]); } /// An invalid email/password credential should not be able to log in a user and a corresponding error should be generated. - (void)testInvalidPasswordAuthentication { - (void)[self userForTest:_cmd]; - - RLMCredentials *credentials = [RLMCredentials credentialsWithEmail:NSStringFromSelector(_cmd) + (void)[self basicCredentialsWithName:self.name register:YES]; + RLMCredentials *credentials = [RLMCredentials credentialsWithEmail:self.name password:@"INVALID_PASSWORD"]; XCTestExpectation *expectation = [self expectationWithDescription:@"login should fail"]; @@ -532,18 +523,18 @@ - (void)testNonExistingEmailAuthentication { /// Registering a user with existing email should return corresponding error. - (void)testExistingEmailRegistration { XCTestExpectation *expectationA = [self expectationWithDescription:@"registration should succeed"]; - [[self.app emailPasswordAuth] registerUserWithEmail:NSStringFromSelector(_cmd) - password:@"password" - completion:^(NSError *error) { + [self.app.emailPasswordAuth registerUserWithEmail:self.name + password:@"password" + completion:^(NSError *error) { XCTAssertNil(error); [expectationA fulfill]; }]; [self waitForExpectationsWithTimeout:2.0 handler:nil]; XCTestExpectation *expectationB = [self expectationWithDescription:@"registration should fail"]; - [[self.app emailPasswordAuth] registerUserWithEmail:NSStringFromSelector(_cmd) - password:@"password" - completion:^(NSError *error) { + [self.app.emailPasswordAuth registerUserWithEmail:self.name + password:@"password" + completion:^(NSError *error) { RLMValidateError(error, RLMAppErrorDomain, RLMAppErrorAccountNameInUse, @"name already in use"); XCTAssertNotNil(error.userInfo[RLMServerLogURLKey]); [expectationB fulfill]; @@ -553,24 +544,17 @@ - (void)testExistingEmailRegistration { } - (void)testSyncErrorHandlerErrorDomain { - RLMUser *user = [self userForTest:_cmd]; - XCTAssertNotNil(user); - + RLMRealmConfiguration *config = self.configuration; XCTestExpectation *expectation = [self expectationWithDescription:@"should fail after setting bad token"]; - self.app.syncManager.errorHandler = ^(NSError *error, __unused RLMSyncSession *session) { + self.app.syncManager.errorHandler = ^(NSError *error, RLMSyncSession *) { RLMValidateError(error, RLMSyncErrorDomain, RLMSyncErrorClientUserError, @"Unable to refresh the user access token: signature is invalid"); [expectation fulfill]; }; - [self setInvalidTokensForUser:user]; - - [self immediatelyOpenRealmForPartitionValue:NSStringFromSelector(_cmd) - user:user - encryptionKey:nil - stopPolicy:RLMSyncStopPolicyAfterChangesUploaded]; - - [self waitForExpectationsWithTimeout:30.0 handler:nil]; + [self setInvalidTokensForUser:config.syncConfiguration.user]; + [RLMRealm realmWithConfiguration:config error:nil]; + [self waitForExpectations:@[expectation] timeout:3.0]; } #pragma mark - User Profile @@ -615,18 +599,18 @@ - (void)testUserProfileInitialization { /// It should be possible to successfully open a Realm configured for sync with a normal user. - (void)testOpenRealmWithNormalCredentials { - RLMRealm *realm = [self realmForTest:_cmd]; + RLMRealm *realm = [self openRealm]; XCTAssertTrue(realm.isEmpty); } /// If client B adds objects to a synced Realm, client A should see those objects. - (void)testAddObjects { - RLMRealm *realm = [self realmForTest:_cmd]; + RLMRealm *realm = [self openRealm]; NSDictionary *values = [AllTypesSyncObject values:1]; CHECK_COUNT(0, Person, realm); CHECK_COUNT(0, AllTypesSyncObject, realm); - [self writeToPartition:_cmd block:^(RLMRealm *realm) { + [self writeToPartition:self.name block:^(RLMRealm *realm) { [realm addObjects:@[[Person john], [Person paul], [Person george]]]; AllTypesSyncObject *obj = [[AllTypesSyncObject alloc] initWithValue:values]; obj.objectCol = [Person ringo]; @@ -654,14 +638,8 @@ - (void)testAddObjects { - (void)testAddObjectsWithNilPartitionValue { RLMRealm *realm = [self openRealmForPartitionValue:nil user:self.anonymousUser]; - // This test needs the database to be empty of any documents with a nil partition - [realm transactionWithBlock:^{ - [realm deleteAllObjects]; - }]; - [self waitForUploadsForRealm:realm]; - CHECK_COUNT(0, Person, realm); - [self writeToPartition:nil userName:NSStringFromSelector(_cmd) block:^(RLMRealm *realm) { + [self writeToPartition:nil block:^(RLMRealm *realm) { [realm addObjects:@[[Person john], [Person paul], [Person george], [Person ringo]]]; }]; [self waitForDownloadsForRealm:realm]; @@ -669,14 +647,14 @@ - (void)testAddObjectsWithNilPartitionValue { } - (void)testRountripForDistinctPrimaryKey { - RLMRealm *realm = [self realmForTest:_cmd]; + RLMRealm *realm = [self openRealm]; CHECK_COUNT(0, Person, realm); CHECK_COUNT(0, UUIDPrimaryKeyObject, realm); CHECK_COUNT(0, StringPrimaryKeyObject, realm); CHECK_COUNT(0, IntPrimaryKeyObject, realm); - [self writeToPartition:_cmd block:^(RLMRealm *realm) { + [self writeToPartition:self.name block:^(RLMRealm *realm) { Person *person = [[Person alloc] initWithPrimaryKey:[[RLMObjectId alloc] initWithString:@"1234567890ab1234567890ab" error:nil] age:5 firstName:@"Ringo" @@ -719,125 +697,102 @@ - (void)testRountripForDistinctPrimaryKey { XCTAssertEqual(intPrimaryKeyObject.intCol, 30); } -/// If client B adds objects to a synced Realm, client A should see those objects. - (void)testAddObjectsMultipleApps { - NSString *appId1; - NSString *appId2; - if (self.isParent) { - appId1 = [RealmServer.shared createAppAndReturnError:nil]; - appId2 = [RealmServer.shared createAppAndReturnError:nil]; - - } else { - appId1 = self.appIds[0]; - appId2 = self.appIds[1]; - } - + NSString *appId1 = [RealmServer.shared createAppWithPartitionKeyType:@"string" types:@[Person.self] persistent:false error:nil]; + NSString *appId2 = [RealmServer.shared createAppWithPartitionKeyType:@"string" types:@[Person.self] persistent:false error:nil]; RLMApp *app1 = [self appWithId:appId1]; RLMApp *app2 = [self appWithId:appId2]; - [self logInUserForCredentials:[RLMCredentials anonymousCredentials] app:app1]; - [self logInUserForCredentials:[RLMCredentials anonymousCredentials] app:app2]; - RLMRealm *realm1 = [self openRealmForPartitionValue:appId1 user:app1.currentUser]; - RLMRealm *realm2 = [self openRealmForPartitionValue:appId2 user:app2.currentUser]; + auto openRealm = [=](RLMApp *app) { + RLMUser *user = [self createUserForApp:app]; + RLMRealmConfiguration *config = [user configurationWithPartitionValue:self.name]; + config.objectClasses = @[Person.self]; + return [self openRealmWithConfiguration:config]; + }; - if (self.isParent) { - CHECK_COUNT(0, Person, realm1); - CHECK_COUNT(0, Person, realm2); - int code = [self runChildAndWaitWithAppIds:@[appId1, appId2]]; - XCTAssertEqual(0, code); - [self waitForDownloadsForRealm:realm1]; - [self waitForDownloadsForRealm:realm2]; - CHECK_COUNT(2, Person, realm1); - CHECK_COUNT(2, Person, realm2); - XCTAssertEqual([Person objectsInRealm:realm1 where:@"firstName = 'John'"].count, 1UL); - XCTAssertEqual([Person objectsInRealm:realm1 where:@"firstName = 'Paul'"].count, 1UL); - XCTAssertEqual([Person objectsInRealm:realm1 where:@"firstName = 'Ringo'"].count, 0UL); - XCTAssertEqual([Person objectsInRealm:realm1 where:@"firstName = 'George'"].count, 0UL); - - XCTAssertEqual([Person objectsInRealm:realm2 where:@"firstName = 'John'"].count, 0UL); - XCTAssertEqual([Person objectsInRealm:realm2 where:@"firstName = 'Paul'"].count, 0UL); - XCTAssertEqual([Person objectsInRealm:realm2 where:@"firstName = 'Ringo'"].count, 1UL); - XCTAssertEqual([Person objectsInRealm:realm2 where:@"firstName = 'George'"].count, 1UL); - - [RealmServer.shared deleteApp:appId1 error:nil]; - [RealmServer.shared deleteApp:appId2 error:nil]; - } else { - // Add objects. - [self addPersonsToRealm:realm1 - persons:@[[Person john], - [Person paul]]]; - [self addPersonsToRealm:realm2 - persons:@[[Person ringo], - [Person george]]]; + RLMRealm *realm1 = openRealm(app1); + RLMRealm *realm2 = openRealm(app2); - [self waitForUploadsForRealm:realm1]; - [self waitForUploadsForRealm:realm2]; - CHECK_COUNT(2, Person, realm1); - CHECK_COUNT(2, Person, realm2); - } -} + CHECK_COUNT(0, Person, realm1); + CHECK_COUNT(0, Person, realm2); -/// If client B adds objects to a synced Realm, client A should see those objects. -- (void)testSessionRefresh { - RLMUser *user = [self logInUserForCredentials:[self basicCredentialsWithName:NSStringFromSelector(_cmd) register:self.isParent]]; - RLMUser *user2 = [self logInUserForCredentials:[self basicCredentialsWithName:@"lmao@10gen.com" register:self.isParent]]; + @autoreleasepool { + RLMRealm *realm = openRealm(app1); + [self addPersonsToRealm:realm + persons:@[[Person john], [Person paul]]]; + [self waitForUploadsForRealm:realm]; + } - [user _syncUser]->update_access_token(self.badAccessToken.UTF8String); + // realm2 should not see realm1's objcets despite being the same partition + // as they're from different apps + [self waitForDownloadsForRealm:realm1]; + [self waitForDownloadsForRealm:realm2]; + CHECK_COUNT(2, Person, realm1); + CHECK_COUNT(0, Person, realm2); - NSString *realmId = NSStringFromSelector(_cmd); - RLMRealm *realm = [self openRealmForPartitionValue:realmId user:user]; - RLMRealm *realm2 = [self openRealmForPartitionValue:realmId user:user2]; - if (self.isParent) { - CHECK_COUNT(0, Person, realm); - RLMRunChildAndWait(); - [self waitForDownloadsForUser:user - realms:@[realm] - partitionValues:@[realmId] expectedCounts:@[@4]]; - [self waitForDownloadsForUser:user2 - realms:@[realm2] - partitionValues:@[realmId] expectedCounts:@[@4]]; - } else { - // Add objects. + @autoreleasepool { + RLMRealm *realm = openRealm(app2); [self addPersonsToRealm:realm - persons:@[[Person john], - [Person paul], - [Person ringo], - [Person george]]]; + persons:@[[Person ringo], [Person george]]]; [self waitForUploadsForRealm:realm]; } + + [self waitForDownloadsForRealm:realm1]; + [self waitForDownloadsForRealm:realm2]; + CHECK_COUNT(2, Person, realm1); + CHECK_COUNT(2, Person, realm2); + + XCTAssertEqual([Person objectsInRealm:realm1 where:@"firstName = 'John'"].count, 1UL); + XCTAssertEqual([Person objectsInRealm:realm1 where:@"firstName = 'Paul'"].count, 1UL); + XCTAssertEqual([Person objectsInRealm:realm1 where:@"firstName = 'Ringo'"].count, 0UL); + XCTAssertEqual([Person objectsInRealm:realm1 where:@"firstName = 'George'"].count, 0UL); + + XCTAssertEqual([Person objectsInRealm:realm2 where:@"firstName = 'John'"].count, 0UL); + XCTAssertEqual([Person objectsInRealm:realm2 where:@"firstName = 'Paul'"].count, 0UL); + XCTAssertEqual([Person objectsInRealm:realm2 where:@"firstName = 'Ringo'"].count, 1UL); + XCTAssertEqual([Person objectsInRealm:realm2 where:@"firstName = 'George'"].count, 1UL); +} + +- (void)testSessionRefresh { + RLMUser *user = [self createUser]; + + // Should result in an access token error followed by a refresh when we + // open the Realm which is entirely transparent to the user + user._syncUser->update_access_token(self.badAccessToken.UTF8String); + RLMRealm *realm = [self openRealmForPartitionValue:self.name user:user]; + + RLMRealm *realm2 = [self openRealm]; + [self addPersonsToRealm:realm2 + persons:@[[Person john], + [Person paul], + [Person ringo], + [Person george]]]; + [self waitForUploadsForRealm:realm2]; + [self waitForDownloadsForRealm:realm]; + CHECK_COUNT(4, Person, realm); } -/// If client B deletes objects from a synced Realm, client A should see the effects of that deletion. - (void)testDeleteObjects { - RLMUser *user = [self userForTest:_cmd]; - RLMRealm *realm = [self openRealmForPartitionValue:NSStringFromSelector(_cmd) user:user]; - if (self.isParent) { - // Add objects. - [self addPersonsToRealm:realm persons:@[[Person john]]]; - [self waitForUploadsForRealm:realm]; - CHECK_COUNT(1, Person, realm); - RLMRunChildAndWait(); - [self waitForDownloadsForRealm:realm]; - CHECK_COUNT(0, Person, realm); - } else { - CHECK_COUNT(1, Person, realm); - [realm beginWriteTransaction]; - [realm deleteAllObjects]; - [realm commitWriteTransaction]; - [self waitForUploadsForRealm:realm]; - CHECK_COUNT(0, Person, realm); - } + RLMRealm *realm1 = [self openRealm]; + [self addPersonsToRealm:realm1 persons:@[[Person john]]]; + [self waitForUploadsForRealm:realm1]; + CHECK_COUNT(1, Person, realm1); + + RLMRealm *realm2 = [self openRealm]; + CHECK_COUNT(1, Person, realm2); + [realm2 beginWriteTransaction]; + [realm2 deleteAllObjects]; + [realm2 commitWriteTransaction]; + [self waitForUploadsForRealm:realm2]; + + [self waitForDownloadsForRealm:realm1]; + CHECK_COUNT(0, Person, realm1); } - (void)testIncomingSyncWritesTriggerNotifications { - NSString *baseName = NSStringFromSelector(_cmd); - auto user = [&] { - NSString *name = [baseName stringByAppendingString:[NSUUID UUID].UUIDString]; - return [self logInUserForCredentials:[self basicCredentialsWithName:name register:YES]]; - }; - RLMRealm *syncRealm = [self openRealmWithConfiguration:[user() configurationWithTestSelector:_cmd]]; - RLMRealm *asyncRealm = [self asyncOpenRealmWithConfiguration:[user() configurationWithTestSelector:_cmd]]; - RLMRealm *writeRealm = [self asyncOpenRealmWithConfiguration:[user() configurationWithTestSelector:_cmd]]; + RLMRealm *syncRealm = [self openRealm]; + RLMRealm *asyncRealm = [self asyncOpenRealmWithConfiguration:self.configuration]; + RLMRealm *writeRealm = [self openRealm]; __block XCTestExpectation *ex = [self expectationWithDescription:@"got initial notification"]; ex.expectedFulfillmentCount = 2; @@ -858,14 +813,11 @@ - (void)testIncomingSyncWritesTriggerNotifications { [token2 invalidate]; } -#pragma mark - RLMValue Sync with missing schema - +#pragma mark - RLMValue Sync with missing schema - (void)testMissingSchema { @autoreleasepool { - auto c = [self.anonymousUser configurationWithPartitionValue:NSStringFromSelector(_cmd)]; - c.objectClasses = @[Person.self, AllTypesSyncObject.self, RLMSetSyncObject.self]; - RLMRealm *realm = [RLMRealm realmWithConfiguration:c error:nil]; - [self waitForDownloadsForRealm:realm]; + RLMRealm *realm = [self openRealm]; AllTypesSyncObject *obj = [[AllTypesSyncObject alloc] initWithValue:[AllTypesSyncObject values:0]]; RLMSetSyncObject *o = [RLMSetSyncObject new]; Person *p = [Person john]; @@ -879,12 +831,12 @@ - (void)testMissingSchema { CHECK_COUNT(1, AllTypesSyncObject, realm); } - RLMUser *user = [self userForTest:_cmd]; - auto c = [user configurationWithPartitionValue:NSStringFromSelector(_cmd)]; + RLMUser *user = [self createUser]; + auto c = [user configurationWithPartitionValue:self.name]; c.objectClasses = @[Person.self, AllTypesSyncObject.self]; RLMRealm *realm = [RLMRealm realmWithConfiguration:c error:nil]; [self waitForDownloadsForRealm:realm]; - RLMResults *res = [AllTypesSyncObject allObjectsInRealm:realm]; + RLMResults *res = [AllTypesSyncObject allObjectsInRealm:realm]; AllTypesSyncObject *o = res.firstObject; Person *p = o.objectCol; RLMSet *anySet = ((RLMObject *)o.anyCol)[@"anySet"]; @@ -903,7 +855,7 @@ - (void)testEncryptedSyncedRealm { RLMUser *user = [self userForTest:_cmd]; NSData *key = RLMGenerateKey(); - RLMRealm *realm = [self openRealmForPartitionValue:NSStringFromSelector(_cmd) + RLMRealm *realm = [self openRealmForPartitionValue:self.name user:user encryptionKey:key stopPolicy:RLMSyncStopPolicyAfterChangesUploaded]; @@ -911,12 +863,9 @@ - (void)testEncryptedSyncedRealm { if (self.isParent) { CHECK_COUNT(0, Person, realm); RLMRunChildAndWait(); - [self waitForDownloadsForUser:user - realms:@[realm] - partitionValues:@[NSStringFromSelector(_cmd)] - expectedCounts:@[@1]]; + [self waitForDownloadsForRealm:realm]; + CHECK_COUNT(1, Person, realm); } else { - // Add objects. [self addPersonsToRealm:realm persons:@[[Person john]]]; [self waitForUploadsForRealm:realm]; CHECK_COUNT(1, Person, realm); @@ -925,11 +874,11 @@ - (void)testEncryptedSyncedRealm { /// If an encrypted synced Realm is re-opened with the wrong key, throw an exception. - (void)testEncryptedSyncedRealmWrongKey { - RLMUser *user = [self userForTest:_cmd]; + RLMUser *user = [self createUser]; NSString *path; @autoreleasepool { - RLMRealm *realm = [self openRealmForPartitionValue:NSStringFromSelector(_cmd) + RLMRealm *realm = [self openRealmForPartitionValue:self.name user:user encryptionKey:RLMGenerateKey() stopPolicy:RLMSyncStopPolicyImmediately]; @@ -954,38 +903,33 @@ - (void)testEncryptedSyncedRealmWrongKey { /// If a client opens multiple Realms, there should be one session object for each Realm that was opened. - (void)testMultipleRealmsSessions { - NSString *partitionValueA = NSStringFromSelector(_cmd); + NSString *partitionValueA = self.name; NSString *partitionValueB = [partitionValueA stringByAppendingString:@"bar"]; NSString *partitionValueC = [partitionValueA stringByAppendingString:@"baz"]; - RLMUser *user = [self userForTest:_cmd]; + RLMUser *user = [self createUser]; - // Open three Realms. - - __attribute__((objc_precise_lifetime)) RLMRealm *realmealmA = [self openRealmForPartitionValue:partitionValueA - user:user]; - __attribute__((objc_precise_lifetime)) RLMRealm *realmealmB = [self openRealmForPartitionValue:partitionValueB - user:user]; - __attribute__((objc_precise_lifetime)) RLMRealm *realmealmC = [self openRealmForPartitionValue:partitionValueC - user:user]; + __attribute__((objc_precise_lifetime)) + RLMRealm *realmA = [self openRealmForPartitionValue:partitionValueA user:user]; + __attribute__((objc_precise_lifetime)) + RLMRealm *realmB = [self openRealmForPartitionValue:partitionValueB user:user]; + __attribute__((objc_precise_lifetime)) + RLMRealm *realmC = [self openRealmForPartitionValue:partitionValueC user:user]; // Make sure there are three active sessions for the user. - XCTAssert(user.allSessions.count == 3, @"Expected 3 sessions, but didn't get 3 sessions"); + XCTAssertEqual(user.allSessions.count, 3U); XCTAssertNotNil([user sessionForPartitionValue:partitionValueA], @"Expected to get a session for partition value A"); XCTAssertNotNil([user sessionForPartitionValue:partitionValueB], @"Expected to get a session for partition value B"); XCTAssertNotNil([user sessionForPartitionValue:partitionValueC], @"Expected to get a session for partition value C"); - XCTAssertTrue([user sessionForPartitionValue:partitionValueA].state == RLMSyncSessionStateActive, - @"Expected active session for URL A"); - XCTAssertTrue([user sessionForPartitionValue:partitionValueB].state == RLMSyncSessionStateActive, - @"Expected active session for URL B"); - XCTAssertTrue([user sessionForPartitionValue:partitionValueC].state == RLMSyncSessionStateActive, - @"Expected active session for URL C"); + XCTAssertEqual(realmA.syncSession.state, RLMSyncSessionStateActive); + XCTAssertEqual(realmB.syncSession.state, RLMSyncSessionStateActive); + XCTAssertEqual(realmC.syncSession.state, RLMSyncSessionStateActive); } /// A client should be able to open multiple Realms and add objects to each of them. - (void)testMultipleRealmsAddObjects { - NSString *partitionValueA = NSStringFromSelector(_cmd); + NSString *partitionValueA = self.name; NSString *partitionValueB = [partitionValueA stringByAppendingString:@"bar"]; NSString *partitionValueC = [partitionValueA stringByAppendingString:@"baz"]; RLMUser *user = [self userForTest:_cmd]; @@ -999,12 +943,12 @@ - (void)testMultipleRealmsAddObjects { CHECK_COUNT(0, Person, realmB); CHECK_COUNT(0, Person, realmC); RLMRunChildAndWait(); - [self waitForDownloadsForUser:user - realms:@[realmA, realmB, realmC] - partitionValues:@[partitionValueA, - partitionValueB, - partitionValueC] - expectedCounts:@[@3, @2, @5]]; + [self waitForDownloadsForRealm:realmA]; + [self waitForDownloadsForRealm:realmB]; + [self waitForDownloadsForRealm:realmC]; + CHECK_COUNT(3, Person, realmA); + CHECK_COUNT(2, Person, realmB); + CHECK_COUNT(5, Person, realmC); RLMResults *resultsA = [Person objectsInRealm:realmA where:@"firstName == %@", @"Ringo"]; RLMResults *resultsB = [Person objectsInRealm:realmB where:@"firstName == %@", @"Ringo"]; @@ -1037,7 +981,7 @@ - (void)testMultipleRealmsAddObjects { /// A client should be able to open multiple Realms and delete objects from each of them. - (void)testMultipleRealmsDeleteObjects { - NSString *partitionValueA = NSStringFromSelector(_cmd); + NSString *partitionValueA = self.name; NSString *partitionValueB = [partitionValueA stringByAppendingString:@"bar"]; NSString *partitionValueC = [partitionValueA stringByAppendingString:@"baz"]; RLMUser *user = [self userForTest:_cmd]; @@ -1068,12 +1012,12 @@ - (void)testMultipleRealmsDeleteObjects { CHECK_COUNT(5, Person, realmB); CHECK_COUNT(2, Person, realmC); RLMRunChildAndWait(); - [self waitForDownloadsForUser:user - realms:@[realmA, realmB, realmC] - partitionValues:@[partitionValueA, - partitionValueB, - partitionValueC] - expectedCounts:@[@0, @0, @0]]; + [self waitForDownloadsForRealm:realmA]; + [self waitForDownloadsForRealm:realmB]; + [self waitForDownloadsForRealm:realmC]; + CHECK_COUNT(0, Person, realmA); + CHECK_COUNT(0, Person, realmB); + CHECK_COUNT(0, Person, realmC); } else { // Delete all the objects from the Realms. CHECK_COUNT(4, Person, realmA); @@ -1101,41 +1045,32 @@ - (void)testMultipleRealmsDeleteObjects { /// When a session opened by a Realm goes out of scope, it should stay alive long enough to finish any waiting uploads. - (void)testUploadChangesWhenRealmOutOfScope { const NSInteger OBJECT_COUNT = 3; - RLMUser *user = [self userForTest:_cmd]; - if (self.isParent) { - // Open the Realm in an autorelease pool so that it is destroyed as soon as possible. - @autoreleasepool { - RLMRealm *realm = [self openRealmForPartitionValue:NSStringFromSelector(_cmd) user:user]; - [self addPersonsToRealm:realm - persons:@[[Person john], - [Person paul], - [Person ringo]]]; - CHECK_COUNT(OBJECT_COUNT, Person, realm); - } - - // We have to use a sleep here because explicitly waiting for uploads - // would retain the session, defeating the purpose of this test - sleep(2); - - RLMRunChildAndWait(); - } else { - RLMRealm *realm = [self openRealmForPartitionValue:NSStringFromSelector(_cmd) user:user]; + // Open the Realm in an autorelease pool so that it is destroyed as soon as possible. + @autoreleasepool { + RLMRealm *realm = [self openRealm]; + [self addPersonsToRealm:realm + persons:@[[Person john], [Person paul], [Person ringo]]]; CHECK_COUNT(OBJECT_COUNT, Person, realm); } + + [self.app.syncManager waitForSessionTermination]; + + RLMRealm *realm = [self openRealm]; + CHECK_COUNT(OBJECT_COUNT, Person, realm); } #pragma mark - Logging Back In /// A Realm that was opened before a user logged out should be able to resume uploading if the user logs back in. - (void)testLogBackInSameRealmUpload { - RLMCredentials *credentials = [self basicCredentialsWithName:NSStringFromSelector(_cmd) + RLMCredentials *credentials = [self basicCredentialsWithName:self.name register:self.isParent]; RLMUser *user = [self logInUserForCredentials:credentials]; RLMRealmConfiguration *config; @autoreleasepool { - RLMRealm *realm = [self openRealmForPartitionValue:NSStringFromSelector(_cmd) user:user]; + RLMRealm *realm = [self openRealmForPartitionValue:self.name user:user]; config = realm.configuration; [self addPersonsToRealm:realm persons:@[[Person john]]]; CHECK_COUNT(1, Person, realm); @@ -1153,16 +1088,16 @@ - (void)testLogBackInSameRealmUpload { // Verify that the post-login objects were actually synced XCTAssertTrue([RLMRealm deleteFilesForConfiguration:config error:nil]); - RLMRealm *realm = [self openRealmForPartitionValue:NSStringFromSelector(_cmd) user:user]; + RLMRealm *realm = [self openRealm]; CHECK_COUNT(4, Person, realm); } /// A Realm that was opened before a user logged out should be able to resume downloading if the user logs back in. - (void)testLogBackInSameRealmDownload { - RLMCredentials *credentials = [self basicCredentialsWithName:NSStringFromSelector(_cmd) + RLMCredentials *credentials = [self basicCredentialsWithName:self.name register:self.isParent]; RLMUser *user = [self logInUserForCredentials:credentials]; - RLMRealm *realm = [self openRealmForPartitionValue:NSStringFromSelector(_cmd) user:user]; + RLMRealm *realm = [self openRealmForPartitionValue:self.name user:user]; if (self.isParent) { [self addPersonsToRealm:realm persons:@[[Person john]]]; @@ -1186,36 +1121,29 @@ - (void)testLogBackInSameRealmDownload { /// A Realm that was opened while a user was logged out should be able to start uploading if the user logs back in. - (void)testLogBackInDeferredRealmUpload { - NSString *partitionValue = NSStringFromSelector(_cmd); - RLMCredentials *credentials = [self basicCredentialsWithName:partitionValue - register:self.isParent]; + RLMCredentials *credentials = [self basicCredentialsWithName:self.name register:YES]; RLMUser *user = [self logInUserForCredentials:credentials]; + [self logOutUser:user]; - if (self.isParent) { - [self logOutUser:user]; - - // Open a Realm after the user's been logged out. - RLMRealm *realm = [self immediatelyOpenRealmForPartitionValue:partitionValue user:user]; + // Open a Realm after the user's been logged out. + RLMRealm *realm = [self immediatelyOpenRealmForPartitionValue:self.name user:user]; - [self addPersonsToRealm:realm persons:@[[Person john]]]; - CHECK_COUNT(1, Person, realm); + [self addPersonsToRealm:realm persons:@[[Person john]]]; + CHECK_COUNT(1, Person, realm); - user = [self logInUserForCredentials:credentials]; - [self addPersonsToRealm:realm - persons:@[[Person john], [Person paul], [Person ringo]]]; - [self waitForUploadsForRealm:realm]; - CHECK_COUNT(4, Person, realm); + [self logInUserForCredentials:credentials]; + [self addPersonsToRealm:realm + persons:@[[Person john], [Person paul], [Person ringo]]]; + [self waitForUploadsForRealm:realm]; + CHECK_COUNT(4, Person, realm); - RLMRunChildAndWait(); - } else { - RLMRealm *realm = [self openRealmForPartitionValue:partitionValue user:user]; - CHECK_COUNT(4, Person, realm); - } + RLMRealm *realm2 = [self openRealm]; + CHECK_COUNT(4, Person, realm2); } /// A Realm that was opened while a user was logged out should be able to start downloading if the user logs back in. - (void)testLogBackInDeferredRealmDownload { - RLMCredentials *credentials = [self basicCredentialsWithName:NSStringFromSelector(_cmd) + RLMCredentials *credentials = [self basicCredentialsWithName:self.name register:self.isParent]; RLMUser *user = [self logInUserForCredentials:credentials]; @@ -1224,17 +1152,16 @@ - (void)testLogBackInDeferredRealmDownload { RLMRunChildAndWait(); // Open a Realm after the user's been logged out. - RLMRealm *realm = [self immediatelyOpenRealmForPartitionValue:NSStringFromSelector(_cmd) user:user]; + RLMRealm *realm = [self immediatelyOpenRealmForPartitionValue:self.name user:user]; [self addPersonsToRealm:realm persons:@[[Person john]]]; CHECK_COUNT(1, Person, realm); user = [self logInUserForCredentials:credentials]; - [self waitForDownloadsForUser:user - realms:@[realm] - partitionValues:@[NSStringFromSelector(_cmd)] expectedCounts:@[@4]]; + [self waitForDownloadsForRealm:realm]; + CHECK_COUNT(4, Person, realm); } else { - RLMRealm *realm = [self openRealmForPartitionValue:NSStringFromSelector(_cmd) user:user]; + RLMRealm *realm = [self openRealmForPartitionValue:self.name user:user]; [self addPersonsToRealm:realm persons:@[[Person john], [Person paul], [Person ringo]]]; [self waitForUploadsForRealm:realm]; @@ -1245,117 +1172,98 @@ - (void)testLogBackInDeferredRealmDownload { /// After logging back in, a Realm whose path has been opened for the first time should properly upload changes. - (void)testLogBackInOpenFirstTimePathUpload { - RLMCredentials *credentials = [self basicCredentialsWithName:NSStringFromSelector(_cmd) - register:self.isParent]; + RLMCredentials *credentials = [self basicCredentialsWithName:self.name register:YES]; RLMUser *user = [self logInUserForCredentials:credentials]; - if (self.isParent) { - [self logOutUser:user]; - user = [self logInUserForCredentials:[self basicCredentialsWithName:NSStringFromSelector(_cmd) - register:NO]]; - RLMRealm *realm = [self immediatelyOpenRealmForPartitionValue:NSStringFromSelector(_cmd) user:user]; + [self logOutUser:user]; + + @autoreleasepool { + auto c = [user configurationWithPartitionValue:self.name]; + c.objectClasses = @[Person.self]; + RLMRealm *realm = [RLMRealm realmWithConfiguration:c error:nil]; [self addPersonsToRealm:realm - persons:@[[Person john], - [Person paul]]]; + persons:@[[Person john], [Person paul]]]; + [self logInUserForCredentials:credentials]; [self waitForUploadsForRealm:realm]; - CHECK_COUNT(2, Person, realm); - RLMRunChildAndWait(); - } else { - RLMRealm *realm = [self openRealmForPartitionValue:NSStringFromSelector(_cmd) user:user]; - CHECK_COUNT(2, Person, realm); } + + RLMRealm *realm = [self openRealm]; + CHECK_COUNT(2, Person, realm); } /// After logging back in, a Realm whose path has been opened for the first time should properly download changes. - (void)testLogBackInOpenFirstTimePathDownload { - RLMCredentials *credentials = [self basicCredentialsWithName:NSStringFromSelector(_cmd) - register:self.isParent]; + RLMCredentials *credentials = [self basicCredentialsWithName:self.name register:YES]; RLMUser *user = [self logInUserForCredentials:credentials]; + [self logOutUser:user]; - if (self.isParent) { - [self logOutUser:user]; - user = [self logInUserForCredentials:[self basicCredentialsWithName:NSStringFromSelector(_cmd) - register:NO]]; - RLMRealm *realm = [self openRealmForPartitionValue:NSStringFromSelector(_cmd) user:user]; - RLMRunChildAndWait(); - [self waitForDownloadsForRealm:realm]; - CHECK_COUNT(2, Person, realm); - } else { - RLMRealm *realm = [self openRealmForPartitionValue:NSStringFromSelector(_cmd) user:user]; + auto c = [user configurationWithPartitionValue:self.name]; + c.objectClasses = @[Person.self]; + RLMRealm *realm = [RLMRealm realmWithConfiguration:c error:nil]; + + @autoreleasepool { + RLMRealm *realm = [self openRealm]; [self addPersonsToRealm:realm - persons:@[[Person john], - [Person paul]]]; + persons:@[[Person john], [Person paul]]]; [self waitForUploadsForRealm:realm]; CHECK_COUNT(2, Person, realm); } + + CHECK_COUNT(0, Person, realm); + [self logInUserForCredentials:credentials]; + [self waitForDownloadsForRealm:realm]; + CHECK_COUNT(2, Person, realm); } /// If a client logs in, connects, logs out, and logs back in, sync should properly upload changes for a new /// `RLMRealm` that is opened for the same path as a previously-opened Realm. - (void)testLogBackInReopenRealmUpload { - RLMCredentials *credentials = [self basicCredentialsWithName:NSStringFromSelector(_cmd) + RLMCredentials *credentials = [self basicCredentialsWithName:self.name register:self.isParent]; RLMUser *user = [self logInUserForCredentials:credentials]; - if (self.isParent) { - @autoreleasepool { - RLMRealm *realm = [self openRealmForPartitionValue:NSStringFromSelector(_cmd) user:user]; - [self addPersonsToRealm:realm persons:@[[Person john]]]; - [self waitForUploadsForRealm:realm]; - CHECK_COUNT(1, Person, realm); - [self logOutUser:user]; - user = [self logInUserForCredentials:[self basicCredentialsWithName:NSStringFromSelector(_cmd) - register:NO]]; - } - - RLMRealm *realm = [self openRealmForPartitionValue:NSStringFromSelector(_cmd) user:user]; - [self addPersonsToRealm:realm - persons:@[[Person john], - [Person paul], - [Person george], - [Person ringo]]]; - CHECK_COUNT(5, Person, realm); + @autoreleasepool { + RLMRealm *realm = [self openRealmForPartitionValue:self.name user:user]; + [self addPersonsToRealm:realm persons:@[[Person john]]]; [self waitForUploadsForRealm:realm]; - - RLMRunChildAndWait(); - } else { - RLMRealm *realm = [self openRealmForPartitionValue:NSStringFromSelector(_cmd) user:user]; - CHECK_COUNT(5, Person, realm); + CHECK_COUNT(1, Person, realm); + [self logOutUser:user]; + user = [self logInUserForCredentials:credentials]; } + + RLMRealm *realm = [self openRealmForPartitionValue:self.name user:user]; + [self addPersonsToRealm:realm + persons:@[[Person john], [Person paul], [Person george], [Person ringo]]]; + CHECK_COUNT(5, Person, realm); + [self waitForUploadsForRealm:realm]; + + RLMRealm *realm2 = [self openRealmForPartitionValue:self.name user:self.createUser]; + CHECK_COUNT(5, Person, realm2); } /// If a client logs in, connects, logs out, and logs back in, sync should properly download changes for a new /// `RLMRealm` that is opened for the same path as a previously-opened Realm. - (void)testLogBackInReopenRealmDownload { - RLMCredentials *credentials = [self basicCredentialsWithName:NSStringFromSelector(_cmd) + RLMCredentials *credentials = [self basicCredentialsWithName:self.name register:self.isParent]; RLMUser *user = [self logInUserForCredentials:credentials]; - if (self.isParent) { - RLMRealm *realm = [self openRealmForPartitionValue:NSStringFromSelector(_cmd) user:user]; - [self addPersonsToRealm:realm persons:@[[Person john]]]; - [self waitForUploadsForRealm:realm]; - XCTAssert([Person allObjectsInRealm:realm].count == 1, @"Expected 1 item"); - [self logOutUser:user]; - user = [self logInUserForCredentials:[self basicCredentialsWithName:NSStringFromSelector(_cmd) - register:NO]]; - RLMRunChildAndWait(); - // Open the Realm again and get the items. - realm = [self openRealmForPartitionValue:NSStringFromSelector(_cmd) user:user]; - [self waitForDownloadsForUser:user - realms:@[realm] - partitionValues:@[NSStringFromSelector(_cmd)] expectedCounts:@[@5]]; - } else { - RLMRealm *realm = [self openRealmForPartitionValue:NSStringFromSelector(_cmd) user:user]; - CHECK_COUNT(1, Person, realm); - [self addPersonsToRealm:realm - persons:@[[Person john], - [Person paul], - [Person george], - [Person ringo]]]; - [self waitForUploadsForRealm:realm]; - CHECK_COUNT(5, Person, realm); - } + RLMRealm *realm = [self openRealmForPartitionValue:self.name user:user]; + [self addPersonsToRealm:realm persons:@[[Person john]]]; + [self waitForUploadsForRealm:realm]; + XCTAssert([Person allObjectsInRealm:realm].count == 1, @"Expected 1 item"); + [self logOutUser:user]; + user = [self logInUserForCredentials:credentials]; + RLMRealm *realm2 = [self openRealmForPartitionValue:self.name user:self.createUser]; + CHECK_COUNT(1, Person, realm2); + [self addPersonsToRealm:realm2 + persons:@[[Person john], [Person paul], [Person george], [Person ringo]]]; + [self waitForUploadsForRealm:realm2]; + CHECK_COUNT(5, Person, realm2); + + // Open the Realm again and get the items. + realm = [self openRealmForPartitionValue:self.name user:user]; + CHECK_COUNT(5, Person, realm2); } #pragma mark - Session suspend and resume @@ -1398,12 +1306,10 @@ - (void)testSuspendAndResume { CHECK_COUNT(0, Person, realmA); CHECK_COUNT(1, Person, realmB); [self addPersonsToRealm:realmA - persons:@[[Person john], - [Person paul]]]; + persons:@[[Person john], [Person paul]]]; [self waitForUploadsForRealm:realmA]; [self addPersonsToRealm:realmB - persons:@[[Person john], - [Person paul]]]; + persons:@[[Person john], [Person paul]]]; [self waitForUploadsForRealm:realmB]; CHECK_COUNT(2, Person, realmA); CHECK_COUNT(3, Person, realmB); @@ -1416,9 +1322,10 @@ - (void)testSuspendAndResume { - (void)testClientReset { RLMUser *user = [self userForTest:_cmd]; // Open the Realm - __attribute__((objc_precise_lifetime)) RLMRealm *realm = [self openRealmForPartitionValue:@"realm_id" - user:user - clientResetMode:RLMClientResetModeManual]; + __attribute__((objc_precise_lifetime)) + RLMRealm *realm = [self openRealmForPartitionValue:@"realm_id" + user:user + clientResetMode:RLMClientResetModeManual]; __block NSError *theError = nil; XCTestExpectation *ex = [self expectationWithDescription:@"Waiting for error handler to be called..."]; @@ -1440,54 +1347,60 @@ - (void)testClientReset { /// Test manually initiating client reset. - (void)testClientResetManualInitiation { - RLMUser *user = [self userForTest:_cmd]; - NSString *partitionValue = NSStringFromSelector(_cmd); + RLMUser *user = [self createUser]; __block NSError *theError = nil; @autoreleasepool { - __attribute__((objc_precise_lifetime)) RLMRealm *realm = [self openRealmForPartitionValue:partitionValue user:user clientResetMode:RLMClientResetModeManual]; + __attribute__((objc_precise_lifetime)) + RLMRealm *realm = [self openRealmForPartitionValue:self.name user:user + clientResetMode:RLMClientResetModeManual]; XCTestExpectation *ex = [self expectationWithDescription:@"Waiting for error handler to be called..."]; self.app.syncManager.errorHandler = ^(NSError *error, RLMSyncSession *) { theError = error; [ex fulfill]; }; - [user simulateClientResetErrorForSession:partitionValue]; + [user simulateClientResetErrorForSession:self.name]; [self waitForExpectationsWithTimeout:30 handler:nil]; XCTAssertNotNil(theError); } + // At this point the Realm should be invalidated and client reset should be possible. NSString *pathValue = [theError rlmSync_clientResetBackedUpRealmPath]; XCTAssertFalse([NSFileManager.defaultManager fileExistsAtPath:pathValue]); - [RLMSyncSession immediatelyHandleError:theError.rlmSync_errorActionToken syncManager:self.app.syncManager]; + [RLMSyncSession immediatelyHandleError:theError.rlmSync_errorActionToken + syncManager:self.app.syncManager]; XCTAssertTrue([NSFileManager.defaultManager fileExistsAtPath:pathValue]); } - (void)testSetClientResetMode { - RLMUser *user = [self userForTest:_cmd]; - NSString *partitionValue = NSStringFromSelector(_cmd); + RLMUser *user = [self createUser]; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - RLMRealmConfiguration *config = [user configurationWithPartitionValue:partitionValue clientResetMode:RLMClientResetModeDiscardLocal]; + RLMRealmConfiguration *config = [user configurationWithPartitionValue:self.name + clientResetMode:RLMClientResetModeDiscardLocal]; XCTAssertEqual(config.syncConfiguration.clientResetMode, RLMClientResetModeDiscardLocal); #pragma clang diagnostic pop // Default is recover - config = [user configurationWithPartitionValue:partitionValue]; + config = [user configurationWithPartitionValue:self.name]; XCTAssertEqual(config.syncConfiguration.clientResetMode, RLMClientResetModeRecoverUnsyncedChanges); RLMSyncErrorReportingBlock block = ^(NSError *, RLMSyncSession *) { XCTFail("Should never hit"); }; - RLMAssertThrowsWithReason([user configurationWithPartitionValue:partitionValue clientResetMode:RLMClientResetModeDiscardUnsyncedChanges manualClientResetHandler:block], @"A manual client reset handler can only be set with RLMClientResetModeManual"); + RLMAssertThrowsWithReason([user configurationWithPartitionValue:self.name + clientResetMode:RLMClientResetModeDiscardUnsyncedChanges + manualClientResetHandler:block], + @"A manual client reset handler can only be set with RLMClientResetModeManual"); } - (void)testSetClientResetCallbacks { - RLMUser *user = [self userForTest:_cmd]; - NSString *partitionValue = NSStringFromSelector(_cmd); + RLMUser *user = [self createUser]; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - RLMRealmConfiguration *config = [user configurationWithPartitionValue:partitionValue clientResetMode:RLMClientResetModeDiscardLocal]; + RLMRealmConfiguration *config = [user configurationWithPartitionValue:self.name + clientResetMode:RLMClientResetModeDiscardLocal]; XCTAssertNil(config.syncConfiguration.beforeClientReset); XCTAssertNil(config.syncConfiguration.afterClientReset); @@ -1498,7 +1411,7 @@ - (void)testSetClientResetCallbacks { RLMClientResetAfterBlock afterBlock = ^(RLMRealm *before __unused, RLMRealm *after __unused) { XCTAssert(false, @"Should not execute callback"); }; - RLMRealmConfiguration *config2 = [user configurationWithPartitionValue:partitionValue + RLMRealmConfiguration *config2 = [user configurationWithPartitionValue:self.name clientResetMode:RLMClientResetModeDiscardLocal notifyBeforeReset:beforeBlock notifyAfterReset:afterBlock]; @@ -1515,7 +1428,6 @@ - (void)testBeforeClientResetCallbackNotVersioned { XCTestExpectation *beforeExpectation = [self expectationWithDescription:@"block called once"]; syncConfig.clientResetMode = RLMClientResetModeRecoverUnsyncedChanges; syncConfig.beforeClientReset = ^(RLMRealm *beforeFrozen) { - XCTAssertNotEqual(RLMNotVersioned, beforeFrozen->_realm->schema_version()); [beforeExpectation fulfill]; }; @@ -1572,14 +1484,15 @@ - (void)testAfterClientResetCallbackNotVersioned { static const NSInteger NUMBER_OF_BIG_OBJECTS = 2; -- (void)populateDataForUser:(RLMUser *)user partitionValue:(NSString *)partitionValue { +- (void)populateData { NSURL *realmURL; + RLMUser *user = [self createUser]; @autoreleasepool { - RLMRealm *realm = [self openRealmForPartitionValue:partitionValue user:user]; + RLMRealm *realm = [self openRealmWithUser:user]; realmURL = realm.configuration.fileURL; CHECK_COUNT(0, HugeSyncObject, realm); [realm beginWriteTransaction]; - for (NSInteger i=0; i callCount{0}; std::atomic transferred{0}; std::atomic transferrable{0}; BOOL hasBeenFulfilled = NO; - // Register a notifier. - RLMRealm *realm = [self openRealmForPartitionValue:NSStringFromSelector(_cmd) user:user]; - RLMSyncSession *session = realm.syncSession; - XCTAssertNotNil(session); - XCTestExpectation *ex = [self expectationWithDescription:@"streaming-download-notifier"]; - RLMNotificationToken *token = [session addProgressNotificationForDirection:RLMSyncProgressDirectionDownload - mode:RLMSyncProgressModeReportIndefinitely - block:[&](NSUInteger xfr, NSUInteger xfb) { + RLMNotificationToken *token = [session + addProgressNotificationForDirection:RLMSyncProgressDirectionDownload + mode:RLMSyncProgressModeReportIndefinitely + block:[&](NSUInteger xfr, NSUInteger xfb) { // Make sure the values are increasing, and update our stored copies. XCTAssertGreaterThanOrEqual(xfr, transferred.load()); XCTAssertGreaterThanOrEqual(xfb, transferrable.load()); @@ -1623,8 +1528,9 @@ - (void)testStreamingDownloadNotifier { hasBeenFulfilled = YES; } }]; - // Wait for the child process to upload everything. - RLMRunChildAndWait(); + + [self populateData]; + [self waitForExpectationsWithTimeout:30.0 handler:nil]; [token invalidate]; // The notifier should have been called at least twice: once at the beginning and at least once @@ -1634,19 +1540,14 @@ - (void)testStreamingDownloadNotifier { } - (void)testStreamingUploadNotifier { - RLMCredentials *credentials = [self basicCredentialsWithName:NSStringFromSelector(_cmd) - register:self.isParent]; - RLMUser *user = [self logInUserForCredentials:credentials]; - std::atomic callCount{0}; - std::atomic transferred{0}; - std::atomic transferrable{0}; - // Open the Realm - RLMRealm *realm = [self openRealmForPartitionValue:NSStringFromSelector(_cmd) user:user]; - - // Register a notifier. + RLMRealm *realm = [self openRealm]; RLMSyncSession *session = realm.syncSession; XCTAssertNotNil(session); + XCTestExpectation *ex = [self expectationWithDescription:@"streaming-upload-expectation"]; + std::atomic callCount{0}; + std::atomic transferred{0}; + std::atomic transferrable{0}; auto token = [session addProgressNotificationForDirection:RLMSyncProgressDirectionUpload mode:RLMSyncProgressModeReportIndefinitely block:[&](NSUInteger xfr, NSUInteger xfb) { @@ -1660,15 +1561,18 @@ - (void)testStreamingUploadNotifier { [ex fulfill]; } }]; + // Upload lots of data [realm beginWriteTransaction]; for (NSInteger i=0; i> *projection = @{@"name": @1, @"breed": @1}; - NSArray> *sorting = @[@{@"age": @1}, @{@"coat": @1}]; - - RLMFindOneAndModifyOptions *findOneAndModifyOptions1 = [[RLMFindOneAndModifyOptions alloc] init]; - XCTAssertNil(findOneAndModifyOptions1.projection); - XCTAssertEqual(findOneAndModifyOptions1.sorting.count, 0U); - XCTAssertFalse(findOneAndModifyOptions1.shouldReturnNewDocument); - XCTAssertFalse(findOneAndModifyOptions1.upsert); - - RLMFindOneAndModifyOptions *findOneAndModifyOptions2 = [[RLMFindOneAndModifyOptions alloc] init]; - findOneAndModifyOptions2.projection = projection; - findOneAndModifyOptions2.sorting = sorting; - findOneAndModifyOptions2.shouldReturnNewDocument = YES; - findOneAndModifyOptions2.upsert = YES; - XCTAssertNotNil(findOneAndModifyOptions2.projection); - XCTAssertEqual(findOneAndModifyOptions2.sorting.count, 2U); - XCTAssertTrue(findOneAndModifyOptions2.shouldReturnNewDocument); - XCTAssertTrue(findOneAndModifyOptions2.upsert); - XCTAssertFalse([findOneAndModifyOptions2.projection isEqual:@{}]); - XCTAssertTrue([findOneAndModifyOptions2.projection isEqual:projection]); - XCTAssertFalse([findOneAndModifyOptions2.sorting isEqual:@{}]); - XCTAssertTrue([findOneAndModifyOptions2.sorting isEqual:sorting]); - - RLMFindOneAndModifyOptions *findOneAndModifyOptions3 = [[RLMFindOneAndModifyOptions alloc] - initWithProjection:projection - sorting:sorting - upsert:YES - shouldReturnNewDocument:YES]; - XCTAssertNotNil(findOneAndModifyOptions3.projection); - XCTAssertEqual(findOneAndModifyOptions3.sorting.count, 2U); - XCTAssertTrue(findOneAndModifyOptions3.shouldReturnNewDocument); - XCTAssertTrue(findOneAndModifyOptions3.upsert); - XCTAssertFalse([findOneAndModifyOptions3.projection isEqual:@{}]); - XCTAssertTrue([findOneAndModifyOptions3.projection isEqual:projection]); - XCTAssertFalse([findOneAndModifyOptions3.sorting isEqual:@{}]); - XCTAssertTrue([findOneAndModifyOptions3.sorting isEqual:sorting]); - - findOneAndModifyOptions3.projection = nil; - findOneAndModifyOptions3.sorting = @[]; - XCTAssertNil(findOneAndModifyOptions3.projection); - XCTAssertEqual(findOneAndModifyOptions3.sorting.count, 0U); - XCTAssertTrue([findOneAndModifyOptions3.sorting isEqual:@[]]); - - RLMFindOneAndModifyOptions *findOneAndModifyOptions4 = [[RLMFindOneAndModifyOptions alloc] - initWithProjection:nil - sorting:@[] - upsert:NO - shouldReturnNewDocument:NO]; - XCTAssertNil(findOneAndModifyOptions4.projection); - XCTAssertEqual(findOneAndModifyOptions4.sorting.count, 0U); - XCTAssertFalse(findOneAndModifyOptions4.upsert); - XCTAssertFalse(findOneAndModifyOptions4.shouldReturnNewDocument); -} - -- (void)testFindOptions { - NSDictionary> *projection = @{@"name": @1, @"breed": @1}; - NSArray> *sorting = @[@{@"age": @1}, @{@"coat": @1}]; - - RLMFindOptions *findOptions1 = [[RLMFindOptions alloc] init]; - XCTAssertNil(findOptions1.projection); - XCTAssertEqual(findOptions1.sorting.count, 0U); - XCTAssertEqual(findOptions1.limit, 0); - - findOptions1.limit = 37; - findOptions1.projection = projection; - findOptions1.sorting = sorting; - XCTAssertEqual(findOptions1.limit, 37); - XCTAssertTrue([findOptions1.projection isEqual:projection]); - XCTAssertEqual(findOptions1.sorting.count, 2U); - XCTAssertTrue([findOptions1.sorting isEqual:sorting]); - - RLMFindOptions *findOptions2 = [[RLMFindOptions alloc] initWithProjection:projection - sorting:sorting]; - XCTAssertTrue([findOptions2.projection isEqual:projection]); - XCTAssertEqual(findOptions2.sorting.count, 2U); - XCTAssertEqual(findOptions2.limit, 0); - XCTAssertTrue([findOptions2.sorting isEqual:sorting]); - - RLMFindOptions *findOptions3 = [[RLMFindOptions alloc] initWithLimit:37 - projection:projection - sorting:sorting]; - XCTAssertTrue([findOptions3.projection isEqual:projection]); - XCTAssertEqual(findOptions3.sorting.count, 2U); - XCTAssertEqual(findOptions3.limit, 37); - XCTAssertTrue([findOptions3.sorting isEqual:sorting]); - - findOptions3.projection = nil; - findOptions3.sorting = @[]; - XCTAssertNil(findOptions3.projection); - XCTAssertEqual(findOptions3.sorting.count, 0U); - - RLMFindOptions *findOptions4 = [[RLMFindOptions alloc] initWithProjection:nil - sorting:@[]]; - XCTAssertNil(findOptions4.projection); - XCTAssertEqual(findOptions4.sorting.count, 0U); - XCTAssertEqual(findOptions4.limit, 0); -} - -- (void)testMongoInsert { - RLMMongoClient *client = [self.anonymousUser mongoClientWithServiceName:@"mongodb1"]; - RLMMongoDatabase *database = [client databaseWithName:@"test_data"]; - RLMMongoCollection *collection = [database collectionWithName:@"Dog"]; - - XCTestExpectation *insertOneExpectation = [self expectationWithDescription:@"should insert one document"]; - [collection insertOneDocument:@{@"name": @"fido", @"breed": @"cane corso"} completion:^(id objectId, NSError *error) { - XCTAssertEqual(objectId.bsonType, RLMBSONTypeObjectId); - XCTAssertNotEqualObjects(((RLMObjectId *)objectId).stringValue, @""); - XCTAssertNil(error); - [insertOneExpectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:60.0 handler:nil]; - - XCTestExpectation *insertManyExpectation = [self expectationWithDescription:@"should insert one document"]; - [collection insertManyDocuments:@[ - @{@"name": @"fido", @"breed": @"cane corso"}, - @{@"name": @"fido", @"breed": @"cane corso"}, - @{@"name": @"rex", @"breed": @"tibetan mastiff"}] - completion:^(NSArray> *objectIds, NSError *error) { - XCTAssertGreaterThan(objectIds.count, 0U); - XCTAssertNil(error); - [insertManyExpectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:60.0 handler:nil]; - - XCTestExpectation *findExpectation = [self expectationWithDescription:@"should find documents"]; - RLMFindOptions *options = [[RLMFindOptions alloc] initWithLimit:0 projection:nil sorting:@[]]; - [collection findWhere:@{@"name": @"fido", @"breed": @"cane corso"} - options:options - completion:^(NSArray *documents, NSError *error) { - XCTAssertEqual(documents.count, 3U); - XCTAssertNil(error); - [findExpectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:60.0 handler:nil]; -} - -- (void)testMongoFind { - RLMMongoClient *client = [self.anonymousUser mongoClientWithServiceName:@"mongodb1"]; - RLMMongoDatabase *database = [client databaseWithName:@"test_data"]; - RLMMongoCollection *collection = [database collectionWithName:@"Dog"]; - - XCTestExpectation *insertManyExpectation = [self expectationWithDescription:@"should insert one document"]; - [collection insertManyDocuments:@[ - @{@"name": @"fido", @"breed": @"cane corso"}, - @{@"name": @"fido", @"breed": @"cane corso"}, - @{@"name": @"rex", @"breed": @"tibetan mastiff"}] - completion:^(NSArray> *objectIds, NSError *error) { - XCTAssertGreaterThan(objectIds.count, 0U); - XCTAssertNil(error); - [insertManyExpectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:60.0 handler:nil]; - - XCTestExpectation *findExpectation = [self expectationWithDescription:@"should find documents"]; - RLMFindOptions *options = [[RLMFindOptions alloc] initWithLimit:0 projection:nil sorting:@[]]; - [collection findWhere:@{@"name": @"fido", @"breed": @"cane corso"} - options:options - completion:^(NSArray *documents, NSError *error) { - XCTAssertEqual(documents.count, 2U); - XCTAssertNil(error); - [findExpectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:60.0 handler:nil]; - - XCTestExpectation *findExpectation2 = [self expectationWithDescription:@"should find documents"]; - [collection findWhere:@{@"name": @"fido", @"breed": @"cane corso"} - completion:^(NSArray *documents, NSError *error) { - XCTAssertEqual(documents.count, 2U); - XCTAssertNil(error); - [findExpectation2 fulfill]; - }]; - [self waitForExpectationsWithTimeout:60.0 handler:nil]; - - XCTestExpectation *findExpectation3 = [self expectationWithDescription:@"should not find documents"]; - [collection findWhere:@{@"name": @"should not exist", @"breed": @"should not exist"} - completion:^(NSArray *documents, NSError *error) { - XCTAssertEqual(documents.count, NSUInteger(0)); - XCTAssertNil(error); - [findExpectation3 fulfill]; - }]; - [self waitForExpectationsWithTimeout:60.0 handler:nil]; - - XCTestExpectation *findExpectation4 = [self expectationWithDescription:@"should not find documents"]; - [collection findWhere:@{} - completion:^(NSArray *documents, NSError *error) { - XCTAssertGreaterThan(documents.count, 0U); - XCTAssertNil(error); - [findExpectation4 fulfill]; - }]; - [self waitForExpectationsWithTimeout:60.0 handler:nil]; - - XCTestExpectation *findOneExpectation1 = [self expectationWithDescription:@"should find documents"]; - [collection findOneDocumentWhere:@{@"name": @"fido", @"breed": @"cane corso"} - completion:^(NSDictionary *document, NSError *error) { - XCTAssertTrue([document[@"name"] isEqualToString:@"fido"]); - XCTAssertTrue([document[@"breed"] isEqualToString:@"cane corso"]); - XCTAssertNil(error); - [findOneExpectation1 fulfill]; - }]; - [self waitForExpectationsWithTimeout:60.0 handler:nil]; - - XCTestExpectation *findOneExpectation2 = [self expectationWithDescription:@"should find documents"]; - [collection findOneDocumentWhere:@{@"name": @"fido", @"breed": @"cane corso"} - options:options - completion:^(NSDictionary *document, NSError *error) { - XCTAssertTrue([document[@"name"] isEqualToString:@"fido"]); - XCTAssertTrue([document[@"breed"] isEqualToString:@"cane corso"]); - XCTAssertNil(error); - [findOneExpectation2 fulfill]; - }]; - [self waitForExpectationsWithTimeout:60.0 handler:nil]; -} - -- (void)testMongoAggregateAndCount { - RLMMongoClient *client = [self.anonymousUser mongoClientWithServiceName:@"mongodb1"]; - RLMMongoDatabase *database = [client databaseWithName:@"test_data"]; - RLMMongoCollection *collection = [database collectionWithName:@"Dog"]; - - XCTestExpectation *insertManyExpectation = [self expectationWithDescription:@"should insert one document"]; - [collection insertManyDocuments:@[ - @{@"name": @"fido", @"breed": @"cane corso"}, - @{@"name": @"fido", @"breed": @"cane corso"}, - @{@"name": @"rex", @"breed": @"tibetan mastiff"}] - completion:^(NSArray> *objectIds, NSError *error) { - XCTAssertEqual(objectIds.count, 3U); - XCTAssertNil(error); - [insertManyExpectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:60.0 handler:nil]; - - XCTestExpectation *aggregateExpectation1 = [self expectationWithDescription:@"should aggregate documents"]; - [collection aggregateWithPipeline:@[@{@"name" : @"fido"}] - completion:^(NSArray *documents, NSError *error) { - RLMValidateErrorContains(error, RLMAppErrorDomain, RLMAppErrorMongoDBError, - @"Unrecognized pipeline stage name: 'name'"); - XCTAssertNil(documents); - [aggregateExpectation1 fulfill]; - }]; - [self waitForExpectationsWithTimeout:60.0 handler:nil]; - - XCTestExpectation *aggregateExpectation2 = [self expectationWithDescription:@"should aggregate documents"]; - [collection aggregateWithPipeline:@[@{@"$match" : @{@"name" : @"fido"}}, @{@"$group" : @{@"_id" : @"$name"}}] - completion:^(NSArray *documents, NSError *error) { - XCTAssertNil(error); - XCTAssertNotNil(documents); - XCTAssertGreaterThan(documents.count, 0U); - [aggregateExpectation2 fulfill]; - }]; - [self waitForExpectationsWithTimeout:60.0 handler:nil]; - - XCTestExpectation *countExpectation1 = [self expectationWithDescription:@"should aggregate documents"]; - [collection countWhere:@{@"name" : @"fido"} - completion:^(NSInteger count, NSError *error) { - XCTAssertGreaterThan(count, 0); - XCTAssertNil(error); - [countExpectation1 fulfill]; - }]; - [self waitForExpectationsWithTimeout:60.0 handler:nil]; - - XCTestExpectation *countExpectation2 = [self expectationWithDescription:@"should aggregate documents"]; - [collection countWhere:@{@"name" : @"fido"} - limit:1 - completion:^(NSInteger count, NSError *error) { - XCTAssertEqual(count, 1); - XCTAssertNil(error); - [countExpectation2 fulfill]; - }]; - [self waitForExpectationsWithTimeout:60.0 handler:nil]; -} - -- (void)testMongoUpdate { - RLMMongoClient *client = [self.anonymousUser mongoClientWithServiceName:@"mongodb1"]; - RLMMongoDatabase *database = [client databaseWithName:@"test_data"]; - RLMMongoCollection *collection = [database collectionWithName:@"Dog"]; - - XCTestExpectation *updateExpectation1 = [self expectationWithDescription:@"should update document"]; - [collection updateOneDocumentWhere:@{@"name" : @"scrabby doo"} - updateDocument:@{@"name" : @"scooby"} - upsert:YES - completion:^(RLMUpdateResult *result, NSError *error) { - XCTAssertNotNil(result); - XCTAssertNotNil(result.documentId); - XCTAssertEqual(result.modifiedCount, (NSUInteger)0); - XCTAssertEqual(result.matchedCount, (NSUInteger)0); - XCTAssertNil(error); - [updateExpectation1 fulfill]; - }]; - [self waitForExpectationsWithTimeout:60.0 handler:nil]; - - XCTestExpectation *updateExpectation2 = [self expectationWithDescription:@"should update document"]; - [collection updateOneDocumentWhere:@{@"name" : @"scooby"} - updateDocument:@{@"name" : @"fred"} - upsert:NO - completion:^(RLMUpdateResult *result, NSError *error) { - XCTAssertNotNil(result); - XCTAssertNil(result.documentId); - XCTAssertEqual(result.modifiedCount, (NSUInteger)1); - XCTAssertEqual(result.matchedCount, (NSUInteger)1); - XCTAssertNil(error); - [updateExpectation2 fulfill]; - }]; - [self waitForExpectationsWithTimeout:60.0 handler:nil]; - - XCTestExpectation *updateExpectation3 = [self expectationWithDescription:@"should update document"]; - [collection updateOneDocumentWhere:@{@"name" : @"fred"} - updateDocument:@{@"name" : @"scrabby"} - completion:^(RLMUpdateResult *result, NSError *error) { - XCTAssertNotNil(result); - XCTAssertNil(result.documentId); - XCTAssertEqual(result.modifiedCount, (NSUInteger)1); - XCTAssertEqual(result.matchedCount, (NSUInteger)1); - XCTAssertNil(error); - [updateExpectation3 fulfill]; - }]; - [self waitForExpectationsWithTimeout:60.0 handler:nil]; - - XCTestExpectation *updateManyExpectation1 = [self expectationWithDescription:@"should update many documents"]; - [collection updateManyDocumentsWhere:@{@"name" : @"scrabby"} - updateDocument:@{@"name" : @"fred"} - completion:^(RLMUpdateResult *result, NSError *error) { - XCTAssertNotNil(result); - XCTAssertNil(result.documentId); - XCTAssertEqual(result.modifiedCount, (NSUInteger)1); - XCTAssertEqual(result.matchedCount, (NSUInteger)1); - XCTAssertNil(error); - [updateManyExpectation1 fulfill]; - }]; - [self waitForExpectationsWithTimeout:60.0 handler:nil]; - - XCTestExpectation *updateManyExpectation2 = [self expectationWithDescription:@"should update many documents"]; - [collection updateManyDocumentsWhere:@{@"name" : @"john"} - updateDocument:@{@"name" : @"alex"} - upsert:YES - completion:^(RLMUpdateResult *result, NSError *error) { - XCTAssertNotNil(result); - XCTAssertNotNil(result.documentId); - XCTAssertEqual(result.modifiedCount, (NSUInteger)0); - XCTAssertEqual(result.matchedCount, (NSUInteger)0); - XCTAssertNil(error); - [updateManyExpectation2 fulfill]; - }]; - [self waitForExpectationsWithTimeout:60.0 handler:nil]; -} - -- (void)testMongoFindAndModify { - RLMMongoClient *client = [self.anonymousUser mongoClientWithServiceName:@"mongodb1"]; - RLMMongoDatabase *database = [client databaseWithName:@"test_data"]; - RLMMongoCollection *collection = [database collectionWithName:@"Dog"]; - - NSArray> *sorting = @[@{@"name": @1}, @{@"breed": @1}]; - RLMFindOneAndModifyOptions *findAndModifyOptions = [[RLMFindOneAndModifyOptions alloc] - initWithProjection:@{@"name" : @1, @"breed" : @1} - sorting:sorting - upsert:YES - shouldReturnNewDocument:YES]; - - XCTestExpectation *findOneAndUpdateExpectation1 = [self expectationWithDescription:@"should find one document and update"]; - [collection findOneAndUpdateWhere:@{@"name" : @"alex"} - updateDocument:@{@"name" : @"max"} - options:findAndModifyOptions - completion:^(NSDictionary *document, NSError *error) { - XCTAssertTrue([document[@"name"] isEqualToString:@"max"]); - XCTAssertNil(error); - [findOneAndUpdateExpectation1 fulfill]; - }]; - [self waitForExpectationsWithTimeout:60.0 handler:nil]; - - XCTestExpectation *findOneAndUpdateExpectation2 = [self expectationWithDescription:@"should find one document and update"]; - [collection findOneAndUpdateWhere:@{@"name" : @"max"} - updateDocument:@{@"name" : @"john"} - completion:^(NSDictionary *document, NSError *error) { - XCTAssertTrue([document[@"name"] isEqualToString:@"max"]); - XCTAssertNil(error); - [findOneAndUpdateExpectation2 fulfill]; - }]; - [self waitForExpectationsWithTimeout:60.0 handler:nil]; - - XCTestExpectation *findOneAndReplaceExpectation1 = [self expectationWithDescription:@"should find one document and replace"]; - [collection findOneAndReplaceWhere:@{@"name" : @"alex"} - replacementDocument:@{@"name" : @"max"} - options:findAndModifyOptions - completion:^(NSDictionary *document, NSError *error) { - XCTAssertTrue([document[@"name"] isEqualToString:@"max"]); - XCTAssertNil(error); - [findOneAndReplaceExpectation1 fulfill]; - }]; - [self waitForExpectationsWithTimeout:60.0 handler:nil]; - - XCTestExpectation *findOneAndReplaceExpectation2 = [self expectationWithDescription:@"should find one document and replace"]; - [collection findOneAndReplaceWhere:@{@"name" : @"max"} - replacementDocument:@{@"name" : @"john"} - completion:^(NSDictionary *document, NSError *error) { - XCTAssertTrue([document[@"name"] isEqualToString:@"max"]); - XCTAssertNil(error); - [findOneAndReplaceExpectation2 fulfill]; - }]; - [self waitForExpectationsWithTimeout:60.0 handler:nil]; -} - -- (void)testMongoDelete { - RLMMongoClient *client = [self.anonymousUser mongoClientWithServiceName:@"mongodb1"]; - RLMMongoDatabase *database = [client databaseWithName:@"test_data"]; - RLMMongoCollection *collection = [database collectionWithName:@"Dog"]; - - NSArray *objectIds = [self prepareDogDocumentsIn:collection]; - RLMObjectId *rexObjectId = objectIds[1]; - - XCTestExpectation *deleteOneExpectation1 = [self expectationWithDescription:@"should delete first document in collection"]; - [collection deleteOneDocumentWhere:@{@"_id" : rexObjectId} - completion:^(NSInteger count, NSError *error) { - XCTAssertEqual(count, 1); - XCTAssertNil(error); - [deleteOneExpectation1 fulfill]; - }]; - [self waitForExpectationsWithTimeout:60.0 handler:nil]; - - XCTestExpectation *findExpectation1 = [self expectationWithDescription:@"should find documents"]; - [collection findWhere:@{} - completion:^(NSArray *documents, NSError *error) { - XCTAssertEqual(documents.count, 2U); - XCTAssertTrue([documents[0][@"name"] isEqualToString:@"fido"]); - XCTAssertNil(error); - [findExpectation1 fulfill]; - }]; - [self waitForExpectationsWithTimeout:60.0 handler:nil]; - - XCTestExpectation *deleteManyExpectation1 = [self expectationWithDescription:@"should delete many documents"]; - [collection deleteManyDocumentsWhere:@{@"name" : @"rex"} - completion:^(NSInteger count, NSError *error) { - XCTAssertEqual(count, 0U); - XCTAssertNil(error); - [deleteManyExpectation1 fulfill]; - }]; - [self waitForExpectationsWithTimeout:60.0 handler:nil]; - - XCTestExpectation *deleteManyExpectation2 = [self expectationWithDescription:@"should delete many documents"]; - [collection deleteManyDocumentsWhere:@{@"breed" : @"cane corso"} - completion:^(NSInteger count, NSError *error) { - XCTAssertEqual(count, 1); - XCTAssertNil(error); - [deleteManyExpectation2 fulfill]; - }]; - [self waitForExpectationsWithTimeout:60.0 handler:nil]; - - XCTestExpectation *findOneAndDeleteExpectation1 = [self expectationWithDescription:@"should find one and delete"]; - [collection findOneAndDeleteWhere:@{@"name": @"john"} - completion:^(NSDictionary> *document, NSError *error) { - XCTAssertNotNil(document); - NSString *name = (NSString *)document[@"name"]; - XCTAssertTrue([name isEqualToString:@"john"]); - XCTAssertNil(error); - [findOneAndDeleteExpectation1 fulfill]; - }]; - [self waitForExpectationsWithTimeout:60.0 handler:nil]; - - XCTestExpectation *findOneAndDeleteExpectation2 = [self expectationWithDescription:@"should find one and delete"]; - NSDictionary> *projection = @{@"name": @1, @"breed": @1}; - NSArray> *sortDescriptors = @[@{@"_id": @1}, @{@"breed": @1}]; - RLMFindOneAndModifyOptions *findOneAndModifyOptions = [[RLMFindOneAndModifyOptions alloc] - initWithProjection:projection - sorting:sortDescriptors - upsert:YES - shouldReturnNewDocument:YES]; - - [collection findOneAndDeleteWhere:@{@"name": @"john"} - options:findOneAndModifyOptions - completion:^(NSDictionary> *document, NSError *error) { - XCTAssertNil(document); - // FIXME: when a projection is used, the server reports the error - // "expected pre-image to match projection matcher" when there are no - // matches, rather than simply doing nothing like when there is no projection -// XCTAssertNil(error); - (void)error; - [findOneAndDeleteExpectation2 fulfill]; - }]; - [self waitForExpectationsWithTimeout:60.0 handler:nil]; -} - -#pragma mark - Watch - -- (void)testWatch { - [self performWatchTest:nil]; -} - -- (void)testWatchAsync { - auto asyncQueue = dispatch_queue_create("io.realm.watchQueue", DISPATCH_QUEUE_CONCURRENT); - [self performWatchTest:asyncQueue]; -} - -- (void)performWatchTest:(nullable dispatch_queue_t)delegateQueue { - XCTestExpectation *expectation = [self expectationWithDescription:@"watch collection and receive change event 3 times"]; - - RLMMongoClient *client = [self.anonymousUser mongoClientWithServiceName:@"mongodb1"]; - RLMMongoDatabase *database = [client databaseWithName:@"test_data"]; - __block RLMMongoCollection *collection = [database collectionWithName:@"Dog"]; - - __block RLMWatchTestUtility *testUtility = - [[RLMWatchTestUtility alloc] initWithChangeEventCount:3 - expectation:expectation]; - - __block RLMChangeStream *changeStream = [collection watchWithDelegate:testUtility delegateQueue:delegateQueue]; - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - WAIT_FOR_SEMAPHORE(testUtility.isOpenSemaphore, 30.0); - for (int i = 0; i < 3; i++) { - [collection insertOneDocument:@{@"name": @"fido"} completion:^(id objectId, NSError *error) { - XCTAssertNil(error); - XCTAssertNotNil(objectId); - }]; - WAIT_FOR_SEMAPHORE(testUtility.semaphore, 30.0); - } - [changeStream close]; - }); - - [self waitForExpectations:@[expectation] timeout:60.0]; -} - -- (void)testWatchWithMatchFilter { - [self performWatchWithMatchFilterTest:nil]; -} - -- (void)testWatchWithMatchFilterAsync { - auto asyncQueue = dispatch_queue_create("io.realm.watchQueue", DISPATCH_QUEUE_CONCURRENT); - [self performWatchWithMatchFilterTest:asyncQueue]; -} - -- (NSArray *)prepareDogDocumentsIn:(RLMMongoCollection *)collection { - __block NSArray *objectIds; - XCTestExpectation *ex = [self expectationWithDescription:@"delete existing documents"]; - [collection deleteManyDocumentsWhere:@{} completion:^(NSInteger, NSError *error) { - XCTAssertNil(error); - [ex fulfill]; - }]; - [self waitForExpectations:@[ex] timeout:60.0]; - - XCTestExpectation *insertManyExpectation = [self expectationWithDescription:@"should insert documents"]; - [collection insertManyDocuments:@[ - @{@"name": @"fido", @"breed": @"cane corso"}, - @{@"name": @"rex", @"breed": @"tibetan mastiff"}, - @{@"name": @"john", @"breed": @"tibetan mastiff"}] - completion:^(NSArray> *ids, NSError *error) { - XCTAssertEqual(ids.count, 3U); - for (id objectId in ids) { - XCTAssertEqual(objectId.bsonType, RLMBSONTypeObjectId); - } - XCTAssertNil(error); - objectIds = (NSArray *)ids; - [insertManyExpectation fulfill]; - }]; - [self waitForExpectations:@[insertManyExpectation] timeout:60.0]; - return objectIds; -} - -- (void)performWatchWithMatchFilterTest:(nullable dispatch_queue_t)delegateQueue { - RLMMongoClient *client = [self.anonymousUser mongoClientWithServiceName:@"mongodb1"]; - RLMMongoDatabase *database = [client databaseWithName:@"test_data"]; - __block RLMMongoCollection *collection = [database collectionWithName:@"Dog"]; - NSArray *objectIds = [self prepareDogDocumentsIn:collection]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"watch collection and receive change event 3 times"]; - - __block RLMWatchTestUtility *testUtility = - [[RLMWatchTestUtility alloc] initWithChangeEventCount:3 - matchingObjectId:objectIds[0] - expectation:expectation]; - - __block RLMChangeStream *changeStream = [collection watchWithMatchFilter:@{@"fullDocument._id": objectIds[0]} - delegate:testUtility - delegateQueue:delegateQueue]; - - dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - WAIT_FOR_SEMAPHORE(testUtility.isOpenSemaphore, 30.0); - for (int i = 0; i < 3; i++) { - [collection updateOneDocumentWhere:@{@"_id": objectIds[0]} - updateDocument:@{@"breed": @"king charles", @"name": [NSString stringWithFormat:@"fido-%d", i]} - completion:^(RLMUpdateResult *, NSError *error) { - XCTAssertNil(error); - }]; - - [collection updateOneDocumentWhere:@{@"_id": objectIds[1]} - updateDocument:@{@"breed": @"french bulldog", @"name": [NSString stringWithFormat:@"fido-%d", i]} - completion:^(RLMUpdateResult *, NSError *error) { - XCTAssertNil(error); - }]; - WAIT_FOR_SEMAPHORE(testUtility.semaphore, 30.0); - } - [changeStream close]; - }); - [self waitForExpectations:@[expectation] timeout:60.0]; -} - -- (void)testWatchWithFilterIds { - [self performWatchWithFilterIdsTest:nil]; -} - -- (void)testWatchWithFilterIdsAsync { - auto asyncQueue = dispatch_queue_create("io.realm.watchQueue", DISPATCH_QUEUE_CONCURRENT); - [self performWatchWithFilterIdsTest:asyncQueue]; -} - -- (void)performWatchWithFilterIdsTest:(nullable dispatch_queue_t)delegateQueue { - RLMMongoClient *client = [self.anonymousUser mongoClientWithServiceName:@"mongodb1"]; - RLMMongoDatabase *database = [client databaseWithName:@"test_data"]; - __block RLMMongoCollection *collection = [database collectionWithName:@"Dog"]; - NSArray *objectIds = [self prepareDogDocumentsIn:collection]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"watch collection and receive change event 3 times"]; - - __block RLMWatchTestUtility *testUtility = - [[RLMWatchTestUtility alloc] initWithChangeEventCount:3 - matchingObjectId:objectIds[0] - expectation:expectation]; - - __block RLMChangeStream *changeStream = [collection watchWithFilterIds:@[objectIds[0]] - delegate:testUtility - delegateQueue:delegateQueue]; - - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - WAIT_FOR_SEMAPHORE(testUtility.isOpenSemaphore, 30.0); - for (int i = 0; i < 3; i++) { - [collection updateOneDocumentWhere:@{@"_id": objectIds[0]} - updateDocument:@{@"breed": @"king charles", @"name": [NSString stringWithFormat:@"fido-%d", i]} - completion:^(RLMUpdateResult *, NSError *error) { - XCTAssertNil(error); - }]; - - [collection updateOneDocumentWhere:@{@"_id": objectIds[1]} - updateDocument:@{@"breed": @"french bulldog", @"name": [NSString stringWithFormat:@"fido-%d", i]} - completion:^(RLMUpdateResult *, NSError *error) { - XCTAssertNil(error); - }]; - WAIT_FOR_SEMAPHORE(testUtility.semaphore, 30.0); - } - [changeStream close]; - }); - - [self waitForExpectations:@[expectation] timeout:60.0]; -} - -- (void)testMultipleWatchStreams { - auto asyncQueue = dispatch_queue_create("io.realm.watchQueue", DISPATCH_QUEUE_CONCURRENT); - [self performMultipleWatchStreamsTest:asyncQueue]; -} - -- (void)testMultipleWatchStreamsAsync { - [self performMultipleWatchStreamsTest:nil]; -} - -- (void)performMultipleWatchStreamsTest:(nullable dispatch_queue_t)delegateQueue { - RLMMongoClient *client = [self.anonymousUser mongoClientWithServiceName:@"mongodb1"]; - RLMMongoDatabase *database = [client databaseWithName:@"test_data"]; - __block RLMMongoCollection *collection = [database collectionWithName:@"Dog"]; - NSArray *objectIds = [self prepareDogDocumentsIn:collection]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"watch collection and receive change event 3 times"]; - expectation.expectedFulfillmentCount = 2; - - __block RLMWatchTestUtility *testUtility1 = - [[RLMWatchTestUtility alloc] initWithChangeEventCount:3 - matchingObjectId:objectIds[0] - expectation:expectation]; - - __block RLMWatchTestUtility *testUtility2 = - [[RLMWatchTestUtility alloc] initWithChangeEventCount:3 - matchingObjectId:objectIds[1] - expectation:expectation]; - - __block RLMChangeStream *changeStream1 = [collection watchWithFilterIds:@[objectIds[0]] - delegate:testUtility1 - delegateQueue:delegateQueue]; - - __block RLMChangeStream *changeStream2 = [collection watchWithFilterIds:@[objectIds[1]] - delegate:testUtility2 - delegateQueue:delegateQueue]; - - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - WAIT_FOR_SEMAPHORE(testUtility1.isOpenSemaphore, 30.0); - WAIT_FOR_SEMAPHORE(testUtility2.isOpenSemaphore, 30.0); - for (int i = 0; i < 3; i++) { - [collection updateOneDocumentWhere:@{@"_id": objectIds[0]} - updateDocument:@{@"breed": @"king charles", @"name": [NSString stringWithFormat:@"fido-%d", i]} - completion:^(RLMUpdateResult *, NSError *error) { - XCTAssertNil(error); - }]; - - [collection updateOneDocumentWhere:@{@"_id": objectIds[1]} - updateDocument:@{@"breed": @"french bulldog", @"name": [NSString stringWithFormat:@"fido-%d", i]} - completion:^(RLMUpdateResult *, NSError *error) { - XCTAssertNil(error); - }]; - - [collection updateOneDocumentWhere:@{@"_id": objectIds[2]} - updateDocument:@{@"breed": @"german shepard", @"name": [NSString stringWithFormat:@"fido-%d", i]} - completion:^(RLMUpdateResult *, NSError *error) { - XCTAssertNil(error); - }]; - WAIT_FOR_SEMAPHORE(testUtility1.semaphore, 30.0); - WAIT_FOR_SEMAPHORE(testUtility2.semaphore, 30.0); - } - [changeStream1 close]; - [changeStream2 close]; - }); - - [self waitForExpectations:@[expectation] timeout:60.0]; -} - #pragma mark - File paths static NSString *newPathForPartitionValue(RLMUser *user, id partitionValue) { diff --git a/Realm/ObjectServerTests/RLMSubscriptionTests.mm b/Realm/ObjectServerTests/RLMSubscriptionTests.mm new file mode 100644 index 0000000000..9d6bbbae3d --- /dev/null +++ b/Realm/ObjectServerTests/RLMSubscriptionTests.mm @@ -0,0 +1,668 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2021 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +#import "RLMSyncTestCase.h" +#import "RLMSyncSubscription_Private.h" +#import "RLMApp_Private.hpp" + +@interface RLMSubscriptionTests : RLMSyncTestCase +@end + +@implementation RLMSubscriptionTests +- (NSArray *)defaultObjectTypes { + return @[Dog.self, Person.self]; +} + +- (NSString *)createAppWithError:(NSError **)error { + return [self createFlexibleSyncAppWithError:error]; +} + +- (RLMRealmConfiguration *)configurationForUser:(RLMUser *)user { + return [user flexibleSyncConfiguration]; +} + +- (void)testCreateFlexibleSyncApp { + NSString *appId = [RealmServer.shared createAppWithFields:@[@"age"] + types:@[Person.self] + persistent:false + error:nil]; + RLMApp *app = [self appWithId:appId]; + XCTAssertNotNil(app); +} + +- (void)testFlexibleSyncOpenRealm { + XCTAssertNotNil([self openRealm]); +} + +- (void)testGetSubscriptionsWhenLocalRealm { + RLMRealmConfiguration *configuration = [RLMRealmConfiguration defaultConfiguration]; + configuration.objectClasses = @[Person.self]; + RLMRealm *realm = [RLMRealm realmWithConfiguration:configuration error:nil]; + RLMAssertThrowsWithReason(realm.subscriptions, @"This Realm was not configured with flexible sync"); +} + +- (void)testGetSubscriptionsWhenPbsRealm { + RLMRealmConfiguration *config = [self.createUser configurationWithPartitionValue:nil]; + config.objectClasses = @[]; + RLMRealm *realm = [RLMRealm realmWithConfiguration:config error:nil]; + RLMAssertThrowsWithReason(realm.subscriptions, @"This Realm was not configured with flexible sync"); +} + +- (void)testFlexibleSyncRealmFilePath { + RLMUser *user = [self createUser]; + RLMRealmConfiguration *config = [user flexibleSyncConfiguration]; + NSString *expected = [NSString stringWithFormat:@"mongodb-realm/%@/%@/flx_sync_default.realm", self.appId, user.identifier]; + XCTAssertTrue([config.fileURL.path hasSuffix:expected]); +} + +- (void)testGetSubscriptionsWhenFlexibleSync { + RLMRealm *realm = [self openRealm]; + RLMSyncSubscriptionSet *subs = realm.subscriptions; + XCTAssertNotNil(subs); + XCTAssertEqual(subs.version, 0UL); + XCTAssertEqual(subs.count, 0UL); +} + +- (void)testGetSubscriptionsWhenSameVersion { + RLMRealm *realm = [self openRealm]; + RLMSyncSubscriptionSet *subs1 = realm.subscriptions; + RLMSyncSubscriptionSet *subs2 = realm.subscriptions; + XCTAssertEqual(subs1.version, 0UL); + XCTAssertEqual(subs2.version, 0UL); +} + +- (void)testCheckVersionAfterAddSubscription { + RLMRealm *realm = [self openRealm]; + RLMSyncSubscriptionSet *subs = realm.subscriptions; + XCTAssertNotNil(subs); + XCTAssertEqual(subs.version, 0UL); + XCTAssertEqual(subs.count, 0UL); + + [subs update:^{ + [subs addSubscriptionWithClassName:Person.className + where:@"age > 15"]; + }]; + + XCTAssertEqual(subs.version, 1UL); + XCTAssertEqual(subs.count, 1UL); +} + +- (void)testEmptyWriteSubscriptions { + RLMRealm *realm = [self openRealm]; + RLMSyncSubscriptionSet *subs = realm.subscriptions; + XCTAssertNotNil(subs); + XCTAssertEqual(subs.version, 0UL); + XCTAssertEqual(subs.count, 0UL); + + [subs update:^{}]; + + XCTAssertEqual(subs.version, 1UL); + XCTAssertEqual(subs.count, 0UL); +} + +- (void)testAddAndFindSubscriptionByQuery { + RLMRealm *realm = [self openRealm]; + RLMSyncSubscriptionSet *subs = realm.subscriptions; + + [subs update:^{ + [subs addSubscriptionWithClassName:Person.className + where:@"age > 15"]; + }]; + + RLMSyncSubscription *foundSubscription = [subs subscriptionWithClassName:Person.className + where:@"age > 15"]; + XCTAssertNotNil(foundSubscription); + XCTAssertNil(foundSubscription.name); + XCTAssert(foundSubscription.queryString, @"age > 15"); +} + +- (void)testAddAndFindSubscriptionWithCompoundQuery { + RLMRealm *realm = [self openRealm]; + RLMSyncSubscriptionSet *subs = realm.subscriptions; + XCTAssertNotNil(subs); + XCTAssertEqual(subs.version, 0UL); + XCTAssertEqual(subs.count, 0UL); + + [subs update:^{ + [subs addSubscriptionWithClassName:Person.className + where:@"firstName == %@ and lastName == %@", @"John", @"Doe"]; + }]; + + XCTAssertEqual(subs.version, 1UL); + XCTAssertEqual(subs.count, 1UL); + + RLMSyncSubscription *foundSubscription = [subs subscriptionWithClassName:Person.className + where:@"firstName == %@ and lastName == %@", @"John", @"Doe"]; + XCTAssertNotNil(foundSubscription); + XCTAssertNil(foundSubscription.name); + XCTAssert(foundSubscription.queryString, @"firstName == 'John' and lastName == 'Doe'"); +} + +- (void)testAddAndFindSubscriptionWithPredicate { + RLMRealm *realm = [self openRealm]; + RLMSyncSubscriptionSet *subs = realm.subscriptions; + XCTAssertNotNil(subs); + XCTAssertEqual(subs.version, 0UL); + XCTAssertEqual(subs.count, 0UL); + + [subs update:^{ + [subs addSubscriptionWithClassName:Person.className + predicate:[NSPredicate predicateWithFormat:@"age == %d", 20]]; + }]; + + XCTAssertEqual(subs.version, 1UL); + XCTAssertEqual(subs.count, 1UL); + + RLMSyncSubscription *foundSubscription = [subs subscriptionWithClassName:Person.className + predicate:[NSPredicate predicateWithFormat:@"age == %d", 20]]; + XCTAssertNotNil(foundSubscription); + XCTAssertNil(foundSubscription.name); + XCTAssert(foundSubscription.queryString, @"age == 20"); +} + +- (void)testAddSubscriptionWithoutWriteThrow { + RLMRealm *realm = [self openRealm]; + RLMSyncSubscriptionSet *subs = realm.subscriptions; + RLMAssertThrowsWithReason([subs addSubscriptionWithClassName:Person.className where:@"age > 15"], + @"Can only add, remove, or update subscriptions within a write subscription block."); +} + +- (void)testAddAndFindSubscriptionByName { + RLMRealm *realm = [self openRealm]; + RLMSyncSubscriptionSet *subs = realm.subscriptions; + XCTAssertNotNil(realm.subscriptions); + XCTAssertEqual(realm.subscriptions.version, 0UL); + XCTAssertEqual(realm.subscriptions.count, 0UL); + + [subs update:^{ + [subs addSubscriptionWithClassName:Person.className + subscriptionName:@"person_older_15" + where:@"age > 15"]; + }]; + + RLMSyncSubscription *foundSubscription = [subs subscriptionWithName:@"person_older_15"]; + XCTAssertNotNil(foundSubscription); + XCTAssert(foundSubscription.name, @"person_older_15"); + XCTAssert(foundSubscription.queryString, @"age > 15"); +} + +- (void)testAddDuplicateSubscription { + RLMRealm *realm = [self openRealm]; + RLMSyncSubscriptionSet *subs = realm.subscriptions; + + [subs update:^{ + [subs addSubscriptionWithClassName:Person.className + where:@"age > 15"]; + [subs addSubscriptionWithClassName:Person.className + where:@"age > 15"]; + }]; + + XCTAssertEqual(subs.version, 1UL); + XCTAssertEqual(subs.count, 1UL); +} + +- (void)testAddDuplicateNamedSubscriptionWillThrow { + RLMRealm *realm = [self openRealm]; + RLMSyncSubscriptionSet *subs = realm.subscriptions; + + [subs update:^{ + [subs addSubscriptionWithClassName:Person.className + subscriptionName:@"person_age" + where:@"age > 15"]; + RLMAssertThrowsWithReason([subs addSubscriptionWithClassName:Person.className + subscriptionName:@"person_age" + where:@"age > 20"], + @"A subscription named 'person_age' already exists. If you meant to update the existing subscription please use the `update` method."); + }]; + + XCTAssertEqual(subs.version, 1UL); + XCTAssertEqual(subs.count, 1UL); + + RLMSyncSubscription *foundSubscription = [subs subscriptionWithName:@"person_age"]; + XCTAssertNotNil(foundSubscription); + + XCTAssertEqualObjects(foundSubscription.name, @"person_age"); + XCTAssertEqualObjects(foundSubscription.queryString, @"age > 15"); + XCTAssertEqualObjects(foundSubscription.objectClassName, @"Person"); +} + +- (void)testAddDuplicateSubscriptionWithPredicate { + RLMRealm *realm = [self openRealm]; + RLMSyncSubscriptionSet *subs = realm.subscriptions; + + [subs update:^{ + [subs addSubscriptionWithClassName:Person.className + where:@"age > 15"]; + [subs addSubscriptionWithClassName:Person.className + predicate:[NSPredicate predicateWithFormat:@"age > %d", 15]]; + }]; + + XCTAssertEqual(subs.version, 1UL); + XCTAssertEqual(subs.count, 1UL); +} + +- (void)testAddDuplicateSubscriptionWithDifferentName { + RLMRealm *realm = [self openRealm]; + RLMSyncSubscriptionSet *subs = realm.subscriptions; + + [subs update:^{ + [subs addSubscriptionWithClassName:Person.className + subscriptionName:@"person_age_1" + where:@"age > 15"]; + [subs addSubscriptionWithClassName:Person.className + subscriptionName:@"person_age_2" + predicate:[NSPredicate predicateWithFormat:@"age > %d", 15]]; + }]; + + XCTAssertEqual(subs.version, 1UL); + XCTAssertEqual(subs.count, 2UL); + + RLMSyncSubscription *foundSubscription = [subs subscriptionWithName:@"person_age_1"]; + XCTAssertNotNil(foundSubscription); + + RLMSyncSubscription *foundSubscription2 = [subs subscriptionWithName:@"person_age_2"]; + XCTAssertNotNil(foundSubscription2); + + XCTAssertNotEqualObjects(foundSubscription.name, foundSubscription2.name); + XCTAssertEqualObjects(foundSubscription.queryString, foundSubscription2.queryString); + XCTAssertEqualObjects(foundSubscription.objectClassName, foundSubscription2.objectClassName); +} + +- (void)testOverrideNamedWithUnnamedSubscription { + RLMRealm *realm = [self openRealm]; + RLMSyncSubscriptionSet *subs = realm.subscriptions; + + [subs update:^{ + [subs addSubscriptionWithClassName:Person.className + subscriptionName:@"person_age_1" + where:@"age > 15"]; + [subs addSubscriptionWithClassName:Person.className + predicate:[NSPredicate predicateWithFormat:@"age > %d", 15]]; + }]; + + XCTAssertEqual(subs.version, 1UL); + XCTAssertEqual(subs.count, 2UL); +} + +- (void)testOverrideUnnamedWithNamedSubscription { + RLMRealm *realm = [self openRealm]; + RLMSyncSubscriptionSet *subs = realm.subscriptions; + + [subs update:^{ + [subs addSubscriptionWithClassName:Person.className + predicate:[NSPredicate predicateWithFormat:@"age > %d", 15]]; + [subs addSubscriptionWithClassName:Person.className + subscriptionName:@"person_age_1" + where:@"age > 15"]; + }]; + + XCTAssertEqual(subs.version, 1UL); + XCTAssertEqual(subs.count, 2UL); +} + +- (void)testAddSubscriptionInDifferentWriteBlocks { + RLMRealm *realm = [self openRealm]; + RLMSyncSubscriptionSet *subs = realm.subscriptions; + + [subs update:^{ + [subs addSubscriptionWithClassName:Person.className + subscriptionName:@"person_age_1" + where:@"age > 15"]; + }]; + + [subs update:^{ + [subs addSubscriptionWithClassName:Person.className + subscriptionName:@"person_age_2" + predicate:[NSPredicate predicateWithFormat:@"age > %d", 20]]; + }]; + + XCTAssertEqual(realm.subscriptions.version, 2UL); + XCTAssertEqual(realm.subscriptions.count, 2UL); + + RLMSyncSubscription *foundSubscription = [subs subscriptionWithName:@"person_age_1"]; + XCTAssertNotNil(foundSubscription); + + RLMSyncSubscription *foundSubscription2 = [subs subscriptionWithName:@"person_age_2"]; + XCTAssertNotNil(foundSubscription2); +} + +- (void)testRemoveSubscriptionByName { + RLMRealm *realm = [self openRealm]; + RLMSyncSubscriptionSet *subs = realm.subscriptions; + + [subs update:^{ + [subs addSubscriptionWithClassName:Person.className + subscriptionName:@"person_age_1" + where:@"age > 15"]; + [subs addSubscriptionWithClassName:Person.className + subscriptionName:@"person_age_2" + predicate:[NSPredicate predicateWithFormat:@"age > %d", 20]]; + }]; + + XCTAssertEqual(subs.version, 1UL); + XCTAssertEqual(subs.count, 2UL); + + [subs update:^{ + [subs removeSubscriptionWithName:@"person_age_1"]; + }]; + + XCTAssertEqual(subs.version, 2UL); + XCTAssertEqual(subs.count, 1UL); + + RLMSyncSubscription *foundSubscription = [subs subscriptionWithName:@"person_age_1"]; + XCTAssertNil(foundSubscription); + + RLMSyncSubscription *foundSubscription2 = [subs subscriptionWithName:@"person_age_2"]; + XCTAssertNotNil(foundSubscription2); +} + +- (void)testRemoveSubscriptionWithoutWriteThrow { + RLMRealm *realm = [self openRealm]; + RLMSyncSubscriptionSet *subs = realm.subscriptions; + + [subs update:^{ + [subs addSubscriptionWithClassName:Person.className + subscriptionName:@"person_age_1" + where:@"age > 15"]; + }]; + + XCTAssertEqual(subs.version, 1UL); + XCTAssertEqual(subs.count, 1UL); + RLMAssertThrowsWithReason([subs removeSubscriptionWithName:@"person_age_1"], @"Can only add, remove, or update subscriptions within a write subscription block."); +} + +- (void)testRemoveSubscriptionByQuery { + RLMRealm *realm = [self openRealm]; + RLMSyncSubscriptionSet *subs = realm.subscriptions; + + [subs update:^{ + [subs addSubscriptionWithClassName:Person.className + subscriptionName:@"person_age" + where:@"age > 15"]; + [subs addSubscriptionWithClassName:Person.className + subscriptionName:@"person_firstname" + where:@"firstName == %@", @"John"]; + [subs addSubscriptionWithClassName:Person.className + subscriptionName:@"person_lastname" + predicate:[NSPredicate predicateWithFormat:@"lastName == %@", @"Doe"]]; + }]; + + XCTAssertEqual(subs.version, 1UL); + XCTAssertEqual(subs.count, 3UL); + + [subs update:^{ + [subs removeSubscriptionWithClassName:Person.className where:@"firstName == %@", @"John"]; + [subs removeSubscriptionWithClassName:Person.className predicate:[NSPredicate predicateWithFormat:@"lastName == %@", @"Doe"]]; + }]; + + XCTAssertEqual(subs.version, 2UL); + XCTAssertEqual(subs.count, 1UL); + + RLMSyncSubscription *foundSubscription = [subs subscriptionWithName:@"person_age"]; + XCTAssertNotNil(foundSubscription); + + RLMSyncSubscription *foundSubscription2 = [subs subscriptionWithName:@"person_firstname"]; + XCTAssertNil(foundSubscription2); + + RLMSyncSubscription *foundSubscription3 = [subs subscriptionWithName:@"person_lastname"]; + XCTAssertNil(foundSubscription3); +} + +- (void)testRemoveSubscription { + RLMRealm *realm = [self openRealm]; + RLMSyncSubscriptionSet *subs = realm.subscriptions; + + [subs update:^{ + [subs addSubscriptionWithClassName:Person.className + subscriptionName:@"person_age" + where:@"age > 15"]; + [subs addSubscriptionWithClassName:Person.className + subscriptionName:@"person_firstname" + where:@"firstName == '%@'", @"John"]; + [subs addSubscriptionWithClassName:Person.className + subscriptionName:@"person_lastname" + predicate:[NSPredicate predicateWithFormat:@"lastName == %@", @"Doe"]]; + }]; + + XCTAssertEqual(subs.version, 1UL); + XCTAssertEqual(subs.count, 3UL); + + RLMSyncSubscription *foundSubscription = [subs subscriptionWithName:@"person_age"]; + XCTAssertNotNil(foundSubscription); + + [subs update:^{ + [subs removeSubscription:foundSubscription]; + }]; + + XCTAssertEqual(subs.version, 2UL); + XCTAssertEqual(subs.count, 2UL); + + RLMSyncSubscription *foundSubscription2 = [subs subscriptionWithName:@"person_firstname"]; + XCTAssertNotNil(foundSubscription2); + + [subs update:^{ + [subs removeSubscription:foundSubscription2]; + }]; + + XCTAssertEqual(subs.version, 3UL); + XCTAssertEqual(subs.count, 1UL); +} + +- (void)testRemoveAllSubscription { + RLMRealm *realm = [self openRealm]; + RLMSyncSubscriptionSet *subs = realm.subscriptions; + + [subs update:^{ + [subs addSubscriptionWithClassName:Person.className + subscriptionName:@"person_age" + where:@"age > 15"]; + [subs addSubscriptionWithClassName:Person.className + subscriptionName:@"person_firstname" + where:@"firstName == '%@'", @"John"]; + [subs addSubscriptionWithClassName:Person.className + subscriptionName:@"person_lastname" + predicate:[NSPredicate predicateWithFormat:@"lastName == %@", @"Doe"]]; + }]; + + XCTAssertEqual(subs.version, 1UL); + XCTAssertEqual(subs.count, 3UL); + + [subs update:^{ + [subs removeAllSubscriptions]; + }]; + + XCTAssertEqual(subs.version, 2UL); + XCTAssertEqual(subs.count, 0UL); + + RLMSyncSubscription *foundSubscription = [subs subscriptionWithName:@"person_age_3"]; + XCTAssertNil(foundSubscription); +} + +- (void)testRemoveAllSubscriptionForType { + RLMRealm *realm = [self openRealm]; + RLMSyncSubscriptionSet *subs = realm.subscriptions; + + [subs update:^{ + [subs addSubscriptionWithClassName:Person.className + subscriptionName:@"person_age" + where:@"age > 15"]; + [subs addSubscriptionWithClassName:Dog.className + subscriptionName:@"dog_name" + where:@"name == '%@'", @"Tomas"]; + [subs addSubscriptionWithClassName:Person.className + subscriptionName:@"person_lastname" + predicate:[NSPredicate predicateWithFormat:@"lastName == %@", @"Doe"]]; + }]; + + XCTAssertEqual(subs.version, 1UL); + XCTAssertEqual(subs.count, 3UL); + + [subs update:^{ + [subs removeAllSubscriptionsWithClassName:Person.className]; + }]; + + XCTAssertEqual(subs.version, 2UL); + XCTAssertEqual(subs.count, 1UL); + + RLMSyncSubscription *foundSubscription = [subs subscriptionWithName:@"dog_name"]; + XCTAssertNotNil(foundSubscription); + + [subs update:^{ + [subs removeAllSubscriptionsWithClassName:Dog.className]; + }]; + + XCTAssertEqual(subs.version, 3UL); + XCTAssertEqual(subs.count, 0UL); +} + +- (void)testUpdateSubscriptionQuery { + RLMRealm *realm = [self openRealm]; + RLMSyncSubscriptionSet *subs = realm.subscriptions; + + [subs update:^{ + [subs addSubscriptionWithClassName:Person.className + subscriptionName:@"person_age" + where:@"age > 15"]; + }]; + + XCTAssertEqual(subs.version, 1UL); + XCTAssertEqual(subs.count, 1UL); + + RLMSyncSubscription *foundSubscription = [subs subscriptionWithName:@"person_age"]; + XCTAssertNotNil(foundSubscription); + + [subs update:^{ + [foundSubscription updateSubscriptionWhere:@"age > 20"]; + }]; + + XCTAssertEqual(subs.version, 2UL); + XCTAssertEqual(subs.count, 1UL); + + RLMSyncSubscription *foundSubscription2 = [subs subscriptionWithName:@"person_age"]; + XCTAssertNotNil(foundSubscription2); + XCTAssertEqualObjects(foundSubscription2.queryString, @"age > 20"); + XCTAssertEqualObjects(foundSubscription2.objectClassName, @"Person"); +} + +- (void)testUpdateSubscriptionQueryWithoutWriteThrow { + RLMRealm *realm = [self openRealm]; + RLMSyncSubscriptionSet *subs = realm.subscriptions; + + [subs update:^{ + [subs addSubscriptionWithClassName:Person.className + subscriptionName:@"subscription_1" + where:@"age > 15"]; + }]; + + XCTAssertEqual(subs.version, 1UL); + XCTAssertEqual(subs.count, 1UL); + + RLMSyncSubscription *foundSubscription = [subs subscriptionWithName:@"subscription_1"]; + XCTAssertNotNil(foundSubscription); + + RLMAssertThrowsWithReason([foundSubscription updateSubscriptionWithPredicate:[NSPredicate predicateWithFormat:@"name == 'Tomas'"]], @"Can only add, remove, or update subscriptions within a write subscription block."); +} + +- (void)testSubscriptionSetIterate { + RLMRealm *realm = [self openRealm]; + RLMSyncSubscriptionSet *subs = realm.subscriptions; + + double numberOfSubs = 100; + [subs update:^{ + for (int i = 0; i < numberOfSubs; ++i) { + [subs addSubscriptionWithClassName:Person.className + subscriptionName:[NSString stringWithFormat:@"person_age_%d", i] + where:[NSString stringWithFormat:@"age > %d", i]]; + } + }]; + + XCTAssertEqual(subs.version, 1UL); + XCTAssertEqual(subs.count, (unsigned long)numberOfSubs); + + __weak id objects[(unsigned long)pow(numberOfSubs, 2.0) + (unsigned long)numberOfSubs]; + NSInteger count = 0; + for (RLMSyncSubscription *sub in subs) { + XCTAssertNotNil(sub); + objects[count++] = sub; + for (RLMSyncSubscription *sub in subs) { + objects[count++] = sub; + } + } + XCTAssertEqual(count, pow(numberOfSubs, 2) + numberOfSubs); +} + +- (void)testSubscriptionSetFirstAndLast { + RLMRealm *realm = [self openRealm]; + RLMSyncSubscriptionSet *subs = realm.subscriptions; + + XCTAssertNil(subs.firstObject); + XCTAssertNil(subs.lastObject); + + int numberOfSubs = 20; + [subs update:^{ + for (int i = 1; i <= numberOfSubs; ++i) { + [subs addSubscriptionWithClassName:Person.className + subscriptionName:[NSString stringWithFormat:@"person_age_%d", i] + where:[NSString stringWithFormat:@"age > %d", i]]; + } + }]; + + XCTAssertEqual(subs.version, 1UL); + XCTAssertEqual(subs.count, (unsigned long)numberOfSubs); + + RLMSyncSubscription *firstSubscription = subs.firstObject; + XCTAssertEqualObjects(firstSubscription.name, @"person_age_1"); + XCTAssertEqualObjects(firstSubscription.queryString, @"age > 1"); + + RLMSyncSubscription *lastSubscription = subs.lastObject; + XCTAssertEqualObjects(lastSubscription.name, ([NSString stringWithFormat:@"person_age_%d", numberOfSubs])); + XCTAssertEqualObjects(lastSubscription.queryString, ([NSString stringWithFormat:@"age > %d", numberOfSubs])); +} + +- (void)testSubscriptionSetSubscript { + RLMRealm *realm = [self openRealm]; + RLMSyncSubscriptionSet *subs = realm.subscriptions; + + XCTAssertEqual(subs.count, 0UL); + + int numberOfSubs = 20; + [subs update:^{ + for (int i = 1; i <= numberOfSubs; ++i) { + [subs addSubscriptionWithClassName:Person.className + subscriptionName:[NSString stringWithFormat:@"person_age_%d", i] + where:[NSString stringWithFormat:@"age > %d", i]]; + } + }]; + + XCTAssertEqual(subs.version, 1UL); + XCTAssertEqual(subs.count, (unsigned long)numberOfSubs); + + RLMSyncSubscription *firstSubscription = subs[0]; + XCTAssertEqualObjects(firstSubscription.name, @"person_age_1"); + XCTAssertEqualObjects(firstSubscription.queryString, @"age > 1"); + + RLMSyncSubscription *lastSubscription = subs[numberOfSubs-1]; + XCTAssertEqualObjects(lastSubscription.name, ([NSString stringWithFormat:@"person_age_%d", numberOfSubs])); + XCTAssertEqualObjects(lastSubscription.queryString, ([NSString stringWithFormat:@"age > %d", numberOfSubs])); + + int index = (numberOfSubs/2); + RLMSyncSubscription *objectAtIndexSubscription = [subs objectAtIndex:index]; + XCTAssertEqualObjects(objectAtIndexSubscription.name, ([NSString stringWithFormat:@"person_age_%d", index+1])); + XCTAssertEqualObjects(objectAtIndexSubscription.queryString, ([NSString stringWithFormat:@"age > %d", index+1])); +} +@end diff --git a/Realm/ObjectServerTests/RLMSyncTestCase.h b/Realm/ObjectServerTests/RLMSyncTestCase.h index a0c20f4cc2..f58013f7af 100644 --- a/Realm/ObjectServerTests/RLMSyncTestCase.h +++ b/Realm/ObjectServerTests/RLMSyncTestCase.h @@ -28,11 +28,16 @@ RLM_HEADER_AUDIT_BEGIN(nullability, sendability) @interface RealmServer : NSObject + (RealmServer *)shared; + (bool)haveServer; -- (NSString *)createAppForBSONType:(NSString *)bsonType error:(NSError **)error; -- (NSString *)createAppAndReturnError:(NSError **)error; -- (NSString *)createAppWithQueryableFields:(NSArray *)queryableFields error:(NSError **)error; -- (NSString *)createAppForAsymmetricSchema:(NSArray *)schema error:(NSError **)error; -- (void)deleteApp:(NSString *)appId error:(NSError **)error; +- (nullable NSString *)createAppWithFields:(NSArray *)fields + types:(nullable NSArray *)types + persistent:(bool)persistent + error:(NSError **)error; +- (nullable NSString *)createAppWithPartitionKeyType:(NSString *)type + types:(nullable NSArray *)types + persistent:(bool)persistent + error:(NSError **)error; +- (BOOL)deleteAppsAndReturnError:(NSError **)error; +- (BOOL)deleteApp:(NSString *)appId error:(NSError **)error; @end @interface AsyncOpenConnectionTimeoutTransport : RLMNetworkTransport @@ -48,14 +53,22 @@ RLM_HEADER_AUDIT_BEGIN(nullability, sendability) /// Any stray app ids passed between processes @property (nonatomic, readonly) NSArray *appIds; -- (RLMUser *)userForTest:(SEL)sel; +#pragma mark - Customization points + +- (NSArray *)defaultObjectTypes; +- (nullable NSString *)createAppWithError:(NSError **)error; +- (nullable NSString *)createFlexibleSyncAppWithError:(NSError **)error; +- (RLMRealmConfiguration *)configurationForUser:(RLMUser *)user; + +#pragma mark - Helpers -- (RLMRealm *)realmForTest:(SEL)sel; +- (RLMUser *)userForTest:(SEL)sel; +- (RLMUser *)userForTest:(SEL)sel app:(RLMApp *)app; - (RLMCredentials *)basicCredentialsWithName:(NSString *)name register:(BOOL)shouldRegister NS_SWIFT_NAME(basicCredentials(name:register:)); - (RLMCredentials *)basicCredentialsWithName:(NSString *)name register:(BOOL)shouldRegister - app:(nullable RLMApp*)app NS_SWIFT_NAME(basicCredentials(name:register:app:)); + app:(RLMApp*)app NS_SWIFT_NAME(basicCredentials(name:register:app:)); /// Synchronously open a synced Realm via asyncOpen and return the Realm. - (RLMRealm *)asyncOpenRealmWithConfiguration:(RLMRealmConfiguration *)configuration; @@ -63,6 +76,13 @@ RLM_HEADER_AUDIT_BEGIN(nullability, sendability) /// Synchronously open a synced Realm via asyncOpen and return the expected error. - (NSError *)asyncOpenErrorWithConfiguration:(RLMRealmConfiguration *)configuration; +- (RLMRealmConfiguration *)configuration NS_REFINED_FOR_SWIFT; + +// Open the realm with the partition value `self.name` using a newly created user +- (RLMRealm *)openRealm NS_REFINED_FOR_SWIFT; +// Open the realm with the partition value `self.name` using the given user +- (RLMRealm *)openRealmWithUser:(RLMUser *)user; + /// Synchronously open a synced Realm and wait for downloads. - (RLMRealm *)openRealmForPartitionValue:(nullable id)partitionValue user:(RLMUser *)user; @@ -101,6 +121,10 @@ RLM_HEADER_AUDIT_BEGIN(nullability, sendability) - (RLMUser *)logInUserForCredentials:(RLMCredentials *)credentials; - (RLMUser *)logInUserForCredentials:(RLMCredentials *)credentials app:(RLMApp *)app; +/// Synchronously register and log in a new non-anonymous user +- (RLMUser *)createUser; +- (RLMUser *)createUserForApp:(RLMApp *)app; + - (RLMCredentials *)jwtCredentialWithAppId:(NSString *)appId; /// Synchronously, log out. @@ -110,12 +134,6 @@ RLM_HEADER_AUDIT_BEGIN(nullability, sendability) - (void)addAllTypesSyncObjectToRealm:(RLMRealm *)realm values:(NSDictionary *)dictionary person:(Person *)person; -/// Synchronously wait for downloads to complete for any number of Realms, and then check their `SyncObject` counts. -- (void)waitForDownloadsForUser:(RLMUser *)user - realms:(NSArray *)realms - partitionValues:(NSArray *)partitionValues - expectedCounts:(NSArray *)counts; - /// Wait for downloads to complete; drop any error. - (void)waitForDownloadsForRealm:(RLMRealm *)realm; - (void)waitForDownloadsForRealm:(RLMRealm *)realm error:(NSError **)error; @@ -124,17 +142,10 @@ RLM_HEADER_AUDIT_BEGIN(nullability, sendability) - (void)waitForUploadsForRealm:(RLMRealm *)realm; - (void)waitForUploadsForRealm:(RLMRealm *)realm error:(NSError **)error; -/// Wait for downloads to complete while spinning the runloop. This method uses expectations. -- (void)waitForDownloadsForUser:(RLMUser *)user - partitionValue:(NSString *)partitionValue - expectation:(nullable XCTestExpectation *)expectation - error:(NSError **)error; - /// Set the user's tokens to invalid ones to test invalid token handling. - (void)setInvalidTokensForUser:(RLMUser *)user; -- (void)writeToPartition:(SEL)testSel block:(void (^)(RLMRealm *))block; -- (void)writeToPartition:(nullable NSString *)testName userName:(NSString *)userNameBase block:(void (^)(RLMRealm *))block; +- (void)writeToPartition:(nullable NSString *)partition block:(void (^)(RLMRealm *))block; - (void)resetSyncManager; @@ -146,19 +157,13 @@ RLM_HEADER_AUDIT_BEGIN(nullability, sendability) - (NSString *)partitionBsonType:(id)bson; -- (RLMApp *)appWithId:(NSString *)appId; +- (RLMApp *)appWithId:(NSString *)appId NS_SWIFT_NAME(app(id:)); - (void)resetAppCache; #pragma mark Flexible Sync App -@property (nonatomic, readonly) NSString *flexibleSyncAppId; -@property (nonatomic, readonly) RLMApp *flexibleSyncApp; - -- (RLMUser *)flexibleSyncUser:(SEL)testSel; -- (RLMRealm *)openFlexibleSyncRealm:(SEL)testSel; -- (RLMRealm *)getFlexibleSyncRealm:(SEL)testSel; -- (bool)populateData:(void (^)(RLMRealm *))block; +- (void)populateData:(void (^)(RLMRealm *))block; - (void)writeQueryAndCompleteForRealm:(RLMRealm *)realm block:(void (^)(RLMSyncSubscriptionSet *))block; @end @@ -172,6 +177,10 @@ RLM_HEADER_AUDIT_BEGIN(nullability, sendability) - (void)unpause; @end +@interface RLMUser (Test) +- (RLMMongoCollection *)collectionForType:(Class)type app:(RLMApp *)app NS_SWIFT_NAME(collection(for:app:)); +@end + FOUNDATION_EXTERN int64_t RLMGetClientFileIdent(RLMRealm *realm); RLM_HEADER_AUDIT_END(nullability, sendability) diff --git a/Realm/ObjectServerTests/RLMSyncTestCase.mm b/Realm/ObjectServerTests/RLMSyncTestCase.mm index 1874d4acfb..0310b197a9 100644 --- a/Realm/ObjectServerTests/RLMSyncTestCase.mm +++ b/Realm/ObjectServerTests/RLMSyncTestCase.mm @@ -55,7 +55,7 @@ - (BOOL)waitForUploadCompletionOnQueue:(dispatch_queue_t)queue callback:(void(^) - (BOOL)waitForDownloadCompletionOnQueue:(dispatch_queue_t)queue callback:(void(^)(NSError *))callback; @end -@interface RLMUser() +@interface RLMUser () - (std::shared_ptr)_syncUser; @end @@ -89,23 +89,23 @@ - (void)sendRequestToServer:(RLMRequest *)request completion:(RLMNetworkTranspor #pragma mark RLMSyncTestCase @implementation RLMSyncTestCase { - NSString *_appId; RLMApp *_app; - NSString *_flexibleSyncAppId; - RLMApp *_flexibleSyncApp; +} + +- (NSArray *)defaultObjectTypes { + return @[Person.self]; } #pragma mark - Helper methods - (RLMUser *)userForTest:(SEL)sel { - return [self logInUserForCredentials:[self basicCredentialsWithName:NSStringFromSelector(sel) - register:self.isParent]]; + return [self userForTest:sel app:self.app]; } -- (RLMRealm *)realmForTest:(SEL)sel { - RLMUser *user = [self userForTest:sel]; - NSString *realmId = NSStringFromSelector(sel); - return [self openRealmForPartitionValue:realmId user:user]; +- (RLMUser *)userForTest:(SEL)sel app:(RLMApp *)app { + return [self logInUserForCredentials:[self basicCredentialsWithName:NSStringFromSelector(sel) + register:self.isParent app:app] + app:app]; } - (RLMUser *)anonymousUser { @@ -113,21 +113,19 @@ - (RLMUser *)anonymousUser { } - (RLMCredentials *)basicCredentialsWithName:(NSString *)name register:(BOOL)shouldRegister { - return [self basicCredentialsWithName:name register:shouldRegister app:nil]; + return [self basicCredentialsWithName:name register:shouldRegister app:self.app]; } -- (RLMCredentials *)basicCredentialsWithName:(NSString *)name register:(BOOL)shouldRegister app:(nullable RLMApp *) app { +- (RLMCredentials *)basicCredentialsWithName:(NSString *)name register:(BOOL)shouldRegister app:(RLMApp *)app { if (shouldRegister) { - XCTestExpectation *expectation = [self expectationWithDescription:@""]; - RLMApp *currentApp = app ?: self.app; - [currentApp.emailPasswordAuth registerUserWithEmail:name password:@"password" completion:^(NSError *error) { + XCTestExpectation *ex = [self expectationWithDescription:@""]; + [app.emailPasswordAuth registerUserWithEmail:name password:@"password" completion:^(NSError *error) { XCTAssertNil(error); - [expectation fulfill]; + [ex fulfill]; }]; - [self waitForExpectationsWithTimeout:20.0 handler:nil]; + [self waitForExpectations:@[ex] timeout:20.0]; } - return [RLMCredentials credentialsWithEmail:name - password:@"password"]; + return [RLMCredentials credentialsWithEmail:name password:@"password"]; } - (RLMAppConfiguration*)defaultAppConfiguration { @@ -152,35 +150,38 @@ - (void)addAllTypesSyncObjectToRealm:(RLMRealm *)realm values:(NSDictionary *)di [realm commitWriteTransaction]; } -- (void)waitForDownloadsForUser:(RLMUser *)user - realms:(NSArray *)realms - partitionValues:(NSArray *)partitionValues - expectedCounts:(NSArray *)counts { - NSAssert(realms.count == counts.count && realms.count == partitionValues.count, - @"Test logic error: all array arguments must be the same size."); - for (NSUInteger i = 0; i < realms.count; i++) { - [self waitForDownloadsForUser:user partitionValue:partitionValues[i] expectation:nil error:nil]; - [realms[i] refresh]; - CHECK_COUNT([counts[i] integerValue], Person, realms[i]); - } +- (RLMRealmConfiguration *)configuration { + RLMRealmConfiguration *configuration = [self configurationForUser:self.createUser]; + configuration.objectClasses = self.defaultObjectTypes; + return configuration; +} + +- (RLMRealmConfiguration *)configurationForUser:(RLMUser *)user { + return [user configurationWithPartitionValue:self.name]; +} + +- (RLMRealm *)openRealm { + return [self openRealmWithUser:self.createUser]; +} + +- (RLMRealm *)openRealmWithUser:(RLMUser *)user { + auto c = [self configurationForUser:user]; + c.objectClasses = self.defaultObjectTypes; + return [self openRealmWithConfiguration:c]; } - (RLMRealm *)openRealmForPartitionValue:(nullable id)partitionValue user:(RLMUser *)user { - return [self openRealmForPartitionValue:partitionValue - user:user - clientResetMode:RLMClientResetModeRecoverUnsyncedChanges - encryptionKey:nil - stopPolicy:RLMSyncStopPolicyAfterChangesUploaded]; + auto c = [user configurationWithPartitionValue:partitionValue]; + c.objectClasses = self.defaultObjectTypes; + return [self openRealmWithConfiguration:c]; } - (RLMRealm *)openRealmForPartitionValue:(nullable id)partitionValue user:(RLMUser *)user clientResetMode:(RLMClientResetMode)clientResetMode { - return [self openRealmForPartitionValue:partitionValue - user:user - clientResetMode:clientResetMode - encryptionKey:nil - stopPolicy:RLMSyncStopPolicyAfterChangesUploaded]; + auto c = [user configurationWithPartitionValue:partitionValue clientResetMode:clientResetMode]; + c.objectClasses = self.defaultObjectTypes; + return [self openRealmWithConfiguration:c]; } - (RLMRealm *)openRealmForPartitionValue:(nullable id)partitionValue @@ -282,15 +283,22 @@ - (RLMRealm *)immediatelyOpenRealmForPartitionValue:(NSString *)partitionValue stopPolicy:(RLMSyncStopPolicy)stopPolicy { auto c = [user configurationWithPartitionValue:partitionValue clientResetMode:clientResetMode]; c.encryptionKey = encryptionKey; - c.objectClasses = @[Dog.self, Person.self, HugeSyncObject.self, RLMSetSyncObject.self, - RLMArraySyncObject.self, UUIDPrimaryKeyObject.self, StringPrimaryKeyObject.self, - IntPrimaryKeyObject.self, AllTypesSyncObject.self, RLMDictionarySyncObject.self]; + c.objectClasses = self.defaultObjectTypes; RLMSyncConfiguration *syncConfig = c.syncConfiguration; syncConfig.stopPolicy = stopPolicy; c.syncConfiguration = syncConfig; return [RLMRealm realmWithConfiguration:c error:nil]; } +- (RLMUser *)createUser { + return [self createUserForApp:self.app]; +} + +- (RLMUser *)createUserForApp:(RLMApp *)app { + NSString *name = [self.name stringByAppendingFormat:@" %@", NSUUID.UUID.UUIDString]; + return [self logInUserForCredentials:[self basicCredentialsWithName:name register:YES app:app] app:app]; +} + - (RLMUser *)logInUserForCredentials:(RLMCredentials *)credentials { return [self logInUserForCredentials:credentials app:self.app]; } @@ -380,34 +388,13 @@ - (void)waitForUploadsForRealm:(RLMRealm *)realm { [self waitForUploadsForRealm:realm error:nil]; } -- (void)waitForDownloadsForUser:(RLMUser *)user - partitionValue:(NSString *)partitionValue - expectation:(XCTestExpectation *)expectation - error:(NSError **)error { - RLMSyncSession *session = [user sessionForPartitionValue:partitionValue]; - NSAssert(session, @"Cannot call with invalid partition value"); - XCTestExpectation *ex = expectation ?: [self expectationWithDescription:@"Wait for download completion"]; - __block NSError *theError = nil; - BOOL queued = [session waitForDownloadCompletionOnQueue:dispatch_get_global_queue(0, 0) callback:^(NSError *err) { - theError = err; - [ex fulfill]; - }]; - if (!queued) { - XCTFail(@"Download waiter did not queue; session was invalid or errored out."); - return; - } - [self waitForExpectations:@[ex] timeout:60.0]; - if (error) { - *error = theError; - } -} - - (void)waitForUploadsForRealm:(RLMRealm *)realm error:(NSError **)error { RLMSyncSession *session = realm.syncSession; NSAssert(session, @"Cannot call with invalid Realm"); XCTestExpectation *ex = [self expectationWithDescription:@"Wait for upload completion"]; __block NSError *completionError; - BOOL queued = [session waitForUploadCompletionOnQueue:dispatch_get_global_queue(0, 0) callback:^(NSError *error) { + BOOL queued = [session waitForUploadCompletionOnQueue:dispatch_get_global_queue(0, 0) + callback:^(NSError *error) { completionError = error; [ex fulfill]; }]; @@ -425,7 +412,8 @@ - (void)waitForDownloadsForRealm:(RLMRealm *)realm error:(NSError **)error { NSAssert(session, @"Cannot call with invalid Realm"); XCTestExpectation *ex = [self expectationWithDescription:@"Wait for download completion"]; __block NSError *completionError; - BOOL queued = [session waitForDownloadCompletionOnQueue:nil callback:^(NSError *error) { + BOOL queued = [session waitForDownloadCompletionOnQueue:dispatch_get_global_queue(0, 0) + callback:^(NSError *error) { completionError = error; [ex fulfill]; }]; @@ -442,23 +430,15 @@ - (void)waitForDownloadsForRealm:(RLMRealm *)realm error:(NSError **)error { - (void)setInvalidTokensForUser:(RLMUser *)user { auto token = self.badAccessToken.UTF8String; + user._syncUser->log_out(); user._syncUser->log_in(token, token); } -- (void)writeToPartition:(SEL)testSel block:(void (^)(RLMRealm *))block { - NSString *testName = NSStringFromSelector(testSel); - [self writeToPartition:testName userName:testName block:block]; -} - -- (void)writeToPartition:(nullable NSString *)testName userName:(NSString *)userNameBase block:(void (^)(RLMRealm *))block { +- (void)writeToPartition:(NSString *)partition block:(void (^)(RLMRealm *))block { @autoreleasepool { - NSString *userName = [userNameBase stringByAppendingString:[NSUUID UUID].UUIDString]; - RLMUser *user = [self logInUserForCredentials:[self basicCredentialsWithName:userName - register:YES]]; - auto c = [user configurationWithPartitionValue:testName]; - c.objectClasses = @[Dog.self, Person.self, HugeSyncObject.self, RLMSetSyncObject.self, - RLMArraySyncObject.self, UUIDPrimaryKeyObject.self, StringPrimaryKeyObject.self, - IntPrimaryKeyObject.self, AllTypesSyncObject.self, RLMDictionarySyncObject.self]; + RLMUser *user = [self createUser]; + auto c = [user configurationWithPartitionValue:partition]; + c.objectClasses = self.defaultObjectTypes; [self writeToConfiguration:c block:block]; } } @@ -523,37 +503,46 @@ - (void)setUp { - (void)tearDown { [self resetSyncManager]; + [RealmServer.shared deleteAppsAndReturnError:nil]; [super tearDown]; } -- (NSString *)appId { - if (!_appId) { - static NSString *s_appId; - if (self.isParent && s_appId) { - _appId = s_appId; - } - else { - NSError *error; - _appId = NSProcessInfo.processInfo.environment[@"RLMParentAppId"] ?: [RealmServer.shared createAppAndReturnError:&error]; - if (error) { - NSLog(@"Failed to create app: %@", error); - abort(); - } +static NSString *s_appId; +static bool s_opensApp; ++ (void)tearDown { + if (s_appId && s_opensApp) { + [RealmServer.shared deleteApp:s_appId error:nil]; + s_appId = nil; + s_opensApp = false; + } +} - if (self.isParent) { - s_appId = _appId; - } - } +- (NSString *)appId { + if (s_appId) { + return s_appId; } - return _appId; + if (NSString *appId = NSProcessInfo.processInfo.environment[@"RLMParentAppId"]) { + return s_appId = appId; + } + NSError *error; + s_appId = [self createAppWithError:&error]; + if (error) { + NSLog(@"Failed to create app: %@", error); + abort(); + } + s_opensApp = true; + return s_appId; +} + +- (NSString *)createAppWithError:(NSError **)error { + return [RealmServer.shared createAppWithPartitionKeyType:@"string" + types:self.defaultObjectTypes + persistent:true error:error]; } - (RLMApp *)app { if (!_app) { _app = [self appWithId:self.appId]; - RLMSyncManager *syncManager = self.app.syncManager; - syncManager.userAgent = self.name; - [RLMLogger defaultLogger].level = RLMLogLevelOff; } return _app; } @@ -610,7 +599,7 @@ - (NSString *)badAccessToken { - (void)cleanupRemoteDocuments:(RLMMongoCollection *)collection { XCTestExpectation *deleteManyExpectation = [self expectationWithDescription:@"should delete many documents"]; [collection deleteManyDocumentsWhere:@{} - completion:^(NSInteger, NSError * error) { + completion:^(NSInteger, NSError *error) { XCTAssertNil(error); [deleteManyExpectation fulfill]; }]; @@ -626,17 +615,21 @@ - (NSURL *)clientDataRoot { } - (NSTask *)childTask { - return [self childTaskWithAppIds:_appId ? @[_appId] : @[]]; + return [self childTaskWithAppIds:s_appId ? @[s_appId] : @[]]; } - (RLMApp *)appWithId:(NSString *)appId { auto config = self.defaultAppConfiguration; config.appId = appId; - return [RLMApp appWithConfiguration:config]; + RLMApp *app = [RLMApp appWithConfiguration:config]; + RLMSyncManager *syncManager = app.syncManager; + syncManager.userAgent = self.name; + RLMLogger.defaultLogger.level = RLMLogLevelWarn; + return app; } - (NSString *)partitionBsonType:(id)bson { - switch(bson.bsonType){ + switch (bson.bsonType){ case RLMBSONTypeString: return @"string"; case RLMBSONTypeUUID: @@ -647,118 +640,29 @@ - (NSString *)partitionBsonType:(id)bson { case RLMBSONTypeObjectId: return @"objectId"; default: - return(@""); - } -} - -#pragma mark Flexible Sync App - -- (NSString *)flexibleSyncAppId { - if (!_flexibleSyncAppId) { - static NSString *s_appId; - if (s_appId) { - _flexibleSyncAppId = s_appId; - } - else { - NSError *error; - _flexibleSyncAppId = [RealmServer.shared createAppWithQueryableFields:@[@"age", @"breed", @"partition", @"firstName", @"boolCol", @"intCol", @"stringCol", @"dateCol", @"lastName", @"_id", @"uuidCol"] error:&error]; - if (error) { - NSLog(@"Failed to create app: %@", error); - abort(); - } - - s_appId = _flexibleSyncAppId; - } + return @""; } - return _flexibleSyncAppId; } -- (RLMApp *)flexibleSyncApp { - if (!_flexibleSyncApp) { - _flexibleSyncApp = [self appWithId:self.flexibleSyncAppId]; - RLMSyncManager *syncManager = self.flexibleSyncApp.syncManager; - RLMLogger.defaultLogger.level = RLMLogLevelOff; - syncManager.userAgent = self.name; - } - return _flexibleSyncApp; -} - -- (RLMRealm *)flexibleSyncRealmForUser:(RLMUser *)user { - RLMRealmConfiguration *config = [user flexibleSyncConfiguration]; - config.objectClasses = @[Dog.self, - Person.self]; - RLMRealm *realm = [RLMRealm realmWithConfiguration:config error:nil]; - [self waitForDownloadsForRealm:realm]; - return realm; -} - -- (RLMUser *)flexibleSyncUser:(SEL)testSel { - return [self logInUserForCredentials:[self basicCredentialsWithName:NSStringFromSelector(testSel) - register:YES - app:self.flexibleSyncApp] - app:self.flexibleSyncApp]; -} - -- (RLMRealm *)getFlexibleSyncRealm:(SEL)testSel { - RLMRealm *realm = [self flexibleSyncRealmForUser:[self flexibleSyncUser:testSel]]; - XCTAssertNotNil(realm); - return realm; -} +#pragma mark Flexible Sync App -- (RLMRealm *)openFlexibleSyncRealm:(SEL)testSel { - RLMUser *user = [self flexibleSyncUser:testSel]; - RLMRealmConfiguration *config = [user flexibleSyncConfiguration]; - config.objectClasses = @[Dog.self, - Person.self]; - RLMRealm *realm = [RLMRealm realmWithConfiguration:config error:nil]; - XCTAssertNotNil(realm); - return realm; +- (NSString *)createFlexibleSyncAppWithError:(NSError **)error { + NSArray *fields = @[@"age", @"breed", @"partition", @"firstName", @"boolCol", @"intCol", @"stringCol", @"dateCol", @"lastName", @"_id", @"uuidCol"]; + return [RealmServer.shared createAppWithFields:fields + types:self.defaultObjectTypes + persistent:true + error:error]; } -- (bool)populateData:(void (^)(RLMRealm *))block { - return [self writeToFlxRealm:^(RLMRealm *realm) { - [realm beginWriteTransaction]; - block(realm); - [realm commitWriteTransaction]; - [self waitForUploadsForRealm:realm]; - }]; +- (void)populateData:(void (^)(RLMRealm *))block { + RLMRealm *realm = [self openRealm]; + RLMRealmSubscribeToAll(realm); + [realm beginWriteTransaction]; + block(realm); + [realm commitWriteTransaction]; + [self waitForUploadsForRealm:realm]; } -- (bool)writeToFlxRealm:(void (^)(RLMRealm *))block { - NSString *userName = [NSStringFromSelector(_cmd) stringByAppendingString:[NSUUID UUID].UUIDString]; - RLMUser *user = [self logInUserForCredentials:[self basicCredentialsWithName:userName - register:YES - app:self.flexibleSyncApp] - app:self.flexibleSyncApp]; - RLMRealmConfiguration *config = [user flexibleSyncConfiguration]; - config.objectClasses = @[Dog.self, Person.self]; - RLMRealm *realm = [RLMRealm realmWithConfiguration:config error:nil]; - - RLMSyncSubscriptionSet *subs = realm.subscriptions; - XCTAssertNotNil(subs); - - XCTestExpectation *ex = [self expectationWithDescription:@"state change complete"]; - [subs update:^{ - [subs addSubscriptionWithClassName:Person.className - subscriptionName:@"person_all" - where:@"TRUEPREDICATE"]; - [subs addSubscriptionWithClassName:Dog.className - subscriptionName:@"dog_all" - where:@"TRUEPREDICATE"]; - } onComplete: ^(NSError *error){ - XCTAssertNil(error); - [ex fulfill]; - }]; - - __block bool didComplete = false; - [self waitForExpectationsWithTimeout:60.0 handler:^(NSError *error) { - didComplete = error == nil; - }]; - if (didComplete) { - block(realm); - } - return didComplete; -} - (void)writeQueryAndCompleteForRealm:(RLMRealm *)realm block:(void (^)(RLMSyncSubscriptionSet *))block { RLMSyncSubscriptionSet *subs = realm.subscriptions; XCTAssertNotNil(subs); @@ -800,6 +704,14 @@ - (void)waitForCompletion { } @end +@implementation RLMUser (Test) +- (RLMMongoCollection *)collectionForType:(Class)type app:(RLMApp *)app { + return [[[self mongoClientWithServiceName:@"mongodb1"] + databaseWithName:@"test_data"] + collectionWithName:[NSString stringWithFormat:@"%@ %@", [type className], app.appId]]; +} +@end + int64_t RLMGetClientFileIdent(RLMRealm *realm) { return realm::SyncSession::OnlyForTesting::get_file_ident(*realm->_realm->sync_session()).ident; } diff --git a/Realm/ObjectServerTests/RLMUser+ObjectServerTests.h b/Realm/ObjectServerTests/RLMUser+ObjectServerTests.h index 981c86b9c3..d73e7865f2 100644 --- a/Realm/ObjectServerTests/RLMUser+ObjectServerTests.h +++ b/Realm/ObjectServerTests/RLMUser+ObjectServerTests.h @@ -21,8 +21,6 @@ RLM_HEADER_AUDIT_BEGIN(nullability) @interface RLMUser (ObjectServerTests) -- (BOOL)waitForUploadToFinish:(NSString *)partitionValue; -- (BOOL)waitForDownloadToFinish:(NSString *)partitionValue; - (void)simulateClientResetErrorForSession:(NSString *)partitionValue; @end diff --git a/Realm/ObjectServerTests/RLMUser+ObjectServerTests.mm b/Realm/ObjectServerTests/RLMUser+ObjectServerTests.mm index 0baccafde1..073605054c 100644 --- a/Realm/ObjectServerTests/RLMUser+ObjectServerTests.mm +++ b/Realm/ObjectServerTests/RLMUser+ObjectServerTests.mm @@ -29,36 +29,6 @@ @implementation RLMUser (ObjectServerTests) -- (BOOL)waitForUploadToFinish:(NSString *)partitionValue { - const NSTimeInterval timeout = 20; - dispatch_semaphore_t sema = dispatch_semaphore_create(0); - RLMSyncSession *session = [self sessionForPartitionValue:partitionValue]; - NSAssert(session, @"Cannot call with invalid URL"); - BOOL couldWait = [session waitForUploadCompletionOnQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0) - callback:^(NSError *){ - dispatch_semaphore_signal(sema); - }]; - if (!couldWait) { - return NO; - } - return dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC))) == 0; -} - -- (BOOL)waitForDownloadToFinish:(NSString *)partitionValue { - const NSTimeInterval timeout = 20; - dispatch_semaphore_t sema = dispatch_semaphore_create(0); - RLMSyncSession *session = [self sessionForPartitionValue:partitionValue]; - NSAssert(session, @"Cannot call with invalid URL"); - BOOL couldWait = [session waitForDownloadCompletionOnQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0) - callback:^(NSError *){ - dispatch_semaphore_signal(sema); - }]; - if (!couldWait) { - return NO; - } - return dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC))) == 0; -} - - (void)simulateClientResetErrorForSession:(NSString *)partitionValue { RLMSyncSession *session = [self sessionForPartitionValue:partitionValue]; NSAssert(session, @"Cannot call with invalid URL"); diff --git a/Realm/ObjectServerTests/RealmServer.swift b/Realm/ObjectServerTests/RealmServer.swift index d0f8a98574..e5c1264e93 100644 --- a/Realm/ObjectServerTests/RealmServer.swift +++ b/Realm/ObjectServerTests/RealmServer.swift @@ -143,7 +143,7 @@ typealias Json = Any #endif private extension ObjectSchema { - func stitchRule(_ partitionKeyType: String?, id: String? = nil) -> [String: Json] { + func stitchRule(_ partitionKeyType: String?, id: String? = nil, appId: String) -> [String: Json] { var stitchProperties: [String: Json] = [:] // We only add a partition property for pbs @@ -165,7 +165,7 @@ private extension ObjectSchema { } else if id != nil { stitchProperties[property.columnName] = property.stitchRule(self) relationships[property.columnName] = [ - "ref": "#/relationship/mongodb1/test_data/\(property.objectClassName!)", + "ref": "#/relationship/mongodb1/test_data/\(property.objectClassName!) \(appId)", "foreign_key": "_id", "is_list": property.isArray || property.isSet || property.isMap ] @@ -184,7 +184,7 @@ private extension ObjectSchema { "metadata": [ "data_source": "mongodb1", "database": "test_data", - "collection": "\(className)", + "collection": "\(className) \(appId)" ], "relationships": relationships ] @@ -208,6 +208,14 @@ struct AdminProfile: Codable { extension DispatchGroup: @unchecked Sendable { } +private extension DispatchGroup { + func throwingWait(timeout: DispatchTime) throws { + if wait(timeout: timeout) == .timedOut { + throw URLError(.timedOut) + } + } +} + // MARK: AdminSession /// An authenticated session for using the Admin API @available(macOS 10.12, *) @@ -440,9 +448,9 @@ class Admin { loginRequest.allHTTPHeaderFields = ["Content-Type": "application/json;charset=utf-8", "Accept": "application/json"] - loginRequest.httpBody = try! JSONEncoder().encode(["provider": "userpass", - "username": "unique_user@domain.com", - "password": "password"]) + loginRequest.httpBody = try JSONEncoder().encode(["provider": "userpass", + "username": "unique_user@domain.com", + "password": "password"]) return try URLSession(configuration: .default, delegate: nil, delegateQueue: OperationQueue()) .resultDataTask(with: loginRequest) .flatMap { data in @@ -511,6 +519,9 @@ public class RealmServer: NSObject { /// The current admin session private var session: AdminSession? + /// Created appIds which should be cleaned up + private var appIds = [String]() + /// Check if the BaaS files are present and we can run the server @objc public class func haveServer() -> Bool { let goDir = RealmServer.buildDir.appendingPathComponent("stitch") @@ -570,9 +581,9 @@ public class RealmServer: NSObject { /// Launch the mongo server in the background. /// This process should run until the test suite is complete. private func launchMongoProcess() throws { - try! FileManager().createDirectory(at: tempDir, - withIntermediateDirectories: false, - attributes: nil) + try FileManager().createDirectory(at: tempDir, + withIntermediateDirectories: false, + attributes: nil) mongoProcess.launchPath = RealmServer.binDir.appendingPathComponent("mongod").path mongoProcess.arguments = [ @@ -635,8 +646,8 @@ public class RealmServer: NSObject { serverProcess.environment = env // golang server needs a tmp directory - try! FileManager.default.createDirectory(atPath: "\(tempDir.path)/tmp", - withIntermediateDirectories: false, attributes: nil) + try FileManager.default.createDirectory(atPath: "\(tempDir.path)/tmp", + withIntermediateDirectories: false, attributes: nil) serverProcess.launchPath = "\(binDir)/stitch_server" serverProcess.currentDirectoryPath = tempDir.path serverProcess.arguments = [ @@ -671,7 +682,7 @@ public class RealmServer: NSObject { parts.append("🔴") } else if let json = try? JSONSerialization.jsonObject(with: part.data(using: .utf8)!) { parts.append(String(data: try! JSONSerialization.data(withJSONObject: json, - options: .prettyPrinted), + options: .prettyPrinted), encoding: .utf8)!) } else if !part.isEmpty { parts.append(String(part)) @@ -680,6 +691,7 @@ public class RealmServer: NSObject { print(parts.joined(separator: "\t")) } + serverProcess.standardError = nil if logLevel != .none { serverProcess.standardOutput = pipe } else { @@ -699,7 +711,7 @@ public class RealmServer: NSObject { delegateQueue: OperationQueue()) session.dataTask(with: URL(string: "http://localhost:9090/api/admin/v3.0/groups/groupId/apps/appId")!) { (_, _, error) in if error != nil { - usleep(50000) + Thread.sleep(forTimeInterval: 0.1) pingServer(tries + 1) } else { group.leave() @@ -749,9 +761,7 @@ public class RealmServer: NSObject { public typealias AppId = String /// Create a new server app - /// This will create a App with different configuration depending on the SyncMode (partition based sync or flexible sync), partition type is used only in case - /// this is partition based sync, and will crash if one is not provided in that mode - func createAppForSyncMode(_ syncMode: SyncMode, _ objectsSchema: [ObjectSchema]) throws -> AppId { + func createApp(syncMode: SyncMode, types: [ObjectBase.Type], persistent: Bool) throws -> AppId { let session = try XCTUnwrap(session) let info = try session.apps.post(["name": "test"]).get() @@ -810,10 +820,12 @@ public class RealmServer: NSObject { } } - _ = app.secrets.post([ + app.secrets.post(on: group, [ "name": "BackingDB_uri", "value": "mongodb://localhost:26000" - ]) + ], failOnError) + + try group.throwingWait(timeout: .now() + 5.0) let appService: [String: Json] = [ "name": "mongodb1", @@ -828,54 +840,42 @@ public class RealmServer: NSObject { throw URLError(.badServerResponse) } - // Creating the schema is a two-step process where we first add all the - // objects with their properties to them so that we can add relationships - let syncTypes: [ObjectSchema] + let schema = types.map { ObjectiveCSupport.convert(object: $0.sharedSchema()!) } + let partitionKeyType: String? if case .pbs(let bsonType) = syncMode { - syncTypes = objectsSchema.filter { - guard let pk = $0.primaryKeyProperty else { return false } - return pk.columnName == "_id" - } partitionKeyType = bsonType } else { - syncTypes = objectsSchema.filter { - let validSyncClasses = ["Dog", "Person", "SwiftPerson", "SwiftTypesSyncObject", "PersonAsymmetric", "SwiftObjectAsymmetric", "HugeObjectAsymmetric", "SwiftCustomColumnObject", "SwiftCustomColumnAsymmetricObject"] - return validSyncClasses.contains($0.className) - } partitionKeyType = nil } - var schemaCreations = [Result]() - var asymmetricTables = [String]() - for objectSchema in syncTypes { - schemaCreations.append(app.schemas.post(objectSchema.stitchRule(partitionKeyType))) - if objectSchema.isAsymmetric { - asymmetricTables.append(objectSchema.className) - } - } - var schemaIds: [String: String] = [:] - for result in schemaCreations { - guard case .success(let data) = result else { - fatalError("Failed to create schema: \(result)") + // Creating the schema is a two-step process where we first add all the + // objects with their properties to them so that we can add relationships + let lockedSchemaIds = Locked([String: String]()) + for objectSchema in schema { + app.schemas.post(on: group, objectSchema.stitchRule(partitionKeyType, appId: clientAppId)) { + switch $0 { + case .success(let data): + lockedSchemaIds.withLock { + $0[objectSchema.className] = ((data as! [String: Any])["_id"] as! String) + } + case .failure(let error): + XCTFail(error.localizedDescription) + } } - let dict = (data as! [String: Any]) - let metadata = dict["metadata"] as! [String: String] - schemaIds[metadata["collection"]!] = dict["_id"]! as? String } + try group.throwingWait(timeout: .now() + 5.0) - var schemaUpdates = [Result]() - for objectSchema in syncTypes { + let schemaIds = lockedSchemaIds.value + for objectSchema in schema { let schemaId = schemaIds[objectSchema.className]! - schemaUpdates.append(app.schemas[schemaId].put(objectSchema.stitchRule(partitionKeyType, id: schemaId))) + app.schemas[schemaId].put(on: group, data: objectSchema.stitchRule(partitionKeyType, id: schemaId, appId: clientAppId), failOnError) } + try group.throwingWait(timeout: .now() + 5.0) - for result in schemaUpdates { - if case .failure(let error) = result { - fatalError("Failed to create relationships for schema: \(error)") - } + let asymmetricTables = schema.compactMap { + $0.isAsymmetric ? $0.className : nil } - let serviceConfig: [String: Json] switch syncMode { case .pbs(let bsonType): @@ -903,7 +903,7 @@ public class RealmServer: NSObject { "asymmetric_tables": asymmetricTables as [Json] ] ] - app.services[serviceId].default_rule.post(on: group, [ + _ = try app.services[serviceId].default_rule.post([ "roles": [[ "name": "all", "apply_when": [String: Json](), @@ -916,7 +916,7 @@ public class RealmServer: NSObject { "insert": true, "delete": true ]] - ], failOnError) + ]).get() } _ = try app.services[serviceId].config.patch(serviceConfig).get() @@ -993,39 +993,38 @@ public class RealmServer: NSObject { "sync": ["disable_client_error_backoff": true] ], failOnError) - guard case .success = group.wait(timeout: .now() + 15.0) else { - throw URLError(.timedOut) - } + try group.throwingWait(timeout: .now() + 5.0) - return clientAppId - } + // Wait for initial sync to complete as connecting before that has a lot of problems + try waitForSync(appServerId: appId, expectedCount: schema.count - asymmetricTables.count) - @objc public func createAppWithQueryableFields(_ fields: [String]) throws -> AppId { - let schema = ObjectiveCSupport.convert(object: RLMSchema.shared()) - return try createAppForSyncMode(.flx(fields), schema.objectSchema) - } + if !persistent { + appIds.append(appId) + } - @objc public func createAppForAsymmetricSchema(_ schema: [RLMObjectSchema]) throws -> AppId { - try createAppForSyncMode(.flx([]), schema.map(ObjectiveCSupport.convert(object:))) + return clientAppId } - public func createAppForAsymmetricSchema(_ schema: [ObjectSchema]) throws -> AppId { - try createAppForSyncMode(.flx([]), schema) + @objc public func createApp(fields: [String], types: [ObjectBase.Type], persistent: Bool = false) throws -> AppId { + return try createApp(syncMode: .flx(fields), types: types, persistent: persistent) } - @objc public func createAppForBSONType(_ bsonType: String) throws -> AppId { - let schema = ObjectiveCSupport.convert(object: RLMSchema.shared()) - return try createAppForSyncMode(.pbs(bsonType), schema.objectSchema) + @objc public func createApp(partitionKeyType: String = "string", types: [ObjectBase.Type], persistent: Bool = false) throws -> AppId { + return try createApp(syncMode: .pbs(partitionKeyType), types: types, persistent: persistent) } - @objc public func createApp() throws -> AppId { - let schema = ObjectiveCSupport.convert(object: RLMSchema.shared()) - return try createAppForSyncMode(.pbs("string"), schema.objectSchema) + /// Delete all Apps created without `persistent: true` + @objc func deleteApps() throws { + for appId in appIds { + let app = try XCTUnwrap(session).apps[appId] + _ = try app.delete().get() + } + appIds = [] } - @objc public func deleteApp(_ appId: AppId) throws { - let appServerId = try RealmServer.shared.retrieveAppServerId(appId) - let app = try XCTUnwrap(session).apps[appServerId] + @objc func deleteApp(_ appId: String) throws { + let serverAppId = try retrieveAppServerId(appId) + let app = try XCTUnwrap(session).apps[serverAppId] _ = try app.delete().get() } @@ -1214,8 +1213,37 @@ public class RealmServer: NSObject { let session = try XCTUnwrap(session) let appServerId = try retrieveAppServerId(appId) let ident = RLMGetClientFileIdent(ObjectiveCSupport.convert(object: realm)) + XCTAssertNotEqual(ident, 0) _ = try session.privateApps[appServerId].sync.forceReset.put(["file_ident": ident]).get() } + + public func waitForSync(appId: String) throws { + try waitForSync(appServerId: retrieveAppServerId(appId), expectedCount: 1) + } + + public func waitForSync(appServerId: String, expectedCount: Int) throws { + let session = try XCTUnwrap(session) + let start = Date() + while true { + let complete = try session.apps[appServerId].sync.progress.get() + .map { resp in + guard let resp = resp as? Dictionary else { return false } + guard let progress = resp["progress"] else { return false } + guard let progress = progress as? Dictionary else { return false } + let values = progress.compactMapValues { $0 as? Dictionary } + let complete = values.allSatisfy { $0.value["complete"] as? Bool ?? false } + return complete && progress.count >= expectedCount + } + .get() + if complete { + break + } + if -start.timeIntervalSinceNow > 60.0 { + throw "Waiting for sync to complete timed out" + } + Thread.sleep(forTimeInterval: 0.1) + } + } } @Sendable private func failOnError(_ result: Result) { @@ -1224,4 +1252,7 @@ public class RealmServer: NSObject { } } +extension String: Error { +} + #endif diff --git a/Realm/ObjectServerTests/SwiftAsymmetricSyncServerTests.swift b/Realm/ObjectServerTests/SwiftAsymmetricSyncServerTests.swift index 87bb5415c6..2a9fe4ac59 100644 --- a/Realm/ObjectServerTests/SwiftAsymmetricSyncServerTests.swift +++ b/Realm/ObjectServerTests/SwiftAsymmetricSyncServerTests.swift @@ -123,6 +123,7 @@ class SwiftCustomColumnAsymmetricObject: AsymmetricObject { } } +@available(macOS 13, *) class SwiftAsymmetricSyncTests: SwiftSyncTestCase { override class var defaultTestSuite: XCTestSuite { // async/await is currently incompatible with thread sanitizer and will @@ -134,28 +135,26 @@ class SwiftAsymmetricSyncTests: SwiftSyncTestCase { return super.defaultTestSuite } - var asymmetricApp: App { - var appId = SwiftAsymmetricSyncTests.asymmetricAppId - if appId == nil { - do { - let objectSchemas = [SwiftObjectAsymmetric.self, - HugeObjectAsymmetric.self, - SwiftCustomColumnAsymmetricObject.self].map { RLMObjectSchema(forObjectClass: $0) } - appId = try RealmServer.shared.createAppForAsymmetricSchema(objectSchemas) - SwiftAsymmetricSyncTests.asymmetricAppId = appId - } catch { - XCTFail("Failed to create Asymmetric app: \(error)") - } - } + static let objectTypes = [ + HugeObjectAsymmetric.self, + SwiftCustomColumnAsymmetricObject.self, + SwiftObjectAsymmetric.self, + ] + + override func createApp() throws -> String { + try RealmServer.shared.createApp(fields: [], types: SwiftAsymmetricSyncTests.objectTypes, persistent: true) + } - return App(id: appId!, configuration: AppConfiguration(baseURL: "http://localhost:9090")) + override var objectTypes: [ObjectBase.Type] { + SwiftAsymmetricSyncTests.objectTypes + } + + override func configuration(user: User) -> Realm.Configuration { + user.flexibleSyncConfiguration() } - static var asymmetricAppId: String? func testAsymmetricObjectSchema() throws { - var configuration = (try logInUser(for: basicCredentials(app: asymmetricApp), app: asymmetricApp)).flexibleSyncConfiguration() - configuration.objectTypes = [SwiftObjectAsymmetric.self] - let realm = try Realm(configuration: configuration) + let realm = try openRealm() XCTAssertTrue(realm.schema.objectSchema[0].isAsymmetric) } @@ -167,7 +166,7 @@ class SwiftAsymmetricSyncTests: SwiftSyncTestCase { } func testOpenPBSConfigurationRealmWithAsymmetricObjectError() throws { - let user = try logInUser(for: basicCredentials(app: self.flexibleSyncApp), app: self.flexibleSyncApp) + let user = createUser() var configuration = user.configuration(partitionValue: #function) configuration.objectTypes = [SwiftObjectAsymmetric.self] @@ -184,26 +183,13 @@ class SwiftAsymmetricSyncTests: SwiftSyncTestCase { } } -#if canImport(_Concurrency) -@available(macOS 12.0, *) +#if swift(>=5.8) +@available(macOS 13.0, *) extension SwiftAsymmetricSyncTests { - func config() async throws -> Realm.Configuration { - var config = (try await asymmetricApp.login(credentials: basicCredentials(app: asymmetricApp))).flexibleSyncConfiguration() - config.objectTypes = [SwiftObjectAsymmetric.self, HugeObjectAsymmetric.self, SwiftCustomColumnAsymmetricObject.self] - return config - } - - func realm() async throws -> Realm { - let realm = try await Realm(configuration: config()) - return realm - } - @MainActor - func setupCollection(_ collection: String) async throws -> MongoCollection { - let user = try await asymmetricApp.login(credentials: .anonymous) - let mongoClient = user.mongoClient("mongodb1") - let database = mongoClient.database(named: "test_data") - let collection = database.collection(withName: collection) + func setupCollection(_ type: ObjectBase.Type) async throws -> MongoCollection { + let user = try await app.login(credentials: .anonymous) + let collection = user.collection(for: type, app: app) if try await collection.count(filter: [:]) > 0 { removeAllFromCollection(collection) } @@ -211,14 +197,12 @@ extension SwiftAsymmetricSyncTests { } @MainActor - func checkCountInMongo(_ expectedCount: Int, forCollection collection: String) async throws { + func checkCountInMongo(_ expectedCount: Int, type: ObjectBase.Type) async throws { let waitStart = Date() - let user = try await asymmetricApp.login(credentials: .anonymous) - let mongoClient = user.mongoClient("mongodb1") - let database = mongoClient.database(named: "test_data") - let collection = database.collection(withName: collection) - while collection.count(filter: [:]).await(self) != expectedCount && waitStart.timeIntervalSinceNow > -600.0 { - sleep(5) + let user = try await app.login(credentials: .anonymous) + let collection = user.collection(for: type, app: app) + while try await collection.count(filter: [:]) < expectedCount && waitStart.timeIntervalSinceNow > -600.0 { + try await Task.sleep(for: .seconds(5)) } XCTAssertEqual(collection.count(filter: [:]).await(self), expectedCount) @@ -226,9 +210,9 @@ extension SwiftAsymmetricSyncTests { @MainActor func testCreateAsymmetricObject() async throws { - let realm = try await realm() + _ = try await setupCollection(SwiftObjectAsymmetric.self) + let realm = try await openRealm() - // Create Asymmetric Objects try realm.write { for i in 1...15 { realm.create(SwiftObjectAsymmetric.self, @@ -238,17 +222,14 @@ extension SwiftAsymmetricSyncTests { } waitForUploads(for: realm) - // We use the Mongo client API to check if the documents were create, - // because we cannot query `AsymmetricObject`s directly. - try await checkCountInMongo(15, forCollection: "SwiftObjectAsymmetric") + try await checkCountInMongo(15, type: SwiftObjectAsymmetric.self) } @MainActor func testPropertyTypesAsymmetricObject() async throws { - let collection = try await setupCollection("SwiftObjectAsymmetric") - let realm = try await realm() + let collection = try await setupCollection(SwiftObjectAsymmetric.self) + let realm = try await openRealm() - // Create Asymmetric Objects try realm.write { realm.create(SwiftObjectAsymmetric.self, value: SwiftObjectAsymmetric(string: "name_\(#function)", @@ -256,9 +237,7 @@ extension SwiftAsymmetricSyncTests { } waitForUploads(for: realm) - // We use the Mongo client API to check if the documents were create, - // because we cannot query AsymmetricObjects directly. - try await checkCountInMongo(1, forCollection: "SwiftObjectAsymmetric") + try await checkCountInMongo(1, type: SwiftObjectAsymmetric.self) let document = try await collection.find(filter: [:])[0] XCTAssertEqual(document["string"]??.stringValue, "name_\(#function)") @@ -273,7 +252,8 @@ extension SwiftAsymmetricSyncTests { @MainActor func testCreateHugeAsymmetricObject() async throws { - let realm = try await realm() + _ = try await setupCollection(HugeObjectAsymmetric.self) + let realm = try await openRealm() // Create Asymmetric Objects try realm.write { @@ -283,13 +263,13 @@ extension SwiftAsymmetricSyncTests { } waitForUploads(for: realm) - try await checkCountInMongo(2, forCollection: "HugeObjectAsymmetric") + try await checkCountInMongo(2, type: HugeObjectAsymmetric.self) } @MainActor func testCreateCustomAsymmetricObject() async throws { - let collection = try await setupCollection("SwiftCustomColumnAsymmetricObject") - let realm = try await realm() + let collection = try await setupCollection(SwiftCustomColumnAsymmetricObject.self) + let realm = try await openRealm() let objectId = ObjectId.generate() let valuesDictionary: [String: Any] = ["id": objectId, @@ -304,7 +284,7 @@ extension SwiftAsymmetricSyncTests { } waitForUploads(for: realm) - try await checkCountInMongo(1, forCollection: "SwiftCustomColumnAsymmetricObject") + try await checkCountInMongo(1, type: SwiftCustomColumnAsymmetricObject.self) let filter: Document = ["_id": .objectId(objectId)] let document = try await collection.findOneDocument(filter: filter) diff --git a/Realm/ObjectServerTests/SwiftCollectionSyncTests.swift b/Realm/ObjectServerTests/SwiftCollectionSyncTests.swift index a73d7ced7d..81a58fa484 100644 --- a/Realm/ObjectServerTests/SwiftCollectionSyncTests.swift +++ b/Realm/ObjectServerTests/SwiftCollectionSyncTests.swift @@ -26,34 +26,30 @@ import RealmSyncTestSupport import RealmTestSupport #endif +@available(macOS 13, *) class CollectionSyncTestCase: SwiftSyncTestCase { var readRealm: Realm! - var writeRealm: Realm! override func setUp() { super.setUp() - - let readUser = logInUser(for: basicCredentials(name: self.name + " read user", register: true)) - let writeUser = logInUser(for: basicCredentials(name: self.name + " write user", register: true)) // This autoreleasepool is needed to ensure that the Realms are closed // immediately in tearDown() rather than escaping to an outer pool. autoreleasepool { - readRealm = try! openRealm(partitionValue: self.name, user: readUser) - writeRealm = try! openRealm(partitionValue: self.name, user: writeUser) + readRealm = try! openRealm() } } override func tearDown() { readRealm = nil - writeRealm = nil super.tearDown() } + override var objectTypes: [ObjectBase.Type] { + [SwiftCollectionSyncObject.self, SwiftPerson.self] + } + func write(_ fn: (Realm) -> Void) throws { - try writeRealm.write { - fn(writeRealm) - } - waitForUploads(for: writeRealm) + try super.write(fn) waitForDownloads(for: readRealm) } @@ -175,7 +171,7 @@ class CollectionSyncTestCase: SwiftSyncTestCase { if T.self is SwiftPerson.Type { // formIntersection won't work with unique Objects collection.removeAll() - collection.insert(set.values[0]) + collection.insert(realm.create(SwiftPerson.self, value: set.values[0], update: .all) as! T) } else { collection.formIntersection(otherCollection) } diff --git a/Realm/ObjectServerTests/SwiftFlexibleSyncServerTests.swift b/Realm/ObjectServerTests/SwiftFlexibleSyncServerTests.swift index 5a9d086702..cfbbc3ca2d 100644 --- a/Realm/ObjectServerTests/SwiftFlexibleSyncServerTests.swift +++ b/Realm/ObjectServerTests/SwiftFlexibleSyncServerTests.swift @@ -29,17 +29,24 @@ import SwiftUI import RealmSwiftTestSupport #endif +@available(macOS 13.0, *) class SwiftFlexibleSyncTests: SwiftSyncTestCase { - func testCreateFlexibleSyncApp() throws { - let appId = try RealmServer.shared.createAppWithQueryableFields(["age"]) - let flexibleApp = app(withId: appId) - let user = try logInUser(for: basicCredentials(app: flexibleApp), app: flexibleApp) - XCTAssertNotNil(user) - try RealmServer.shared.deleteApp(appId) + override func configuration(user: User) -> Realm.Configuration { + user.flexibleSyncConfiguration() + } + + override var objectTypes: [ObjectBase.Type] { + [SwiftPerson.self, SwiftTypesSyncObject.self] } - func testFlexibleSyncOpenRealm() throws { - _ = try openFlexibleSyncRealm() + override func createApp() throws -> String { + try createFlexibleSyncApp() + } + + func testCreateFlexibleSyncApp() throws { + let appId = try RealmServer.shared.createApp(fields: ["age"], types: [SwiftPerson.self]) + let flexibleApp = app(id: appId) + _ = try logInUser(for: basicCredentials(app: flexibleApp), app: flexibleApp) } func testGetSubscriptionsWhenLocalRealm() throws { @@ -51,25 +58,24 @@ class SwiftFlexibleSyncTests: SwiftSyncTestCase { // FIXME: Using `assertThrows` within a Server test will crash on tear down func skip_testGetSubscriptionsWhenPbsRealm() throws { - let user = try logInUser(for: basicCredentials()) - let realm = try openRealm(partitionValue: #function, user: user) + let realm = try Realm(configuration: createUser().configuration(partitionValue: name)) assertThrows(realm.subscriptions) } func testFlexibleSyncPath() throws { - let user = try logInUser(for: basicCredentials(app: flexibleSyncApp), app: flexibleSyncApp) - let config = user.flexibleSyncConfiguration() - XCTAssertTrue(config.fileURL!.path.hasSuffix("mongodb-realm/\(flexibleSyncAppId)/\(user.id)/flx_sync_default.realm")) + let config = try configuration() + let user = config.syncConfiguration!.user + XCTAssertTrue(config.fileURL!.path.hasSuffix("mongodb-realm/\(appId)/\(user.id)/flx_sync_default.realm")) } func testGetSubscriptions() throws { - let realm = try openFlexibleSyncRealm() + let realm = try openRealm() let subscriptions = realm.subscriptions XCTAssertEqual(subscriptions.count, 0) } func testWriteEmptyBlock() throws { - let realm = try openFlexibleSyncRealm() + let realm = try openRealm() let subscriptions = realm.subscriptions subscriptions.update {} @@ -77,7 +83,7 @@ class SwiftFlexibleSyncTests: SwiftSyncTestCase { } func testAddOneSubscriptionWithoutName() throws { - let realm = try openFlexibleSyncRealm() + let realm = try openRealm() let subscriptions = realm.subscriptions subscriptions.update { subscriptions.append(QuerySubscription { @@ -89,7 +95,7 @@ class SwiftFlexibleSyncTests: SwiftSyncTestCase { } func testAddOneSubscriptionWithName() throws { - let realm = try openFlexibleSyncRealm() + let realm = try openRealm() let subscriptions = realm.subscriptions subscriptions.update { subscriptions.append(QuerySubscription(name: "person_age") { @@ -101,7 +107,7 @@ class SwiftFlexibleSyncTests: SwiftSyncTestCase { } func testAddSubscriptionsInDifferentBlocks() throws { - let realm = try openFlexibleSyncRealm() + let realm = try openRealm() let subscriptions = realm.subscriptions subscriptions.update { subscriptions.append(QuerySubscription(name: "person_age") { @@ -118,7 +124,7 @@ class SwiftFlexibleSyncTests: SwiftSyncTestCase { } func testAddSeveralSubscriptionsWithoutName() throws { - let realm = try openFlexibleSyncRealm() + let realm = try openRealm() let subscriptions = realm.subscriptions subscriptions.update { subscriptions.append( @@ -137,7 +143,7 @@ class SwiftFlexibleSyncTests: SwiftSyncTestCase { } func testAddSeveralSubscriptionsWithName() throws { - let realm = try openFlexibleSyncRealm() + let realm = try openRealm() let subscriptions = realm.subscriptions subscriptions.update { subscriptions.append( @@ -155,7 +161,7 @@ class SwiftFlexibleSyncTests: SwiftSyncTestCase { } func testAddMixedSubscriptions() throws { - let realm = try openFlexibleSyncRealm() + let realm = try openRealm() let subscriptions = realm.subscriptions subscriptions.update { subscriptions.append(QuerySubscription(name: "person_age_15") { @@ -173,7 +179,7 @@ class SwiftFlexibleSyncTests: SwiftSyncTestCase { } func testAddDuplicateSubscriptions() throws { - let realm = try openFlexibleSyncRealm() + let realm = try openRealm() let subscriptions = realm.subscriptions subscriptions.update { subscriptions.append( @@ -188,7 +194,7 @@ class SwiftFlexibleSyncTests: SwiftSyncTestCase { } func testAddDuplicateSubscriptionWithDifferentName() throws { - let realm = try openFlexibleSyncRealm() + let realm = try openRealm() let subscriptions = realm.subscriptions subscriptions.update { subscriptions.append( @@ -204,7 +210,7 @@ class SwiftFlexibleSyncTests: SwiftSyncTestCase { // FIXME: Using `assertThrows` within a Server test will crash on tear down func skip_testSameNamedSubscriptionThrows() throws { - let realm = try openFlexibleSyncRealm() + let realm = try openRealm() let subscriptions = realm.subscriptions subscriptions.update { subscriptions.append(QuerySubscription(name: "person_age_1") { @@ -219,7 +225,7 @@ class SwiftFlexibleSyncTests: SwiftSyncTestCase { // FIXME: Using `assertThrows` within a Server test will crash on tear down func skip_testAddSubscriptionOutsideWriteThrows() throws { - let realm = try openFlexibleSyncRealm() + let realm = try openRealm() let subscriptions = realm.subscriptions assertThrows(subscriptions.append(QuerySubscription(name: "person_age_1") { $0.age > 15 @@ -227,7 +233,7 @@ class SwiftFlexibleSyncTests: SwiftSyncTestCase { } func testFindSubscriptionByName() throws { - let realm = try openFlexibleSyncRealm() + let realm = try openRealm() let subscriptions = realm.subscriptions subscriptions.update { subscriptions.append( @@ -250,7 +256,7 @@ class SwiftFlexibleSyncTests: SwiftSyncTestCase { } func testFindSubscriptionByQuery() throws { - let realm = try openFlexibleSyncRealm() + let realm = try openRealm() let subscriptions = realm.subscriptions subscriptions.update { subscriptions.append(QuerySubscription(name: "person_firstname_james") { @@ -276,7 +282,7 @@ class SwiftFlexibleSyncTests: SwiftSyncTestCase { } func testRemoveSubscriptionByName() throws { - let realm = try openFlexibleSyncRealm() + let realm = try openRealm() let subscriptions = realm.subscriptions subscriptions.update { subscriptions.append(QuerySubscription(name: "person_firstname_james") { @@ -299,7 +305,7 @@ class SwiftFlexibleSyncTests: SwiftSyncTestCase { } func testRemoveSubscriptionByQuery() throws { - let realm = try openFlexibleSyncRealm() + let realm = try openRealm() let subscriptions = realm.subscriptions subscriptions.update { subscriptions.append( @@ -333,7 +339,7 @@ class SwiftFlexibleSyncTests: SwiftSyncTestCase { } func testRemoveSubscription() throws { - let realm = try openFlexibleSyncRealm() + let realm = try openRealm() let subscriptions = realm.subscriptions subscriptions.update { subscriptions.append(QuerySubscription(name: "person_names") { @@ -365,7 +371,7 @@ class SwiftFlexibleSyncTests: SwiftSyncTestCase { } func testRemoveSubscriptionsByType() throws { - let realm = try openFlexibleSyncRealm() + let realm = try openRealm() let subscriptions = realm.subscriptions subscriptions.update { subscriptions.append( @@ -396,7 +402,7 @@ class SwiftFlexibleSyncTests: SwiftSyncTestCase { } func testRemoveAllSubscriptions() throws { - let realm = try openFlexibleSyncRealm() + let realm = try openRealm() let subscriptions = realm.subscriptions subscriptions.update { subscriptions.append( @@ -423,7 +429,7 @@ class SwiftFlexibleSyncTests: SwiftSyncTestCase { } func testRemoveAllUnnamedSubscriptions() throws { - let realm = try openFlexibleSyncRealm() + let realm = try openRealm() let subscriptions = realm.subscriptions subscriptions.update { subscriptions.append( @@ -450,7 +456,7 @@ class SwiftFlexibleSyncTests: SwiftSyncTestCase { } func testSubscriptionSetIterate() throws { - let realm = try openFlexibleSyncRealm() + let realm = try openRealm() let subscriptions = realm.subscriptions let numberOfSubs = 50 @@ -474,7 +480,7 @@ class SwiftFlexibleSyncTests: SwiftSyncTestCase { } func testSubscriptionSetFirstAndLast() throws { - let realm = try openFlexibleSyncRealm() + let realm = try openRealm() let subscriptions = realm.subscriptions let numberOfSubs = 20 @@ -498,7 +504,7 @@ class SwiftFlexibleSyncTests: SwiftSyncTestCase { } func testSubscriptionSetSubscript() throws { - let realm = try openFlexibleSyncRealm() + let realm = try openRealm() let subscriptions = realm.subscriptions let numberOfSubs = 20 @@ -522,7 +528,7 @@ class SwiftFlexibleSyncTests: SwiftSyncTestCase { } func testUpdateQueries() throws { - let realm = try openFlexibleSyncRealm() + let realm = try openRealm() let subscriptions = realm.subscriptions subscriptions.update { subscriptions.append( @@ -547,16 +553,12 @@ class SwiftFlexibleSyncTests: SwiftSyncTestCase { } func testUpdateQueriesWithoutName() throws { - let realm = try openFlexibleSyncRealm() + let realm = try openRealm() let subscriptions = realm.subscriptions subscriptions.update { subscriptions.append( - QuerySubscription { - $0.age > 15 - }, - QuerySubscription { - $0.age > 20 - }) + QuerySubscription { $0.age > 15 }, + QuerySubscription { $0.age > 20 }) } XCTAssertEqual(subscriptions.count, 2) @@ -577,7 +579,7 @@ class SwiftFlexibleSyncTests: SwiftSyncTestCase { // FIXME: Using `assertThrows` within a Server test will crash on tear down func skip_testFlexibleSyncAppUpdateQueryWithDifferentObjectTypeWillThrow() throws { - let realm = try openFlexibleSyncRealm() + let realm = try openRealm() let subscriptions = realm.subscriptions subscriptions.update { subscriptions.append( @@ -595,7 +597,7 @@ class SwiftFlexibleSyncTests: SwiftSyncTestCase { } func testFlexibleSyncTransactionsWithPredicateFormatAndNSPredicate() throws { - let realm = try openFlexibleSyncRealm() + let realm = try openRealm() let subscriptions = realm.subscriptions subscriptions.update { subscriptions.append( @@ -621,37 +623,29 @@ class SwiftFlexibleSyncTests: SwiftSyncTestCase { XCTAssertEqual(subscriptions.count, 2) } -} -// MARK: - Completion Block -class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { - private var cancellables: Set = [] - - override class var defaultTestSuite: XCTestSuite { - if hasCombine() { - return super.defaultTestSuite + func populateSwiftPerson(count: Int = 10) throws { + try write { realm in + for i in 1...count { + realm.add(SwiftPerson(firstName: self.name, lastName: "lastname_\(i)", age: i)) + } } - return XCTestSuite(name: "\(type(of: self))") } - override func tearDown() { - cancellables.forEach { $0.cancel() } - cancellables = [] - super.tearDown() + func populateSwiftTypesObject(count: Int = 1) throws { + try write { realm in + for _ in 1...count { + let swiftTypes = SwiftTypesSyncObject() + swiftTypes.stringCol = self.name + realm.add(swiftTypes) + } + } } func testFlexibleSyncAppWithoutQuery() throws { - try populateFlexibleSyncData { realm in - for i in 1...10 { - // Using firstname to query only objects from this test - let person = SwiftPerson(firstName: "\(#function)", - lastName: "lastname_\(i)", - age: i) - realm.add(person) - } - } + try populateSwiftPerson() - let realm = try flexibleSyncRealm() + let realm = try openRealm() checkCount(expected: 0, realm, SwiftPerson.self) let subscriptions = realm.subscriptions @@ -662,16 +656,9 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { } func testFlexibleSyncAppAddQuery() throws { - try populateFlexibleSyncData { realm in - for i in 1...25 { - let person = SwiftPerson(firstName: "\(#function)", - lastName: "lastname_\(i)", - age: i) - realm.add(person) - } - } + try populateSwiftPerson(count: 25) - let realm = try flexibleSyncRealm() + let realm = try openRealm() checkCount(expected: 0, realm, SwiftPerson.self) let subscriptions = realm.subscriptions @@ -680,7 +667,7 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { let ex = expectation(description: "state change complete") subscriptions.update({ subscriptions.append(QuerySubscription(name: "person_age_15") { - $0.age > 15 && $0.firstName == "\(#function)" + $0.age > 15 && $0.firstName == name }) }, onComplete: { error in XCTAssertNil(error) @@ -694,19 +681,10 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { } func testFlexibleSyncAppMultipleQuery() throws { - try populateFlexibleSyncData { realm in - for i in 1...20 { - let person = SwiftPerson(firstName: "\(#function)", - lastName: "lastname_\(i)", - age: i) - realm.add(person) - } - let swiftTypes = SwiftTypesSyncObject() - swiftTypes.stringCol = "\(#function)" - realm.add(swiftTypes) - } + try populateSwiftPerson(count: 20) + try populateSwiftTypesObject() - let realm = try flexibleSyncRealm() + let realm = try openRealm() checkCount(expected: 0, realm, SwiftPerson.self) let subscriptions = realm.subscriptions @@ -715,10 +693,10 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { let ex = expectation(description: "state change complete") subscriptions.update({ subscriptions.append(QuerySubscription(name: "person_age_10") { - $0.age > 10 && $0.firstName == "\(#function)" + $0.age > 10 && $0.firstName == name }) subscriptions.append(QuerySubscription(name: "swift_object_equal_1") { - $0.intCol == 1 && $0.stringCol == "\(#function)" + $0.intCol == 1 && $0.stringCol == name }) }, onComplete: { error in XCTAssertNil(error) @@ -732,19 +710,10 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { } func testFlexibleSyncAppRemoveQuery() throws { - try populateFlexibleSyncData { realm in - for i in 1...30 { - let person = SwiftPerson(firstName: "\(#function)", - lastName: "lastname_\(i)", - age: i) - realm.add(person) - } - let swiftTypes = SwiftTypesSyncObject() - swiftTypes.stringCol = "\(#function)" - realm.add(swiftTypes) - } + try populateSwiftPerson(count: 30) + try populateSwiftTypesObject() - let realm = try flexibleSyncRealm() + let realm = try openRealm() checkCount(expected: 0, realm, SwiftPerson.self) let subscriptions = realm.subscriptions @@ -753,10 +722,10 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { let ex = expectation(description: "state change complete") subscriptions.update({ subscriptions.append(QuerySubscription(name: "person_age_5") { - $0.age > 5 && $0.firstName == "\(#function)" + $0.age > 5 && $0.firstName == name }) subscriptions.append(QuerySubscription(name: "swift_object_equal_1") { - $0.intCol == 1 && $0.stringCol == "\(#function)" + $0.intCol == 1 && $0.stringCol == name }) }, onComplete: { error in XCTAssertNil(error) @@ -783,19 +752,10 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { } func testFlexibleSyncAppRemoveAllQueries() throws { - try populateFlexibleSyncData { realm in - for i in 1...25 { - let person = SwiftPerson(firstName: "\(#function)", - lastName: "lastname_\(i)", - age: i) - realm.add(person) - } - let swiftTypes = SwiftTypesSyncObject() - swiftTypes.stringCol = "\(#function)" - realm.add(swiftTypes) - } + try populateSwiftPerson(count: 25) + try populateSwiftTypesObject() - let realm = try flexibleSyncRealm() + let realm = try openRealm() checkCount(expected: 0, realm, SwiftPerson.self) let subscriptions = realm.subscriptions @@ -804,10 +764,10 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { let ex = expectation(description: "state change complete") subscriptions.update({ subscriptions.append(QuerySubscription(name: "person_age_5") { - $0.age > 5 && $0.firstName == "\(#function)" + $0.age > 5 && $0.firstName == name }) subscriptions.append(QuerySubscription(name: "swift_object_equal_1") { - $0.intCol == 1 && $0.stringCol == "\(#function)" + $0.intCol == 1 && $0.stringCol == name }) }, onComplete: { error in XCTAssertNil(error) @@ -824,7 +784,7 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { subscriptions.update({ subscriptions.removeAll() subscriptions.append(QuerySubscription(name: "person_age_20") { - $0.age > 20 && $0.firstName == "\(#function)" + $0.age > 20 && $0.firstName == name }) }, onComplete: { error in XCTAssertNil(error) @@ -838,19 +798,10 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { } func testFlexibleSyncAppRemoveQueriesByType() throws { - try populateFlexibleSyncData { realm in - for i in 1...21 { - let person = SwiftPerson(firstName: "\(#function)", - lastName: "lastname_\(i)", - age: i) - realm.add(person) - } - let swiftTypes = SwiftTypesSyncObject() - swiftTypes.stringCol = "\(#function)" - realm.add(swiftTypes) - } + try populateSwiftPerson(count: 21) + try populateSwiftTypesObject() - let realm = try flexibleSyncRealm() + let realm = try openRealm() checkCount(expected: 0, realm, SwiftPerson.self) let subscriptions = realm.subscriptions @@ -860,13 +811,13 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { subscriptions.update({ subscriptions.append( QuerySubscription(name: "person_age_5") { - $0.age > 20 && $0.firstName == "\(#function)" + $0.age > 20 && $0.firstName == name }, QuerySubscription(name: "person_age_10") { - $0.lastName == "lastname_1" && $0.firstName == "\(#function)" + $0.lastName == "lastname_1" && $0.firstName == name }) subscriptions.append(QuerySubscription(name: "swift_object_equal_1") { - $0.intCol == 1 && $0.stringCol == "\(#function)" + $0.intCol == 1 && $0.stringCol == name }) }, onComplete: { error in XCTAssertNil(error) @@ -893,16 +844,9 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { } func testFlexibleSyncAppUpdateQuery() throws { - try populateFlexibleSyncData { realm in - for i in 1...25 { - let person = SwiftPerson(firstName: "\(#function)", - lastName: "lastname_\(i)", - age: i) - realm.add(person) - } - } + try populateSwiftPerson(count: 25) - let realm = try flexibleSyncRealm() + let realm = try openRealm() checkCount(expected: 0, realm, SwiftPerson.self) let subscriptions = realm.subscriptions @@ -911,7 +855,7 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { let ex = expectation(description: "state change complete") subscriptions.update({ subscriptions.append(QuerySubscription(name: "person_age") { - $0.age > 20 && $0.firstName == "\(#function)" + $0.age > 20 && $0.firstName == name }) }, onComplete: { error in XCTAssertNil(error) @@ -928,7 +872,7 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { let ex2 = expectation(description: "state change complete") subscriptions.update({ foundSubscription?.updateQuery(toType: SwiftPerson.self, where: { - $0.age > 5 && $0.firstName == "\(#function)" + $0.age > 5 && $0.firstName == name }) }, onComplete: { error in XCTAssertNil(error) @@ -941,25 +885,15 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { } func testFlexibleSyncInitialSubscriptions() throws { - try populateFlexibleSyncData { realm in - for i in 1...20 { - let person = SwiftPerson(firstName: "\(#function)", - lastName: "lastname_\(i)", - age: i) - realm.add(person) - } - } + try populateSwiftPerson(count: 20) - let user = try logInUser(for: basicCredentials(app: self.flexibleSyncApp), app: self.flexibleSyncApp) + let user = createUser() var config = user.flexibleSyncConfiguration(initialSubscriptions: { subscriptions in subscriptions.append(QuerySubscription(name: "person_age_10") { - $0.age > 10 && $0.firstName == "\(#function)" + $0.age > 10 && $0.firstName == self.name }) }) - if config.objectTypes == nil { - config.objectTypes = [SwiftPerson.self, - SwiftTypesSyncObject.self] - } + config.objectTypes = [SwiftPerson.self, SwiftTypesSyncObject.self] let realm = try Realm(configuration: config) let subscriptions = realm.subscriptions XCTAssertEqual(subscriptions.count, 1) @@ -983,7 +917,7 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { let appConfig = AppConfiguration(baseURL: "http://localhost:5678", transport: AsyncOpenConnectionTimeoutTransport(), syncTimeouts: SyncTimeoutOptions(connectTimeout: 2000)) - let app = App(id: flexibleSyncAppId, configuration: appConfig) + let app = App(id: appId, configuration: appConfig) let user = try logInUser(for: basicCredentials(app: app), app: app) let config = user.flexibleSyncConfiguration(cancelAsyncOpenOnNonFatalErrors: true) @@ -1009,894 +943,4 @@ class SwiftFlexibleSyncServerTests: SwiftSyncTestCase { } } -// MARK: - Async Await -#if canImport(_Concurrency) -@available(macOS 12.0, *) -extension SwiftFlexibleSyncServerTests { - @MainActor - private func populateFlexibleSyncData(_ block: @escaping (Realm) -> Void) async throws { - let realm = try await flexibleSyncRealm() - let subscriptions = realm.subscriptions - try await subscriptions.update { - subscriptions.append(QuerySubscription()) - subscriptions.append(QuerySubscription()) - subscriptions.append(QuerySubscription()) - } - - try realm.write { - block(realm) - } - waitForUploads(for: realm) - } - - @MainActor - func populateSwiftPerson() async throws { - try await populateFlexibleSyncData { realm in - realm.deleteAll() // Remove all objects for a clean state - for i in 1...10 { - let person = SwiftPerson(firstName: "\(#function)", - lastName: "lastname_\(i)", - age: i) - realm.add(person) - } - } - } - - @MainActor - func setupCollection(_ collection: String) async throws -> MongoCollection { - let user = try await flexibleSyncApp.login(credentials: .anonymous) - let mongoClient = user.mongoClient("mongodb1") - let database = mongoClient.database(named: "test_data") - let collection = database.collection(withName: collection) - removeAllFromCollection(collection) - return collection - } - - @MainActor - func testFlexibleSyncAppAddQueryAsyncAwait() async throws { - try await populateFlexibleSyncData { realm in - for i in 1...25 { - let person = SwiftPerson(firstName: "\(#function)", - lastName: "lastname_\(i)", - age: i) - realm.add(person) - } - } - - let realm = try await flexibleSyncRealm() - checkCount(expected: 0, realm, SwiftPerson.self) - - let subscriptions = realm.subscriptions - XCTAssertEqual(subscriptions.count, 0) - - try await subscriptions.update { - subscriptions.append(QuerySubscription(name: "person_age_15") { - $0.age > 15 && $0.firstName == "\(#function)" - }) - } - - checkCount(expected: 10, realm, SwiftPerson.self) - } - -#if false // FIXME: this is no longer an error and needs to be updated to something which is - @MainActor - func testStates() async throws { - let realm = try await flexibleSyncRealm() - let subscriptions = realm.subscriptions - XCTAssertEqual(subscriptions.count, 0) - - // should complete - try await subscriptions.update { - subscriptions.append(QuerySubscription(name: "person_age_15") { - $0.age > 15 && $0.firstName == "\(#function)" - }) - } - XCTAssertEqual(subscriptions.state, .complete) - // should error - do { - try await subscriptions.update { - subscriptions.append(QuerySubscription(name: "swiftObject_longCol") { - $0.longCol == Int64(1) - }) - } - XCTFail("Invalid query should have failed") - } catch Realm.Error.subscriptionFailed { - guard case .error = subscriptions.state else { - return XCTFail("Adding a query for a not queryable field should change the subscription set state to error") - } - } - } -#endif - - @MainActor - func testFlexibleSyncAllDocumentsForType() async throws { - try await populateFlexibleSyncData { realm in - realm.deleteAll() // Remove all objects for a clean state - for i in 1...28 { - let person = SwiftPerson(firstName: "\(#function)", - lastName: "lastname_\(i)", - age: i) - realm.add(person) - } - } - - let realm = try await flexibleSyncRealm() - checkCount(expected: 0, realm, SwiftPerson.self) - - let subscriptions = realm.subscriptions - XCTAssertEqual(subscriptions.count, 0) - - try await subscriptions.update { - subscriptions.append(QuerySubscription(name: "person_age_all")) - } - XCTAssertEqual(subscriptions.state, .complete) - XCTAssertEqual(subscriptions.count, 1) - checkCount(expected: 28, realm, SwiftPerson.self) - } - - @MainActor - func testFlexibleSyncNotInitialSubscriptions() async throws { - let config = try await flexibleSyncConfig() - let realm = try await Realm(configuration: config, downloadBeforeOpen: .always) - XCTAssertEqual(realm.subscriptions.count, 0) - } - - @MainActor - func testFlexibleSyncInitialSubscriptionsAsync() async throws { - try await populateFlexibleSyncData { realm in - for i in 1...20 { - let person = SwiftPerson(firstName: "\(#function)", - lastName: "lastname_\(i)", - age: i) - realm.add(person) - } - } - - let user = try await logInUser(for: basicCredentials(app: self.flexibleSyncApp), app: self.flexibleSyncApp) - var config = user.flexibleSyncConfiguration(initialSubscriptions: { subscriptions in - subscriptions.append(QuerySubscription(name: "person_age_10") { - $0.age > 10 && $0.firstName == "\(#function)" - }) - }) - - if config.objectTypes == nil { - config.objectTypes = [SwiftPerson.self] - } - let realm = try await Realm(configuration: config, downloadBeforeOpen: .once) - XCTAssertEqual(realm.subscriptions.count, 1) - checkCount(expected: 10, realm, SwiftPerson.self) - } - - @MainActor - func testFlexibleSyncInitialSubscriptionsNotRerunOnOpen() async throws { - let user = try await logInUser(for: basicCredentials(app: self.flexibleSyncApp), app: self.flexibleSyncApp) - var config = user.flexibleSyncConfiguration(initialSubscriptions: { subscriptions in - subscriptions.append(QuerySubscription(name: "person_age_10") { - $0.age > 10 && $0.firstName == "\(#function)" - }) - }) - - if config.objectTypes == nil { - config.objectTypes = [SwiftPerson.self] - } - let realm = try await Realm(configuration: config, downloadBeforeOpen: .once) - XCTAssertEqual(realm.subscriptions.count, 1) - - let realm2 = try await Realm(configuration: config, downloadBeforeOpen: .once) - XCTAssertNotNil(realm2) - XCTAssertEqual(realm.subscriptions.count, 1) - } - - @MainActor - func testFlexibleSyncInitialSubscriptionsRerunOnOpenNamedQuery() async throws { - let user = try await logInUser(for: basicCredentials(app: self.flexibleSyncApp), app: self.flexibleSyncApp) - var config = user.flexibleSyncConfiguration(initialSubscriptions: { subscriptions in - if subscriptions.first(named: "person_age_10") == nil { - subscriptions.append(QuerySubscription(name: "person_age_10") { - $0.age > 20 && $0.firstName == "\(#function)" - }) - } - }, rerunOnOpen: true) - - if config.objectTypes == nil { - config.objectTypes = [SwiftPerson.self] - } - let realm = try await Realm(configuration: config, downloadBeforeOpen: .once) - XCTAssertEqual(realm.subscriptions.count, 1) - - let realm2 = try await Realm(configuration: config, downloadBeforeOpen: .once) - XCTAssertNotNil(realm2) - XCTAssertEqual(realm.subscriptions.count, 1) - } - - @MainActor - func testFlexibleSyncInitialSubscriptionsRerunOnOpenUnnamedQuery() async throws { - try await populateFlexibleSyncData { realm in - for i in 1...30 { - let object = SwiftTypesSyncObject() - object.dateCol = Calendar.current.date( - byAdding: .hour, - value: -i, - to: Date())! - realm.add(object) - } - } - let user = try await logInUser(for: basicCredentials(app: self.flexibleSyncApp), app: self.flexibleSyncApp) - let isFirstOpen = Locked(true) - var config = user.flexibleSyncConfiguration(initialSubscriptions: { subscriptions in - subscriptions.append(QuerySubscription(query: { - let date = isFirstOpen.wrappedValue ? Calendar.current.date( - byAdding: .hour, - value: -10, - to: Date()) : Calendar.current.date( - byAdding: .hour, - value: -20, - to: Date()) - isFirstOpen.wrappedValue = false - return $0.dateCol < Date() && $0.dateCol > date! - })) - }, rerunOnOpen: true) - - if config.objectTypes == nil { - config.objectTypes = [SwiftTypesSyncObject.self, SwiftPerson.self] - } - let c = config - _ = try await Task { @MainActor in - let realm = try await Realm(configuration: c, downloadBeforeOpen: .always) - XCTAssertEqual(realm.subscriptions.count, 1) - checkCount(expected: 9, realm, SwiftTypesSyncObject.self) - }.value - - _ = try await Task { @MainActor in - let realm = try await Realm(configuration: c, downloadBeforeOpen: .always) - XCTAssertEqual(realm.subscriptions.count, 2) - checkCount(expected: 19, realm, SwiftTypesSyncObject.self) - }.value - } - - @MainActor - func testFlexibleSyncInitialSubscriptionsThrows() async throws { - let user = try await logInUser(for: basicCredentials(app: self.flexibleSyncApp), app: self.flexibleSyncApp) - var config = user.flexibleSyncConfiguration(initialSubscriptions: { subscriptions in - if subscriptions.first(named: "query_uuid") == nil { - subscriptions.append(QuerySubscription(query: { - $0.uuidCol == UUID() - })) - } - }) - - if config.objectTypes == nil { - config.objectTypes = [SwiftTypesSyncObject.self, SwiftPerson.self] - } - do { - _ = try await Realm(configuration: config, downloadBeforeOpen: .once) - } catch let error as Realm.Error { - XCTAssertEqual(error.code, .subscriptionFailed) - } - } - - @MainActor - func testFlexibleSyncInitialSubscriptionsDefaultConfiguration() async throws { - let user = try await logInUser(for: basicCredentials(app: self.flexibleSyncApp), app: self.flexibleSyncApp) - var config = user.flexibleSyncConfiguration(initialSubscriptions: { subscriptions in - subscriptions.append(QuerySubscription()) - }) - - if config.objectTypes == nil { - config.objectTypes = [SwiftTypesSyncObject.self, SwiftPerson.self] - } - Realm.Configuration.defaultConfiguration = config - - let realm = try await Realm(downloadBeforeOpen: .once) - XCTAssertEqual(realm.subscriptions.count, 1) - } - - // MARK: Subscribe - -#if swift(>=5.8) - @MainActor - func testSubscribe() async throws { - try await populateSwiftPerson() - - let realm = try openFlexibleSyncRealm() - let results0 = try await realm.objects(SwiftPerson.self).where { $0.age >= 6 }.subscribe() - XCTAssertEqual(results0.count, 5) - XCTAssertEqual(realm.subscriptions.count, 1) - let results1 = try await realm.objects(SwiftPerson.self).where { $0.lastName == "lastname_3" }.subscribe() - XCTAssertEqual(results1.count, 1) - XCTAssertEqual(results0.count, 5) - XCTAssertEqual(realm.subscriptions.count, 2) - let results2 = realm.objects(SwiftPerson.self) - XCTAssertEqual(results2.count, 6) - } - - @MainActor - func testSubscribeReassign() async throws { - try await populateSwiftPerson() - let realm = try openFlexibleSyncRealm() - - var results0 = try await realm.objects(SwiftPerson.self).where { $0.age >= 8 }.subscribe() - XCTAssertEqual(results0.count, 3) - XCTAssertEqual(realm.subscriptions.count, 1) - results0 = try await results0.where { $0.age < 8 }.subscribe() // results0 local query is { $0.age >= 8 AND $0.age < 8 } - XCTAssertEqual(results0.count, 0) // no matches because local query is impossible - XCTAssertEqual(realm.subscriptions.count, 2) // two subscriptions: "$0.age >= 8 AND $0.age < 8" and "$0.age >= 8" - let results1 = realm.objects(SwiftPerson.self) - XCTAssertEqual(results1.count, 3) // three objects from "$0.age >= 8". None "$0.age >= 8 AND $0.age < 8". - } - - @MainActor - func testSubscribeSameQueryNoName() async throws { - try await populateSwiftPerson() - let realm = try openFlexibleSyncRealm() - - let results0 = try await realm.objects(SwiftPerson.self).where { $0.age >= 8 }.subscribe() - let ex = XCTestExpectation(description: "no attempt to re-create subscription, returns immediately") - realm.syncSession!.suspend() - _ = try await realm.objects(SwiftPerson.self).where { $0.age >= 8 }.subscribe() - _ = try await results0.subscribe() - XCTAssertEqual(realm.subscriptions.count, 1) - ex.fulfill() - await fulfillment(of: [ex], timeout: 2.0) - XCTAssertEqual(realm.subscriptions.count, 1) - } - - @MainActor - func testSubscribeSameQuerySameName() async throws { - try await populateSwiftPerson() - let realm = try openFlexibleSyncRealm() - - let results0 = try await realm.objects(SwiftPerson.self).where { $0.age >= 8 }.subscribe(name: "8 or older") - realm.syncSession!.suspend() - let ex = XCTestExpectation(description: "no attempt to re-create subscription, returns immediately") - Task { - _ = try await realm.objects(SwiftPerson.self).where { $0.age >= 8 }.subscribe(name: "8 or older") - _ = try await results0.subscribe(name: "8 or older") - XCTAssertEqual(realm.subscriptions.count, 1) - ex.fulfill() - } - await fulfillment(of: [ex], timeout: 5.0) - XCTAssertEqual(realm.subscriptions.count, 1) - } - - @MainActor - func testSubscribeSameQueryDifferentName() async throws { - try await populateSwiftPerson() - let realm = try openFlexibleSyncRealm() - - let results0 = try await realm.objects(SwiftPerson.self).where { $0.age >= 8 }.subscribe() - _ = try await realm.objects(SwiftPerson.self).where { $0.age >= 8 }.subscribe(name: "8 or older") - _ = try await results0.subscribe(name: "older than 7") - XCTAssertEqual(realm.subscriptions.count, 3) - let subscriptions = realm.subscriptions - XCTAssertNil(subscriptions[0]!.name) - XCTAssertEqual(subscriptions[1]!.name, "8 or older") - XCTAssertEqual(subscriptions[2]!.name, "older than 7") - } - - @MainActor - func testSubscribeDifferentQuerySameName() async throws { - try await populateSwiftPerson() - let realm = try openFlexibleSyncRealm() - - _ = try await realm.objects(SwiftPerson.self).where { $0.age > 8 }.subscribe(name: "group1") - _ = try await realm.objects(SwiftPerson.self).where { $0.age > 5 }.subscribe(name: "group1") - XCTAssertEqual(realm.subscriptions.count, 1) -XCTAssertNotNil(realm.subscriptions.first(ofType: SwiftPerson.self) { $0.age > 5 }) - } - - @MainActor - func testSubscribeOnRealmConfinedActor() async throws { - try await populateSwiftPerson() - - try await populateSwiftPerson() - - let user = try await logInUser(for: basicCredentials(app: self.flexibleSyncApp), app: self.flexibleSyncApp) - var config = user.flexibleSyncConfiguration() - config.objectTypes = [SwiftPerson.self] - let realm = try await Realm(configuration: config, actor: MainActor.shared) - let results1 = try await realm.objects(SwiftPerson.self).where { $0.age > 8 }.subscribe(waitForSync: .onCreation) - XCTAssertEqual(results1.count, 2) - let results2 = try await realm.objects(SwiftPerson.self).where { $0.age > 6 }.subscribe(waitForSync: .always) - XCTAssertEqual(results2.count, 4) - let results3 = try await realm.objects(SwiftPerson.self).where { $0.age > 4 }.subscribe(waitForSync: .never) - XCTAssertEqual(results3.count, 4) - XCTAssertEqual(realm.subscriptions.count, 3) - } - - @CustomGlobalActor - func testSubscribeOnRealmConfinedCustomActor() async throws { - try await populateSwiftPerson() - - let user = try await logInUser(for: basicCredentials(app: self.flexibleSyncApp), app: self.flexibleSyncApp) - var config = user.flexibleSyncConfiguration() - config.objectTypes = [SwiftPerson.self] - let realm = try await Realm(configuration: config, actor: CustomGlobalActor.shared) - let results1 = try await realm.objects(SwiftPerson.self).where { $0.age > 8 }.subscribe(waitForSync: .onCreation) - XCTAssertEqual(results1.count, 2) - let results2 = try await realm.objects(SwiftPerson.self).where { $0.age > 6 }.subscribe(waitForSync: .always) - XCTAssertEqual(results2.count, 4) - let results3 = try await realm.objects(SwiftPerson.self).where { $0.age > 4 }.subscribe(waitForSync: .never) - XCTAssertEqual(results3.count, 4) - XCTAssertEqual(realm.subscriptions.count, 3) - } - - @MainActor - func testUnsubscribe() async throws { - try await populateSwiftPerson() - let realm = try openFlexibleSyncRealm() - - let results1 = try await realm.objects(SwiftPerson.self).where { $0.lastName == "lastname_3" }.subscribe() - XCTAssertEqual(realm.subscriptions.count, 1) - results1.unsubscribe() - XCTAssertEqual(realm.subscriptions.count, 0) - } - - @MainActor - func testUnsubscribeAfterReassign() async throws { - try await populateSwiftPerson() - let realm = try openFlexibleSyncRealm() - - var results0 = try await realm.objects(SwiftPerson.self).where { $0.age >= 8 }.subscribe() - XCTAssertEqual(results0.count, 3) - XCTAssertEqual(realm.subscriptions.count, 1) - results0 = try await results0.where { $0.age < 8 }.subscribe() // subscribes to "age >= 8 && age < 8" because that's the local query - XCTAssertEqual(results0.count, 0) - XCTAssertEqual(realm.subscriptions.count, 2) // Two subs present:1) "age >= 8" 2) "age >= 8 && age < 8" - let results1 = realm.objects(SwiftPerson.self) - XCTAssertEqual(results1.count, 3) - results0.unsubscribe() // unsubscribes from "age >= 8 && age < 8" - XCTAssertEqual(realm.subscriptions.count, 1) - XCTAssertNotNil(realm.subscriptions.first(ofType: SwiftPerson.self) { $0.age >= 8 }) - XCTAssertEqual(results0.count, 0) // local query is still "age >= 8 && age < 8". - XCTAssertEqual(results1.count, 3) - } - - @MainActor - func testUnsubscribeWithoutSubscriptionExistingNamed() async throws { - try await populateSwiftPerson() - let realm = try openFlexibleSyncRealm() - - _ = try await realm.objects(SwiftPerson.self).where { $0.age >= 8 }.subscribe(name: "sub1") - XCTAssertEqual(realm.subscriptions.count, 1) - let results = realm.objects(SwiftPerson.self).where { $0.age >= 8 } - results.unsubscribe() - XCTAssertEqual(realm.subscriptions.count, 1) - XCTAssertEqual(realm.subscriptions.first!.name, "sub1") - } - - func testUnsubscribeNoExistingMatch() async throws { - try await populateSwiftPerson() - let realm = try openFlexibleSyncRealm() - - XCTAssertEqual(realm.subscriptions.count, 0) - _ = try await realm.objects(SwiftPerson.self).where { $0.age >= 8 }.subscribe(name: "age_older_8") - let results0 = realm.objects(SwiftPerson.self).where { $0.age >= 8 } - XCTAssertEqual(realm.subscriptions.count, 1) - XCTAssertEqual(results0.count, 3) - results0.unsubscribe() - XCTAssertEqual(realm.subscriptions.count, 1) - XCTAssertEqual(results0.count, 3) // Results are not modified because there is no subscription associated to the unsubscribed result - } - - @MainActor - func testUnsubscribeNamed() async throws { - try await populateSwiftPerson() - let realm = try openFlexibleSyncRealm() - - _ = try await realm.objects(SwiftPerson.self).where { $0.age >= 8 }.subscribe() - _ = try await realm.objects(SwiftPerson.self).where { $0.age >= 8 }.subscribe(name: "first_named") - let results = try await realm.objects(SwiftPerson.self).where { $0.age >= 8 }.subscribe(name: "second_named") - XCTAssertEqual(realm.subscriptions.count, 3) - - results.unsubscribe() - XCTAssertEqual(realm.subscriptions.count, 2) - XCTAssertEqual(realm.subscriptions[0]!.name, nil) - XCTAssertEqual(realm.subscriptions[1]!.name, "first_named") - results.unsubscribe() // check again for case when subscription doesn't exist - XCTAssertEqual(realm.subscriptions.count, 2) - XCTAssertEqual(realm.subscriptions[0]!.name, nil) - XCTAssertEqual(realm.subscriptions[1]!.name, "first_named") - } - - @MainActor - func testUnsubscribeReassign() async throws { - try await populateSwiftPerson() - let realm = try openFlexibleSyncRealm() - - _ = try await realm.objects(SwiftPerson.self).where { $0.age >= 8 }.subscribe(name: "first_named") - var results = try await realm.objects(SwiftPerson.self).where { $0.age >= 8 }.subscribe(name: "second_named") - // expect `results` associated subscription to be reassigned to the id which matches the unnamed subscription - results = try await realm.objects(SwiftPerson.self).where { $0.age >= 8 }.subscribe() - XCTAssertEqual(realm.subscriptions.count, 3) - - results.unsubscribe() - // so the two named subscriptions remain. - XCTAssertEqual(realm.subscriptions.count, 2) - XCTAssertEqual(realm.subscriptions[0]!.name, "first_named") - XCTAssertEqual(realm.subscriptions[1]!.name, "second_named") - } - - @MainActor - func testUnsubscribeSameQueryDifferentName() async throws { - try await populateSwiftPerson() - let realm = try openFlexibleSyncRealm() - - _ = try await realm.objects(SwiftPerson.self).where { $0.age >= 8 }.subscribe() - let results2 = realm.objects(SwiftPerson.self).where { $0.age >= 8 } - XCTAssertEqual(realm.subscriptions.count, 1) - results2.unsubscribe() - XCTAssertEqual(realm.subscriptions.count, 0) - } - - @MainActor - func testSubscribeNameAcrossTypes() async throws { - try await populateSwiftPerson() - let realm = try openFlexibleSyncRealm() - - let results = try await realm.objects(SwiftPerson.self).where { $0.age >= 8 }.subscribe(name: "sameName") - XCTAssertEqual(realm.subscriptions.count, 1) - XCTAssertEqual(results.count, 3) - _ = try await realm.objects(SwiftTypesSyncObject.self).subscribe(name: "sameName") - XCTAssertEqual(realm.subscriptions.count, 1) - XCTAssertEqual(results.count, 0) - } - - @MainActor - func testSubscribeOnCreation() async throws { - try await populateSwiftPerson() - let realm = try openFlexibleSyncRealm() - - var results = try await realm.objects(SwiftPerson.self).where { $0.age >= 8 }.subscribe(waitForSync: .onCreation) - XCTAssertEqual(results.count, 3) - let expectation = XCTestExpectation(description: "method doesn't hang") - realm.syncSession!.suspend() - Task { - results = try! await realm.objects(SwiftPerson.self).where { $0.age >= 8 }.subscribe(waitForSync: .onCreation) - XCTAssertEqual(results.count, 3) // expect method to return immediately, and not hang while no connection - XCTAssertEqual(realm.subscriptions.count, 1) - expectation.fulfill() - } - await fulfillment(of: [expectation], timeout: 2.0) - } - - @MainActor - func testSubscribeAlways() async throws { - let collection = try await setupCollection("SwiftPerson") - try await populateSwiftPerson() - let realm = try openFlexibleSyncRealm() - - var results = try await realm.objects(SwiftPerson.self).where { $0.age >= 9 }.subscribe(waitForSync: .always) - XCTAssertEqual(results.count, 2) - - // suspend session on client. Add a document that isn't on the client. - realm.syncSession!.suspend() - let serverObject: Document = [ - "_id": .objectId(ObjectId.generate()), - "firstName": .string("Paul"), - "lastName": .string("M"), - "age": .int32(30) - ] - collection.insertOne(serverObject).await(self, timeout: 10.0) - - let start = Date() - while collection.count(filter: [:]).await(self) != 11 && start.timeIntervalSinceNow > -10.0 { - sleep(1) // wait until server sync - } - - // Resume the client session. - realm.syncSession!.resume() - XCTAssertEqual(results.count, 2) - results = try await realm.objects(SwiftPerson.self).where { $0.age >= 9 }.subscribe(waitForSync: .always) - // Expect this subscribe call to wait for sync downloads, even though the subscription already existed - XCTAssertEqual(results.count, 3) // Count is 3 because it includes the object/document that was created while offline. - XCTAssertEqual(realm.subscriptions.count, 1) - } - - @MainActor - func testSubscribeNever() async throws { - try await populateSwiftPerson() - let realm = try openFlexibleSyncRealm() - - let expectation = XCTestExpectation(description: "test doesn't hang") - Task { - let results = try await realm.objects(SwiftPerson.self).where { $0.age >= 8 }.subscribe(waitForSync: .never) - XCTAssertEqual(results.count, 0) // expect no objects to be able to sync because of immediate return - XCTAssertEqual(realm.subscriptions.count, 1) - expectation.fulfill() - } - await fulfillment(of: [expectation], timeout: 1) - } - - @MainActor - func testSubscribeTimeout() async throws { - try await populateSwiftPerson() - let realm = try openFlexibleSyncRealm() - - realm.syncSession!.suspend() - let timeout = 1.0 - do { - _ = try await realm.objects(SwiftPerson.self).where { $0.age >= 8 } - .subscribe(waitForSync: .always, timeout: timeout) - XCTFail("subscribe did not time out") - } catch let error as NSError { - XCTAssertEqual(error.code, Int(ETIMEDOUT)) - XCTAssertEqual(error.domain, NSPOSIXErrorDomain) - XCTAssertEqual(error.localizedDescription, "Waiting for update timed out after \(timeout) seconds.") - } - } - - @MainActor - func testSubscribeTimeoutSucceeds() async throws { - try await populateSwiftPerson() - - let realm = try openFlexibleSyncRealm() - let results0 = try await realm.objects(SwiftPerson.self).where { $0.age >= 6 }.subscribe(timeout: 2.0) - XCTAssertEqual(results0.count, 5) - XCTAssertEqual(realm.subscriptions.count, 1) - let results1 = try await realm.objects(SwiftPerson.self).where { $0.lastName == "lastname_3" }.subscribe(timeout: 2.0) - XCTAssertEqual(results1.count, 1) - XCTAssertEqual(results0.count, 5) - XCTAssertEqual(realm.subscriptions.count, 2) - - let results2 = realm.objects(SwiftPerson.self) - XCTAssertEqual(results2.count, 6) - } -#endif - - // MARK: - Custom Column - - @MainActor - func testCustomColumnFlexibleSyncSchema() async throws { - let user = try await logInUser(for: basicCredentials(app: self.flexibleSyncApp), app: self.flexibleSyncApp) - var config = user.flexibleSyncConfiguration() - config.objectTypes = [SwiftCustomColumnObject.self] - let realm = try await Realm(configuration: config) - - for property in realm.schema.objectSchema.first(where: { $0.className == "SwiftCustomColumnObject" })!.properties { - XCTAssertEqual(customColumnPropertiesMapping[property.name], property.columnName) - } - } - - @MainActor - func testCreateCustomColumnFlexibleSyncSubscription() async throws { - let objectId = ObjectId.generate() - try await populateFlexibleSyncData { realm in - let valuesDictionary: [String: Any] = ["id": objectId, - "boolCol": true, - "intCol": 365, - "doubleCol": 365.365, - "stringCol": "@#¢∞¬÷÷", - "binaryCol": "string".data(using: String.Encoding.utf8)!, - "dateCol": Date(timeIntervalSince1970: -365), - "longCol": 365, - "decimalCol": Decimal128(365), - "uuidCol": UUID(uuidString: "629bba42-97dc-4fee-97ff-78af054952ec")!, - "objectIdCol": ObjectId.generate()] - - realm.create(SwiftCustomColumnObject.self, value: valuesDictionary) - } - - let user = try await logInUser(for: basicCredentials(app: self.flexibleSyncApp), app: self.flexibleSyncApp) - var config = user.flexibleSyncConfiguration(initialSubscriptions: { subscriptions in - subscriptions.append(QuerySubscription()) - }) - config.objectTypes = [SwiftCustomColumnObject.self] - let realm = try await Realm(configuration: config, downloadBeforeOpen: .once) - XCTAssertEqual(realm.subscriptions.count, 1) - - let foundObject = realm.object(ofType: SwiftCustomColumnObject.self, forPrimaryKey: objectId) - XCTAssertNotNil(foundObject) - XCTAssertEqual(foundObject!.id, objectId) - XCTAssertEqual(foundObject!.boolCol, true) - XCTAssertEqual(foundObject!.intCol, 365) - XCTAssertEqual(foundObject!.doubleCol, 365.365) - XCTAssertEqual(foundObject!.stringCol, "@#¢∞¬÷÷") - XCTAssertEqual(foundObject!.binaryCol, "string".data(using: String.Encoding.utf8)!) - XCTAssertEqual(foundObject!.dateCol, Date(timeIntervalSince1970: -365)) - XCTAssertEqual(foundObject!.longCol, 365) - XCTAssertEqual(foundObject!.decimalCol, Decimal128(365)) - XCTAssertEqual(foundObject!.uuidCol, UUID(uuidString: "629bba42-97dc-4fee-97ff-78af054952ec")!) - XCTAssertNotNil(foundObject?.objectIdCol) - XCTAssertNil(foundObject?.objectCol) - } - - @MainActor - func testCustomColumnFlexibleSyncSubscriptionNSPredicate() async throws { - let objectId = ObjectId.generate() - let linkedObjectId = ObjectId.generate() - try await populateFlexibleSyncData { realm in - let object = SwiftCustomColumnObject() - object.id = objectId - object.binaryCol = "string".data(using: String.Encoding.utf8)! - let linkedObject = SwiftCustomColumnObject() - linkedObject.id = linkedObjectId - object.objectCol = linkedObject - realm.add(object) - } - let user = try await logInUser(for: basicCredentials(app: self.flexibleSyncApp), app: self.flexibleSyncApp) - - var config = user.flexibleSyncConfiguration(initialSubscriptions: { subscriptions in - subscriptions.append(QuerySubscription(where: NSPredicate(format: "id == %@ || id == %@", objectId, linkedObjectId))) - }) - config.objectTypes = [SwiftCustomColumnObject.self] - let realm = try await Realm(configuration: config, downloadBeforeOpen: .once) - XCTAssertEqual(realm.subscriptions.count, 1) - checkCount(expected: 2, realm, SwiftCustomColumnObject.self) - - let foundObject = realm.objects(SwiftCustomColumnObject.self).where { $0.id == objectId }.first - XCTAssertNotNil(foundObject) - XCTAssertEqual(foundObject!.id, objectId) - XCTAssertEqual(foundObject!.boolCol, true) - XCTAssertEqual(foundObject!.intCol, 1) - XCTAssertEqual(foundObject!.doubleCol, 1.1) - XCTAssertEqual(foundObject!.stringCol, "string") - XCTAssertEqual(foundObject!.binaryCol, "string".data(using: String.Encoding.utf8)!) - XCTAssertEqual(foundObject!.dateCol, Date(timeIntervalSince1970: -1)) - XCTAssertEqual(foundObject!.longCol, 1) - XCTAssertEqual(foundObject!.decimalCol, Decimal128(1)) - XCTAssertEqual(foundObject!.uuidCol, UUID(uuidString: "85d4fbee-6ec6-47df-bfa1-615931903d7e")!) - XCTAssertNil(foundObject?.objectIdCol) - XCTAssertEqual(foundObject!.objectCol!.id, linkedObjectId) - } - - @MainActor - func testCustomColumnFlexibleSyncSubscriptionFilter() async throws { - let objectId = ObjectId.generate() - let linkedObjectId = ObjectId.generate() - try await populateFlexibleSyncData { realm in - let object = SwiftCustomColumnObject() - object.id = objectId - object.binaryCol = "string".data(using: String.Encoding.utf8)! - let linkedObject = SwiftCustomColumnObject() - linkedObject.id = linkedObjectId - object.objectCol = linkedObject - realm.add(object) - } - let user = try await logInUser(for: basicCredentials(app: self.flexibleSyncApp), app: self.flexibleSyncApp) - - var config = user.flexibleSyncConfiguration(initialSubscriptions: { subscriptions in - subscriptions.append(QuerySubscription(where: "id == %@ || id == %@", objectId, linkedObjectId)) - }) - config.objectTypes = [SwiftCustomColumnObject.self] - let realm = try await Realm(configuration: config, downloadBeforeOpen: .once) - XCTAssertEqual(realm.subscriptions.count, 1) - checkCount(expected: 2, realm, SwiftCustomColumnObject.self) - - let foundObject = realm.objects(SwiftCustomColumnObject.self).where { $0.id == objectId }.first - XCTAssertNotNil(foundObject) - XCTAssertEqual(foundObject!.id, objectId) - XCTAssertEqual(foundObject!.boolCol, true) - XCTAssertEqual(foundObject!.intCol, 1) - XCTAssertEqual(foundObject!.doubleCol, 1.1) - XCTAssertEqual(foundObject!.stringCol, "string") - XCTAssertEqual(foundObject!.binaryCol, "string".data(using: String.Encoding.utf8)!) - XCTAssertEqual(foundObject!.dateCol, Date(timeIntervalSince1970: -1)) - XCTAssertEqual(foundObject!.longCol, 1) - XCTAssertEqual(foundObject!.decimalCol, Decimal128(1)) - XCTAssertEqual(foundObject!.uuidCol, UUID(uuidString: "85d4fbee-6ec6-47df-bfa1-615931903d7e")!) - XCTAssertNil(foundObject?.objectIdCol) - XCTAssertEqual(foundObject!.objectCol!.id, linkedObjectId) - } - - @MainActor - func testCustomColumnFlexibleSyncSubscriptionQuery() async throws { - let objectId = ObjectId.generate() - let linkedObjectId = ObjectId.generate() - try await populateFlexibleSyncData { realm in - let object = SwiftCustomColumnObject() - object.id = objectId - object.binaryCol = "string".data(using: String.Encoding.utf8)! - let linkedObject = SwiftCustomColumnObject() - linkedObject.id = linkedObjectId - object.objectCol = linkedObject - realm.add(object) - } - let user = try await logInUser(for: basicCredentials(app: self.flexibleSyncApp), app: self.flexibleSyncApp) - - var config = user.flexibleSyncConfiguration(initialSubscriptions: { subscriptions in - subscriptions.append(QuerySubscription { - $0.id == objectId || $0.id == linkedObjectId - }) - }) - config.objectTypes = [SwiftCustomColumnObject.self] - let realm = try await Realm(configuration: config, downloadBeforeOpen: .once) - XCTAssertEqual(realm.subscriptions.count, 1) - checkCount(expected: 2, realm, SwiftCustomColumnObject.self) - - let foundObject = realm.objects(SwiftCustomColumnObject.self).where { $0.id == objectId }.first - - XCTAssertNotNil(foundObject) - XCTAssertEqual(foundObject!.id, objectId) - XCTAssertEqual(foundObject!.boolCol, true) - XCTAssertEqual(foundObject!.intCol, 1) - XCTAssertEqual(foundObject!.doubleCol, 1.1) - XCTAssertEqual(foundObject!.stringCol, "string") - XCTAssertEqual(foundObject!.binaryCol, "string".data(using: String.Encoding.utf8)!) - XCTAssertEqual(foundObject!.dateCol, Date(timeIntervalSince1970: -1)) - XCTAssertEqual(foundObject!.longCol, 1) - XCTAssertEqual(foundObject!.decimalCol, Decimal128(1)) - XCTAssertEqual(foundObject!.uuidCol, UUID(uuidString: "85d4fbee-6ec6-47df-bfa1-615931903d7e")!) - XCTAssertNil(foundObject?.objectIdCol) - XCTAssertEqual(foundObject!.objectCol!.id, linkedObjectId) - } -} -#endif // canImport(_Concurrency) - -// MARK: - Combine -#if !(os(iOS) && (arch(i386) || arch(arm))) -@available(macOS 10.15, *) -extension SwiftFlexibleSyncServerTests { - func testFlexibleSyncCombineWrite() throws { - try populateFlexibleSyncData { realm in - for i in 1...25 { - let person = SwiftPerson(firstName: "\(#function)", - lastName: "lastname_\(i)", - age: i) - realm.add(person) - } - } - - let realm = try flexibleSyncRealm() - checkCount(expected: 0, realm, SwiftPerson.self) - - let subscriptions = realm.subscriptions - XCTAssertEqual(subscriptions.count, 0) - - let ex = expectation(description: "state change complete") - subscriptions.updateSubscriptions { - subscriptions.append(QuerySubscription(name: "person_age_10") { - $0.age > 10 && $0.firstName == "\(#function)" - }) - }.sink(receiveCompletion: { @Sendable _ in }, - receiveValue: { @Sendable _ in ex.fulfill() } - ).store(in: &cancellables) - - waitForExpectations(timeout: 20.0, handler: nil) - - waitForDownloads(for: realm) - checkCount(expected: 15, realm, SwiftPerson.self) - } - -#if false // FIXME: this is no longer an error and needs to be updated to something which is - func testFlexibleSyncCombineWriteFails() throws { - let realm = try flexibleSyncRealm() - checkCount(expected: 0, realm, SwiftPerson.self) - - let subscriptions = realm.subscriptions - XCTAssertEqual(subscriptions.count, 0) - - let ex = expectation(description: "state change error") - subscriptions.updateSubscriptions { - subscriptions.append(QuerySubscription(name: "swiftObject_longCol") { - $0.longCol == Int64(1) - }) - } - .sink(receiveCompletion: { result in - if case .failure(let error as Realm.Error) = result { - XCTAssertEqual(error.code, .subscriptionFailed) - guard case .error = subscriptions.state else { - return XCTFail("Adding a query for a not queryable field should change the subscription set state to error") - } - } else { - XCTFail("Expected an error but got \(result)") - } - ex.fulfill() - }, receiveValue: { _ in }) - .store(in: &cancellables) - - waitForExpectations(timeout: 20.0, handler: nil) - - waitForDownloads(for: realm) - checkCount(expected: 0, realm, SwiftPerson.self) - } -#endif -} - -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -@globalActor actor CustomGlobalActor: GlobalActor { - static let shared = CustomGlobalActor() -} - -#endif // canImport(Combine) #endif // os(macOS) diff --git a/Realm/ObjectServerTests/SwiftMongoClientTests.swift b/Realm/ObjectServerTests/SwiftMongoClientTests.swift index cc9f7f0164..27141fcc05 100644 --- a/Realm/ObjectServerTests/SwiftMongoClientTests.swift +++ b/Realm/ObjectServerTests/SwiftMongoClientTests.swift @@ -30,11 +30,17 @@ import RealmSwiftTestSupport #endif // MARK: - SwiftMongoClientTests +@available(macOS 13.0, *) class SwiftMongoClientTests: SwiftSyncTestCase { + override var objectTypes: [ObjectBase.Type] { + [Dog.self] + } + override func tearDown() { _ = setupMongoCollection() super.tearDown() } + func testMongoClient() { let user = try! logInUser(for: .anonymous) let mongoClient = user.mongoClient("mongodb1") @@ -46,10 +52,7 @@ class SwiftMongoClientTests: SwiftSyncTestCase { } func setupMongoCollection() -> MongoCollection { - let user = try! logInUser(for: basicCredentials()) - let mongoClient = user.mongoClient("mongodb1") - let database = mongoClient.database(named: "test_data") - let collection = database.collection(withName: "Dog") + let collection = createUser().collection(for: Dog.self, app: app) removeAllFromCollection(collection) return collection } @@ -740,7 +743,7 @@ class SwiftMongoClientTests: SwiftSyncTestCase { // thread and doesn't let async tasks run. Xcode 14.3 introduced a new async // version of it which does work, but there doesn't appear to be a workaround // for older Xcode versions. - @available(macOS 12.0, *) + @available(macOS 13, *) func performAsyncWatchTest(filterIds: Bool = false, matchFilter: Bool = false) async throws { let collection = setupMongoCollection() let objectIds = insertDocuments(collection) @@ -785,17 +788,17 @@ class SwiftMongoClientTests: SwiftSyncTestCase { _ = await task.result } - @available(macOS 12.0, *) + @available(macOS 13, *) func testWatchAsync() async throws { try await performAsyncWatchTest() } - @available(macOS 12.0, *) + @available(macOS 13, *) func testWatchWithMatchFilterAsync() async throws { try await performAsyncWatchTest(matchFilter: true) } - @available(macOS 12.0, *) + @available(macOS 13, *) func testWatchWithFilterIdsAsync() async throws { try await performAsyncWatchTest(filterIds: true) } @@ -854,9 +857,7 @@ class SwiftMongoClientTests: SwiftSyncTestCase { } func testShouldNotDeleteOnMigrationWithSync() throws { - let user = try logInUser(for: basicCredentials()) - var configuration = user.configuration(testName: appId) - + var configuration = try configuration() assertThrows(configuration.deleteRealmIfMigrationNeeded = true, reason: "Cannot set 'deleteRealmIfMigrationNeeded' when sync is enabled ('syncConfig' is set).") @@ -867,9 +868,14 @@ class SwiftMongoClientTests: SwiftSyncTestCase { } } +#if swift(>=5.8) // MARK: - AsyncAwaitMongoClientTests -@available(macOS 12.0, *) +@available(macOS 13, *) class AsyncAwaitMongoClientTests: SwiftSyncTestCase { + override var objectTypes: [ObjectBase.Type] { + [Dog.self] + } + override class var defaultTestSuite: XCTestSuite { // async/await is currently incompatible with thread sanitizer and will // produce many false positives @@ -882,10 +888,7 @@ class AsyncAwaitMongoClientTests: SwiftSyncTestCase { } func setupMongoCollection() async throws -> MongoCollection { - let user = try await self.app.login(credentials: basicCredentials()) - let mongoClient = user.mongoClient("mongodb1") - let database = mongoClient.database(named: "test_data") - let collection = database.collection(withName: "Dog") + let collection = try await createUser().collection(for: Dog.self, app: app) _ = try await collection.deleteManyDocuments(filter: [:]) return collection } @@ -1244,5 +1247,6 @@ class AsyncAwaitMongoClientTests: SwiftSyncTestCase { XCTAssertEqual(count5, 1) } } +#endif #endif // os(macOS) diff --git a/Realm/ObjectServerTests/SwiftObjectServerPartitionTests.swift b/Realm/ObjectServerTests/SwiftObjectServerPartitionTests.swift index d3f7391808..0aabd8c1e5 100644 --- a/Realm/ObjectServerTests/SwiftObjectServerPartitionTests.swift +++ b/Realm/ObjectServerTests/SwiftObjectServerPartitionTests.swift @@ -23,15 +23,19 @@ import XCTest import RealmSwiftSyncTestSupport #endif -@available(OSX 10.14, *) +@available(macOS 13, *) @objc(SwiftObjectServerPartitionTests) class SwiftObjectServerPartitionTests: SwiftSyncTestCase { - func writeObjects(_ user: User, _ partitionValue: T) throws { - try autoreleasepool { - var config = user.configuration(partitionValue: partitionValue) - config.objectTypes = [SwiftPerson.self] - let realm = try Realm(configuration: config) + func configuration(_ user: User, _ partitionValue: some BSON) -> Realm.Configuration { + var config = user.configuration(partitionValue: partitionValue) + config.objectTypes = [SwiftPerson.self] + return config + } + + func writeObjects(_ user: User, _ partitionValue: some BSON) throws { + try autoreleasepool { + let realm = try Realm(configuration: configuration(user, partitionValue)) try realm.write { realm.add(SwiftPerson(firstName: "Ringo", lastName: "Starr")) realm.add(SwiftPerson(firstName: "John", lastName: "Lennon")) @@ -42,14 +46,13 @@ class SwiftObjectServerPartitionTests: SwiftSyncTestCase { } } - func roundTripForPartitionValue(partitionValue: T) throws { + func roundTripForPartitionValue(partitionValue: some BSON) throws { let partitionType = partitionBsonType(ObjectiveCSupport.convert(object: AnyBSON(partitionValue))!) - let appId = try RealmServer.shared.createAppForBSONType(partitionType) - - let partitionApp = app(withId: appId) - let user = try logInUser(for: basicCredentials(app: partitionApp), app: partitionApp) - let user2 = try logInUser(for: Credentials.anonymous, app: partitionApp) - let realm = try openRealm(partitionValue: partitionValue, user: user) + let appId = try RealmServer.shared.createApp(partitionKeyType: partitionType, types: [SwiftPerson.self]) + let partitionApp = app(id: appId) + let user = createUser(for: partitionApp) + let user2 = createUser(for: partitionApp) + let realm = try Realm(configuration: configuration(user, partitionValue)) checkCount(expected: 0, realm, SwiftPerson.self) try writeObjects(user2, partitionValue) @@ -61,8 +64,6 @@ class SwiftObjectServerPartitionTests: SwiftSyncTestCase { waitForDownloads(for: realm) checkCount(expected: 8, realm, SwiftPerson.self) XCTAssertEqual(realm.objects(SwiftPerson.self).filter { $0.firstName == "Ringo" }.count, 2) - - try RealmServer.shared.deleteApp(appId) } func testSwiftRoundTripForObjectIdPartitionValue() throws { diff --git a/Realm/ObjectServerTests/SwiftObjectServerTests.swift b/Realm/ObjectServerTests/SwiftObjectServerTests.swift index f626adcabc..cdd0b5ab92 100644 --- a/Realm/ObjectServerTests/SwiftObjectServerTests.swift +++ b/Realm/ObjectServerTests/SwiftObjectServerTests.swift @@ -31,11 +31,6 @@ import RealmTestSupport import RealmSwiftTestSupport #endif -// SE-0392 exposes this functionality directly, but for now we have to call the -// internal standard library function -@_silgen_name("swift_job_run") -private func _swiftJobRun(_ job: UnownedJob, _ executor: UnownedSerialExecutor) - func assertAppError(_ error: AppError, _ code: AppError.Code, _ message: String, line: UInt = #line, file: StaticString = #file) { XCTAssertEqual(error.code, code, file: file, line: line) @@ -50,210 +45,176 @@ func assertSyncError(_ error: Error, _ code: SyncError.Code, _ message: String, XCTAssertEqual(e.localizedDescription, message, file: file, line: line) } -@available(OSX 10.14, *) +@available(macOS 13.0, *) @objc(SwiftObjectServerTests) class SwiftObjectServerTests: SwiftSyncTestCase { - /// It should be possible to successfully open a Realm configured for sync. - func testBasicSwiftSync() throws { - let user = try logInUser(for: basicCredentials()) - let realm = try openRealm(partitionValue: #function, user: user) - XCTAssert(realm.isEmpty, "Freshly synced Realm was not empty...") + override var objectTypes: [ObjectBase.Type] { + [ + SwiftCustomColumnObject.self, + SwiftPerson.self, + SwiftTypesSyncObject.self, + SwiftHugeSyncObject.self, + SwiftIntPrimaryKeyObject.self, + SwiftUUIDPrimaryKeyObject.self, + SwiftStringPrimaryKeyObject.self, + SwiftMissingObject.self, + SwiftAnyRealmValueObject.self, + ] } - func testBasicSwiftSyncWithAnyBSONPartitionValue() throws { - let user = try logInUser(for: basicCredentials()) - let realm = try openRealm(partitionValue: .string(#function), user: user) - XCTAssert(realm.isEmpty, "Freshly synced Realm was not empty...") + func testBasicSwiftSync() throws { + XCTAssert(try openRealm().isEmpty, "Freshly synced Realm was not empty...") } - /// If client B adds objects to a Realm, client A should see those new objects. func testSwiftAddObjects() throws { - let user = try logInUser(for: basicCredentials()) - let realm = try openRealm(partitionValue: #function, user: user) - if isParent { - checkCount(expected: 0, realm, SwiftPerson.self) - checkCount(expected: 0, realm, SwiftTypesSyncObject.self) - executeChild() - waitForDownloads(for: realm) - checkCount(expected: 4, realm, SwiftPerson.self) - checkCount(expected: 1, realm, SwiftTypesSyncObject.self) - - let obj = realm.objects(SwiftTypesSyncObject.self).first! - XCTAssertEqual(obj.boolCol, true) - XCTAssertEqual(obj.intCol, 1) - XCTAssertEqual(obj.doubleCol, 1.1) - XCTAssertEqual(obj.stringCol, "string") - XCTAssertEqual(obj.binaryCol, "string".data(using: String.Encoding.utf8)!) - XCTAssertEqual(obj.decimalCol, Decimal128(1)) - XCTAssertEqual(obj.dateCol, Date(timeIntervalSince1970: -1)) - XCTAssertEqual(obj.longCol, Int64(1)) - XCTAssertEqual(obj.uuidCol, UUID(uuidString: "85d4fbee-6ec6-47df-bfa1-615931903d7e")!) - XCTAssertEqual(obj.anyCol.intValue, 1) - XCTAssertEqual(obj.objectCol!.firstName, "George") - - } else { - // Add objects - try realm.write { - realm.add(SwiftPerson(firstName: "Ringo", lastName: "Starr")) - realm.add(SwiftPerson(firstName: "John", lastName: "Lennon")) - realm.add(SwiftPerson(firstName: "Paul", lastName: "McCartney")) - realm.add(SwiftTypesSyncObject(person: SwiftPerson(firstName: "George", lastName: "Harrison"))) - } - waitForUploads(for: realm) - checkCount(expected: 4, realm, SwiftPerson.self) - checkCount(expected: 1, realm, SwiftTypesSyncObject.self) + let realm = try openRealm() + checkCount(expected: 0, realm, SwiftPerson.self) + checkCount(expected: 0, realm, SwiftTypesSyncObject.self) + + try write { realm in + realm.add(SwiftPerson(firstName: "Ringo", lastName: "Starr")) + realm.add(SwiftPerson(firstName: "John", lastName: "Lennon")) + realm.add(SwiftPerson(firstName: "Paul", lastName: "McCartney")) + realm.add(SwiftTypesSyncObject(person: SwiftPerson(firstName: "George", lastName: "Harrison"))) } + + waitForDownloads(for: realm) + checkCount(expected: 4, realm, SwiftPerson.self) + checkCount(expected: 1, realm, SwiftTypesSyncObject.self) + + let obj = realm.objects(SwiftTypesSyncObject.self).first! + XCTAssertEqual(obj.boolCol, true) + XCTAssertEqual(obj.intCol, 1) + XCTAssertEqual(obj.doubleCol, 1.1) + XCTAssertEqual(obj.stringCol, "string") + XCTAssertEqual(obj.binaryCol, "string".data(using: String.Encoding.utf8)!) + XCTAssertEqual(obj.decimalCol, Decimal128(1)) + XCTAssertEqual(obj.dateCol, Date(timeIntervalSince1970: -1)) + XCTAssertEqual(obj.longCol, Int64(1)) + XCTAssertEqual(obj.uuidCol, UUID(uuidString: "85d4fbee-6ec6-47df-bfa1-615931903d7e")!) + XCTAssertEqual(obj.anyCol.intValue, 1) + XCTAssertEqual(obj.objectCol!.firstName, "George") } func testSwiftRountripForDistinctPrimaryKey() throws { - let user = try logInUser(for: basicCredentials()) - let realm = try openRealm(partitionValue: #function, user: user) - if isParent { - checkCount(expected: 0, realm, SwiftPerson.self) // ObjectId - checkCount(expected: 0, realm, SwiftUUIDPrimaryKeyObject.self) - checkCount(expected: 0, realm, SwiftStringPrimaryKeyObject.self) - checkCount(expected: 0, realm, SwiftIntPrimaryKeyObject.self) - executeChild() - waitForDownloads(for: realm) - checkCount(expected: 1, realm, SwiftPerson.self) - checkCount(expected: 1, realm, SwiftUUIDPrimaryKeyObject.self) - checkCount(expected: 1, realm, SwiftStringPrimaryKeyObject.self) - checkCount(expected: 1, realm, SwiftIntPrimaryKeyObject.self) - - let swiftOjectIdPrimaryKeyObject = realm.object(ofType: SwiftPerson.self, - forPrimaryKey: ObjectId("1234567890ab1234567890ab"))! - XCTAssertEqual(swiftOjectIdPrimaryKeyObject.firstName, "Ringo") - XCTAssertEqual(swiftOjectIdPrimaryKeyObject.lastName, "Starr") - - let swiftUUIDPrimaryKeyObject = realm.object(ofType: SwiftUUIDPrimaryKeyObject.self, - forPrimaryKey: UUID(uuidString: "85d4fbee-6ec6-47df-bfa1-615931903d7e")!)! - XCTAssertEqual(swiftUUIDPrimaryKeyObject.strCol, "Steve") - XCTAssertEqual(swiftUUIDPrimaryKeyObject.intCol, 10) - - let swiftStringPrimaryKeyObject = realm.object(ofType: SwiftStringPrimaryKeyObject.self, - forPrimaryKey: "1234567890ab1234567890ab")! - XCTAssertEqual(swiftStringPrimaryKeyObject.strCol, "Paul") - XCTAssertEqual(swiftStringPrimaryKeyObject.intCol, 20) - - let swiftIntPrimaryKeyObject = realm.object(ofType: SwiftIntPrimaryKeyObject.self, - forPrimaryKey: 1234567890)! - XCTAssertEqual(swiftIntPrimaryKeyObject.strCol, "Jackson") - XCTAssertEqual(swiftIntPrimaryKeyObject.intCol, 30) - } else { - try realm.write { - let swiftPerson = SwiftPerson(firstName: "Ringo", lastName: "Starr") - swiftPerson._id = ObjectId("1234567890ab1234567890ab") - realm.add(swiftPerson) - realm.add(SwiftUUIDPrimaryKeyObject(id: UUID(uuidString: "85d4fbee-6ec6-47df-bfa1-615931903d7e")!, strCol: "Steve", intCol: 10)) - realm.add(SwiftStringPrimaryKeyObject(id: "1234567890ab1234567890ab", strCol: "Paul", intCol: 20)) - realm.add(SwiftIntPrimaryKeyObject(id: 1234567890, strCol: "Jackson", intCol: 30)) - } - waitForUploads(for: realm) - checkCount(expected: 1, realm, SwiftPerson.self) - checkCount(expected: 1, realm, SwiftUUIDPrimaryKeyObject.self) - checkCount(expected: 1, realm, SwiftStringPrimaryKeyObject.self) - checkCount(expected: 1, realm, SwiftIntPrimaryKeyObject.self) + let realm = try openRealm() + checkCount(expected: 0, realm, SwiftPerson.self) // ObjectId + checkCount(expected: 0, realm, SwiftUUIDPrimaryKeyObject.self) + checkCount(expected: 0, realm, SwiftStringPrimaryKeyObject.self) + checkCount(expected: 0, realm, SwiftIntPrimaryKeyObject.self) + + try write { realm in + let swiftPerson = SwiftPerson(firstName: "Ringo", lastName: "Starr") + swiftPerson._id = ObjectId("1234567890ab1234567890ab") + realm.add(swiftPerson) + realm.add(SwiftUUIDPrimaryKeyObject(id: UUID(uuidString: "85d4fbee-6ec6-47df-bfa1-615931903d7e")!, strCol: "Steve", intCol: 10)) + realm.add(SwiftStringPrimaryKeyObject(id: "1234567890ab1234567890ab", strCol: "Paul", intCol: 20)) + realm.add(SwiftIntPrimaryKeyObject(id: 1234567890, strCol: "Jackson", intCol: 30)) } + + waitForDownloads(for: realm) + checkCount(expected: 1, realm, SwiftPerson.self) + checkCount(expected: 1, realm, SwiftUUIDPrimaryKeyObject.self) + checkCount(expected: 1, realm, SwiftStringPrimaryKeyObject.self) + checkCount(expected: 1, realm, SwiftIntPrimaryKeyObject.self) + + let swiftOjectIdPrimaryKeyObject = realm.object(ofType: SwiftPerson.self, + forPrimaryKey: ObjectId("1234567890ab1234567890ab"))! + XCTAssertEqual(swiftOjectIdPrimaryKeyObject.firstName, "Ringo") + XCTAssertEqual(swiftOjectIdPrimaryKeyObject.lastName, "Starr") + + let swiftUUIDPrimaryKeyObject = realm.object(ofType: SwiftUUIDPrimaryKeyObject.self, + forPrimaryKey: UUID(uuidString: "85d4fbee-6ec6-47df-bfa1-615931903d7e")!)! + XCTAssertEqual(swiftUUIDPrimaryKeyObject.strCol, "Steve") + XCTAssertEqual(swiftUUIDPrimaryKeyObject.intCol, 10) + + let swiftStringPrimaryKeyObject = realm.object(ofType: SwiftStringPrimaryKeyObject.self, + forPrimaryKey: "1234567890ab1234567890ab")! + XCTAssertEqual(swiftStringPrimaryKeyObject.strCol, "Paul") + XCTAssertEqual(swiftStringPrimaryKeyObject.intCol, 20) + + let swiftIntPrimaryKeyObject = realm.object(ofType: SwiftIntPrimaryKeyObject.self, + forPrimaryKey: 1234567890)! + XCTAssertEqual(swiftIntPrimaryKeyObject.strCol, "Jackson") + XCTAssertEqual(swiftIntPrimaryKeyObject.intCol, 30) } func testSwiftAddObjectsWithNilPartitionValue() throws { - let user = try logInUser(for: basicCredentials()) - let realm = try openRealm(partitionValue: .null, user: user) - - if isParent { - // This test needs the database to be empty of any documents with a nil partition - try realm.write { - realm.deleteAll() - } - waitForUploads(for: realm) + // Use a fresh app as other tests touch the nil partition on the shared app + let app = app(id: try RealmServer.shared.createApp(types: [SwiftPerson.self])) + var config = createUser(for: app).configuration(partitionValue: .null) + config.objectTypes = [SwiftPerson.self] - checkCount(expected: 0, realm, SwiftPerson.self) - executeChild() - waitForDownloads(for: realm) - checkCount(expected: 3, realm, SwiftPerson.self) + let realm = try Realm(configuration: config) + checkCount(expected: 0, realm, SwiftPerson.self) - try realm.write { - realm.deleteAll() - } - waitForUploads(for: realm) - } else { - // Add objects + try autoreleasepool { + var config = createUser(for: app).configuration(partitionValue: .null) + config.objectTypes = [SwiftPerson.self] + let realm = try Realm(configuration: config) try realm.write { realm.add(SwiftPerson(firstName: "Ringo", lastName: "Starr")) realm.add(SwiftPerson(firstName: "John", lastName: "Lennon")) realm.add(SwiftPerson(firstName: "Paul", lastName: "McCartney")) } waitForUploads(for: realm) - checkCount(expected: 3, realm, SwiftPerson.self) } + + waitForDownloads(for: realm) + checkCount(expected: 3, realm, SwiftPerson.self) } - /// If client B removes objects from a Realm, client A should see those changes. func testSwiftDeleteObjects() throws { - let user = try logInUser(for: basicCredentials()) - let realm = try openRealm(partitionValue: #function, user: user) - if isParent { - try realm.write { - realm.add(SwiftPerson(firstName: "Ringo", lastName: "Starr")) - realm.add(SwiftPerson(firstName: "John", lastName: "Lennon")) - realm.add(SwiftPerson(firstName: "Paul", lastName: "McCartney")) - realm.add(SwiftTypesSyncObject(person: SwiftPerson(firstName: "George", lastName: "Harrison"))) - } - waitForUploads(for: realm) - checkCount(expected: 4, realm, SwiftPerson.self) - checkCount(expected: 1, realm, SwiftTypesSyncObject.self) - executeChild() - } else { - checkCount(expected: 4, realm, SwiftPerson.self) - checkCount(expected: 1, realm, SwiftTypesSyncObject.self) - try realm.write { - realm.deleteAll() - } - waitForUploads(for: realm) - checkCount(expected: 0, realm, SwiftPerson.self) - checkCount(expected: 0, realm, SwiftTypesSyncObject.self) + let realm = try openRealm() + try realm.write { + realm.add(SwiftPerson(firstName: "Ringo", lastName: "Starr")) + realm.add(SwiftPerson(firstName: "John", lastName: "Lennon")) + realm.add(SwiftPerson(firstName: "Paul", lastName: "McCartney")) + realm.add(SwiftTypesSyncObject(person: SwiftPerson(firstName: "George", lastName: "Harrison"))) } - } + waitForUploads(for: realm) + checkCount(expected: 4, realm, SwiftPerson.self) + checkCount(expected: 1, realm, SwiftTypesSyncObject.self) - /// A client should be able to open multiple Realms and add objects to each of them. - func testMultipleRealmsAddObjects() throws { - let partitionValueA = #function - let partitionValueB = "\(#function)bar" - let partitionValueC = "\(#function)baz" + try write { realm in + realm.deleteAll() + } - let user = try logInUser(for: basicCredentials()) + checkCount(expected: 0, realm, SwiftPerson.self) + checkCount(expected: 0, realm, SwiftTypesSyncObject.self) + } - let realmA = try openRealm(partitionValue: partitionValueA, user: user) - let realmB = try openRealm(partitionValue: partitionValueB, user: user) - let realmC = try openRealm(partitionValue: partitionValueC, user: user) + func testMultiplePartitions() throws { + let partitionValueA = name + let partitionValueB = "\(name)bar" + let partitionValueC = "\(name)baz" - if self.isParent { - checkCount(expected: 0, realmA, SwiftPerson.self) - checkCount(expected: 0, realmB, SwiftPerson.self) - checkCount(expected: 0, realmC, SwiftPerson.self) - executeChild() + let user1 = createUser() - waitForDownloads(for: realmA) - waitForDownloads(for: realmB) - waitForDownloads(for: realmC) + let realmA = try openRealm(user: user1, partitionValue: partitionValueA) + let realmB = try openRealm(user: user1, partitionValue: partitionValueB) + let realmC = try openRealm(user: user1, partitionValue: partitionValueC) + checkCount(expected: 0, realmA, SwiftPerson.self) + checkCount(expected: 0, realmB, SwiftPerson.self) + checkCount(expected: 0, realmC, SwiftPerson.self) - checkCount(expected: 3, realmA, SwiftPerson.self) - checkCount(expected: 2, realmB, SwiftPerson.self) - checkCount(expected: 5, realmC, SwiftPerson.self) + try autoreleasepool { + let user2 = createUser() - XCTAssertEqual(realmA.objects(SwiftPerson.self).filter("firstName == %@", "Ringo").count, 1) - XCTAssertEqual(realmB.objects(SwiftPerson.self).filter("firstName == %@", "Ringo").count, 0) - } else { - // Add objects. + let realmA = try openRealm(user: user2, partitionValue: partitionValueA) try realmA.write { realmA.add(SwiftPerson(firstName: "Ringo", lastName: "Starr")) realmA.add(SwiftPerson(firstName: "John", lastName: "Lennon")) realmA.add(SwiftPerson(firstName: "Paul", lastName: "McCartney")) } + + let realmB = try openRealm(user: user2, partitionValue: partitionValueB) try realmB.write { realmB.add(SwiftPerson(firstName: "John", lastName: "Lennon")) realmB.add(SwiftPerson(firstName: "Paul", lastName: "McCartney")) } + + let realmC = try openRealm(user: user2, partitionValue: partitionValueC) try realmC.write { realmC.add(SwiftPerson(firstName: "Ringo", lastName: "Starr")) realmC.add(SwiftPerson(firstName: "John", lastName: "Lennon")) @@ -265,16 +226,22 @@ class SwiftObjectServerTests: SwiftSyncTestCase { waitForUploads(for: realmA) waitForUploads(for: realmB) waitForUploads(for: realmC) - - checkCount(expected: 3, realmA, SwiftPerson.self) - checkCount(expected: 2, realmB, SwiftPerson.self) - checkCount(expected: 5, realmC, SwiftPerson.self) } + + waitForDownloads(for: realmA) + waitForDownloads(for: realmB) + waitForDownloads(for: realmC) + + checkCount(expected: 3, realmA, SwiftPerson.self) + checkCount(expected: 2, realmB, SwiftPerson.self) + checkCount(expected: 5, realmC, SwiftPerson.self) + + XCTAssertEqual(realmA.objects(SwiftPerson.self).filter("firstName == %@", "Ringo").count, 1) + XCTAssertEqual(realmB.objects(SwiftPerson.self).filter("firstName == %@", "Ringo").count, 0) } func testConnectionState() throws { - let user = try logInUser(for: basicCredentials()) - let realm = try immediatelyOpenRealm(partitionValue: #function, user: user) + let realm = try openRealm(wait: false) let session = realm.syncSession! func wait(forState desiredState: SyncSession.ConnectionState) { @@ -298,3225 +265,1062 @@ class SwiftObjectServerTests: SwiftSyncTestCase { wait(forState: .connected) } - // MARK: - Client reset - - func waitForSyncDisabled(flexibleSync: Bool = false, appServerId: String, syncServiceId: String) { - XCTAssertTrue(try RealmServer.shared.isSyncEnabled(flexibleSync: flexibleSync, appServerId: appServerId, syncServiceId: syncServiceId)) - _ = expectSuccess(RealmServer.shared.disableSync( - flexibleSync: flexibleSync, appServerId: appServerId, syncServiceId: syncServiceId)) - XCTAssertFalse(try RealmServer.shared.isSyncEnabled(appServerId: appServerId, syncServiceId: syncServiceId)) - } - - func waitForSyncEnabled(flexibleSync: Bool = false, appServerId: String, syncServiceId: String, syncServiceConfig: [String: Any]) { - while true { - do { - _ = try RealmServer.shared.enableSync( - flexibleSync: flexibleSync, appServerId: appServerId, - syncServiceId: syncServiceId, syncServiceConfiguration: syncServiceConfig).get() - break - } catch { - // "cannot transition sync service state to \"enabled\" while sync is being terminated. Please try again in a few minutes after sync termination has completed" - guard error.localizedDescription.contains("Please try again in a few minutes") else { - XCTFail("\(error))") - return + // MARK: - Progress notifiers + @MainActor + func testStreamingDownloadNotifier() throws { + let realm = try openRealm(wait: false) + let session = try XCTUnwrap(realm.syncSession) + var ex = expectation(description: "first download") + var minimumDownloadSize = 1000000 + var callCount = 0 + var progress: SyncSession.Progress? + let token = session.addProgressNotification(for: .download, mode: .reportIndefinitely) { p in + DispatchQueue.main.async { @MainActor in + // Verify that progress doesn't decrease, but sometimes it won't + // have increased since the last call + if let progress = progress { + XCTAssertGreaterThanOrEqual(p.transferredBytes, progress.transferredBytes) + XCTAssertGreaterThanOrEqual(p.transferrableBytes, progress.transferrableBytes) + if p.transferredBytes == progress.transferredBytes && p.transferrableBytes == progress.transferrableBytes { + return + } + } + progress = p + callCount += 1 + if p.transferredBytes > minimumDownloadSize && p.isTransferComplete { + ex.fulfill() } - print("waiting for sync to terminate...") - sleep(1) } } - XCTAssertTrue(try RealmServer.shared.isSyncEnabled(flexibleSync: flexibleSync, appServerId: appServerId, syncServiceId: syncServiceId)) - } - - func waitForDevModeEnabled(appServerId: String, syncServiceId: String, syncServiceConfig: [String: Any]) throws { - let devModeEnabled = try RealmServer.shared.isDevModeEnabled(appServerId: appServerId, syncServiceId: syncServiceId) - if !devModeEnabled { - _ = expectSuccess(RealmServer.shared.enableDevMode( - appServerId: appServerId, syncServiceId: syncServiceId, - syncServiceConfiguration: syncServiceConfig)) - } - XCTAssertTrue(try RealmServer.shared.isDevModeEnabled(appServerId: appServerId, syncServiceId: syncServiceId)) - } - - // Uses admin API to toggle recovery mode on the baas server - func waitForEditRecoveryMode(flexibleSync: Bool = false, appId: String, disable: Bool) throws { - // Retrieve server IDs - let appServerId = try RealmServer.shared.retrieveAppServerId(appId) - let syncServiceId = try RealmServer.shared.retrieveSyncServiceId(appServerId: appServerId) - guard let syncServiceConfig = try RealmServer.shared.getSyncServiceConfiguration(appServerId: appServerId, syncServiceId: syncServiceId) else { fatalError("precondition failure: no sync service configuration found") } - - _ = expectSuccess(RealmServer.shared.patchRecoveryMode( - flexibleSync: flexibleSync, disable: disable, appServerId, - syncServiceId, syncServiceConfig)) - } - - // This function disables sync, executes a block while the sync service is disabled, then re-enables the sync service and dev mode. - func executeBlockOffline(flexibleSync: Bool = false, appId: String, block: () throws -> Void) throws { - let appServerId = try RealmServer.shared.retrieveAppServerId(appId) - let syncServiceId = try RealmServer.shared.retrieveSyncServiceId(appServerId: appServerId) - guard let syncServiceConfig = try RealmServer.shared.getSyncServiceConfiguration(appServerId: appServerId, syncServiceId: syncServiceId) else { fatalError("precondition failure: no sync service configuration found") } - - waitForSyncDisabled(flexibleSync: flexibleSync, appServerId: appServerId, syncServiceId: syncServiceId) - - try autoreleasepool(invoking: block) + XCTAssertNotNil(token) - waitForSyncEnabled(flexibleSync: flexibleSync, appServerId: appServerId, syncServiceId: syncServiceId, syncServiceConfig: syncServiceConfig) - try waitForDevModeEnabled(appServerId: appServerId, syncServiceId: syncServiceId, syncServiceConfig: syncServiceConfig) - } + try populateRealm() + waitForExpectations(timeout: 60.0, handler: nil) - func expectSyncError(_ fn: () -> Void) -> SyncError? { - let error = Locked(SyncError?.none) - let ex = expectation(description: "Waiting for error handler to be called...") - app.syncManager.errorHandler = { @Sendable (e, _) in - if let e = e as? SyncError { - error.value = e - } else { - XCTFail("Error \(e) was not a sync error. Something is wrong.") - } - ex.fulfill() - } + XCTAssertGreaterThanOrEqual(callCount, 1) + let p1 = try XCTUnwrap(progress) + XCTAssertEqual(p1.transferredBytes, p1.transferrableBytes) + let initialCallCount = callCount + minimumDownloadSize = p1.transferredBytes + 1000000 - fn() + // Run a second time to upload more data and verify that the callback continues to be called + ex = expectation(description: "second download") + try populateRealm() + waitForExpectations(timeout: 60.0, handler: nil) + XCTAssertGreaterThanOrEqual(callCount, initialCallCount) + let p2 = try XCTUnwrap(progress) + XCTAssertEqual(p2.transferredBytes, p2.transferrableBytes) - waitForExpectations(timeout: 10, handler: nil) - XCTAssertNotNil(error.value) - return error.value + token!.invalidate() } - func testClientReset() throws { - let user = try logInUser(for: basicCredentials()) - let realm = try openRealm(partitionValue: #function, user: user, clientResetMode: .manual()) - - let e = expectSyncError { - user.simulateClientResetError(forSession: #function) - } - let error = try XCTUnwrap(e) - XCTAssertEqual(error.code, .clientResetError) - let resetInfo = try XCTUnwrap(error.clientResetInfo()) - XCTAssertTrue(resetInfo.0.contains("mongodb-realm/\(self.appId)/recovered-realms/recovered_realm")) - - realm.invalidate() // ensure realm is kept alive to here - } + @MainActor + func testStreamingUploadNotifier() throws { + let realm = try openRealm(wait: false) + let session = try XCTUnwrap(realm.syncSession) - func testClientResetManualInitiation() throws { - let user = try logInUser(for: basicCredentials()) + var ex = expectation(description: "initial upload") + var progress: SyncSession.Progress? - let e: SyncError? = try autoreleasepool { - let realm = try openRealm(partitionValue: #function, user: user, clientResetMode: .manual()) - return expectSyncError { - user.simulateClientResetError(forSession: #function) - realm.invalidate() + let token = session.addProgressNotification(for: .upload, mode: .reportIndefinitely) { p in + DispatchQueue.main.async { @MainActor in + if let progress = progress { + XCTAssertGreaterThanOrEqual(p.transferredBytes, progress.transferredBytes) + XCTAssertGreaterThanOrEqual(p.transferrableBytes, progress.transferrableBytes) + // The sync client sometimes sends spurious notifications + // where nothing has changed, and we should just ignore those + if p.transferredBytes == progress.transferredBytes && p.transferrableBytes == progress.transferrableBytes { + return + } + } + progress = p + if p.transferredBytes > 100 && p.isTransferComplete { + ex.fulfill() + } } } - let error = try XCTUnwrap(e) - let (path, errorToken) = error.clientResetInfo()! - XCTAssertFalse(FileManager.default.fileExists(atPath: path)) - SyncSession.immediatelyHandleError(errorToken, syncManager: self.app.syncManager) - XCTAssertTrue(FileManager.default.fileExists(atPath: path)) - } - - // After restarting sync, the sync history translator service needs time - // to resynthesize the new history from existing objects on the server. - // This method creates a new document on the server and then waits for it to - // be synchronized to a newly created Realm to confirm everything is up-to-date. - func waitForServerHistoryAfterRestart(config: Realm.Configuration, collection: MongoCollection) { - XCTAssertFalse(FileManager.default.fileExists(atPath: config.fileURL!.path)) - let realm = Realm.asyncOpen(configuration: config).await(self) - XCTAssertTrue(realm.isEmpty) - - // Create an object on the server which should be present after client reset - removeAllFromCollection(collection) - let serverObject: Document = [ - "_id": .objectId(ObjectId.generate()), - "firstName": .string("Paul"), - "lastName": .string("M"), - "age": .int32(30), - "realm_id": config.syncConfiguration?.partitionValue - ] - collection.insertOne(serverObject).await(self, timeout: 30.0) - XCTAssertEqual(collection.count(filter: [:]).await(self), 1) - - // Wait for the document to be processed by the translator - let start = Date() - while realm.isEmpty && start.timeIntervalSinceNow > -60.0 { - self.waitForDownloads(for: realm) - sleep(1) // Wait between requests - } - if realm.objects(SwiftPerson.self).count > 0 { - XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) - XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "Paul") - } else { - XCTFail("Waited longer than one minute for history to resynthesize") - return - } - } - - func prepareClientReset(_ partition: String, _ user: User, appId: String? = nil) throws { - try autoreleasepool { - // Initialize the local file so that we have conflicting history - var configuration = user.configuration(partitionValue: partition) - configuration.objectTypes = [SwiftPerson.self] - let realm = try Realm(configuration: configuration) - waitForUploads(for: realm) - realm.syncSession!.suspend() - try RealmServer.shared.triggerClientReset(appId ?? self.appId, realm) + XCTAssertNotNil(token) + waitForExpectations(timeout: 10.0, handler: nil) - // Add an object to the local realm that won't be synced due to the suspend + for i in 0..<5 { + ex = expectation(description: "write transaction upload \(i)") try realm.write { - realm.add(SwiftPerson(firstName: "John", lastName: "L")) + for _ in 0.. (User, String) { - let appId = try RealmServer.shared.createAppWithQueryableFields(["age"]) - let app = app(withId: appId) - let user = try logInUser(for: basicCredentials(app: app), app: app) - let collection = try setupMongoCollection(user: user, for: SwiftPerson.self) - - if disableRecoveryMode { - // Disable recovery mode on the server. - // This attempts to simulate a case where recovery mode fails when - // using RecoverOrDiscardLocal - try waitForEditRecoveryMode(flexibleSync: true, appId: appId, disable: true) + func testStreamingNotifierInvalidate() throws { + let realm = try openRealm() + let session = try XCTUnwrap(realm.syncSession) + let downloadCount = Locked(0) + let uploadCount = Locked(0) + let tokenDownload = session.addProgressNotification(for: .download, mode: .reportIndefinitely) { _ in + downloadCount.wrappedValue += 1 + } + let tokenUpload = session.addProgressNotification(for: .upload, mode: .reportIndefinitely) { _ in + uploadCount.wrappedValue += 1 } - // Initialize the local file so that we have conflicting history - try autoreleasepool { - var configuration = user.flexibleSyncConfiguration() - configuration.objectTypes = [SwiftPerson.self] - let realm = try Realm(configuration: configuration) - let subscriptions = realm.subscriptions - updateAllPeopleSubscription(subscriptions) + try populateRealm() + waitForDownloads(for: realm) + try realm.write { + realm.add(SwiftHugeSyncObject.create()) } + waitForUploads(for: realm) - // Sync is disabled, block executed, sync re-enabled - try executeBlockOffline(flexibleSync: true, appId: appId) { - var configuration = user.flexibleSyncConfiguration() - configuration.objectTypes = [SwiftPerson.self] - let realm = try Realm(configuration: configuration) - let syncSession = realm.syncSession! - syncSession.suspend() + tokenDownload!.invalidate() + tokenUpload!.invalidate() + RLMSyncSession.notificationsQueue().sync { } - try realm.write { - // Add an object to the local realm that will not be in the server realm (because sync is disabled). - realm.add(SwiftPerson(firstName: "John", lastName: "L")) - } - XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) - } + XCTAssertGreaterThan(downloadCount.wrappedValue, 1) + XCTAssertGreaterThan(uploadCount.wrappedValue, 1) + + // There's inherently a race condition here: notification callbacks can + // be called up to one more time after they're invalidated if the sync + // worker thread is in the middle of processing a change at the time + // that the invalidation is requested, and there's no way to wait for that. + // This whole test takes 250ms, so we don't need a very long sleep. + Thread.sleep(forTimeInterval: 0.2) - // Object created above should not have been synced - XCTAssertEqual(collection.count(filter: [:]).await(self), 0) + downloadCount.wrappedValue = 0 + uploadCount.wrappedValue = 0 - autoreleasepool { - var config = user.flexibleSyncConfiguration { subscriptions in - subscriptions.append(QuerySubscription(name: "all_people")) - } - config.fileURL = RLMTestRealmURL() - config.objectTypes = [SwiftPerson.self] - waitForServerHistoryAfterRestart(config: config, collection: collection) + try populateRealm() + waitForDownloads(for: realm) + try realm.write { + realm.add(SwiftHugeSyncObject.create()) } + waitForUploads(for: realm) - return (user, appId) + // We check that the notification block is not called after we reset the + // counters on the notifiers and call invalidated(). + XCTAssertEqual(downloadCount.wrappedValue, 0) + XCTAssertEqual(uploadCount.wrappedValue, 0) } - func assertManualClientReset(_ user: User, app: App) -> ErrorReportingBlock { - let ex = self.expectation(description: "get client reset error") - return { error, session in - guard let error = error as? SyncError else { - XCTFail("Bad error type: \(error)") - return - } - XCTAssertEqual(error.code, .clientResetError) - XCTAssertEqual(session?.state, .inactive) - XCTAssertEqual(session?.connectionState, .disconnected) - XCTAssertEqual(session?.parentUser()?.id, user.id) - guard let (resetInfo) = error.clientResetInfo() else { - XCTAssertNotNil(error.clientResetInfo()) - return - } - XCTAssertTrue(resetInfo.0.contains("mongodb-realm/\(app.appId)/recovered-realms/recovered_realm")) - SyncSession.immediatelyHandleError(resetInfo.1, syncManager: app.syncManager) - ex.fulfill() - } - } + // MARK: - Download Realm - func assertDiscardLocal() -> (@Sendable (Realm) -> Void, @Sendable (Realm, Realm) -> Void) { - let beforeCallbackEx = expectation(description: "before reset callback") - @Sendable func beforeClientReset(_ before: Realm) { - let results = before.objects(SwiftPerson.self) - XCTAssertEqual(results.count, 1) - XCTAssertEqual(results.filter("firstName == 'John'").count, 1) + func testDownloadRealm() throws { + try populateRealm() - beforeCallbackEx.fulfill() + let ex = expectation(description: "download-realm") + let config = try configuration() + let pathOnDisk = ObjectiveCSupport.convert(object: config).pathOnDisk + XCTAssertFalse(FileManager.default.fileExists(atPath: pathOnDisk)) + Realm.asyncOpen(configuration: config) { result in + switch result { + case .success(let realm): + self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) + case .failure(let error): + XCTFail("No realm on async open: \(error)") + } + ex.fulfill() } - let afterCallbackEx = expectation(description: "before reset callback") - @Sendable func afterClientReset(_ before: Realm, _ after: Realm) { - let results = before.objects(SwiftPerson.self) - XCTAssertEqual(results.count, 1) - XCTAssertEqual(results.filter("firstName == 'John'").count, 1) - - let results2 = after.objects(SwiftPerson.self) - XCTAssertEqual(results2.count, 1) - XCTAssertEqual(results2.filter("firstName == 'Paul'").count, 1) - - // Fulfill on the main thread to make it harder to hit a race - // condition where the test completes before the client reset finishes - // unwinding. This does not fully fix the problem. - DispatchQueue.main.async { - afterCallbackEx.fulfill() + func fileSize(path: String) -> Int { + if let attr = try? FileManager.default.attributesOfItem(atPath: path) { + return attr[.size] as! Int } + return 0 } - return (beforeClientReset, afterClientReset) + XCTAssertFalse(RLMHasCachedRealmForPath(pathOnDisk)) + waitForExpectations(timeout: 10.0, handler: nil) + XCTAssertGreaterThan(fileSize(path: pathOnDisk), 0) + XCTAssertFalse(RLMHasCachedRealmForPath(pathOnDisk)) } - func assertRecover() -> (@Sendable (Realm) -> Void, @Sendable (Realm, Realm) -> Void) { - let beforeCallbackEx = expectation(description: "before reset callback") - @Sendable func beforeClientReset(_ before: Realm) { - let results = before.objects(SwiftPerson.self) - XCTAssertEqual(results.count, 1) - XCTAssertEqual(results.filter("firstName == 'John'").count, 1) - beforeCallbackEx.fulfill() + func testDownloadRealmToCustomPath() throws { + try populateRealm() + + let ex = expectation(description: "download-realm") + var config = try configuration() + config.fileURL = realmURLForFile("copy") + let pathOnDisk = ObjectiveCSupport.convert(object: config).pathOnDisk + XCTAssertEqual(pathOnDisk, config.fileURL!.path) + XCTAssertFalse(FileManager.default.fileExists(atPath: pathOnDisk)) + Realm.asyncOpen(configuration: config) { result in + switch result { + case .success(let realm): + self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) + case .failure(let error): + XCTFail("No realm on async open: \(error)") + } + ex.fulfill() } - let afterCallbackEx = expectation(description: "after reset callback") - @Sendable func afterClientReset(_ before: Realm, _ after: Realm) { - let results = before.objects(SwiftPerson.self) - XCTAssertEqual(results.count, 1) - XCTAssertEqual(results.filter("firstName == 'John'").count, 1) - - let results2 = after.objects(SwiftPerson.self) - XCTAssertEqual(results2.count, 2) - XCTAssertEqual(results2.filter("firstName == 'John'").count, 1) - XCTAssertEqual(results2.filter("firstName == 'Paul'").count, 1) - - // Fulfill on the main thread to make it harder to hit a race - // condition where the test completes before the client reset finishes - // unwinding. This does not fully fix the problem. - DispatchQueue.main.async { - afterCallbackEx.fulfill() + func fileSize(path: String) -> Int { + if let attr = try? FileManager.default.attributesOfItem(atPath: path) { + return attr[.size] as! Int } + return 0 } - return (beforeClientReset, afterClientReset) + XCTAssertFalse(RLMHasCachedRealmForPath(pathOnDisk)) + waitForExpectations(timeout: 10.0, handler: nil) + XCTAssertGreaterThan(fileSize(path: pathOnDisk), 0) + XCTAssertFalse(RLMHasCachedRealmForPath(pathOnDisk)) } - func testClientResetManual() throws { - let creds = basicCredentials() - try autoreleasepool { - let user = try logInUser(for: creds) - try prepareClientReset(#function, user) - - var configuration = user.configuration(partitionValue: #function, clientResetMode: .manual()) - configuration.objectTypes = [SwiftPerson.self] - - let syncManager = self.app.syncManager - syncManager.errorHandler = assertManualClientReset(user, app: app) - - try autoreleasepool { - let realm = try Realm(configuration: configuration) - waitForExpectations(timeout: 15.0) - realm.refresh() - // The locally created object should still be present as we didn't - // actually handle the client reset - XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) - XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "John") - } - } + func testCancelDownloadRealm() throws { + try populateRealm() - let user = try logInUser(for: creds) - var configuration = user.configuration(partitionValue: #function) - configuration.objectTypes = [SwiftPerson.self] + // Use a serial queue for asyncOpen to ensure that the first one adds + // the completion block before the second one cancels it + let queue = DispatchQueue(label: "io.realm.asyncOpen") + RLMSetAsyncOpenQueue(queue) - try autoreleasepool { - let realm = try Realm(configuration: configuration) - waitForDownloads(for: realm) - // After reopening, the old Realm file should have been moved aside - // and we should now have the data from the server - XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) - XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "Paul") + let ex = expectation(description: "async open") + ex.expectedFulfillmentCount = 2 + let config = try configuration() + let completion = { (result: Result) -> Void in + guard case .failure = result else { + XCTFail("No error on cancelled async open") + return ex.fulfill() + } + ex.fulfill() } + Realm.asyncOpen(configuration: config, callback: completion) + let task = Realm.asyncOpen(configuration: config, callback: completion) + queue.sync { task.cancel() } + waitForExpectations(timeout: 10.0, handler: nil) } - func testClientResetManualWithEnumCallback() throws { - let creds = basicCredentials() - try autoreleasepool { - let user = try logInUser(for: creds) - try prepareClientReset(#function, user) - - var configuration = user.configuration(partitionValue: #function, clientResetMode: .manual(errorHandler: assertManualClientReset(user, app: app))) - configuration.objectTypes = [SwiftPerson.self] + func testAsyncOpenProgress() throws { + try populateRealm() - switch configuration.syncConfiguration!.clientResetMode { - case .manual(let block): - XCTAssertNotNil(block) - default: - XCTFail("Should be set to manual") - } + let ex1 = expectation(description: "async open") + let ex2 = expectation(description: "download progress") + let config = try configuration() + let task = Realm.asyncOpen(configuration: config) { result in + XCTAssertNotNil(try? result.get()) + ex1.fulfill() + } - try autoreleasepool { - let realm = try Realm(configuration: configuration) - waitForExpectations(timeout: 15.0) - realm.refresh() - // The locally created object should still be present as we didn't - // actually handle the client reset - XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) - XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "John") + task.addProgressNotification { progress in + if progress.isTransferComplete { + ex2.fulfill() } } - let user = try logInUser(for: creds) - var configuration = user.configuration(partitionValue: #function, clientResetMode: .manual()) - configuration.objectTypes = [SwiftPerson.self] - - try autoreleasepool { - let realm = try Realm(configuration: configuration) - waitForDownloads(for: realm) - // After reopening, the old Realm file should have been moved aside - // and we should now have the data from the server - XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) - XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "Paul") - } + waitForExpectations(timeout: 10.0, handler: nil) } - func testClientResetManualManagerFallback() throws { - let creds = basicCredentials() - try autoreleasepool { - let user = try logInUser(for: creds) - try prepareClientReset(#function, user) - - // No callback is passed into enum `.manual`, but a syncManager.errorHandler exists, - // so expect that to be used instead. - var configuration = user.configuration(partitionValue: #function, clientResetMode: .manual()) - configuration.objectTypes = [SwiftPerson.self] - - let syncManager = self.app.syncManager - syncManager.errorHandler = assertManualClientReset(user, app: app) - - try autoreleasepool { - let realm = try Realm(configuration: configuration) - waitForExpectations(timeout: 15.0) // Wait for expectations in asssertManualClientReset - // The locally created object should still be present as we didn't - // actually handle the client reset - XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) - XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "John") - } - } - - let user = try logInUser(for: creds) - var configuration = user.configuration(partitionValue: #function) - configuration.objectTypes = [SwiftPerson.self] + func config(baseURL: String, transport: RLMNetworkTransport, syncTimeouts: SyncTimeoutOptions? = nil) throws -> Realm.Configuration { + let appId = try RealmServer.shared.createApp(types: []) + let appConfig = AppConfiguration(baseURL: baseURL, transport: transport, syncTimeouts: syncTimeouts) + let app = App(id: appId, configuration: appConfig) - try autoreleasepool { - let realm = try Realm(configuration: configuration) - waitForDownloads(for: realm) - // After reopening, the old Realm file should have been moved aside - // and we should now have the data from the server - XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) - XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "Paul") - } + let user = try logInUser(for: basicCredentials(app: app), app: app) + var config = user.configuration(partitionValue: name, cancelAsyncOpenOnNonFatalErrors: true) + config.objectTypes = [] + return config } - // If the syncManager.ErrorHandler and manual enum callback - // are both set, use the enum callback. - func testClientResetManualEnumCallbackNotManager() throws { - let creds = basicCredentials() - try autoreleasepool { - let user = try logInUser(for: creds) - try prepareClientReset(#function, user) + func testAsyncOpenTimeout() throws { + let proxy = TimeoutProxyServer(port: 5678, targetPort: 9090) + try proxy.start() - var configuration = user.configuration(partitionValue: #function, clientResetMode: .manual(errorHandler: assertManualClientReset(user, app: app))) - configuration.objectTypes = [SwiftPerson.self] + let config = try config(baseURL: "http://localhost:5678", + transport: AsyncOpenConnectionTimeoutTransport(), + syncTimeouts: .init(connectTimeout: 2000, connectionLingerTime: 1)) - switch configuration.syncConfiguration!.clientResetMode { - case .manual(let block): - XCTAssertNotNil(block) - default: - XCTFail("Should be set to manual") + // Two second timeout with a one second delay should work + autoreleasepool { + proxy.delay = 1.0 + let ex = expectation(description: "async open") + Realm.asyncOpen(configuration: config) { result in + let realm = try? result.get() + XCTAssertNotNil(realm) + realm?.syncSession?.suspend() + ex.fulfill() } + waitForExpectations(timeout: 10.0, handler: nil) + } + + // The client doesn't disconnect immediately, and there isn't a good way + // to wait for it. In practice this should take more like 10ms to happen + // so a 1s sleep is plenty. + sleep(1) - let syncManager = self.app.syncManager - syncManager.errorHandler = { error, _ in - guard nil != error as? SyncError else { - XCTFail("Bad error type: \(error)") + // Two second timeout with a two second delay should fail + autoreleasepool { + proxy.delay = 3.0 + let ex = expectation(description: "async open") + Realm.asyncOpen(configuration: config) { result in + guard case .failure(let error) = result else { + XCTFail("Did not fail: \(result)") return } - XCTFail("Expected the syncManager.ErrorHandler to not be called") - } - - try autoreleasepool { - let realm = try Realm(configuration: configuration) - waitForExpectations(timeout: 15.0) - // The locally created object should still be present as we didn't - // actually handle the client reset - XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) - XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "John") + if let error = error as NSError? { + XCTAssertEqual(error.code, Int(ETIMEDOUT)) + XCTAssertEqual(error.domain, NSPOSIXErrorDomain) + } + ex.fulfill() } + waitForExpectations(timeout: 20.0, handler: nil) } - let user = try logInUser(for: creds) - var configuration = user.configuration(partitionValue: #function) - configuration.objectTypes = [SwiftPerson.self] - - try autoreleasepool { - let realm = try Realm(configuration: configuration) - waitForDownloads(for: realm) - // After reopening, the old Realm file should have been moved aside - // and we should now have the data from the server - XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) - XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "Paul") - } - } - - func testClientResetManualWithoutLiveRealmInstance() throws { - let creds = basicCredentials() - let user = try logInUser(for: creds) - try prepareClientReset(#function, user) - - var configuration = user.configuration(partitionValue: #function, clientResetMode: .manual()) - configuration.objectTypes = [SwiftPerson.self] - - let syncManager = self.app.syncManager - syncManager.errorHandler = assertManualClientReset(user, app: app) - - try autoreleasepool { - _ = try Realm(configuration: configuration) - // We have to wait for the error to arrive (or the session will just - // transition to inactive without calling the error handler), but we - // need to ensure the Realm is deallocated before the error handler - // is invoked on the main thread. - sleep(1) - } - waitForExpectations(timeout: 15.0) - syncManager.waitForSessionTermination() - resetSyncManager() + proxy.stop() } - @available(*, deprecated) // .discardLocal - func testClientResetDiscardLocal() throws { - let user = try logInUser(for: basicCredentials()) - try prepareClientReset(#function, user) - - let (assertBeforeBlock, assertAfterBlock) = assertDiscardLocal() - var configuration = user.configuration(partitionValue: #function, - clientResetMode: .discardLocal(beforeReset: assertBeforeBlock, afterReset: assertAfterBlock)) - configuration.objectTypes = [SwiftPerson.self] - - let syncConfig = try XCTUnwrap(configuration.syncConfiguration) - switch syncConfig.clientResetMode { - case .discardUnsyncedChanges(let before, let after): - XCTAssertNotNil(before) - XCTAssertNotNil(after) - default: - XCTFail("Should be set to discardLocal") + class LocationOverrideTransport: RLMNetworkTransport { + let hostname: String + let wsHostname: String + init(hostname: String = "http://localhost:9090", wsHostname: String = "ws://invalid.com:9090") { + self.hostname = hostname + self.wsHostname = wsHostname } - try autoreleasepool { - let realm = try Realm(configuration: configuration) - let results = realm.objects(SwiftPerson.self) - XCTAssertEqual(results.count, 1) - waitForExpectations(timeout: 15.0) - realm.refresh() // expectation is potentially fulfilled before autorefresh - // The Person created locally ("John") should have been discarded, - // while the one from the server ("Paul") should be present - XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) - XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "Paul") + override func sendRequest(toServer request: RLMRequest, completion: @escaping RLMNetworkTransportCompletionBlock) { + if request.url.hasSuffix("location") { + let response = RLMResponse() + response.httpStatusCode = 200 + response.body = "{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":\"\(hostname)\",\"ws_hostname\":\"\(wsHostname)\"}" + completion(response) + } else { + super.sendRequest(toServer: request, completion: completion) + } } } - func testClientResetDiscardUnsyncedChanges() throws { - let user = try logInUser(for: basicCredentials()) - try prepareClientReset(#function, user) - - let (assertBeforeBlock, assertAfterBlock) = assertDiscardLocal() - var configuration = user.configuration(partitionValue: #function, - clientResetMode: .discardUnsyncedChanges(beforeReset: assertBeforeBlock, afterReset: assertAfterBlock)) - configuration.objectTypes = [SwiftPerson.self] - - guard let syncConfig = configuration.syncConfiguration else { fatalError("Test condition failure. SyncConfiguration not set.") } - switch syncConfig.clientResetMode { - case .discardUnsyncedChanges(let before, let after): - XCTAssertNotNil(before) - XCTAssertNotNil(after) - default: - XCTFail("Should be set to discardUnsyncedChanges") - } - - try autoreleasepool { - let realm = try Realm(configuration: configuration) - waitForExpectations(timeout: 15.0) - realm.refresh() - // The Person created locally ("John") should have been discarded, - // while the one from the server ("Paul") should be present - XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) - XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "Paul") + func testDNSError() throws { + let config = try config(baseURL: "http://localhost:9090", transport: LocationOverrideTransport(wsHostname: "ws://invalid.com:9090")) + Realm.asyncOpen(configuration: config).awaitFailure(self, timeout: 40) { error in + assertSyncError(error, .connectionFailed, "Failed to connect to sync: Host not found (authoritative)") } } - @available(*, deprecated) // .discardLocal - func testClientResetDiscardLocalAsyncOpen() throws { - let user = try logInUser(for: basicCredentials()) - try prepareClientReset(#function, user) - - let (assertBeforeBlock, assertAfterBlock) = assertDiscardLocal() - var configuration = user.configuration(partitionValue: #function, clientResetMode: .discardLocal(beforeReset: assertBeforeBlock, afterReset: assertAfterBlock)) - configuration.objectTypes = [SwiftPerson.self] - - let asyncOpenEx = expectation(description: "async open") - Realm.asyncOpen(configuration: configuration) { result in - let realm = try! result.get() - XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) - XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "Paul") - asyncOpenEx.fulfill() + func testTLSError() throws { + let config = try config(baseURL: "http://localhost:9090", transport: LocationOverrideTransport(wsHostname: "wss://localhost:9090")) + Realm.asyncOpen(configuration: config).awaitFailure(self) { error in + assertSyncError(error, .tlsHandshakeFailed, "TLS handshake failed: SecureTransport error: record overflow (-9847)") } - waitForExpectations(timeout: 15.0) } - func testClientResetRecover() throws { - let user = try logInUser(for: basicCredentials()) - try prepareClientReset(#function, user) - - let (assertBeforeBlock, assertAfterBlock) = assertRecover() - var configuration = user.configuration(partitionValue: #function, clientResetMode: .recoverUnsyncedChanges(beforeReset: assertBeforeBlock, afterReset: assertAfterBlock)) - configuration.objectTypes = [SwiftPerson.self] - - let syncConfig = try XCTUnwrap(configuration.syncConfiguration) - switch syncConfig.clientResetMode { - case .recoverUnsyncedChanges(let before, let after): - XCTAssertNotNil(before) - XCTAssertNotNil(after) - default: - XCTFail("Should be set to recover") - } - try autoreleasepool { - let realm = try Realm(configuration: configuration) - waitForExpectations(timeout: 15.0) - XCTAssertEqual(realm.objects(SwiftPerson.self).count, 2) - // The object created locally (John) and the object created on the server (Paul) - // should both be integrated into the new realm file. - XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "John") - XCTAssertEqual(realm.objects(SwiftPerson.self)[1].firstName, "Paul") - } + func testAppCredentialSupport() { + XCTAssertEqual(ObjectiveCSupport.convert(object: Credentials.facebook(accessToken: "accessToken")), + RLMCredentials(facebookToken: "accessToken")) - // Wait for the recovered object to be processed by the translator as - // otherwise it'll show up in the middle of later tests - waitForCollectionCount(user.collection(for: SwiftPerson.self), 2) - } + XCTAssertEqual(ObjectiveCSupport.convert(object: Credentials.google(serverAuthCode: "serverAuthCode")), + RLMCredentials(googleAuthCode: "serverAuthCode")) - func testClientResetRecoverAsyncOpen() throws { - let user = try logInUser(for: basicCredentials()) - try prepareClientReset(#function, user) - - let (assertBeforeBlock, assertAfterBlock) = assertRecover() - var configuration = user.configuration(partitionValue: #function, clientResetMode: .recoverUnsyncedChanges(beforeReset: assertBeforeBlock, afterReset: assertAfterBlock)) - configuration.objectTypes = [SwiftPerson.self] - - let syncConfig = try XCTUnwrap(configuration.syncConfiguration) - switch syncConfig.clientResetMode { - case .recoverUnsyncedChanges(let before, let after): - XCTAssertNotNil(before) - XCTAssertNotNil(after) - default: - XCTFail("Should be set to recover") - } - autoreleasepool { - let realm = Realm.asyncOpen(configuration: configuration).await(self) - XCTAssertEqual(realm.objects(SwiftPerson.self).count, 2) - // The object created locally (John) and the object created on the server (Paul) - // should both be integrated into the new realm file. - XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "John") - XCTAssertEqual(realm.objects(SwiftPerson.self)[1].firstName, "Paul") - waitForExpectations(timeout: 15.0) - } + XCTAssertEqual(ObjectiveCSupport.convert(object: Credentials.apple(idToken: "idToken")), + RLMCredentials(appleToken: "idToken")) - // Wait for the recovered object to be processed by the translator as - // otherwise it'll show up in the middle of later tests - waitForCollectionCount(user.collection(for: SwiftPerson.self), 2) - } + XCTAssertEqual(ObjectiveCSupport.convert(object: Credentials.emailPassword(email: "email", password: "password")), + RLMCredentials(email: "email", password: "password")) - func testClientResetRecoverWithSchemaChanges() throws { - let user = try logInUser(for: basicCredentials()) - try prepareClientReset(#function, user) - - let beforeCallbackEx = expectation(description: "before reset callback") - @Sendable func beforeClientReset(_ before: Realm) { - let person = before.objects(SwiftPersonWithAdditionalProperty.self).first! - XCTAssertEqual(person.objectSchema.properties.map(\.name), - ["_id", "firstName", "lastName", "age", "newProperty"]) - XCTAssertEqual(person.newProperty, 0) - beforeCallbackEx.fulfill() - } - let afterCallbackEx = expectation(description: "after reset callback") - @Sendable func afterClientReset(_ before: Realm, _ after: Realm) { - let beforePerson = before.objects(SwiftPersonWithAdditionalProperty.self).first! - XCTAssertEqual(beforePerson.objectSchema.properties.map(\.name), - ["_id", "firstName", "lastName", "age", "newProperty"]) - XCTAssertEqual(beforePerson.newProperty, 0) - let afterPerson = after.objects(SwiftPersonWithAdditionalProperty.self).first! - XCTAssertEqual(afterPerson.objectSchema.properties.map(\.name), - ["_id", "firstName", "lastName", "age", "newProperty"]) - XCTAssertEqual(afterPerson.newProperty, 0) - - // Fulfill on the main thread to make it harder to hit a race - // condition where the test completes before the client reset finishes - // unwinding. This does not fully fix the problem. - DispatchQueue.main.async { - afterCallbackEx.fulfill() - } - } + XCTAssertEqual(ObjectiveCSupport.convert(object: Credentials.jwt(token: "token")), + RLMCredentials(jwt: "token")) - var configuration = user.configuration(partitionValue: #function, clientResetMode: .recoverUnsyncedChanges(beforeReset: beforeClientReset, afterReset: afterClientReset)) - configuration.objectTypes = [SwiftPersonWithAdditionalProperty.self] + XCTAssertEqual(ObjectiveCSupport.convert(object: Credentials.function(payload: ["dog": ["name": "fido"]])), + RLMCredentials(functionPayload: ["dog": ["name" as NSString: "fido" as NSString] as NSDictionary])) - autoreleasepool { - _ = Realm.asyncOpen(configuration: configuration).await(self) - waitForExpectations(timeout: 15.0) - } + XCTAssertEqual(ObjectiveCSupport.convert(object: Credentials.userAPIKey("key")), + RLMCredentials(userAPIKey: "key")) - // Wait for the recovered object to be processed by the translator as - // otherwise it'll show up in the middle of later tests - waitForCollectionCount(user.collection(for: SwiftPerson.self), 2) + XCTAssertEqual(ObjectiveCSupport.convert(object: Credentials.serverAPIKey("key")), + RLMCredentials(serverAPIKey: "key")) + XCTAssertEqual(ObjectiveCSupport.convert(object: Credentials.anonymous), + RLMCredentials.anonymous()) } - func testClientResetRecoverOrDiscardLocalFailedRecovery() throws { - let appId = try RealmServer.shared.createApp() - // Disable recovery mode on the server. - // This attempts to simulate a case where recovery mode fails when - // using RecoverOrDiscardLocal - try waitForEditRecoveryMode(appId: appId, disable: true) - - let app = app(withId: appId) - let user = try logInUser(for: basicCredentials(app: app), app: app) - try prepareClientReset(#function, user, appId: appId) - - // Expect the recovery to fail back to discardLocal logic - let (assertBeforeBlock, assertAfterBlock) = assertDiscardLocal() - var configuration = user.configuration(partitionValue: #function, clientResetMode: .recoverOrDiscardUnsyncedChanges(beforeReset: assertBeforeBlock, afterReset: assertAfterBlock)) - configuration.objectTypes = [SwiftPerson.self] - - let syncConfig = try XCTUnwrap(configuration.syncConfiguration) - switch syncConfig.clientResetMode { - case .recoverOrDiscardUnsyncedChanges(let before, let after): - XCTAssertNotNil(before) - XCTAssertNotNil(after) - default: - XCTFail("Should be set to recoverOrDiscard") - } - - // Expect the recovery to fail back to discardLocal logic - try autoreleasepool { - let realm = try Realm(configuration: configuration) - waitForExpectations(timeout: 15.0) - realm.refresh() - XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) - // The Person created locally ("John") should have been discarded, - // while the one from the server ("Paul") should be present. - XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "Paul") - } + // MARK: - Authentication - try RealmServer.shared.deleteApp(appId) - } + func testInvalidCredentials() throws { + let email = "testInvalidCredentialsEmail" + let credentials = basicCredentials() + let user = try logInUser(for: credentials) + XCTAssertEqual(user.state, .loggedIn) - @available(*, deprecated) // .discardLocal - func testFlexibleSyncDiscardLocalClientReset() throws { - let (user, appId) = try prepareFlexibleClientReset() + let credentials2 = Credentials.emailPassword(email: email, password: "NOT_A_VALID_PASSWORD") + let ex = expectation(description: "Should fail to log in the user") - let (assertBeforeBlock, assertAfterBlock) = assertDiscardLocal() - var config = user.flexibleSyncConfiguration(clientResetMode: .discardLocal(beforeReset: assertBeforeBlock, afterReset: assertAfterBlock)) - config.objectTypes = [SwiftPerson.self] - let syncConfig = try XCTUnwrap(config.syncConfiguration) - switch syncConfig.clientResetMode { - case .discardUnsyncedChanges(let before, let after): - XCTAssertNotNil(before) - XCTAssertNotNil(after) - default: - XCTFail("Should be set to discardUnsyncedChanges") + self.app.login(credentials: credentials2) { result in + guard case .failure = result else { + XCTFail("Login should not have been successful") + return ex.fulfill() + } + ex.fulfill() } - try autoreleasepool { - XCTAssertEqual(user.flexibleSyncConfiguration().fileURL, config.fileURL) - let realm = try Realm(configuration: config) - let subscriptions = realm.subscriptions - XCTAssertEqual(subscriptions.count, 1) // subscription created during prepareFlexibleSyncClientReset - XCTAssertEqual(subscriptions.first?.name, "all_people") - - waitForExpectations(timeout: 15.0) - realm.refresh() - XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) - XCTAssertEqual(realm.objects(SwiftPerson.self).first?.firstName, "Paul") - } + waitForExpectations(timeout: 10, handler: nil) + } - try RealmServer.shared.deleteApp(appId) + func testCustomTokenAuthentication() { + let user = logInUser(for: jwtCredential(withAppId: appId)) + XCTAssertEqual(user.profile.metadata["anotherName"], "Bar Foo") + XCTAssertEqual(user.profile.metadata["name"], "Foo Bar") + XCTAssertEqual(user.profile.metadata["occupation"], "firefighter") } - func testFlexibleSyncDiscardUnsyncedChangesClientReset() throws { - let (user, appId) = try prepareFlexibleClientReset() + // MARK: - User-specific functionality - let (assertBeforeBlock, assertAfterBlock) = assertDiscardLocal() - var config = user.flexibleSyncConfiguration(clientResetMode: .discardUnsyncedChanges(beforeReset: assertBeforeBlock, afterReset: assertAfterBlock)) - config.objectTypes = [SwiftPerson.self] - let syncConfig = try XCTUnwrap(config.syncConfiguration) - switch syncConfig.clientResetMode { - case .discardUnsyncedChanges(let before, let after): - XCTAssertNotNil(before) - XCTAssertNotNil(after) - default: - XCTFail("Should be set to discardUnsyncedChanges") - } + func testUserExpirationCallback() throws { + let user = createUser() - try autoreleasepool { - XCTAssertEqual(user.flexibleSyncConfiguration().fileURL, config.fileURL) - let realm = try Realm(configuration: config) - let subscriptions = realm.subscriptions - XCTAssertEqual(subscriptions.count, 1) // subscription created during prepareFlexibleSyncClientReset - XCTAssertEqual(subscriptions.first?.name, "all_people") - - waitForExpectations(timeout: 15.0) - realm.refresh() - XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) - XCTAssertEqual(realm.objects(SwiftPerson.self).first?.firstName, "Paul") + // Set a callback on the user + let blockCalled = Locked(false) + let ex = expectation(description: "Error callback should fire upon receiving an error") + app.syncManager.errorHandler = { @Sendable (error, _) in + assertSyncError(error, .clientUserError, "Unable to refresh the user access token: signature is invalid") + blockCalled.value = true + ex.fulfill() } - try RealmServer.shared.deleteApp(appId) - } - - func testFlexibleSyncClientResetRecover() throws { - let (user, appId) = try prepareFlexibleClientReset() - - let (assertBeforeBlock, assertAfterBlock) = assertRecover() - var config = user.flexibleSyncConfiguration(clientResetMode: .recoverUnsyncedChanges(beforeReset: assertBeforeBlock, afterReset: assertAfterBlock)) + // Screw up the token on the user. + setInvalidTokensFor(user) + // Try to open a Realm with the user; this will cause our errorHandler block defined above to be fired. + XCTAssertFalse(blockCalled.value) + var config = user.configuration(partitionValue: name) config.objectTypes = [SwiftPerson.self] - let syncConfig = try XCTUnwrap(config.syncConfiguration) - switch syncConfig.clientResetMode { - case .recoverUnsyncedChanges(let before, let after): - XCTAssertNotNil(before) - XCTAssertNotNil(after) - default: - XCTFail("Should be set to recover") - } - - try autoreleasepool { - XCTAssertEqual(user.flexibleSyncConfiguration().fileURL, config.fileURL) - let realm = try Realm(configuration: config) - let subscriptions = realm.subscriptions - XCTAssertEqual(subscriptions.count, 1) // subscription created during prepareFlexibleSyncClientReset - XCTAssertEqual(subscriptions.first?.name, "all_people") - - waitForExpectations(timeout: 15.0) // wait for expectations in assertRecover - realm.refresh() - XCTAssertEqual(realm.objects(SwiftPerson.self).count, 2) - // The object created locally (John) and the object created on the server (Paul) - // should both be integrated into the new realm file. - XCTAssertEqual(realm.objects(SwiftPerson.self).filter("firstName == 'John'").count, 1) - XCTAssertEqual(realm.objects(SwiftPerson.self).filter("firstName == 'Paul'").count, 1) - } + _ = try Realm(configuration: config) - try RealmServer.shared.deleteApp(appId) + waitForExpectations(timeout: 10.0, handler: nil) } - func testFlexibleSyncClientResetRecoverWithInitialSubscriptions() throws { - let (user, appId) = try prepareFlexibleClientReset() - - let (assertBeforeBlock, assertAfterBlock) = assertRecover() - var config = user.flexibleSyncConfiguration(clientResetMode: .recoverUnsyncedChanges(beforeReset: assertBeforeBlock, afterReset: assertAfterBlock), - initialSubscriptions: { subscriptions in - subscriptions.append(QuerySubscription(name: "all_people")) - }) - config.objectTypes = [SwiftPerson.self] - let syncConfig = try XCTUnwrap(config.syncConfiguration) - switch syncConfig.clientResetMode { - case .recoverUnsyncedChanges(let before, let after): - XCTAssertNotNil(before) - XCTAssertNotNil(after) - default: - XCTFail("Should be set to recover") - } + private func realmURLForFile(_ fileName: String) -> URL { + let testDir = RLMRealmPathForFile("mongodb-realm") + let directory = URL(fileURLWithPath: testDir, isDirectory: true) + return directory.appendingPathComponent(fileName, isDirectory: false) + } - try autoreleasepool { - XCTAssertEqual(user.flexibleSyncConfiguration().fileURL, config.fileURL) - let realm = try Realm(configuration: config) - let subscriptions = realm.subscriptions - XCTAssertEqual(subscriptions.count, 1) - XCTAssertEqual(subscriptions.first?.name, "all_people") - - waitForExpectations(timeout: 15.0) - realm.refresh() - XCTAssertEqual(realm.objects(SwiftPerson.self).count, 2) - // The object created locally (John) and the object created on the server (Paul) - // should both be integrated into the new realm file. - XCTAssertEqual(realm.objects(SwiftPerson.self).filter("firstName == 'John'").count, 1) - XCTAssertEqual(realm.objects(SwiftPerson.self).filter("firstName == 'Paul'").count, 1) - } + // MARK: - App tests - try RealmServer.shared.deleteApp(appId) + private func appConfig() -> AppConfiguration { + return AppConfiguration(baseURL: "http://localhost:9090") } - @available(*, deprecated) // .discardLocal - func testFlexibleSyncClientResetDiscardLocalWithInitialSubscriptions() throws { - let (user, appId) = try prepareFlexibleClientReset() - - let (assertBeforeBlock, assertAfterBlock) = assertDiscardLocal() - var config = user.flexibleSyncConfiguration(clientResetMode: .discardLocal(beforeReset: assertBeforeBlock, afterReset: assertAfterBlock), - initialSubscriptions: { subscriptions in - subscriptions.append(QuerySubscription(name: "all_people")) - }) - config.objectTypes = [SwiftPerson.self] - let syncConfig = try XCTUnwrap(config.syncConfiguration) - switch syncConfig.clientResetMode { - case .discardUnsyncedChanges(let before, let after): - XCTAssertNotNil(before) - XCTAssertNotNil(after) - default: - XCTFail("Should be set to discardUnsyncedChanges") - } + func testAppInit() { + let appName = "translate-utwuv" - try autoreleasepool { - XCTAssertEqual(user.flexibleSyncConfiguration().fileURL, config.fileURL) - let realm = try Realm(configuration: config) - let subscriptions = realm.subscriptions - XCTAssertEqual(subscriptions.count, 1) - XCTAssertEqual(subscriptions.first?.name, "all_people") - - waitForExpectations(timeout: 15.0) - realm.refresh() - XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) - // The Person created locally ("John") should have been discarded, - // while the one from the server ("Paul") should be present - XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) - XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "Paul") - } + let appWithNoConfig = App(id: appName) + XCTAssertEqual(appWithNoConfig.allUsers.count, 0) - try RealmServer.shared.deleteApp(appId) + let appWithConfig = App(id: appName, configuration: appConfig()) + XCTAssertEqual(appWithConfig.allUsers.count, 0) } - func testFlexibleSyncClientResetRecoverOrDiscardLocalFailedRecovery() throws { - let (user, appId) = try prepareFlexibleClientReset(disableRecoveryMode: true) - - // Expect the client reset process to discard the local changes - let (assertBeforeBlock, assertAfterBlock) = assertDiscardLocal() - var config = user.flexibleSyncConfiguration(clientResetMode: .recoverOrDiscardUnsyncedChanges(beforeReset: assertBeforeBlock, afterReset: assertAfterBlock)) - config.objectTypes = [SwiftPerson.self] - guard let syncConfig = config.syncConfiguration else { - fatalError("Test condition failure. SyncConfiguration not set.") - } - switch syncConfig.clientResetMode { - case .recoverOrDiscardUnsyncedChanges(let before, let after): - XCTAssertNotNil(before) - XCTAssertNotNil(after) - default: - XCTFail("Should be set to recoverOrDiscard") - } + func testAppLogin() { + let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" + let password = randomString(10) - try autoreleasepool { - XCTAssertEqual(user.flexibleSyncConfiguration().fileURL, config.fileURL) - let realm = try Realm(configuration: config) - let subscriptions = realm.subscriptions - XCTAssertEqual(subscriptions.count, 1) // subscription created during prepareFlexibleSyncClientReset - XCTAssertEqual(subscriptions.first?.name, "all_people") - - waitForExpectations(timeout: 15.0) - realm.refresh() - XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) - // The Person created locally ("John") should have been discarded, - // while the one from the server ("Paul") should be present. - XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "Paul") - } + app.emailPasswordAuth.registerUser(email: email, password: password).await(self) + let syncUser = app.login(credentials: Credentials.emailPassword(email: email, password: password)).await(self) - try RealmServer.shared.deleteApp(appId) + XCTAssertEqual(syncUser.id, app.currentUser?.id) + XCTAssertEqual(app.allUsers.count, 1) } - func testFlexibleClientResetManual() throws { - let (user, appId) = try prepareFlexibleClientReset() - try autoreleasepool { - var config = user.flexibleSyncConfiguration(clientResetMode: .manual(errorHandler: assertManualClientReset(user, app: App(id: appId)))) - config.objectTypes = [SwiftPerson.self] + func testAppSwitchAndRemove() { + let email1 = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" + let password1 = randomString(10) + let email2 = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" + let password2 = randomString(10) - switch config.syncConfiguration!.clientResetMode { - case .manual(let block): - XCTAssertNotNil(block) - default: - XCTFail("Should be set to manual") - } - try autoreleasepool { - let realm = try Realm(configuration: config) - waitForExpectations(timeout: 15.0) - // The locally created object should still be present as we didn't - // actually handle the client reset - XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) - XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "John") - } - } - - var config = user.flexibleSyncConfiguration(clientResetMode: .manual()) - config.objectTypes = [SwiftPerson.self] - - try autoreleasepool { - let realm = try Realm(configuration: config) - let subscriptions = realm.subscriptions - updateAllPeopleSubscription(subscriptions) - XCTAssertEqual(subscriptions.count, 1) - waitForDownloads(for: realm) - - // After reopening, the old Realm file should have been moved aside - // and we should now have the data from the server - XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) - XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "Paul") - } - - try RealmServer.shared.deleteApp(appId) - } - - func testDefaultClientResetMode() throws { - let user = try logInUser(for: basicCredentials(app: self.flexibleSyncApp), app: self.flexibleSyncApp) - let fConfig = user.flexibleSyncConfiguration() - let pConfig = user.configuration(partitionValue: #function) - - switch fConfig.syncConfiguration!.clientResetMode { - case .recoverUnsyncedChanges: - return - default: - XCTFail("expected recover mode") - } - switch pConfig.syncConfiguration!.clientResetMode { - case .recoverUnsyncedChanges: - return - default: - XCTFail("expected recover mode") - } - } - - // MARK: - Progress notifiers - @MainActor - func testStreamingDownloadNotifier() throws { - let user = try logInUser(for: basicCredentials()) - if !isParent { - return try populateRealm(user: user, partitionValue: #function) - } - - let realm = try immediatelyOpenRealm(partitionValue: #function, user: user) - let session = try XCTUnwrap(realm.syncSession) - var ex = expectation(description: "first download") - var minimumDownloadSize = 1000000 - var callCount = 0 - var progress: SyncSession.Progress? - let token = session.addProgressNotification(for: .download, mode: .reportIndefinitely) { p in - DispatchQueue.main.async { @MainActor in - // Verify that progress doesn't decrease, but sometimes it won't - // have increased since the last call - if let progress = progress { - XCTAssertGreaterThanOrEqual(p.transferredBytes, progress.transferredBytes) - XCTAssertGreaterThanOrEqual(p.transferrableBytes, progress.transferrableBytes) - if p.transferredBytes == progress.transferredBytes && p.transferrableBytes == progress.transferrableBytes { - return - } - } - progress = p - callCount += 1 - if p.transferredBytes > minimumDownloadSize && p.isTransferComplete { - ex.fulfill() - } - } - } - XCTAssertNotNil(token) - - // Wait for the child process to upload all the data. - executeChild() - waitForExpectations(timeout: 60.0, handler: nil) - XCTAssertGreaterThanOrEqual(callCount, 1) - let p1 = try XCTUnwrap(progress) - XCTAssertEqual(p1.transferredBytes, p1.transferrableBytes) - let initialCallCount = callCount - minimumDownloadSize = p1.transferredBytes + 1000000 - - // Run a second time to upload more data and verify that the callback continues to be called - ex = expectation(description: "second download") - executeChild() - waitForExpectations(timeout: 60.0, handler: nil) - XCTAssertGreaterThanOrEqual(callCount, initialCallCount) - let p2 = try XCTUnwrap(progress) - XCTAssertEqual(p2.transferredBytes, p2.transferrableBytes) - - token!.invalidate() - } - - @MainActor - func testStreamingUploadNotifier() throws { - let user = try logInUser(for: basicCredentials()) - - let realm = try immediatelyOpenRealm(partitionValue: #function, user: user) - let session = try XCTUnwrap(realm.syncSession) - - var ex = expectation(description: "initial upload") - var progress: SyncSession.Progress? - - let token = session.addProgressNotification(for: .upload, mode: .reportIndefinitely) { p in - DispatchQueue.main.async { @MainActor in - if let progress = progress { - XCTAssertGreaterThanOrEqual(p.transferredBytes, progress.transferredBytes) - XCTAssertGreaterThanOrEqual(p.transferrableBytes, progress.transferrableBytes) - // The sync client sometimes sends spurious notifications - // where nothing has changed, and we should just ignore those - if p.transferredBytes == progress.transferredBytes && p.transferrableBytes == progress.transferrableBytes { - return - } - } - progress = p - if p.transferredBytes > 100 && p.isTransferComplete { - ex.fulfill() - } - } - } - XCTAssertNotNil(token) - waitForExpectations(timeout: 10.0, handler: nil) - - for i in 0..<5 { - ex = expectation(description: "write transaction upload \(i)") - try realm.write { - for _ in 0.. Int { - if let attr = try? FileManager.default.attributesOfItem(atPath: path) { - return attr[.size] as! Int - } - return 0 - } - XCTAssertFalse(RLMHasCachedRealmForPath(pathOnDisk)) - waitForExpectations(timeout: 10.0, handler: nil) - XCTAssertGreaterThan(fileSize(path: pathOnDisk), 0) - XCTAssertFalse(RLMHasCachedRealmForPath(pathOnDisk)) + XCTAssertEqual(syncUser2.id, app.currentUser!.id) + XCTAssertEqual(app.allUsers.count, 1) } - func testDownloadRealmToCustomPath() throws { - let user = try logInUser(for: basicCredentials()) - if !isParent { - return try populateRealm(user: user, partitionValue: #function) - } - - // Wait for the child process to upload everything. - executeChild() + func testSafelyRemoveUser() throws { + // A user can have its state updated asynchronously so we need to make sure + // that remotely disabling / deleting a user is handled correctly in the + // sync error handler. + let user = createUser() + _ = try RealmServer.shared.removeUserForApp(appId, userId: user.id).get() - let ex = expectation(description: "download-realm") - let customFileURL = realmURLForFile("copy") - var config = user.configuration(testName: #function) - config.fileURL = customFileURL - let pathOnDisk = ObjectiveCSupport.convert(object: config).pathOnDisk - XCTAssertEqual(pathOnDisk, customFileURL.path) - XCTAssertFalse(FileManager.default.fileExists(atPath: pathOnDisk)) - Realm.asyncOpen(configuration: config) { result in - switch result { - case .success(let realm): - self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) - case .failure(let error): - XCTFail("No realm on async open: \(error)") - } + // Set a callback on the user + let ex = expectation(description: "Error callback should fire upon receiving an error") + ex.assertForOverFulfill = false // error handler can legally be called multiple times + app.syncManager.errorHandler = { @Sendable (error, _) in + assertSyncError(error, .clientUserError, "Unable to refresh the user access token: invalid session: failed to find refresh token") ex.fulfill() } - func fileSize(path: String) -> Int { - if let attr = try? FileManager.default.attributesOfItem(atPath: path) { - return attr[.size] as! Int - } - return 0 - } - XCTAssertFalse(RLMHasCachedRealmForPath(pathOnDisk)) - waitForExpectations(timeout: 10.0, handler: nil) - XCTAssertGreaterThan(fileSize(path: pathOnDisk), 0) - XCTAssertFalse(RLMHasCachedRealmForPath(pathOnDisk)) - } - - func testCancelDownloadRealm() throws { - let user = try logInUser(for: basicCredentials()) - if !isParent { - return try populateRealm(user: user, partitionValue: #function) - } - - // Wait for the child process to upload everything. - executeChild() - - // Use a serial queue for asyncOpen to ensure that the first one adds - // the completion block before the second one cancels it - let queue = DispatchQueue(label: "io.realm.asyncOpen") - RLMSetAsyncOpenQueue(queue) - let ex = expectation(description: "async open") - ex.expectedFulfillmentCount = 2 - let config = user.configuration(testName: #function) - let completion = { (result: Result) -> Void in - guard case .failure = result else { - XCTFail("No error on cancelled async open") - return ex.fulfill() - } - ex.fulfill() - } - Realm.asyncOpen(configuration: config, callback: completion) - let task = Realm.asyncOpen(configuration: config, callback: completion) - queue.sync { task.cancel() } - waitForExpectations(timeout: 10.0, handler: nil) + // Try to open a Realm with the user; this will cause our errorHandler block defined above to be fired. + var config = user.configuration(partitionValue: name) + config.objectTypes = [SwiftPerson.self] + _ = try Realm(configuration: config) + wait(for: [ex], timeout: 20.0) } - func testAsyncOpenProgress() throws { - let user = try logInUser(for: basicCredentials()) - if !isParent { - return try populateRealm(user: user, partitionValue: #function) - } - - // Wait for the child process to upload everything. - executeChild() - let ex1 = expectation(description: "async open") - let ex2 = expectation(description: "download progress") - let config = user.configuration(testName: #function) - let task = Realm.asyncOpen(configuration: config) { result in - XCTAssertNotNil(try? result.get()) - ex1.fulfill() - } - - task.addProgressNotification { progress in - if progress.isTransferComplete { - ex2.fulfill() - } - } - - waitForExpectations(timeout: 10.0, handler: nil) - } - - func config(baseURL: String, transport: RLMNetworkTransport, syncTimeouts: SyncTimeoutOptions? = nil) throws -> (String, Realm.Configuration) { - let appId = try RealmServer.shared.createApp() - let appConfig = AppConfiguration(baseURL: baseURL, transport: transport, syncTimeouts: syncTimeouts) - let app = App(id: appId, configuration: appConfig) - - let user = try logInUser(for: basicCredentials(app: app), app: app) - var config = user.configuration(partitionValue: #function, cancelAsyncOpenOnNonFatalErrors: true) - config.objectTypes = [] - return (appId, config) - } - - func testAsyncOpenTimeout() throws { - let proxy = TimeoutProxyServer(port: 5678, targetPort: 9090) - try proxy.start() - - let (appId, config) = try config(baseURL: "http://localhost:5678", - transport: AsyncOpenConnectionTimeoutTransport(), - syncTimeouts: .init(connectTimeout: 2000, connectionLingerTime: 1)) - - // Two second timeout with a one second delay should work - autoreleasepool { - proxy.delay = 1.0 - let ex = expectation(description: "async open") - Realm.asyncOpen(configuration: config) { result in - let realm = try? result.get() - XCTAssertNotNil(realm) - realm?.syncSession?.suspend() - ex.fulfill() - } - waitForExpectations(timeout: 10.0, handler: nil) - } - - // The client doesn't disconnect immediately, and there isn't a good way - // to wait for it. In practice this should take more like 10ms to happen - // so a 1s sleep is plenty. - sleep(1) - - // Two second timeout with a two second delay should fail - autoreleasepool { - proxy.delay = 3.0 - let ex = expectation(description: "async open") - Realm.asyncOpen(configuration: config) { result in - guard case .failure(let error) = result else { - XCTFail("Did not fail: \(result)") - return - } - if let error = error as NSError? { - XCTAssertEqual(error.code, Int(ETIMEDOUT)) - XCTAssertEqual(error.domain, NSPOSIXErrorDomain) - } - ex.fulfill() - } - waitForExpectations(timeout: 20.0, handler: nil) - } - - proxy.stop() - try RealmServer.shared.deleteApp(appId) - } - - class LocationOverrideTransport: RLMNetworkTransport { - let hostname: String - let wsHostname: String - init(hostname: String = "http://localhost:9090", wsHostname: String = "ws://invalid.com:9090") { - self.hostname = hostname - self.wsHostname = wsHostname - } - - override func sendRequest(toServer request: RLMRequest, completion: @escaping RLMNetworkTransportCompletionBlock) { - if request.url.hasSuffix("location") { - let response = RLMResponse() - response.httpStatusCode = 200 - response.body = "{\"deployment_model\":\"GLOBAL\",\"location\":\"US-VA\",\"hostname\":\"\(hostname)\",\"ws_hostname\":\"\(wsHostname)\"}" - completion(response) - } else { - super.sendRequest(toServer: request, completion: completion) - } - } - } - - func testDNSError() throws { - let (appId, config) = try config(baseURL: "http://localhost:9090", transport: LocationOverrideTransport(wsHostname: "ws://invalid.com:9090")) - Realm.asyncOpen(configuration: config).awaitFailure(self, timeout: 40) { error in - assertSyncError(error, .connectionFailed, "Failed to connect to sync: Host not found (authoritative)") - } - try RealmServer.shared.deleteApp(appId) - } - - func testTLSError() throws { - let (appId, config) = try config(baseURL: "http://localhost:9090", transport: LocationOverrideTransport(wsHostname: "wss://localhost:9090")) - Realm.asyncOpen(configuration: config).awaitFailure(self) { error in - assertSyncError(error, .tlsHandshakeFailed, "TLS handshake failed: SecureTransport error: record overflow (-9847)") - } - try RealmServer.shared.deleteApp(appId) - } - - func testAppCredentialSupport() { - XCTAssertEqual(ObjectiveCSupport.convert(object: Credentials.facebook(accessToken: "accessToken")), - RLMCredentials(facebookToken: "accessToken")) - - XCTAssertEqual(ObjectiveCSupport.convert(object: Credentials.google(serverAuthCode: "serverAuthCode")), - RLMCredentials(googleAuthCode: "serverAuthCode")) - - XCTAssertEqual(ObjectiveCSupport.convert(object: Credentials.apple(idToken: "idToken")), - RLMCredentials(appleToken: "idToken")) - - XCTAssertEqual(ObjectiveCSupport.convert(object: Credentials.emailPassword(email: "email", password: "password")), - RLMCredentials(email: "email", password: "password")) - - XCTAssertEqual(ObjectiveCSupport.convert(object: Credentials.jwt(token: "token")), - RLMCredentials(jwt: "token")) - - XCTAssertEqual(ObjectiveCSupport.convert(object: Credentials.function(payload: ["dog": ["name": "fido"]])), - RLMCredentials(functionPayload: ["dog": ["name" as NSString: "fido" as NSString] as NSDictionary])) - - XCTAssertEqual(ObjectiveCSupport.convert(object: Credentials.userAPIKey("key")), - RLMCredentials(userAPIKey: "key")) - - XCTAssertEqual(ObjectiveCSupport.convert(object: Credentials.serverAPIKey("key")), - RLMCredentials(serverAPIKey: "key")) - - XCTAssertEqual(ObjectiveCSupport.convert(object: Credentials.anonymous), - RLMCredentials.anonymous()) - } - - // MARK: - Authentication - - func testInvalidCredentials() throws { - let email = "testInvalidCredentialsEmail" - let credentials = basicCredentials() - let user = try logInUser(for: credentials) - XCTAssertEqual(user.state, .loggedIn) - - let credentials2 = Credentials.emailPassword(email: email, password: "NOT_A_VALID_PASSWORD") - let ex = expectation(description: "Should fail to log in the user") - - self.app.login(credentials: credentials2) { result in - guard case .failure = result else { - XCTFail("Login should not have been successful") - return ex.fulfill() - } - ex.fulfill() - } - - waitForExpectations(timeout: 10, handler: nil) - } - - func testCustomTokenAuthentication() { - let user = logInUser(for: jwtCredential(withAppId: appId)) - XCTAssertEqual(user.profile.metadata["anotherName"], "Bar Foo") - XCTAssertEqual(user.profile.metadata["name"], "Foo Bar") - XCTAssertEqual(user.profile.metadata["occupation"], "firefighter") - } - - // MARK: - User-specific functionality - - func testUserExpirationCallback() throws { - let user = try logInUser(for: basicCredentials()) - - // Set a callback on the user - let blockCalled = Locked(false) - let ex = expectation(description: "Error callback should fire upon receiving an error") - app.syncManager.errorHandler = { @Sendable (error, _) in - assertSyncError(error, .clientUserError, "Unable to refresh the user access token: signature is invalid") - blockCalled.value = true - ex.fulfill() - } - - // Screw up the token on the user. - setInvalidTokensFor(user) - // Try to open a Realm with the user; this will cause our errorHandler block defined above to be fired. - XCTAssertFalse(blockCalled.value) - _ = try immediatelyOpenRealm(partitionValue: "realm_id", user: user) - - waitForExpectations(timeout: 10.0, handler: nil) - } - - private func realmURLForFile(_ fileName: String) -> URL { - let testDir = RLMRealmPathForFile("mongodb-realm") - let directory = URL(fileURLWithPath: testDir, isDirectory: true) - return directory.appendingPathComponent(fileName, isDirectory: false) - } - - // MARK: - App tests - - private func appConfig() -> AppConfiguration { - return AppConfiguration(baseURL: "http://localhost:9090") - } - - func expectSuccess(_ result: Result) -> T? { - switch result { - case .success(let value): - return value - case .failure(let error): - XCTFail("unexpected error: \(error)") - return nil - } - } - - func testAppInit() { - let appName = "translate-utwuv" - - let appWithNoConfig = App(id: appName) - XCTAssertEqual(appWithNoConfig.allUsers.count, 0) - - let appWithConfig = App(id: appName, configuration: appConfig()) - XCTAssertEqual(appWithConfig.allUsers.count, 0) - } - - func testAppLogin() { - 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) - - XCTAssertEqual(syncUser.id, app.currentUser?.id) - XCTAssertEqual(app.allUsers.count, 1) - } - - func testAppSwitchAndRemove() { - let email1 = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - let password1 = randomString(10) - let email2 = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - let password2 = randomString(10) - - app.emailPasswordAuth.registerUser(email: email1, password: password1).await(self) - app.emailPasswordAuth.registerUser(email: email2, password: password2).await(self) - - let syncUser1 = app.login(credentials: Credentials.emailPassword(email: email1, password: password1)).await(self) - let syncUser2 = app.login(credentials: Credentials.emailPassword(email: email2, password: password2)).await(self) - - XCTAssertEqual(app.allUsers.count, 2) - - XCTAssertEqual(syncUser2.id, app.currentUser!.id) - - app.switch(to: syncUser1) - XCTAssertTrue(syncUser1.id == app.currentUser?.id) - - syncUser1.remove().await(self) - - XCTAssertEqual(syncUser2.id, app.currentUser!.id) - XCTAssertEqual(app.allUsers.count, 1) - } - - func testSafelyRemoveUser() throws { - // A user can have its state updated asynchronously so we need to make sure - // that remotely disabling / deleting a user is handled correctly in the - // sync error handler. - app.login(credentials: .anonymous).await(self) - - let user = app.currentUser! - _ = expectSuccess(RealmServer.shared.removeUserForApp(appId, userId: user.id)) - - // Set a callback on the user - let ex = expectation(description: "Error callback should fire upon receiving an error") - ex.assertForOverFulfill = false // error handler can legally be called multiple times - app.syncManager.errorHandler = { @Sendable (error, _) in - assertSyncError(error, .clientUserError, "Unable to refresh the user access token: invalid session: failed to find refresh token") - ex.fulfill() - } - - // Try to open a Realm with the user; this will cause our errorHandler block defined above to be fired. - _ = try immediatelyOpenRealm(partitionValue: #function, user: user) - wait(for: [ex], timeout: 20.0) - } - - func testDeleteUser() { - func userExistsOnServer(_ user: User) -> Bool { - var userExists = false - switch RealmServer.shared.retrieveUser(appId, userId: user.id) { - case .success(let u): - let u = u as! [String: Any] - XCTAssertEqual(u["_id"] as! String, user.id) - userExists = true - case .failure: - userExists = false - } - return userExists - } - - 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) - XCTAssertTrue(userExistsOnServer(syncUser)) - - XCTAssertEqual(syncUser.id, app.currentUser?.id) - XCTAssertEqual(app.allUsers.count, 1) - - syncUser.delete().await(self) - - XCTAssertFalse(userExistsOnServer(syncUser)) - XCTAssertNil(app.currentUser) - XCTAssertEqual(app.allUsers.count, 0) - } - - func testAppLinkUser() { - 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 credentials = Credentials.emailPassword(email: email, password: password) - let syncUser = app.login(credentials: Credentials.anonymous).await(self) - syncUser.linkUser(credentials: credentials).await(self) - XCTAssertEqual(syncUser.id, app.currentUser?.id) - XCTAssertEqual(syncUser.identities.count, 2) - } - - // MARK: - Provider Clients - - func testEmailPasswordProviderClient() { - let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - let password = randomString(10) - app.emailPasswordAuth.registerUser(email: email, password: password).await(self) - - app.emailPasswordAuth.confirmUser("atoken", tokenId: "atokenid").awaitFailure(self) { - assertAppError($0, .badRequest, "invalid token data") - } - app.emailPasswordAuth.resendConfirmationEmail(email: "atoken").awaitFailure(self) { - assertAppError($0, .userNotFound, "user not found") - } - app.emailPasswordAuth.retryCustomConfirmation(email: email).awaitFailure(self) { - assertAppError($0, .unknown, - "cannot run confirmation for \(email): automatic confirmation is enabled") - } - app.emailPasswordAuth.sendResetPasswordEmail(email: "atoken").awaitFailure(self) { - assertAppError($0, .userNotFound, "user not found") - } - app.emailPasswordAuth.resetPassword(to: "password", token: "atoken", tokenId: "tokenId").awaitFailure(self) { - assertAppError($0, .badRequest, "invalid token data") - } - app.emailPasswordAuth.callResetPasswordFunction(email: email, - password: randomString(10), - args: [[:]]).awaitFailure(self) { - assertAppError($0, .unknown, "failed to reset password for user \"\(email)\"") - } - } - - func testUserAPIKeyProviderClient() { - 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 credentials = Credentials.emailPassword(email: email, password: password) - let syncUser = app.login(credentials: credentials).await(self) - - let apiKey = syncUser.apiKeysAuth.createAPIKey(named: "my-api-key").await(self) - XCTAssertEqual(apiKey.name, "my-api-key") - XCTAssertNotNil(apiKey.key) - XCTAssertNotEqual(apiKey.key!, "my-api-key") - XCTAssertFalse(apiKey.key!.isEmpty) - - syncUser.apiKeysAuth.fetchAPIKey(apiKey.objectId).await(self) - - let apiKeys = syncUser.apiKeysAuth.fetchAPIKeys().await(self) - XCTAssertEqual(apiKeys.count, 1) - - syncUser.apiKeysAuth.disableAPIKey(apiKey.objectId).await(self) - syncUser.apiKeysAuth.enableAPIKey(apiKey.objectId).await(self) - syncUser.apiKeysAuth.deleteAPIKey(apiKey.objectId).await(self) - - let apiKeys2 = syncUser.apiKeysAuth.fetchAPIKeys().await(self) - XCTAssertEqual(apiKeys2.count, 0) - } - - func testCallFunction() { - 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 credentials = Credentials.emailPassword(email: email, password: password) - let syncUser = app.login(credentials: credentials).await(self) - - let bson = syncUser.functions.sum([1, 2, 3, 4, 5]).await(self) - guard case let .int32(sum) = bson else { - XCTFail("unexpected bson type in sum: \(bson)") - return - } - XCTAssertEqual(sum, 15) - } - - func testPushRegistration() { - 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 credentials = Credentials.emailPassword(email: email, password: password) - app.login(credentials: credentials).await(self) - - let client = app.pushClient(serviceName: "gcm") - client.registerDevice(token: "some-token", user: app.currentUser!).await(self) - client.deregisterDevice(user: app.currentUser!).await(self) - } - - func testCustomUserData() { - let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - let password = randomString(10) - - let credentials = Credentials.emailPassword(email: email, password: password) - app.emailPasswordAuth.registerUser(email: email, password: password).await(self) - let user = app.login(credentials: credentials).await(self) - user.functions.updateUserData([["favourite_colour": "green", "apples": 10]]).await(self) - - let customData = user.refreshCustomData().await(self) - XCTAssertEqual(customData["apples"] as! Int, 10) - XCTAssertEqual(customData["favourite_colour"] as! String, "green") - - XCTAssertEqual(user.customData["favourite_colour"], .string("green")) - XCTAssertEqual(user.customData["apples"], .int64(10)) - } - - // MARK: User Profile - - func testUserProfileInitialization() { - let profile = UserProfile() - XCTAssertNil(profile.name) - XCTAssertNil(profile.maxAge) - XCTAssertNil(profile.minAge) - XCTAssertNil(profile.birthday) - XCTAssertNil(profile.gender) - XCTAssertNil(profile.firstName) - XCTAssertNil(profile.lastName) - XCTAssertNil(profile.pictureURL) - XCTAssertEqual(profile.metadata, [:]) - } - - // MARK: Seed file path - - func testSeedFilePathOpenLocalToSync() throws { - var config = Realm.Configuration() - config.fileURL = RLMTestRealmURL() - config.objectTypes = [SwiftHugeSyncObject.self] - let realm = try Realm(configuration: config) - try realm.write { - for _ in 0.. SwiftMissingObject.anyCol -> SwiftPerson.firstName - let anyCol = ((obj!.anyCol.dynamicObject?.anyCol as? Object)?["anyCol"] as? Object) - XCTAssertEqual((anyCol?["firstName"] as? String), "Rick") - try realm.write { - anyCol?["firstName"] = "Morty" - } - XCTAssertEqual((anyCol?["firstName"] as? String), "Morty") - let objectCol = (obj!.anyCol.dynamicObject?.objectCol as? Object) - XCTAssertEqual((objectCol?["firstName"] as? String), "Morty") - } -} - -// XCTest doesn't care about the @available on the class and will try to run -// the tests even on older versions. Putting this check inside `defaultTestSuite` -// results in a warning about it being redundant due to the enclosing check, so -// it needs to be out of line. -func hasCombine() -> Bool { - if #available(macOS 10.15, watchOS 6.0, iOS 13.0, tvOS 13.0, *) { - return true - } - return false -} - -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) -@objc(CombineObjectServerTests) -class CombineObjectServerTests: SwiftSyncTestCase { - override class var defaultTestSuite: XCTestSuite { - if hasCombine() { - return super.defaultTestSuite - } - return XCTestSuite(name: "\(type(of: self))") - } - - var subscriptions: Set = [] - - override func tearDown() { - subscriptions.forEach { $0.cancel() } - subscriptions = [] - super.tearDown() - } - - // swiftlint:disable multiple_closures_with_trailing_closure - func testWatchCombine() throws { - let collection = try setupMongoCollection(for: Dog.self) - let document: Document = ["name": "fido", "breed": "cane corso"] - - let watchEx1 = Locked(expectation(description: "Main thread watch")) - let watchEx2 = Locked(expectation(description: "Background thread watch")) - - collection.watch() - .onOpen { - watchEx1.wrappedValue.fulfill() - } - .subscribe(on: DispatchQueue.global()) - .receive(on: DispatchQueue.global()) - .sink(receiveCompletion: { @Sendable _ in }) { @Sendable _ in - XCTAssertFalse(Thread.isMainThread) - watchEx1.wrappedValue.fulfill() - }.store(in: &subscriptions) - - collection.watch() - .onOpen { - watchEx2.wrappedValue.fulfill() - } - .subscribe(on: DispatchQueue.main) - .receive(on: DispatchQueue.main) - .sink(receiveCompletion: { _ in }) { _ in - XCTAssertTrue(Thread.isMainThread) - watchEx2.wrappedValue.fulfill() - }.store(in: &subscriptions) - - for _ in 0..<3 { - wait(for: [watchEx1.wrappedValue, watchEx2.wrappedValue], timeout: 60.0) - watchEx1.wrappedValue = expectation(description: "Main thread watch") - watchEx2.wrappedValue = expectation(description: "Background thread watch") - collection.insertOne(document) { result in - if case .failure(let error) = result { - XCTFail("Failed to insert: \(error)") - } - } - } - wait(for: [watchEx1.wrappedValue, watchEx2.wrappedValue], timeout: 60.0) - } - - func testWatchCombineWithFilterIds() throws { - let collection = try setupMongoCollection(for: Dog.self) - let document: Document = ["name": "fido", "breed": "cane corso"] - let document2: Document = ["name": "rex", "breed": "cane corso"] - let document3: Document = ["name": "john", "breed": "cane corso"] - let document4: Document = ["name": "ted", "breed": "bullmastiff"] - - let objIds = collection.insertMany([document, document2, document3, document4]).await(self) - let objectIds = objIds.map { $0.objectIdValue! } - - let watchEx1 = Locked(expectation(description: "Main thread watch")) - let watchEx2 = Locked(expectation(description: "Background thread watch")) - collection.watch(filterIds: [objectIds[0]]) - .onOpen { - watchEx1.wrappedValue.fulfill() - } - .subscribe(on: DispatchQueue.main) - .receive(on: DispatchQueue.main) - .sink(receiveCompletion: { _ in }) { changeEvent in - XCTAssertTrue(Thread.isMainThread) - guard let doc = changeEvent.documentValue else { - return - } - - let objectId = doc["fullDocument"]??.documentValue!["_id"]??.objectIdValue! - if objectId == objectIds[0] { - watchEx1.wrappedValue.fulfill() - } - }.store(in: &subscriptions) - - collection.watch(filterIds: [objectIds[1]]) - .onOpen { - watchEx2.wrappedValue.fulfill() - } - .subscribe(on: DispatchQueue.global()) - .receive(on: DispatchQueue.global()) - .sink(receiveCompletion: { _ in }) { @Sendable changeEvent in - XCTAssertFalse(Thread.isMainThread) - guard let doc = changeEvent.documentValue else { - return - } - - let objectId = doc["fullDocument"]??.documentValue!["_id"]??.objectIdValue! - if objectId == objectIds[1] { - watchEx2.wrappedValue.fulfill() - } - }.store(in: &subscriptions) - - for i in 0..<3 { - wait(for: [watchEx1.wrappedValue, watchEx2.wrappedValue], timeout: 60.0) - watchEx1.wrappedValue = expectation(description: "Main thread watch") - watchEx2.wrappedValue = expectation(description: "Background thread watch") - - let name: AnyBSON = .string("fido-\(i)") - collection.updateOneDocument(filter: ["_id": AnyBSON.objectId(objectIds[0])], - update: ["name": name, "breed": "king charles"]) { result in - if case .failure(let error) = result { - XCTFail("Failed to update: \(error)") - } - } - collection.updateOneDocument(filter: ["_id": AnyBSON.objectId(objectIds[1])], - update: ["name": name, "breed": "king charles"]) { result in - if case .failure(let error) = result { - XCTFail("Failed to update: \(error)") - } - } - } - wait(for: [watchEx1.wrappedValue, watchEx2.wrappedValue], timeout: 60.0) - } - - func testWatchCombineWithMatchFilter() throws { - let collection = try setupMongoCollection(for: Dog.self) - let document: Document = ["name": "fido", "breed": "cane corso"] - let document2: Document = ["name": "rex", "breed": "cane corso"] - let document3: Document = ["name": "john", "breed": "cane corso"] - let document4: Document = ["name": "ted", "breed": "bullmastiff"] - - let objIds = collection.insertMany([document, document2, document3, document4]).await(self) - XCTAssertEqual(objIds.count, 4) - let objectIds = objIds.map { $0.objectIdValue! } - - let watchEx1 = Locked(expectation(description: "Main thread watch")) - let watchEx2 = Locked(expectation(description: "Background thread watch")) - collection.watch(matchFilter: ["fullDocument._id": AnyBSON.objectId(objectIds[0])]) - .onOpen { - watchEx1.wrappedValue.fulfill() - } - .subscribe(on: DispatchQueue.main) - .receive(on: DispatchQueue.main) - .sink(receiveCompletion: { _ in }) { changeEvent in - XCTAssertTrue(Thread.isMainThread) - guard let doc = changeEvent.documentValue else { - return - } - - let objectId = doc["fullDocument"]??.documentValue!["_id"]??.objectIdValue! - if objectId == objectIds[0] { - watchEx1.wrappedValue.fulfill() - } - }.store(in: &subscriptions) - - collection.watch(matchFilter: ["fullDocument._id": AnyBSON.objectId(objectIds[1])]) - .onOpen { - watchEx2.wrappedValue.fulfill() - } - .subscribe(on: DispatchQueue.global()) - .receive(on: DispatchQueue.global()) - .sink(receiveCompletion: { _ in }) { @Sendable changeEvent in - XCTAssertFalse(Thread.isMainThread) - guard let doc = changeEvent.documentValue else { - return - } - - let objectId = doc["fullDocument"]??.documentValue!["_id"]??.objectIdValue! - if objectId == objectIds[1] { - watchEx2.wrappedValue.fulfill() - } - }.store(in: &subscriptions) - - for i in 0..<3 { - wait(for: [watchEx1.wrappedValue, watchEx2.wrappedValue], timeout: 60.0) - watchEx1.wrappedValue = expectation(description: "Main thread watch") - watchEx2.wrappedValue = expectation(description: "Background thread watch") - - let name: AnyBSON = .string("fido-\(i)") - collection.updateOneDocument(filter: ["_id": AnyBSON.objectId(objectIds[0])], - update: ["name": name, "breed": "king charles"]) { result in - if case .failure(let error) = result { - XCTFail("Failed to update: \(error)") - } - } - collection.updateOneDocument(filter: ["_id": AnyBSON.objectId(objectIds[1])], - update: ["name": name, "breed": "king charles"]) { result in - if case .failure(let error) = result { - XCTFail("Failed to update: \(error)") - } - } - } - wait(for: [watchEx1.wrappedValue, watchEx2.wrappedValue], timeout: 60.0) - } - - // MARK: - Combine promises - - func testAppLoginCombine() { - let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - let password = randomString(10) - - let loginEx = expectation(description: "Login user") - let appEx = expectation(description: "App changes triggered") - var triggered = 0 - app.objectWillChange.sink { _ in - triggered += 1 - if triggered == 2 { - appEx.fulfill() - } - }.store(in: &subscriptions) - - app.emailPasswordAuth.registerUser(email: email, password: password) - .flatMap { @Sendable in self.app.login(credentials: .emailPassword(email: email, password: password)) } - .receive(on: DispatchQueue.main) - .sink(receiveCompletion: { result in - if case let .failure(error) = result { - XCTFail("Should have completed login chain: \(error.localizedDescription)") - } - }, receiveValue: { user in - user.objectWillChange.sink { @Sendable user in - XCTAssert(!user.isLoggedIn) - loginEx.fulfill() - }.store(in: &self.subscriptions) - XCTAssertEqual(user.id, self.app.currentUser?.id) - user.logOut { _ in } // logout user and make sure it is observed - }) - .store(in: &subscriptions) - wait(for: [loginEx, appEx], timeout: 30.0) - XCTAssertEqual(self.app.allUsers.count, 1) - XCTAssertEqual(triggered, 2) - } - - func testAsyncOpenCombine() { - let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - let password = randomString(10) - app.emailPasswordAuth.registerUser(email: email, password: password) - .flatMap { @Sendable in self.app.login(credentials: .emailPassword(email: email, password: password)) } - .flatMap { @Sendable user in - Realm.asyncOpen(configuration: user.configuration(testName: #function)) - } - .await(self, timeout: 30.0) { realm in - try! realm.write { - realm.add(SwiftHugeSyncObject.create()) - realm.add(SwiftHugeSyncObject.create()) - } - let progressEx = self.expectation(description: "Should upload") - let token = realm.syncSession!.addProgressNotification(for: .upload, mode: .forCurrentlyOutstandingWork) { - if $0.isTransferComplete { - progressEx.fulfill() - } - } - self.wait(for: [progressEx], timeout: 30.0) - token?.invalidate() - } - - let chainEx = expectation(description: "Should chain realm login => realm async open") - let progressEx = expectation(description: "Should receive progress notification") - app.login(credentials: .anonymous) - .flatMap { @Sendable in - Realm.asyncOpen(configuration: $0.configuration(testName: #function)).onProgressNotification { - if $0.isTransferComplete { - progressEx.fulfill() - } - } - } - .expectValue(self, chainEx) { realm in - XCTAssertEqual(realm.objects(SwiftHugeSyncObject.self).count, 2) - }.store(in: &subscriptions) - wait(for: [chainEx, progressEx], timeout: 30.0) - } - - func testAsyncOpenStandaloneCombine() throws { - try autoreleasepool { - let realm = try Realm() - try realm.write { - (0..<10000).forEach { _ in realm.add(SwiftPerson(firstName: "Charlie", lastName: "Bucket")) } - } - } - - Realm.asyncOpen().await(self) { realm in - XCTAssertEqual(realm.objects(SwiftPerson.self).count, 10000) - } - } - - func testDeleteUserCombine() { - let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - let password = randomString(10) - - let appEx = expectation(description: "App changes triggered") - var triggered = 0 - app.objectWillChange.sink { _ in - triggered += 1 - if triggered == 2 { - appEx.fulfill() - } - }.store(in: &subscriptions) - - app.emailPasswordAuth.registerUser(email: email, password: password) - .flatMap { @Sendable in self.app.login(credentials: .emailPassword(email: email, password: password)) } - .flatMap { @Sendable in $0.delete() } - .await(self) - wait(for: [appEx], timeout: 30.0) - XCTAssertEqual(self.app.allUsers.count, 0) - XCTAssertEqual(triggered, 2) - } - - func testMongoCollectionInsertCombine() throws { - let collection = try setupMongoCollection(for: Dog.self) - let document: Document = ["name": "fido", "breed": "cane corso"] - let document2: Document = ["name": "rex", "breed": "tibetan mastiff"] - - collection.insertOne(document).await(self) - collection.insertMany([document, document2]) - .await(self) { objectIds in - XCTAssertEqual(objectIds.count, 2) - } - collection.find(filter: [:]) - .await(self) { findResult in - XCTAssertEqual(findResult.map({ $0["name"]??.stringValue }), ["fido", "fido", "rex"]) - } - } - - func testMongoCollectionFindCombine() throws { - let collection = try setupMongoCollection(for: Dog.self) - let document: Document = ["name": "fido", "breed": "cane corso"] - let document2: Document = ["name": "rex", "breed": "tibetan mastiff"] - let document3: Document = ["name": "rex", "breed": "tibetan mastiff", "coat": ["fawn", "brown", "white"]] - let findOptions = FindOptions(1, nil) - - collection.find(filter: [:], options: findOptions) - .await(self) { findResult in - XCTAssertEqual(findResult.count, 0) - } - collection.insertMany([document, document2, document3]).await(self) - collection.find(filter: [:]) - .await(self) { findResult in - XCTAssertEqual(findResult.map({ $0["name"]??.stringValue }), ["fido", "rex", "rex"]) - } - collection.find(filter: [:], options: findOptions) - .await(self) { findResult in - XCTAssertEqual(findResult.count, 1) - XCTAssertEqual(findResult[0]["name"]??.stringValue, "fido") - } - collection.find(filter: document3, options: findOptions) - .await(self) { findResult in - XCTAssertEqual(findResult.count, 1) - } - collection.findOneDocument(filter: document).await(self) - - collection.findOneDocument(filter: document, options: findOptions).await(self) - } - - func testMongoCollectionCountAndAggregateCombine() throws { - let collection = try setupMongoCollection(for: Dog.self) - let document: Document = ["name": "fido", "breed": "cane corso"] - - collection.insertMany([document]).await(self) - collection.aggregate(pipeline: [["$match": ["name": "fido"]], ["$group": ["_id": "$name"]]]) - .await(self) - collection.count(filter: document).await(self) { count in - XCTAssertEqual(count, 1) - } - collection.count(filter: document, limit: 1).await(self) { count in - XCTAssertEqual(count, 1) - } - } - - func testMongoCollectionDeleteOneCombine() throws { - let collection = try setupMongoCollection(for: Dog.self) - let document: Document = ["name": "fido", "breed": "cane corso"] - let document2: Document = ["name": "rex", "breed": "cane corso"] - - collection.deleteOneDocument(filter: document).await(self) { count in - XCTAssertEqual(count, 0) - } - collection.insertMany([document, document2]).await(self) - collection.deleteOneDocument(filter: document).await(self) { count in - XCTAssertEqual(count, 1) - } - } - - func testMongoCollectionDeleteManyCombine() throws { - let collection = try setupMongoCollection(for: Dog.self) - let document: Document = ["name": "fido", "breed": "cane corso"] - let document2: Document = ["name": "rex", "breed": "cane corso"] - - collection.deleteManyDocuments(filter: document).await(self) { count in - XCTAssertEqual(count, 0) - } - collection.insertMany([document, document2]).await(self) - collection.deleteManyDocuments(filter: ["breed": "cane corso"]).await(self) { count in - XCTAssertEqual(count, 2) - } - } - - func testMongoCollectionUpdateOneCombine() throws { - let collection = try setupMongoCollection(for: Dog.self) - let document: Document = ["name": "fido", "breed": "cane corso"] - let document2: Document = ["name": "rex", "breed": "cane corso"] - let document3: Document = ["name": "john", "breed": "cane corso"] - let document4: Document = ["name": "ted", "breed": "bullmastiff"] - let document5: Document = ["name": "bill", "breed": "great dane"] - - collection.insertMany([document, document2, document3, document4]).await(self) - collection.updateOneDocument(filter: document, update: document2).await(self) { updateResult in - XCTAssertEqual(updateResult.matchedCount, 1) - XCTAssertEqual(updateResult.modifiedCount, 1) - XCTAssertNil(updateResult.documentId) - } - - collection.updateOneDocument(filter: document5, update: document2, upsert: true).await(self) { updateResult in - XCTAssertEqual(updateResult.matchedCount, 0) - XCTAssertEqual(updateResult.modifiedCount, 0) - XCTAssertNotNil(updateResult.documentId) - } - } - - func testMongoCollectionUpdateManyCombine() throws { - let collection = try setupMongoCollection(for: Dog.self) - let document: Document = ["name": "fido", "breed": "cane corso"] - let document2: Document = ["name": "rex", "breed": "cane corso"] - let document3: Document = ["name": "john", "breed": "cane corso"] - let document4: Document = ["name": "ted", "breed": "bullmastiff"] - let document5: Document = ["name": "bill", "breed": "great dane"] - - collection.insertMany([document, document2, document3, document4]).await(self) - collection.updateManyDocuments(filter: document, update: document2).await(self) { updateResult in - XCTAssertEqual(updateResult.matchedCount, 1) - XCTAssertEqual(updateResult.modifiedCount, 1) - XCTAssertNil(updateResult.documentId) - } - collection.updateManyDocuments(filter: document5, update: document2, upsert: true).await(self) { updateResult in - XCTAssertEqual(updateResult.matchedCount, 0) - XCTAssertEqual(updateResult.modifiedCount, 0) - XCTAssertNotNil(updateResult.documentId) + func testDeleteUser() { + func userExistsOnServer(_ user: User) -> Bool { + var userExists = false + switch RealmServer.shared.retrieveUser(appId, userId: user.id) { + case .success(let u): + let u = u as! [String: Any] + XCTAssertEqual(u["_id"] as! String, user.id) + userExists = true + case .failure: + userExists = false + } + return userExists } - } - - func testMongoCollectionFindAndUpdateCombine() throws { - let collection = try setupMongoCollection(for: Dog.self) - let document: Document = ["name": "fido", "breed": "cane corso"] - let document2: Document = ["name": "rex", "breed": "cane corso"] - let document3: Document = ["name": "john", "breed": "cane corso"] - collection.findOneAndUpdate(filter: document, update: document2).await(self) + let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" + let password = randomString(10) - let options1 = FindOneAndModifyOptions(["name": 1], [["_id": 1]], true, true) - collection.findOneAndUpdate(filter: document2, update: document3, options: options1).await(self) { updateResult in - guard let updateResult = updateResult else { - XCTFail("Should find") - return - } - XCTAssertEqual(updateResult["name"]??.stringValue, "john") - } + app.emailPasswordAuth.registerUser(email: email, password: password).await(self) - let options2 = FindOneAndModifyOptions(["name": 1], [["_id": 1]], true, true) - collection.findOneAndUpdate(filter: document, update: document2, options: options2).await(self) { updateResult in - guard let updateResult = updateResult else { - XCTFail("Should find") - return - } - XCTAssertEqual(updateResult["name"]??.stringValue, "rex") - } - } + let syncUser = app.login(credentials: Credentials.emailPassword(email: email, password: password)).await(self) + XCTAssertTrue(userExistsOnServer(syncUser)) - func testMongoCollectionFindAndReplaceCombine() throws { - let collection = try setupMongoCollection(for: Dog.self) - let document: Document = ["name": "fido", "breed": "cane corso"] - let document2: Document = ["name": "rex", "breed": "cane corso"] - let document3: Document = ["name": "john", "breed": "cane corso"] + XCTAssertEqual(syncUser.id, app.currentUser?.id) + XCTAssertEqual(app.allUsers.count, 1) - collection.findOneAndReplace(filter: document, replacement: document2).await(self) { updateResult in - XCTAssertNil(updateResult) - } + syncUser.delete().await(self) - let options1 = FindOneAndModifyOptions(["name": 1], [["_id": 1]], true, true) - collection.findOneAndReplace(filter: document2, replacement: document3, options: options1).await(self) { updateResult in - guard let updateResult = updateResult else { - XCTFail("Should find") - return - } - XCTAssertEqual(updateResult["name"]??.stringValue, "john") - } + XCTAssertFalse(userExistsOnServer(syncUser)) + XCTAssertNil(app.currentUser) + XCTAssertEqual(app.allUsers.count, 0) + } - let options2 = FindOneAndModifyOptions(["name": 1], [["_id": 1]], true, false) - collection.findOneAndReplace(filter: document, replacement: document2, options: options2).await(self) { updateResult in - XCTAssertNil(updateResult) - } + func testAppLinkUser() { + 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 credentials = Credentials.emailPassword(email: email, password: password) + let syncUser = app.login(credentials: Credentials.anonymous).await(self) + syncUser.linkUser(credentials: credentials).await(self) + XCTAssertEqual(syncUser.id, app.currentUser?.id) + XCTAssertEqual(syncUser.identities.count, 2) } - func testMongoCollectionFindAndDeleteCombine() throws { - let collection = try setupMongoCollection(for: Dog.self) - let document: Document = ["name": "fido", "breed": "cane corso"] - collection.insertMany([document]).await(self) + // MARK: - Provider Clients - collection.findOneAndDelete(filter: document).await(self) { updateResult in - XCTAssertNotNil(updateResult) - } - collection.findOneAndDelete(filter: document).await(self) { updateResult in - XCTAssertNil(updateResult) - } + func testEmailPasswordProviderClient() { + let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" + let password = randomString(10) + app.emailPasswordAuth.registerUser(email: email, password: password).await(self) - collection.insertMany([document]).await(self) - let options1 = FindOneAndModifyOptions(["name": 1], [["_id": 1]], false, false) - collection.findOneAndDelete(filter: document, options: options1).await(self) { deleteResult in - XCTAssertNotNil(deleteResult) + app.emailPasswordAuth.confirmUser("atoken", tokenId: "atokenid").awaitFailure(self) { + assertAppError($0, .badRequest, "invalid token data") } - collection.findOneAndDelete(filter: document, options: options1).await(self) { deleteResult in - XCTAssertNil(deleteResult) + app.emailPasswordAuth.resendConfirmationEmail(email: "atoken").awaitFailure(self) { + assertAppError($0, .userNotFound, "user not found") } - - collection.insertMany([document]).await(self) - let options2 = FindOneAndModifyOptions(["name": 1], [["_id": 1]]) - collection.findOneAndDelete(filter: document, options: options2).await(self) { deleteResult in - XCTAssertNotNil(deleteResult) + app.emailPasswordAuth.retryCustomConfirmation(email: email).awaitFailure(self) { + assertAppError($0, .unknown, + "cannot run confirmation for \(email): automatic confirmation is enabled") } - collection.findOneAndDelete(filter: document, options: options2).await(self) { deleteResult in - XCTAssertNil(deleteResult) + app.emailPasswordAuth.sendResetPasswordEmail(email: "atoken").awaitFailure(self) { + assertAppError($0, .userNotFound, "user not found") } - - collection.insertMany([document]).await(self) - collection.find(filter: [:]).await(self) { updateResult in - XCTAssertEqual(updateResult.count, 1) + app.emailPasswordAuth.resetPassword(to: "password", token: "atoken", tokenId: "tokenId").awaitFailure(self) { + assertAppError($0, .badRequest, "invalid token data") + } + app.emailPasswordAuth.callResetPasswordFunction(email: email, + password: randomString(10), + args: [[:]]).awaitFailure(self) { + assertAppError($0, .unknown, "failed to reset password for user \"\(email)\"") } } - func testCallFunctionCombine() { + func testUserAPIKeyProviderClient() { 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 credentials = Credentials.emailPassword(email: email, password: password) - app.login(credentials: credentials).await(self) { user in - XCTAssertNotNil(user) - } + let syncUser = app.login(credentials: credentials).await(self) - app.currentUser?.functions.sum([1, 2, 3, 4, 5]).await(self) { bson in - guard case let .int32(sum) = bson else { - XCTFail("Should be int32") - return - } - XCTAssertEqual(sum, 15) - } + let apiKey = syncUser.apiKeysAuth.createAPIKey(named: "my-api-key").await(self) + XCTAssertEqual(apiKey.name, "my-api-key") + XCTAssertNotNil(apiKey.key) + XCTAssertNotEqual(apiKey.key!, "my-api-key") + XCTAssertFalse(apiKey.key!.isEmpty) - app.currentUser?.functions.updateUserData([["favourite_colour": "green", "apples": 10]]).await(self) { bson in - guard case let .bool(upd) = bson else { - XCTFail("Should be bool") - return - } - XCTAssertTrue(upd) - } + syncUser.apiKeysAuth.fetchAPIKey(apiKey.objectId).await(self) + + let apiKeys = syncUser.apiKeysAuth.fetchAPIKeys().await(self) + XCTAssertEqual(apiKeys.count, 1) + + syncUser.apiKeysAuth.disableAPIKey(apiKey.objectId).await(self) + syncUser.apiKeysAuth.enableAPIKey(apiKey.objectId).await(self) + syncUser.apiKeysAuth.deleteAPIKey(apiKey.objectId).await(self) + + let apiKeys2 = syncUser.apiKeysAuth.fetchAPIKeys().await(self) + XCTAssertEqual(apiKeys2.count, 0) } - func testAPIKeyAuthCombine() { + func testCallFunction() { 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 user = app.login(credentials: Credentials.emailPassword(email: email, password: password)).await(self) + let credentials = Credentials.emailPassword(email: email, password: password) + let syncUser = app.login(credentials: credentials).await(self) - let apiKey = user.apiKeysAuth.createAPIKey(named: "my-api-key").await(self) - user.apiKeysAuth.fetchAPIKey(apiKey.objectId).await(self) - user.apiKeysAuth.fetchAPIKeys().await(self) { userApiKeys in - XCTAssertEqual(userApiKeys.count, 1) + let bson = syncUser.functions.sum([1, 2, 3, 4, 5]).await(self) + guard case let .int32(sum) = bson else { + XCTFail("unexpected bson type in sum: \(bson)") + return } - - user.apiKeysAuth.disableAPIKey(apiKey.objectId).await(self) - user.apiKeysAuth.enableAPIKey(apiKey.objectId).await(self) - user.apiKeysAuth.deleteAPIKey(apiKey.objectId).await(self) + XCTAssertEqual(sum, 15) } - func testPushRegistrationCombine() { + func testPushRegistration() { let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" let password = randomString(10) app.emailPasswordAuth.registerUser(email: email, password: password).await(self) - app.login(credentials: Credentials.emailPassword(email: email, password: password)).await(self) + let credentials = Credentials.emailPassword(email: email, password: password) + app.login(credentials: credentials).await(self) let client = app.pushClient(serviceName: "gcm") client.registerDevice(token: "some-token", user: app.currentUser!).await(self) client.deregisterDevice(user: app.currentUser!).await(self) } -} -@available(macOS 12.0, *) -class AsyncAwaitObjectServerTests: SwiftSyncTestCase { - override class var defaultTestSuite: XCTestSuite { - // async/await is currently incompatible with thread sanitizer and will - // produce many false positives - // https://bugs.swift.org/browse/SR-15444 - if RLMThreadSanitizerEnabled() { - return XCTestSuite(name: "\(type(of: self))") - } - return super.defaultTestSuite + func testCustomUserData() { + let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" + let password = randomString(10) + + let credentials = Credentials.emailPassword(email: email, password: password) + app.emailPasswordAuth.registerUser(email: email, password: password).await(self) + let user = app.login(credentials: credentials).await(self) + user.functions.updateUserData([["favourite_colour": "green", "apples": 10]]).await(self) + + let customData = user.refreshCustomData().await(self) + XCTAssertEqual(customData["apples"] as! Int, 10) + XCTAssertEqual(customData["favourite_colour"] as! String, "green") + + XCTAssertEqual(user.customData["favourite_colour"], .string("green")) + XCTAssertEqual(user.customData["apples"], .int64(10)) } - func assertThrowsError( - _ expression: @autoclosure () async throws -> T, - file: StaticString = #filePath, line: UInt = #line, - _ errorHandler: (_ error: E) -> Void - ) async { - do { - _ = try await expression() - XCTFail("Expression should have thrown an error", file: file, line: line) - } catch let error as E { - errorHandler(error) - } catch { - XCTFail("Expected error of type \(E.self) but got \(error)") - } + // MARK: User Profile + + func testUserProfileInitialization() { + let profile = UserProfile() + XCTAssertNil(profile.name) + XCTAssertNil(profile.maxAge) + XCTAssertNil(profile.minAge) + XCTAssertNil(profile.birthday) + XCTAssertNil(profile.gender) + XCTAssertNil(profile.firstName) + XCTAssertNil(profile.lastName) + XCTAssertNil(profile.pictureURL) + XCTAssertEqual(profile.metadata, [:]) } - @MainActor func testAsyncOpenStandalone() async throws { - try autoreleasepool { - let configuration = Realm.Configuration(objectTypes: [SwiftPerson.self]) - let realm = try Realm(configuration: configuration) - try realm.write { - (0..<10).forEach { _ in realm.add(SwiftPerson(firstName: "Charlie", lastName: "Bucket")) } + // MARK: Seed file path + + func testSeedFilePathOpenLocalToSync() throws { + var config = Realm.Configuration() + config.fileURL = RLMTestRealmURL() + config.objectTypes = [SwiftHugeSyncObject.self] + let realm = try Realm(configuration: config) + try realm.write { + for _ in 0..=5.8) - // A custom executor which cancels the task after the requested number of - // invocations. This is a very naive executor which just synchronously - // invokes jobs, which generally is not a legal thing to do - final class CancellingExecutor: SerialExecutor, @unchecked Sendable { - private var remaining: Locked - private var pendingJob: UnownedJob? - var task: Task? { - didSet { - if let pendingJob = pendingJob { - self.pendingJob = nil - enqueue(pendingJob) - } + // MARK: Write Copy For Configuration + + func testWriteCopySyncedRealm() throws { + // user1 creates and writeCopies a realm to be opened by another user + var config = try configuration() + config.objectTypes = [SwiftHugeSyncObject.self] + let syncedRealm = try Realm(configuration: config) + try syncedRealm.write { + for _ in 0.. UnownedSerialExecutor { - UnownedSerialExecutor(ordinary: self) - } - } + var syncConfig = try configuration() + syncConfig.objectTypes = [SwiftPerson.self] - // An actor that does nothing other than have a custom executor - actor CustomExecutorActor { - nonisolated let executor: UnownedSerialExecutor - init(_ executor: UnownedSerialExecutor) { - self.executor = executor - } - nonisolated var unownedExecutor: UnownedSerialExecutor { - executor + let localRealm = try Realm(configuration: localConfig) + try localRealm.write { + localRealm.add(SwiftPerson(firstName: "John", lastName: "Doe")) } - } - @MainActor func testAsyncOpenTaskCancellation() async throws { - // Populate the Realm on the server - let user = try await self.app.login(credentials: basicCredentials()) - let configuration = user.configuration(testName: #function) - try await Task { @MainActor in - let realm = try await Realm(configuration: configuration) - try realm.write { - realm.add(SwiftHugeSyncObject.create()) - realm.add(SwiftHugeSyncObject.create()) - } - waitForUploads(for: realm) - }.value + try localRealm.writeCopy(configuration: syncConfig) - func isolatedOpen(_ actor: isolated CustomExecutorActor) async throws { - _ = try await Realm(configuration: configuration, actor: actor, downloadBeforeOpen: .always) - } + let syncedRealm = try Realm(configuration: syncConfig) + XCTAssertEqual(syncedRealm.objects(SwiftPerson.self).count, 1) + waitForDownloads(for: syncedRealm) - // Try opening the Realm with the Task being cancelled at every possible - // point between executor invocations. This doesn't really test that - // cancellation is *correct*; just that cancellation never results in - // a hang or crash. - for i in 0 ..< .max { - RLMWaitForRealmToClose(configuration.fileURL!.path) - _ = try Realm.deleteFiles(for: configuration) - - let executor = CancellingExecutor(cancelAfter: i) - executor.task = Task { - try await isolatedOpen(.init(executor.asUnownedSerialExecutor())) - } - do { - try await executor.task!.value - break - } catch is CancellationError { - // pass - } catch { - XCTFail("Expected CancellationError but got \(error)") - } + try syncedRealm.write { + syncedRealm.add(SwiftPerson(firstName: "Jane", lastName: "Doe")) } - // Repeat the above, but with a cached Realm so that we hit that code path instead - let cachedRealm = try await Realm(configuration: configuration, downloadBeforeOpen: .always) - for i in 0 ..< .max { - let executor = CancellingExecutor(cancelAfter: i) - executor.task = Task { - try await isolatedOpen(.init(executor.asUnownedSerialExecutor())) - } - do { - try await executor.task!.value - break - } catch is CancellationError { - // pass - } catch { - XCTFail("Expected CancellationError but got \(error)") - } - } - cachedRealm.invalidate() + waitForUploads(for: syncedRealm) + let syncedResults = syncedRealm.objects(SwiftPerson.self) + XCTAssertEqual(syncedResults.where { $0.firstName == "John" }.count, 1) + XCTAssertEqual(syncedResults.where { $0.firstName == "Jane" }.count, 1) } -#endif - func testCallResetPasswordAsyncAwait() async throws { - let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - let password = randomString(10) - try await app.emailPasswordAuth.registerUser(email: email, password: password) - let auth = app.emailPasswordAuth - await assertThrowsError(try await auth.callResetPasswordFunction(email: email, - password: randomString(10), - args: [[:]])) { - assertAppError($0, .unknown, "failed to reset password for user \"\(email)\"") - } - } + func testWriteCopySynedRealmToLocal() throws { + var syncConfig = try configuration() + syncConfig.objectTypes = [SwiftPerson.self] + let syncedRealm = try Realm(configuration: syncConfig) + waitForDownloads(for: syncedRealm) - func testAppLinkUserAsyncAwait() async throws { - let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - let password = randomString(10) - try await app.emailPasswordAuth.registerUser(email: email, password: password) + try syncedRealm.write { + syncedRealm.add(SwiftPerson(firstName: "Jane", lastName: "Doe")) + } + waitForUploads(for: syncedRealm) + XCTAssertEqual(syncedRealm.objects(SwiftPerson.self).count, 1) - let syncUser = try await self.app.login(credentials: Credentials.anonymous) + var localConfig = Realm.Configuration() + localConfig.objectTypes = [SwiftPerson.self] + localConfig.fileURL = realmURLForFile("test.realm") + // `realm_id` will be removed in the local realm, so we need to bump + // the schema version. + localConfig.schemaVersion = 1 - let credentials = Credentials.emailPassword(email: email, password: password) - let linkedUser = try await syncUser.linkUser(credentials: credentials) - XCTAssertEqual(linkedUser.id, app.currentUser?.id) - XCTAssertEqual(linkedUser.identities.count, 2) - } + try syncedRealm.writeCopy(configuration: localConfig) - func testUserCallFunctionAsyncAwait() async throws { - let user = try await self.app.login(credentials: basicCredentials()) - guard case let .int32(sum) = try await user.functions.sum([1, 2, 3, 4, 5]) else { - return XCTFail("Should be int32") + let localRealm = try Realm(configuration: localConfig) + try localRealm.write { + localRealm.add(SwiftPerson(firstName: "John", lastName: "Doe")) } - XCTAssertEqual(sum, 15) - } - - // MARK: - Objective-C async await - func testPushRegistrationAsyncAwait() async throws { - let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - let password = randomString(10) - try await app.emailPasswordAuth.registerUser(email: email, password: password) - - _ = try await app.login(credentials: Credentials.emailPassword(email: email, password: password)) - let client = app.pushClient(serviceName: "gcm") - try await client.registerDevice(token: "some-token", user: app.currentUser!) - try await client.deregisterDevice(user: app.currentUser!) + let results = localRealm.objects(SwiftPerson.self) + XCTAssertEqual(results.where { $0.firstName == "John" }.count, 1) + XCTAssertEqual(results.where { $0.firstName == "Jane" }.count, 1) } - func testEmailPasswordProviderClientAsyncAwait() async throws { - let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - let password = randomString(10) - let auth = app.emailPasswordAuth - try await auth.registerUser(email: email, password: password) + func testWriteCopyLocalRealmForSyncWithExistingData() throws { + var initialSyncConfig = try configuration() + initialSyncConfig.objectTypes = [SwiftPerson.self] - await assertThrowsError(try await auth.confirmUser("atoken", tokenId: "atokenid")) { - assertAppError($0, .badRequest, "invalid token data") - } - await assertThrowsError(try await auth.resendConfirmationEmail(email)) { - assertAppError($0, .userAlreadyConfirmed, "already confirmed") - } - await assertThrowsError(try await auth.retryCustomConfirmation(email)) { - assertAppError($0, .unknown, - "cannot run confirmation for \(email): automatic confirmation is enabled") - } - await assertThrowsError(try await auth.sendResetPasswordEmail("atoken")) { - assertAppError($0, .userNotFound, "user not found") + // Make sure objects with confliciting primary keys sync ok. + let conflictingObjectId = ObjectId.generate() + let person = SwiftPerson(firstName: "Foo", lastName: "Bar") + person._id = conflictingObjectId + let initialRealm = try Realm(configuration: initialSyncConfig) + try initialRealm.write { + initialRealm.add(person) + initialRealm.add(SwiftPerson(firstName: "Foo2", lastName: "Bar2")) } - } + waitForUploads(for: initialRealm) - func testUserAPIKeyProviderClientAsyncAwait() async throws { - let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - let password = randomString(10) - try await app.emailPasswordAuth.registerUser(email: email, password: password) + var localConfig = Realm.Configuration() + localConfig.objectTypes = [SwiftPerson.self] + localConfig.fileURL = realmURLForFile("test.realm") - let credentials = Credentials.emailPassword(email: email, password: password) - let syncUser = try await self.app.login(credentials: credentials) - let apiKey = try await syncUser.apiKeysAuth.createAPIKey(named: "my-api-key") - XCTAssertNotNil(apiKey) + var syncConfig = try configuration() + syncConfig.objectTypes = [SwiftPerson.self] + + let localRealm = try Realm(configuration: localConfig) + // `person2` will override what was previously stored on the server. + let person2 = SwiftPerson(firstName: "John", lastName: "Doe") + person2._id = conflictingObjectId + try localRealm.write { + localRealm.add(person2) + localRealm.add(SwiftPerson(firstName: "Foo3", lastName: "Bar3")) + } - let fetchedApiKey = try await syncUser.apiKeysAuth.fetchAPIKey(apiKey.objectId) - XCTAssertNotNil(fetchedApiKey) + try localRealm.writeCopy(configuration: syncConfig) - let fetchedApiKeys = try await syncUser.apiKeysAuth.fetchAPIKeys() - XCTAssertNotNil(fetchedApiKeys) - XCTAssertEqual(fetchedApiKeys.count, 1) + let syncedRealm = try Realm(configuration: syncConfig) + waitForDownloads(for: syncedRealm) + XCTAssertTrue(syncedRealm.objects(SwiftPerson.self).count == 3) - try await syncUser.apiKeysAuth.disableAPIKey(apiKey.objectId) - try await syncUser.apiKeysAuth.enableAPIKey(apiKey.objectId) - try await syncUser.apiKeysAuth.deleteAPIKey(apiKey.objectId) + try syncedRealm.write { + syncedRealm.add(SwiftPerson(firstName: "Jane", lastName: "Doe")) + } - let newFetchedApiKeys = try await syncUser.apiKeysAuth.fetchAPIKeys() - XCTAssertNotNil(newFetchedApiKeys) - XCTAssertEqual(newFetchedApiKeys.count, 0) + waitForUploads(for: syncedRealm) + let syncedResults = syncedRealm.objects(SwiftPerson.self) + XCTAssertEqual(syncedResults.where { + $0.firstName == "John" && + $0.lastName == "Doe" && + $0._id == conflictingObjectId + }.count, 1) + XCTAssertTrue(syncedRealm.objects(SwiftPerson.self).count == 4) } - func testCustomUserDataAsyncAwait() async throws { - let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - let password = randomString(10) - try await app.emailPasswordAuth.registerUser(email: email, password: password) + func testWriteCopyFailBeforeSynced() throws { + var user1Config = try configuration() + user1Config.objectTypes = [SwiftPerson.self] + let user1Realm = try Realm(configuration: user1Config) + // Suspend the session so that changes cannot be uploaded + user1Realm.syncSession?.suspend() + try user1Realm.write { + user1Realm.add(SwiftPerson()) + } - let user = try await self.app.login(credentials: .anonymous) - XCTAssertNotNil(user) + var user2Config = try configuration() + user2Config.objectTypes = [SwiftPerson.self] + let pathOnDisk = ObjectiveCSupport.convert(object: user2Config).pathOnDisk + XCTAssertFalse(FileManager.default.fileExists(atPath: pathOnDisk)) - _ = try await user.functions.updateUserData([ - ["favourite_colour": "green", "apples": 10] - ]) + let realm = try Realm(configuration: user1Config) + realm.syncSession?.suspend() + try realm.write { + realm.add(SwiftPerson()) + } - try await app.currentUser?.refreshCustomData() - XCTAssertEqual(app.currentUser?.customData["favourite_colour"], .string("green")) - XCTAssertEqual(app.currentUser?.customData["apples"], .int64(10)) + // Changes have yet to be uploaded so expect an exception + XCTAssertThrowsError(try realm.writeCopy(configuration: user2Config)) { error in + XCTAssertEqual(error.localizedDescription, "All client changes must be integrated in server before writing copy") + } } - func testDeleteUserAsyncAwait() async throws { - let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - let password = randomString(10) - let credentials: Credentials = .emailPassword(email: email, password: password) - try await app.emailPasswordAuth.registerUser(email: email, password: password) + func testServerSchemaValidationWithCustomColumnNames() throws { + let appId = try RealmServer.shared.createApp(types: [SwiftCustomColumnObject.self]) + let className = "SwiftCustomColumnObject \(appId)" + RealmServer.shared.retrieveSchemaProperties(appId, className: className) { result in + switch result { + case .failure(let error): + XCTFail("Couldn't retrieve schema properties for \(className): \(error)") + case .success(let properties): + for (_, value) in customColumnPropertiesMapping { + XCTAssertTrue(properties.contains(where: { $0 == value })) + } + } + } + } - let user = try await self.app.login(credentials: credentials) - XCTAssertNotNil(user) + func testVerifyDocumentsWithCustomColumnNames() throws { + let collection = try setupMongoCollection(for: SwiftCustomColumnObject.self) + let objectId = ObjectId.generate() + let linkedObjectId = ObjectId.generate() - XCTAssertNotNil(app.currentUser) - try await user.delete() + try write { realm in + let object = SwiftCustomColumnObject() + object.id = objectId + let linkedObject = SwiftCustomColumnObject() + linkedObject.id = linkedObjectId + object.objectCol = linkedObject + realm.add(object) + } + waitForCollectionCount(collection, 2) - XCTAssertNil(app.currentUser) - XCTAssertEqual(app.allUsers.count, 0) + let filter: Document = ["_id": .objectId(objectId)] + collection.findOneDocument(filter: filter) + .await(self) { document in + XCTAssertNotNil(document) + XCTAssertEqual(document?["_id"]??.objectIdValue, objectId) + XCTAssertNil(document?["id"] as Any?) + XCTAssertEqual(document?["custom_boolCol"]??.boolValue, true) + XCTAssertNil(document?["boolCol"] as Any?) + XCTAssertEqual(document?["custom_intCol"]??.int64Value, 1) + XCTAssertNil(document?["intCol"] as Any?) + XCTAssertEqual(document?["custom_stringCol"]??.stringValue, "string") + XCTAssertNil(document?["stringCol"] as Any?) + XCTAssertEqual(document?["custom_decimalCol"]??.decimal128Value, Decimal128(1)) + XCTAssertNil(document?["decimalCol"] as Any?) + XCTAssertEqual(document?["custom_objectCol"]??.objectIdValue, linkedObjectId) + XCTAssertNil(document?["objectCol"] as Any?) + } } - /// If client B adds objects to a Realm, client A should see those new objects. - func testSwiftAddObjectsAsync() async throws { - let user = try await logInUser(for: basicCredentials()) - let realm = try openRealm(partitionValue: #function, user: user) - if isParent { - checkCount(expected: 0, realm, SwiftPerson.self) - checkCount(expected: 0, realm, SwiftTypesSyncObject.self) - executeChild() - try await realm.syncSession?.wait(for: .download) - checkCount(expected: 4, realm, SwiftPerson.self) - checkCount(expected: 1, realm, SwiftTypesSyncObject.self) - - let obj = realm.objects(SwiftTypesSyncObject.self).first! - XCTAssertEqual(obj.boolCol, true) - XCTAssertEqual(obj.intCol, 1) - XCTAssertEqual(obj.doubleCol, 1.1) - XCTAssertEqual(obj.stringCol, "string") - XCTAssertEqual(obj.binaryCol, "string".data(using: String.Encoding.utf8)!) - XCTAssertEqual(obj.decimalCol, Decimal128(1)) - XCTAssertEqual(obj.dateCol, Date(timeIntervalSince1970: -1)) - XCTAssertEqual(obj.longCol, Int64(1)) - XCTAssertEqual(obj.uuidCol, UUID(uuidString: "85d4fbee-6ec6-47df-bfa1-615931903d7e")!) - XCTAssertEqual(obj.anyCol.intValue, 1) - XCTAssertEqual(obj.objectCol!.firstName, "George") - - } else { - // Add objects + /// The purpose of this test is to confirm that when an Object is set on a mixed Column and an old + /// version of an app does not have that Realm Object / Schema we can still access that object via + /// `AnyRealmValue.dynamicSchema`. + func testMissingSchema() throws { + try autoreleasepool { + // Imagine this is v2 of an app with 3 classes + var config = createUser().configuration(partitionValue: name) + config.objectTypes = [SwiftPerson.self, SwiftAnyRealmValueObject.self, SwiftMissingObject.self] + let realm = try openRealm(configuration: config) try realm.write { - realm.add(SwiftPerson(firstName: "Ringo", lastName: "Starr")) - realm.add(SwiftPerson(firstName: "John", lastName: "Lennon")) - realm.add(SwiftPerson(firstName: "Paul", lastName: "McCartney")) - realm.add(SwiftTypesSyncObject(person: SwiftPerson(firstName: "George", lastName: "Harrison"))) + let so1 = SwiftPerson() + so1.firstName = "Rick" + so1.lastName = "Sanchez" + let so2 = SwiftPerson() + so2.firstName = "Squidward" + so2.lastName = "Tentacles" + + let syncObj2 = SwiftMissingObject() + syncObj2.objectCol = so1 + syncObj2.anyCol = .object(so1) + + let syncObj = SwiftMissingObject() + syncObj.objectCol = so1 + syncObj.anyCol = .object(syncObj2) + let obj = SwiftAnyRealmValueObject() + obj.anyCol = .object(syncObj) + obj.otherAnyCol = .object(so2) + realm.add(obj) } - try await realm.syncSession?.wait(for: .upload) - checkCount(expected: 4, realm, SwiftPerson.self) - checkCount(expected: 1, realm, SwiftTypesSyncObject.self) + waitForUploads(for: realm) } + + // Imagine this is v1 of an app with just 2 classes, `SwiftMissingObject` + // did not exist when this version was shipped, + // but v2 managed to sync `SwiftMissingObject` to this Realm. + var config = createUser().configuration(partitionValue: name) + config.objectTypes = [SwiftAnyRealmValueObject.self, SwiftPerson.self] + let realm = try openRealm(configuration: config) + let obj = realm.objects(SwiftAnyRealmValueObject.self).first + // SwiftMissingObject.anyCol -> SwiftMissingObject.anyCol -> SwiftPerson.firstName + let anyCol = ((obj!.anyCol.dynamicObject?.anyCol as? Object)?["anyCol"] as? Object) + XCTAssertEqual((anyCol?["firstName"] as? String), "Rick") + try realm.write { + anyCol?["firstName"] = "Morty" + } + XCTAssertEqual((anyCol?["firstName"] as? String), "Morty") + let objectCol = (obj!.anyCol.dynamicObject?.objectCol as? Object) + XCTAssertEqual((objectCol?["firstName"] as? String), "Morty") } } diff --git a/Realm/ObjectServerTests/SwiftSyncTestCase.swift b/Realm/ObjectServerTests/SwiftSyncTestCase.swift index 44fb3695f8..10abf94250 100644 --- a/Realm/ObjectServerTests/SwiftSyncTestCase.swift +++ b/Realm/ObjectServerTests/SwiftSyncTestCase.swift @@ -28,18 +28,6 @@ import RealmSyncTestSupport import RealmSwiftTestSupport #endif -public extension User { - func configuration(testName: T) -> Realm.Configuration { - var config = self.configuration(partitionValue: testName) - config.objectTypes = [SwiftPerson.self, SwiftHugeSyncObject.self, SwiftTypesSyncObject.self, SwiftCustomColumnObject.self] - return config - } - - func collection(for object: ObjectBase.Type) -> MongoCollection { - mongoClient("mongodb1").database(named: "test_data").collection(withName: object.className()) - } -} - public func randomString(_ length: Int) -> String { let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" return String((0.. Realm.Configuration { + user.configuration(partitionValue: self.name) + } + + open var objectTypes: [ObjectBase.Type] { + [SwiftPerson.self] + } + + override open func defaultObjectTypes() -> [AnyClass] { + objectTypes + } + public func executeChild(file: StaticString = #file, line: UInt = #line) { XCTAssert(0 == runChildAndWait(), "Tests in child process failed", file: file, line: line) } @@ -76,56 +77,41 @@ open class SwiftSyncTestCase: RLMSyncTestCase { XCTAssertNil(error) ex.fulfill() }) - waitForExpectations(timeout: 40, handler: nil) + wait(for: [ex], timeout: 4) return credentials } - public func openRealm(partitionValue: AnyBSON, user: User) throws -> Realm { - let config = user.configuration(partitionValue: partitionValue) - return try openRealm(configuration: config) + public func openRealm(app: App? = nil, wait: Bool = true) throws -> Realm { + let realm = try Realm(configuration: configuration(app: app)) + if wait { + waitForDownloads(for: realm) + } + return realm } - public func openRealm(partitionValue: T, - user: User, - clientResetMode: ClientResetMode? = .recoverUnsyncedChanges(), - file: StaticString = #file, - line: UInt = #line) throws -> Realm { - let config: Realm.Configuration - if clientResetMode != nil { - config = user.configuration(partitionValue: partitionValue, clientResetMode: clientResetMode!) - } else { - config = user.configuration(partitionValue: partitionValue) - } - return try openRealm(configuration: config) + public func configuration(app: App? = nil) throws -> Realm.Configuration { + let user = try createUser(app: app) + var config = configuration(user: user) + config.objectTypes = self.objectTypes + return config } public func openRealm(configuration: Realm.Configuration) throws -> Realm { - var configuration = configuration - if configuration.objectTypes == nil { - configuration.objectTypes = [SwiftPerson.self, - SwiftHugeSyncObject.self, - SwiftCollectionSyncObject.self, - SwiftUUIDPrimaryKeyObject.self, - SwiftStringPrimaryKeyObject.self, - SwiftIntPrimaryKeyObject.self, - SwiftTypesSyncObject.self] - } - let realm = try Realm(configuration: configuration) - waitForDownloads(for: realm) - return realm + Realm.asyncOpen(configuration: configuration).await(self) } - public func immediatelyOpenRealm(partitionValue: String, user: User) throws -> Realm { - var configuration = user.configuration(partitionValue: partitionValue) - if configuration.objectTypes == nil { - configuration.objectTypes = [SwiftPerson.self, - SwiftHugeSyncObject.self, - SwiftTypesSyncObject.self] - } - return try Realm(configuration: configuration) + public func openRealm(user: User, partitionValue: String) throws -> Realm { + var config = user.configuration(partitionValue: partitionValue) + config.objectTypes = self.objectTypes + return try openRealm(configuration: config) } - open func logInUser(for credentials: Credentials, app: App? = nil) throws -> User { + public func createUser(app: App? = nil) throws -> User { + let app = app ?? self.app + return try logInUser(for: basicCredentials(app: app), app: app) + } + + public func logInUser(for credentials: Credentials, app: App? = nil) throws -> User { let user = (app ?? self.app).login(credentials: credentials).await(self, timeout: 60.0) XCTAssertTrue(user.isLoggedIn) return user @@ -139,6 +125,22 @@ open class SwiftSyncTestCase: RLMSyncTestCase { waitForDownloads(for: ObjectiveCSupport.convert(object: realm)) } + public func write(app: App? = nil, _ block: (Realm) throws -> Void) throws { + try autoreleasepool { + let realm = try openRealm(app: app) + RLMRealmSubscribeToAll(ObjectiveCSupport.convert(object: realm)) + + try realm.write { + try block(realm) + } + waitForUploads(for: realm) + + let syncSession = try XCTUnwrap(realm.syncSession) + syncSession.suspend() + syncSession.parentUser()?.remove().await(self) + } + } + public func checkCount(expected: Int, _ realm: Realm, _ type: T.Type, @@ -148,8 +150,8 @@ open class SwiftSyncTestCase: RLMSyncTestCase { let actual = realm.objects(type).count XCTAssertEqual(actual, expected, "Error: expected \(expected) items, but got \(actual) (process: \(isParent ? "parent" : "child"))", - file: file, - line: line) + file: file, + line: line) } var exceptionThrown = false @@ -173,93 +175,18 @@ open class SwiftSyncTestCase: RLMSyncTestCase { } public static let bigObjectCount = 2 - public func populateRealm(user: User? = nil, partitionValue: T) throws { - try autoreleasepool { - let user = try (user ?? logInUser(for: basicCredentials())) - let config = user.configuration(testName: partitionValue) - - let realm = try openRealm(configuration: config) - try realm.write { - for _ in 0.. Realm { - var config = user.flexibleSyncConfiguration() - if config.objectTypes == nil { - config.objectTypes = [SwiftPerson.self, - SwiftTypesSyncObject.self] - } - let realm = try Realm(configuration: config) - waitForDownloads(for: realm) - return realm - } - - public func openFlexibleSyncRealm() throws -> Realm { - let user = try logInUser(for: basicCredentials(app: self.flexibleSyncApp), app: self.flexibleSyncApp) - var config = user.flexibleSyncConfiguration() - if config.objectTypes == nil { - config.objectTypes = [SwiftPerson.self, - SwiftTypesSyncObject.self] - } - return try Realm(configuration: config) - } - - public func flexibleSyncRealm() throws -> Realm { - let user = try logInUser(for: basicCredentials(app: self.flexibleSyncApp), app: self.flexibleSyncApp) - return try openFlexibleSyncRealmForUser(user) - } - - public func populateFlexibleSyncData(_ block: @escaping (Realm) -> Void) throws { - try writeToFlxRealm { realm in - try realm.write { - block(realm) + public func populateRealm() throws { + try write { realm in + for _ in 0..(name: "all_people")) - } onComplete: { error in - XCTAssertNil(error) - expectation.fulfill() - } - wait(for: [expectation], timeout: 15.0) - } - - public func writeToFlxRealm(_ block: @escaping (Realm) throws -> Void) throws { - let realm = try flexibleSyncRealm() - let subscriptions = realm.subscriptions - XCTAssertNotNil(subscriptions) - let ex = expectation(description: "state change complete") - subscriptions.update({ - subscriptions.append(QuerySubscription()) - subscriptions.append(QuerySubscription()) - }, onComplete: { error in - XCTAssertNil(error) - ex.fulfill() - }) - XCTAssertEqual(subscriptions.count, 2) - - waitForExpectations(timeout: 20.0, handler: nil) - try block(realm) - } - // MARK: - Mongo Client - public func setupMongoCollection(user: User? = nil, for type: ObjectBase.Type) throws -> MongoCollection { - let u = try user ?? logInUser(for: basicCredentials()) - let collection = u.collection(for: type) + public func setupMongoCollection(for type: ObjectBase.Type) throws -> MongoCollection { + let collection = anonymousUser.collection(for: type, app: app) removeAllFromCollection(collection) return collection } @@ -277,15 +204,14 @@ open class SwiftSyncTestCase: RLMSyncTestCase { public func waitForCollectionCount(_ collection: MongoCollection, _ count: Int) { let waitStart = Date() - while collection.count(filter: [:]).await(self) != count && waitStart.timeIntervalSinceNow > -600.0 { + while collection.count(filter: [:]).await(self) < count && waitStart.timeIntervalSinceNow > -600.0 { sleep(1) } XCTAssertEqual(collection.count(filter: [:]).await(self), count) } -} -@available(macOS 12.0, *) -extension SwiftSyncTestCase { +#if swift(>=5.8) + // MARK: - Async helpers public func basicCredentials(usernameSuffix: String = "", app: App? = nil) async throws -> Credentials { let email = "\(randomString(10))\(usernameSuffix)" let password = "abcdef" @@ -294,23 +220,30 @@ extension SwiftSyncTestCase { return credentials } - // MARK: - Flexible Sync Async Use Cases - - public func flexibleSyncConfig() async throws -> Realm.Configuration { - var config = (try await self.flexibleSyncApp.login(credentials: basicCredentials(app: flexibleSyncApp))).flexibleSyncConfiguration() - if config.objectTypes == nil { - config.objectTypes = [SwiftPerson.self, - SwiftTypesSyncObject.self, - SwiftCustomColumnObject.self] - } - return config + @MainActor + @nonobjc public func openRealm() async throws -> Realm { + try await Realm(configuration: configuration(), downloadBeforeOpen: .always) } @MainActor - public func flexibleSyncRealm() async throws -> Realm { - let realm = try await Realm(configuration: flexibleSyncConfig()) - return realm + public func write(_ block: @escaping (Realm) throws -> Void) async throws { + try await Task { + let realm = try await openRealm() + try await realm.asyncWrite { + try block(realm) + } + let syncSession = try XCTUnwrap(realm.syncSession) + try await syncSession.wait(for: .upload) + syncSession.suspend() + try await syncSession.parentUser()?.remove() + }.value + } + + public func createUser(app: App? = nil) async throws -> User { + let credentials = try await basicCredentials(app: app) + return try await (app ?? self.app).login(credentials: credentials) } +#endif // swift(>=5.8) } @available(macOS 10.15, watchOS 6.0, iOS 13.0, tvOS 13.0, *) diff --git a/Realm/ObjectServerTests/SwiftUIServerTests.swift b/Realm/ObjectServerTests/SwiftUIServerTests.swift index 23e091e7eb..29dbaef54e 100644 --- a/Realm/ObjectServerTests/SwiftUIServerTests.swift +++ b/Realm/ObjectServerTests/SwiftUIServerTests.swift @@ -26,14 +26,17 @@ import RealmSwiftSyncTestSupport import RealmSyncTestSupport #endif -@available(OSX 11, *) +@available(macOS 13, *) @MainActor class SwiftUIServerTests: SwiftSyncTestCase { + override var objectTypes: [ObjectBase.Type] { + [SwiftHugeSyncObject.self] + } // Configuration for tests private func configuration(user: User, partition: T) -> Realm.Configuration { var userConfiguration = user.configuration(partitionValue: partition) - userConfiguration.objectTypes = [SwiftHugeSyncObject.self] + userConfiguration.objectTypes = self.objectTypes return userConfiguration } @@ -46,12 +49,11 @@ class SwiftUIServerTests: SwiftSyncTestCase { var cancellables: Set = [] // MARK: - AsyncOpen - func asyncOpen(user: User, appId: String? = nil, partitionValue: T, timeout: UInt? = nil, - handler: @escaping (AsyncOpenState) -> Void) { - let configuration = self.configuration(user: user, partition: partitionValue) + func asyncOpen(appId: String?, partitionValue: String, configuration: Realm.Configuration?, + timeout: UInt? = nil, handler: @escaping (AsyncOpenState) -> Void) { let asyncOpen = AsyncOpen(appId: appId, partitionValue: partitionValue, - configuration: configuration, + configuration: configuration ?? Realm.Configuration(objectTypes: self.objectTypes), timeout: timeout) _ = asyncOpen.wrappedValue // Retrieving the wrappedValue to simulate a SwiftUI environment where this is called when initialising the view. asyncOpen.projectedValue @@ -61,11 +63,24 @@ class SwiftUIServerTests: SwiftSyncTestCase { asyncOpen.cancel() } - func testAsyncOpenOpenRealm() throws { - let user = try logInUser(for: basicCredentials()) + func asyncOpen(user: User, appId: String?, partitionValue: String, timeout: UInt? = nil, + handler: @escaping (AsyncOpenState) -> Void) { + let configuration = self.configuration(user: user, partition: partitionValue) + asyncOpen(appId: appId, + partitionValue: partitionValue, + configuration: configuration, + timeout: timeout, + handler: handler) + } + + func asyncOpen(handler: @escaping (AsyncOpenState) -> Void) throws { + asyncOpen(appId: appId, partitionValue: self.name, configuration: try configuration(), + handler: handler) + } + func testAsyncOpenOpenRealm() throws { let ex = expectation(description: "download-realm-async-open") - asyncOpen(user: user, appId: appId, partitionValue: #function) { asyncOpenState in + try asyncOpen { asyncOpenState in if case .open = asyncOpenState { ex.fulfill() } @@ -73,14 +88,9 @@ class SwiftUIServerTests: SwiftSyncTestCase { } func testAsyncOpenDownloadRealm() throws { - let user = try logInUser(for: basicCredentials()) - if !isParent { - return try populateRealm(user: user, partitionValue: #function) - } - executeChild() - + try populateRealm() let ex = expectation(description: "download-populated-realm-async-open") - asyncOpen(user: user, appId: appId, partitionValue: #function) { asyncOpenState in + try asyncOpen { asyncOpenState in if case let .open(realm) = asyncOpenState { self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) ex.fulfill() @@ -89,11 +99,11 @@ class SwiftUIServerTests: SwiftSyncTestCase { } func testAsyncOpenWaitingForUserWithoutUserLoggedIn() throws { - let user = try logInUser(for: basicCredentials()) - user.logOut { _ in } // Logout current user + let user = createUser() + user.logOut().await(self) let ex = expectation(description: "download-realm-async-open-not-logged") - asyncOpen(user: user, appId: appId, partitionValue: #function) { asyncOpenState in + asyncOpen(user: user, appId: appId, partitionValue: name) { asyncOpenState in if case .waitingForUser = asyncOpenState { ex.fulfill() } @@ -105,15 +115,16 @@ class SwiftUIServerTests: SwiftSyncTestCase { let proxy = TimeoutProxyServer(port: 5678, targetPort: 9090) try proxy.start() - let appId = try RealmServer.shared.createApp() + let appId = try RealmServer.shared.createApp(types: self.objectTypes) let appConfig = AppConfiguration(baseURL: "http://localhost:5678", transport: AsyncOpenConnectionTimeoutTransport()) let app = App(id: appId, configuration: appConfig) - let user = try logInUser(for: basicCredentials(app: app), app: app) + let user = try createUser(app: app) proxy.dropConnections = true let ex = expectation(description: "download-realm-async-open-no-connection") - asyncOpen(user: user, appId: appId, partitionValue: #function, timeout: 1000) { asyncOpenState in + asyncOpen(user: user, appId: appId, + partitionValue: name, timeout: 1000) { asyncOpenState in if case let .error(error) = asyncOpenState, let nsError = error as NSError? { XCTAssertEqual(nsError.code, Int(ETIMEDOUT)) @@ -123,19 +134,13 @@ class SwiftUIServerTests: SwiftSyncTestCase { } proxy.stop() - try RealmServer.shared.deleteApp(appId) } @MainActor func testAsyncOpenProgressNotification() throws { - let user = try logInUser(for: basicCredentials()) - if !isParent { - return try populateRealm(user: user, partitionValue: #function) - } - executeChild() - + try populateRealm() let ex = expectation(description: "progress-async-open") - asyncOpen(user: user, appId: appId, partitionValue: #function) { asyncOpenState in + try asyncOpen { asyncOpenState in if case let .progress(progress) = asyncOpenState { XCTAssertTrue(progress.fractionCompleted > 0) if progress.isFinished { @@ -147,14 +152,9 @@ class SwiftUIServerTests: SwiftSyncTestCase { // Cached App is already created on the setup of the test func testAsyncOpenWithACachedApp() throws { - let user = try logInUser(for: basicCredentials()) - if !isParent { - return try populateRealm(user: user, partitionValue: #function) - } - executeChild() - + try populateRealm() let ex = expectation(description: "download-cached-app-async-open") - asyncOpen(user: user, partitionValue: #function) { asyncOpenState in + asyncOpen(user: createUser(), appId: nil, partitionValue: name) { asyncOpenState in if case let .open(realm) = asyncOpenState { self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) ex.fulfill() @@ -164,29 +164,23 @@ class SwiftUIServerTests: SwiftSyncTestCase { func testAsyncOpenThrowExceptionWithoutCachedApp() throws { resetAppCache() - assertThrows(AsyncOpen(partitionValue: #function), + assertThrows(AsyncOpen(partitionValue: name), reason: "Cannot AsyncOpen the Realm because no appId was found. You must either explicitly pass an appId or initialize an App before displaying your View.") } func testAsyncOpenThrowExceptionWithMoreThanOneCachedApp() throws { _ = App(id: "fake 1") _ = App(id: "fake 2") - assertThrows(AsyncOpen(partitionValue: #function), + assertThrows(AsyncOpen(partitionValue: name), reason: "Cannot AsyncOpen the Realm because more than one appId was found. When using multiple Apps you must explicitly pass an appId to indicate which to use.") } func testAsyncOpenWithDifferentPartitionValues() throws { - let partitionValueA = #function - let partitionValueB = "\(#function)bar" - - let user = try logInUser(for: basicCredentials()) - if !isParent { - return try populateRealm(user: user, partitionValue: partitionValueA) - } - executeChild() + try populateRealm() + let emptyPartition = "\(name) empty partition" let ex = expectation(description: "download-partition-value-async-open") - asyncOpen(user: user, partitionValue: partitionValueA) { asyncOpenState in + try asyncOpen { asyncOpenState in if case let .open(realm) = asyncOpenState { self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) ex.fulfill() @@ -194,7 +188,7 @@ class SwiftUIServerTests: SwiftSyncTestCase { } let ex2 = expectation(description: "download-other-partition-value-async-open") - asyncOpen(user: user, partitionValue: partitionValueB) { asyncOpenState in + asyncOpen(user: createUser(), appId: nil, partitionValue: emptyPartition) { asyncOpenState in if case let .open(realm) = asyncOpenState { self.checkCount(expected: 0, realm, SwiftHugeSyncObject.self) ex2.fulfill() @@ -203,63 +197,52 @@ class SwiftUIServerTests: SwiftSyncTestCase { } func testAsyncOpenWithMultiUserApp() throws { - let partitionValueA = #function - let partitionValueB = "\(#function)bar" + try populateRealm() - let syncUser1 = try logInUser(for: basicCredentials()) - let syncUser2 = try logInUser(for: basicCredentials()) + let syncUser1 = createUser() + let syncUser2 = createUser() XCTAssertEqual(app.allUsers.count, 2) - XCTAssertEqual(syncUser2.id, app.currentUser!.id) - - if !isParent { - return try populateRealm(user: syncUser1, partitionValue: partitionValueA) - } - executeChild() + XCTAssertEqual(syncUser2.id, app.currentUser?.id) let ex = expectation(description: "test-multiuser1-app-async-open") - asyncOpen(user: syncUser2, appId: appId, partitionValue: partitionValueB) { asyncOpenState in + asyncOpen(user: syncUser2, appId: appId, partitionValue: name) { asyncOpenState in if case let .open(realm) = asyncOpenState { - self.checkCount(expected: 0, realm, SwiftHugeSyncObject.self) + self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) + XCTAssertEqual(realm.syncSession?.parentUser(), syncUser2) ex.fulfill() } } app.switch(to: syncUser1) XCTAssertEqual(app.allUsers.count, 2) - XCTAssertEqual(syncUser1.id, app.currentUser!.id) + XCTAssertEqual(syncUser1.id, app.currentUser?.id) let ex2 = expectation(description: "test-multiuser2-app-async-open") - asyncOpen(user: syncUser2, appId: appId, partitionValue: partitionValueA) { asyncOpenState in + asyncOpen(user: syncUser1, appId: appId, partitionValue: name) { asyncOpenState in if case let .open(realm) = asyncOpenState { self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) + XCTAssertEqual(realm.syncSession?.parentUser(), syncUser1) ex2.fulfill() } } } func testAsyncOpenWithUserAfterLogoutFromAnonymous() throws { - let partitionValueA = #function - let partitionValueB = "\(#function)bar" + try populateRealm() - let user = try logInUser(for: basicCredentials()) - if !isParent { - return try populateRealm(user: user, partitionValue: partitionValueB) - } - executeChild() - - let anonymousUser = try logInUser(for: .anonymous) + let anonymousUser = self.anonymousUser let ex = expectation(description: "download-realm-anonymous-user-async-open") - asyncOpen(user: anonymousUser, appId: appId, partitionValue: partitionValueA) { asyncOpenState in + asyncOpen(user: anonymousUser, appId: appId, partitionValue: name) { asyncOpenState in if case let .open(realm) = asyncOpenState { - self.checkCount(expected: 0, realm, SwiftHugeSyncObject.self) + self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) ex.fulfill() } } - app.currentUser?.logOut { _ in } // Logout anonymous user + anonymousUser.logOut().await(self) let ex2 = expectation(description: "download-realm-after-logout-async-open") - asyncOpen(user: user, appId: appId, partitionValue: partitionValueB) { asyncOpenState in + asyncOpen(user: createUser(), appId: appId, partitionValue: name) { asyncOpenState in if case let .open(realm) = asyncOpenState { self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) ex2.fulfill() @@ -268,8 +251,8 @@ class SwiftUIServerTests: SwiftSyncTestCase { } // MARK: - AutoOpen - func autoOpen(user: User, appId: String? = nil, partitionValue: String, timeout: UInt? = nil, handler: @escaping (AsyncOpenState) -> Void) { - let configuration = self.configuration(user: user, partition: partitionValue) + func autoOpen(appId: String?, partitionValue: String, configuration: Realm.Configuration?, + timeout: UInt?, handler: @escaping (AsyncOpenState) -> Void) { let autoOpen = AutoOpen(appId: appId, partitionValue: partitionValue, configuration: configuration, @@ -282,11 +265,24 @@ class SwiftUIServerTests: SwiftSyncTestCase { autoOpen.cancel() } - func testAutoOpenOpenRealm() throws { - let user = try logInUser(for: basicCredentials()) + func autoOpen(user: User, appId: String?, partitionValue: String, timeout: UInt? = nil, + handler: @escaping (AsyncOpenState) -> Void) { + let configuration = self.configuration(user: user, partition: partitionValue) + autoOpen(appId: appId, + partitionValue: partitionValue, + configuration: configuration, + timeout: timeout, + handler: handler) + } + + func autoOpen(handler: @escaping (AsyncOpenState) -> Void) throws { + autoOpen(appId: appId, partitionValue: self.name, configuration: try configuration(), + timeout: nil, handler: handler) + } + func testAutoOpenOpenRealm() throws { let ex = expectation(description: "download-realm-auto-open") - autoOpen(user: user, appId: appId, partitionValue: #function) { autoOpenState in + try autoOpen { autoOpenState in if case .open = autoOpenState { ex.fulfill() } @@ -294,15 +290,9 @@ class SwiftUIServerTests: SwiftSyncTestCase { } func testAutoOpenDownloadRealm() throws { - let user = try logInUser(for: basicCredentials()) - - if !isParent { - return try populateRealm(user: user, partitionValue: #function) - } - executeChild() - + try populateRealm() let ex = expectation(description: "download-populated-realm-auto-open") - autoOpen(user: user, appId: appId, partitionValue: #function) { autoOpenState in + try autoOpen { autoOpenState in if case let .open(realm) = autoOpenState { self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) ex.fulfill() @@ -313,10 +303,10 @@ class SwiftUIServerTests: SwiftSyncTestCase { @MainActor func testAutoOpenWaitingForUserWithoutUserLoggedIn() throws { let user = try logInUser(for: basicCredentials()) - user.logOut { _ in } // Logout current user + user.logOut().await(self) let ex = expectation(description: "download-realm-auto-open-not-logged") - autoOpen(user: user, appId: appId, partitionValue: #function) { autoOpenState in + autoOpen(user: user, appId: appId, partitionValue: name) { autoOpenState in if case .waitingForUser = autoOpenState { ex.fulfill() } @@ -325,10 +315,7 @@ class SwiftUIServerTests: SwiftSyncTestCase { // In case of no internet connection AutoOpen should return an opened Realm, offline-first approach func testAutoOpenOpenRealmWithoutInternetConnection() throws { - try autoreleasepool { - let user = try logInUser(for: basicCredentials(app: self.app), app: self.app) - try populateRealm(user: user, partitionValue: #function) - } + try populateRealm() resetAppCache() let proxy = TimeoutProxyServer(port: 5678, targetPort: 9090) @@ -336,10 +323,10 @@ class SwiftUIServerTests: SwiftSyncTestCase { let appConfig = AppConfiguration(baseURL: "http://localhost:5678", transport: AsyncOpenConnectionTimeoutTransport()) let app = App(id: appId, configuration: appConfig) - let user = try logInUser(for: basicCredentials(app: app), app: app) + let user = try createUser(app: app) proxy.dropConnections = true let ex = expectation(description: "download-realm-auto-open-no-connection") - autoOpen(user: user, appId: appId, partitionValue: #function, timeout: 1000) { autoOpenState in + autoOpen(user: user, appId: appId, partitionValue: name, timeout: 1000) { autoOpenState in if case let .open(realm) = autoOpenState { XCTAssertTrue(realm.isEmpty) // should not have downloaded anything ex.fulfill() @@ -351,13 +338,14 @@ class SwiftUIServerTests: SwiftSyncTestCase { proxy.stop() } + #if false // In case of no internet connection AutoOpen should return an opened Realm, offline-first approach func testAutoOpenOpenForFlexibleSyncConfigWithoutInternetConnection() throws { try autoreleasepool { - try populateFlexibleSyncData { realm in + try write { realm in for i in 1...10 { // Using firstname to query only objects from this test - let person = SwiftPerson(firstName: "\(#function)", + let person = SwiftPerson(firstName: "\(name)", lastName: "lastname_\(i)", age: i) realm.add(person) @@ -396,16 +384,14 @@ class SwiftUIServerTests: SwiftSyncTestCase { App.resetAppCache() proxy.stop() } + #endif func testAutoOpenProgressNotification() throws { - try autoreleasepool { - let user = try logInUser(for: basicCredentials()) - try populateRealm(user: user, partitionValue: #function) - } + try populateRealm() - let user = try logInUser(for: basicCredentials()) + let user = createUser() let ex = expectation(description: "progress-auto-open") - autoOpen(user: user, appId: appId, partitionValue: #function) { autoOpenState in + autoOpen(user: user, appId: appId, partitionValue: name) { autoOpenState in if case let .progress(progress) = autoOpenState { XCTAssertTrue(progress.fractionCompleted > 0) if progress.isFinished { @@ -417,14 +403,10 @@ class SwiftUIServerTests: SwiftSyncTestCase { // App is already created on the setup of the test func testAutoOpenWithACachedApp() throws { - let user = try logInUser(for: basicCredentials()) - if !isParent { - return try populateRealm(user: user, partitionValue: #function) - } - executeChild() + try populateRealm() let ex = expectation(description: "download-cached-app-auto-open") - autoOpen(user: user, partitionValue: #function) { autoOpenState in + try autoOpen { autoOpenState in if case let .open(realm) = autoOpenState { self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) ex.fulfill() @@ -434,7 +416,7 @@ class SwiftUIServerTests: SwiftSyncTestCase { func testAutoOpenThrowExceptionWithoutCachedApp() throws { resetAppCache() - assertThrows(AutoOpen(partitionValue: #function), + assertThrows(AutoOpen(partitionValue: name), reason: "Cannot AsyncOpen the Realm because no appId was found. You must either explicitly pass an appId or initialize an App before displaying your View.") } @@ -442,25 +424,21 @@ class SwiftUIServerTests: SwiftSyncTestCase { func testAutoOpenThrowExceptionWithMoreThanOneCachedApp() throws { _ = App(id: "fake 1") _ = App(id: "fake 2") - assertThrows(AutoOpen(partitionValue: #function), + assertThrows(AutoOpen(partitionValue: name), reason: "Cannot AsyncOpen the Realm because more than one appId was found. When using multiple Apps you must explicitly pass an appId to indicate which to use.") } @MainActor func testAutoOpenWithMultiUserApp() throws { - let partitionValueA = #function - let partitionValueB = "\(#function)bar" + try populateRealm() + let partitionValueA = name + let partitionValueB = "\(name) 2" - let syncUser1 = try logInUser(for: basicCredentials()) - let syncUser2 = try logInUser(for: basicCredentials()) + let syncUser1 = createUser() + let syncUser2 = createUser() XCTAssertEqual(app.allUsers.count, 2) XCTAssertEqual(syncUser2.id, app.currentUser!.id) - if !isParent { - return try populateRealm(user: syncUser1, partitionValue: partitionValueA) - } - executeChild() - let ex = expectation(description: "test-multiuser1-app-auto-open") autoOpen(user: syncUser2, appId: appId, partitionValue: partitionValueB) { autoOpenState in if case let .open(realm) = autoOpenState { @@ -483,28 +461,23 @@ class SwiftUIServerTests: SwiftSyncTestCase { } func testAutoOpenWithUserAfterLogoutFromAnonymous() throws { - let partitionValueA = #function - let partitionValueB = "\(#function)bar" + let partitionValueA = name + let partitionValueB = "\(name) 2" + try populateRealm() - let anonymousUser = try logInUser(for: .anonymous) + let anonymousUser = self.anonymousUser let ex = expectation(description: "download-realm-anonymous-user-auto-open") - autoOpen(user: anonymousUser, appId: appId, partitionValue: partitionValueA) { autoOpenState in + autoOpen(user: anonymousUser, appId: appId, partitionValue: partitionValueB) { autoOpenState in if case let .open(realm) = autoOpenState { self.checkCount(expected: 0, realm, SwiftHugeSyncObject.self) ex.fulfill() } } - app.currentUser?.logOut { _ in } // Logout anonymous user - - let user = try logInUser(for: basicCredentials()) - if !isParent { - return try populateRealm(user: user, partitionValue: partitionValueB) - } - executeChild() + anonymousUser.logOut().await(self) let ex2 = expectation(description: "download-realm-after-logout-auto-open") - autoOpen(user: user, appId: appId, partitionValue: partitionValueB) { autoOpenState in + autoOpen(user: createUser(), appId: appId, partitionValue: partitionValueA) { autoOpenState in if case let .open(realm) = autoOpenState { self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) ex2.fulfill() @@ -513,17 +486,13 @@ class SwiftUIServerTests: SwiftSyncTestCase { } func testAutoOpenWithDifferentPartitionValues() throws { - let partitionValueA = #function - let partitionValueB = "\(#function)bar" - - let user = try logInUser(for: basicCredentials()) - if !isParent { - return try populateRealm(user: user, partitionValue: partitionValueA) - } - executeChild() + try populateRealm() + let partitionValueA = name + let partitionValueB = "\(name) 2" + let user = createUser() let ex = expectation(description: "download-partition-value-auto-open") - autoOpen(user: user, partitionValue: partitionValueA) { autoOpenState in + autoOpen(user: user, appId: nil, partitionValue: partitionValueA) { autoOpenState in if case let .open(realm) = autoOpenState { self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) ex.fulfill() @@ -531,7 +500,7 @@ class SwiftUIServerTests: SwiftSyncTestCase { } let ex2 = expectation(description: "download-other-partition-value-auto-open") - autoOpen(user: user, partitionValue: partitionValueB) { autoOpenState in + autoOpen(user: user, appId: nil, partitionValue: partitionValueB) { autoOpenState in if case let .open(realm) = autoOpenState { self.checkCount(expected: 0, realm, SwiftHugeSyncObject.self) ex2.fulfill() @@ -541,17 +510,13 @@ class SwiftUIServerTests: SwiftSyncTestCase { // MARK: - Mixed AsyncOpen & AutoOpen func testCombineAsyncOpenAutoOpenWithDifferentPartitionValues() throws { - let partitionValueA = #function - let partitionValueB = "\(#function)bar" - - let user = try logInUser(for: basicCredentials()) - if !isParent { - return try populateRealm(user: user, partitionValue: partitionValueA) - } - executeChild() + try populateRealm() + let partitionValueA = name + let partitionValueB = "\(name) 2" + let user = createUser() let ex = expectation(description: "download-partition-value-async-open-mixed") - asyncOpen(user: user, partitionValue: partitionValueA) { asyncOpenState in + asyncOpen(user: user, appId: nil, partitionValue: partitionValueA) { asyncOpenState in if case let .open(realm) = asyncOpenState { self.checkCount(expected: SwiftSyncTestCase.bigObjectCount, realm, SwiftHugeSyncObject.self) ex.fulfill() @@ -559,7 +524,7 @@ class SwiftUIServerTests: SwiftSyncTestCase { } let ex2 = expectation(description: "download-partition-value-auto-open-mixed") - autoOpen(user: user, partitionValue: partitionValueB) { autoOpenState in + autoOpen(user: user, appId: nil, partitionValue: partitionValueB) { autoOpenState in if case let .open(realm) = autoOpenState { self.checkCount(expected: 0, realm, SwiftHugeSyncObject.self) ex2.fulfill() @@ -568,18 +533,16 @@ class SwiftUIServerTests: SwiftSyncTestCase { } func testCombineAsyncOpenAutoOpenWithMultiUserApp() throws { - let partitionValueA = #function - let partitionValueB = "\(#function)bar" + try populateRealm() + let partitionValueA = name + let partitionValueB = "\(name) 2" - let syncUser1 = try logInUser(for: basicCredentials()) - let syncUser2 = try logInUser(for: basicCredentials()) + let syncUser1 = createUser() + let syncUser2 = createUser() XCTAssertEqual(app.allUsers.count, 2) XCTAssertEqual(syncUser2.id, app.currentUser!.id) - if !isParent { - return try populateRealm(user: syncUser1, partitionValue: partitionValueA) - } - executeChild() + anonymousUser.remove().await(self) let ex = expectation(description: "test-combine-multiuser1-app-auto-open") autoOpen(user: syncUser2, appId: appId, partitionValue: partitionValueB) { autoOpenState in diff --git a/Realm/ObjectServerTests/TimeoutProxyServer.swift b/Realm/ObjectServerTests/TimeoutProxyServer.swift index e5cbbe8e6d..e7ea3cc41d 100644 --- a/Realm/ObjectServerTests/TimeoutProxyServer.swift +++ b/Realm/ObjectServerTests/TimeoutProxyServer.swift @@ -19,7 +19,7 @@ #if os(macOS) import Foundation -@preconcurrency import Network +import Network @available(OSX 10.14, *) @objc(TimeoutProxyServer) diff --git a/Realm/RLMError.mm b/Realm/RLMError.mm index d464010cdf..c60c5501d5 100644 --- a/Realm/RLMError.mm +++ b/Realm/RLMError.mm @@ -127,9 +127,18 @@ NSInteger translateFileError(realm::ErrorCodes::Error code) { } NSString *errorDomain(realm::ErrorCodes::Error error) { - if (error == realm::ErrorCodes::SyncConnectTimeout) { - return NSPOSIXErrorDomain; + // Special-case errors where our error domain doesn't match core's category + // NEXT-MAJOR: we should unify everything into RLMErrorDomain + using ec = realm::ErrorCodes::Error; + switch (error) { + case ec::SubscriptionFailed: + return RLMErrorDomain; + case ec::SyncConnectTimeout: + return NSPOSIXErrorDomain; + default: + break; } + auto category = realm::ErrorCodes::error_categories(error); if (category.test(realm::ErrorCategory::sync_error)) { return RLMSyncErrorDomain; diff --git a/Realm/RLMRealm.mm b/Realm/RLMRealm.mm index 268a65ad6f..9a84a67b80 100644 --- a/Realm/RLMRealm.mm +++ b/Realm/RLMRealm.mm @@ -90,6 +90,10 @@ static void RLMAddSkipBackupAttributeToItemAtPath(std::string_view path) { void RLMWaitForRealmToClose(NSString *path) { NSString *lockfilePath = [path stringByAppendingString:@".lock"]; + if (![NSFileManager.defaultManager fileExistsAtPath:lockfilePath]) { + return; + } + File lockfile(lockfilePath.UTF8String, File::mode_Update); lockfile.set_fifo_path([path stringByAppendingString:@".management"].UTF8String, "lock.fifo"); while (!lockfile.try_rw_lock_exclusive()) { @@ -1146,4 +1150,20 @@ - (RLMSyncSubscriptionSet *)subscriptions { @throw RLMException(@"Realm was not compiled with sync enabled"); #endif } + +void RLMRealmSubscribeToAll(RLMRealm *realm) { + if (!realm.isFlexibleSync) { + return; + } + + auto subs = realm->_realm->get_latest_subscription_set().make_mutable_copy(); + auto& group = realm->_realm->read_group(); + for (auto key : group.get_table_keys()) { + if (!std::string_view(group.get_table_name(key)).starts_with("class_")) { + continue; + } + subs.insert_or_assign(group.get_table(key)->where()); + } + subs.commit(); +} @end diff --git a/Realm/RLMRealm_Private.h b/Realm/RLMRealm_Private.h index 95841491ff..d247de39bc 100644 --- a/Realm/RLMRealm_Private.h +++ b/Realm/RLMRealm_Private.h @@ -56,6 +56,8 @@ FOUNDATION_EXTERN RLMRealm *_Nullable RLMGetAnyCachedRealm(RLMRealmConfiguration // Scheduler an async refresh for the given Realm FOUNDATION_EXTERN RLMAsyncRefreshTask *_Nullable RLMRealmRefreshAsync(RLMRealm *rlmRealm) NS_RETURNS_RETAINED; +FOUNDATION_EXTERN void RLMRealmSubscribeToAll(RLMRealm *); + // RLMRealm private members @interface RLMRealm () @property (nonatomic, readonly) BOOL dynamic; diff --git a/Realm/RLMSyncSession.mm b/Realm/RLMSyncSession.mm index 98d562acc0..ec80cbb8ed 100644 --- a/Realm/RLMSyncSession.mm +++ b/Realm/RLMSyncSession.mm @@ -232,7 +232,7 @@ + (void)immediatelyHandleError:(RLMSyncErrorActionToken *)token syncManager:(RLM } token->_isValid = NO; - [syncManager syncManager]->immediately_run_file_actions(std::move(token->_originalPath)); + syncManager.syncManager->immediately_run_file_actions(token->_originalPath); } + (nullable RLMSyncSession *)sessionForRealm:(RLMRealm *)realm { diff --git a/Realm/Tests/SwiftUISyncTestHost/ContentView.swift b/Realm/Tests/SwiftUISyncTestHost/ContentView.swift index 2bb44e0ca1..66a22fae49 100644 --- a/Realm/Tests/SwiftUISyncTestHost/ContentView.swift +++ b/Realm/Tests/SwiftUISyncTestHost/ContentView.swift @@ -20,6 +20,13 @@ import SwiftUI import RealmSwift import Combine +class SwiftPerson: Object, ObjectKeyIdentifiable { + @Persisted(primaryKey: true) public var _id: ObjectId + @Persisted public var firstName: String = "" + @Persisted public var lastName: String = "" + @Persisted public var age: Int = 30 +} + enum LoggingViewState { case initial case loggingIn @@ -28,7 +35,7 @@ enum LoggingViewState { } struct MainView: View { - let testType: String = ProcessInfo.processInfo.environment["async_view_type"]! + let testType = TestType(rawValue: ProcessInfo.processInfo.environment["async_view_type"]!)! let partitionValue: String? = ProcessInfo.processInfo.environment["partition_value"] @State var viewState: LoggingViewState = .initial @@ -49,14 +56,14 @@ struct MainView: View { } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.purple) - .transition(AnyTransition.move(edge: .trailing)).animation(.default) + .transition(AnyTransition.move(edge: .trailing)) case .loggingIn: VStack { ProgressView("Logging in...") } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.blue) - .transition(AnyTransition.move(edge: .leading)).animation(.default) + .transition(AnyTransition.move(edge: .leading)) case .loggedIn: VStack { Text("Logged in") @@ -68,76 +75,74 @@ struct MainView: View { } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.yellow) - .transition(AnyTransition.move(edge: .trailing)).animation(.default) + .transition(AnyTransition.move(edge: .trailing)) case .syncing: switch testType { - case "async_open": + case .asyncOpen: AsyncOpenView() .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.green) - .transition(AnyTransition.move(edge: .leading)).animation(.default) - case "async_open_environment_partition": + .transition(AnyTransition.move(edge: .leading)) + case .asyncOpenEnvironmentPartition: AsyncOpenPartitionView() - .environment(\.partitionValue, partitionValue ?? user!.id) + .environment(\.partitionValue, partitionValue) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.green) - .transition(AnyTransition.move(edge: .leading)).animation(.default) - case "async_open_environment_configuration": + .transition(AnyTransition.move(edge: .leading)) + case .asyncOpenEnvironmentConfiguration: AsyncOpenPartitionView() - .environment(\.realmConfiguration, user!.configuration(partitionValue: user!.id)) + .environment(\.realmConfiguration, user!.configuration(partitionValue: partitionValue!)) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.green) - .transition(AnyTransition.move(edge: .leading)).animation(.default) - case "async_open_flexible_sync": - AsyncOpenFlexibleSyncView() + .transition(AnyTransition.move(edge: .leading)) + case .asyncOpenFlexibleSync: + AsyncOpenFlexibleSyncView(firstName: partitionValue!) .environment(\.realmConfiguration, user!.flexibleSyncConfiguration()) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.green) - .transition(AnyTransition.move(edge: .leading)).animation(.default) - case "async_open_custom_configuration": + .transition(AnyTransition.move(edge: .leading)) + case .asyncOpenCustomConfiguration: AsyncOpenPartitionView() .environment(\.realmConfiguration, getConfigurationForUser(user!)) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.green) - .transition(AnyTransition.move(edge: .leading)).animation(.default) - case "auto_open": + .transition(AnyTransition.move(edge: .leading)) + case .autoOpen: AutoOpenView() .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.green) - .transition(AnyTransition.move(edge: .leading)).animation(.default) - case "auto_open_environment_partition": + .transition(AnyTransition.move(edge: .leading)) + case .autoOpenEnvironmentPartition: AutoOpenPartitionView() - .environment(\.partitionValue, user!.id) + .environment(\.partitionValue, partitionValue) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.green) - .transition(AnyTransition.move(edge: .leading)).animation(.default) - case "auto_open_environment_configuration": + .transition(AnyTransition.move(edge: .leading)) + case .autoOpenEnvironmentConfiguration: AutoOpenPartitionView() - .environment(\.realmConfiguration, user!.configuration(partitionValue: user!.id)) + .environment(\.realmConfiguration, user!.configuration(partitionValue: partitionValue!)) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.green) - .transition(AnyTransition.move(edge: .leading)).animation(.default) - case "auto_open_flexible_sync": - AutoOpenFlexibleSyncView() + .transition(AnyTransition.move(edge: .leading)) + case .autoOpenFlexibleSync: + AutoOpenFlexibleSyncView(firstName: partitionValue!) .environment(\.realmConfiguration, user!.flexibleSyncConfiguration()) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.green) - .transition(AnyTransition.move(edge: .leading)).animation(.default) - case "auto_open_custom_configuration": + .transition(AnyTransition.move(edge: .leading)) + case .autoOpenCustomConfiguration: AutoOpenPartitionView() .environment(\.realmConfiguration, getConfigurationForUser(user!)) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.green) - .transition(AnyTransition.move(edge: .leading)).animation(.default) - default: - EmptyView() + .transition(AnyTransition.move(edge: .leading)) } } } } func getConfigurationForUser(_ user: User) -> Realm.Configuration { - var configuration = user.configuration(partitionValue: user.id) + var configuration = user.configuration(partitionValue: partitionValue!) configuration.encryptionKey = getKey() return configuration } @@ -193,6 +198,7 @@ class LoginHelper: ObservableObject { private let appConfig = AppConfiguration(baseURL: "http://localhost:9090") func login(email: String, password: String, completion: @escaping (User) -> Void) { + Logger.shared.level = .all let app = RealmSwift.App(id: ProcessInfo.processInfo.environment["app_id"]!, configuration: appConfig) app.login(credentials: .emailPassword(email: email, password: password)) .receive(on: DispatchQueue.main) @@ -238,7 +244,7 @@ struct AsyncOpenView: View { if canNavigate { ListView() .environment(\.realm, realm) - .transition(AnyTransition.move(edge: .leading)).animation(.default) + .transition(AnyTransition.move(edge: .leading)) } else { VStack { Text(String(progress!.completedUnitCount)) @@ -252,12 +258,12 @@ struct AsyncOpenView: View { case .error(let error): ErrorView(error: error) .background(Color.red) - .transition(AnyTransition.move(edge: .trailing)).animation(.default) + .transition(AnyTransition.move(edge: .trailing)) case .progress(let progress): ProgressView(progress) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.yellow) - .transition(AnyTransition.move(edge: .trailing)).animation(.default) + .transition(AnyTransition.move(edge: .trailing)) .onAppear { self.progress = progress } @@ -285,7 +291,7 @@ struct AutoOpenView: View { if canNavigate { ListView() .environment(\.realm, realm) - .transition(AnyTransition.move(edge: .leading)).animation(.default) + .transition(AnyTransition.move(edge: .leading)) } else { VStack { Text(String(progress!.completedUnitCount)) @@ -299,12 +305,12 @@ struct AutoOpenView: View { case .error(let error): ErrorView(error: error) .background(Color.red) - .transition(AnyTransition.move(edge: .trailing)).animation(.default) + .transition(AnyTransition.move(edge: .trailing)) case .progress(let progress): ProgressView(progress) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.yellow) - .transition(AnyTransition.move(edge: .trailing)).animation(.default) + .transition(AnyTransition.move(edge: .trailing)) .onAppear { self.progress = progress } @@ -332,25 +338,23 @@ struct AsyncOpenPartitionView: View { } case .open(let realm): if ProcessInfo.processInfo.environment["is_sectioned_results"] == "true" { - if #available(macOS 12.0, *) { - SectionedResultsView() - .environment(\.realm, realm) - .transition(AnyTransition.move(edge: .leading)).animation(.default) - } + SectionedResultsView() + .environment(\.realm, realm) + .transition(AnyTransition.move(edge: .leading)) } else { ListView() .environment(\.realm, realm) - .transition(AnyTransition.move(edge: .leading)).animation(.default) + .transition(AnyTransition.move(edge: .leading)) } case .error(let error): ErrorView(error: error) .background(Color.red) - .transition(AnyTransition.move(edge: .trailing)).animation(.default) + .transition(AnyTransition.move(edge: .trailing)) case .progress(let progress): ProgressView(progress) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.yellow) - .transition(AnyTransition.move(edge: .trailing)).animation(.default) + .transition(AnyTransition.move(edge: .trailing)) } } } @@ -376,16 +380,16 @@ struct AutoOpenPartitionView: View { case .open(let realm): ListView() .environment(\.realm, realm) - .transition(AnyTransition.move(edge: .leading)).animation(.default) + .transition(AnyTransition.move(edge: .leading)) case .error(let error): ErrorView(error: error) .background(Color.red) - .transition(AnyTransition.move(edge: .trailing)).animation(.default) + .transition(AnyTransition.move(edge: .trailing)) case .progress(let progress): ProgressView(progress) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.yellow) - .transition(AnyTransition.move(edge: .trailing)).animation(.default) + .transition(AnyTransition.move(edge: .trailing)) } } } @@ -402,6 +406,7 @@ struct AsyncOpenFlexibleSyncView: View { @AsyncOpen(appId: ProcessInfo.processInfo.environment["app_id"]!, timeout: 2000) var asyncOpen + let firstName: String var body: some View { VStack { @@ -420,7 +425,7 @@ struct AsyncOpenFlexibleSyncView: View { let subs = realm.subscriptions try await subs.update { subs.append(QuerySubscription(name: "person_age") { - $0.age > 5 && $0.firstName == ProcessInfo.processInfo.environment["firstName"]! + $0.age > 5 && $0.firstName == firstName }) } subscriptionState = .completed @@ -437,17 +442,17 @@ struct AsyncOpenFlexibleSyncView: View { case .navigate: ListView() .environment(\.realm, realm) - .transition(AnyTransition.move(edge: .leading)).animation(.default) + .transition(AnyTransition.move(edge: .leading)) } case .error(let error): ErrorView(error: error) .background(Color.red) - .transition(AnyTransition.move(edge: .trailing)).animation(.default) + .transition(AnyTransition.move(edge: .trailing)) case .progress(let progress): ProgressView(progress) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.yellow) - .transition(AnyTransition.move(edge: .trailing)).animation(.default) + .transition(AnyTransition.move(edge: .trailing)) } } } @@ -458,6 +463,7 @@ struct AutoOpenFlexibleSyncView: View { @AutoOpen(appId: ProcessInfo.processInfo.environment["app_id"]!, timeout: 2000) var asyncOpen + let firstName: String var body: some View { VStack { @@ -476,7 +482,7 @@ struct AutoOpenFlexibleSyncView: View { let subs = realm.subscriptions try await subs.update { subs.append(QuerySubscription(name: "person_age") { - $0.age > 2 && $0.firstName == ProcessInfo.processInfo.environment["firstName"]! + $0.age > 2 && $0.firstName == firstName }) } subscriptionState = .completed @@ -493,17 +499,17 @@ struct AutoOpenFlexibleSyncView: View { case .navigate: ListView() .environment(\.realm, realm) - .transition(AnyTransition.move(edge: .leading)).animation(.default) + .transition(AnyTransition.move(edge: .leading)) } case .error(let error): ErrorView(error: error) .background(Color.red) - .transition(AnyTransition.move(edge: .trailing)).animation(.default) + .transition(AnyTransition.move(edge: .trailing)) case .progress(let progress): ProgressView(progress) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.yellow) - .transition(AnyTransition.move(edge: .trailing)).animation(.default) + .transition(AnyTransition.move(edge: .trailing)) } } } @@ -529,11 +535,11 @@ struct ListView: View { Text("\(object.firstName)") } } - .navigationTitle("SwiftHugeSyncObject's List") + .navigationTitle("SwiftPerson's List") } } -@available(macOS 12.0, *) +@available(macOS 13, *) struct SectionedResultsView: View { @ObservedSectionedResults(SwiftPerson.self, sectionKeyPath: \.firstName) var objects diff --git a/Realm/Tests/SwiftUISyncTestHost/TestType.swift b/Realm/Tests/SwiftUISyncTestHost/TestType.swift new file mode 100644 index 0000000000..b7a1d104af --- /dev/null +++ b/Realm/Tests/SwiftUISyncTestHost/TestType.swift @@ -0,0 +1,30 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2023 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +enum TestType: String { + case asyncOpen + case asyncOpenEnvironmentPartition + case asyncOpenEnvironmentConfiguration + case asyncOpenFlexibleSync + case asyncOpenCustomConfiguration + case autoOpen + case autoOpenEnvironmentPartition + case autoOpenEnvironmentConfiguration + case autoOpenFlexibleSync + case autoOpenCustomConfiguration +} diff --git a/Realm/Tests/SwiftUISyncTestHostUITests/SwiftUISyncTestHostUITests.swift b/Realm/Tests/SwiftUISyncTestHostUITests/SwiftUISyncTestHostUITests.swift index 170c821a5a..bf8687d6f3 100644 --- a/Realm/Tests/SwiftUISyncTestHostUITests/SwiftUISyncTestHostUITests.swift +++ b/Realm/Tests/SwiftUISyncTestHostUITests/SwiftUISyncTestHostUITests.swift @@ -19,52 +19,40 @@ import XCTest import RealmSwift -class SwiftUISyncTestHostUITests: SwiftSyncTestCase { - +class SwiftUISyncUITests: SwiftSyncTestCase { override func tearDown() { logoutAllUsers() application.terminate() super.tearDown() } - override func setUp() { - super.setUp() - application.launchEnvironment["app_id"] = appId + override var objectTypes: [ObjectBase.Type] { + [SwiftPerson.self] } - // Application - private let application = XCUIApplication() + let application = XCUIApplication() - private func openRealm(configuration: Realm.Configuration, for user: User) throws -> Realm { - var configuration = configuration - if configuration.objectTypes == nil { - configuration.objectTypes = [SwiftPerson.self] + func populateData(count: Int) throws { + try write { realm in + for i in 1...count { + realm.add(SwiftPerson(firstName: name, lastName: randomString(7), age: i)) + } } - let realm = try Realm(configuration: configuration) - waitForDownloads(for: realm) - return realm } - @discardableResult - private func populateForEmail(_ email: String, n: Int) throws -> User { - let user = logInUser(for: basicCredentials(name: email, register: true)) - let config = user.configuration(partitionValue: user.id) - let realm = try openRealm(configuration: config, for: user) - try realm.write { - for _ in (1...n) { - realm.add(SwiftPerson(firstName: randomString(7), lastName: randomString(7))) - } - } - waitForUploads(for: realm) - return user + func registerEmail() -> String { + let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" + app.emailPasswordAuth.registerUser(email: email, password: "password").await(self) + return email } // Login for given user - private enum UserType: Int { + enum UserType: Int { case first = 1 case second = 2 } - private func loginUser(_ type: UserType) { + + func loginUser(_ type: UserType) { let loginButton = application.buttons["login_button_\(type.rawValue)"] XCTAssertTrue(loginButton.waitForExistence(timeout: 2)) loginButton.tap() @@ -73,7 +61,7 @@ class SwiftUISyncTestHostUITests: SwiftSyncTestCase { XCTAssertTrue(loggingView.waitForExistence(timeout: 6)) } - private func asyncOpen() { + func asyncOpen() { loginUser(.first) // Query for button to start syncing @@ -82,25 +70,31 @@ class SwiftUISyncTestHostUITests: SwiftSyncTestCase { syncButtonView.tap() } - private func logoutAllUsers() { + func logoutAllUsers() { let loginButton = application.buttons["logout_users_button"] XCTAssertTrue(loginButton.waitForExistence(timeout: 2)) loginButton.tap() } -} -// MARK: - AsyncOpen -extension SwiftUISyncTestHostUITests { - func testDownloadRealmAsyncOpenApp() throws { - let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - let user = try populateForEmail(email, n: 1) - - application.launchEnvironment["email1"] = email - application.launchEnvironment["async_view_type"] = "async_open" - application.launchEnvironment["partition_value"] = user.id + func launch(_ test: TestType, _ env: [String: String]? = nil) { + application.launchEnvironment["app_id"] = appId + application.launchEnvironment["partition_value"] = name + application.launchEnvironment["email1"] = registerEmail() + application.launchEnvironment["email2"] = registerEmail() + application.launchEnvironment["async_view_type"] = test.rawValue + if let env { + application.launchEnvironment.merge(env, uniquingKeysWith: { $1 }) + } application.launch() - asyncOpen() + } +} + +class PBSSwiftUISyncUITests: SwiftUISyncUITests { + // MARK: - AsyncOpen + func testDownloadRealmAsyncOpenApp() throws { + try populateData(count: 1) + launch(.asyncOpen) // Test progress is greater than 0 let progressView = application.staticTexts["progress_text_view"] @@ -119,64 +113,26 @@ extension SwiftUISyncTestHostUITests { } func testDownloadRealmAsyncOpenAppWithEnvironmentPartitionValue() throws { - let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - try populateForEmail(email, n: 2) - - application.launchEnvironment["email1"] = email - application.launchEnvironment["async_view_type"] = "async_open_environment_partition" - application.launch() - - asyncOpen() + try populateData(count: 2) + launch(.asyncOpenEnvironmentPartition) - // Test show ListView after syncing realm let table = application.tables.firstMatch XCTAssertTrue(table.waitForExistence(timeout: 6)) XCTAssertEqual(table.cells.count, 2) } func testDownloadRealmAsyncOpenAppWithEnvironmentConfiguration() throws { - let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - try populateForEmail(email, n: 3) - - application.launchEnvironment["email1"] = email - application.launchEnvironment["async_view_type"] = "async_open_environment_configuration" - application.launch() - - asyncOpen() + try populateData(count: 3) + launch(.asyncOpenEnvironmentConfiguration) - // Test show ListView after syncing realm let table = application.tables.firstMatch XCTAssertTrue(table.waitForExistence(timeout: 6)) XCTAssertEqual(table.cells.count, 3) } func testObservedResults() throws { - // This test ensures that `@ObservedResults` correctly observes both local - // and sync changes to a collection. - let partitionValue = "test" - let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - let email2 = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - - let user1 = logInUser(for: basicCredentials(name: email, register: true)) - let user2 = logInUser(for: basicCredentials(name: email2, register: true)) - - let config1 = user1.configuration(partitionValue: partitionValue) - let config2 = user2.configuration(partitionValue: partitionValue) - - let realm = try Realm(configuration: config1) - try realm.write { - realm.add(SwiftPerson(firstName: "Joe", lastName: "Blogs")) - realm.add(SwiftPerson(firstName: "Jane", lastName: "Doe")) - } - user1.waitForUpload(toFinish: partitionValue) - - application.launchEnvironment["email1"] = email - application.launchEnvironment["email2"] = email2 - application.launchEnvironment["async_view_type"] = "async_open_environment_partition" - application.launchEnvironment["partition_value"] = partitionValue - application.launch() - - asyncOpen() + try populateData(count: 2) + launch(.asyncOpenEnvironmentPartition) // Test show ListView after syncing realm let table = application.tables.firstMatch @@ -194,17 +150,11 @@ extension SwiftUISyncTestHostUITests { XCTAssertTrue(table.waitForExistence(timeout: 6)) XCTAssertEqual(table.cells.count, 2) - let realm2 = try Realm(configuration: config2) - waitForDownloads(for: realm2) - try! realm2.write { - realm2.add(SwiftPerson(firstName: "Joe2", lastName: "Blogs")) - realm2.add(SwiftPerson(firstName: "Jane2", lastName: "Doe")) - } - user2.waitForUpload(toFinish: partitionValue) + try populateData(count: 2) + XCTAssertTrue(table.cells.element(boundBy: 3).waitForExistence(timeout: 6)) XCTAssertEqual(table.cells.count, 4) loginUser(.first) - waitForDownloads(for: realm) // Make sure the first user also has 4 SwiftPerson's XCTAssertTrue(syncButtonView.waitForExistence(timeout: 2)) syncButtonView.tap() @@ -212,35 +162,13 @@ extension SwiftUISyncTestHostUITests { XCTAssertEqual(table.cells.count, 4) } - @available(macOS 12.0, *) func testObservedSectionedResults() throws { - // This test ensures that `@ObservedResults` correctly observes both local - // and sync changes to a collection. - let partitionValue = "test2" - let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - let email2 = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - - let user1 = logInUser(for: basicCredentials(name: email, register: true)) - let user2 = logInUser(for: basicCredentials(name: email2, register: true)) - - let config1 = user1.configuration(partitionValue: partitionValue) - let config2 = user2.configuration(partitionValue: partitionValue) - - let realm = try Realm(configuration: config1) - try realm.write { + try write { realm in realm.add(SwiftPerson(firstName: "Joe", lastName: "Blogs")) realm.add(SwiftPerson(firstName: "Jane", lastName: "Doe")) } - user1.waitForUpload(toFinish: partitionValue) - application.launchEnvironment["email1"] = email - application.launchEnvironment["email2"] = email2 - application.launchEnvironment["async_view_type"] = "async_open_environment_partition" - application.launchEnvironment["is_sectioned_results"] = "true" - application.launchEnvironment["partition_value"] = partitionValue - application.launch() - - asyncOpen() + launch(.asyncOpenEnvironmentPartition, ["is_sectioned_results": "true"]) // Test show ListView after syncing realm let table = application.tables.firstMatch @@ -259,17 +187,13 @@ extension SwiftUISyncTestHostUITests { XCTAssertTrue(table.waitForExistence(timeout: 6)) XCTAssertEqual(table.cells.count, 4) - let realm2 = try Realm(configuration: config2) - waitForDownloads(for: realm2) - try! realm2.write { - realm2.add(SwiftPerson(firstName: "Joe", lastName: "Blogs2")) - realm2.add(SwiftPerson(firstName: "Jane", lastName: "Doe2")) + try write { realm in + realm.add(SwiftPerson(firstName: "Joe", lastName: "Blogs2")) + realm.add(SwiftPerson(firstName: "Jane", lastName: "Doe2")) } - user2.waitForUpload(toFinish: partitionValue) XCTAssertEqual(table.cells.count, 6) loginUser(.first) - waitForDownloads(for: realm) // Make sure the first user also has 4 SwiftPerson's XCTAssertTrue(syncButtonView.waitForExistence(timeout: 2)) syncButtonView.tap() @@ -279,22 +203,13 @@ extension SwiftUISyncTestHostUITests { } func testAsyncOpenMultiUser() throws { - let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - try populateForEmail(email, n: 2) - let email2 = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - try populateForEmail(email2, n: 1) - - application.launchEnvironment["email1"] = email - application.launchEnvironment["email2"] = email2 - application.launchEnvironment["async_view_type"] = "async_open_environment_partition" - application.launch() - - asyncOpen() + try populateData(count: 3) + launch(.asyncOpenEnvironmentPartition) // Test show ListView after syncing realm let table = application.tables.firstMatch XCTAssertTrue(table.waitForExistence(timeout: 6)) - XCTAssertEqual(table.cells.count, 2) + XCTAssertEqual(table.cells.count, 3) loginUser(.second) @@ -305,18 +220,12 @@ extension SwiftUISyncTestHostUITests { // Test show ListView after logging new user XCTAssertTrue(table.waitForExistence(timeout: 6)) - XCTAssertEqual(table.cells.count, 1) + XCTAssertEqual(table.cells.count, 3) } func testAsyncOpenAndLogout() throws { - let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - try populateForEmail(email, n: 2) - - application.launchEnvironment["email1"] = email - application.launchEnvironment["async_view_type"] = "async_open_environment_partition" - application.launch() - - asyncOpen() + try populateData(count: 2) + launch(.asyncOpenEnvironmentPartition) // Test show ListView after syncing realm let table = application.tables.firstMatch @@ -332,16 +241,8 @@ extension SwiftUISyncTestHostUITests { } func testAsyncOpenWithDeferRealmConfiguration() throws { - let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - let user = try populateForEmail(email, n: 20) - - application.launchEnvironment["email1"] = email - application.launchEnvironment["async_view_type"] = "async_open_custom_configuration" - application.launchEnvironment["app_id"] = appId - application.launchEnvironment["partition_value"] = user.id - application.launch() - - asyncOpen() + try populateData(count: 20) + launch(.asyncOpenCustomConfiguration) // Test show ListView after syncing realm let table = application.tables.firstMatch @@ -357,21 +258,11 @@ extension SwiftUISyncTestHostUITests { // Synced Realm and Sync Metadata XCTAssertEqual(counter, 2) } -} -// MARK: - AutoOpen -extension SwiftUISyncTestHostUITests { + // MARK: - AutoOpen func testDownloadRealmAutoOpenApp() throws { - let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - let user = try populateForEmail(email, n: 1) - - application.launchEnvironment["email1"] = email - application.launchEnvironment["async_view_type"] = "auto_open" - application.launchEnvironment["partition_value"] = user.id - application.launch() - - // Test that the user is already logged in - asyncOpen() + try populateData(count: 1) + launch(.autoOpen) // Test progress is greater than 0 let progressView = application.staticTexts["progress_text_view"] @@ -383,61 +274,37 @@ extension SwiftUISyncTestHostUITests { let nextViewView = application.buttons["show_list_button_view"] nextViewView.tap() - // Test show ListView after syncing realm let table = application.tables.firstMatch XCTAssertTrue(table.waitForExistence(timeout: 6)) XCTAssertEqual(table.cells.count, 1) } func testDownloadRealmAutoOpenAppWithEnvironmentPartitionValue() throws { - let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - try populateForEmail(email, n: 2) - - application.launchEnvironment["email1"] = email - application.launchEnvironment["async_view_type"] = "auto_open_environment_partition" - application.launch() + try populateData(count: 2) + launch(.autoOpenEnvironmentPartition) - asyncOpen() - - // Test show ListView after syncing realm let table = application.tables.firstMatch XCTAssertTrue(table.waitForExistence(timeout: 6)) XCTAssertEqual(table.cells.count, 2) } func testDownloadRealmAutoOpenAppWithEnvironmentConfiguration() throws { - let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - try populateForEmail(email, n: 3) - - application.launchEnvironment["email1"] = email - application.launchEnvironment["async_view_type"] = "auto_open_environment_configuration" - application.launch() - - asyncOpen() + try populateData(count: 3) + launch(.autoOpenEnvironmentConfiguration) - // Test show ListView after syncing realm let table = application.tables.firstMatch XCTAssertTrue(table.waitForExistence(timeout: 6)) XCTAssertEqual(table.cells.count, 3) } func testAutoOpenMultiUser() throws { - let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - try populateForEmail(email, n: 2) - let email2 = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - try populateForEmail(email2, n: 1) - - application.launchEnvironment["email1"] = email - application.launchEnvironment["email2"] = email2 - application.launchEnvironment["async_view_type"] = "auto_open_environment_partition" - application.launch() - - asyncOpen() + try populateData(count: 3) + launch(.autoOpenEnvironmentPartition) // Test show ListView after syncing realm let table = application.tables.firstMatch XCTAssertTrue(table.waitForExistence(timeout: 6)) - XCTAssertEqual(table.cells.count, 2) + XCTAssertEqual(table.cells.count, 3) loginUser(.second) @@ -448,18 +315,12 @@ extension SwiftUISyncTestHostUITests { // Test show ListView after logging new user XCTAssertTrue(table.waitForExistence(timeout: 6)) - XCTAssertEqual(table.cells.count, 1) + XCTAssertEqual(table.cells.count, 3) } func testAutoOpenAndLogout() throws { - let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - try populateForEmail(email, n: 2) - - application.launchEnvironment["email1"] = email - application.launchEnvironment["async_view_type"] = "auto_open_environment_partition" - application.launch() - - asyncOpen() + try populateData(count: 2) + launch(.autoOpenEnvironmentPartition) // Test show ListView after syncing realm let table = application.tables.firstMatch @@ -475,16 +336,8 @@ extension SwiftUISyncTestHostUITests { } func testAutoOpenWithDeferRealmConfiguration() throws { - let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - let user = try populateForEmail(email, n: 20) - - application.launchEnvironment["email1"] = email - application.launchEnvironment["async_view_type"] = "auto_open_custom_configuration" - application.launchEnvironment["app_id"] = appId - application.launchEnvironment["partition_value"] = user.id - application.launch() - - asyncOpen() + try populateData(count: 20) + launch(.autoOpenCustomConfiguration) // Test show ListView after syncing realm let table = application.tables.firstMatch @@ -502,48 +355,18 @@ extension SwiftUISyncTestHostUITests { } } -// MARK: - Flexible Sync -extension SwiftUISyncTestHostUITests { - private func populateFlexibleSyncForEmail(_ email: String, n: Int, _ block: @escaping (Realm) -> Void) throws { - let user = logInUser(for: basicCredentials(name: email, register: true, app: flexibleSyncApp), app: flexibleSyncApp) - - let config = user.flexibleSyncConfiguration() - let realm = try Realm(configuration: config) - let subs = realm.subscriptions - let ex = expectation(description: "state change complete") - subs.update({ - subs.append(QuerySubscription(name: "person_age", where: "TRUEPREDICATE")) - }, onComplete: { error in - if error == nil { - ex.fulfill() - } else { - XCTFail("Subscription Set could not complete with \(error!)") - } - }) - waitForExpectations(timeout: 20.0, handler: nil) +class FLXSwiftUISyncUITests: SwiftUISyncUITests { + override func createApp() throws -> String { + try createFlexibleSyncApp() + } - try realm.write { - block(realm) - } - waitForUploads(for: realm) + override func configuration(user: User) -> Realm.Configuration { + user.flexibleSyncConfiguration() } func testFlexibleSyncAsyncOpenRoundTrip() throws { - let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - try populateFlexibleSyncForEmail(email, n: 10) { realm in - for index in (1...10) { - realm.add(SwiftPerson(firstName: "\(#function)", lastName: "Smith", age: index)) - } - } - - application.launchEnvironment["email1"] = email - application.launchEnvironment["async_view_type"] = "async_open_flexible_sync" - // Override appId for flexible app Id - application.launchEnvironment["app_id"] = flexibleSyncAppId - application.launchEnvironment["firstName"] = "\(#function)" - application.launch() - - asyncOpen() + try populateData(count: 10) + launch(.asyncOpenFlexibleSync) // Query for button to navigate to next view let nextViewView = application.buttons["show_list_button_view"] @@ -557,21 +380,8 @@ extension SwiftUISyncTestHostUITests { } func testFlexibleSyncAutoOpenRoundTrip() throws { - let email = "realm_tests_do_autoverify\(randomString(7))@\(randomString(7)).com" - try populateFlexibleSyncForEmail(email, n: 10) { realm in - for index in (1...20) { - realm.add(SwiftPerson(firstName: "\(#function)", lastName: "Smith", age: index)) - } - } - - application.launchEnvironment["email1"] = email - application.launchEnvironment["async_view_type"] = "auto_open_flexible_sync" - // Override appId for flexible app Id - application.launchEnvironment["app_id"] = flexibleSyncAppId - application.launchEnvironment["firstName"] = "\(#function)" - application.launch() - - asyncOpen() + try populateData(count: 20) + launch(.autoOpenFlexibleSync) // Query for button to navigate to next view let nextViewView = application.buttons["show_list_button_view"] diff --git a/RealmSwift/Combine.swift b/RealmSwift/Combine.swift index 3903626ce4..5b219efce2 100644 --- a/RealmSwift/Combine.swift +++ b/RealmSwift/Combine.swift @@ -894,13 +894,8 @@ extension RealmKeyedCollection { @frozen public struct AsyncOpenSubscription: Subscription { private let task: Realm.AsyncOpenTask - internal init(task: Realm.AsyncOpenTask, - callbackQueue: DispatchQueue, - onProgressNotificationCallback: ((SyncSession.Progress) -> Void)?) { + internal init(task: Realm.AsyncOpenTask) { self.task = task - if let onProgressNotificationCallback = onProgressNotificationCallback { - self.task.addProgressNotification(queue: callbackQueue, block: onProgressNotificationCallback) - } } /// A unique identifier for identifying publisher streams. @@ -968,14 +963,20 @@ public enum RealmPublishers { /// :nodoc: public func receive(subscriber: S) where S: Subscriber, S.Failure == Failure, Output == S.Input { - subscriber.receive(subscription: AsyncOpenSubscription(task: Realm.AsyncOpenTask(rlmTask: RLMRealm.asyncOpen(with: configuration.rlmConfiguration, callbackQueue: callbackQueue, callback: { rlmRealm, error in + let rlmTask = RLMRealm.asyncOpen(with: configuration.rlmConfiguration, + callbackQueue: callbackQueue) { rlmRealm, error in if let realm = rlmRealm.flatMap(Realm.init) { _ = subscriber.receive(realm) subscriber.receive(completion: .finished) } else { subscriber.receive(completion: .failure(error ?? Realm.Error.callFailed)) } - })), callbackQueue: callbackQueue, onProgressNotificationCallback: onProgressNotificationCallback)) + } + let task = Realm.AsyncOpenTask(rlmTask: rlmTask) + if let onProgressNotificationCallback { + task.addProgressNotification(queue: callbackQueue, block: onProgressNotificationCallback) + } + subscriber.receive(subscription: AsyncOpenSubscription(task: task)) } /// Specifies the scheduler on which to perform the async open task. diff --git a/RealmSwift/SyncSubscription.swift b/RealmSwift/SyncSubscription.swift index 4904a6e223..70f6fc2ccf 100644 --- a/RealmSwift/SyncSubscription.swift +++ b/RealmSwift/SyncSubscription.swift @@ -96,6 +96,22 @@ import Realm.Private _rlmSyncSubscription.update(with: query?(Query()).predicate ?? NSPredicate(format: "TRUEPREDICATE")) } + /** + Updates a Flexible Sync's subscription with an allowed query which will be used to bootstrap data + from the server when committed. + + - warning: This method may only be called during a write subscription block. + + - parameter type: The type of the object to be queried. + - parameter query: A query which will be used to modify the existing query. + */ + public func updateQuery(toType type: T.Type, where query: (Query) -> Query) { + guard _rlmSyncSubscription.objectClassName == "\(T.self)" else { + throwRealmException("Updating a subscription query of a different Object Type is not allowed.") + } + _rlmSyncSubscription.update(with: query(Query()).predicate) + } + /// :nodoc: @available(*, unavailable, renamed: "updateQuery", message: "SyncSubscription update is unavailable, please use `.updateQuery` instead.") public func update(toType type: T.Type, where query: @escaping (Query) -> Query) { @@ -166,6 +182,20 @@ import Realm.Private self.predicate = query?(Query()).predicate ?? NSPredicate(format: "TRUEPREDICATE") } + /** + Creates a `QuerySubscription` for the given type. + + - parameter name: Name of the subscription. + - parameter query: The query for the subscription. + */ + public init(name: String? = nil, query: QueryFunction) { + // This overload is required to make `query` non-escaping, as optional + // function parameters always are. + self.name = name + self.className = "\(T.self)" + self.predicate = query(Query()).predicate + } + /** Creates a `QuerySubscription` for the given type. @@ -261,7 +291,7 @@ import Realm.Private the subscription by query and/or name. - returns: A query builder that produces a subscription which can used to search for the subscription. */ - public func first(ofType type: T.Type, `where` query: @escaping (Query) -> Query) -> SyncSubscription? { + public func first(ofType type: T.Type, `where` query: (Query) -> Query) -> SyncSubscription? { return rlmSyncSubscriptionSet.subscription(withClassName: "\(T.self)", predicate: query(Query()).predicate).map(SyncSubscription.init) } From 710d6b6866d83678725e1a6605e02ca3aefc6379 Mon Sep 17 00:00:00 2001 From: Thomas Goyne Date: Tue, 19 Dec 2023 10:08:50 -0800 Subject: [PATCH 10/10] Add a bit of documentation for RLMSyncTestCase --- Realm/ObjectServerTests/RLMSyncTestCase.h | 77 +++++++++++++++++-- Realm/ObjectServerTests/RLMSyncTestCase.mm | 8 -- .../ObjectServerTests/SwiftSyncTestCase.swift | 15 ++++ 3 files changed, 87 insertions(+), 13 deletions(-) diff --git a/Realm/ObjectServerTests/RLMSyncTestCase.h b/Realm/ObjectServerTests/RLMSyncTestCase.h index f58013f7af..931a135211 100644 --- a/Realm/ObjectServerTests/RLMSyncTestCase.h +++ b/Realm/ObjectServerTests/RLMSyncTestCase.h @@ -25,24 +25,72 @@ typedef void(^RLMSyncBasicErrorReportingBlock)(NSError * _Nullable); RLM_HEADER_AUDIT_BEGIN(nullability, sendability) +// RealmServer is implemented in Swift @interface RealmServer : NSObject +// Get the shared singleton instance + (RealmServer *)shared; +// Check if baas is installed. When running via SPM we can't install it +// automatically, so we skip running tests which require it if it's missing. + (bool)haveServer; +// Create a FLX app with the given queryable fields and object types. If +// `persistent:NO` the app will be deleted at the end of the current test, and +// otherwise it will remain until `deleteApp:` is called on it. - (nullable NSString *)createAppWithFields:(NSArray *)fields types:(nullable NSArray *)types persistent:(bool)persistent error:(NSError **)error; +// Create a PBS app with the given partition key type and object types. If +// `persistent:NO` the app will be deleted at the end of the current test, and +// otherwise it will remain until `deleteApp:` is called on it. - (nullable NSString *)createAppWithPartitionKeyType:(NSString *)type types:(nullable NSArray *)types persistent:(bool)persistent error:(NSError **)error; + +// Delete all apps created with `persistent:NO`. Called from `-tearDown`. - (BOOL)deleteAppsAndReturnError:(NSError **)error; +// Delete a specific app created with `persistent:YES`. Called from `+tearDown` +// to delete the shared app for each test case. - (BOOL)deleteApp:(NSString *)appId error:(NSError **)error; @end @interface AsyncOpenConnectionTimeoutTransport : RLMNetworkTransport @end +// RLMSyncTestCase adds some helper functions for writing sync tests, and most +// importantly creates a shared Atlas app which is used by all tests in a test +// case. `self.app` and `self.appId` create the App if needed, and then the +// App is deleted at the end of the test case (i.e. in `+tearDown`). +// +// Each test case subclass must override `defaultObjectTypes` to return the +// `RLMObject` subclasses which the test case uses. These types are the only +// ones which will be present in the server schema, and using any other types +// will result in an error due to developer mode not being used. +// +// By default the app is a partition-based sync app. Test cases which test +// flexible sync must override `createAppWithError:` to call +// `createFlexibleSyncAppWithError:` and `configurationForUser:` to call `[user +// flexibleSyncConfiguration]`. +// +// Most tests can simply call `[self openRealm]` to obtain a synchronized +// Realm. For PBS tests, this will use the current test's name as the partition +// value. This creates a new user each time, so multiple calls to `openRealm` +// will produce separate Realm files. Users can also be created directly with +// `[self createUser]`. +// +// `writeToPartition:block:` for PBS and `populateData:` for FLX is the +// preferred way to populate the server-side state. This creates a new user, +// opens the Realm, calls the block in a write transaction to populate the +// data, waits for uploads to complete, and then deletes the user. +// +// Each test case's server state is fully isolated from other test cases due to +// the combination of creating a new app for each test case and that we add the +// app ID to the name of the collections used by the app. However, state can +// leak between tests within a test case. For partition-based tests this is +// mostly not a problem: each test uses the test name as the partition key and +// so will naturally be partitioned from other tests. For flexible sync, we +// follow the pattern of setting one of the fields in all objects created to +// the test's name and including that in subscriptions. @interface RLMSyncTestCase : RLMMultiProcessTestCase @property (nonatomic, readonly) NSString *appId; @@ -55,18 +103,29 @@ RLM_HEADER_AUDIT_BEGIN(nullability, sendability) #pragma mark - Customization points +// Override to return the set of RLMObject subclasses used by this test case - (NSArray *)defaultObjectTypes; +// Override to customize how the shared App is created for this test case. Most +// commonly this is overrided to `return [self createFlexibleSyncAppWithError:error];` +// for flexible sync test cases. - (nullable NSString *)createAppWithError:(NSError **)error; - (nullable NSString *)createFlexibleSyncAppWithError:(NSError **)error; +// Override to produce flexible sync configurations instead of the default PBS one. - (RLMRealmConfiguration *)configurationForUser:(RLMUser *)user; #pragma mark - Helpers +// Obtain a user with a name derived from test selector, registering it first +// if this is the parent process. This should only be used in multi-process +// tests (and most tests should not need to be multi-process). - (RLMUser *)userForTest:(SEL)sel; - (RLMUser *)userForTest:(SEL)sel app:(RLMApp *)app; -- (RLMCredentials *)basicCredentialsWithName:(NSString *)name register:(BOOL)shouldRegister NS_SWIFT_NAME(basicCredentials(name:register:)); - +// Create new login credentials for this test, possibly registering the user +// first. This is needed to be able to log a user back in after logging out. If +// a user is only logged in one time, use `createUser` instead. +- (RLMCredentials *)basicCredentialsWithName:(NSString *)name + register:(BOOL)shouldRegister NS_SWIFT_NAME(basicCredentials(name:register:)); - (RLMCredentials *)basicCredentialsWithName:(NSString *)name register:(BOOL)shouldRegister app:(RLMApp*)app NS_SWIFT_NAME(basicCredentials(name:register:app:)); @@ -76,6 +135,7 @@ RLM_HEADER_AUDIT_BEGIN(nullability, sendability) /// Synchronously open a synced Realm via asyncOpen and return the expected error. - (NSError *)asyncOpenErrorWithConfiguration:(RLMRealmConfiguration *)configuration; +// Create a new user, and return a configuration using that user. - (RLMRealmConfiguration *)configuration NS_REFINED_FOR_SWIFT; // Open the realm with the partition value `self.name` using a newly created user @@ -127,13 +187,11 @@ RLM_HEADER_AUDIT_BEGIN(nullability, sendability) - (RLMCredentials *)jwtCredentialWithAppId:(NSString *)appId; -/// Synchronously, log out. +/// Log out and wait for the completion handler to be called - (void)logOutUser:(RLMUser *)user; - (void)addPersonsToRealm:(RLMRealm *)realm persons:(NSArray *)persons; -- (void)addAllTypesSyncObjectToRealm:(RLMRealm *)realm values:(NSDictionary *)dictionary person:(Person *)person; - /// Wait for downloads to complete; drop any error. - (void)waitForDownloadsForRealm:(RLMRealm *)realm; - (void)waitForDownloadsForRealm:(RLMRealm *)realm error:(NSError **)error; @@ -169,15 +227,24 @@ RLM_HEADER_AUDIT_BEGIN(nullability, sendability) @end @interface RLMSyncManager () +// Wait for all sync sessions associated with this sync manager to be fully +// torn down. Once this returns, it is guaranteed that reopening a Realm will +// actually create a new sync session. - (void)waitForSessionTermination; @end +// Suspend or resume a sync session without fully tearing it down. These do +// what `suspend` and `resume` will do in the next major version, but it would +// be a breaking change to swap them. @interface RLMSyncSession () - (void)pause; - (void)unpause; @end @interface RLMUser (Test) +// Get the mongo collection for the given object type in the given app. This +// must be used instead of the normal public API because we scope our +// collection names to the app. - (RLMMongoCollection *)collectionForType:(Class)type app:(RLMApp *)app NS_SWIFT_NAME(collection(for:app:)); @end diff --git a/Realm/ObjectServerTests/RLMSyncTestCase.mm b/Realm/ObjectServerTests/RLMSyncTestCase.mm index 0310b197a9..bc87d2e62f 100644 --- a/Realm/ObjectServerTests/RLMSyncTestCase.mm +++ b/Realm/ObjectServerTests/RLMSyncTestCase.mm @@ -142,14 +142,6 @@ - (void)addPersonsToRealm:(RLMRealm *)realm persons:(NSArray *)persons [realm commitWriteTransaction]; } -- (void)addAllTypesSyncObjectToRealm:(RLMRealm *)realm values:(NSDictionary *)dictionary person:(Person *)person { - [realm beginWriteTransaction]; - AllTypesSyncObject *obj = [[AllTypesSyncObject alloc] initWithValue:dictionary]; - obj.objectCol = person; - [realm addObject:obj]; - [realm commitWriteTransaction]; -} - - (RLMRealmConfiguration *)configuration { RLMRealmConfiguration *configuration = [self configurationForUser:self.createUser]; configuration.objectClasses = self.defaultObjectTypes; diff --git a/Realm/ObjectServerTests/SwiftSyncTestCase.swift b/Realm/ObjectServerTests/SwiftSyncTestCase.swift index 10abf94250..d892c57014 100644 --- a/Realm/ObjectServerTests/SwiftSyncTestCase.swift +++ b/Realm/ObjectServerTests/SwiftSyncTestCase.swift @@ -48,6 +48,8 @@ public enum ProcessKind { } } +// SwiftSyncTestCase wraps RLMSyncTestCase to make it more pleasant to use from +// Swift. Most of the comments there apply to this as well. @available(macOS 13, *) @MainActor open class SwiftSyncTestCase: RLMSyncTestCase { @@ -56,6 +58,8 @@ open class SwiftSyncTestCase: RLMSyncTestCase { user.configuration(partitionValue: self.name) } + // Must be overriden in each subclass to specify which types will be used + // in this test case. open var objectTypes: [ObjectBase.Type] { [SwiftPerson.self] } @@ -125,6 +129,9 @@ open class SwiftSyncTestCase: RLMSyncTestCase { waitForDownloads(for: ObjectiveCSupport.convert(object: realm)) } + // Populate the server-side data using the given block, which is called in + // a write transaction. Note that unlike the obj-c versions, this works for + // both PBS and FLX sync. public func write(app: App? = nil, _ block: (Realm) throws -> Void) throws { try autoreleasepool { let realm = try openRealm(app: app) @@ -212,6 +219,10 @@ open class SwiftSyncTestCase: RLMSyncTestCase { #if swift(>=5.8) // MARK: - Async helpers + + // These are async versions of the synchronous functions defined above. + // They should function identially other than being async rather than using + // expecatations to synchronously await things. public func basicCredentials(usernameSuffix: String = "", app: App? = nil) async throws -> Credentials { let email = "\(randomString(10))\(usernameSuffix)" let password = "abcdef" @@ -260,6 +271,8 @@ public extension Publisher { }) } + // Synchronously await non-error completion of the publisher, calling the + // `receiveValue` callback with the value if supplied. @MainActor func await(_ testCase: XCTestCase, timeout: TimeInterval = 20.0, receiveValue: (@Sendable (Self.Output) -> Void)? = nil) { let expectation = testCase.expectation(description: "Async combine pipeline") @@ -268,6 +281,7 @@ public extension Publisher { cancellable.cancel() } + // Synchronously await non-error completion of the publisher, returning the published value. @discardableResult @MainActor func await(_ testCase: XCTestCase, timeout: TimeInterval = 20.0) -> Self.Output { @@ -279,6 +293,7 @@ public extension Publisher { return value.wrappedValue! } + // Synchrously await error completion of the publisher @MainActor func awaitFailure(_ testCase: XCTestCase, timeout: TimeInterval = 20.0, _ errorHandler: (@Sendable (Self.Failure) -> Void)? = nil) {