diff --git a/src/controllers/groups.js b/src/controllers/groups.js index 2cbbcc7c76..25a6d928db 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -32,17 +32,18 @@ groupsController.getGroupsFromSet = function (uid, sort, start, stop, callback) set = 'groups:visible:createtime'; } - groups.getGroupsFromSet(set, uid, start, stop, function (err, groups) { - if (err) { - return callback(err); + async.waterfall([ + function (next) { + groups.getGroupsFromSet(set, uid, start, stop, next); + }, + function (groupsData, next) { + next(null, { + groups: groupsData, + allowGroupCreation: parseInt(meta.config.allowGroupCreation, 10) === 1, + nextStart: stop + 1 + }); } - - callback(null, { - groups: groups, - allowGroupCreation: parseInt(meta.config.allowGroupCreation, 10) === 1, - nextStart: stop + 1 - }); - }); + ], callback); }; groupsController.details = function (req, res, callback) { diff --git a/src/groups/membership.js b/src/groups/membership.js index e57847a61c..68a9724c9c 100644 --- a/src/groups/membership.js +++ b/src/groups/membership.js @@ -1,6 +1,6 @@ 'use strict'; -var async = require('async'); +var async = require('async'); var winston = require('winston'); var _ = require('underscore'); @@ -138,7 +138,6 @@ module.exports = function (Groups) { }; Groups.acceptMembership = function (groupName, uid, callback) { - // Note: For simplicity, this method intentially doesn't check the caller uid for ownership! async.waterfall([ async.apply(db.setRemove, 'group:' + groupName + ':pending', uid), async.apply(db.setRemove, 'group:' + groupName + ':invited', uid), @@ -147,7 +146,6 @@ module.exports = function (Groups) { }; Groups.rejectMembership = function (groupName, uid, callback) { - // Note: For simplicity, this method intentially doesn't check the caller uid for ownership! async.parallel([ async.apply(db.setRemove, 'group:' + groupName + ':pending', uid), async.apply(db.setRemove, 'group:' + groupName + ':invited', uid) diff --git a/src/socket.io/groups.js b/src/socket.io/groups.js index ba274f32fb..47453b6e0a 100644 --- a/src/socket.io/groups.js +++ b/src/socket.io/groups.js @@ -141,22 +141,25 @@ SocketGroups.issueMassInvite = isOwner(function (socket, data, callback) { if (!data || !data.usernames || !data.groupName) { return callback(new Error('[[error:invalid-data]]')); } - var usernames = data.usernames.split(','); + var usernames = String(data.usernames).split(','); usernames = usernames.map(function (username) { return username && username.trim(); }); - user.getUidsByUsernames(usernames, function (err, uids) { - if (err) { - return callback(err); - } - uids = uids.filter(function (uid) { - return !!uid && parseInt(uid, 10); - }); - async.eachSeries(uids, function (uid, next) { - groups.invite(data.groupName, uid, next); - }, callback); - }); + async.waterfall([ + function (next) { + user.getUidsByUsernames(usernames, next); + }, + function (uids, next) { + uids = uids.filter(function (uid) { + return !!uid && parseInt(uid, 10); + }); + + async.eachSeries(uids, function (uid, next) { + groups.invite(data.groupName, uid, next); + }, next); + } + ], callback); }); SocketGroups.rescindInvite = isOwner(function (socket, data, callback) { @@ -181,13 +184,14 @@ SocketGroups.kick = isOwner(function (socket, data, callback) { return callback(new Error('[[error:cant-kick-self]]')); } - groups.ownership.isOwner(data.uid, data.groupName, function (err, isOwner) { - if (err) { - return callback(err); + async.waterfall([ + function (next) { + groups.ownership.isOwner(data.uid, data.groupName, next); + }, + function (isOwner, next) { + groups.kick(data.uid, data.groupName, isOwner, next); } - groups.kick(data.uid, data.groupName, isOwner, callback); - }); - + ], callback); }); SocketGroups.create = function (socket, data, callback) { @@ -199,32 +203,19 @@ SocketGroups.create = function (socket, data, callback) { return callback(new Error('[[error:invalid-group-name]]')); } - data.ownerUid = socket.uid; groups.create(data, callback); }; -SocketGroups.delete = function (socket, data, callback) { +SocketGroups.delete = isOwner(function (socket, data, callback) { if (data.groupName === 'administrators' || data.groupName === 'registered-users' || data.groupName === 'Global Moderators') { return callback(new Error('[[error:not-allowed]]')); } - async.parallel({ - isOwner: async.apply(groups.ownership.isOwner, socket.uid, data.groupName), - isAdmin: async.apply(user.isAdministrator, socket.uid) - }, function (err, checks) { - if (err) { - return callback(err); - } - if (!checks.isOwner && !checks.isAdmin) { - return callback(new Error('[[error:no-privileges]]')); - } - - groups.destroy(data.groupName, callback); - }); -}; + groups.destroy(data.groupName, callback); +}); SocketGroups.search = function (socket, data, callback) { data.options = data.options || {}; @@ -242,7 +233,7 @@ SocketGroups.search = function (socket, data, callback) { SocketGroups.loadMore = function (socket, data, callback) { if (!data.sort || !utils.isNumber(data.after) || parseInt(data.after, 10) < 0) { - return callback(); + return callback(new Error('[[error:invalid-data]]')); } var groupsPerPage = 9; @@ -261,13 +252,17 @@ SocketGroups.loadMoreMembers = function (socket, data, callback) { return callback(new Error('[[error:invalid-data]]')); } data.after = parseInt(data.after, 10); - user.getUsersFromSet('group:' + data.groupName + ':members', socket.uid, data.after, data.after + 9, function (err, users) { - if (err) { - return callback(err); + async.waterfall([ + function (next) { + user.getUsersFromSet('group:' + data.groupName + ':members', socket.uid, data.after, data.after + 9, next); + }, + function (users, next) { + next(null, { + users: users, + nextStart: data.after + 10 + }); } - - callback(null, {users: users, nextStart: data.after + 10}); - }); + ], callback); }; SocketGroups.cover = {}; diff --git a/test/groups.js b/test/groups.js index a6dda5b887..6e8bf6e0b7 100644 --- a/test/groups.js +++ b/test/groups.js @@ -433,7 +433,6 @@ describe('Groups', function () { var socketGroups = require('../src/socket.io/groups'); var meta = require('../src/meta'); - it('should error if data is null', function (done) { socketGroups.before({uid: 0}, 'groups.join', null, function (err) { assert.equal(err.message, '[[error:invalid-data]]'); @@ -623,6 +622,99 @@ describe('Groups', function () { }); }); + it('should issue invite to user', function (done) { + User.create({username: 'invite1'}, function (err, uid) { + assert.ifError(err); + socketGroups.issueInvite({uid: adminUid}, {groupName: 'PrivateCanJoin', toUid: uid}, function (err) { + assert.ifError(err); + Groups.isInvited(uid, 'PrivateCanJoin', function (err, isInvited) { + assert.ifError(err); + assert(isInvited); + done(); + }); + }); + }); + }); + + it('should fail with invalid data', function (done) { + socketGroups.issueMassInvite({uid: adminUid}, {groupName: 'PrivateCanJoin', usernames: null}, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + done(); + }); + }); + + it('should issue mass invite to users', function (done) { + User.create({username: 'invite2'}, function (err, uid) { + assert.ifError(err); + socketGroups.issueMassInvite({uid: adminUid}, {groupName: 'PrivateCanJoin', usernames: 'invite1, invite2'}, function (err) { + assert.ifError(err); + Groups.isInvited(uid, 'PrivateCanJoin', function (err, isInvited) { + assert.ifError(err); + assert(isInvited); + done(); + }); + }); + }); + }); + + it('should rescind invite', function (done) { + User.create({username: 'invite3'}, function (err, uid) { + assert.ifError(err); + socketGroups.issueInvite({uid: adminUid}, {groupName: 'PrivateCanJoin', toUid: uid}, function (err) { + assert.ifError(err); + socketGroups.rescindInvite({uid: adminUid}, {groupName: 'PrivateCanJoin', toUid: uid}, function (err) { + assert.ifError(err); + Groups.isInvited(uid, 'PrivateCanJoin', function (err, isInvited) { + assert.ifError(err); + assert(!isInvited); + done(); + }); + }); + }); + }); + }); + + it('should error if user is not invited', function (done) { + socketGroups.acceptInvite({uid: adminUid}, {groupName: 'PrivateCanJoin'}, function (err) { + assert.equal(err.message, '[[error:not-invited]]'); + done(); + }); + }); + + it('should accept invite', function (done) { + User.create({username: 'invite4'}, function (err, uid) { + assert.ifError(err); + socketGroups.issueInvite({uid: adminUid}, {groupName: 'PrivateCanJoin', toUid: uid}, function (err) { + assert.ifError(err); + socketGroups.acceptInvite({uid: uid}, {groupName: 'PrivateCanJoin'}, function (err) { + assert.ifError(err); + Groups.isMember(uid, 'PrivateCanJoin', function (err, isMember) { + assert.ifError(err); + assert(isMember); + done(); + }); + }); + }); + }); + }); + + it('should reject invite', function (done) { + User.create({username: 'invite5'}, function (err, uid) { + assert.ifError(err); + socketGroups.issueInvite({uid: adminUid}, {groupName: 'PrivateCanJoin', toUid: uid}, function (err) { + assert.ifError(err); + socketGroups.rejectInvite({uid: uid}, {groupName: 'PrivateCanJoin'}, function (err) { + assert.ifError(err); + Groups.isInvited(uid, 'PrivateCanJoin', function (err, isInvited) { + assert.ifError(err); + assert(!isInvited); + done(); + }); + }); + }); + }); + }); + it('should grant ownership to user', function (done) { socketGroups.grant({uid: adminUid}, {groupName: 'PrivateCanJoin', toUid: testUid}, function (err) { assert.ifError(err); @@ -645,6 +737,13 @@ describe('Groups', function () { }); }); + it('should fail to kick user with invalid data', function (done) { + socketGroups.kick({uid: adminUid}, {groupName: 'PrivateCanJoin', uid: adminUid}, function (err) { + assert.equal(err.message, '[[error:cant-kick-self]]'); + done(); + }); + }); + it('should kick user from group', function (done) { socketGroups.kick({uid: adminUid}, {groupName: 'PrivateCanJoin', uid: testUid}, function (err) { assert.ifError(err); @@ -656,8 +755,130 @@ describe('Groups', function () { }); }); + it('should fail to create group with invalid data', function (done) { + socketGroups.create({uid: 0}, {}, function (err) { + assert.equal(err.message, '[[error:no-privileges]]'); + done(); + }); + }); + it('should fail to create group if group creation is disabled', function (done) { + var oldValue = meta.config.allowGroupCreation; + meta.config.allowGroupCreation = 0; + socketGroups.create({uid: 1}, {}, function (err) { + assert.equal(err.message, '[[error:group-creation-disabled]]'); + meta.config.allowGroupCreation = oldValue; + done(); + }); + }); + + it('should fail to create group if name is privilege group', function (done) { + var oldValue = meta.config.allowGroupCreation; + meta.config.allowGroupCreation = 1; + socketGroups.create({uid: 1}, {name: 'cid:1:privileges:groups:find'}, function (err) { + assert.equal(err.message, '[[error:invalid-group-name]]'); + meta.config.allowGroupCreation = oldValue; + done(); + }); + }); + + + it('should create/update group', function (done) { + var oldValue = meta.config.allowGroupCreation; + meta.config.allowGroupCreation = 1; + socketGroups.create({uid: adminUid}, {name: 'createupdategroup'}, function (err, groupData) { + meta.config.allowGroupCreation = oldValue; + assert.ifError(err); + assert(groupData); + var data = { + groupName: 'createupdategroup', + values: { + name: 'renamedupdategroup', + description: 'cat group', + userTitle: 'cats', + userTitleEnabled: 1, + disableJoinRequests: 1, + hidden: 1, + private: 0 + } + }; + socketGroups.update({uid: adminUid}, data, function (err) { + assert.ifError(err); + Groups.get('renamedupdategroup', {}, function (err, groupData) { + assert.ifError(err); + assert.equal(groupData.name, 'renamedupdategroup'); + assert.equal(groupData.userTitle, 'cats'); + assert.equal(groupData.description, 'cat group'); + assert.equal(groupData.hidden, true); + assert.equal(groupData.disableJoinRequests, true); + assert.equal(groupData.private, false); + done(); + }); + }); + }); + }); + + it('should delete group', function (done) { + socketGroups.delete({uid: adminUid}, {groupName: 'renamedupdategroup'}, function (err) { + assert.ifError(err); + Groups.exists('renamedupdategroup', function (err, exists) { + assert.ifError(err); + assert(!exists); + done(); + }); + }); + }); + it('should fail to delete group if name is special', function (done) { + socketGroups.delete({uid: adminUid}, {groupName: 'administrators'}, function (err) { + assert.equal(err.message, '[[error:not-allowed]]'); + done(); + }); + }); + + it('should fail to delete group if name is special', function (done) { + socketGroups.delete({uid: adminUid}, {groupName: 'registered-users'}, function (err) { + assert.equal(err.message, '[[error:not-allowed]]'); + done(); + }); + }); + + it('should fail to delete group if name is special', function (done) { + socketGroups.delete({uid: adminUid}, {groupName: 'Global Moderators'}, function (err) { + assert.equal(err.message, '[[error:not-allowed]]'); + done(); + }); + }); + + it('should fail to load more groups with invalid data', function (done) { + socketGroups.loadMore({uid: adminUid}, {}, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + done(); + }); + }); + + it('should load more groups', function (done) { + socketGroups.loadMore({uid: adminUid}, {after: 0, sort: 'count'}, function (err, data) { + assert.ifError(err); + assert(Array.isArray(data.groups)); + done(); + }); + }); + + it('should fail to load more members with invalid data', function (done) { + socketGroups.loadMoreMembers({uid: adminUid}, {}, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + done(); + }); + }); + + it('should load more members', function (done) { + socketGroups.loadMoreMembers({uid: adminUid}, {after: 0, groupName: 'PrivateCanJoin'}, function (err, data) { + assert.ifError(err); + assert(Array.isArray(data.users)); + done(); + }); + }); }); describe('admin socket methods', function () { @@ -895,6 +1116,13 @@ describe('Groups', function () { }); }); + it('should fail to remove cover if not logged in', function (done) { + socketGroups.cover.remove({uid: 0}, {groupName: 'Test'}, function (err) { + assert.equal(err.message, '[[error:no-privileges]]'); + done(); + }); + }); + it('should fail to remove cover if not owner', function (done) { socketGroups.cover.remove({uid: regularUid}, {groupName: 'Test'}, function (err) { assert.equal(err.message, '[[error:no-privileges]]');