An amazing project that generates micro reports from tournament results
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2263 lines
116 KiB

////////////////////////////////////////////////////////////////////////////
//
// 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
@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<OBJECT_COUNT; i++) {
[realm addObject:[[SyncObject alloc] initWithValue:@[[NSString stringWithFormat:@"parent-%@", @(i+1)]]]];
}
[realm commitWriteTransaction];
CHECK_COUNT(OBJECT_COUNT, SyncObject, realm);
}
// Run the sub-test. (Give the upload a bit of time to start.)
// NOTE: This sleep should be fine because:
// - There is currently no API that allows asynchronous coordination for waiting for an upload to begin.
// - A delay longer than the specified one will not affect the outcome of the test.
sleep(2);
RLMRunChildAndWait();
} else {
RLMRealm *realm = [self openRealmForURL:url user:user];
// Wait for download to complete.
[self waitForDownloadsForRealm:realm];
CHECK_COUNT(OBJECT_COUNT, SyncObject, realm);
}
}
#pragma mark - Logging Back In
/// A Realm that was opened before a user logged out should be able to resume uploading if the user logs back in.
- (void)testLogBackInSameRealmUpload {
NSURL *url = REALM_URL();
// Log in the user.
RLMSyncUser *user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd)
register:self.isParent]
server:[RLMObjectServerTests authServerURL]];
RLMRealm *realm = [self openRealmForURL:url user:user];
if (self.isParent) {
[self addSyncObjectsToRealm:realm descriptions:@[@"parent-1"]];
CHECK_COUNT(1, SyncObject, realm);
[self waitForUploadsForRealm:realm];
// Log out the user.
[user logOut];
// Log the user back in.
user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd)
register:NO]
server:[RLMObjectServerTests authServerURL]];
[self addSyncObjectsToRealm:realm descriptions:@[@"parent-2", @"parent-3"]];
[self waitForUploadsForRealm:realm];
CHECK_COUNT(3, SyncObject, realm);
RLMRunChildAndWait();
} else {
[self waitForDownloadsForRealm:realm];
CHECK_COUNT(3, SyncObject, realm);
}
}
/// A Realm that was opened before a user logged out should be able to resume downloading if the user logs back in.
- (void)testLogBackInSameRealmDownload {
NSURL *url = REALM_URL();
// Log in the user.
RLMSyncUser *user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd)
register:self.isParent]
server:[RLMObjectServerTests authServerURL]];
RLMRealm *realm = [self openRealmForURL:url user:user];
if (self.isParent) {
[self addSyncObjectsToRealm:realm descriptions:@[@"parent-1"]];
CHECK_COUNT(1, SyncObject, realm);
[self waitForUploadsForRealm:realm];
// Log out the user.
[user logOut];
// Log the user back in.
user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd)
register:NO]
server:[RLMObjectServerTests authServerURL]];
RLMRunChildAndWait();
[self waitForDownloadsForRealm:realm];
CHECK_COUNT(3, SyncObject, realm);
} else {
[self waitForDownloadsForRealm:realm];
[self addSyncObjectsToRealm:realm descriptions:@[@"child-1", @"child-2"]];
[self waitForUploadsForRealm:realm];
CHECK_COUNT(3, SyncObject, realm);
}
}
/// A Realm that was opened while a user was logged out should be able to start uploading if the user logs back in.
- (void)testLogBackInDeferredRealmUpload {
NSURL *url = REALM_URL();
// Log in the user.
RLMSyncUser *user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd)
register:self.isParent]
server:[RLMObjectServerTests authServerURL]];
NSError *error = nil;
if (self.isParent) {
// Semaphore for knowing when the Realm is successfully opened for sync.
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
RLMRealmConfiguration *config = [user configurationWithURL:url fullSynchronization:true];
[user logOut];
// Open a Realm after the user's been logged out.
[self primeSyncManagerWithSemaphore:sema];
RLMRealm *realm = [RLMRealm realmWithConfiguration:config error:&error];
XCTAssertNil(error, @"Error when opening Realm: %@", error);
[self addSyncObjectsToRealm:realm descriptions:@[@"parent-1"]];
CHECK_COUNT(1, SyncObject, realm);
user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd)
register:NO]
server:[RLMObjectServerTests authServerURL]];
// Wait for the Realm's session to be bound.
WAIT_FOR_SEMAPHORE(sema, 30);
[self addSyncObjectsToRealm:realm descriptions:@[@"parent-2", @"parent-3"]];
[self waitForUploadsForRealm:realm];
CHECK_COUNT(3, SyncObject, realm);
RLMRunChildAndWait();
} else {
RLMRealm *realm = [self openRealmForURL:url user:user];
XCTAssertNil(error, @"Error when opening Realm: %@", error);
[self waitForDownloadsForRealm:realm];
CHECK_COUNT(3, SyncObject, realm);
}
}
/// A Realm that was opened while a user was logged out should be able to start downloading if the user logs back in.
- (void)testLogBackInDeferredRealmDownload {
NSURL *url = REALM_URL();
// Log in the user.
RLMSyncUser *user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd)
register:self.isParent]
server:[RLMObjectServerTests authServerURL]];
NSError *error = nil;
if (self.isParent) {
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
RLMRunChildAndWait();
RLMRealmConfiguration *config = [user configurationWithURL:url fullSynchronization:true];
[user logOut];
// Open a Realm after the user's been logged out.
[self primeSyncManagerWithSemaphore:sema];
RLMRealm *realm = [RLMRealm realmWithConfiguration:config error:&error];
XCTAssertNil(error, @"Error when opening Realm: %@", error);
[self addSyncObjectsToRealm:realm descriptions:@[@"parent-1"]];
CHECK_COUNT(1, SyncObject, realm);
user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd)
register:NO]
server:[RLMObjectServerTests authServerURL]];
// Wait for the Realm's session to be bound.
WAIT_FOR_SEMAPHORE(sema, 30);
[self waitForDownloadsForUser:user realms:@[realm] realmURLs:@[url] expectedCounts:@[@4]];
} else {
RLMRealm *realm = [self openRealmForURL:url user:user];
XCTAssertNil(error, @"Error when opening Realm: %@", error);
[self addSyncObjectsToRealm:realm descriptions:@[@"child-1", @"child-2", @"child-3"]];
[self waitForUploadsForRealm:realm];
CHECK_COUNT(3, SyncObject, realm);
}
}
/// After logging back in, a Realm whose path has been opened for the first time should properly upload changes.
- (void)testLogBackInOpenFirstTimePathUpload {
NSURL *url = REALM_URL();
// Log in the user.
RLMSyncUser *user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd)
register:self.isParent]
server:[RLMObjectServerTests authServerURL]];
// Now run a basic multi-client test.
if (self.isParent) {
// Log out the user.
[user logOut];
// Log the user back in.
user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd)
register:NO]
server:[RLMObjectServerTests authServerURL]];
// Open the Realm (for the first time).
RLMRealm *realm = [self openRealmForURL:url user:user];
[self addSyncObjectsToRealm:realm descriptions:@[@"child-1", @"child-2"]];
[self waitForUploadsForRealm:realm];
CHECK_COUNT(2, SyncObject, realm);
RLMRunChildAndWait();
} else {
RLMRealm *realm = [self openRealmForURL:url user:user];
// Add objects.
[self waitForDownloadsForRealm:realm];
CHECK_COUNT(2, SyncObject, realm);
}
}
/// After logging back in, a Realm whose path has been opened for the first time should properly download changes.
- (void)testLogBackInOpenFirstTimePathDownload {
NSURL *url = REALM_URL();
// Log in the user.
RLMSyncUser *user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd)
register:self.isParent]
server:[RLMObjectServerTests authServerURL]];
// Now run a basic multi-client test.
if (self.isParent) {
// Log out the user.
[user logOut];
// Log the user back in.
user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd)
register:NO]
server:[RLMObjectServerTests authServerURL]];
// Open the Realm (for the first time).
RLMRealm *realm = [self openRealmForURL:url user:user];
// Run the sub-test.
RLMRunChildAndWait();
[self waitForDownloadsForRealm:realm];
CHECK_COUNT(2, SyncObject, realm);
} else {
RLMRealm *realm = [self openRealmForURL:url user:user];
// Add objects.
[self waitForDownloadsForRealm:realm];
[self addSyncObjectsToRealm:realm descriptions:@[@"child-1", @"child-2"]];
[self waitForUploadsForRealm:realm];
CHECK_COUNT(2, SyncObject, realm);
}
}
/// If a client logs in, connects, logs out, and logs back in, sync should properly upload changes for a new
/// `RLMRealm` that is opened for the same path as a previously-opened Realm.
- (void)testLogBackInReopenRealmUpload {
NSURL *url = REALM_URL();
// Log in the user.
RLMSyncUser *user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd)
register:self.isParent]
server:[RLMObjectServerTests authServerURL]];
// Open the Realm
RLMRealm *realm = [self openRealmForURL:url user:user];
if (self.isParent) {
[self addSyncObjectsToRealm:realm descriptions:@[@"parent-1"]];
[self waitForUploadsForRealm:realm];
CHECK_COUNT(1, SyncObject, realm);
// Log out the user.
[user logOut];
// Log the user back in.
user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd)
register:NO]
server:[RLMObjectServerTests authServerURL]];
// Open the Realm again.
realm = [self immediatelyOpenRealmForURL:url user:user];
[self addSyncObjectsToRealm:realm descriptions:@[@"child-1", @"child-2", @"child-3", @"child-4"]];
CHECK_COUNT(5, SyncObject, realm);
[self waitForUploadsForRealm:realm];
RLMRunChildAndWait();
} else {
[self waitForDownloadsForRealm:realm];
CHECK_COUNT(5, SyncObject, realm);
}
}
/// If a client logs in, connects, logs out, and logs back in, sync should properly download changes for a new
/// `RLMRealm` that is opened for the same path as a previously-opened Realm.
- (void)testLogBackInReopenRealmDownload {
NSURL *url = REALM_URL();
// Log in the user.
RLMSyncUser *user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd)
register:self.isParent]
server:[RLMObjectServerTests authServerURL]];
// Open the Realm
RLMRealm *realm = [self openRealmForURL:url user:user];
if (self.isParent) {
[self addSyncObjectsToRealm:realm descriptions:@[@"parent-1"]];
[self waitForUploadsForRealm:realm];
XCTAssert([SyncObject allObjectsInRealm:realm].count == 1, @"Expected 1 item");
// Log out the user.
[user logOut];
// Log the user back in.
user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:NSStringFromSelector(_cmd)
register:NO]
server:[RLMObjectServerTests authServerURL]];
// Run the sub-test.
RLMRunChildAndWait();
// Open the Realm again and get the items.
realm = [self immediatelyOpenRealmForURL:url user:user];
[self waitForDownloadsForUser:user realms:@[realm] realmURLs:@[url] expectedCounts:@[@5]];
} else {
// Add objects.
[self waitForDownloadsForRealm:realm];
CHECK_COUNT(1, SyncObject, realm);
[self addSyncObjectsToRealm:realm descriptions:@[@"child-1", @"child-2", @"child-3", @"child-4"]];
[self waitForUploadsForRealm:realm];
CHECK_COUNT(5, SyncObject, realm);
}
}
#pragma mark - Session suspend and resume
- (void)testSuspendAndResume {
NSURL *urlA = CUSTOM_REALM_URL(@"a");
NSURL *urlB = CUSTOM_REALM_URL(@"b");
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];
if (self.isParent) {
[self waitForDownloadsForRealm:realmA];
[self waitForDownloadsForRealm:realmB];
CHECK_COUNT(0, SyncObject, realmA);
CHECK_COUNT(0, SyncObject, realmB);
// Suspend the session for realm A and then add an object to each Realm
RLMSyncSession *sessionA = [RLMSyncSession sessionForRealm:realmA];
[sessionA suspend];
[self addSyncObjectsToRealm:realmA descriptions:@[@"child-A1"]];
[self addSyncObjectsToRealm:realmB descriptions:@[@"child-B1"]];
[self waitForUploadsForRealm:realmB];
RLMRunChildAndWait();
// A should still be 1 since it's suspended. If it wasn't suspended, it
// should have downloaded before B due to the ordering in the child.
[self waitForDownloadsForRealm:realmB];
CHECK_COUNT(1, SyncObject, realmA);
CHECK_COUNT(3, SyncObject, realmB);
// A should see the other two from the child after resuming
[sessionA resume];
[self waitForDownloadsForRealm:realmA];
CHECK_COUNT(3, SyncObject, realmA);
} else {
// Child shouldn't see the object in A
[self waitForDownloadsForRealm:realmA];
[self waitForDownloadsForRealm:realmB];
CHECK_COUNT(0, SyncObject, realmA);
CHECK_COUNT(1, SyncObject, realmB);
[self addSyncObjectsToRealm:realmA descriptions:@[@"child-A2", @"child-A3"]];
[self waitForUploadsForRealm:realmA];
[self addSyncObjectsToRealm:realmB descriptions:@[@"child-B2", @"child-B3"]];
[self waitForUploadsForRealm:realmB];
CHECK_COUNT(2, SyncObject, realmA);
CHECK_COUNT(3, SyncObject, realmB);
}
}
#pragma mark - Client reset
/// Ensure that a client reset error is propagated up to the binding successfully.
- (void)testClientReset {
NSURL *url = REALM_URL();
NSString *sessionName = NSStringFromSelector(_cmd);
RLMSyncUser *user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:sessionName
register:true]
server:[RLMObjectServerTests authServerURL]];
// Open the Realm
__attribute__((objc_precise_lifetime)) RLMRealm *realm = [self openRealmForURL:url user:user];
__block NSError *theError = nil;
XCTestExpectation *ex = [self expectationWithDescription:@"Waiting for error handler to be called..."];
[RLMSyncManager sharedManager].errorHandler = ^void(NSError *error, RLMSyncSession *session) {
// Make sure we're actually looking at the right session.
XCTAssertTrue([[session.realmURL absoluteString] rangeOfString:sessionName].location != NSNotFound);
theError = error;
[ex fulfill];
};
[user simulateClientResetErrorForSession:url];
[self waitForExpectationsWithTimeout:10 handler:nil];
XCTAssertNotNil(theError);
XCTAssertTrue(theError.code == RLMSyncErrorClientResetError);
NSString *pathValue = [theError rlmSync_clientResetBackedUpRealmPath];
XCTAssertNotNil(pathValue);
// Sanity check the recovery path.
NSString *recoveryPath = @"io.realm.object-server-recovered-realms/recovered_realm";
XCTAssertTrue([pathValue rangeOfString:recoveryPath].location != NSNotFound);
XCTAssertNotNil([theError rlmSync_errorActionToken]);
}
/// Test manually initiating client reset.
- (void)testClientResetManualInitiation {
NSURL *url = REALM_URL();
NSString *sessionName = NSStringFromSelector(_cmd);
RLMSyncUser *user = [self logInUserForCredentials:[RLMObjectServerTests basicCredentialsWithName:sessionName
register:true]
server:[RLMObjectServerTests authServerURL]];
__block NSError *theError = nil;
@autoreleasepool {
__attribute__((objc_precise_lifetime)) RLMRealm *realm = [self openRealmForURL:url user:user];
XCTestExpectation *ex = [self expectationWithDescription:@"Waiting for error handler to be called..."];
[RLMSyncManager sharedManager].errorHandler = ^void(NSError *error, RLMSyncSession *session) {
// Make sure we're actually looking at the right session.
XCTAssertTrue([[session.realmURL absoluteString] rangeOfString:sessionName].location != NSNotFound);
theError = error;
[ex fulfill];
};
[user simulateClientResetErrorForSession:url];
[self waitForExpectationsWithTimeout:10 handler:nil];
XCTAssertNotNil(theError);
}
// At this point the Realm should be invalidated and client reset should be possible.
NSString *pathValue = [theError rlmSync_clientResetBackedUpRealmPath];
XCTAssertFalse([[NSFileManager defaultManager] fileExistsAtPath:pathValue]);
[RLMSyncSession immediatelyHandleError:[theError rlmSync_errorActionToken]];
XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:pathValue]);
}
#pragma mark - Progress Notifications
- (void)testStreamingDownloadNotifier {
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]];
__block NSInteger callCount = 0;
__block NSUInteger transferred = 0;
__block NSUInteger transferrable = 0;
// Open the Realm
RLMRealm *realm = [self openRealmForURL:url user:user];
if (self.isParent) {
__block BOOL hasBeenFulfilled = NO;
// Register a notifier.
RLMSyncSession *session = [user sessionForURL:url];
XCTAssertNotNil(session);
XCTestExpectation *ex = [self expectationWithDescription:@"streaming-download-notifier"];
RLMProgressNotificationToken *token = [session addProgressNotificationForDirection:RLMSyncProgressDirectionDownload
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 (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<NUMBER_OF_BIG_OBJECTS; i++) {
[realm addObject:[HugeSyncObject object]];
}
[realm commitWriteTransaction];
[self waitForUploadsForRealm:realm];
CHECK_COUNT(NUMBER_OF_BIG_OBJECTS, HugeSyncObject, realm);
}
}
- (void)testStreamingUploadNotifier {
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]];
__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<NUMBER_OF_BIG_OBJECTS; i++) {
[realm addObject:[HugeSyncObject object]];
}
[realm commitWriteTransaction];
// Wait for upload to begin and finish
[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));
}
#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;
};
XCTAssertNil(RLMGetAnyCachedRealmForPath(c.pathOnDisk.UTF8String));
[self waitForExpectationsWithTimeout:10.0 handler:nil];
XCTAssertGreaterThan(fileSize(c.pathOnDisk), 0U);
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<NUMBER_OF_BIG_OBJECTS; i++) {
[realm addObject:[HugeSyncObject object]];
}
[realm commitWriteTransaction];
[self waitForUploadsForRealm:realm];
CHECK_COUNT(NUMBER_OF_BIG_OBJECTS, HugeSyncObject, realm);
}
}
- (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) {
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 < NUMBER_OF_BIG_OBJECTS; i++) {
[realm addObject:[HugeSyncObject object]];
}
[realm commitWriteTransaction];
[self waitForUploadsForRealm:realm];
CHECK_COUNT(NUMBER_OF_BIG_OBJECTS, HugeSyncObject, realm);
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];
}
#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;
config.shouldCompactOnLaunch = ^(NSUInteger, NSUInteger){
blockCalled = true;
return YES;
};
@autoreleasepool {
[RLMRealm realmWithConfiguration:config error:nil];
}
XCTAssertTrue(blockCalled);
auto finalSize = [[fileManager attributesOfItemAtPath:path error:nil][NSFileSize] unsignedLongLongValue];
XCTAssertLessThan(finalSize, initialSize);
// Immediately after compaction the file is two pages (8192 bytes), but it
// grows to three pages shortly later due to sync performing a write
XCTAssertLessThanOrEqual(finalSize, 12288U);
}
#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