//////////////////////////////////////////////////////////////////////////// // // 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 "RLMSyncUtil_Private.h" #import "RLMRealm+Sync.h" #import "RLMRealmConfiguration_Private.h" #import "RLMRealmUtil.hpp" #import "RLMRealm_Dynamic.h" #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 @implementation PersonObject + (NSDictionary *)linkingObjectsProperties { return @{@"parents": [RLMPropertyDescriptor descriptorWithClass:PersonObject.class propertyName:@"children"]}; } @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)); } else { // Write lots of data to the Realm, then wait for it to be uploaded. [realm beginWriteTransaction]; for (NSInteger i=0; i= transferred); XCTAssert(xfb >= transferrable); transferred = xfr; transferrable = xfb; callCount++; if (transferred > 0 && transferred >= transferrable && !hasBeenFulfilled) { [ex fulfill]; hasBeenFulfilled = YES; } }]; // 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(); // Log in the user. RLMSyncUser *user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd) register:self.isParent] server:[RLMObjectServerTests authServerURL]]; if (self.isParent) { // 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; }; NSUInteger sizeBefore = fileSize(c.pathOnDisk); @autoreleasepool { // We have partial transaction logs but no data XCTAssertGreaterThan(sizeBefore, 0U); XCTAssertTrue([[RLMRealm realmWithConfiguration:c error:nil] isEmpty]); } XCTAssertNil(RLMGetAnyCachedRealmForPath(c.pathOnDisk.UTF8String)); [self waitForExpectationsWithTimeout:10.0 handler:nil]; XCTAssertGreaterThan(fileSize(c.pathOnDisk), sizeBefore); XCTAssertNil(RLMGetAnyCachedRealmForPath(c.pathOnDisk.UTF8String)); } else { RLMRealm *realm = [self openRealmForURL:url user:user]; // Write lots of data to the Realm, then wait for it to be uploaded. [realm beginWriteTransaction]; for (NSInteger i=0; i 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); } - (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); } - (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]; [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)); XCTAssertThrows([subscriptions sortedResultsUsingKeyPath:@"name" ascending:NO]); XCTAssertThrows([subscriptions sortedResultsUsingDescriptors:@[]]); XCTAssertThrows([subscriptions distinctResultsUsingKeyPaths:@[@"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"]]]; } @end