diff --git a/src/databaseerror.js b/src/databaseerror.js index 72475766a..2dfa89f3f 100644 --- a/src/databaseerror.js +++ b/src/databaseerror.js @@ -30,3 +30,4 @@ DatabaseError.INTERNAL_ERROR = 'Internal error'; DatabaseError.ALREADY_EXISTS = 'Entry already exist'; DatabaseError.NOT_FOUND = 'Record not found'; DatabaseError.BAD_FIELD = 'Invalid field'; +DatabaseError.IN_USE = 'In Use'; diff --git a/src/groupdb.js b/src/groupdb.js index 04c1d56e4..1f246467d 100644 --- a/src/groupdb.js +++ b/src/groupdb.js @@ -6,12 +6,15 @@ exports = module.exports = { del: del, count: count, + getMembers: getMembers, + addMember: addMember, + removeMember: removeMember, + _clear: clear }; var assert = require('assert'), database = require('./database.js'), - debug = require('debug')('box:groupdb'), DatabaseError = require('./databaseerror'); var GROUPS_FIELDS = [ 'id', 'name' ].join(','); @@ -48,6 +51,7 @@ function del(id, callback) { assert.strictEqual(typeof callback, 'function'); database.query('DELETE FROM groups WHERE id = ?', [ id ], function (error, result) { + if (error && error.code === 'ER_ROW_IS_REFERENCED_2') return callback(new DatabaseError(DatabaseError.IN_USE)); if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error)); if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND)); @@ -66,10 +70,49 @@ function count(callback) { } function clear(callback) { - database.query('DELETE FROM groups', function (error) { + database.query('DELETE FROM groupMembers', function (error) { if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error)); - callback(error); + database.query('DELETE FROM groups', function (error) { + if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error)); + + callback(error); + }); }); } +function getMembers(groupId, callback) { + database.query('SELECT userId FROM groupMembers WHERE groupId=?', [ groupId ], function (error, result) { + if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error)); + // if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND)); // need to differentiate group with no members and invalid groupId + + callback(error, result.map(function (r) { return r.userId; })); + }); +} + +function addMember(groupId, userId, callback) { + assert.strictEqual(typeof groupId, 'string'); + assert.strictEqual(typeof userId, 'string'); + assert.strictEqual(typeof callback, 'function'); + + database.query('INSERT INTO groupMembers (groupId, userId) VALUES (?, ?)', [ groupId, userId ], function (error, result) { + if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error)); + if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND)); + if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error)); + + callback(null); + }); +} + +function removeMember(groupId, userId, callback) { + assert.strictEqual(typeof groupId, 'string'); + assert.strictEqual(typeof userId, 'string'); + assert.strictEqual(typeof callback, 'function'); + + database.query('DELETE FROM groupMembers WHERE groupId = ? AND userId = ?', [ groupId, userId ], function (error, result) { + if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error)); + if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND)); + + callback(null); + }); +} diff --git a/src/groups.js b/src/groups.js index 67550e19e..682398913 100644 --- a/src/groups.js +++ b/src/groups.js @@ -7,14 +7,17 @@ exports = module.exports = { create: create, remove: remove, - get: get + get: get, + + getMembers: getMembers, + addMember: addMember, + removeMember: removeMember }; var assert = require('assert'), DatabaseError = require('./databaseerror.js'), groupdb = require('./groupdb.js'), - util = require('util'), - _ = require('underscore'); + util = require('util'); // http://dustinsenos.com/articles/customErrorsInNode // http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi @@ -41,6 +44,7 @@ GroupError.INTERNAL_ERROR = 'Internal Error'; GroupError.ALREADY_EXISTS = 'Already Exists'; GroupError.NOT_FOUND = 'Not Found'; GroupError.BAD_NAME = 'Bad name'; +GroupError.NOT_EMPTY = 'Not Empty'; function validateGroupname(name) { assert.strictEqual(typeof name, 'string'); @@ -74,6 +78,7 @@ function remove(id, callback) { groupdb.del(id, function (error) { if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND)); + if (error && error.reason === DatabaseError.IN_USE) return callback(new GroupError(GroupError.NOT_EMPTY)); if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error)); callback(null); @@ -92,3 +97,40 @@ function get(id, callback) { }); } +function getMembers(groupId, callback) { + assert.strictEqual(typeof groupId, 'string'); + assert.strictEqual(typeof callback, 'function'); + + groupdb.getMembers(groupId, function (error, result) { + if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND)); + if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error)); + + return callback(null, result); + }); +} + +function addMember(groupId, userId, callback) { + assert.strictEqual(typeof groupId, 'string'); + assert.strictEqual(typeof userId, 'string'); + assert.strictEqual(typeof callback, 'function'); + + groupdb.addMember(groupId, userId, function (error) { + if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND)); + if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error)); + + return callback(null); + }); +} + +function removeMember(groupId, userId, callback) { + assert.strictEqual(typeof groupId, 'string'); + assert.strictEqual(typeof userId, 'string'); + assert.strictEqual(typeof callback, 'function'); + + groupdb.removeMember(groupId, userId, function (error) { + if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND)); + if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error)); + + return callback(null); + }); +} diff --git a/src/test/group-test.js b/src/test/group-test.js index dbdaced7f..a37d21b86 100644 --- a/src/test/group-test.js +++ b/src/test/group-test.js @@ -6,11 +6,13 @@ 'use strict'; -var database = require('../database.js'), +var async = require('async'), + database = require('../database.js'), expect = require('expect.js'), groups = require('../groups.js'), - groupdb = require('../groupdb.js'), - GroupError = groups.GroupError; + GroupError = groups.GroupError, + hat = require('hat'), + userdb = require('../userdb.js'); var GROUP_NAME = 'administrators', GROUP_ID = 'gid:' + GROUP_NAME; @@ -60,6 +62,13 @@ describe('Groups', function () { }); }); + it('cannot add existing group', function (done) { + groups.create(GROUP_NAME, function (error) { + expect(error.reason).to.be(GroupError.ALREADY_EXISTS); + done(); + }); + }); + it('cannot get invalid group', function (done) { groups.get('sometrandom', function (error) { expect(error.reason).to.be(GroupError.NOT_FOUND); @@ -89,3 +98,107 @@ describe('Groups', function () { }); }); }); + +describe('Group membership', function () { + var USER_0 = { + id: 'uuid213', + username: 'uuid213', + password: 'secret', + email: 'safe@me.com', + admin: false, + salt: 'morton', + createdAt: 'sometime back', + modifiedAt: 'now', + resetToken: hat(256), + displayName: '' + }; + + before(function (done) { + async.series([ + setup, + groups.create.bind(null, GROUP_NAME), + userdb.add.bind(null, USER_0.id, USER_0) + ], done); + }); + after(cleanup); + + it('cannot add non-existent user', function (done) { + groups.addMember(GROUP_ID, 'randomuser', function (error) { + expect(error.reason).to.be(GroupError.NOT_FOUND); + done(); + }); + }); + + it('cannot add non-existent group', function (done) { + groups.addMember('randomgroup', USER_0.id, function (error) { + expect(error.reason).to.be(GroupError.NOT_FOUND); + done(); + }); + }); + + it('can add member', function (done) { + groups.addMember(GROUP_ID, USER_0.id, function (error) { + expect(error).to.be(null); + done(); + }); + }); + + it('can get members', function (done) { + groups.getMembers(GROUP_ID, function (error, result) { + expect(error).to.be(null); + expect(result.length).to.be(1); + expect(result[0]).to.be(USER_0.id); + done(); + }); + }); + + it('cannot get members of non-existent group', function (done) { + groups.getMembers('randomgroup', function (error, result) { + expect(result.length).to.be(0); // currently, we cannot differentiate invalid groups and empty groups + done(); + }); + }); + + it('cannot remove non-existent user', function (done) { + groups.removeMember(GROUP_ID, 'randomuser', function (error) { + expect(error.reason).to.be(GroupError.NOT_FOUND); + done(); + }); + }); + + it('cannot remove non-existent group', function (done) { + groups.removeMember('randomgroup', USER_0.id, function (error) { + expect(error.reason).to.be(GroupError.NOT_FOUND); + done(); + }); + }); + + it('cannot remove group with member', function (done) { + groups.remove(GROUP_ID, function (error) { + expect(error.reason).to.be(GroupError.NOT_EMPTY); + done(); + }); + }); + + it('can remove member', function (done) { + groups.removeMember(GROUP_ID, USER_0.id, function (error) { + expect(error).to.be(null); + done(); + }); + }); + + it('has no members', function (done) { + groups.getMembers(GROUP_ID, function (error, result) { + expect(error).to.be(null); + expect(result.length).to.be(0); + done(); + }); + }); + + it('can remove group with no members', function (done) { + groups.remove(GROUP_ID, function (error) { + expect(error).to.be(null); + done(); + }); + }); +});