Merge pull request #1681 from realm/tg/permissions

Add basic object-level permissions support
This commit is contained in:
Thomas Goyne 2018-03-06 10:29:44 -08:00 committed by GitHub
commit d90403b279
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 855 additions and 461 deletions

View File

@ -15,7 +15,14 @@
- Any number of sort/distinct conditions can be indicated, they will be applied in the specified order. - Any number of sort/distinct conditions can be indicated, they will be applied in the specified order.
- Sort or distinct cannot operate independently, these conditions must be attached to at least one query filter. - Sort or distinct cannot operate independently, these conditions must be attached to at least one query filter.
* [Sync] Added `Realm.Results.subscribe()` to subscribe to partial synced Realms. * [Sync] Added `Realm.Results.subscribe()` to subscribe to partial synced Realms.
* [Sync] Added class `Realm.Sync.Subscription` to support partial synced Realms. * [Sync] Added class `Realm.Sync.Subscription` and enum `Realm.Sync.SubscriptionState` to support partial synced Realms.
* [Sync] Added an object-level permission subsystem. It is possible to grant fine-grained priviliges to users.
* Added object-level permissions:
- Schemas `Realm.Permissions.Realm`, `Realm.Permissions.Class`, `Realm.Permissions.Role`, `Realm.Permissions.User`, and `Realm.Permissions.Permission` to support working with permissions. These schemas can be used in user-defined Realms and schemas.
- Permissions are enforced by the object server but connectivity is not required.
- Method `Realm.privilges()` to compute privileges on a Realm, a Realm object schema, or a Realm object. The method returns either a `Realm.Permissions.Realm` or `Realm.Permissions.Class` object.
- For non-synced Realms, all privileges are always granted.
- For more details, please read the reference documentation.
### Internal ### Internal
* Updated to Realm Core 5.3.0. * Updated to Realm Core 5.3.0.

View File

@ -229,3 +229,161 @@ class PermissionOfferResponse {
*/ */
get realmUrl() {} get realmUrl() {}
} }
/**
* A permission which can be applied to a Realm, Class, or specific Object.
* Permissions are applied by adding the permission to the Realm.Permission singleton
* object, the RealmClass.Permission object for the desired class, or to a user-defined
* Realm.List<Realm.Permission> property on a specific Object instance. The meaning of each of
* the properties of Permission depend on what the permission is applied to, and so are
* left undocumented here.
* @since 2.3.0
*/
class Permission {
/**
* The Role which this Permission applies to. All users within the Role are
* granted the permissions specified by the fields below any
* objects/classes/realms which use this Permission.
*
* This property cannot be modified once set.
* @type {Role}
*/
get role() {}
/**
* Whether the user can read the object to which this Permission is attached.
* @type {boolean}
*/
get canRead() {}
/**
* Whether the user can modify the object to which this Permission is attached.
* @type {boolean}
*/
get canUpdate() {}
/**
* Whether the user can delete the object to which this Permission is attached.
*
* This property is only applicable to Permissions attached to Objects, and not
* to Realms or Classes.
* @type {boolean}
*/
get canDelete() {}
/**
* Whether the user can add or modify Permissions for the object which this
* Permission is attached to.
* @type {boolean}
*/
get canSetPermissions() {}
/**
* Whether the user can subscribe to queries for this object type.
*
* This property is only applicable to Permissions attached to Classes, and not
* to Realms or Objects.
* @type {boolean}
*/
get canQuery() {}
/**
* Whether the user can create new objects of the type this Permission is attached to.
*
* This property is only applicable to Permissions attached to Classes, and not
* to Realms or Objects.
* @type {boolean}
*/
get canCreate() {}
/**
* Whether the user can modify the schema of the Realm which this
* Permission is attached to.
*
* This property is only applicable to Permissions attached to Realms, and not
* to Realms or Objects.
* @type {boolean}
*/
get canModifySchema() {}
}
/**
* A representation of a sync user within the permissions system.
*
* User objects are created automatically for each sync user which connects to
* a Realm, and can also be created manually if you wish to grant permissions to a user
* which has not yet connected to this Realm.
* @since 2.3.0
*/
class User {
/**
* The unique Realm Object Server user ID string identifying this user. This will have
* the same value as Realm.Sync.User.identity.
* @type {string}
*/
get id() {}
}
/**
* A Role within the permissions system.
*
* A Role consists of a name for the role and a list of users which are members of the role.
* Roles are granted privileges on Realms, Classes and Objects, and in turn grant those
* privileges to all users which are members of the role.
* A role named "everyone" is automatically created in new Realms, and all new users which
* connect to the Realm are automatically added to it. Any other roles you wish to use are
* managed as normal Realm objects.
* @since 2.3.0
*/
class Role {
/**
* The name of the Role.
* @type {string}
*/
get name() {}
/**
* The users which belong to the role.
* @type {Array<Realm.Sync.Permissions.User>}
*/
get members() {}
}
/**
* An object which describes class-wide permissions.
*
* An instance of this object is automatically created in the Realm for class in your schema,
* and should not be created manually.
* @since 2.3.0
*/
class Class {
/**
* The name of the class which these permissions apply to.
* @type {string}
*/
get class_name() {}
/**
* The permissions for this class.
* @type {Array<Realm.Sync.Permissions.Permission>}
*/
get permissions() {}
}
/**
* A singleton object which describes Realm-wide permissions.
*
* An object of this type is automatically created in the Realm for you, and more objects
* cannot be created manually.
* @since 2.3.0
*/
class Realm {
/**
* The permissions for the Realm.
* @type {Array<Realm.Sync.Permission>}
*/
get permissions() {}
}

View File

@ -125,6 +125,27 @@ class Realm {
*/ */
close() {} close() {}
/**
* Returns the granted privilges.
*
* This combines all privileges granted on the Realm/Class/Object by all Roles which
* the current User is a member of into the final privileges which will
* be enforced by the server.
*
* The privilege calculation is done locally using cached data, and inherently may
* be stale. It is possible that this method may indicate that an operation is
* permitted but the server will still reject it if permission is revoked before
* the changes have been integrated on the server.
*
* Non-synchronized Realms always have permission to perform all operations.
*
* @param {(Realm~ObjectType|Realm.Object)} arg - the object type or the object to compute priviliges from
* @returns {Object} as the computed priviliges as properties
* @since 2.3.0
* @see {Realm.Permissions} for details of priviliges and roles.
*/
privileges(arg) {}
/** /**
* Create a new Realm object of the given type and with the specified properties. * Create a new Realm object of the given type and with the specified properties.
* @param {Realm~ObjectType} type - The type of Realm object to create. * @param {Realm~ObjectType} type - The type of Realm object to create.

View File

@ -167,6 +167,68 @@ module.exports = function(realmConstructor) {
Complete: 1, // The subscription has been processed by the sync server and data is being synced to the device. Complete: 1, // The subscription has been processed by the sync server and data is being synced to the device.
Invalidated: 3, // The subscription has been removed. Invalidated: 3, // The subscription has been removed.
}; };
// Define the permission schemas as constructors so that they can be
// passed into directly to functions which want object type names
const permissionsSchema = Object.freeze({
Class: function() {},
Permission: function() {},
Realm: function() {},
Role: function() {},
User: function() {},
});
permissionsSchema.Permission.schema = Object.freeze({
name: '__Permission',
properties: {
role: '__Role',
canRead: {type: 'bool', default: false},
canUpdate: {type: 'bool', default: false},
canDelete: {type: 'bool', default: false},
canSetPermissions: {type: 'bool', default: false},
canQuery: {type: 'bool', default: false},
canCreate: {type: 'bool', default: false},
canModifySchema: {type: 'bool', default: false},
}
});
permissionsSchema.User.schema = Object.freeze({
name: '__User',
primaryKey: 'id',
properties: {
id: 'string'
}
});
permissionsSchema.Role.schema = Object.freeze({
name: '__Role',
primaryKey: 'name',
properties: {
name: 'string',
members: '__User[]'
}
});
permissionsSchema.Class.schema = Object.freeze({
name: '__Class',
primaryKey: 'class_name',
properties: {
class_name: 'string',
permissions: '__Permission[]'
}
});
permissionsSchema.Realm.schema = Object.freeze({
name: '__Realm',
primaryKey: 'id',
properties: {
id: 'int',
permissions: '__Permission[]'
}
});
Object.defineProperty(realmConstructor, 'Permissions', {
value: permissionsSchema,
configurable: false
});
} }
// TODO: Remove this now useless object. // TODO: Remove this now useless object.

54
lib/index.d.ts vendored
View File

@ -81,7 +81,7 @@ declare namespace Realm {
path?: string; path?: string;
readOnly?: boolean; readOnly?: boolean;
inMemory?: boolean; inMemory?: boolean;
schema?: ObjectClass[] | ObjectSchema[]; schema?: (ObjectClass | ObjectSchema)[];
schemaVersion?: number; schemaVersion?: number;
sync?: Realm.Sync.SyncConfiguration; sync?: Realm.Sync.SyncConfiguration;
deleteRealmIfMigrationNeeded?: boolean; deleteRealmIfMigrationNeeded?: boolean;
@ -406,7 +406,7 @@ declare namespace Realm.Sync {
* @see { @link https://realm.io/docs/javascript/latest/api/Realm.Sync.Subscription.html } * @see { @link https://realm.io/docs/javascript/latest/api/Realm.Sync.Subscription.html }
*/ */
class Subscription { class Subscription {
readonly state: number; readonly state: SubscriptionState;
readonly error: string; readonly error: string;
unsubscribe(): void; unsubscribe(): void;
@ -414,6 +414,14 @@ declare namespace Realm.Sync {
removeListener(subscruptionCallback: SubscriptionNotificationCallback): void; removeListener(subscruptionCallback: SubscriptionNotificationCallback): void;
} }
enum SubscriptionState {
Error,
Creating,
Pending,
Complete,
Invalidated,
}
/** /**
* AuthError * AuthError
* @see { @link https://realm.io/docs/javascript/latest/api/Realm.Sync.AuthError.html } * @see { @link https://realm.io/docs/javascript/latest/api/Realm.Sync.AuthError.html }
@ -475,6 +483,42 @@ declare namespace Realm.Sync {
} }
} }
declare namespace Realm.Permissions {
class Permission {
static schema: ObjectSchema;
identity: string;
canRead: boolean;
canUpdate: boolean;
canDelete: boolean;
canSetPermissions: boolean;
canQuery: boolean;
canCreate: boolean;
canModifySchema: boolean;
}
class User {
static schema: ObjectSchema;
identity: string;
}
class Role {
static schema: ObjectSchema;
name: string;
members: User[];
}
class Class {
static schema: ObjectSchema;
class_name: string;
permissions: Permission[];
}
class Realm {
static schema: ObjectSchema;
permissions: Permission[];
}
}
interface ProgressPromise extends Promise<Realm> { interface ProgressPromise extends Promise<Realm> {
progress(callback: Realm.Sync.ProgressNotificationCallback): Promise<Realm> progress(callback: Realm.Sync.ProgressNotificationCallback): Promise<Realm>
@ -544,7 +588,7 @@ declare class Realm {
* @param {boolean} update? * @param {boolean} update?
* @returns T * @returns T
*/ */
create<T>(type: string | Realm.ObjectClass | Function, properties: T & Realm.ObjectPropsType, update?: boolean): T; create<T>(type: string | Realm.ObjectClass | Function, properties: T | Realm.ObjectPropsType, update?: boolean): T;
/** /**
* @param {Realm.Object|Realm.Object[]|Realm.List<any>|Realm.Results<any>|any} object * @param {Realm.Object|Realm.Object[]|Realm.List<any>|Realm.Results<any>|any} object
@ -620,6 +664,10 @@ declare class Realm {
* @returns boolean * @returns boolean
*/ */
compact(): boolean; compact(): boolean;
privileges() : Realm.Permissions.Realm;
privileges(objectType: string | Realm.ObjectSchema | Function) : Realm.Permissions.Class;
privileges(obj: Realm.Object) : Realm.Permissions.Class;
} }
declare module 'realm' { declare module 'realm' {

View File

@ -35,7 +35,6 @@
#include "js_sync.hpp" #include "js_sync.hpp"
#include "sync/sync_config.hpp" #include "sync/sync_config.hpp"
#include "sync/sync_manager.hpp" #include "sync/sync_manager.hpp"
#include "sync/partial_sync.hpp"
#endif #endif
#include "shared_realm.hpp" #include "shared_realm.hpp"
@ -186,6 +185,7 @@ public:
static void compact(ContextType, ObjectType, Arguments, ReturnValue &); static void compact(ContextType, ObjectType, Arguments, ReturnValue &);
static void delete_model(ContextType, ObjectType, Arguments, ReturnValue &); static void delete_model(ContextType, ObjectType, Arguments, ReturnValue &);
static void object_for_object_id(ContextType, ObjectType, Arguments, ReturnValue&); static void object_for_object_id(ContextType, ObjectType, Arguments, ReturnValue&);
static void privileges(ContextType, ObjectType, Arguments, ReturnValue&);
// properties // properties
static void get_empty(ContextType, ObjectType, ReturnValue &); static void get_empty(ContextType, ObjectType, ReturnValue &);
@ -242,6 +242,7 @@ public:
{"close", wrap<close>}, {"close", wrap<close>},
{"compact", wrap<compact>}, {"compact", wrap<compact>},
{"deleteModel", wrap<delete_model>}, {"deleteModel", wrap<delete_model>},
{"privileges", wrap<privileges>},
{"_objectForObjectId", wrap<object_for_object_id>}, {"_objectForObjectId", wrap<object_for_object_id>},
#if REALM_ENABLE_SYNC #if REALM_ENABLE_SYNC
{"_waitForDownload", wrap<wait_for_download_completion>}, {"_waitForDownload", wrap<wait_for_download_completion>},
@ -291,7 +292,8 @@ public:
return name; return name;
} }
static const ObjectSchema& validated_object_schema_for_value(ContextType ctx, const SharedRealm &realm, const ValueType &value, std::string& object_type) { static const ObjectSchema& validated_object_schema_for_value(ContextType ctx, const SharedRealm &realm, const ValueType &value) {
std::string object_type;
if (Value::is_constructor(ctx, value)) { if (Value::is_constructor(ctx, value)) {
FunctionType constructor = Value::to_constructor(ctx, value); FunctionType constructor = Value::to_constructor(ctx, value);
@ -541,7 +543,7 @@ void RealmClass<T>::constructor(ContextType ctx, ObjectType this_object, size_t
template<typename T> template<typename T>
SharedRealm RealmClass<T>::create_shared_realm(ContextType ctx, realm::Realm::Config config, bool schema_updated, SharedRealm RealmClass<T>::create_shared_realm(ContextType ctx, realm::Realm::Config config, bool schema_updated,
ObjectDefaultsMap && defaults, ConstructorMap && constructors) { ObjectDefaultsMap&& defaults, ConstructorMap&& constructors) {
config.execution_context = Context<T>::get_execution_context_id(ctx); config.execution_context = Context<T>::get_execution_context_id(ctx);
SharedRealm realm; SharedRealm realm;
@ -551,9 +553,6 @@ SharedRealm RealmClass<T>::create_shared_realm(ContextType ctx, realm::Realm::Co
catch (const RealmFileException& ex) { catch (const RealmFileException& ex) {
handleRealmFileException(ctx, config, ex); handleRealmFileException(ctx, config, ex);
} }
catch (...) {
throw;
}
GlobalContextType global_context = Context<T>::get_global_context(ctx); GlobalContextType global_context = Context<T>::get_global_context(ctx);
if (!realm->m_binding_context) { if (!realm->m_binding_context) {
@ -790,10 +789,8 @@ void RealmClass<T>::objects(ContextType ctx, ObjectType this_object, Arguments a
args.validate_maximum(1); args.validate_maximum(1);
SharedRealm realm = *get_internal<T, RealmClass<T>>(this_object); SharedRealm realm = *get_internal<T, RealmClass<T>>(this_object);
std::string object_type; auto& object_schema = validated_object_schema_for_value(ctx, realm, args[0]);
validated_object_schema_for_value(ctx, realm, args[0], object_type); return_value.set(ResultsClass<T>::create_instance(ctx, realm, object_schema.name));
return_value.set(ResultsClass<T>::create_instance(ctx, realm, object_type));
} }
template<typename T> template<typename T>
@ -802,7 +799,7 @@ void RealmClass<T>::object_for_primary_key(ContextType ctx, ObjectType this_obje
SharedRealm realm = *get_internal<T, RealmClass<T>>(this_object); SharedRealm realm = *get_internal<T, RealmClass<T>>(this_object);
std::string object_type; std::string object_type;
auto &object_schema = validated_object_schema_for_value(ctx, realm, args[0], object_type); auto &object_schema = validated_object_schema_for_value(ctx, realm, args[0]);
NativeAccessor accessor(ctx, realm, object_schema); NativeAccessor accessor(ctx, realm, object_schema);
auto realm_object = realm::Object::get_for_primary_key(accessor, realm, object_schema, args[1]); auto realm_object = realm::Object::get_for_primary_key(accessor, realm, object_schema, args[1]);
@ -820,8 +817,7 @@ void RealmClass<T>::create(ContextType ctx, ObjectType this_object, Arguments ar
SharedRealm realm = *get_internal<T, RealmClass<T>>(this_object); SharedRealm realm = *get_internal<T, RealmClass<T>>(this_object);
realm->verify_open(); realm->verify_open();
std::string object_type; auto &object_schema = validated_object_schema_for_value(ctx, realm, args[0]);
auto &object_schema = validated_object_schema_for_value(ctx, realm, args[0], object_type);
ObjectType object = Value::validated_to_object(ctx, args[1], "properties"); ObjectType object = Value::validated_to_object(ctx, args[1], "properties");
if (Value::is_array(ctx, args[1])) { if (Value::is_array(ctx, args[1])) {
@ -1008,36 +1004,6 @@ void RealmClass<T>::compact(ContextType ctx, ObjectType this_object, Arguments a
return_value.set(realm->compact()); return_value.set(realm->compact());
} }
#if REALM_ENABLE_SYNC
namespace {
// FIXME: Sync should provide this: https://github.com/realm/realm-sync/issues/1796
inline sync::ObjectID object_id_from_string(std::string const& string)
{
if (string.front() != '{' || string.back() != '}')
throw std::invalid_argument("Invalid object ID.");
size_t dash_index = string.find('-');
if (dash_index == std::string::npos)
throw std::invalid_argument("Invalid object ID.");
std::string high_string = string.substr(1, dash_index - 1);
std::string low_string = string.substr(dash_index + 1, string.size() - dash_index - 2);
if (high_string.size() == 0 || high_string.size() > 16 || low_string.size() == 0 || low_string.size() > 16)
throw std::invalid_argument("Invalid object ID.");
auto isxdigit = static_cast<int(*)(int)>(std::isxdigit);
if (!std::all_of(high_string.begin(), high_string.end(), isxdigit) ||
!std::all_of(low_string.begin(), low_string.end(), isxdigit)) {
throw std::invalid_argument("Invalid object ID.");
}
return sync::ObjectID(strtoull(high_string.c_str(), nullptr, 16), strtoull(low_string.c_str(), nullptr, 16));
}
} // unnamed namespace
#endif // REALM_ENABLE_SYNC
template<typename T> template<typename T>
void RealmClass<T>::object_for_object_id(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue& return_value) { void RealmClass<T>::object_for_object_id(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue& return_value) {
args.validate_count(2); args.validate_count(2);
@ -1047,21 +1013,67 @@ void RealmClass<T>::object_for_object_id(ContextType ctx, ObjectType this_object
if (!sync::has_object_ids(realm->read_group())) if (!sync::has_object_ids(realm->read_group()))
throw std::logic_error("Realm._objectForObjectId() can only be used with synced Realms."); throw std::logic_error("Realm._objectForObjectId() can only be used with synced Realms.");
std::string object_type = Value::validated_to_string(ctx, args[0]); auto& object_schema = validated_object_schema_for_value(ctx, realm, args[0]);
validated_object_schema_for_value(ctx, realm, args[0], object_type);
std::string object_id_string = Value::validated_to_string(ctx, args[1]); std::string object_id_string = Value::validated_to_string(ctx, args[1]);
auto object_id = object_id_from_string(object_id_string); auto object_id = sync::ObjectID::from_string(object_id_string);
const Group& group = realm->read_group(); const Group& group = realm->read_group();
size_t ndx = sync::row_for_object_id(group, *ObjectStore::table_for_object_type(group, object_type), object_id); size_t ndx = sync::row_for_object_id(group, *ObjectStore::table_for_object_type(group, object_schema.name), object_id);
if (ndx != realm::npos) { if (ndx != realm::npos) {
return_value.set(RealmObjectClass<T>::create_instance(ctx, realm::Object(realm, object_type, ndx))); return_value.set(RealmObjectClass<T>::create_instance(ctx, realm::Object(realm, object_schema.name, ndx)));
} }
#else #else
throw std::logic_error("Realm._objectForObjectId() can only be used with synced Realms."); throw std::logic_error("Realm._objectForObjectId() can only be used with synced Realms.");
#endif // REALM_ENABLE_SYNC #endif // REALM_ENABLE_SYNC
} }
template<typename T>
void RealmClass<T>::privileges(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) {
args.validate_maximum(1);
using Privilege = realm::ComputedPrivileges;
auto has_privilege = [](Privilege actual, Privilege expected) {
return (static_cast<int>(actual) & static_cast<int>(expected)) == static_cast<int>(expected);
};
SharedRealm realm = *get_internal<T, RealmClass<T>>(this_object);
if (args.count == 0) {
auto p = realm->get_privileges();
ObjectType object = Object::create_empty(ctx);
Object::set_property(ctx, object, "read", Value::from_boolean(ctx, has_privilege(p, Privilege::Read)));
Object::set_property(ctx, object, "update", Value::from_boolean(ctx,has_privilege(p, Privilege::Update)));
Object::set_property(ctx, object, "modifySchema", Value::from_boolean(ctx, has_privilege(p, Privilege::ModifySchema)));
Object::set_property(ctx, object, "setPermissions", Value::from_boolean(ctx, has_privilege(p, Privilege::SetPermissions)));
return_value.set(object);
return;
}
if (Value::is_object(ctx, args[0])) {
auto arg = Value::to_object(ctx, args[0]);
if (Object::template is_instance<RealmObjectClass<T>>(ctx, arg)) {
auto obj = get_internal<T, RealmObjectClass<T>>(arg);
auto p = realm->get_privileges(obj->row());
ObjectType object = Object::create_empty(ctx);
Object::set_property(ctx, object, "read", Value::from_boolean(ctx, has_privilege(p, Privilege::Read)));
Object::set_property(ctx, object, "update", Value::from_boolean(ctx,has_privilege(p, Privilege::Update)));
Object::set_property(ctx, object, "delete", Value::from_boolean(ctx,has_privilege(p, Privilege::Delete)));
Object::set_property(ctx, object, "setPermissions", Value::from_boolean(ctx, has_privilege(p, Privilege::SetPermissions)));
return_value.set(object);
return;
}
}
auto& object_schema = validated_object_schema_for_value(ctx, realm, args[0]);
auto p = realm->get_privileges(object_schema.name);
ObjectType object = Object::create_empty(ctx);
Object::set_property(ctx, object, "read", Value::from_boolean(ctx, has_privilege(p, Privilege::Read)));
Object::set_property(ctx, object, "update", Value::from_boolean(ctx,has_privilege(p, Privilege::Update)));
Object::set_property(ctx, object, "create", Value::from_boolean(ctx, has_privilege(p, Privilege::Create)));
Object::set_property(ctx, object, "subscribe", Value::from_boolean(ctx, has_privilege(p, Privilege::Query)));
Object::set_property(ctx, object, "setPermissions", Value::from_boolean(ctx, has_privilege(p, Privilege::SetPermissions)));
return_value.set(object);
}
} // js } // js
} // realm } // realm

View File

@ -37,7 +37,8 @@ module.exports = {
} }
else if (type === 'object') { else if (type === 'object') {
for (const key of Object.keys(val1)) { for (const key of Object.keys(val1)) {
this.assertEqual(val1[key], val2[key], errorMessage, depth + 1); const message = errorMessage ? `${errorMessage}: ${key}` : key;
this.assertEqual(val1[key], val2[key], message, depth + 1);
} }
} }
else if (type === 'list') { else if (type === 'list') {

View File

@ -9,6 +9,8 @@ const realmName = process.argv[4];
const realmModule = process.argv[5]; const realmModule = process.argv[5];
const Realm = require(realmModule); const Realm = require(realmModule);
// Ensure that schemas.js gets the correct module with `require('realm')`
require.cache[require.resolve('realm')] = require.cache[require.resolve(realmModule)];
let schemas = require(process.argv[2]); let schemas = require(process.argv[2]);
function createObjects(user) { function createObjects(user) {

View File

@ -16,33 +16,33 @@
// //
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
'use strict'; 'use strict';
var Realm = require('realm'); var Realm = require('realm');
var TestCase = require('./asserts'); var TestCase = require('./asserts');
function uuid() { function uuid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16); return v.toString(16);
}); });
} }
function createUsersWithTestRealms(count) { function createUsersWithTestRealms(count) {
const createUserWithTestRealm = () => { const createUserWithTestRealm = () => {
return Realm.Sync.User.register('http://localhost:9080', uuid(), 'password') return Realm.Sync.User
.then(user => { .register('http://localhost:9080', uuid(), 'password')
new Realm({sync: {user, url: 'realm://localhost:9080/~/test'}}).close(); .then(user => {
return user; new Realm({sync: {user, url: 'realm://localhost:9080/~/test'}}).close();
}); return user;
}; });
};
return Promise.all(Array.from({length: count}, createUserWithTestRealm)); return Promise.all(Array.from({length: count}, createUserWithTestRealm));
} }
function wait(t) { function wait(t) {
return new Promise(resolve => setTimeout(resolve, t)); return new Promise(resolve => setTimeout(resolve, t));
} }
function repeatUntil(fn, predicate) { function repeatUntil(fn, predicate) {
@ -57,66 +57,183 @@ function repeatUntil(fn, predicate) {
return check; return check;
} }
function subscribe(results) {
const subscription = results.subscribe();
return new Promise((resolve, reject) => {
subscription.addListener((subscription, state) => {
if (state == Realm.Sync.SubscriptionState.Complete) {
resolve();
}
else if (state == Realm.Sync.SubscriptionState.Error) {
reject();
}
});
setTimeout(() => reject("listener never called"), 5000);
});
}
function waitForUpload(realm) {
let session = realm.syncSession;
return new Promise(resolve => {
let callback = (transferred, total) => {
if (transferred === total) {
session.removeProgressNotification(callback);
resolve(realm);
}
};
session.addProgressNotification('upload', 'forCurrentlyOutstandingWork', callback);
});
}
function permissionForPath(permissions, path) {
for (const permission of permissions) {
if (permission.path == path) {
return permission;
}
}
}
module.exports = { module.exports = {
testApplyAndGetGrantedPermissions() { testApplyAndGetGrantedPermissions() {
return createUsersWithTestRealms(1) return createUsersWithTestRealms(1).then(([user]) => {
.then(([user]) => { const path = `/${user.identity}/test`;
return user.applyPermissions({ userId: `${user.identity}` }, `/${user.identity}/test`, 'read') return user
.then(repeatUntil(() => user.getGrantedPermissions('any'), .applyPermissions({userId: `${user.identity}`},
permissions => permissions.length > 1)) `/${user.identity}/test`, 'read')
.then(permissions => { .then(repeatUntil(() => user.getGrantedPermissions('any'),
TestCase.assertEqual(permissions[0].path, `/${user.identity}/test`); permissions => {
TestCase.assertEqual(permissions[0].mayRead, true); let permission = permissionForPath(permissions, path);
TestCase.assertEqual(permissions[0].mayWrite, false); return permission && !permission.mayWrite;
TestCase.assertEqual(permissions[0].mayManage, false); }))
}); .then(permissions => {
let permission = permissionForPath(permissions, path);
TestCase.assertDefined(permission);
TestCase.assertEqual(permission.mayRead, true);
TestCase.assertEqual(permission.mayWrite, false);
TestCase.assertEqual(permission.mayManage, false);
});
}); });
}, },
testOfferPermissions() { testOfferPermissions() {
return createUsersWithTestRealms(2) return createUsersWithTestRealms(2).then(([user1, user2]) => {
.then(([user1, user2]) => { const path = `/${user1.identity}/test`;
return user1.offerPermissions(`/${user1.identity}/test`, 'read') return user1.offerPermissions(`/${user1.identity}/test`, 'read')
.then(token => user2.acceptPermissionOffer(token)) .then(token => user2.acceptPermissionOffer(token))
.then(realmUrl => { .then(realmUrl => {
TestCase.assertEqual(realmUrl, `/${user1.identity}/test`); TestCase.assertEqual(realmUrl, path);
return realmUrl; return realmUrl;
}) })
.then(repeatUntil(() => user2.getGrantedPermissions('any'), .then(repeatUntil(() => user2.getGrantedPermissions('any'),
permissions => permissions.length > 1)) permissions => permissions.length > 2
.then(permissions => { && permissionForPath(permissions, path)))
TestCase.assertEqual(permissions[2].path, `/${user1.identity}/test`); .then(permissions => {
TestCase.assertEqual(permissions[2].mayRead, true); let permission = permissionForPath(permissions, path)
TestCase.assertEqual(permissions[2].mayWrite, false); TestCase.assertDefined(permission);
TestCase.assertEqual(permissions[2].mayManage, false); TestCase.assertEqual(permission.mayRead, true);
}); TestCase.assertEqual(permission.mayWrite, false);
TestCase.assertEqual(permission.mayManage, false);
});
}); });
}, },
testInvalidatePermissionOffer() { testInvalidatePermissionOffer() {
let user1, user2, token; let user1, user2, token;
return createUsersWithTestRealms(2) return createUsersWithTestRealms(2)
.then(users => { .then(users => {
user1 = users[0]; user1 = users[0];
user2 = users[1]; user2 = users[1];
return user1.offerPermissions(`/${user1.identity}/test`, 'read'); return user1.offerPermissions(`/${user1.identity}/test`, 'read');
}) })
.then(t => { token = t; return user1.invalidatePermissionOffer(token); }) .then(t => {
// Since we don't yet support notification when the invalidation has gone through, token = t;
// wait for a bit and hope the server is done processing. return user1.invalidatePermissionOffer(token);
.then(wait(100)) })
.then(() => user2.acceptPermissionOffer(token)) // Since we don't yet support notification when the invalidation has
// We want the call to fail, i.e. the catch() below should be called. // gone through, wait for a bit and hope the server is done
.then(() => { throw new Error("User was able to accept an invalid permission offer token"); }) // processing.
.catch(error => { .then(() => wait(100))
try { .then(() => user2.acceptPermissionOffer(token))
TestCase.assertEqual(error.message, 'The permission offer is expired.'); // We want the call to fail, i.e. the catch() below should be
TestCase.assertEqual(error.statusCode, 701); // called.
} .then(() => {
catch (e) { throw new Error("User was able to accept an invalid permission offer token");
throw new Error(e); })
} .catch(error => {
}); try {
TestCase.assertEqual(error.message, 'The permission offer is expired.');
TestCase.assertEqual(error.statusCode, 701);
}
catch (e) {
throw new Error(e);
}
});
}, },
}
testObjectPermissions() {
let config = (user, url) => {
return {
schema: [
Realm.Permissions.Permission,
Realm.Permissions.User,
Realm.Permissions.Role,
{
name: 'Object',
properties: {
value: 'int',
permissions: '__Permission[]'
}
}
],
sync: {user, url, partial: true}
};
};
let owner, otherUser
return Realm.Sync.User
.register('http://localhost:9080', uuid(), 'password')
.then(user => {
owner = user;
new Realm({sync: {user, url: 'realm://localhost:9080/~/test'}}).close();
return Realm.Sync.User.register('http://localhost:9080', uuid(), 'password')
})
.then(user => {
otherUser = user;
return owner.applyPermissions({userId: otherUser.identity},
`/${owner.identity}/test`, 'read')
})
.then(() => {
let realm = new Realm(config(owner, 'realm://localhost:9080/~/test'));
realm.write(() => {
let user = realm.create(Realm.Permissions.User, {id: otherUser.identity})
let role = realm.create(Realm.Permissions.Role, {name: 'reader'})
role.members.push(user)
let obj1 = realm.create('Object', {value: 1});
let obj2 = realm.create('Object', {value: 2});
obj2.permissions.push(realm.create(Realm.Permissions.Permission,
{role: role, canRead: true, canUpdate: false}))
});
return waitForUpload(realm).then(() => realm.close());
})
.then(() => Realm.open(config(otherUser, `realm://localhost:9080/${owner.identity}/test`)))
.then((realm) => subscribe(realm.objects('Object')).then(() => realm))
.then((realm) => {
// Should have full access to the Realm as a whole
TestCase.assertSimilar('object', realm.privileges(),
{read: true, update: true, modifySchema: true, setPermissions: true});
TestCase.assertSimilar('object', realm.privileges('Object'),
{read: true, update: true, create: true, subscribe: true, setPermissions: true});
// Verify that checking via constructor works too
TestCase.assertSimilar('object', realm.privileges(Realm.Permissions.User),
{read: true, update: true, create: true, subscribe: true, setPermissions: true});
// Should only be able to see the second object
let results = realm.objects('Object')
TestCase.assertEqual(results.length, 1);
TestCase.assertEqual(results[0].value, 2);
TestCase.assertSimilar('object', realm.privileges(results[0]),
{read: true, update: false, delete: false, setPermissions: false});
realm.close();
});
}
}

View File

@ -61,27 +61,23 @@ function copyFileToTempDir(filename) {
return tmpFile.name; return tmpFile.name;
} }
function runOutOfProcess(nodeJsFilePath) { function runOutOfProcess() {
var nodeArgs = Array.prototype.slice.call(arguments); const args = Array.prototype.slice.call(arguments);
let tmpDir = tmp.dirSync(); let tmpDir = tmp.dirSync();
let content = fs.readFileSync(nodeJsFilePath, 'utf8'); console.log(`runOutOfProcess : ${args.join(' ')}`);
let tmpFile = tmp.fileSync({ dir: tmpDir.name });
fs.appendFileSync(tmpFile.fd, content, { encoding: 'utf8' });
nodeArgs[0] = tmpFile.name;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
console.log('runOutOfProcess command\n node ' + nodeArgs.join(" ")); execFile(process.execPath, args, {cwd: tmpDir.name}, (error, stdout, stderr) => {
const child = execFile('node', nodeArgs, { cwd: tmpDir.name }, (error, stdout, stderr) => {
if (error) { if (error) {
console.error("runOutOfProcess failed\n" + error); console.error("runOutOfProcess failed\n", error, stdout, stderr);
reject(new Error(`Running ${nodeJsFilePath} failed. error: ${error}`)); reject(new Error(`Running ${args[0]} failed. error: ${error}`));
return; return;
} }
console.log('runOutOfProcess success\n' + stdout); console.log('runOutOfProcess success\n' + stdout);
resolve(); resolve();
}); });
} }
catch (e) { catch (e) {
reject(e); reject(e);
} }
@ -275,43 +271,40 @@ module.exports = {
const expectedObjectsCount = 3; const expectedObjectsCount = 3;
return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH)
.then(() => { .then(() => Realm.Sync.User.login('http://localhost:9080', username, 'password'))
return Realm.Sync.User.login('http://localhost:9080', username, 'password').then(user => { .then(user => {
return new Promise((resolve, reject) => { const accessTokenRefreshed = this;
const accessTokenRefreshed = this; let successCounter = 0;
let successCounter = 0;
let config = { let config = {
sync: { user, url: `realm://localhost:9080/~/${realmName}` } sync: { user, url: `realm://localhost:9080/~/${realmName}` }
}; };
return new Promise((resolve, reject) => {
Realm.openAsync(config, (error, realm) => {
if (error) {
reject(error);
return;
}
try {
let actualObjectsCount = realm.objects('Dog').length;
TestCase.assertEqual(actualObjectsCount, expectedObjectsCount, "Synced realm does not contain the expected objects count");
Realm.openAsync(config, (error, realm) => { let firstDog = realm.objects('Dog')[0];
try { TestCase.assertTrue(({}).hasOwnProperty.call(firstDog, 'name'), "Synced realm does not have an inffered schema");
if (error) { TestCase.assertTrue(firstDog.name, "Synced realm object's property should have a value");
reject(error); TestCase.assertTrue(firstDog.name.indexOf('Lassy') !== -1, "Synced realm object's property should contain the actual written value");
}
let actualObjectsCount = realm.objects('Dog').length; const session = realm.syncSession;
TestCase.assertEqual(actualObjectsCount, expectedObjectsCount, "Synced realm does not contain the expected objects count"); TestCase.assertInstanceOf(session, Realm.Sync.Session);
TestCase.assertEqual(session.user.identity, user.identity);
let firstDog = realm.objects('Dog')[0]; TestCase.assertEqual(session.config.url, config.sync.url);
TestCase.assertTrue(({}).hasOwnProperty.call(firstDog, 'name'), "Synced realm does not have an inffered schema"); TestCase.assertEqual(session.config.user.identity, config.sync.user.identity);
TestCase.assertTrue(firstDog.name, "Synced realm object's property should have a value"); TestCase.assertEqual(session.state, 'active');
TestCase.assertTrue(firstDog.name.indexOf('Lassy') !== -1, "Synced realm object's property should contain the actual written value"); resolve();
}
catch (e) {
const session = realm.syncSession; reject(e);
TestCase.assertInstanceOf(session, Realm.Sync.Session); }
TestCase.assertEqual(session.user.identity, user.identity);
TestCase.assertEqual(session.config.url, config.sync.url);
TestCase.assertEqual(session.config.user.identity, config.sync.user.identity);
TestCase.assertEqual(session.state, 'active');
resolve();
}
catch (e) {
reject(e);
}
});
}); });
}); });
}); });
@ -321,26 +314,22 @@ module.exports = {
const username = uuid(); const username = uuid();
const expectedObjectsCount = 3; const expectedObjectsCount = 3;
const accessTokenRefreshed = this;
let successCounter = 0;
return new Promise((resolve, reject) => { let config = {
const accessTokenRefreshed = this; schema: [{ name: 'Dog', properties: { name: 'string' } }],
let successCounter = 0; };
let config = { return Realm.open(config).then(realm => {
schema: [{ name: 'Dog', properties: { name: 'string' } }], realm.write(() => {
}; for (let i = 1; i <= 3; i++) {
realm.create('Dog', { name: `Lassy ${i}` });
}
});
Realm.open(config).then(realm => { let actualObjectsCount = realm.objects('Dog').length;
realm.write(() => { TestCase.assertEqual(actualObjectsCount, expectedObjectsCount, "Local realm does not contain the expected objects count");
for (let i = 1; i <= 3; i++) {
realm.create('Dog', { name: `Lassy ${i}` });
}
});
let actualObjectsCount = realm.objects('Dog').length;
TestCase.assertEqual(actualObjectsCount, expectedObjectsCount, "Local realm does not contain the expected objects count");
resolve();
}).catch(error => reject(error));
}); });
}, },
@ -412,37 +401,33 @@ module.exports = {
const realmName = uuid(); const realmName = uuid();
return runOutOfProcess(__dirname + '/nested-list-helper.js', __dirname + '/schemas.js', username, realmName, REALM_MODULE_PATH) return runOutOfProcess(__dirname + '/nested-list-helper.js', __dirname + '/schemas.js', username, realmName, REALM_MODULE_PATH)
.then(() => { .then(() => Realm.Sync.User.login('http://localhost:9080', username, 'password'))
return Realm.Sync.User.login('http://localhost:9080', username, 'password').then(user => { .then(user => {
return new Promise((resolve, reject) => { let config = {
let config = { schema: [schemas.ParentObject, schemas.NameObject],
schema: [schemas.ParentObject, schemas.NameObject], sync: { user, url: `realm://localhost:9080/~/${realmName}` }
sync: { user, url: `realm://localhost:9080/~/${realmName}` } };
}; return Realm.open(config)
Realm.open(config).then(realm => { }).then(realm => {
let objects = realm.objects('ParentObject'); let objects = realm.objects('ParentObject');
let json = JSON.stringify(objects); let json = JSON.stringify(objects);
TestCase.assertEqual(json, '{"0":{"id":1,"name":{"0":{"family":"Larsen","given":{"0":"Hans","1":"Jørgen"},"prefix":{}},"1":{"family":"Hansen","given":{"0":"Ib"},"prefix":{}}}},"1":{"id":2,"name":{"0":{"family":"Petersen","given":{"0":"Gurli","1":"Margrete"},"prefix":{}}}}}'); TestCase.assertEqual(json, '{"0":{"id":1,"name":{"0":{"family":"Larsen","given":{"0":"Hans","1":"Jørgen"},"prefix":{}},"1":{"family":"Hansen","given":{"0":"Ib"},"prefix":{}}}},"1":{"id":2,"name":{"0":{"family":"Petersen","given":{"0":"Gurli","1":"Margrete"},"prefix":{}}}}}');
TestCase.assertEqual(objects.length, 2); TestCase.assertEqual(objects.length, 2);
TestCase.assertEqual(objects[0].name.length, 2); TestCase.assertEqual(objects[0].name.length, 2);
TestCase.assertEqual(objects[0].name[0].given.length, 2); TestCase.assertEqual(objects[0].name[0].given.length, 2);
TestCase.assertEqual(objects[0].name[0].prefix.length, 0); TestCase.assertEqual(objects[0].name[0].prefix.length, 0);
TestCase.assertEqual(objects[0].name[0].given[0], 'Hans'); TestCase.assertEqual(objects[0].name[0].given[0], 'Hans');
TestCase.assertEqual(objects[0].name[0].given[1], 'Jørgen') TestCase.assertEqual(objects[0].name[0].given[1], 'Jørgen')
TestCase.assertEqual(objects[0].name[1].given.length, 1); TestCase.assertEqual(objects[0].name[1].given.length, 1);
TestCase.assertEqual(objects[0].name[1].given[0], 'Ib'); TestCase.assertEqual(objects[0].name[1].given[0], 'Ib');
TestCase.assertEqual(objects[0].name[1].prefix.length, 0); TestCase.assertEqual(objects[0].name[1].prefix.length, 0);
TestCase.assertEqual(objects[1].name.length, 1); TestCase.assertEqual(objects[1].name.length, 1);
TestCase.assertEqual(objects[1].name[0].given.length, 2); TestCase.assertEqual(objects[1].name[0].given.length, 2);
TestCase.assertEqual(objects[1].name[0].prefix.length, 0); TestCase.assertEqual(objects[1].name[0].prefix.length, 0);
TestCase.assertEqual(objects[1].name[0].given[0], 'Gurli'); TestCase.assertEqual(objects[1].name[0].given[0], 'Gurli');
TestCase.assertEqual(objects[1].name[0].given[1], 'Margrete'); TestCase.assertEqual(objects[1].name[0].given[1], 'Margrete');
resolve();
}).catch(() => reject());
});
});
}); });
}, },
@ -610,60 +595,59 @@ module.exports = {
const realmName = uuid(); const realmName = uuid();
return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH)
.then(() => { .then(() => Realm.Sync.User.login('http://localhost:9080', username, 'password'))
return Realm.Sync.User.login('http://localhost:9080', username, 'password').then(user => { .then(user => {
return new Promise((resolve, reject) => { let config = {
let config = { sync: {
sync: { user,
user, url: `realm://localhost:9080/~/${realmName}`
url: `realm://localhost:9080/~/${realmName}` },
}, schema: [{ name: 'Dog', properties: { name: 'string' } }],
schema: [{ name: 'Dog', properties: { name: 'string' } }], };
};
let realm = new Realm(config); let realm = new Realm(config);
let unregisterFunc; let unregisterFunc;
let writeDataFunc = () => { let writeDataFunc = () => {
realm.write(() => { realm.write(() => {
for (let i = 1; i <= 3; i++) { for (let i = 1; i <= 3; i++) {
realm.create('Dog', { name: `Lassy ${i}` }); realm.create('Dog', { name: `Lassy ${i}` });
} }
}); });
}
return new Promise((resolve, reject) => {
let syncFinished = false;
let failOnCall = false;
const progressCallback = (transferred, total) => {
if (failOnCall) {
reject(new Error("Progress callback should not be called after removeProgressNotification"));
} }
let syncFinished = false; syncFinished = transferred === total;
let failOnCall = false;
const progressCallback = (transferred, total) => {
if (failOnCall) {
reject(new Error("Progress callback should not be called after removeProgressNotification"));
}
syncFinished = transferred === total; //unregister and write some new data.
if (syncFinished) {
failOnCall = true;
unregisterFunc();
//unregister and write some new data. //use second callback to wait for sync finished
if (syncFinished) { realm.syncSession.addProgressNotification('upload', 'reportIndefinitely', (transferred, transferable) => {
failOnCall = true; if (transferred === transferable) {
unregisterFunc(); resolve();
}
});
writeDataFunc();
}
};
//use second callback to wait for sync finished realm.syncSession.addProgressNotification('upload', 'reportIndefinitely', progressCallback);
realm.syncSession.addProgressNotification('upload', 'reportIndefinitely', (transferred, transferable) => {
if (transferred === transferable) {
resolve();
}
});
writeDataFunc();
}
};
realm.syncSession.addProgressNotification('upload', 'reportIndefinitely', progressCallback); unregisterFunc = () => {
realm.syncSession.removeProgressNotification(progressCallback);
};
unregisterFunc = () => { writeDataFunc();
realm.syncSession.removeProgressNotification(progressCallback);
};
writeDataFunc();
});
}); });
}); });
}, },
@ -675,36 +659,24 @@ module.exports = {
const username = uuid(); const username = uuid();
const realmName = uuid(); const realmName = uuid();
let progressCalled = false;
return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH)
.then(() => { .then(() => Realm.Sync.User.login('http://localhost:9080', username, 'password'))
return Realm.Sync.User.login('http://localhost:9080', username, 'password').then(user => { .then(user => {
return new Promise((resolve, reject) => { let config = {
let config = { sync: {
sync: { user,
user, url: `realm://localhost:9080/~/${realmName}`
url: `realm://localhost:9080/~/${realmName}` },
}, schema: [{ name: 'Dog', properties: { name: 'string' } }],
schema: [{ name: 'Dog', properties: { name: 'string' } }], };
};
let progressCalled = false; return Promise.race([
Realm.open(config) Realm.open(config).progress((transferred, total) => { progressCalled = true; }),
.progress((transferred, total) => { new Promise((_, reject) => setTimeout(() => reject("Progress Notifications API failed to call progress callback for Realm constructor"), 5000))
progressCalled = true; ]);
}) }).then(() => TestCase.assertTrue(progressCalled));
.then(() => {
TestCase.assertTrue(progressCalled);
resolve();
})
.catch((e) => reject(e));
setTimeout(function() {
reject("Progress Notifications API failed to call progress callback for Realm constructor");
}, 5000);
});
});
});
}, },
testProgressNotificationsForRealmOpenAsync() { testProgressNotificationsForRealmOpenAsync() {
@ -716,37 +688,36 @@ module.exports = {
const realmName = uuid(); const realmName = uuid();
return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH)
.then(() => { .then(() => Realm.Sync.User.login('http://localhost:9080', username, 'password'))
return Realm.Sync.User.login('http://localhost:9080', username, 'password').then(user => { .then(user => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let config = { let config = {
sync: { sync: {
user, user,
url: `realm://localhost:9080/~/${realmName}` url: `realm://localhost:9080/~/${realmName}`
}, },
schema: [{ name: 'Dog', properties: { name: 'string' } }], schema: [{ name: 'Dog', properties: { name: 'string' } }],
}; };
let progressCalled = false; let progressCalled = false;
Realm.openAsync(config, Realm.openAsync(config,
(error, realm) => { (error, realm) => {
if (error) { if (error) {
reject(error); reject(error);
return; return;
} }
TestCase.assertTrue(progressCalled); TestCase.assertTrue(progressCalled);
resolve(); resolve();
}, },
(transferred, total) => { (transferred, total) => {
progressCalled = true; progressCalled = true;
}); });
setTimeout(function() { setTimeout(function() {
reject("Progress Notifications API failed to call progress callback for Realm constructor"); reject("Progress Notifications API failed to call progress callback for Realm constructor");
}, 5000); }, 5000);
});
}); });
}); });
}, },
@ -761,38 +732,37 @@ module.exports = {
const realmName = uuid(); const realmName = uuid();
return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH)
.then(() => { .then(() => Realm.Sync.User.login('http://localhost:9080', username, 'password'))
return Realm.Sync.User.login('http://localhost:9080', username, 'password').then(user => { .then(user => {
let config = { let config = {
sync: { sync: {
user: user, user: user,
url: `realm://localhost:9080/~/${realmName}`, url: `realm://localhost:9080/~/${realmName}`,
partial: true, partial: true,
error: (session, error) => console.log(error) error: (session, error) => console.log(error)
}, },
schema: [{ name: 'Dog', properties: { name: 'string' } }] schema: [{ name: 'Dog', properties: { name: 'string' } }]
}; };
Realm.deleteFile(config); Realm.deleteFile(config);
const realm = new Realm(config); const realm = new Realm(config);
TestCase.assertEqual(realm.objects('Dog').length, 0); TestCase.assertEqual(realm.objects('Dog').length, 0);
var results = realm.objects('Dog').filtered("name == 'Lassy 1'"); var results = realm.objects('Dog').filtered("name == 'Lassy 1'");
var subscription = results.subscribe(); var subscription = results.subscribe();
TestCase.assertEqual(subscription.state, Realm.Sync.SubscriptionState.Creating); TestCase.assertEqual(subscription.state, Realm.Sync.SubscriptionState.Creating);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
subscription.addListener((subscription, state) => { subscription.addListener((subscription, state) => {
if (state == Realm.Sync.SubscriptionState.Complete) { if (state == Realm.Sync.SubscriptionState.Complete) {
TestCase.assertEqual(results.length, 1); TestCase.assertEqual(results.length, 1);
TestCase.assertTrue(results[0].name === 'Lassy 1', "The object is not synced correctly"); TestCase.assertTrue(results[0].name === 'Lassy 1', "The object is not synced correctly");
resolve(); resolve();
} }
});
setTimeout(function() {
reject("listener never called");
}, 5000);
}); });
}) setTimeout(function() {
}) reject("listener never called");
}, 5000);
});
});
}, },
testPartialSyncAnonymous_ResultsListener() { testPartialSyncAnonymous_ResultsListener() {
@ -805,38 +775,37 @@ module.exports = {
const realmName = uuid(); const realmName = uuid();
return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH)
.then(() => { .then(() => Realm.Sync.User.login('http://localhost:9080', username, 'password'))
return Realm.Sync.User.login('http://localhost:9080', username, 'password').then(user => { .then(user => {
let config = { let config = {
sync: { sync: {
user: user, user: user,
url: `realm://localhost:9080/~/${realmName}`, url: `realm://localhost:9080/~/${realmName}`,
partial: true, partial: true,
error: (session, error) => console.log(error) error: (session, error) => console.log(error)
}, },
schema: [{ name: 'Dog', properties: { name: 'string' } }] schema: [{ name: 'Dog', properties: { name: 'string' } }]
}; };
Realm.deleteFile(config); Realm.deleteFile(config);
const realm = new Realm(config); const realm = new Realm(config);
TestCase.assertEqual(realm.objects('Dog').length, 0); TestCase.assertEqual(realm.objects('Dog').length, 0);
var results = realm.objects('Dog').filtered("name == 'Lassy 1'"); var results = realm.objects('Dog').filtered("name == 'Lassy 1'");
var subscription = results.subscribe(); var subscription = results.subscribe();
TestCase.assertEqual(subscription.state, Realm.Sync.SubscriptionState.Creating); TestCase.assertEqual(subscription.state, Realm.Sync.SubscriptionState.Creating);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
results.addListener((collection, changes) => { results.addListener((collection, changes) => {
if (subscription.state === Realm.Sync.SubscriptionState.Complete) { if (subscription.state === Realm.Sync.SubscriptionState.Complete) {
TestCase.assertEqual(collection.length, 1); TestCase.assertEqual(collection.length, 1);
TestCase.assertTrue(collection[0].name === 'Lassy 1', "The object is not synced correctly"); TestCase.assertTrue(collection[0].name === 'Lassy 1', "The object is not synced correctly");
resolve(); resolve();
} }
});
setTimeout(function() {
reject("listener never called");
}, 5000);
}); });
}) setTimeout(function() {
}) reject("listener never called");
}, 5000);
});
});
}, },
testPartialSyncMultipleSubscriptions() { testPartialSyncMultipleSubscriptions() {
@ -849,56 +818,55 @@ module.exports = {
const realmName = uuid(); const realmName = uuid();
return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH)
.then(() => { .then(() => Realm.Sync.User.login('http://localhost:9080', username, 'password'))
return Realm.Sync.User.login('http://localhost:9080', username, 'password').then(user => { .then(user => {
let config = { let config = {
sync: { sync: {
user: user, user: user,
url: `realm://localhost:9080/~/${realmName}`, url: `realm://localhost:9080/~/${realmName}`,
partial: true, partial: true,
error: (session, error) => console.log(error) error: (session, error) => console.log(error)
}, },
schema: [{ name: 'Dog', properties: { name: 'string' } }] schema: [{ name: 'Dog', properties: { name: 'string' } }]
}; };
Realm.deleteFile(config); Realm.deleteFile(config);
const realm = new Realm(config); const realm = new Realm(config);
TestCase.assertEqual(realm.objects('Dog').length, 0); TestCase.assertEqual(realm.objects('Dog').length, 0);
var results1 = realm.objects('Dog').filtered("name == 'Lassy 1'"); var results1 = realm.objects('Dog').filtered("name == 'Lassy 1'");
var results2 = realm.objects('Dog').filtered("name == 'Lassy 2'"); var results2 = realm.objects('Dog').filtered("name == 'Lassy 2'");
var subscription1 = results1.subscribe(); var subscription1 = results1.subscribe();
var subscription2 = results2.subscribe(); var subscription2 = results2.subscribe();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let called1 = false; let called1 = false;
let called2 = false; let called2 = false;
results1.addListener((collection, changeset) => { results1.addListener((collection, changeset) => {
if (subscription1.state == Realm.Sync.SubscriptionState.Complete) { if (subscription1.state == Realm.Sync.SubscriptionState.Complete) {
TestCase.assertEqual(collection.length, 1); TestCase.assertEqual(collection.length, 1);
TestCase.assertTrue(collection[0].name === 'Lassy 1', "The object is not synced correctly"); TestCase.assertTrue(collection[0].name === 'Lassy 1', "The object is not synced correctly");
called1 = true; called1 = true;
if (called1 && called2) { if (called1 && called2) {
resolve(); resolve();
}
} }
}); }
results2.addListener((collection, changeset) => {
if (subscription2.state == Realm.Sync.SubscriptionState.Complete) {
TestCase.assertEqual(collection.length, 1);
TestCase.assertTrue(collection[0].name === 'Lassy 2', "The object is not synced correctly");
called2 = true;
if (called1 && called2) {
resolve();
}
}
});
setTimeout(function() {
reject("listener never called");
}, 5000);
}); });
}) results2.addListener((collection, changeset) => {
}) if (subscription2.state == Realm.Sync.SubscriptionState.Complete) {
TestCase.assertEqual(collection.length, 1);
TestCase.assertTrue(collection[0].name === 'Lassy 2', "The object is not synced correctly");
called2 = true;
if (called1 && called2) {
resolve();
}
}
});
setTimeout(function() {
reject("listener never called");
}, 5000);
});
});
}, },
testPartialSyncFailing() { testPartialSyncFailing() {
@ -911,23 +879,22 @@ module.exports = {
const realmName = uuid(); const realmName = uuid();
return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH)
.then(() => { .then(() => Realm.Sync.User.login('http://localhost:9080', username, 'password'))
return Realm.Sync.User.login('http://localhost:9080', username, 'password').then(user => { .then(user => {
let config = { let config = {
sync: { sync: {
user: user, user: user,
url: `realm://localhost:9080/~/${realmName}`, url: `realm://localhost:9080/~/${realmName}`,
partial: false, // <---- calling subscribe should fail partial: false, // <---- calling subscribe should fail
error: (session, error) => console.log(error) error: (session, error) => console.log(error)
}, },
schema: [{ name: 'Dog', properties: { name: 'string' } }] schema: [{ name: 'Dog', properties: { name: 'string' } }]
}; };
Realm.deleteFile(config); Realm.deleteFile(config);
const realm = new Realm(config); const realm = new Realm(config);
TestCase.assertEqual(realm.objects('Dog').length, 0); TestCase.assertEqual(realm.objects('Dog').length, 0);
TestCase.assertThrows(function () { var subscription = realm.objects('Dog').filtered("name == 'Lassy 1'").subscribe(); } ); TestCase.assertThrows(function () { var subscription = realm.objects('Dog').filtered("name == 'Lassy 1'").subscribe(); } );
});
}); });
}, },
@ -941,36 +908,35 @@ module.exports = {
const realmName = uuid(); const realmName = uuid();
return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH)
.then(() => { .then(() => Realm.Sync.User.login('http://localhost:9080', username, 'password'))
return Realm.Sync.User.login('http://localhost:9080', username, 'password').then(user => { .then(user => {
let config = { let config = {
sync: { sync: {
user: user, user: user,
url: `realm://localhost:9080/~/${realmName}`, url: `realm://localhost:9080/~/${realmName}`,
partial: true, partial: true,
error: (session, error) => console.log(error) error: (session, error) => console.log(error)
}, },
schema: [{ name: 'Dog', properties: { name: 'string' } }] schema: [{ name: 'Dog', properties: { name: 'string' } }]
}; };
Realm.deleteFile(config); Realm.deleteFile(config);
const realm = new Realm(config); const realm = new Realm(config);
var results = realm.objects('Dog').filtered("name == 'Lassy 1'"); var results = realm.objects('Dog').filtered("name == 'Lassy 1'");
var subscription = results.subscribe(); var subscription = results.subscribe();
TestCase.assertEqual(subscription.state, Realm.Sync.SubscriptionState.Creating); TestCase.assertEqual(subscription.state, Realm.Sync.SubscriptionState.Creating);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
results.addListener((collection, changes) => { results.addListener((collection, changes) => {
if (subscription.state === Realm.Sync.SubscriptionState.Complete) { if (subscription.state === Realm.Sync.SubscriptionState.Complete) {
subscription.unsubscribe(); subscription.unsubscribe();
} }
if (subscription.state === Realm.Sync.SubscriptionState.Invalidated) { if (subscription.state === Realm.Sync.SubscriptionState.Invalidated) {
resolve(); resolve();
} }
});
setTimeout(function() {
reject("listener never called");
}, 5000);
}); });
setTimeout(function() {
reject("listener never called");
}, 5000);
}); });
}); });
}, },