diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee index 9e04b5e990..9757a3e741 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee @@ -5,6 +5,7 @@ EditorRealTimeController = require "../Editor/EditorRealTimeController" LimitationsManager = require "../Subscription/LimitationsManager" UserGetter = require "../User/UserGetter" EmailHelper = require "../Helpers/EmailHelper" +logger = require 'logger-sharelatex' module.exports = CollaboratorsController = @@ -50,3 +51,12 @@ module.exports = CollaboratorsController = return callback(error) if error? EditorRealTimeController.emitToRoom(project_id, 'userRemovedFromProject', user_id) callback() + + getAllMembers: (req, res, next) -> + projectId = req.params.Project_id + logger.log {projectId}, "getting all active members for project" + CollaboratorsHandler.getAllMembers projectId, (err, members) -> + if err? + logger.err {projectId}, "error getting members for project" + return next(err) + res.json({members: members}) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee index 58ad246b60..822201a83e 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee @@ -8,6 +8,7 @@ async = require "async" PrivilegeLevels = require "../Authorization/PrivilegeLevels" Errors = require "../Errors/Errors" EmailHelper = require "../Helpers/EmailHelper" +ProjectEditorHandler = require "../Project/ProjectEditorHandler" module.exports = CollaboratorsHandler = @@ -144,3 +145,12 @@ module.exports = CollaboratorsHandler = if error? logger.error {err: error, project_id, user_id}, "error flushing to TPDS after adding collaborator" callback() + + getAllMembers: (projectId, callback=(err, members)->) -> + logger.log {projectId}, "fetching all members" + CollaboratorsHandler.getMembersWithPrivilegeLevels projectId, (error, rawMembers) -> + if error? + logger.err {projectId, error}, "error getting members for project" + return callback(error) + {owner, members} = ProjectEditorHandler.buildOwnerAndMembersViews(rawMembers) + callback(null, members) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee index b19011c5f9..7fc4722ef2 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee @@ -11,6 +11,13 @@ module.exports = webRouter.post '/project/:Project_id/users', AuthorizationMiddlewear.ensureUserCanAdminProject, CollaboratorsController.addUserToProject webRouter.delete '/project/:Project_id/users/:user_id', AuthorizationMiddlewear.ensureUserCanAdminProject, CollaboratorsController.removeUserFromProject + webRouter.get( + '/project/:Project_id/members', + AuthenticationController.requireLogin(), + AuthorizationMiddlewear.ensureUserCanAdminProject, + CollaboratorsController.getAllMembers + ) + # invites webRouter.post( '/project/:Project_id/invite', @@ -26,7 +33,7 @@ module.exports = ) webRouter.get( - '/project/:Project_id/invite', + '/project/:Project_id/invites', AuthenticationController.requireLogin(), AuthorizationMiddlewear.ensureUserCanAdminProject, CollaboratorsInviteController.getAllInvites diff --git a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee index 28d637eab6..a20cc99fc0 100644 --- a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee @@ -17,16 +17,11 @@ module.exports = ProjectEditorHandler = members: [] invites: invites || [] - owner = null - for member in members - if member.privilegeLevel == "owner" - owner = member.user - else - result.members.push @buildUserModelView member.user, member.privilegeLevel - if owner? - result.owner = @buildUserModelView owner, "owner" + {owner, ownerFeatures, members} = @buildOwnerAndMembersViews(members) + result.owner = owner + result.members = members - result.features = _.defaults(owner?.features or {}, { + result.features = _.defaults(ownerFeatures or {}, { collaborators: -1 # Infinite versioning: false dropbox:false @@ -38,6 +33,18 @@ module.exports = ProjectEditorHandler = return result + buildOwnerAndMembersViews: (members) -> + owner = null + ownerFeatures = null + filteredMembers = [] + for member in members + if member.privilegeLevel == "owner" + ownerFeatures = member.user.features + owner = @buildUserModelView member.user, "owner" + else + filteredMembers.push @buildUserModelView member.user, member.privilegeLevel + {owner: owner, ownerFeatures: ownerFeatures, members: filteredMembers} + buildUserModelView: (user, privileges) -> _id : user._id first_name : user.first_name diff --git a/services/web/public/coffee/ide/share/services/projectInvites.coffee b/services/web/public/coffee/ide/share/services/projectInvites.coffee index cad5122386..4c0d30add6 100644 --- a/services/web/public/coffee/ide/share/services/projectInvites.coffee +++ b/services/web/public/coffee/ide/share/services/projectInvites.coffee @@ -25,7 +25,7 @@ define [ }) getInvites: () -> - $http.get("/project/#{ide.project_id}/invite", { + $http.get("/project/#{ide.project_id}/invites", { json: true headers: "X-Csrf-Token": window.csrfToken diff --git a/services/web/public/coffee/ide/share/services/projectMembers.coffee b/services/web/public/coffee/ide/share/services/projectMembers.coffee index 3f86b45cd4..f1b2c8c3fe 100644 --- a/services/web/public/coffee/ide/share/services/projectMembers.coffee +++ b/services/web/public/coffee/ide/share/services/projectMembers.coffee @@ -17,5 +17,13 @@ define [ privileges: privileges _csrf: window.csrfToken }) + + getMembers: () -> + $http.get("/project/#{ide.project_id}/members", { + json: true + headers: + "X-Csrf-Token": window.csrfToken + }) + } ] diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee index 32de9ebe0a..6a76afc459 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee @@ -18,6 +18,7 @@ describe "CollaboratorsController", -> '../Subscription/LimitationsManager' : @LimitationsManager = {} '../Project/ProjectEditorHandler' : @ProjectEditorHandler = {} '../User/UserGetter': @UserGetter = {} + 'logger-sharelatex': @logger = {err: sinon.stub(), erro: sinon.stub(), log: sinon.stub()} @res = new MockResponse() @req = new MockRequest() @@ -73,10 +74,10 @@ describe "CollaboratorsController", -> beforeEach -> @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, false) @CollaboratorsController.addUserToProject @req, @res, @next - + it "should not add the user to the project", -> @CollaboratorsHandler.addEmailToProject.called.should.equal false - + it "should not emit a userAddedToProject event", -> @EditorRealTimeController.emitToRoom.called.should.equal false @@ -93,17 +94,17 @@ describe "CollaboratorsController", -> @res.status = sinon.stub().returns @res @res.send = sinon.stub() @CollaboratorsController.addUserToProject @req, @res, @next - + it "should not add the user to the project", -> @CollaboratorsHandler.addEmailToProject.called.should.equal false - + it "should not emit a userAddedToProject event", -> @EditorRealTimeController.emitToRoom.called.should.equal false it "should return a 400 response", -> @res.status.calledWith(400).should.equal true @res.send.calledWith("invalid email address").should.equal true - + describe "removeUserFromProject", -> beforeEach -> @req.params = @@ -113,17 +114,17 @@ describe "CollaboratorsController", -> @EditorRealTimeController.emitToRoom = sinon.stub() @CollaboratorsHandler.removeUserFromProject = sinon.stub().callsArg(2) @CollaboratorsController.removeUserFromProject @req, @res - + it "should from the user from the project", -> @CollaboratorsHandler.removeUserFromProject .calledWith(@project_id, @user_id) .should.equal true - + it "should emit a userRemovedFromProject event to the proejct", -> @EditorRealTimeController.emitToRoom .calledWith(@project_id, 'userRemovedFromProject', @user_id) .should.equal true - + it "should send the back a success response", -> @res.sendStatus.calledWith(204).should.equal true @@ -141,7 +142,7 @@ describe "CollaboratorsController", -> @CollaboratorsHandler.removeUserFromProject .calledWith(@project_id, @user_id) .should.equal true - + it "should emit a userRemovedFromProject event to the proejct", -> @EditorRealTimeController.emitToRoom .calledWith(@project_id, 'userRemovedFromProject', @user_id) @@ -150,3 +151,37 @@ describe "CollaboratorsController", -> it "should return a success code", -> @res.sendStatus.calledWith(204).should.equal true + describe 'getAllMembers', -> + beforeEach -> + @req.session = + user: _id: @user_id = "user-id-123" + @req.params = Project_id: @project_id + @res.json = sinon.stub() + @next = sinon.stub() + @members = [{a: 1}] + @CollaboratorsHandler.getAllMembers = sinon.stub().callsArgWith(1, null, @members) + @CollaboratorsController.getAllMembers(@req, @res, @next) + + it 'should not produce an error', -> + @next.callCount.should.equal 0 + + it 'should produce a json response', -> + @res.json.callCount.should.equal 1 + @res.json.calledWith({members: @members}).should.equal true + + it 'should call CollaboratorsHandler.getAllMembers', -> + @CollaboratorsHandler.getAllMembers.callCount.should.equal 1 + + describe 'when CollaboratorsHandler.getAllMembers produces an error', -> + beforeEach -> + @res.json = sinon.stub() + @next = sinon.stub() + @CollaboratorsHandler.getAllMembers = sinon.stub().callsArgWith(1, new Error('woops')) + @CollaboratorsController.getAllMembers(@req, @res, @next) + + it 'should produce an error', -> + @next.callCount.should.equal 1 + @next.firstCall.args[0].should.be.instanceof Error + + it 'should not produce a json response', -> + @res.json.callCount.should.equal 0 diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee index 37e39aaacb..efbf30487e 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsHandlerTests.coffee @@ -18,13 +18,14 @@ describe "CollaboratorsHandler", -> "../Project/ProjectEntityHandler": @ProjectEntityHandler = {} "./CollaboratorsEmailHandler": @CollaboratorsEmailHandler = {} "../Errors/Errors": Errors + "../Project/ProjectEditorHandler": @ProjectEditorHandler = {} @project_id = "mock-project-id" @user_id = "mock-user-id" @adding_user_id = "adding-user-id" @email = "joe@sharelatex.com" @callback = sinon.stub() - + describe "getMemberIdsWithPrivilegeLevels", -> describe "with project", -> beforeEach -> @@ -46,16 +47,16 @@ describe "CollaboratorsHandler", -> { id: "read-write-ref-2", privilegeLevel: "readAndWrite" } ]) .should.equal true - + describe "with a missing project", -> beforeEach -> @Project.findOne = sinon.stub().yields(null, null) - + it "should return a NotFoundError", (done) -> @CollaboratorHandler.getMemberIdsWithPrivilegeLevels @project_id, (error) -> error.should.be.instanceof Errors.NotFoundError done() - + describe "getMemberIds", -> beforeEach -> @CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() @@ -68,7 +69,7 @@ describe "CollaboratorsHandler", -> @callback .calledWith(null, ["member-id-1", "member-id-2"]) .should.equal true - + describe "getMembersWithPrivilegeLevels", -> beforeEach -> @CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() @@ -86,7 +87,7 @@ describe "CollaboratorsHandler", -> @UserGetter.getUser.withArgs("read-write-ref-2").yields(null, { _id: "read-write-ref-2" }) @UserGetter.getUser.withArgs("doesnt-exist").yields(null, null) @CollaboratorHandler.getMembersWithPrivilegeLevels @project_id, @callback - + it "should return an array of members with their privilege levels", -> @callback .calledWith(null, [ @@ -96,7 +97,7 @@ describe "CollaboratorsHandler", -> { user: { _id: "read-write-ref-2" }, privilegeLevel: "readAndWrite" } ]) .should.equal true - + describe "getMemberIdPrivilegeLevel", -> beforeEach -> @CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() @@ -116,7 +117,7 @@ describe "CollaboratorsHandler", -> @CollaboratorHandler.getMemberIdPrivilegeLevel "member-id-3", @project_id, (error, level) -> expect(level).to.equal false done() - + describe "isUserMemberOfProject", -> beforeEach -> @CollaboratorHandler.getMemberIdsWithPrivilegeLevels = sinon.stub() @@ -128,7 +129,7 @@ describe "CollaboratorsHandler", -> { id: @user_id, privilegeLevel: "readAndWrite" } ]) @CollaboratorHandler.isUserMemberOfProject @user_id, @project_id, @callback - + it "should return true and the privilegeLevel", -> @callback .calledWith(null, true, "readAndWrite") @@ -140,12 +141,12 @@ describe "CollaboratorsHandler", -> { id: "not-the-user", privilegeLevel: "readOnly" } ]) @CollaboratorHandler.isUserMemberOfProject @user_id, @project_id, @callback - + it "should return false", -> @callback .calledWith(null, false, null) .should.equal true - + describe "getProjectsUserIsCollaboratorOf", -> beforeEach -> @fields = "mock fields" @@ -153,7 +154,7 @@ describe "CollaboratorsHandler", -> @Project.find.withArgs({collaberator_refs:@user_id}, @fields).yields(null, ["mock-read-write-project-1", "mock-read-write-project-2"]) @Project.find.withArgs({readOnly_refs:@user_id}, @fields).yields(null, ["mock-read-only-project-1", "mock-read-only-project-2"]) @CollaboratorHandler.getProjectsUserIsCollaboratorOf @user_id, @fields, @callback - + it "should call the callback with the projects", -> @callback .calledWith(null, ["mock-read-write-project-1", "mock-read-write-project-2"], ["mock-read-only-project-1", "mock-read-only-project-2"]) @@ -172,7 +173,7 @@ describe "CollaboratorsHandler", -> "$pull":{collaberator_refs:@user_id, readOnly_refs:@user_id} }) .should.equal true - + describe "addUserToProject", -> beforeEach -> @Project.update = sinon.stub().callsArg(2) @@ -182,7 +183,7 @@ describe "CollaboratorsHandler", -> @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user = { _id: @user_id, email: @email }) @CollaboratorsEmailHandler.notifyUserOfProjectShare = sinon.stub() @ContactManager.addContact = sinon.stub() - + describe "as readOnly", -> beforeEach -> @CollaboratorHandler.addUserIdToProject @project_id, @adding_user_id, @user_id, "readOnly", @callback @@ -195,7 +196,7 @@ describe "CollaboratorsHandler", -> "$addToSet":{ readOnly_refs: @user_id } }) .should.equal true - + it "should flush the project to the TPDS", -> @ProjectEntityHandler.flushProjectToThirdPartyDataStore .calledWith(@project_id) @@ -205,12 +206,12 @@ describe "CollaboratorsHandler", -> @CollaboratorsEmailHandler.notifyUserOfProjectShare .calledWith(@project_id, @email) .should.equal true - + it "should add the user as a contact for the adding user", -> @ContactManager.addContact .calledWith(@adding_user_id, @user_id) .should.equal true - + describe "as readAndWrite", -> beforeEach -> @CollaboratorHandler.addUserIdToProject @project_id, @adding_user_id, @user_id, "readAndWrite", @callback @@ -223,19 +224,19 @@ describe "CollaboratorsHandler", -> "$addToSet":{ collaberator_refs: @user_id } }) .should.equal true - + it "should flush the project to the TPDS", -> @ProjectEntityHandler.flushProjectToThirdPartyDataStore .calledWith(@project_id) .should.equal true - + describe "with invalid privilegeLevel", -> beforeEach -> @CollaboratorHandler.addUserIdToProject @project_id, @adding_user_id, @user_id, "notValid", @callback it "should call the callback with an error", -> @callback.calledWith(new Error()).should.equal true - + describe "when user already exists as a collaborator", -> beforeEach -> @project.collaberator_refs = [@user_id] @@ -243,7 +244,7 @@ describe "CollaboratorsHandler", -> it "should not add the user again", -> @Project.update.called.should.equal false - + describe "addEmailToProject", -> beforeEach -> @UserCreator.getUserOrCreateHoldingAccount = sinon.stub().callsArgWith(1, null, @user = {_id: @user_id}) @@ -252,27 +253,27 @@ describe "CollaboratorsHandler", -> describe "with a valid email", -> beforeEach -> @CollaboratorHandler.addEmailToProject @project_id, @adding_user_id, (@email = "Joe@example.com"), (@privilegeLevel = "readAndWrite"), @callback - + it "should get the user with the lowercased email", -> @UserCreator.getUserOrCreateHoldingAccount .calledWith(@email.toLowerCase()) .should.equal true - + it "should add the user to the project by id", -> @CollaboratorHandler.addUserIdToProject .calledWith(@project_id, @adding_user_id, @user_id, @privilegeLevel) .should.equal true - + it "should return the callback with the user_id", -> @callback.calledWith(null, @user_id).should.equal true - + describe "with an invalid email", -> beforeEach -> @CollaboratorHandler.addEmailToProject @project_id, @adding_user_id, "not-and-email", (@privilegeLevel = "readAndWrite"), @callback - + it "should call the callback with an error", -> @callback.calledWith(new Error()).should.equal true - + it "should not add any users to the proejct", -> @CollaboratorHandler.addUserIdToProject.called.should.equal false @@ -286,9 +287,67 @@ describe "CollaboratorsHandler", -> ) @CollaboratorHandler.removeUserFromProject = sinon.stub().yields() @CollaboratorHandler.removeUserFromAllProjets @user_id, done - + it "should remove the user from each project", -> for project_id in ["read-and-write-0", "read-and-write-1", "read-only-0", "read-only-1"] @CollaboratorHandler.removeUserFromProject .calledWith(project_id, @user_id) - .should.equal true \ No newline at end of file + .should.equal true + + describe 'getAllMembers', -> + + beforeEach -> + @owning_user = {_id: 'owner-id', email: 'owner@example.com', features: {a: 1}} + @readwrite_user = {_id: 'readwrite-id', email: 'readwrite@example.com'} + @members = [ + {user: @owning_user, privilegeLevel: "owner"}, + {user: @readwrite_user, privilegeLevel: "readAndWrite"} + ] + @CollaboratorHandler.getMembersWithPrivilegeLevels = sinon.stub().callsArgWith(1, null, @members) + @ProjectEditorHandler.buildOwnerAndMembersViews = sinon.stub().returns(@views = { + owner: @owning_user, + ownerFeatures: @owning_user.features, + members: [ {_id: @readwrite_user._id, email: @readwrite_user.email} ] + }) + @callback = sinon.stub() + @CollaboratorHandler.getAllMembers @project_id, @callback + + it 'should not produce an error', -> + @callback.callCount.should.equal 1 + expect(@callback.firstCall.args[0]).to.equal null + + it 'should produce a list of members', -> + @callback.callCount.should.equal 1 + expect(@callback.firstCall.args[1]).to.deep.equal @views.members + + it 'should call getMembersWithPrivileges', -> + @CollaboratorHandler.getMembersWithPrivilegeLevels.callCount.should.equal 1 + @CollaboratorHandler.getMembersWithPrivilegeLevels.firstCall.args[0].should.equal @project_id + + it 'should call ProjectEditorHandler.buildOwnerAndMembersViews', -> + @ProjectEditorHandler.buildOwnerAndMembersViews.callCount.should.equal 1 + @ProjectEditorHandler.buildOwnerAndMembersViews.firstCall.args[0].should.equal @members + + describe 'when getMembersWithPrivileges produces an error', -> + + beforeEach -> + @CollaboratorHandler.getMembersWithPrivilegeLevels = sinon.stub().callsArgWith(1, new Error('woops')) + @ProjectEditorHandler.buildOwnerAndMembersViews = sinon.stub().returns(@views = { + owner: @owning_user, + ownerFeatures: @owning_user.features, + members: [ {_id: @readwrite_user._id, email: @readwrite_user.email} ] + }) + @callback = sinon.stub() + @CollaboratorHandler.getAllMembers @project_id, @callback + + it 'should produce an error', -> + @callback.callCount.should.equal 1 + expect(@callback.firstCall.args[0]).to.not.equal null + expect(@callback.firstCall.args[0]).to.be.instanceof Error + + it 'should call getMembersWithPrivileges', -> + @CollaboratorHandler.getMembersWithPrivilegeLevels.callCount.should.equal 1 + @CollaboratorHandler.getMembersWithPrivilegeLevels.firstCall.args[0].should.equal @project_id + + it 'should not call ProjectEditorHandler.buildOwnerAndMembersViews', -> + @ProjectEditorHandler.buildOwnerAndMembersViews.callCount.should.equal 0 diff --git a/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee index 671ae4a65b..dc68e5dda6 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee @@ -1,4 +1,5 @@ chai = require('chai') +expect = chai.expect should = chai.should() modulePath = "../../../../app/js/Features/Project/ProjectEditorHandler" @@ -105,7 +106,7 @@ describe "ProjectEditorHandler", -> it "should include the deletedDocs", -> should.exist @result.deletedDocs @result.deletedDocs.should.equal @project.deletedDocs - + it "should gather readOnly_refs and collaberators_refs into a list of members", -> findMember = (id) => for member in @result.members @@ -176,10 +177,46 @@ describe "ProjectEditorHandler", -> compileGroup:"priority" compileTimeout: 96 @result = @handler.buildProjectModelView @project, @members - + it "should copy the owner features to the project", -> @result.features.versioning.should.equal @owner.features.versioning @result.features.collaborators.should.equal @owner.features.collaborators @result.features.compileGroup.should.equal @owner.features.compileGroup @result.features.compileTimeout.should.equal @owner.features.compileTimeout + describe 'buildOwnerAndMembersViews', -> + beforeEach -> + @owner.features = + versioning: true + collaborators: 3 + compileGroup:"priority" + compileTimeout: 22 + @result = @handler.buildOwnerAndMembersViews @members + + it 'should produce an object with owner, ownerFeatures and members keys', -> + expect(@result).to.have.all.keys ['owner', 'ownerFeatures', 'members'] + + it 'should separate the owner from the members', -> + @result.members.length.should.equal(@members.length-1) + expect(@result.owner._id).to.equal @owner._id + expect(@result.owner.email).to.equal @owner.email + expect(@result.members.filter((m) => m._id == @owner._id).length).to.equal 0 + + it 'should extract the ownerFeatures from the owner object', -> + expect(@result.ownerFeatures).to.deep.equal @owner.features + + describe 'when there is no owner', -> + beforeEach -> + # remove the owner from members list + @membersWithoutOwner = @members.filter((m) => m.user._id != @owner._id) + @result = @handler.buildOwnerAndMembersViews @membersWithoutOwner + + it 'should produce an object with owner, ownerFeatures and members keys', -> + expect(@result).to.have.all.keys ['owner', 'ownerFeatures', 'members'] + + it 'should not separate out an owner', -> + @result.members.length.should.equal @membersWithoutOwner.length + expect(@result.owner).to.equal null + + it 'should not extract the ownerFeatures from the owner object', -> + expect(@result.ownerFeatures).to.equal null