diff --git a/test/api/v3/integration/challenges/GET-challenges_challengeId.test.js b/test/api/v3/integration/challenges/GET-challenges_challengeId.test.js index 049506570e..6a9cd68429 100644 --- a/test/api/v3/integration/challenges/GET-challenges_challengeId.test.js +++ b/test/api/v3/integration/challenges/GET-challenges_challengeId.test.js @@ -63,45 +63,48 @@ describe('GET /challenges/:challengeId', () => { context('private guild', () => { let groupLeader; + let challengeLeader; let group; let challenge; let members; - let user; + let nonMember; + let otherMember; beforeEach(async () => { - user = await generateUser(); + nonMember = await generateUser(); let populatedGroup = await createAndPopulateGroup({ groupDetails: {type: 'guild', privacy: 'private'}, - members: 1, + members: 2, }); groupLeader = populatedGroup.groupLeader; group = populatedGroup.group; members = populatedGroup.members; - challenge = await generateChallenge(groupLeader, group); - await members[0].post(`/challenges/${challenge._id}/join`); - await groupLeader.post(`/challenges/${challenge._id}/join`); + challengeLeader = members[0]; + otherMember = members[1]; + + challenge = await generateChallenge(challengeLeader, group); }); - it('fails if user doesn\'t have access to the challenge', async () => { - await expect(user.get(`/challenges/${challenge._id}`)).to.eventually.be.rejected.and.eql({ + it('fails if user isn\'t in the guild and isn\'t challenge leader', async () => { + await expect(nonMember.get(`/challenges/${challenge._id}`)).to.eventually.be.rejected.and.eql({ code: 404, error: 'NotFound', message: t('challengeNotFound'), }); }); - it('should return challenge data', async () => { - let chal = await members[0].get(`/challenges/${challenge._id}`); + it('returns challenge data for any user in the guild', async () => { + let chal = await otherMember.get(`/challenges/${challenge._id}`); expect(chal.name).to.equal(challenge.name); expect(chal._id).to.equal(challenge._id); expect(chal.leader).to.eql({ - _id: groupLeader._id, - id: groupLeader._id, - profile: {name: groupLeader.profile.name}, + _id: challengeLeader._id, + id: challengeLeader._id, + profile: {name: challengeLeader.profile.name}, }); expect(chal.group).to.eql({ _id: group._id, @@ -114,53 +117,72 @@ describe('GET /challenges/:challengeId', () => { leader: groupLeader.id, }); }); + + it('returns challenge data if challenge leader isn\'t in the guild or challenge', async () => { + await challengeLeader.post(`/groups/${group._id}/leave`); + await challengeLeader.sync(); + expect(challengeLeader.guilds).to.be.empty; // check that leaving worked + + let chal = await challengeLeader.get(`/challenges/${challenge._id}`); + expect(chal.name).to.equal(challenge.name); + expect(chal._id).to.equal(challenge._id); + + expect(chal.leader).to.eql({ + _id: challengeLeader._id, + id: challengeLeader._id, + profile: {name: challengeLeader.profile.name}, + }); + }); }); context('party', () => { let groupLeader; + let challengeLeader; let group; let challenge; let members; - let user; + let nonMember; + let otherMember; beforeEach(async () => { - user = await generateUser(); + nonMember = await generateUser(); let populatedGroup = await createAndPopulateGroup({ - groupDetails: {type: 'party'}, - members: 1, + groupDetails: {type: 'party', privacy: 'private'}, + members: 2, }); groupLeader = populatedGroup.groupLeader; group = populatedGroup.group; members = populatedGroup.members; - challenge = await generateChallenge(groupLeader, group); - await members[0].post(`/challenges/${challenge._id}/join`); - await groupLeader.post(`/challenges/${challenge._id}/join`); + challengeLeader = members[0]; + otherMember = members[1]; + + challenge = await generateChallenge(challengeLeader, group); }); - it('fails if user doesn\'t have access to the challenge', async () => { - await expect(user.get(`/challenges/${challenge._id}`)).to.eventually.be.rejected.and.eql({ + it('fails if user isn\'t in the party and isn\'t challenge leader', async () => { + await expect(nonMember.get(`/challenges/${challenge._id}`)).to.eventually.be.rejected.and.eql({ code: 404, error: 'NotFound', message: t('challengeNotFound'), }); }); - it('should return challenge data', async () => { - let chal = await members[0].get(`/challenges/${challenge._id}`); + it('returns challenge data for any user in the party', async () => { + let chal = await otherMember.get(`/challenges/${challenge._id}`); expect(chal.name).to.equal(challenge.name); expect(chal._id).to.equal(challenge._id); expect(chal.leader).to.eql({ - _id: groupLeader._id, - id: groupLeader.id, - profile: {name: groupLeader.profile.name}, + _id: challengeLeader._id, + id: challengeLeader._id, + profile: {name: challengeLeader.profile.name}, }); expect(chal.group).to.eql({ _id: group._id, - id: group.id, + id: group._id, categories: [], name: group.name, summary: group.name, @@ -169,5 +191,21 @@ describe('GET /challenges/:challengeId', () => { leader: groupLeader.id, }); }); + + it('returns challenge data if challenge leader isn\'t in the party or challenge', async () => { + await challengeLeader.post('/groups/party/leave'); + await challengeLeader.sync(); + expect(challengeLeader.party._id).to.be.undefined; // check that leaving worked + + let chal = await challengeLeader.get(`/challenges/${challenge._id}`); + expect(chal.name).to.equal(challenge.name); + expect(chal._id).to.equal(challenge._id); + + expect(chal.leader).to.eql({ + _id: challengeLeader._id, + id: challengeLeader._id, + profile: {name: challengeLeader.profile.name}, + }); + }); }); }); diff --git a/test/api/v3/integration/challenges/GET-challenges_challengeId_members.test.js b/test/api/v3/integration/challenges/GET-challenges_challengeId_members.test.js index 71aa643869..2b0e006d38 100644 --- a/test/api/v3/integration/challenges/GET-challenges_challengeId_members.test.js +++ b/test/api/v3/integration/challenges/GET-challenges_challengeId_members.test.js @@ -1,6 +1,7 @@ import { generateUser, generateGroup, + createAndPopulateGroup, generateChallenge, translate as t, } from '../../../../helpers/api-integration/v3'; @@ -10,7 +11,7 @@ describe('GET /challenges/:challengeId/members', () => { let user; beforeEach(async () => { - user = await generateUser(); + user = await generateUser({ balance: 1 }); }); it('validates optional req.query.lastId to be an UUID', async () => { @@ -21,7 +22,7 @@ describe('GET /challenges/:challengeId/members', () => { }); }); - it('fails if challenge doesn\'t exists', async () => { + it('fails if challenge doesn\'t exist', async () => { await expect(user.get(`/challenges/${generateUUID()}/members`)).to.eventually.be.rejected.and.eql({ code: 404, error: 'NotFound', @@ -29,8 +30,8 @@ describe('GET /challenges/:challengeId/members', () => { }); }); - it('fails if user doesn\'t have access to the challenge', async () => { - let group = await generateGroup(user); + it('fails if user isn\'t in the private group and isn\'t challenge leader', async () => { + let group = await generateGroup(user, {type: 'party', privacy: 'private'}); let challenge = await generateChallenge(user, group); let anotherUser = await generateUser(); @@ -41,6 +42,27 @@ describe('GET /challenges/:challengeId/members', () => { }); }); + it('works if user isn\'t in the private group but is challenge leader', async () => { + let populatedGroup = await createAndPopulateGroup({ + groupDetails: {type: 'party', privacy: 'private'}, + members: 1, + }); + let groupLeader = populatedGroup.groupLeader; + let challengeLeader = populatedGroup.members[0]; + let challenge = await generateChallenge(challengeLeader, populatedGroup.group); + await groupLeader.post(`/challenges/${challenge._id}/join`); + await challengeLeader.post('/groups/party/leave'); + await challengeLeader.sync(); + expect(challengeLeader.party._id).to.be.undefined; // check that leaving worked + + let res = await challengeLeader.get(`/challenges/${challenge._id}/members`); + expect(res[0]).to.eql({ + _id: groupLeader._id, + id: groupLeader._id, + profile: {name: groupLeader.profile.name}, + }); + }); + it('works with challenges belonging to public guild', async () => { let leader = await generateUser({balance: 4}); let group = await generateGroup(leader, {type: 'guild', privacy: 'public', name: generateUUID()}); diff --git a/test/api/v3/integration/challenges/POST-challenges.test.js b/test/api/v3/integration/challenges/POST-challenges.test.js index 0176a729ff..b53c3750b1 100644 --- a/test/api/v3/integration/challenges/POST-challenges.test.js +++ b/test/api/v3/integration/challenges/POST-challenges.test.js @@ -94,16 +94,6 @@ describe('POST /challenges', () => { }); }); - it('returns an error when non-leader member creates a challenge in leaderOnly group', async () => { - await expect(groupMember.post('/challenges', { - group: group._id, - })).to.eventually.be.rejected.and.eql({ - code: 401, - error: 'NotAuthorized', - message: t('onlyGroupLeaderChal'), - }); - }); - it('allows non-leader member to create a challenge', async () => { let populatedGroup = await createAndPopulateGroup({ members: 1, diff --git a/test/api/v3/integration/challenges/POST-challenges_challengeId_join.test.js b/test/api/v3/integration/challenges/POST-challenges_challengeId_join.test.js index 6c403d05ad..1ca9a434c3 100644 --- a/test/api/v3/integration/challenges/POST-challenges_challengeId_join.test.js +++ b/test/api/v3/integration/challenges/POST-challenges_challengeId_join.test.js @@ -46,7 +46,7 @@ describe('POST /challenges/:challengeId/join', () => { await groupLeader.post(`/challenges/${challenge._id}/join`); }); - it('returns an error when user doesn\'t have permissions to access the challenge', async () => { + it('returns an error when user isn\'t in the private group and isn\'t challenge leader', async () => { let unauthorizedUser = await generateUser(); await expect(unauthorizedUser.post(`/challenges/${challenge._id}/join`)).to.eventually.be.rejected.and.eql({ @@ -56,6 +56,16 @@ describe('POST /challenges/:challengeId/join', () => { }); }); + it('succeeds when user isn\'t in the private group but is challenge leader', async () => { + await groupLeader.post(`/challenges/${challenge._id}/leave`); + await groupLeader.post(`/groups/${group._id}/leave`); + await groupLeader.sync(); + expect(groupLeader.guilds).to.be.empty; // check that leaving worked + + let res = await groupLeader.post(`/challenges/${challenge._id}/join`); + expect(res.name).to.equal(challenge.name); + }); + it('returns challenge data', async () => { let res = await authorizedUser.post(`/challenges/${challenge._id}/join`); diff --git a/website/server/controllers/api-v3/challenges.js b/website/server/controllers/api-v3/challenges.js index 7419949ea0..fe705d3a1a 100644 --- a/website/server/controllers/api-v3/challenges.js +++ b/website/server/controllers/api-v3/challenges.js @@ -251,7 +251,7 @@ api.joinChallenge = { if (challenge.isMember(user)) throw new NotAuthorized(res.t('userAlreadyInChallenge')); let group = await Group.getGroup({user, groupId: challenge.group, fields: basicGroupFields, optionalMembership: true}); - if (!group || !challenge.hasAccess(user, group)) throw new NotFound(res.t('challengeNotFound')); + if (!group || !challenge.canJoin(user, group)) throw new NotFound(res.t('challengeNotFound')); challenge.memberCount += 1; diff --git a/website/server/models/challenge.js b/website/server/models/challenge.js index a094814fe7..bb18888928 100644 --- a/website/server/models/challenge.js +++ b/website/server/models/challenge.js @@ -66,6 +66,11 @@ schema.statics.sanitizeUpdate = function sanitizeUpdate (updateObj) { return this.sanitize(updateObj, noUpdate); }; +// Returns true if user is the leader/owner of the challenge +schema.methods.isLeader = function isChallengeLeader (user) { + return this.leader === user._id; +}; + // Returns true if user is a member of the challenge schema.methods.isMember = function isChallengeMember (user) { return user.challenges.indexOf(this._id) !== -1; @@ -73,20 +78,21 @@ schema.methods.isMember = function isChallengeMember (user) { // Returns true if the user can modify (close, selectWinner, ...) the challenge schema.methods.canModify = function canModifyChallenge (user) { - return user.contributor.admin || this.leader === user._id; + return user.contributor.admin || this.isLeader(user); }; -// Returns true if user has access to the challenge (can join) -schema.methods.hasAccess = function hasAccessToChallenge (user, group) { +// Returns true if user can join the challenge +schema.methods.canJoin = function canJoinChallenge (user, group) { if (group.type === 'guild' && group.privacy === 'public') return true; + if (this.isLeader(user)) return true; // for when leader has left private group that contains the challenge return user.getGroups().indexOf(this.group) !== -1; }; // Returns true if user can view the challenge -// Different from hasAccess because you can see challenges of groups you've been removed from if you're partecipating in them +// Different from canJoin because you can see challenges of groups you've been removed from if you're participating in them schema.methods.canView = function canViewChallenge (user, group) { if (this.isMember(user)) return true; - return this.hasAccess(user, group); + return this.canJoin(user, group); }; // Sync challenge to user, including tasks and tags.