diff --git a/CHANGELOG.md b/CHANGELOG.md index d6dc3998..120afc60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,14 @@ - 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. * [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 * Updated to Realm Core 5.3.0. diff --git a/docs/permission.js b/docs/permission.js index 450ae7f9..dc4d03fb 100644 --- a/docs/permission.js +++ b/docs/permission.js @@ -21,7 +21,7 @@ * They are created exclusively by the client and are processed by the server * as indicated by the status fields. * PermissionChange objects allow to grant and revoke permissions by setting - * mayRead, mayWrite and mayManage accordingly. + * mayRead, mayWrite and mayManage accordingly. * If any of these flags are not set, these are merged * with either the existing or default permissions as applicable. As a * side-effect this causes that the default permissions are permanently @@ -30,7 +30,7 @@ * ErrorCode will be updated accordingly. */ class PermissionChange { - + /** * Gets the unique identifier of this object in the Management realm. * @type {string} @@ -229,3 +229,161 @@ class PermissionOfferResponse { */ 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 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} + */ + 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} + */ + 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} + */ + get permissions() {} +} diff --git a/docs/realm.js b/docs/realm.js index a04c259d..359a714a 100644 --- a/docs/realm.js +++ b/docs/realm.js @@ -125,6 +125,27 @@ class Realm { */ 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. * @param {Realm~ObjectType} type - The type of Realm object to create. diff --git a/lib/extensions.js b/lib/extensions.js index 5c793e34..033d305c 100644 --- a/lib/extensions.js +++ b/lib/extensions.js @@ -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. 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. diff --git a/lib/index.d.ts b/lib/index.d.ts index 76cd5432..21f0e93a 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -81,7 +81,7 @@ declare namespace Realm { path?: string; readOnly?: boolean; inMemory?: boolean; - schema?: ObjectClass[] | ObjectSchema[]; + schema?: (ObjectClass | ObjectSchema)[]; schemaVersion?: number; sync?: Realm.Sync.SyncConfiguration; deleteRealmIfMigrationNeeded?: boolean; @@ -406,7 +406,7 @@ declare namespace Realm.Sync { * @see { @link https://realm.io/docs/javascript/latest/api/Realm.Sync.Subscription.html } */ class Subscription { - readonly state: number; + readonly state: SubscriptionState; readonly error: string; unsubscribe(): void; @@ -414,6 +414,14 @@ declare namespace Realm.Sync { removeListener(subscruptionCallback: SubscriptionNotificationCallback): void; } + enum SubscriptionState { + Error, + Creating, + Pending, + Complete, + Invalidated, + } + /** * AuthError * @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 { progress(callback: Realm.Sync.ProgressNotificationCallback): Promise @@ -544,7 +588,7 @@ declare class Realm { * @param {boolean} update? * @returns T */ - create(type: string | Realm.ObjectClass | Function, properties: T & Realm.ObjectPropsType, update?: boolean): T; + create(type: string | Realm.ObjectClass | Function, properties: T | Realm.ObjectPropsType, update?: boolean): T; /** * @param {Realm.Object|Realm.Object[]|Realm.List|Realm.Results|any} object @@ -620,6 +664,10 @@ declare class Realm { * @returns 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' { diff --git a/src/js_realm.hpp b/src/js_realm.hpp index 2d0a8d57..977a5e12 100644 --- a/src/js_realm.hpp +++ b/src/js_realm.hpp @@ -35,7 +35,6 @@ #include "js_sync.hpp" #include "sync/sync_config.hpp" #include "sync/sync_manager.hpp" -#include "sync/partial_sync.hpp" #endif #include "shared_realm.hpp" @@ -186,6 +185,7 @@ public: static void compact(ContextType, ObjectType, Arguments, ReturnValue &); static void delete_model(ContextType, ObjectType, Arguments, ReturnValue &); static void object_for_object_id(ContextType, ObjectType, Arguments, ReturnValue&); + static void privileges(ContextType, ObjectType, Arguments, ReturnValue&); // properties static void get_empty(ContextType, ObjectType, ReturnValue &); @@ -242,6 +242,7 @@ public: {"close", wrap}, {"compact", wrap}, {"deleteModel", wrap}, + {"privileges", wrap}, {"_objectForObjectId", wrap}, #if REALM_ENABLE_SYNC {"_waitForDownload", wrap}, @@ -291,7 +292,8 @@ public: 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)) { FunctionType constructor = Value::to_constructor(ctx, value); @@ -541,7 +543,7 @@ void RealmClass::constructor(ContextType ctx, ObjectType this_object, size_t template SharedRealm RealmClass::create_shared_realm(ContextType ctx, realm::Realm::Config config, bool schema_updated, - ObjectDefaultsMap && defaults, ConstructorMap && constructors) { + ObjectDefaultsMap&& defaults, ConstructorMap&& constructors) { config.execution_context = Context::get_execution_context_id(ctx); SharedRealm realm; @@ -551,9 +553,6 @@ SharedRealm RealmClass::create_shared_realm(ContextType ctx, realm::Realm::Co catch (const RealmFileException& ex) { handleRealmFileException(ctx, config, ex); } - catch (...) { - throw; - } GlobalContextType global_context = Context::get_global_context(ctx); if (!realm->m_binding_context) { @@ -790,10 +789,8 @@ void RealmClass::objects(ContextType ctx, ObjectType this_object, Arguments a args.validate_maximum(1); SharedRealm realm = *get_internal>(this_object); - std::string object_type; - validated_object_schema_for_value(ctx, realm, args[0], object_type); - - return_value.set(ResultsClass::create_instance(ctx, realm, object_type)); + auto& object_schema = validated_object_schema_for_value(ctx, realm, args[0]); + return_value.set(ResultsClass::create_instance(ctx, realm, object_schema.name)); } template @@ -802,7 +799,7 @@ void RealmClass::object_for_primary_key(ContextType ctx, ObjectType this_obje SharedRealm realm = *get_internal>(this_object); 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); auto realm_object = realm::Object::get_for_primary_key(accessor, realm, object_schema, args[1]); @@ -820,8 +817,7 @@ void RealmClass::create(ContextType ctx, ObjectType this_object, Arguments ar SharedRealm realm = *get_internal>(this_object); realm->verify_open(); - 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]); ObjectType object = Value::validated_to_object(ctx, args[1], "properties"); if (Value::is_array(ctx, args[1])) { @@ -1008,36 +1004,6 @@ void RealmClass::compact(ContextType ctx, ObjectType this_object, Arguments a 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(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 void RealmClass::object_for_object_id(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue& return_value) { args.validate_count(2); @@ -1047,21 +1013,67 @@ void RealmClass::object_for_object_id(ContextType ctx, ObjectType this_object if (!sync::has_object_ids(realm->read_group())) throw std::logic_error("Realm._objectForObjectId() can only be used with synced Realms."); - std::string object_type = Value::validated_to_string(ctx, args[0]); - validated_object_schema_for_value(ctx, realm, args[0], object_type); - + auto& object_schema = validated_object_schema_for_value(ctx, realm, args[0]); 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(); - 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) { - return_value.set(RealmObjectClass::create_instance(ctx, realm::Object(realm, object_type, ndx))); + return_value.set(RealmObjectClass::create_instance(ctx, realm::Object(realm, object_schema.name, ndx))); } #else throw std::logic_error("Realm._objectForObjectId() can only be used with synced Realms."); #endif // REALM_ENABLE_SYNC } +template +void RealmClass::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(actual) & static_cast(expected)) == static_cast(expected); + }; + + SharedRealm realm = *get_internal>(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>(ctx, arg)) { + auto obj = get_internal>(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 } // realm diff --git a/tests/js/asserts.js b/tests/js/asserts.js index ff01df46..154c4821 100644 --- a/tests/js/asserts.js +++ b/tests/js/asserts.js @@ -37,7 +37,8 @@ module.exports = { } else if (type === 'object') { 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') { diff --git a/tests/js/nested-list-helper.js b/tests/js/nested-list-helper.js index e4b0803c..7de46e5e 100644 --- a/tests/js/nested-list-helper.js +++ b/tests/js/nested-list-helper.js @@ -9,6 +9,8 @@ const realmName = process.argv[4]; const realmModule = process.argv[5]; 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]); function createObjects(user) { diff --git a/tests/js/permission-tests.js b/tests/js/permission-tests.js index 4379e494..ccfe3f2b 100644 --- a/tests/js/permission-tests.js +++ b/tests/js/permission-tests.js @@ -16,33 +16,33 @@ // //////////////////////////////////////////////////////////////////////////// - 'use strict'; var Realm = require('realm'); var TestCase = require('./asserts'); function uuid() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { - var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); - return v.toString(16); - }); + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); } function createUsersWithTestRealms(count) { - const createUserWithTestRealm = () => { - return Realm.Sync.User.register('http://localhost:9080', uuid(), 'password') - .then(user => { - new Realm({sync: {user, url: 'realm://localhost:9080/~/test'}}).close(); - return user; - }); - }; + const createUserWithTestRealm = () => { + return Realm.Sync.User + .register('http://localhost:9080', uuid(), 'password') + .then(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) { - return new Promise(resolve => setTimeout(resolve, t)); + return new Promise(resolve => setTimeout(resolve, t)); } function repeatUntil(fn, predicate) { @@ -57,66 +57,183 @@ function repeatUntil(fn, predicate) { 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 = { testApplyAndGetGrantedPermissions() { - return createUsersWithTestRealms(1) - .then(([user]) => { - return user.applyPermissions({ userId: `${user.identity}` }, `/${user.identity}/test`, 'read') - .then(repeatUntil(() => user.getGrantedPermissions('any'), - permissions => permissions.length > 1)) - .then(permissions => { - TestCase.assertEqual(permissions[0].path, `/${user.identity}/test`); - TestCase.assertEqual(permissions[0].mayRead, true); - TestCase.assertEqual(permissions[0].mayWrite, false); - TestCase.assertEqual(permissions[0].mayManage, false); - }); + return createUsersWithTestRealms(1).then(([user]) => { + const path = `/${user.identity}/test`; + return user + .applyPermissions({userId: `${user.identity}`}, + `/${user.identity}/test`, 'read') + .then(repeatUntil(() => user.getGrantedPermissions('any'), + permissions => { + let permission = permissionForPath(permissions, path); + return permission && !permission.mayWrite; + })) + .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() { - return createUsersWithTestRealms(2) - .then(([user1, user2]) => { - return user1.offerPermissions(`/${user1.identity}/test`, 'read') - .then(token => user2.acceptPermissionOffer(token)) - .then(realmUrl => { - TestCase.assertEqual(realmUrl, `/${user1.identity}/test`); - return realmUrl; - }) - .then(repeatUntil(() => user2.getGrantedPermissions('any'), - permissions => permissions.length > 1)) - .then(permissions => { - TestCase.assertEqual(permissions[2].path, `/${user1.identity}/test`); - TestCase.assertEqual(permissions[2].mayRead, true); - TestCase.assertEqual(permissions[2].mayWrite, false); - TestCase.assertEqual(permissions[2].mayManage, false); - }); + return createUsersWithTestRealms(2).then(([user1, user2]) => { + const path = `/${user1.identity}/test`; + return user1.offerPermissions(`/${user1.identity}/test`, 'read') + .then(token => user2.acceptPermissionOffer(token)) + .then(realmUrl => { + TestCase.assertEqual(realmUrl, path); + return realmUrl; + }) + .then(repeatUntil(() => user2.getGrantedPermissions('any'), + permissions => permissions.length > 2 + && permissionForPath(permissions, path))) + .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); + }); }); }, testInvalidatePermissionOffer() { - let user1, user2, token; - return createUsersWithTestRealms(2) - .then(users => { - user1 = users[0]; - user2 = users[1]; - return user1.offerPermissions(`/${user1.identity}/test`, 'read'); - }) - .then(t => { token = t; return user1.invalidatePermissionOffer(token); }) - // Since we don't yet support notification when the invalidation has gone through, - // wait for a bit and hope the server is done processing. - .then(wait(100)) - .then(() => user2.acceptPermissionOffer(token)) - // We want the call to fail, i.e. the catch() below should be called. - .then(() => { throw new Error("User was able to accept an invalid permission offer token"); }) - .catch(error => { - try { - TestCase.assertEqual(error.message, 'The permission offer is expired.'); - TestCase.assertEqual(error.statusCode, 701); - } - catch (e) { - throw new Error(e); - } - }); + let user1, user2, token; + return createUsersWithTestRealms(2) + .then(users => { + user1 = users[0]; + user2 = users[1]; + return user1.offerPermissions(`/${user1.identity}/test`, 'read'); + }) + .then(t => { + token = t; + return user1.invalidatePermissionOffer(token); + }) + // Since we don't yet support notification when the invalidation has + // gone through, wait for a bit and hope the server is done + // processing. + .then(() => wait(100)) + .then(() => user2.acceptPermissionOffer(token)) + // We want the call to fail, i.e. the catch() below should be + // called. + .then(() => { + throw new Error("User was able to accept an invalid permission offer token"); + }) + .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(); + }); + } +} diff --git a/tests/js/session-tests.js b/tests/js/session-tests.js index 24a47288..ee01c3c6 100644 --- a/tests/js/session-tests.js +++ b/tests/js/session-tests.js @@ -61,27 +61,23 @@ function copyFileToTempDir(filename) { return tmpFile.name; } -function runOutOfProcess(nodeJsFilePath) { - var nodeArgs = Array.prototype.slice.call(arguments); +function runOutOfProcess() { + const args = Array.prototype.slice.call(arguments); let tmpDir = tmp.dirSync(); - let content = fs.readFileSync(nodeJsFilePath, 'utf8'); - let tmpFile = tmp.fileSync({ dir: tmpDir.name }); - fs.appendFileSync(tmpFile.fd, content, { encoding: 'utf8' }); - nodeArgs[0] = tmpFile.name; + console.log(`runOutOfProcess : ${args.join(' ')}`); return new Promise((resolve, reject) => { try { - console.log('runOutOfProcess command\n node ' + nodeArgs.join(" ")); - const child = execFile('node', nodeArgs, { cwd: tmpDir.name }, (error, stdout, stderr) => { + execFile(process.execPath, args, {cwd: tmpDir.name}, (error, stdout, stderr) => { if (error) { - console.error("runOutOfProcess failed\n" + error); - reject(new Error(`Running ${nodeJsFilePath} failed. error: ${error}`)); + console.error("runOutOfProcess failed\n", error, stdout, stderr); + reject(new Error(`Running ${args[0]} failed. error: ${error}`)); return; } console.log('runOutOfProcess success\n' + stdout); resolve(); }); - } + } catch (e) { reject(e); } @@ -275,43 +271,40 @@ module.exports = { const expectedObjectsCount = 3; return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) - .then(() => { - return Realm.Sync.User.login('http://localhost:9080', username, 'password').then(user => { - return new Promise((resolve, reject) => { - const accessTokenRefreshed = this; - let successCounter = 0; + .then(() => Realm.Sync.User.login('http://localhost:9080', username, 'password')) + .then(user => { + const accessTokenRefreshed = this; + let successCounter = 0; - let config = { - sync: { user, url: `realm://localhost:9080/~/${realmName}` } - }; + let config = { + 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) => { - try { - if (error) { - reject(error); - } + let firstDog = realm.objects('Dog')[0]; + TestCase.assertTrue(({}).hasOwnProperty.call(firstDog, 'name'), "Synced realm does not have an inffered schema"); + TestCase.assertTrue(firstDog.name, "Synced realm object's property should have a value"); + TestCase.assertTrue(firstDog.name.indexOf('Lassy') !== -1, "Synced realm object's property should contain the actual written value"); - let actualObjectsCount = realm.objects('Dog').length; - TestCase.assertEqual(actualObjectsCount, expectedObjectsCount, "Synced realm does not contain the expected objects count"); - - let firstDog = realm.objects('Dog')[0]; - TestCase.assertTrue(({}).hasOwnProperty.call(firstDog, 'name'), "Synced realm does not have an inffered schema"); - TestCase.assertTrue(firstDog.name, "Synced realm object's property should have a value"); - TestCase.assertTrue(firstDog.name.indexOf('Lassy') !== -1, "Synced realm object's property should contain the actual written value"); - - - const session = realm.syncSession; - 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); - } - }); + const session = realm.syncSession; + 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 expectedObjectsCount = 3; + const accessTokenRefreshed = this; + let successCounter = 0; - return new Promise((resolve, reject) => { - const accessTokenRefreshed = this; - let successCounter = 0; + let config = { + schema: [{ name: 'Dog', properties: { name: 'string' } }], + }; - let config = { - schema: [{ name: 'Dog', properties: { name: 'string' } }], - }; + return Realm.open(config).then(realm => { + realm.write(() => { + for (let i = 1; i <= 3; i++) { + realm.create('Dog', { name: `Lassy ${i}` }); + } + }); - Realm.open(config).then(realm => { - realm.write(() => { - 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)); + let actualObjectsCount = realm.objects('Dog').length; + TestCase.assertEqual(actualObjectsCount, expectedObjectsCount, "Local realm does not contain the expected objects count"); }); }, @@ -412,37 +401,33 @@ module.exports = { const realmName = uuid(); return runOutOfProcess(__dirname + '/nested-list-helper.js', __dirname + '/schemas.js', username, realmName, REALM_MODULE_PATH) - .then(() => { - return Realm.Sync.User.login('http://localhost:9080', username, 'password').then(user => { - return new Promise((resolve, reject) => { - let config = { - schema: [schemas.ParentObject, schemas.NameObject], - sync: { user, url: `realm://localhost:9080/~/${realmName}` } - }; - Realm.open(config).then(realm => { - let objects = realm.objects('ParentObject'); + .then(() => Realm.Sync.User.login('http://localhost:9080', username, 'password')) + .then(user => { + let config = { + schema: [schemas.ParentObject, schemas.NameObject], + sync: { user, url: `realm://localhost:9080/~/${realmName}` } + }; + return Realm.open(config) + }).then(realm => { + let objects = realm.objects('ParentObject'); - 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(objects.length, 2); - TestCase.assertEqual(objects[0].name.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].given[0], 'Hans'); - 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[0], 'Ib'); - TestCase.assertEqual(objects[0].name[1].prefix.length, 0); + 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(objects.length, 2); + TestCase.assertEqual(objects[0].name.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].given[0], 'Hans'); + 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[0], 'Ib'); + TestCase.assertEqual(objects[0].name[1].prefix.length, 0); - TestCase.assertEqual(objects[1].name.length, 1); - TestCase.assertEqual(objects[1].name[0].given.length, 2); - 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[1], 'Margrete'); - resolve(); - }).catch(() => reject()); - }); - }); + TestCase.assertEqual(objects[1].name.length, 1); + TestCase.assertEqual(objects[1].name[0].given.length, 2); + 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[1], 'Margrete'); }); }, @@ -610,60 +595,59 @@ module.exports = { const realmName = uuid(); return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) - .then(() => { - return Realm.Sync.User.login('http://localhost:9080', username, 'password').then(user => { - return new Promise((resolve, reject) => { - let config = { - sync: { - user, - url: `realm://localhost:9080/~/${realmName}` - }, - schema: [{ name: 'Dog', properties: { name: 'string' } }], - }; + .then(() => Realm.Sync.User.login('http://localhost:9080', username, 'password')) + .then(user => { + let config = { + sync: { + user, + url: `realm://localhost:9080/~/${realmName}` + }, + schema: [{ name: 'Dog', properties: { name: 'string' } }], + }; - let realm = new Realm(config); - let unregisterFunc; + let realm = new Realm(config); + let unregisterFunc; - let writeDataFunc = () => { - realm.write(() => { - for (let i = 1; i <= 3; i++) { - realm.create('Dog', { name: `Lassy ${i}` }); - } - }); + let writeDataFunc = () => { + realm.write(() => { + for (let i = 1; i <= 3; 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; - let failOnCall = false; - const progressCallback = (transferred, total) => { - if (failOnCall) { - reject(new Error("Progress callback should not be called after removeProgressNotification")); - } + syncFinished = transferred === total; - syncFinished = transferred === total; + //unregister and write some new data. + if (syncFinished) { + failOnCall = true; + unregisterFunc(); - //unregister and write some new data. - if (syncFinished) { - failOnCall = true; - unregisterFunc(); + //use second callback to wait for sync finished + realm.syncSession.addProgressNotification('upload', 'reportIndefinitely', (transferred, transferable) => { + if (transferred === transferable) { + resolve(); + } + }); + writeDataFunc(); + } + }; - //use second callback to wait for sync finished - realm.syncSession.addProgressNotification('upload', 'reportIndefinitely', (transferred, transferable) => { - if (transferred === transferable) { - resolve(); - } - }); - writeDataFunc(); - } - }; + realm.syncSession.addProgressNotification('upload', 'reportIndefinitely', progressCallback); - realm.syncSession.addProgressNotification('upload', 'reportIndefinitely', progressCallback); + unregisterFunc = () => { + realm.syncSession.removeProgressNotification(progressCallback); + }; - unregisterFunc = () => { - realm.syncSession.removeProgressNotification(progressCallback); - }; - - writeDataFunc(); - }); + writeDataFunc(); }); }); }, @@ -675,36 +659,24 @@ module.exports = { const username = uuid(); const realmName = uuid(); + let progressCalled = false; return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) - .then(() => { - return Realm.Sync.User.login('http://localhost:9080', username, 'password').then(user => { - return new Promise((resolve, reject) => { - let config = { - sync: { - user, - url: `realm://localhost:9080/~/${realmName}` - }, - schema: [{ name: 'Dog', properties: { name: 'string' } }], - }; + .then(() => Realm.Sync.User.login('http://localhost:9080', username, 'password')) + .then(user => { + let config = { + sync: { + user, + url: `realm://localhost:9080/~/${realmName}` + }, + schema: [{ name: 'Dog', properties: { name: 'string' } }], + }; - let progressCalled = false; - Realm.open(config) - .progress((transferred, total) => { - progressCalled = true; - }) - .then(() => { - TestCase.assertTrue(progressCalled); - resolve(); - }) - .catch((e) => reject(e)); - - setTimeout(function() { - reject("Progress Notifications API failed to call progress callback for Realm constructor"); - }, 5000); - }); - }); - }); + return Promise.race([ + Realm.open(config).progress((transferred, total) => { progressCalled = true; }), + new Promise((_, reject) => setTimeout(() => reject("Progress Notifications API failed to call progress callback for Realm constructor"), 5000)) + ]); + }).then(() => TestCase.assertTrue(progressCalled)); }, testProgressNotificationsForRealmOpenAsync() { @@ -716,37 +688,36 @@ module.exports = { const realmName = uuid(); return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) - .then(() => { - return Realm.Sync.User.login('http://localhost:9080', username, 'password').then(user => { - return new Promise((resolve, reject) => { - let config = { - sync: { - user, - url: `realm://localhost:9080/~/${realmName}` - }, - schema: [{ name: 'Dog', properties: { name: 'string' } }], - }; + .then(() => Realm.Sync.User.login('http://localhost:9080', username, 'password')) + .then(user => { + return new Promise((resolve, reject) => { + let config = { + sync: { + user, + url: `realm://localhost:9080/~/${realmName}` + }, + schema: [{ name: 'Dog', properties: { name: 'string' } }], + }; - let progressCalled = false; + let progressCalled = false; - Realm.openAsync(config, - (error, realm) => { - if (error) { - reject(error); - return; - } + Realm.openAsync(config, + (error, realm) => { + if (error) { + reject(error); + return; + } - TestCase.assertTrue(progressCalled); - resolve(); - }, - (transferred, total) => { - progressCalled = true; - }); + TestCase.assertTrue(progressCalled); + resolve(); + }, + (transferred, total) => { + progressCalled = true; + }); - setTimeout(function() { - reject("Progress Notifications API failed to call progress callback for Realm constructor"); - }, 5000); - }); + setTimeout(function() { + reject("Progress Notifications API failed to call progress callback for Realm constructor"); + }, 5000); }); }); }, @@ -761,38 +732,37 @@ module.exports = { const realmName = uuid(); return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) - .then(() => { - return Realm.Sync.User.login('http://localhost:9080', username, 'password').then(user => { - let config = { - sync: { - user: user, - url: `realm://localhost:9080/~/${realmName}`, - partial: true, - error: (session, error) => console.log(error) - }, - schema: [{ name: 'Dog', properties: { name: 'string' } }] - }; + .then(() => Realm.Sync.User.login('http://localhost:9080', username, 'password')) + .then(user => { + let config = { + sync: { + user: user, + url: `realm://localhost:9080/~/${realmName}`, + partial: true, + error: (session, error) => console.log(error) + }, + schema: [{ name: 'Dog', properties: { name: 'string' } }] + }; - Realm.deleteFile(config); - const realm = new Realm(config); - TestCase.assertEqual(realm.objects('Dog').length, 0); - var results = realm.objects('Dog').filtered("name == 'Lassy 1'"); - var subscription = results.subscribe(); - TestCase.assertEqual(subscription.state, Realm.Sync.SubscriptionState.Creating); - return new Promise((resolve, reject) => { - subscription.addListener((subscription, state) => { - if (state == Realm.Sync.SubscriptionState.Complete) { - TestCase.assertEqual(results.length, 1); - TestCase.assertTrue(results[0].name === 'Lassy 1', "The object is not synced correctly"); - resolve(); - } - }); - setTimeout(function() { - reject("listener never called"); - }, 5000); + Realm.deleteFile(config); + const realm = new Realm(config); + TestCase.assertEqual(realm.objects('Dog').length, 0); + var results = realm.objects('Dog').filtered("name == 'Lassy 1'"); + var subscription = results.subscribe(); + TestCase.assertEqual(subscription.state, Realm.Sync.SubscriptionState.Creating); + return new Promise((resolve, reject) => { + subscription.addListener((subscription, state) => { + if (state == Realm.Sync.SubscriptionState.Complete) { + TestCase.assertEqual(results.length, 1); + TestCase.assertTrue(results[0].name === 'Lassy 1', "The object is not synced correctly"); + resolve(); + } }); - }) - }) + setTimeout(function() { + reject("listener never called"); + }, 5000); + }); + }); }, testPartialSyncAnonymous_ResultsListener() { @@ -805,38 +775,37 @@ module.exports = { const realmName = uuid(); return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) - .then(() => { - return Realm.Sync.User.login('http://localhost:9080', username, 'password').then(user => { - let config = { - sync: { - user: user, - url: `realm://localhost:9080/~/${realmName}`, - partial: true, - error: (session, error) => console.log(error) - }, - schema: [{ name: 'Dog', properties: { name: 'string' } }] - }; + .then(() => Realm.Sync.User.login('http://localhost:9080', username, 'password')) + .then(user => { + let config = { + sync: { + user: user, + url: `realm://localhost:9080/~/${realmName}`, + partial: true, + error: (session, error) => console.log(error) + }, + schema: [{ name: 'Dog', properties: { name: 'string' } }] + }; - Realm.deleteFile(config); - const realm = new Realm(config); - TestCase.assertEqual(realm.objects('Dog').length, 0); - var results = realm.objects('Dog').filtered("name == 'Lassy 1'"); - var subscription = results.subscribe(); - TestCase.assertEqual(subscription.state, Realm.Sync.SubscriptionState.Creating); - return new Promise((resolve, reject) => { - results.addListener((collection, changes) => { - if (subscription.state === Realm.Sync.SubscriptionState.Complete) { - TestCase.assertEqual(collection.length, 1); - TestCase.assertTrue(collection[0].name === 'Lassy 1', "The object is not synced correctly"); - resolve(); - } - }); - setTimeout(function() { - reject("listener never called"); - }, 5000); + Realm.deleteFile(config); + const realm = new Realm(config); + TestCase.assertEqual(realm.objects('Dog').length, 0); + var results = realm.objects('Dog').filtered("name == 'Lassy 1'"); + var subscription = results.subscribe(); + TestCase.assertEqual(subscription.state, Realm.Sync.SubscriptionState.Creating); + return new Promise((resolve, reject) => { + results.addListener((collection, changes) => { + if (subscription.state === Realm.Sync.SubscriptionState.Complete) { + TestCase.assertEqual(collection.length, 1); + TestCase.assertTrue(collection[0].name === 'Lassy 1', "The object is not synced correctly"); + resolve(); + } }); - }) - }) + setTimeout(function() { + reject("listener never called"); + }, 5000); + }); + }); }, testPartialSyncMultipleSubscriptions() { @@ -849,56 +818,55 @@ module.exports = { const realmName = uuid(); return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) - .then(() => { - return Realm.Sync.User.login('http://localhost:9080', username, 'password').then(user => { - let config = { - sync: { - user: user, - url: `realm://localhost:9080/~/${realmName}`, - partial: true, - error: (session, error) => console.log(error) - }, - schema: [{ name: 'Dog', properties: { name: 'string' } }] - }; + .then(() => Realm.Sync.User.login('http://localhost:9080', username, 'password')) + .then(user => { + let config = { + sync: { + user: user, + url: `realm://localhost:9080/~/${realmName}`, + partial: true, + error: (session, error) => console.log(error) + }, + schema: [{ name: 'Dog', properties: { name: 'string' } }] + }; - Realm.deleteFile(config); - const realm = new Realm(config); - TestCase.assertEqual(realm.objects('Dog').length, 0); - var results1 = realm.objects('Dog').filtered("name == 'Lassy 1'"); - var results2 = realm.objects('Dog').filtered("name == 'Lassy 2'"); - var subscription1 = results1.subscribe(); - var subscription2 = results2.subscribe(); + Realm.deleteFile(config); + const realm = new Realm(config); + TestCase.assertEqual(realm.objects('Dog').length, 0); + var results1 = realm.objects('Dog').filtered("name == 'Lassy 1'"); + var results2 = realm.objects('Dog').filtered("name == 'Lassy 2'"); + var subscription1 = results1.subscribe(); + var subscription2 = results2.subscribe(); - return new Promise((resolve, reject) => { - let called1 = false; - let called2 = false; - results1.addListener((collection, changeset) => { - if (subscription1.state == Realm.Sync.SubscriptionState.Complete) { - TestCase.assertEqual(collection.length, 1); - TestCase.assertTrue(collection[0].name === 'Lassy 1', "The object is not synced correctly"); - called1 = true; - if (called1 && called2) { - resolve(); - } + return new Promise((resolve, reject) => { + let called1 = false; + let called2 = false; + results1.addListener((collection, changeset) => { + if (subscription1.state == Realm.Sync.SubscriptionState.Complete) { + TestCase.assertEqual(collection.length, 1); + TestCase.assertTrue(collection[0].name === 'Lassy 1', "The object is not synced correctly"); + called1 = true; + if (called1 && called2) { + 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() { @@ -911,23 +879,22 @@ module.exports = { const realmName = uuid(); return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) - .then(() => { - return Realm.Sync.User.login('http://localhost:9080', username, 'password').then(user => { - let config = { - sync: { - user: user, - url: `realm://localhost:9080/~/${realmName}`, - partial: false, // <---- calling subscribe should fail - error: (session, error) => console.log(error) - }, - schema: [{ name: 'Dog', properties: { name: 'string' } }] - }; + .then(() => Realm.Sync.User.login('http://localhost:9080', username, 'password')) + .then(user => { + let config = { + sync: { + user: user, + url: `realm://localhost:9080/~/${realmName}`, + partial: false, // <---- calling subscribe should fail + error: (session, error) => console.log(error) + }, + schema: [{ name: 'Dog', properties: { name: 'string' } }] + }; - Realm.deleteFile(config); - const realm = new Realm(config); - TestCase.assertEqual(realm.objects('Dog').length, 0); - TestCase.assertThrows(function () { var subscription = realm.objects('Dog').filtered("name == 'Lassy 1'").subscribe(); } ); - }); + Realm.deleteFile(config); + const realm = new Realm(config); + TestCase.assertEqual(realm.objects('Dog').length, 0); + TestCase.assertThrows(function () { var subscription = realm.objects('Dog').filtered("name == 'Lassy 1'").subscribe(); } ); }); }, @@ -941,36 +908,35 @@ module.exports = { const realmName = uuid(); return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) - .then(() => { - return Realm.Sync.User.login('http://localhost:9080', username, 'password').then(user => { - let config = { - sync: { - user: user, - url: `realm://localhost:9080/~/${realmName}`, - partial: true, - error: (session, error) => console.log(error) - }, - schema: [{ name: 'Dog', properties: { name: 'string' } }] - }; + .then(() => Realm.Sync.User.login('http://localhost:9080', username, 'password')) + .then(user => { + let config = { + sync: { + user: user, + url: `realm://localhost:9080/~/${realmName}`, + partial: true, + error: (session, error) => console.log(error) + }, + schema: [{ name: 'Dog', properties: { name: 'string' } }] + }; - Realm.deleteFile(config); - const realm = new Realm(config); - var results = realm.objects('Dog').filtered("name == 'Lassy 1'"); - var subscription = results.subscribe(); - TestCase.assertEqual(subscription.state, Realm.Sync.SubscriptionState.Creating); - return new Promise((resolve, reject) => { - results.addListener((collection, changes) => { - if (subscription.state === Realm.Sync.SubscriptionState.Complete) { - subscription.unsubscribe(); - } - if (subscription.state === Realm.Sync.SubscriptionState.Invalidated) { - resolve(); - } - }); - setTimeout(function() { - reject("listener never called"); - }, 5000); + Realm.deleteFile(config); + const realm = new Realm(config); + var results = realm.objects('Dog').filtered("name == 'Lassy 1'"); + var subscription = results.subscribe(); + TestCase.assertEqual(subscription.state, Realm.Sync.SubscriptionState.Creating); + return new Promise((resolve, reject) => { + results.addListener((collection, changes) => { + if (subscription.state === Realm.Sync.SubscriptionState.Complete) { + subscription.unsubscribe(); + } + if (subscription.state === Realm.Sync.SubscriptionState.Invalidated) { + resolve(); + } }); + setTimeout(function() { + reject("listener never called"); + }, 5000); }); }); },