diff --git a/scripts/tests/test-rpc.js b/scripts/tests/test-rpc.js index 368c5314c..70ea13053 100644 --- a/scripts/tests/test-rpc.js +++ b/scripts/tests/test-rpc.js @@ -525,6 +525,26 @@ nThen(function (w) { } console.log("Promoted Alice to ADMIN"); })); +}).nThen(function (w) { + var data = {}; + data[bob.curveKeys.curvePublic] = { + notifications: Hash.createChannelId(), + displayName: "BORB", + }; + + alice.roster.add(data, w(function (err) { + if (err === 'ALREADY_PRESENT' || err === 'NO_CHANGE') { + return void console.log("Duplicate add command failed as expected"); + } + if (err) { + console.error("Unexpected error", err); + process.exit(1); + } + if (!err) { + console.log("Duplicate add succeeded unexpectedly"); + process.exit(1); + } + })); }).nThen(function (w) { alice.roster.checkpoint(w(function (err) { if (!err) { return; } @@ -539,6 +559,16 @@ nThen(function (w) { console.error(err); process.exit(1); })); +}).nThen(function (w) { + alice.roster.remove([ + oscar.curveKeys.curvePublic, + ], w(function (err) { + if (!err) { + console.error("Removal of owner by admin succeeded unexpectedly"); + process.exit(1); + } + console.log("Removal of owner by admin failed as expected"); + })); }).nThen(function (w) { // bob finally connects, this time with the lastKnownHash provided by oscar var rosterKeys = Crypto.Team.deriveMemberKeys(sharedConfig.rosterSeed, bob.curveKeys); @@ -581,16 +611,109 @@ nThen(function (w) { } console.log("'add' by member failed as expected"); })); +}).nThen(function (w) { + bob.roster.remove([ + alice.curveKeys.curvePublic, + ], w(function (err) { + if (!err) { + console.error("Removal of admin by member succeeded unexpectedly"); + process.exit(1); + } + console.log("Removal of admin by member failed as expected"); + })); }).nThen(function (w) { bob.roster.remove([ oscar.curveKeys.curvePublic, - alice.curveKeys.curvePublic + //alice.curveKeys.curvePublic ], w(function (err) { if (err) { return void console.log("command failed as expected"); } w.abort(); console.log("Expected command to fail!"); process.exit(1); })); +}).nThen(function (w) { + var data = {}; + data[bob.curveKeys.curvePublic] = { + displayName: 'BORB', + }; + + bob.roster.describe(data, w(function (err) { + if (err) { + console.error("self-description by a member failed unexpectedly"); + process.exit(1); + } + })); +}).nThen(function (w) { + var data = {}; + data[oscar.curveKeys.curvePublic] = { + displayName: 'NULL', + }; + + bob.roster.describe(data, w(function (err) { + if (!err) { + console.error("description of an owner by a member succeeded unexpectedly"); + process.exit(1); + } + console.log("description of an owner by a member failed as expected"); + })); +}).nThen(function (w) { + var data = {}; + data[alice.curveKeys.curvePublic] = { + displayName: 'NULL', + }; + + bob.roster.describe(data, w(function (err) { + if (!err) { + console.error("description of an admin by a member succeeded unexpectedly"); + process.exit(1); + } + console.log("description of an admin by a member failed as expected"); + })); +}).nThen(function (w) { + var data = {}; + data[bob.curveKeys.curvePublic] = { + displayName: "NULL", + }; + + alice.roster.describe(data, w(function (err) { + if (err) { + console.error("Description of member by admin failed unexpectedly"); + console.error(err); + process.exit(1); + } + })); +}).nThen(function (w) { + alice.roster.metadata({ + name: "BEST TEAM", + topic: "Champions de monde!", + cheese: "Camembert", + }, w(function (err) { + if (err) { + console.error("Metadata change by admin failed unexpectedly"); + console.error(err); + process.exit(1); + } + })); +}).nThen(function (w) { + bob.roster.metadata({ + name: "WORST TEAM", + topic: "not a good team", + }, w(function (err) { + if (!err) { + console.error("Metadata change by member should have failed"); + process.exit(1); + } + })); +}).nThen(function (w) { + oscar.roster.metadata({ + cheese: null, // delete a field that you don't want presenet + }, w(function (err) { + if (err) { + console.error(err); + process.exit(1); + } + + })); }).nThen(function (w) { alice.roster.remove([bob.curveKeys.curvePublic], w(function (err) { if (err) { diff --git a/www/common/outer/roster.js b/www/common/outer/roster.js index ca7c48554..f2de26da7 100644 --- a/www/common/outer/roster.js +++ b/www/common/outer/roster.js @@ -171,6 +171,10 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) { // if no role was provided, assume MEMBER if (typeof(data.role) !== 'string') { data.role = 'MEMBER'; } + if (!canAddRole(author, data.role, members)) { + throw new Error("INSUFFICIENT_PERMISSIONS"); + } + if (typeof(data.displayName) !== 'string') { throw new Error("DISPLAYNAME_REQUIRED"); } if (typeof(data.notifications) !== 'string') { throw new Error("NOTIFICATIONS_REQUIRED"); } }); @@ -178,12 +182,9 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) { var changed = false; // then iterate again and apply it Object.keys(args).forEach(function (curve) { - var data = args[curve]; - if (!canAddRole(author, data.role, members)) { return; } - // this will result in a change changed = true; - members[curve] = data; + members[curve] = args[curve]; }); return changed; @@ -322,6 +323,12 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) { return true; }; + var MANDATORY_METADATA_FIELDS = [ + 'avatar', + 'name', + 'topic', + ]; + // only admin/owner can change group metadata commands.METADATA = function (args, author, roster) { if (!isMap(args)) { throw new Error("INVALID_ARGS"); } @@ -330,6 +337,11 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) { // validate inputs Object.keys(args).forEach(function (k) { + if (args[k] === null) { + if (MANDATORY_METADATA_FIELDS.indexOf(k) === -1) { return; } + throw new Error('CANNOT_REMOVE_MANDATORY_METADATA'); + } + // can't set metadata to anything other than strings // use empty string to unset a value if you must if (typeof(args[k]) !== 'string') { throw new Error("INVALID_ARGUMENTS"); } @@ -338,6 +350,11 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) { var changed = false; // {topic, name, avatar} are all strings... Object.keys(args).forEach(function (k) { + if (typeof(roster.state.metadata[k]) !== 'undefined' && args[k] === null) { + changed = true; + delete roster.state.metadata[k]; + } + // ignore things that won't cause changes if (args[k] === roster.state.metadata[k]) { return; }