diff --git a/test/api/party.coffee b/test/api/party.coffee index 152ac98ab9..1ba86ac0d6 100644 --- a/test/api/party.coffee +++ b/test/api/party.coffee @@ -129,7 +129,7 @@ describe "Party", -> group = undefined participating = [] notParticipating = [] - before (done) -> + beforeEach (done) -> # Tavern boss, side-by-side Group.update( _id: "habitrpg" @@ -310,6 +310,20 @@ describe "Party", -> expect(_.size(res.body.quest.members)).to.equal 3 done() + it "allows quest participants to leave quest", (done) -> + leavingMember = party[1] + expect(group.quest.members[leavingMember._id]).to.eql(true) + + request.post(baseURL + "/groups/" + group._id + "/questLeave") + .set("X-API-User", leavingMember._id) + .set("X-API-Key", leavingMember.apiToken) + .end (err, res) -> + expectCode res, 201 + request.get(baseURL + '/groups/party') + .end (err, res) -> + expect(res.body.quest.members[leavingMember._id]).to.not.be.ok + done() + xit "Hurts the boss", (done) -> request.post(baseURL + "/user/batch-update").end (res) -> user = res.body diff --git a/test/server_side/controllers/groups.test.js b/test/server_side/controllers/groups.test.js new file mode 100644 index 0000000000..4d735e7c71 --- /dev/null +++ b/test/server_side/controllers/groups.test.js @@ -0,0 +1,133 @@ +var sinon = require('sinon'); +var chai = require("chai") +chai.use(require("sinon-chai")) +var expect = chai.expect + +var groupsController = require('../../../website/src/controllers/groups'); + +describe('Groups Controller', function() { + + describe('#questLeave', function() { + var res, req, group, user, saveSpy; + + beforeEach(function() { + sinon.stub(process, 'nextTick').yields(); + + group = { + _id: 'group-id', + type: 'party', + quest: { + leader : 'another-user', + active: true, + members: { + 'user-id': true, + 'another-user': true + }, + key : 'vice1', + progress : { + hp : 364, + collect : {} + } + }, + save: sinon.stub().yields(), + markModified: sinon.spy() + }; + + user = { + _id: 'user-id', + party : { + quest : { + key : 'vice1', + progress : { + up : 50, + down : 0, + collect : {} + }, + completed : null, + RSVPNeeded : false + } + } + }; + + res = { + locals: { + group: group, + user: user + }, + json: sinon.stub(), + send: sinon.stub() + }; + + req = { }; + }); + + afterEach(function () { + process.nextTick.restore(); + }); + + context('error conditions', function() { + it('errors if quest is not active', function() { + group.quest.active = false; + + groupsController.questLeave(req, res); + + expect(res.json).to.be.calledOnce; + expect(res.json).to.be.calledWith( + 404, + { err: 'No active quest to leave' } + ); + }); + + it('errors if user is not part of quest', function() { + delete group.quest.members[user._id]; + + groupsController.questLeave(req, res); + + expect(res.json).to.be.calledOnce; + expect(res.json).to.be.calledWith( + 403, + { err: 'You are not part of the quest' } + ); + }); + + it('does not allow quest leader to leave quest', function() { + group.quest.leader = 'user-id'; + + groupsController.questLeave(req, res); + + expect(res.json).to.be.calledOnce; + expect(res.json).to.be.calledWith( + 403, + { err: 'Quest leader cannot leave quest' } + ); + }); + + it('sends 500 if group cannot save', function() { + group.save = sinon.stub().yields('save error'); + var nextSpy = sinon.spy(); + + groupsController.questLeave(req, res, nextSpy); + + expect(nextSpy).to.be.calledOnce; + expect(nextSpy).to.be.calledWith('save error'); + }); + }); + + context('success', function() { + it('removes user from quest', function() { + expect(group.quest.members[user._id]).to.exist; + + groupsController.questLeave(req, res); + + expect(group.quest.members[user._id]).to.not.exist; + }); + + it('sends back 201 on success', function() { + groupsController.questLeave(req, res); + + expect(res.send).to.be.calledOnce; + expect(res.send).to.be.calledWith(201); + }); + }); + }); +}); diff --git a/website/src/controllers/groups.js b/website/src/controllers/groups.js index 8443a905b8..4a2a1e588f 100644 --- a/website/src/controllers/groups.js +++ b/website/src/controllers/groups.js @@ -1072,3 +1072,29 @@ api.questAbort = function(req, res, next){ group = null; }) } + +api.questLeave = function(req, res, next) { + // Non-member leave quest while still in progress + var group = res.locals.group; + var user = res.locals.user; + + if (!(group.quest && group.quest.active)) { + return res.json(404, { err: 'No active quest to leave' }); + } + + if (!(group.quest.members && group.quest.members[user._id])) { + return res.json(403, { err: 'You are not part of the quest' }); + } + + if (group.quest.leader === user._id) { + return res.json(403, { err: 'Quest leader cannot leave quest' }); + } + + delete group.quest.members[user._id]; + group.markModified('quest.members'); + + group.save(function(err, result) { + if (err) return next(err); + res.send(201); + }); +} diff --git a/website/src/routes/apiv2.coffee b/website/src/routes/apiv2.coffee index 082db2efa6..22babced8f 100644 --- a/website/src/routes/apiv2.coffee +++ b/website/src/routes/apiv2.coffee @@ -527,6 +527,14 @@ module.exports = (swagger, v2) -> middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup] action: groups.questAbort + "/groups/{gid}/questLeave": + spec: + method: 'POST' + description: 'Leave an active quest (Quest leaders cannot leave active quests. They must abort the quest to leave)' + parameters: [path('gid','Group to leave quest in','string')] + middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup] + action: groups.questLeave + #TODO PUT /groups/:gid/chat/:messageId "/groups/{gid}/chat:GET":