//////////////////////////////////////////////////////////////////////////// // // 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 "RLMTestUtils.h" #import "RLMSyncSessionRefreshHandle+ObjectServerTests.h" #import "RLMSyncUser+ObjectServerTests.h" #import "RLMRealm+Sync.h" #import "RLMRealmConfiguration_Private.h" #import "RLMRealmUtil.hpp" #import "RLMRealm_Dynamic.h" #import "RLMRealm_Private.hpp" #import "RLMSyncUtil_Private.h" #import "shared_realm.hpp" #pragma mark - Test objects @interface PartialSyncObjectA : RLMObject @property NSInteger number; @property NSString *string; + (instancetype)objectWithNumber:(NSInteger)number string:(NSString *)string; @end @interface PartialSyncObjectB : RLMObject @property NSInteger number; @property NSString *firstString; @property NSString *secondString; + (instancetype)objectWithNumber:(NSInteger)number firstString:(NSString *)first secondString:(NSString *)second; @end @implementation PartialSyncObjectA + (instancetype)objectWithNumber:(NSInteger)number string:(NSString *)string { PartialSyncObjectA *object = [[PartialSyncObjectA alloc] init]; object.number = number; object.string = string; return object; } @end @implementation PartialSyncObjectB + (instancetype)objectWithNumber:(NSInteger)number firstString:(NSString *)first secondString:(NSString *)second { PartialSyncObjectB *object = [[PartialSyncObjectB alloc] init]; object.number = number; object.firstString = first; object.secondString = second; return object; } @end @interface RLMObjectServerTests : RLMSyncTestCase @end @implementation RLMObjectServerTests #pragma mark - Authentication and Tokens /// Valid username/password credentials should be able to log in a user. Using the same credentials should return the /// same user object. - (void)testUsernamePasswordAuthentication { RLMSyncUser *firstUser = [self logInUserForCredentials:[RLMSyncTestCase basicCredentialsWithName:NSStringFromSelector(_cmd) register:YES] server:[RLMSyncTestCase authServerURL]]; RLMSyncUser *secondUser = [self logInUserForCredentials:[RLMSyncTestCase basicCredentialsWithName:NSStringFromSelector(_cmd) register:NO] server:[RLMSyncTestCase authServerURL]]; // Two users created with the same credential should resolve to the same actual user. XCTAssertTrue([firstUser.identity isEqualToString:secondUser.identity]); // Authentication server property should be properly set. XCTAssertEqualObjects(firstUser.authenticationServer, [RLMSyncTestCase authServerURL]); XCTAssertFalse(firstUser.isAdmin); } /// A valid admin token should be able to log in a user. - (void)testAdminTokenAuthentication { RLMSyncCredentials *credentials = [RLMSyncCredentials credentialsWithAccessToken:self.adminToken identity:@"test"]; XCTAssertNotNil(credentials); RLMSyncUser *user = [self logInUserForCredentials:credentials server:[RLMObjectServerTests authServerURL]]; XCTAssertTrue(user.isAdmin); } /// An invalid username/password credential should not be able to log in a user and a corresponding error should be generated. - (void)testInvalidPasswordAuthentication { [self logInUserForCredentials:[RLMSyncTestCase basicCredentialsWithName:NSStringFromSelector(_cmd) register:YES] server:[RLMSyncTestCase authServerURL]]; RLMSyncCredentials *credentials = [RLMSyncCredentials credentialsWithUsername:NSStringFromSelector(_cmd) password:@"INVALID_PASSWORD" register:NO]; XCTestExpectation *expectation = [self expectationWithDescription:@""]; [RLMSyncUser logInWithCredentials:credentials authServerURL:[RLMObjectServerTests authServerURL] onCompletion:^(RLMSyncUser *user, NSError *error) { XCTAssertNil(user); XCTAssertNotNil(error); XCTAssertEqual(error.domain, RLMSyncAuthErrorDomain); XCTAssertEqual(error.code, RLMSyncAuthErrorInvalidCredential); XCTAssertNotNil(error.localizedDescription); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:2.0 handler:nil]; } /// A non-existsing user should not be able to log in and a corresponding error should be generated. - (void)testNonExistingUsernameAuthentication { RLMSyncCredentials *credentials = [RLMSyncTestCase basicCredentialsWithName:NSStringFromSelector(_cmd) register:NO]; XCTestExpectation *expectation = [self expectationWithDescription:@""]; [RLMSyncUser logInWithCredentials:credentials authServerURL:[RLMObjectServerTests authServerURL] onCompletion:^(RLMSyncUser *user, NSError *error) { XCTAssertNil(user); XCTAssertNotNil(error); XCTAssertEqual(error.domain, RLMSyncAuthErrorDomain); XCTAssertEqual(error.code, RLMSyncAuthErrorInvalidCredential); XCTAssertNotNil(error.localizedDescription); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:2.0 handler:nil]; } /// Registering a user with existing username should return corresponding error. - (void)testExistingUsernameRegistration { RLMSyncCredentials *credentials = [RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd) register:YES]; [self logInUserForCredentials:credentials server:[RLMSyncTestCase authServerURL]]; XCTestExpectation *expectation = [self expectationWithDescription:@""]; [RLMSyncUser logInWithCredentials:credentials authServerURL:[RLMObjectServerTests authServerURL] onCompletion:^(RLMSyncUser *user, NSError *error) { XCTAssertNil(user); XCTAssertNotNil(error); XCTAssertEqual(error.domain, RLMSyncAuthErrorDomain); XCTAssertEqual(error.code, RLMSyncAuthErrorInvalidCredential); XCTAssertNotNil(error.localizedDescription); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:2.0 handler:nil]; } /// Errors reported in RLMSyncManager.errorHandler shouldn't contain sync error domain errors as underlying error - (void)testSyncErrorHandlerErrorDomain { RLMSyncUser *user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd) register:YES] server:[RLMObjectServerTests authServerURL]]; XCTAssertNotNil(user); NSURL *realmURL = [NSURL URLWithString:@"realm://127.0.0.1:9080/THE_PATH_USER_DONT_HAVE_ACCESS_TO/test"]; RLMRealmConfiguration *c = [user configurationWithURL:realmURL fullSynchronization:true]; NSError *error = nil; __attribute__((objc_precise_lifetime)) RLMRealm *realm = [RLMRealm realmWithConfiguration:c error:&error]; XCTAssertNil(error); XCTAssertTrue(realm.isEmpty); XCTestExpectation *expectation = [self expectationWithDescription:@""]; [RLMSyncManager sharedManager].errorHandler = ^(__unused NSError *error, __unused RLMSyncSession *session) { XCTAssertTrue([error.domain isEqualToString:RLMSyncErrorDomain]); XCTAssertFalse([[error.userInfo[kRLMSyncUnderlyingErrorKey] domain] isEqualToString:RLMSyncErrorDomain]); [expectation fulfill]; }; [self waitForExpectationsWithTimeout:2.0 handler:nil]; } /// The pre-emptive token refresh subsystem should function, and properly refresh the token. - (void)testPreemptiveTokenRefresh { // Prepare the test. __block NSInteger refreshCount = 0; __block NSInteger errorCount = 0; [RLMSyncManager sharedManager].errorHandler = ^(__unused NSError *error, __unused RLMSyncSession *session) { errorCount++; }; __block XCTestExpectation *ex; [RLMSyncSessionRefreshHandle calculateFireDateUsingTestLogic:YES blockOnRefreshCompletion:^(BOOL success) { XCTAssertTrue(success); refreshCount++; [ex fulfill]; }]; // Open the Realm. NSURL *url = REALM_URL(); RLMSyncUser *user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd) register:true] server:[RLMObjectServerTests authServerURL]]; __attribute__((objc_precise_lifetime)) RLMRealm *realm = [self openRealmForURL:url user:user]; ex = [self expectationWithDescription:@"Timer fired"]; [self waitForExpectationsWithTimeout:10 handler:nil]; XCTAssertTrue(errorCount == 0); XCTAssertTrue(refreshCount > 0); } #pragma mark - Users /// `[RLMSyncUser all]` should be updated once a user is logged in. - (void)testBasicUserPersistence { XCTAssertNil([RLMSyncUser currentUser]); XCTAssertEqual([[RLMSyncUser allUsers] count], 0U); RLMSyncUser *user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd) register:YES] server:[RLMObjectServerTests authServerURL]]; XCTAssertNotNil(user); XCTAssertEqual([[RLMSyncUser allUsers] count], 1U); XCTAssertEqualObjects([RLMSyncUser allUsers], @{user.identity: user}); XCTAssertEqualObjects([RLMSyncUser currentUser], user); RLMSyncUser *user2 = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:[NSStringFromSelector(_cmd) stringByAppendingString:@"2"] register:YES] server:[RLMObjectServerTests authServerURL]]; XCTAssertEqual([[RLMSyncUser allUsers] count], 2U); NSDictionary *dict2 = @{user.identity: user, user2.identity: user2}; XCTAssertEqualObjects([RLMSyncUser allUsers], dict2); RLMAssertThrowsWithReasonMatching([RLMSyncUser currentUser], @"currentUser cannot be called if more that one valid, logged-in user exists"); } /// `[RLMSyncUser currentUser]` should become nil if the user is logged out. - (void)testCurrentUserLogout { XCTAssertNil([RLMSyncUser currentUser]); RLMSyncUser *user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd) register:YES] server:[RLMObjectServerTests authServerURL]]; XCTAssertNotNil(user); XCTAssertEqualObjects([RLMSyncUser currentUser], user); [user logOut]; XCTAssertNil([RLMSyncUser currentUser]); } /// A sync user should return a session when asked for it based on the path. - (void)testUserGetSessionForValidURL { RLMSyncUser *user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd) register:YES] server:[RLMObjectServerTests authServerURL]]; NSURL *url = REALM_URL(); [self openRealmForURL:url user:user immediatelyBlock:^{ RLMSyncSession *session = [user sessionForURL:url]; XCTAssertNotNil(session); }]; // Check session existence after binding. RLMSyncSession *session = [user sessionForURL:url]; XCTAssertNotNil(session); } /// A sync user should return nil when asked for a URL that doesn't exist. - (void)testUserGetSessionForInvalidURL { RLMSyncUser *user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd) register:YES] server:[RLMObjectServerTests authServerURL]]; RLMSyncSession *badSession = [user sessionForURL:[NSURL URLWithString:@"realm://127.0.0.1:9080/noSuchRealm"]]; XCTAssertNil(badSession); } /// A sync user should be able to successfully change their own password. - (void)testUserChangePassword { NSString *userName = NSStringFromSelector(_cmd); NSString *firstPassword = @"a"; NSString *secondPassword = @"b"; // Successfully create user, change its password, log out, // then fail to change password again due to being logged out. { RLMSyncCredentials *creds = [RLMSyncCredentials credentialsWithUsername:userName password:firstPassword register:YES]; RLMSyncUser *user = [self logInUserForCredentials:creds server:[RLMObjectServerTests authServerURL]]; XCTestExpectation *ex = [self expectationWithDescription:@"change password callback invoked"]; [user changePassword:secondPassword completion:^(NSError * _Nullable error) { XCTAssertNil(error); [ex fulfill]; }]; [self waitForExpectationsWithTimeout:2.0 handler:nil]; [user logOut]; ex = [self expectationWithDescription:@"change password callback invoked"]; [user changePassword:@"fail" completion:^(NSError * _Nullable error) { XCTAssertNotNil(error); [ex fulfill]; }]; [self waitForExpectationsWithTimeout:2.0 handler:nil]; } // Fail to log in with original password. { RLMSyncCredentials *creds = [RLMSyncCredentials credentialsWithUsername:userName password:firstPassword register:NO]; XCTestExpectation *ex = [self expectationWithDescription:@"login callback invoked"]; [RLMSyncUser logInWithCredentials:creds authServerURL:[RLMObjectServerTests authServerURL] onCompletion:^(RLMSyncUser *user, NSError *error) { XCTAssertNil(user); XCTAssertNotNil(error); XCTAssertEqual(error.domain, RLMSyncAuthErrorDomain); XCTAssertEqual(error.code, RLMSyncAuthErrorInvalidCredential); XCTAssertNotNil(error.localizedDescription); [ex fulfill]; }]; [self waitForExpectationsWithTimeout:2.0 handler:nil]; } // Successfully log in with new password. { RLMSyncCredentials *creds = [RLMSyncCredentials credentialsWithUsername:userName password:secondPassword register:NO]; RLMSyncUser *user = [self logInUserForCredentials:creds server:[RLMObjectServerTests authServerURL]]; XCTAssertNotNil(user); XCTAssertEqualObjects(RLMSyncUser.currentUser, user); [user logOut]; XCTAssertNil(RLMSyncUser.currentUser); } } /// A sync admin user should be able to successfully change another user's password. - (void)testOtherUserChangePassword { // Create admin user. NSURL *url = [RLMObjectServerTests authServerURL]; RLMSyncUser *adminUser = [self createAdminUserForURL:url username:[[NSUUID UUID] UUIDString]]; NSString *username = NSStringFromSelector(_cmd); NSString *firstPassword = @"a"; NSString *secondPassword = @"b"; NSString *nonAdminUserID = nil; // Successfully create user. { RLMSyncCredentials *creds = [RLMSyncCredentials credentialsWithUsername:username password:firstPassword register:YES]; RLMSyncUser *user = [self logInUserForCredentials:creds server:url]; nonAdminUserID = user.identity; [user logOut]; } // Fail to change password from non-admin user. { NSString *username2 = [NSString stringWithFormat:@"%@_2", username]; RLMSyncCredentials *creds2 = [RLMSyncCredentials credentialsWithUsername:username2 password:@"a" register:YES]; RLMSyncUser *user2 = [self logInUserForCredentials:creds2 server:url]; XCTestExpectation *ex = [self expectationWithDescription:@"change password callback invoked"]; [user2 changePassword:@"foobar" forUserID:nonAdminUserID completion:^(NSError *error) { XCTAssertNotNil(error); [ex fulfill]; }]; [self waitForExpectationsWithTimeout:2.0 handler:nil]; } // Change password from admin user. { XCTestExpectation *ex = [self expectationWithDescription:@"change password callback invoked"]; [adminUser changePassword:secondPassword forUserID:nonAdminUserID completion:^(NSError *error) { XCTAssertNil(error); [ex fulfill]; }]; [self waitForExpectationsWithTimeout:2.0 handler:nil]; } // Fail to log in with original password. { RLMSyncCredentials *creds = [RLMSyncCredentials credentialsWithUsername:username password:firstPassword register:NO]; XCTestExpectation *ex = [self expectationWithDescription:@"login callback invoked"]; [RLMSyncUser logInWithCredentials:creds authServerURL:[RLMObjectServerTests authServerURL] onCompletion:^(RLMSyncUser *user, NSError *error) { XCTAssertNil(user); XCTAssertNotNil(error); XCTAssertEqual(error.domain, RLMSyncAuthErrorDomain); XCTAssertEqual(error.code, RLMSyncAuthErrorInvalidCredential); XCTAssertNotNil(error.localizedDescription); [ex fulfill]; }]; [self waitForExpectationsWithTimeout:2.0 handler:nil]; } // Successfully log in with new password. { RLMSyncCredentials *creds = [RLMSyncCredentials credentialsWithUsername:username password:secondPassword register:NO]; RLMSyncUser *user = [self logInUserForCredentials:creds server:[RLMObjectServerTests authServerURL]]; XCTAssertNotNil(user); [user logOut]; } } - (void)testRequestPasswordResetForRegisteredUser { NSString *userName = [NSStringFromSelector(_cmd) stringByAppendingString:@"@example.com"]; RLMSyncCredentials *creds = [RLMSyncCredentials credentialsWithUsername:userName password:@"a" register:YES]; [[self logInUserForCredentials:creds server:[RLMObjectServerTests authServerURL]] logOut]; XCTestExpectation *ex = [self expectationWithDescription:@"callback invoked"]; [RLMSyncUser requestPasswordResetForAuthServer:[RLMObjectServerTests authServerURL] userEmail:userName completion:^(NSError *error) { XCTAssertNil(error); [ex fulfill]; }]; [self waitForExpectationsWithTimeout:2.0 handler:nil]; NSString *token = [self emailForAddress:userName]; XCTAssertNotNil(token); // Use the password reset token ex = [self expectationWithDescription:@"callback invoked"]; [RLMSyncUser completePasswordResetForAuthServer:[RLMObjectServerTests authServerURL] token:token password:@"new password" completion:^(NSError *error) { XCTAssertNil(error); [ex fulfill]; }]; [self waitForExpectationsWithTimeout:2.0 handler:nil]; // Should now be able to log in with the new password { RLMSyncCredentials *creds = [RLMSyncCredentials credentialsWithUsername:userName password:@"new password" register:NO]; RLMSyncUser *user = [self logInUserForCredentials:creds server:[RLMObjectServerTests authServerURL]]; XCTAssertNotNil(user); [user logOut]; } // Reusing the token should fail ex = [self expectationWithDescription:@"callback invoked"]; [RLMSyncUser completePasswordResetForAuthServer:[RLMObjectServerTests authServerURL] token:token password:@"new password 2" completion:^(NSError *error) { XCTAssertNotNil(error); [ex fulfill]; }]; [self waitForExpectationsWithTimeout:2.0 handler:nil]; } - (void)testRequestPasswordResetForNonexistentUser { NSString *userName = [NSStringFromSelector(_cmd) stringByAppendingString:@"@example.com"]; XCTestExpectation *ex = [self expectationWithDescription:@"callback invoked"]; [RLMSyncUser requestPasswordResetForAuthServer:[RLMObjectServerTests authServerURL] userEmail:userName completion:^(NSError *error) { // Not an error even though the user doesn't exist XCTAssertNil(error); [ex fulfill]; }]; [self waitForExpectationsWithTimeout:2.0 handler:nil]; // Should not have sent an email to the non-registered user XCTAssertNil([self emailForAddress:userName]); } - (void)testRequestPasswordResetWithBadAuthURL { NSString *userName = [NSStringFromSelector(_cmd) stringByAppendingString:@"@example.com"]; XCTestExpectation *ex = [self expectationWithDescription:@"callback invoked"]; NSURL *badAuthUrl = [[RLMObjectServerTests authServerURL] URLByAppendingPathComponent:@"/bad"]; [RLMSyncUser requestPasswordResetForAuthServer:badAuthUrl userEmail:userName completion:^(NSError *error) { XCTAssertNotNil(error); XCTAssertEqualObjects(error.userInfo[@"statusCode"], @404); [ex fulfill]; }]; [self waitForExpectationsWithTimeout:2.0 handler:nil]; } - (void)testRequestConfirmEmailForRegisteredUser { NSString *userName = [NSStringFromSelector(_cmd) stringByAppendingString:@"@example.com"]; RLMSyncCredentials *creds = [RLMSyncCredentials credentialsWithUsername:userName password:@"a" register:YES]; [[self logInUserForCredentials:creds server:[RLMObjectServerTests authServerURL]] logOut]; // This token is sent by ROS upon user registration NSString *registrationToken = [self emailForAddress:userName]; XCTAssertNotNil(registrationToken); XCTestExpectation *ex = [self expectationWithDescription:@"callback invoked"]; [RLMSyncUser requestEmailConfirmationForAuthServer:[RLMObjectServerTests authServerURL] userEmail:userName completion:^(NSError *error) { XCTAssertNil(error); [ex fulfill]; }]; [self waitForExpectationsWithTimeout:2.0 handler:nil]; // This token should have been created when requestEmailConfirmationForAuthServer was called NSString *token = [self emailForAddress:userName]; XCTAssertNotNil(token); XCTAssertNotEqual(token, registrationToken); // Use the token ex = [self expectationWithDescription:@"callback invoked"]; [RLMSyncUser confirmEmailForAuthServer:[RLMObjectServerTests authServerURL] token:token completion:^(NSError *error) { XCTAssertNil(error); [ex fulfill]; }]; [self waitForExpectationsWithTimeout:2.0 handler:nil]; // Reusing the token should fail ex = [self expectationWithDescription:@"callback invoked"]; [RLMSyncUser confirmEmailForAuthServer:[RLMObjectServerTests authServerURL] token:token completion:^(NSError *error) { XCTAssertNotNil(error); [ex fulfill]; }]; [self waitForExpectationsWithTimeout:2.0 handler:nil]; } - (void)testRequestConfirmEmailForNonexistentUser { NSString *userName = [NSStringFromSelector(_cmd) stringByAppendingString:@"@example.com"]; XCTestExpectation *ex = [self expectationWithDescription:@"callback invoked"]; [RLMSyncUser requestEmailConfirmationForAuthServer:[RLMObjectServerTests authServerURL] userEmail:userName completion:^(NSError *error) { // Not an error even though the user doesn't exist XCTAssertNil(error); [ex fulfill]; }]; [self waitForExpectationsWithTimeout:2.0 handler:nil]; // Should not have sent an email to the non-registered user XCTAssertNil([self emailForAddress:userName]); } /// A sync admin user should be able to retrieve information about other users. - (void)testRetrieveUserInfo { NSString *nonAdminUsername = @"meela@realm.example.org"; NSString *adminUsername = @"jyaku"; NSString *pw = @"p"; NSURL *server = [RLMObjectServerTests authServerURL]; // Create a non-admin user. RLMSyncCredentials *c1 = [RLMSyncCredentials credentialsWithUsername:nonAdminUsername password:pw register:YES]; RLMSyncUser *nonAdminUser = [self logInUserForCredentials:c1 server:server]; // Create an admin user. __unused RLMSyncUser *adminUser = [self createAdminUserForURL:server username:adminUsername]; // Create another admin user. RLMSyncUser *userDoingLookups = [self createAdminUserForURL:server username:[[NSUUID UUID] UUIDString]]; // Get the non-admin user's info. XCTestExpectation *ex1 = [self expectationWithDescription:@"should be able to get info about non-admin user"]; [userDoingLookups retrieveInfoForUser:nonAdminUsername identityProvider:RLMIdentityProviderUsernamePassword completion:^(RLMSyncUserInfo *info, NSError *err) { XCTAssertNil(err); XCTAssertNotNil(info); XCTAssertGreaterThan([info.accounts count], ((NSUInteger) 0)); RLMSyncUserAccountInfo *acctInfo = [info.accounts firstObject]; XCTAssertEqualObjects(acctInfo.providerUserIdentity, nonAdminUsername); XCTAssertEqualObjects(acctInfo.provider, RLMIdentityProviderUsernamePassword); XCTAssertFalse(info.isAdmin); [ex1 fulfill]; }]; [self waitForExpectationsWithTimeout:10 handler:nil]; // Get the admin user's info. XCTestExpectation *ex2 = [self expectationWithDescription:@"should be able to get info about admin user"]; [userDoingLookups retrieveInfoForUser:adminUsername identityProvider:RLMIdentityProviderDebug completion:^(RLMSyncUserInfo *info, NSError *err) { XCTAssertNil(err); XCTAssertNotNil(info); XCTAssertGreaterThan([info.accounts count], ((NSUInteger) 0)); RLMSyncUserAccountInfo *acctInfo = [info.accounts firstObject]; XCTAssertEqualObjects(acctInfo.providerUserIdentity, adminUsername); XCTAssertEqualObjects(acctInfo.provider, RLMIdentityProviderDebug); XCTAssertTrue(info.isAdmin); [ex2 fulfill]; }]; [self waitForExpectationsWithTimeout:10 handler:nil]; // Get invalid user's info. XCTestExpectation *ex3 = [self expectationWithDescription:@"should fail for non-existent user"]; [userDoingLookups retrieveInfoForUser:@"invalid_user@realm.example.org" identityProvider:RLMIdentityProviderUsernamePassword completion:^(RLMSyncUserInfo *info, NSError *err) { XCTAssertNotNil(err); XCTAssertEqualObjects(err.domain, RLMSyncAuthErrorDomain); XCTAssertEqual(err.code, RLMSyncAuthErrorUserDoesNotExist); XCTAssertNil(info); [ex3 fulfill]; }]; [self waitForExpectationsWithTimeout:10 handler:nil]; // Get info using user without admin privileges. XCTestExpectation *ex4 = [self expectationWithDescription:@"should fail for user without admin privileges"]; [nonAdminUser retrieveInfoForUser:adminUsername identityProvider:RLMIdentityProviderUsernamePassword completion:^(RLMSyncUserInfo *info, NSError *err) { XCTAssertNotNil(err); XCTAssertEqualObjects(err.domain, RLMSyncAuthErrorDomain); // FIXME: Shouldn't this be RLMSyncAuthErrorAccessDeniedOrInvalidPath? XCTAssertEqual(err.code, RLMSyncAuthErrorUserDoesNotExist); XCTAssertNil(info); [ex4 fulfill]; }]; [self waitForExpectationsWithTimeout:10 handler:nil]; } /// The login queue argument should be respected. - (void)testLoginQueueForSuccessfulLogin { // Make global queue dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); RLMSyncCredentials *c1 = [RLMSyncCredentials credentialsWithUsername:[[NSUUID UUID] UUIDString] password:@"p" register:YES]; XCTestExpectation *ex1 = [self expectationWithDescription:@"User logs in successfully on background queue"]; [RLMSyncUser logInWithCredentials:c1 authServerURL:[RLMObjectServerTests authServerURL] timeout:30.0 callbackQueue:queue onCompletion:^(RLMSyncUser *user, __unused NSError *error) { XCTAssertNotNil(user); XCTAssertFalse([NSThread isMainThread]); [ex1 fulfill]; }]; [self waitForExpectationsWithTimeout:2.0 handler:nil]; RLMSyncCredentials *c2 = [RLMSyncCredentials credentialsWithUsername:[[NSUUID UUID] UUIDString] password:@"p" register:YES]; XCTestExpectation *ex2 = [self expectationWithDescription:@"User logs in successfully on main queue"]; [RLMSyncUser logInWithCredentials:c2 authServerURL:[RLMObjectServerTests authServerURL] timeout:30.0 callbackQueue:dispatch_get_main_queue() onCompletion:^(RLMSyncUser *user, __unused NSError *error) { XCTAssertNotNil(user); XCTAssertTrue([NSThread isMainThread]); [ex2 fulfill]; }]; [self waitForExpectationsWithTimeout:2.0 handler:nil]; } /// The login queue argument should be respected. - (void)testLoginQueueForFailedLogin { // Make global queue dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); RLMSyncCredentials *c1 = [RLMSyncCredentials credentialsWithUsername:[[NSUUID UUID] UUIDString] password:@"p" register:NO]; XCTestExpectation *ex1 = [self expectationWithDescription:@"Error returned on background queue"]; [RLMSyncUser logInWithCredentials:c1 authServerURL:[RLMObjectServerTests authServerURL] timeout:30.0 callbackQueue:queue onCompletion:^(__unused RLMSyncUser *user, NSError *error) { XCTAssertNotNil(error); XCTAssertFalse([NSThread isMainThread]); [ex1 fulfill]; }]; [self waitForExpectationsWithTimeout:2.0 handler:nil]; RLMSyncCredentials *c2 = [RLMSyncCredentials credentialsWithUsername:[[NSUUID UUID] UUIDString] password:@"p" register:NO]; XCTestExpectation *ex2 = [self expectationWithDescription:@"Error returned on main queue"]; [RLMSyncUser logInWithCredentials:c2 authServerURL:[RLMObjectServerTests authServerURL] timeout:30.0 callbackQueue:dispatch_get_main_queue() onCompletion:^(__unused RLMSyncUser *user, NSError *error) { XCTAssertNotNil(error); XCTAssertTrue([NSThread isMainThread]); [ex2 fulfill]; }]; [self waitForExpectationsWithTimeout:2.0 handler:nil]; } - (void)testUserExpirationCallback { NSString *username = NSStringFromSelector(_cmd); RLMSyncCredentials *credentials = [RLMSyncCredentials credentialsWithUsername:username password:@"a" register:YES]; RLMSyncUser *user = [self logInUserForCredentials:credentials server:[RLMObjectServerTests authServerURL]]; XCTestExpectation *ex = [self expectationWithDescription:@"callback should fire"]; // Set a callback on the user __weak RLMSyncUser *weakUser = user; user.errorHandler = ^(RLMSyncUser *u, NSError *error) { XCTAssertEqualObjects(u.identity, weakUser.identity); // Make sure we get the right error. XCTAssertEqualObjects(error.domain, RLMSyncAuthErrorDomain); XCTAssertEqual(error.code, RLMSyncAuthErrorAccessDeniedOrInvalidPath); [ex fulfill]; }; // Screw up the token on the user using a debug API [self manuallySetRefreshTokenForUser:user value:@"not_a_real_refresh_token"]; // Try to log in a Realm; this will cause our errorHandler block defined above to be fired. __attribute__((objc_precise_lifetime)) RLMRealm *r = [self immediatelyOpenRealmForURL:REALM_URL() user:user]; [self waitForExpectationsWithTimeout:10.0 handler:nil]; XCTAssertTrue(user.state == RLMSyncUserStateLoggedOut); } #pragma mark - Basic Sync /// It should be possible to successfully open a Realm configured for sync with an access token. - (void)testOpenRealmWithAdminToken { // FIXME (tests): opening a Realm with the access token, then opening a Realm at the same virtual path // with normal credentials, causes Realms to fail to bind with a "bad virtual path" error. RLMSyncCredentials *credentials = [RLMSyncCredentials credentialsWithAccessToken:self.adminToken identity:@"test"]; XCTAssertNotNil(credentials); RLMSyncUser *user = [self logInUserForCredentials:credentials server:[RLMObjectServerTests authServerURL]]; NSURL *url = [NSURL URLWithString:@"realm://127.0.0.1:9080/testSyncWithAdminToken"]; RLMRealmConfiguration *c = [user configurationWithURL:url fullSynchronization:YES]; NSError *error = nil; RLMRealm *realm = [RLMRealm realmWithConfiguration:c error:&error]; XCTAssertNil(error); XCTAssertTrue(realm.isEmpty); } /// It should be possible to successfully open a Realm configured for sync with a normal user. - (void)testOpenRealmWithNormalCredentials { RLMSyncUser *user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd) register:YES] server:[RLMObjectServerTests authServerURL]]; NSURL *url = REALM_URL(); RLMRealm *realm = [self openRealmForURL:url user:user]; XCTAssertTrue(realm.isEmpty); } /// If client B adds objects to a synced Realm, client A should see those objects. - (void)testAddObjects { NSURL *url = REALM_URL(); RLMSyncUser *user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd) register:self.isParent] server:[RLMObjectServerTests authServerURL]]; RLMRealm *realm = [self openRealmForURL:url user:user]; if (self.isParent) { CHECK_COUNT(0, SyncObject, realm); RLMRunChildAndWait(); [self waitForDownloadsForUser:user realms:@[realm] realmURLs:@[url] expectedCounts:@[@3]]; } else { // Add objects. [self addSyncObjectsToRealm:realm descriptions:@[@"child-1", @"child-2", @"child-3"]]; [self waitForUploadsForRealm:realm]; CHECK_COUNT(3, SyncObject, realm); } } /// If client B deletes objects from a synced Realm, client A should see the effects of that deletion. - (void)testDeleteObjects { NSURL *url = REALM_URL(); RLMSyncUser *user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd) register:self.isParent] server:[RLMObjectServerTests authServerURL]]; RLMRealm *realm = [self openRealmForURL:url user:user]; if (self.isParent) { // Add objects. [self addSyncObjectsToRealm:realm descriptions:@[@"parent-1", @"parent-2", @"parent-3"]]; [self waitForUploadsForRealm:realm]; CHECK_COUNT(3, SyncObject, realm); RLMRunChildAndWait(); [self waitForDownloadsForRealm:realm]; CHECK_COUNT(0, SyncObject, realm); } else { [self waitForDownloadsForRealm:realm]; CHECK_COUNT(3, SyncObject, realm); [realm beginWriteTransaction]; [realm deleteAllObjects]; [realm commitWriteTransaction]; [self waitForUploadsForRealm:realm]; CHECK_COUNT(0, SyncObject, realm); } } #pragma mark - Encryption /// If client B encrypts its synced Realm, client A should be able to access that Realm with a different encryption key. - (void)testEncryptedSyncedRealm { NSURL *url = REALM_URL(); RLMSyncUser *user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd) register:self.isParent] server:[RLMObjectServerTests authServerURL]]; NSData *key = RLMGenerateKey(); RLMRealm *realm = [self openRealmForURL:url user:user encryptionKey:key stopPolicy:RLMSyncStopPolicyAfterChangesUploaded immediatelyBlock:nil]; if (self.isParent) { CHECK_COUNT(0, SyncObject, realm); RLMRunChildAndWait(); [self waitForDownloadsForUser:user realms:@[realm] realmURLs:@[url] expectedCounts:@[@3]]; } else { // Add objects. [self addSyncObjectsToRealm:realm descriptions:@[@"child-1", @"child-2", @"child-3"]]; [self waitForUploadsForRealm:realm]; CHECK_COUNT(3, SyncObject, realm); } } /// If an encrypted synced Realm is re-opened with the wrong key, throw an exception. - (void)testEncryptedSyncedRealmWrongKey { NSURL *url = REALM_URL(); RLMSyncUser *user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd) register:self.isParent] server:[RLMObjectServerTests authServerURL]]; if (self.isParent) { NSString *path; @autoreleasepool { RLMRealm *realm = [self openRealmForURL:url user:user encryptionKey:RLMGenerateKey() stopPolicy:RLMSyncStopPolicyImmediately immediatelyBlock:nil]; path = realm.configuration.pathOnDisk; CHECK_COUNT(0, SyncObject, realm); RLMRunChildAndWait(); [self waitForDownloadsForUser:user realms:@[realm] realmURLs:@[url] expectedCounts:@[@3]]; } RLMRealmConfiguration *c = [RLMRealmConfiguration defaultConfiguration]; c.fileURL = [NSURL fileURLWithPath:path]; RLMAssertThrowsWithError([RLMRealm realmWithConfiguration:c error:nil], @"Unable to open a realm at path", RLMErrorFileAccess, @"invalid mnemonic"); c.encryptionKey = RLMGenerateKey(); RLMAssertThrowsWithError([RLMRealm realmWithConfiguration:c error:nil], @"Unable to open a realm at path", RLMErrorFileAccess, @"Realm file decryption failed"); } else { RLMRealm *realm = [self openRealmForURL:url user:user encryptionKey:RLMGenerateKey() stopPolicy:RLMSyncStopPolicyImmediately immediatelyBlock:nil]; [self addSyncObjectsToRealm:realm descriptions:@[@"child-1", @"child-2", @"child-3"]]; [self waitForUploadsForRealm:realm]; CHECK_COUNT(3, SyncObject, realm); } } #pragma mark - Multiple Realm Sync /// If a client opens multiple Realms, there should be one session object for each Realm that was opened. - (void)testMultipleRealmsSessions { NSURL *urlA = CUSTOM_REALM_URL(@"a"); NSURL *urlB = CUSTOM_REALM_URL(@"b"); NSURL *urlC = CUSTOM_REALM_URL(@"c"); RLMSyncUser *user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd) register:self.isParent] server:[RLMObjectServerTests authServerURL]]; // Open three Realms. __attribute__((objc_precise_lifetime)) RLMRealm *realmealmA = [self openRealmForURL:urlA user:user]; __attribute__((objc_precise_lifetime)) RLMRealm *realmealmB = [self openRealmForURL:urlB user:user]; __attribute__((objc_precise_lifetime)) RLMRealm *realmealmC = [self openRealmForURL:urlC 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"); XCTAssertNotNil([user sessionForURL:urlA], @"Expected to get a session for URL A"); XCTAssertNotNil([user sessionForURL:urlB], @"Expected to get a session for URL B"); XCTAssertNotNil([user sessionForURL:urlC], @"Expected to get a session for URL C"); XCTAssertTrue([user sessionForURL:urlA].state == RLMSyncSessionStateActive, @"Expected active session for URL A"); XCTAssertTrue([user sessionForURL:urlB].state == RLMSyncSessionStateActive, @"Expected active session for URL B"); XCTAssertTrue([user sessionForURL:urlC].state == RLMSyncSessionStateActive, @"Expected active session for URL C"); } /// A client should be able to open multiple Realms and add objects to each of them. - (void)testMultipleRealmsAddObjects { NSURL *urlA = CUSTOM_REALM_URL(@"a"); NSURL *urlB = CUSTOM_REALM_URL(@"b"); NSURL *urlC = CUSTOM_REALM_URL(@"c"); RLMSyncUser *user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd) register:self.isParent] server:[RLMObjectServerTests authServerURL]]; RLMRealm *realmA = [self openRealmForURL:urlA user:user]; RLMRealm *realmB = [self openRealmForURL:urlB user:user]; RLMRealm *realmC = [self openRealmForURL:urlC user:user]; if (self.isParent) { [self waitForDownloadsForRealm:realmA]; [self waitForDownloadsForRealm:realmB]; [self waitForDownloadsForRealm:realmC]; CHECK_COUNT(0, SyncObject, realmA); CHECK_COUNT(0, SyncObject, realmB); CHECK_COUNT(0, SyncObject, realmC); RLMRunChildAndWait(); [self waitForDownloadsForUser:user realms:@[realmA, realmB, realmC] realmURLs:@[urlA, urlB, urlC] expectedCounts:@[@3, @2, @5]]; } else { // Add objects. [self addSyncObjectsToRealm:realmA descriptions:@[@"child-A1", @"child-A2", @"child-A3"]]; [self addSyncObjectsToRealm:realmB descriptions:@[@"child-B1", @"child-B2"]]; [self addSyncObjectsToRealm:realmC descriptions:@[@"child-C1", @"child-C2", @"child-C3", @"child-C4", @"child-C5"]]; [self waitForUploadsForRealm:realmA]; [self waitForUploadsForRealm:realmB]; [self waitForUploadsForRealm:realmC]; CHECK_COUNT(3, SyncObject, realmA); CHECK_COUNT(2, SyncObject, realmB); CHECK_COUNT(5, SyncObject, realmC); } } /// A client should be able to open multiple Realms and delete objects from each of them. - (void)testMultipleRealmsDeleteObjects { NSURL *urlA = CUSTOM_REALM_URL(@"a"); NSURL *urlB = CUSTOM_REALM_URL(@"b"); NSURL *urlC = CUSTOM_REALM_URL(@"c"); RLMSyncUser *user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd) register:self.isParent] server:[RLMObjectServerTests authServerURL]]; RLMRealm *realmA = [self openRealmForURL:urlA user:user]; RLMRealm *realmB = [self openRealmForURL:urlB user:user]; RLMRealm *realmC = [self openRealmForURL:urlC user:user]; if (self.isParent) { [self waitForDownloadsForRealm:realmA]; [self waitForDownloadsForRealm:realmB]; [self waitForDownloadsForRealm:realmC]; // Add objects. [self addSyncObjectsToRealm:realmA descriptions:@[@"parent-A1", @"parent-A2", @"parent-A3", @"parent-A4"]]; [self addSyncObjectsToRealm:realmB descriptions:@[@"parent-B1", @"parent-B2", @"parent-B3", @"parent-B4", @"parent-B5"]]; [self addSyncObjectsToRealm:realmC descriptions:@[@"parent-C1", @"parent-C2"]]; [self waitForUploadsForRealm:realmA]; [self waitForUploadsForRealm:realmB]; [self waitForUploadsForRealm:realmC]; CHECK_COUNT(4, SyncObject, realmA); CHECK_COUNT(5, SyncObject, realmB); CHECK_COUNT(2, SyncObject, realmC); RLMRunChildAndWait(); [self waitForDownloadsForUser:user realms:@[realmA, realmB, realmC] realmURLs:@[urlA, urlB, urlC] expectedCounts:@[@0, @0, @0]]; } else { // Delete all the objects from the Realms. [self waitForDownloadsForRealm:realmA]; [self waitForDownloadsForRealm:realmB]; [self waitForDownloadsForRealm:realmC]; CHECK_COUNT(4, SyncObject, realmA); CHECK_COUNT(5, SyncObject, realmB); CHECK_COUNT(2, SyncObject, realmC); [realmA beginWriteTransaction]; [realmA deleteAllObjects]; [realmA commitWriteTransaction]; [realmB beginWriteTransaction]; [realmB deleteAllObjects]; [realmB commitWriteTransaction]; [realmC beginWriteTransaction]; [realmC deleteAllObjects]; [realmC commitWriteTransaction]; [self waitForUploadsForRealm:realmA]; [self waitForUploadsForRealm:realmB]; [self waitForUploadsForRealm:realmC]; CHECK_COUNT(0, SyncObject, realmA); CHECK_COUNT(0, SyncObject, realmB); CHECK_COUNT(0, SyncObject, realmC); } } #pragma mark - Session Lifetime /// 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 = 10000; NSURL *url = REALM_URL(); // Log in the user. RLMSyncUser *user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd) register:self.isParent] server:[RLMObjectServerTests authServerURL]]; if (self.isParent) { // Open the Realm in an autorelease pool so that it is destroyed as soon as possible. @autoreleasepool { RLMRealm *realm = [self openRealmForURL:url user:user]; [realm beginWriteTransaction]; for (NSInteger i=0; i= transferred); XCTAssert(xfb >= transferrable); transferred = xfr; transferrable = xfb; callCount++; if (transferrable > 0 && transferred >= transferrable && !hasBeenFulfilled) { [ex fulfill]; hasBeenFulfilled = YES; } }]; // Wait for the child process to upload everything. RLMRunChildAndWait(); [self waitForExpectationsWithTimeout:10.0 handler:nil]; [token invalidate]; // The notifier should have been called at least twice: once at the beginning and at least once // to report progress. XCTAssert(callCount > 1); XCTAssert(transferred >= transferrable, @"Transferred (%@) needs to be greater than or equal to transferrable (%@)", @(transferred), @(transferrable)); } - (void)testStreamingUploadNotifier { NSURL *url = REALM_URL(); RLMSyncUser *user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd) register:self.isParent] server:[RLMObjectServerTests authServerURL]]; __block NSInteger callCount = 0; __block NSUInteger transferred = 0; __block NSUInteger transferrable = 0; // Open the Realm RLMRealm *realm = [self openRealmForURL:url user:user]; // Register a notifier. RLMSyncSession *session = [user sessionForURL:url]; XCTAssertNotNil(session); XCTestExpectation *ex = [self expectationWithDescription:@"streaming-upload-expectation"]; auto token = [session addProgressNotificationForDirection:RLMSyncProgressDirectionUpload mode:RLMSyncProgressModeReportIndefinitely block:^(NSUInteger xfr, NSUInteger xfb) { // Make sure the values are // increasing, and update our // stored copies. XCTAssert(xfr >= transferred); XCTAssert(xfb >= transferrable); transferred = xfr; transferrable = xfb; callCount++; if (transferred > 0 && transferred >= transferrable && transferrable > 1000000 * NUMBER_OF_BIG_OBJECTS) { [ex fulfill]; } }]; // Upload lots of data [realm beginWriteTransaction]; for (NSInteger i=0; i 1); XCTAssert(transferred >= transferrable, @"Transferred (%@) needs to be greater than or equal to transferrable (%@)", @(transferred), @(transferrable)); } #pragma mark - Download Realm - (void)testDownloadRealm { const NSInteger NUMBER_OF_BIG_OBJECTS = 2; NSURL *url = REALM_URL(); RLMSyncUser *user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd) register:self.isParent] server:[RLMObjectServerTests authServerURL]]; if (!self.isParent) { [self populateDataForUser:user url:url]; return; } // Wait for the child process to upload everything. RLMRunChildAndWait(); XCTestExpectation *ex = [self expectationWithDescription:@"download-realm"]; RLMRealmConfiguration *c = [user configurationWithURL:url fullSynchronization:true]; XCTAssertFalse([[NSFileManager defaultManager] fileExistsAtPath:c.pathOnDisk isDirectory:nil]); [RLMRealm asyncOpenWithConfiguration:c callbackQueue:dispatch_get_main_queue() callback:^(RLMRealm * _Nullable realm, NSError * _Nullable error) { XCTAssertNil(error); CHECK_COUNT(NUMBER_OF_BIG_OBJECTS, HugeSyncObject, realm); [ex fulfill]; }]; NSUInteger (^fileSize)(NSString *) = ^NSUInteger(NSString *path) { NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:path error:nil]; if (attributes) return [(NSNumber *)attributes[NSFileSize] unsignedLongLongValue]; return 0; }; XCTAssertNil(RLMGetAnyCachedRealmForPath(c.pathOnDisk.UTF8String)); [self waitForExpectationsWithTimeout:10.0 handler:nil]; XCTAssertGreaterThan(fileSize(c.pathOnDisk), 0U); XCTAssertNil(RLMGetAnyCachedRealmForPath(c.pathOnDisk.UTF8String)); } - (void)testDownloadAlreadyOpenRealm { const NSInteger NUMBER_OF_BIG_OBJECTS = 2; NSURL *url = REALM_URL(); // Log in the user. RLMSyncUser *user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd) register:self.isParent] server:[RLMObjectServerTests authServerURL]]; if (!self.isParent) { [self populateDataForUser:user url:url]; return; } XCTestExpectation *ex = [self expectationWithDescription:@"download-realm"]; RLMRealmConfiguration *c = [user configurationWithURL:url fullSynchronization:true]; XCTAssertFalse([[NSFileManager defaultManager] fileExistsAtPath:c.pathOnDisk isDirectory:nil]); RLMRealm *realm = [RLMRealm realmWithConfiguration:c error:nil]; CHECK_COUNT(0, HugeSyncObject, realm); [realm.syncSession suspend]; // Wait for the child process to upload everything. RLMRunChildAndWait(); auto fileSize = ^NSUInteger(NSString *path) { NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:path error:nil]; return [(NSNumber *)attributes[NSFileSize] unsignedLongLongValue]; }; NSUInteger sizeBefore = fileSize(c.pathOnDisk); XCTAssertGreaterThan(sizeBefore, 0U); XCTAssertNotNil(RLMGetAnyCachedRealmForPath(c.pathOnDisk.UTF8String)); [RLMRealm asyncOpenWithConfiguration:c callbackQueue:dispatch_get_main_queue() callback:^(RLMRealm * _Nullable realm, NSError * _Nullable error) { XCTAssertNil(error); CHECK_COUNT(NUMBER_OF_BIG_OBJECTS, HugeSyncObject, realm); [ex fulfill]; }]; [realm.syncSession resume]; [self waitForExpectationsWithTimeout:2.0 handler:nil]; XCTAssertGreaterThan(fileSize(c.pathOnDisk), sizeBefore); XCTAssertNotNil(RLMGetAnyCachedRealmForPath(c.pathOnDisk.UTF8String)); CHECK_COUNT(NUMBER_OF_BIG_OBJECTS, HugeSyncObject, realm); (void)[realm configuration]; } - (void)testDownloadCancelsOnAuthError { RLMSyncUser *user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd) register:self.isParent] server:[RLMObjectServerTests authServerURL]]; auto c = [user configurationWithURL:[NSURL URLWithString:@"realm://127.0.0.1:9080/invalid"] fullSynchronization:true]; auto ex = [self expectationWithDescription:@"async open"]; [RLMRealm asyncOpenWithConfiguration:c callbackQueue:dispatch_get_main_queue() callback:^(RLMRealm *realm, NSError *error) { XCTAssertNil(realm); XCTAssertNotNil(error); [ex fulfill]; }]; [self waitForExpectationsWithTimeout:2.0 handler:nil]; } - (void)testCancelDownload { NSURL *url = REALM_URL(); RLMSyncUser *user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd) register:self.isParent] server:[RLMObjectServerTests authServerURL]]; if (!self.isParent) { [self populateDataForUser:user url:url]; return; } // Wait for the child process to upload everything. RLMRunChildAndWait(); // Use a serial queue for asyncOpen to ensure that the first one adds // the completion block before the second one cancels it RLMSetAsyncOpenQueue(dispatch_queue_create("io.realm.asyncOpen", 0)); XCTestExpectation *ex = [self expectationWithDescription:@"download-realm"]; RLMRealmConfiguration *c = [user configurationWithURL:url fullSynchronization:true]; [RLMRealm asyncOpenWithConfiguration:c callbackQueue:dispatch_get_main_queue() callback:^(RLMRealm *realm, NSError *error) { XCTAssertNil(realm); XCTAssertNotNil(error); [ex fulfill]; }]; [[RLMRealm asyncOpenWithConfiguration:c callbackQueue:dispatch_get_main_queue() callback:^(RLMRealm *, NSError *) { XCTFail(@"Cancelled callback got called"); }] cancel]; [self waitForExpectationsWithTimeout:2.0 handler:nil]; } - (void)testAsyncOpenProgressNotifications { NSURL *url = REALM_URL(); RLMSyncUser *user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd) register:self.isParent] server:[RLMObjectServerTests authServerURL]]; if (!self.isParent) { [self populateDataForUser:user url:url]; return; } RLMRunChildAndWait(); XCTestExpectation *ex1 = [self expectationWithDescription:@"async open"]; XCTestExpectation *ex2 = [self expectationWithDescription:@"download progress complete"]; RLMRealmConfiguration *c = [user configurationWithURL:url fullSynchronization:true]; auto task = [RLMRealm asyncOpenWithConfiguration:c callbackQueue:dispatch_get_main_queue() callback:^(RLMRealm *realm, NSError *error) { XCTAssertNil(error); XCTAssertNotNil(realm); [ex1 fulfill]; }]; [task addProgressNotificationBlock:^(NSUInteger transferredBytes, NSUInteger transferrableBytes) { if (transferrableBytes > 0 && transferredBytes == transferrableBytes) { [ex2 fulfill]; } }]; [self waitForExpectationsWithTimeout:2.0 handler:nil]; } #pragma mark - Compact on Launch - (void)testCompactOnLaunch { NSURL *url = REALM_URL(); RLMSyncUser *user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd) register:YES] server:[RLMObjectServerTests authServerURL]]; NSString *path; // Create a large object and then delete it in the next transaction so that // the file is bloated @autoreleasepool { auto realm = [self openRealmForURL:url user:user]; [realm beginWriteTransaction]; [realm addObject:[HugeSyncObject object]]; [realm commitWriteTransaction]; [self waitForUploadsForRealm:realm]; [realm beginWriteTransaction]; [realm deleteAllObjects]; [realm commitWriteTransaction]; [self waitForUploadsForRealm:realm]; [self waitForDownloadsForRealm:realm]; path = realm.configuration.pathOnDisk; } auto fileManager = NSFileManager.defaultManager; auto initialSize = [[fileManager attributesOfItemAtPath:path error:nil][NSFileSize] unsignedLongLongValue]; // Reopen the file with a shouldCompactOnLaunch block and verify that it is // actually compacted auto config = [user configurationWithURL:url fullSynchronization:true]; __block bool blockCalled = false; __block NSUInteger usedSize = 0; config.shouldCompactOnLaunch = ^(NSUInteger, NSUInteger used) { usedSize = used; blockCalled = true; return YES; }; @autoreleasepool { [RLMRealm realmWithConfiguration:config error:nil]; } XCTAssertTrue(blockCalled); auto finalSize = [[fileManager attributesOfItemAtPath:path error:nil][NSFileSize] unsignedLongLongValue]; XCTAssertLessThan(finalSize, initialSize); XCTAssertLessThanOrEqual(finalSize, usedSize + 4096U); } #pragma mark - Offline Client Reset - (void)testOfflineClientReset { NSError *error; RLMSyncUser *user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd) register:YES] server:[RLMObjectServerTests authServerURL]]; NSURL *sourceFileURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"sync-1.x" withExtension:@"realm"]; NSString *fileName = [NSString stringWithFormat:@"%@.realm", [NSUUID new]]; NSURL *fileURL = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:fileName]]; [NSFileManager.defaultManager copyItemAtURL:sourceFileURL toURL:fileURL error:&error]; XCTAssertNil(error); if (error) { return; } RLMRealmConfiguration *configuration = [user configurationWithURL:REALM_URL() fullSynchronization:true]; RLMSyncConfiguration *syncConfig = configuration.syncConfiguration; syncConfig.customFileURL = fileURL; configuration.syncConfiguration = syncConfig; RLMRealm *realm = [RLMRealm realmWithConfiguration:configuration error:&error]; XCTAssertNil(realm); XCTAssertEqualObjects(error.domain, RLMErrorDomain); XCTAssertEqual(error.code, RLMErrorIncompatibleSyncedFile); RLMRealmConfiguration *backupConfiguration = error.userInfo[RLMBackupRealmConfigurationErrorKey]; XCTAssertNotNil(backupConfiguration); // Open the backup Realm with a schema subset since it was created using the schema from .NET's unit tests. // The Person class is declared in SwiftObjectServerTests.swift. backupConfiguration.objectClasses = @[NSClassFromString(@"Person")]; error = nil; RLMRealm *backupRealm = [RLMRealm realmWithConfiguration:backupConfiguration error:&error]; XCTAssertNotNil(backupRealm); XCTAssertNil(error); RLMResults *people = [backupRealm allObjects:@"Person"]; XCTAssertEqual(people.count, 1u); XCTAssertEqualObjects([people[0] valueForKey:@"FirstName"], @"John"); XCTAssertEqualObjects([people[0] valueForKey:@"LastName"], @"Smith"); error = nil; realm = [RLMRealm realmWithConfiguration:configuration error:&error]; XCTAssertNotNil(realm); XCTAssertNil(error); } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated" - (void)testAutomaticSyncConfiguration { NSURL *server = [RLMObjectServerTests authServerURL]; // Automatic configuration should throw when there are no logged-in users. XCTAssertThrows([RLMSyncConfiguration automaticConfiguration]); RLMSyncCredentials *credsA = [RLMObjectServerTests basicCredentialsWithName:@"a" register:YES]; RLMSyncUser *userA = [self logInUserForCredentials:credsA server:server]; // Now that there's a logged-in user, we should be able to retrieve the configuration. RLMRealmConfiguration *configuration = [RLMSyncConfiguration automaticConfiguration]; XCTAssert(configuration); @autoreleasepool { // And open it successfully. RLMRealm *realm = [self openRealmWithConfiguration:configuration]; [self waitForDownloadsForRealm:realm]; } RLMSyncCredentials *credsB = [RLMObjectServerTests basicCredentialsWithName:@"b" register:YES]; RLMSyncUser *userB = [self logInUserForCredentials:credsB server:server]; // Automatic configuration should throw since there's more than one logged-in user. XCTAssertThrows([RLMSyncConfiguration automaticConfiguration]); // It should still be possible to explicitly retrieve an automatic configuration for a user. RLMRealmConfiguration *configurationA = [RLMSyncConfiguration automaticConfigurationForUser:userA]; XCTAssert(configurationA); XCTAssertEqualObjects(configuration.syncConfiguration, configurationA.syncConfiguration); RLMRealmConfiguration *configurationB = [RLMSyncConfiguration automaticConfigurationForUser:userB]; XCTAssert(configurationB); XCTAssertNotEqualObjects(configuration.syncConfiguration, configurationB.syncConfiguration); [userB logOut]; // Now that we're back to a single logged-in user, we should be able to retrieve the configuration. configuration = [RLMSyncConfiguration automaticConfiguration]; XCTAssert(configuration); } #pragma clang diagnostic pop #pragma mark - Partial sync - (void)waitForKeyPath:(NSString *)keyPath object:(id)object value:(id)value { [self waitForExpectations:@[[[XCTKVOExpectation alloc] initWithKeyPath:keyPath object:object expectedValue:value]] timeout:20.0]; } - (void)testPartialSync { // Make credentials. NSString *name = NSStringFromSelector(_cmd); NSURL *server = [RLMObjectServerTests authServerURL]; // Log in and populate the Realm. @autoreleasepool { RLMSyncCredentials *creds = [RLMObjectServerTests basicCredentialsWithName:name register:YES]; RLMSyncUser *user = [self logInUserForCredentials:creds server:server]; RLMRealmConfiguration *configuration = [user configuration]; RLMRealm *realm = [self openRealmWithConfiguration:configuration]; [realm beginWriteTransaction]; // FIXME: make this less hideous // Add ten of each object [realm addObject:[PartialSyncObjectA objectWithNumber:0 string:@"realm"]]; [realm addObject:[PartialSyncObjectA objectWithNumber:1 string:@""]]; [realm addObject:[PartialSyncObjectA objectWithNumber:2 string:@""]]; [realm addObject:[PartialSyncObjectA objectWithNumber:3 string:@""]]; [realm addObject:[PartialSyncObjectA objectWithNumber:4 string:@"realm"]]; [realm addObject:[PartialSyncObjectA objectWithNumber:5 string:@"sync"]]; [realm addObject:[PartialSyncObjectA objectWithNumber:6 string:@"partial"]]; [realm addObject:[PartialSyncObjectA objectWithNumber:7 string:@"partial"]]; [realm addObject:[PartialSyncObjectA objectWithNumber:8 string:@"partial"]]; [realm addObject:[PartialSyncObjectA objectWithNumber:9 string:@"partial"]]; [realm addObject:[PartialSyncObjectB objectWithNumber:0 firstString:@"" secondString:@""]]; [realm addObject:[PartialSyncObjectB objectWithNumber:1 firstString:@"" secondString:@""]]; [realm addObject:[PartialSyncObjectB objectWithNumber:2 firstString:@"" secondString:@""]]; [realm addObject:[PartialSyncObjectB objectWithNumber:3 firstString:@"" secondString:@""]]; [realm addObject:[PartialSyncObjectB objectWithNumber:4 firstString:@"" secondString:@""]]; [realm addObject:[PartialSyncObjectB objectWithNumber:5 firstString:@"" secondString:@""]]; [realm addObject:[PartialSyncObjectB objectWithNumber:6 firstString:@"" secondString:@""]]; [realm addObject:[PartialSyncObjectB objectWithNumber:7 firstString:@"" secondString:@""]]; [realm addObject:[PartialSyncObjectB objectWithNumber:8 firstString:@"" secondString:@""]]; [realm addObject:[PartialSyncObjectB objectWithNumber:9 firstString:@"" secondString:@""]]; [realm commitWriteTransaction]; [self waitForUploadsForRealm:realm]; } // Log back in and do partial sync stuff. @autoreleasepool { RLMSyncCredentials *creds = [RLMObjectServerTests basicCredentialsWithName:name register:NO]; RLMSyncUser *user = [self logInUserForCredentials:creds server:server]; RLMRealmConfiguration *configuration = [user configuration]; RLMRealm *realm = [self openRealmWithConfiguration:configuration]; // Perform some partial sync queries RLMResults *objects = [PartialSyncObjectA objectsInRealm:realm where:@"number > 5"]; RLMSyncSubscription *subscription = [objects subscribeWithName:@"query"]; // Wait for the results to become available. [self waitForKeyPath:@"state" object:subscription value:@(RLMSyncSubscriptionStateComplete)]; // Verify that we got what we're looking for XCTAssertEqual(objects.count, 4U); for (PartialSyncObjectA *object in objects) { XCTAssertGreaterThan(object.number, 5); XCTAssertEqualObjects(object.string, @"partial"); } // Verify that we didn't get any other objects XCTAssertEqual([PartialSyncObjectA allObjectsInRealm:realm].count, objects.count); XCTAssertEqual([PartialSyncObjectB allObjectsInRealm:realm].count, 0u); // Create a subscription with the same name but a different query. This should trigger an error. RLMResults *objects2 = [PartialSyncObjectA objectsInRealm:realm where:@"number < 5"]; RLMSyncSubscription *subscription2 = [objects2 subscribeWithName:@"query"]; // Wait for the error to be reported. [self waitForKeyPath:@"state" object:subscription2 value:@(RLMSyncSubscriptionStateError)]; XCTAssertNotNil(subscription2.error); // Unsubscribe from the query, and ensure that it correctly transitions to the invalidated state. [subscription unsubscribe]; [self waitForKeyPath:@"state" object:subscription value:@(RLMSyncSubscriptionStateInvalidated)]; } } - (RLMRealm *)partialRealmWithName:(SEL)sel { NSString *name = NSStringFromSelector(sel); NSURL *server = [RLMObjectServerTests authServerURL]; RLMSyncCredentials *creds = [RLMObjectServerTests basicCredentialsWithName:name register:YES]; RLMSyncUser *user = [self logInUserForCredentials:creds server:server]; RLMRealmConfiguration *configuration = [user configuration]; return [self openRealmWithConfiguration:configuration]; } - (void)testAllSubscriptionsReportsNewlyCreatedSubscription { RLMRealm *realm = [self partialRealmWithName:_cmd]; XCTAssertEqual(0U, realm.subscriptions.count); RLMSyncSubscription *subscription = [[PartialSyncObjectA objectsInRealm:realm where:@"number > 5"] subscribeWithName:@"query"]; // Should still be 0 because the subscription is created asynchronously XCTAssertEqual(0U, realm.subscriptions.count); [self waitForKeyPath:@"state" object:subscription value:@(RLMSyncSubscriptionStateComplete)]; XCTAssertEqual(1U, realm.subscriptions.count); RLMSyncSubscription *subscription2 = realm.subscriptions.firstObject; XCTAssertEqualObjects(@"query", subscription2.name); XCTAssertEqual(RLMSyncSubscriptionStateComplete, subscription2.state); XCTAssertNil(subscription2.error); } - (void)testAllSubscriptionsDoesNotReportLocalError { RLMRealm *realm = [self partialRealmWithName:_cmd]; RLMSyncSubscription *subscription1 = [[PartialSyncObjectA objectsInRealm:realm where:@"number > 5"] subscribeWithName:@"query"]; [self waitForKeyPath:@"state" object:subscription1 value:@(RLMSyncSubscriptionStateComplete)]; RLMSyncSubscription *subscription2 = [[PartialSyncObjectA objectsInRealm:realm where:@"number > 6"] subscribeWithName:@"query"]; [self waitForKeyPath:@"state" object:subscription2 value:@(RLMSyncSubscriptionStateError)]; XCTAssertEqual(1U, realm.subscriptions.count); } - (void)testAllSubscriptionsReportsServerError { RLMRealm *realm = [self partialRealmWithName:_cmd]; RLMSyncSubscription *subscription = [[PersonObject objectsInRealm:realm where:@"SUBQUERY(parents, $p1, $p1.age < 31 AND SUBQUERY($p1.parents, $p2, $p2.age > 35 AND $p2.name == 'Michael').@count > 0).@count > 0"] subscribeWithName:@"query"]; XCTAssertEqual(0U, realm.subscriptions.count); [self waitForKeyPath:@"state" object:subscription value:@(RLMSyncSubscriptionStateError)]; XCTAssertEqual(1U, realm.subscriptions.count); RLMSyncSubscription *subscription2 = realm.subscriptions.lastObject; XCTAssertEqualObjects(@"query", subscription2.name); XCTAssertEqual(RLMSyncSubscriptionStateError, subscription2.state); XCTAssertNotNil(subscription2.error); } - (void)testUnsubscribeUsingOriginalSubscriptionObservingFetched { RLMRealm *realm = [self partialRealmWithName:_cmd]; RLMSyncSubscription *original = [[PartialSyncObjectA allObjectsInRealm:realm] subscribeWithName:@"query"]; [self waitForKeyPath:@"state" object:original value:@(RLMSyncSubscriptionStateComplete)]; XCTAssertEqual(1U, realm.subscriptions.count); RLMSyncSubscription *fetched = realm.subscriptions.firstObject; [original unsubscribe]; [self waitForKeyPath:@"state" object:fetched value:@(RLMSyncSubscriptionStateInvalidated)]; XCTAssertEqual(0U, realm.subscriptions.count); XCTAssertEqual(RLMSyncSubscriptionStateInvalidated, original.state); // XCTKVOExpecatation retains the object and releases it sometime later on // a background thread, which causes issues if the realm is closed after // we reset the global state realm->_realm->close(); } - (void)testUnsubscribeUsingFetchedSubscriptionObservingFetched { RLMRealm *realm = [self partialRealmWithName:_cmd]; RLMSyncSubscription *original = [[PartialSyncObjectA allObjectsInRealm:realm] subscribeWithName:@"query"]; [self waitForKeyPath:@"state" object:original value:@(RLMSyncSubscriptionStateComplete)]; XCTAssertEqual(1U, realm.subscriptions.count); RLMSyncSubscription *fetched = realm.subscriptions.firstObject; [fetched unsubscribe]; [self waitForKeyPath:@"state" object:fetched value:@(RLMSyncSubscriptionStateInvalidated)]; XCTAssertEqual(0U, realm.subscriptions.count); XCTAssertEqual(RLMSyncSubscriptionStateInvalidated, original.state); // XCTKVOExpecatation retains the object and releases it sometime later on // a background thread, which causes issues if the realm is closed after // we reset the global state realm->_realm->close(); } - (void)testUnsubscribeUsingFetchedSubscriptionObservingOriginal { RLMRealm *realm = [self partialRealmWithName:_cmd]; RLMSyncSubscription *original = [[PartialSyncObjectA allObjectsInRealm:realm] subscribeWithName:@"query"]; [self waitForKeyPath:@"state" object:original value:@(RLMSyncSubscriptionStateComplete)]; XCTAssertEqual(1U, realm.subscriptions.count); RLMSyncSubscription *fetched = realm.subscriptions.firstObject; [fetched unsubscribe]; [self waitForKeyPath:@"state" object:original value:@(RLMSyncSubscriptionStateInvalidated)]; XCTAssertEqual(0U, realm.subscriptions.count); XCTAssertEqual(RLMSyncSubscriptionStateInvalidated, fetched.state); } - (void)testSubscriptionWithName { RLMRealm *realm = [self partialRealmWithName:_cmd]; XCTAssertNil([realm subscriptionWithName:@"query"]); RLMSyncSubscription *subscription = [[PartialSyncObjectA allObjectsInRealm:realm] subscribeWithName:@"query"]; XCTAssertNil([realm subscriptionWithName:@"query"]); [self waitForKeyPath:@"state" object:subscription value:@(RLMSyncSubscriptionStateComplete)]; XCTAssertNotNil([realm subscriptionWithName:@"query"]); XCTAssertNil([realm subscriptionWithName:@"query2"]); RLMSyncSubscription *subscription2 = [realm subscriptionWithName:@"query"]; XCTAssertEqualObjects(@"query", subscription2.name); XCTAssertEqual(RLMSyncSubscriptionStateComplete, subscription2.state); XCTAssertNil(subscription2.error); [subscription unsubscribe]; XCTAssertNotNil([realm subscriptionWithName:@"query"]); [self waitForKeyPath:@"state" object:subscription value:@(RLMSyncSubscriptionStateInvalidated)]; XCTAssertNil([realm subscriptionWithName:@"query"]); XCTAssertEqual(RLMSyncSubscriptionStateInvalidated, subscription2.state); } - (void)testSortAndFilterSubscriptions { RLMRealm *realm = [self partialRealmWithName:_cmd]; NSDate *now = NSDate.date; [self waitForKeyPath:@"state" object:[[PartialSyncObjectA allObjectsInRealm:realm] subscribeWithName:@"query 1"] value:@(RLMSyncSubscriptionStateComplete)]; [self waitForKeyPath:@"state" object:[[PartialSyncObjectA allObjectsInRealm:realm] subscribeWithName:@"query 2"] value:@(RLMSyncSubscriptionStateComplete)]; [self waitForKeyPath:@"state" object:[[PartialSyncObjectB allObjectsInRealm:realm] subscribeWithName:@"query 3"] value:@(RLMSyncSubscriptionStateComplete)]; RLMResults *unsupportedQuery = [PersonObject objectsInRealm:realm where:@"SUBQUERY(parents, $p1, $p1.age < 31 AND SUBQUERY($p1.parents, $p2, $p2.age > 35 AND $p2.name == 'Michael').@count > 0).@count > 0"]; [self waitForKeyPath:@"state" object:[unsupportedQuery subscribeWithName:@"query 4"] value:@(RLMSyncSubscriptionStateError)]; auto subscriptions = realm.subscriptions; XCTAssertEqual(4U, subscriptions.count); XCTAssertEqual(0U, ([subscriptions objectsWhere:@"name = %@", @"query 0"].count)); XCTAssertEqualObjects(@"query 1", ([subscriptions objectsWhere:@"name = %@", @"query 1"].firstObject.name)); XCTAssertEqual(3U, ([subscriptions objectsWhere:@"status = %@", @(RLMSyncSubscriptionStateComplete)].count)); XCTAssertEqual(1U, ([subscriptions objectsWhere:@"status = %@", @(RLMSyncSubscriptionStateError)].count)); XCTAssertEqual(4U, ([subscriptions objectsWhere:@"createdAt >= %@", now]).count); XCTAssertEqual(0U, ([subscriptions objectsWhere:@"createdAt < %@", now]).count); XCTAssertEqual(4U, [subscriptions objectsWhere:@"expiresAt = nil"].count); XCTAssertEqual(4U, [subscriptions objectsWhere:@"timeToLive = nil"].count); XCTAssertThrows([subscriptions sortedResultsUsingKeyPath:@"name" ascending:NO]); XCTAssertThrows([subscriptions sortedResultsUsingDescriptors:@[]]); XCTAssertThrows([subscriptions distinctResultsUsingKeyPaths:@[@"name"]]); } - (void)testIncludeLinkingObjectsErrorHandling { RLMRealm *realm = [self partialRealmWithName:_cmd]; RLMResults *objects = [PersonObject allObjectsInRealm:realm]; RLMSyncSubscriptionOptions *opt = [RLMSyncSubscriptionOptions new]; opt.includeLinkingObjectProperties = @[@"nonexistent"]; RLMAssertThrowsWithReason([objects subscribeWithOptions:opt], @"Invalid LinkingObjects inclusion from key path 'nonexistent': property 'PersonObject.nonexistent' does not exist."); opt.includeLinkingObjectProperties = @[@"name"]; RLMAssertThrowsWithReason([objects subscribeWithOptions:opt], @"Invalid LinkingObjects inclusion from key path 'name': property 'PersonObject.name' is of unsupported type 'string'."); opt.includeLinkingObjectProperties = @[@"children.name"]; RLMAssertThrowsWithReason([objects subscribeWithOptions:opt], @"Invalid LinkingObjects inclusion from key path 'children.name': property 'PersonObject.name' is of unsupported type 'string'."); opt.includeLinkingObjectProperties = @[@"children"]; RLMAssertThrowsWithReason([objects subscribeWithOptions:opt], @"Invalid LinkingObjects inclusion from key path 'children': key path must end in a LinkingObjects property and 'PersonObject.children' is of type 'array'."); opt.includeLinkingObjectProperties = @[@"children."]; RLMAssertThrowsWithReason([objects subscribeWithOptions:opt], @"Invalid LinkingObjects inclusion from key path 'children.': missing property name."); opt.includeLinkingObjectProperties = @[@""]; RLMAssertThrowsWithReason([objects subscribeWithOptions:opt], @"Invalid LinkingObjects inclusion from key path '': missing property name."); } #pragma mark - Certificate pinning - (void)attemptLoginWithUsername:(NSString *)userName callback:(void (^)(RLMSyncUser *, NSError *))callback { NSURL *url = [RLMObjectServerTests secureAuthServerURL]; RLMSyncCredentials *creds = [RLMObjectServerTests basicCredentialsWithName:userName register:YES]; XCTestExpectation *expectation = [self expectationWithDescription:@"HTTP login"]; [RLMSyncUser logInWithCredentials:creds authServerURL:url onCompletion:^(RLMSyncUser *user, NSError *error) { callback(user, error); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:4.0 handler:nil]; } - (void)testHTTPSLoginFailsWithoutCertificate { [self attemptLoginWithUsername:NSStringFromSelector(_cmd) callback:^(RLMSyncUser *user, NSError *error) { XCTAssertNil(user); XCTAssertNotNil(error); XCTAssertEqualObjects(error.domain, NSURLErrorDomain); XCTAssertEqual(error.code, NSURLErrorServerCertificateUntrusted); }]; } static NSURL *certificateURL(NSString *filename) { return [NSURL fileURLWithPath:[[[@(__FILE__) stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"certificates"] stringByAppendingPathComponent:filename]]; } - (void)testHTTPSLoginFailsWithIncorrectCertificate { RLMSyncManager.sharedManager.pinnedCertificatePaths = @{@"localhost": certificateURL(@"not-localhost.cer")}; [self attemptLoginWithUsername:NSStringFromSelector(_cmd) callback:^(RLMSyncUser *user, NSError *error) { XCTAssertNil(user); XCTAssertNotNil(error); XCTAssertEqualObjects(error.domain, NSURLErrorDomain); XCTAssertEqual(error.code, NSURLErrorServerCertificateUntrusted); }]; } - (void)testHTTPSLoginFailsWithInvalidPathToCertificate { RLMSyncManager.sharedManager.pinnedCertificatePaths = @{@"localhost": certificateURL(@"nonexistent.pem")}; [self attemptLoginWithUsername:NSStringFromSelector(_cmd) callback:^(RLMSyncUser *user, NSError *error) { XCTAssertNil(user); XCTAssertNotNil(error); XCTAssertEqualObjects(error.domain, NSCocoaErrorDomain); XCTAssertEqual(error.code, NSFileReadNoSuchFileError); }]; } - (void)testHTTPSLoginFailsWithDifferentValidCert { RLMSyncManager.sharedManager.pinnedCertificatePaths = @{@"localhost": certificateURL(@"localhost-other.cer")}; [self attemptLoginWithUsername:NSStringFromSelector(_cmd) callback:^(RLMSyncUser *user, NSError *error) { XCTAssertNil(user); XCTAssertNotNil(error); XCTAssertEqualObjects(error.domain, NSURLErrorDomain); XCTAssertEqual(error.code, NSURLErrorServerCertificateUntrusted); }]; } - (void)testHTTPSLoginFailsWithFileThatIsNotACert { RLMSyncManager.sharedManager.pinnedCertificatePaths = @{@"localhost": certificateURL(@"../test-ros-server.js")}; [self attemptLoginWithUsername:NSStringFromSelector(_cmd) callback:^(RLMSyncUser *user, NSError *error) { XCTAssertNil(user); XCTAssertNotNil(error); XCTAssertEqualObjects(error.domain, NSOSStatusErrorDomain); XCTAssertEqual(error.code, errSecUnknownFormat); }]; } - (void)testHTTPSLoginDoesNotUseCertificateForDifferentDomain { RLMSyncManager.sharedManager.pinnedCertificatePaths = @{@"example.com": certificateURL(@"localhost.cer")}; [self attemptLoginWithUsername:NSStringFromSelector(_cmd) callback:^(RLMSyncUser *user, NSError *error) { XCTAssertNil(user); XCTAssertNotNil(error); XCTAssertEqualObjects(error.domain, NSURLErrorDomain); XCTAssertEqual(error.code, NSURLErrorServerCertificateUntrusted); }]; } - (void)testHTTPSLoginSucceedsWithValidSelfSignedCertificate { RLMSyncManager.sharedManager.pinnedCertificatePaths = @{@"localhost": certificateURL(@"localhost.cer")}; [self attemptLoginWithUsername:NSStringFromSelector(_cmd) callback:^(RLMSyncUser *user, NSError *error) { XCTAssertNotNil(user); XCTAssertNil(error); }]; } - (void)testConfigurationFromUserAutomaticallyUsesCert { RLMSyncManager.sharedManager.pinnedCertificatePaths = @{@"localhost": certificateURL(@"localhost.cer")}; __block RLMSyncUser *user; [self attemptLoginWithUsername:NSStringFromSelector(_cmd) callback:^(RLMSyncUser *u, NSError *error) { XCTAssertNotNil(u); XCTAssertNil(error); user = u; }]; RLMRealmConfiguration *config = [user configuration]; XCTAssertEqualObjects(config.syncConfiguration.realmURL.scheme, @"realms"); XCTAssertEqualObjects(config.syncConfiguration.pinnedCertificateURL, certificateURL(@"localhost.cer")); // Verify that we can actually open the Realm auto realm = [self openRealmWithConfiguration:config]; NSError *error; [self waitForUploadsForRealm:realm error:&error]; XCTAssertNil(error); } - (void)verifyOpenSucceeds:(RLMRealmConfiguration *)config { auto realm = [self openRealmWithConfiguration:config]; NSError *error; [self waitForUploadsForRealm:realm error:&error]; XCTAssertNil(error); } - (void)verifyOpenFails:(RLMRealmConfiguration *)config { [self openRealmWithConfiguration:config]; XCTestExpectation *expectation = [self expectationWithDescription:@"wait for error"]; RLMSyncManager.sharedManager.errorHandler = ^(NSError *error, __unused RLMSyncSession *session) { XCTAssertTrue([error.domain isEqualToString:RLMSyncErrorDomain]); XCTAssertFalse([[error.userInfo[kRLMSyncUnderlyingErrorKey] domain] isEqualToString:RLMSyncErrorDomain]); [expectation fulfill]; }; [self waitForExpectationsWithTimeout:20.0 handler:nil]; } - (void)testConfigurationFromInsecureUserAutomaticallyUsesCert { RLMSyncManager.sharedManager.pinnedCertificatePaths = @{@"localhost": certificateURL(@"localhost.cer")}; RLMSyncUser *user = [self logInUserForCredentials:[RLMSyncTestCase basicCredentialsWithName:NSStringFromSelector(_cmd) register:YES] server:[RLMSyncTestCase authServerURL]]; RLMRealmConfiguration *config = [user configurationWithURL:[NSURL URLWithString:@"realms://localhost:9443/~/default"]]; XCTAssertEqualObjects(config.syncConfiguration.realmURL.scheme, @"realms"); XCTAssertEqualObjects(config.syncConfiguration.pinnedCertificateURL, certificateURL(@"localhost.cer")); [self verifyOpenSucceeds:config]; } - (void)testOpenSecureRealmWithNoCert { RLMSyncUser *user = [self logInUserForCredentials:[RLMSyncTestCase basicCredentialsWithName:NSStringFromSelector(_cmd) register:YES] server:[RLMSyncTestCase authServerURL]]; [self verifyOpenFails:[user configurationWithURL:[NSURL URLWithString:@"realms://localhost:9443/~/default"]]]; } - (void)testOpenSecureRealmWithIncorrectCert { RLMSyncManager.sharedManager.pinnedCertificatePaths = @{@"localhost": certificateURL(@"not-localhost.cer")}; RLMSyncUser *user = [self logInUserForCredentials:[RLMSyncTestCase basicCredentialsWithName:NSStringFromSelector(_cmd) register:YES] server:[RLMSyncTestCase authServerURL]]; [self verifyOpenFails:[user configurationWithURL:[NSURL URLWithString:@"realms://localhost:9443/~/default"]]]; } - (void)DISABLE_testOpenSecureRealmWithMissingCertFile { // FIXME: this currently crashes inside the sync library RLMSyncManager.sharedManager.pinnedCertificatePaths = @{@"localhost": certificateURL(@"nonexistent.pem")}; RLMSyncUser *user = [self logInUserForCredentials:[RLMSyncTestCase basicCredentialsWithName:NSStringFromSelector(_cmd) register:YES] server:[RLMSyncTestCase authServerURL]]; [self verifyOpenFails:[user configurationWithURL:[NSURL URLWithString:@"realms://localhost:9443/~/default"]]]; } #pragma mark - Custom request headers - (void)testLoginFailsWithoutCustomHeader { XCTestExpectation *expectation = [self expectationWithDescription:@"register user"]; [RLMSyncUser logInWithCredentials:[RLMSyncTestCase basicCredentialsWithName:NSStringFromSelector(_cmd) register:YES] authServerURL:[NSURL URLWithString:@"http://127.0.0.1:9081"] onCompletion:^(RLMSyncUser *user, NSError *error) { XCTAssertNil(user); XCTAssertNotNil(error); XCTAssertEqualObjects(@400, error.userInfo[@"statusCode"]); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:4.0 handler:nil]; } - (void)testLoginUsesCustomHeader { RLMSyncManager.sharedManager.customRequestHeaders = @{@"X-Allow-Connection": @"true"}; RLMSyncUser *user = [self logInUserForCredentials:[RLMSyncTestCase basicCredentialsWithName:NSStringFromSelector(_cmd) register:YES] server:[NSURL URLWithString:@"http://127.0.0.1:9081"]]; XCTAssertNotNil(user); } - (void)testModifyCustomHeadersAfterOpeningRealm { RLMSyncManager.sharedManager.customRequestHeaders = @{@"X-Allow-Connection": @"true"}; RLMSyncUser *user = [self logInUserForCredentials:[RLMSyncTestCase basicCredentialsWithName:NSStringFromSelector(_cmd) register:YES] server:[NSURL URLWithString:@"http://127.0.0.1:9081"]]; XCTAssertNotNil(user); RLMSyncManager.sharedManager.customRequestHeaders = nil; NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"realm://127.0.0.1:9081/~/%@", NSStringFromSelector(_cmd)]]; auto c = [user configurationWithURL:url fullSynchronization:true]; // Should initially fail to connect due to the missing header XCTestExpectation *ex1 = [self expectationWithDescription:@"connection failure"]; RLMSyncManager.sharedManager.errorHandler = ^(NSError *error, RLMSyncSession *) { XCTAssertNotNil(error); XCTAssertEqualObjects(@400, [error.userInfo[@"underlying_error"] userInfo][@"statusCode"]); [ex1 fulfill]; }; RLMRealm *realm = [RLMRealm realmWithConfiguration:c error:nil]; RLMSyncSession *syncSession = realm.syncSession; [self waitForExpectationsWithTimeout:4.0 handler:nil]; XCTAssertEqual(syncSession.connectionState, RLMSyncConnectionStateDisconnected); // Should successfully connect once the header is set RLMSyncManager.sharedManager.errorHandler = nil; auto ex2 = [[XCTKVOExpectation alloc] initWithKeyPath:@"connectionState" object:syncSession expectedValue:@(RLMSyncConnectionStateConnected)]; RLMSyncManager.sharedManager.customRequestHeaders = @{@"X-Allow-Connection": @"true"}; [self waitForExpectations:@[ex2] timeout:4.0]; // Should disconnect and fail to reconnect when the wrong header is set XCTestExpectation *ex3 = [self expectationWithDescription:@"reconnection failure"]; RLMSyncManager.sharedManager.errorHandler = ^(NSError *error, RLMSyncSession *) { XCTAssertNotNil(error); XCTAssertEqualObjects(@400, [error.userInfo[@"underlying_error"] userInfo][@"statusCode"]); [ex3 fulfill]; }; auto ex4 = [[XCTKVOExpectation alloc] initWithKeyPath:@"connectionState" object:syncSession expectedValue:@(RLMSyncConnectionStateDisconnected)]; RLMSyncManager.sharedManager.customRequestHeaders = @{@"X-Other-Header": @"true"}; [self waitForExpectations:@[ex3, ex4] timeout:4.0]; } @end