diff --git a/config.json.example b/config.json.example index 62c3439cd2..11887285ed 100644 --- a/config.json.example +++ b/config.json.example @@ -87,5 +87,5 @@ "REDIS_HOST": "aaabbbcccdddeeefff", "REDIS_PORT": "1234", "REDIS_PASSWORD": "12345678", - "TRUSTED_DOMAINS": "localhost,https://habitica.com" + "TRUSTED_DOMAINS": "localhost,habitica.com" } diff --git a/habitica-images b/habitica-images index 0c48d4c818..109539e445 160000 --- a/habitica-images +++ b/habitica-images @@ -1 +1 @@ -Subproject commit 0c48d4c81852b7a4b0a9b7f536ec75ba399fa044 +Subproject commit 109539e445936786e183ac800c1d956e1999f2b8 diff --git a/migrations/archive/2023/20230731_naming_day.js b/migrations/archive/2023/20230731_naming_day.js new file mode 100644 index 0000000000..58ccfbb728 --- /dev/null +++ b/migrations/archive/2023/20230731_naming_day.js @@ -0,0 +1,155 @@ +/* eslint-disable no-console */ +const MIGRATION_NAME = '20230731_naming_day'; +import { v4 as uuid } from 'uuid'; + +import { model as User } from '../../../website/server/models/user'; + +const progressCount = 1000; +let count = 0; + +async function updateUser (user) { + count++; + + let set; + let push; + const inc = { + 'items.food.Cake_Base': 1, + 'items.food.Cake_CottonCandyBlue': 1, + 'items.food.Cake_CottonCandyPink': 1, + 'items.food.Cake_Desert': 1, + 'items.food.Cake_Golden': 1, + 'items.food.Cake_Red': 1, + 'items.food.Cake_Shade': 1, + 'items.food.Cake_Skeleton': 1, + 'items.food.Cake_White': 1, + 'items.food.Cake_Zombie': 1, + 'achievements.habiticaDays': 1, + }; + + if (user && user.items && user.items.gear && user.items.gear.owned && typeof user.items.gear.owned.back_special_namingDay2020 !== 'undefined') { + set = { migration: MIGRATION_NAME }; + push = { + notifications: { + type: 'ITEM_RECEIVED', + data: { + icon: 'notif_namingDay_cake', + title: 'Happy Naming Day!', + text: 'To celebrate the day we became Habitica, we’ve awarded you some cake!', + destination: '/inventory/items', + }, + seen: false, + }, + }; + } else if (user && user.items && user.items.gear && user.items.gear.owned && typeof user.items.gear.owned.body_special_namingDay2018 !== 'undefined') { + set = { migration: MIGRATION_NAME, 'items.gear.owned.back_special_namingDay2020': true }; + push = { + notifications: { + type: 'ITEM_RECEIVED', + data: { + icon: 'notif_namingDay_back', + title: 'Happy Naming Day!', + text: 'To celebrate the day we became Habitica, we’ve awarded you a Royal Purple Gryphon Tail and cake!', + destination: '/inventory/equipment', + }, + seen: false, + }, + }; + } else if (user && user.items && user.items.gear && user.items.gear.owned && typeof user.items.gear.owned.head_special_namingDay2017 !== 'undefined') { + set = { migration: MIGRATION_NAME, 'items.gear.owned.body_special_namingDay2018': true }; + push = { + notifications: { + type: 'ITEM_RECEIVED', + data: { + icon: 'notif_namingDay_body', + title: 'Happy Naming Day!', + text: 'To celebrate the day we became Habitica, we’ve awarded you a Royal Purple Gryphon Cloak and cake!', + destination: '/inventory/equipment', + }, + seen: false, + }, + }; + } else if (user && user.items && user.items.pets && typeof user.items.pets['Gryphon-RoyalPurple'] !== 'undefined') { + set = { migration: MIGRATION_NAME, 'items.gear.owned.head_special_namingDay2017': true }; + push = { + notifications: { + type: 'ITEM_RECEIVED', + data: { + icon: 'notif_namingDay_head', + title: 'Happy Naming Day!', + text: 'To celebrate the day we became Habitica, we’ve awarded you a Royal Purple Gryphon Helm and cake!', + destination: '/inventory/equipment', + }, + seen: false, + }, + }; + } else if (user && user.items && user.items.mounts && typeof user.items.mounts['Gryphon-RoyalPurple'] !== 'undefined') { + set = { migration: MIGRATION_NAME, 'items.pets.Gryphon-RoyalPurple': 5 }; + push = { + notifications: { + type: 'ITEM_RECEIVED', + data: { + icon: 'notif_namingDay_pet', + title: 'Happy Naming Day!', + text: 'To celebrate the day we became Habitica, we’ve awarded you a Royal Purple Gryphon Pet and cake!', + destination: '/inventory/stable', + }, + seen: false, + }, + }; + } else { + set = { migration: MIGRATION_NAME, 'items.mounts.Gryphon-RoyalPurple': true }; + push = { + notifications: { + type: 'ITEM_RECEIVED', + data: { + icon: 'notif_namingDay_mount', + title: 'Happy Naming Day!', + text: 'To celebrate the day we became Habitica, we’ve awarded you a Royal Purple Gryphon Mount and cake!', + destination: '/inventory/stable', + }, + seen: false, + }, + }; + } + + if (count % progressCount === 0) console.warn(`${count} ${user._id}`); + + if (push) { + return await user.updateOne({ $set: set, $inc: inc, $push: push }).exec(); + } else { + return await user.updateOne({ $set: set, $inc: inc }).exec(); + } +} + +export default async function processUsers () { + let query = { + migration: { $ne: MIGRATION_NAME }, + 'auth.timestamps.loggedin': { $gt: new Date('2023-07-01') }, + }; + + const fields = { + _id: 1, + items: 1, + }; + + while (true) { // eslint-disable-line no-constant-condition + const users = await User // eslint-disable-line no-await-in-loop + .find(query) + .limit(250) + .sort({_id: 1}) + .select(fields) + .exec(); + + if (users.length === 0) { + console.warn('All appropriate users found and modified.'); + console.warn(`\n${count} users processed\n`); + break; + } else { + query._id = { + $gt: users[users.length - 1]._id, + }; + } + + await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop + } +}; diff --git a/migrations/archive/2023/20230801_veteran_pet_ladder.js b/migrations/archive/2023/20230801_veteran_pet_ladder.js new file mode 100644 index 0000000000..23b87ec215 --- /dev/null +++ b/migrations/archive/2023/20230801_veteran_pet_ladder.js @@ -0,0 +1,144 @@ +/* eslint-disable no-console */ +const MIGRATION_NAME = '20230801_veteran_pet_ladder'; +import { model as User } from '../../../website/server/models/user'; + +const progressCount = 1000; +let count = 0; + +async function updateUser (user) { + count++; + + const set = {}; + let push = { notifications: { $each: [] }}; + + set.migration = MIGRATION_NAME; + if (user.items.pets['Fox-Veteran']) { + set['items.pets.Dragon-Veteran'] = 5; + push.notifications.$each.push({ + type: 'ITEM_RECEIVED', + data: { + icon: 'icon_pet_veteran_dragon', + title: 'You’ve received a Veteran Pet!', + text: 'To commemorate being here for a new era of Habitica, we’ve awarded you a Veteran Dragon.', + destination: '/inventory/stable', + }, + seen: false, + }); + } else if (user.items.pets['Bear-Veteran']) { + set['items.pets.Fox-Veteran'] = 5; + push.notifications.$each.push({ + type: 'ITEM_RECEIVED', + data: { + icon: 'icon_pet_veteran_fox', + title: 'You’ve received a Veteran Pet!', + text: 'To commemorate being here for a new era of Habitica, we’ve awarded you a Veteran Fox.', + destination: '/inventory/stable', + }, + seen: false, + }); + } else if (user.items.pets['Lion-Veteran']) { + set['items.pets.Bear-Veteran'] = 5; + push.notifications.$each.push({ + type: 'ITEM_RECEIVED', + data: { + icon: 'icon_pet_veteran_bear', + title: 'You’ve received a Veteran Pet!', + text: 'To commemorate being here for a new era of Habitica, we’ve awarded you a Veteran Bear.', + destination: '/inventory/stable', + }, + seen: false, + }); + } else if (user.items.pets['Tiger-Veteran']) { + set['items.pets.Lion-Veteran'] = 5; + push.notifications.$each.push({ + type: 'ITEM_RECEIVED', + data: { + icon: 'icon_pet_veteran_lion', + title: 'You’ve received a Veteran Pet!', + text: 'To commemorate being here for a new era of Habitica, we’ve awarded you a Veteran Lion.', + destination: '/inventory/stable', + }, + seen: false, + }); + } else if (user.items.pets['Wolf-Veteran']) { + set['items.pets.Tiger-Veteran'] = 5; + push.notifications.$each.push({ + type: 'ITEM_RECEIVED', + data: { + icon: 'icon_pet_veteran_tiger', + title: 'You’ve received a Veteran Pet!', + text: 'To commemorate being here for a new era of Habitica, we’ve awarded you a Veteran Tiger.', + destination: '/inventory/stable', + }, + seen: false, + }); + } else { + set['items.pets.Wolf-Veteran'] = 5; + push.notifications.$each.push({ + type: 'ITEM_RECEIVED', + data: { + icon: 'icon_pet_veteran_wolf', + title: 'You’ve received a Veteran Pet!', + text: 'To commemorate being here for a new era of Habitica, we’ve awarded you a Veteran Wolf.', + destination: '/inventory/stable', + }, + seen: false, + }); + } + + if (user.contributor.level > 0) { + set['items.gear.owned.armor_special_heroicTunic'] = true; + set['items.gear.owned.back_special_heroicAureole'] = true; + set['items.gear.owned.headAccessory_special_heroicCirclet'] = true; + push.notifications.$each.push({ + type: 'ITEM_RECEIVED', + data: { + icon: 'heroic_set_icon', + title: 'You’ve received the Heroic Set!', + text: 'To commemorate your hard work as a contributor, we’ve awarded you the Heroic Circlet, Heroic Aureole, and Heroic Tunic.', + destination: '/inventory/equipment', + }, + seen: false, + }); + } + + if (count % progressCount === 0) console.warn(`${count} ${user._id}`); + + return await User.update({_id: user._id}, {$set: set, $push: push}).exec(); +} + +export default async function processUsers () { + let query = { + migration: {$ne: MIGRATION_NAME}, + 'auth.timestamps.loggedin': { $gt: new Date('2023-07-01') }, + }; + + const fields = { + _id: 1, + items: 1, + migration: 1, + contributor: 1, + }; + + while (true) { // eslint-disable-line no-constant-condition + const users = await User // eslint-disable-line no-await-in-loop + .find(query) + .limit(250) + .sort({_id: 1}) + .select(fields) + .lean() + .exec(); + + if (users.length === 0) { + console.warn('All appropriate users found and modified.'); + console.warn(`\n${count} users processed\n`); + break; + } else { + query._id = { + $gt: users[users.length - 1], + }; + } + + await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop + } +}; diff --git a/migrations/archive/2023/20230808_guild_gems.js b/migrations/archive/2023/20230808_guild_gems.js new file mode 100644 index 0000000000..49b880ea5b --- /dev/null +++ b/migrations/archive/2023/20230808_guild_gems.js @@ -0,0 +1,72 @@ +/* eslint-disable no-console */ +import { model as User } from '../../../website/server/models/user'; +import { model as Group } from '../../../website/server/models/group'; + +const guildsPerRun = 500; +const progressCount = 1000; +const guildsQuery = { + type: 'guild', +}; + +let count = 0; +async function updateGroup (guild) { + count++; + if (count % progressCount === 0) { + console.warn(`${count} ${guild._id}`); + } + + if (guild.hasActiveGroupPlan()) { + return console.warn(`Guild ${guild._id} is active Group Plan`); + } + + const leader = await User + .findOne({ _id: guild.leader }) + .select({ _id: true }) + .exec(); + + if (!leader) { + return console.warn(`Leader not found for Guild ${guild._id}`); + } + + if (guild.balance > 0) { + await leader.updateBalance( + guild.balance, + 'create_guild', + '', + `Guild Bank refund for ${guild.name} (${guild._id})`, + ); + } + + return guild.updateOne({ $set: { balance: 0 } }).exec(); +} + +export default async function processGroups () { + const guildFields = { + _id: 1, + balance: 1, + leader: 1, + name: 1, + purchased: 1, + }; + + while (true) { // eslint-disable-line no-constant-condition + const foundGroups = await Group // eslint-disable-line no-await-in-loop + .find(guildsQuery) + .limit(guildsPerRun) + .sort({ _id: 1 }) + .select(guildFields) + .exec(); + + if (foundGroups.length === 0) { + console.warn('All appropriate Guilds found and modified.'); + console.warn(`\n${count} Guilds processed\n`); + break; + } else { + guildsQuery._id = { + $gt: foundGroups[foundGroups.length - 1], + }; + } + + await Promise.all(foundGroups.map(guild => updateGroup(guild))); // eslint-disable-line no-await-in-loop + } +}; diff --git a/package-lock.json b/package-lock.json index 69a0f1bbb2..7bf9e65556 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "habitica", - "version": "4.276.2", + "version": "5.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 7c95dd3491..86a2ba4895 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "habitica", "description": "A habit tracker app which treats your goals like a Role Playing Game.", - "version": "4.276.2", + "version": "5.0.0", "main": "./website/server/index.js", "dependencies": { "@babel/core": "^7.22.5", 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 7f70513da8..1fe1018df0 100644 --- a/test/api/v3/integration/challenges/GET-challenges_challengeId.test.js +++ b/test/api/v3/integration/challenges/GET-challenges_challengeId.test.js @@ -16,60 +16,7 @@ describe('GET /challenges/:challengeId', () => { }); }); - context('public guild', () => { - let groupLeader; - let group; - let challenge; - let user; - - beforeEach(async () => { - user = await generateUser(); - - const populatedGroup = await createAndPopulateGroup({ - groupDetails: { type: 'guild', privacy: 'public' }, - }); - - groupLeader = populatedGroup.groupLeader; - group = populatedGroup.group; - - challenge = await generateChallenge(groupLeader, group); - await groupLeader.post(`/challenges/${challenge._id}/join`); - }); - - it('should return challenge data', async () => { - await challenge.sync(); - const chal = await user.get(`/challenges/${challenge._id}`); - expect(chal.memberCount).to.equal(challenge.memberCount); - 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 }, - auth: { - local: { - username: groupLeader.auth.local.username, - }, - }, - flags: { - verifiedUsername: true, - }, - }); - expect(chal.group).to.eql({ - _id: group._id, - categories: [], - id: group.id, - name: group.name, - summary: group.name, - type: group.type, - privacy: group.privacy, - leader: groupLeader.id, - }); - }); - }); - - context('private guild', () => { + context('Group Plan', () => { let groupLeader; let challengeLeader; let group; @@ -84,14 +31,14 @@ describe('GET /challenges/:challengeId', () => { const populatedGroup = await createAndPopulateGroup({ groupDetails: { type: 'guild', privacy: 'private' }, members: 2, + upgradeToGroupPlan: true, }); groupLeader = populatedGroup.groupLeader; group = populatedGroup.group; members = populatedGroup.members; - challengeLeader = members[0]; // eslint-disable-line prefer-destructuring - otherMember = members[1]; // eslint-disable-line prefer-destructuring + [challengeLeader, otherMember] = members; challenge = await generateChallenge(challengeLeader, group); }); 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 01560cf037..fe729e38f3 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 @@ -71,42 +71,18 @@ describe('GET /challenges/:challengeId/members', () => { }); }); - it('works with challenges belonging to public guild', async () => { - const leader = await generateUser({ balance: 4 }); - const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() }); - const challenge = await generateChallenge(leader, group); - await leader.post(`/challenges/${challenge._id}/join`); - const res = await user.get(`/challenges/${challenge._id}/members`); - expect(res[0]).to.eql({ - _id: leader._id, - id: leader._id, - profile: { name: leader.profile.name }, - auth: { - local: { - username: leader.auth.local.username, - }, - }, - flags: { - verifiedUsername: true, - }, - }); - expect(res[0]).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile']); - expect(res[0].profile).to.have.all.keys(['name']); - }); - it('populates only some fields', async () => { - const anotherUser = await generateUser({ balance: 3 }); - const group = await generateGroup(anotherUser, { type: 'guild', privacy: 'public', name: generateUUID() }); - const challenge = await generateChallenge(anotherUser, group); - await anotherUser.post(`/challenges/${challenge._id}/join`); + const group = await generateGroup(user, { type: 'party', privacy: 'private', name: generateUUID() }); + const challenge = await generateChallenge(user, group); + await user.post(`/challenges/${challenge._id}/join`); const res = await user.get(`/challenges/${challenge._id}/members`); expect(res[0]).to.eql({ - _id: anotherUser._id, - id: anotherUser._id, - profile: { name: anotherUser.profile.name }, + _id: user._id, + id: user._id, + profile: { name: user.profile.name }, auth: { local: { - username: anotherUser.auth.local.username, + username: user.auth.local.username, }, }, flags: { diff --git a/test/api/v3/integration/challenges/GET-challenges_challengeId_members_memberId.test.js b/test/api/v3/integration/challenges/GET-challenges_challengeId_members_memberId.test.js index 03ffa8d8f4..5fe0068899 100644 --- a/test/api/v3/integration/challenges/GET-challenges_challengeId_members_memberId.test.js +++ b/test/api/v3/integration/challenges/GET-challenges_challengeId_members_memberId.test.js @@ -72,20 +72,6 @@ describe('GET /challenges/:challengeId/members/:memberId', () => { }); }); - it('works with challenges belonging to a public guild', async () => { - const groupLeader = await generateUser({ balance: 4 }); - const group = await generateGroup(groupLeader, { type: 'guild', privacy: 'public', name: generateUUID() }); - const challenge = await generateChallenge(groupLeader, group); - await groupLeader.post(`/challenges/${challenge._id}/join`); - const taskText = 'Test Text'; - await groupLeader.post(`/tasks/challenge/${challenge._id}`, [{ type: 'habit', text: taskText }]); - - const memberProgress = await user.get(`/challenges/${challenge._id}/members/${groupLeader._id}`); - expect(memberProgress).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile', 'tasks']); - expect(memberProgress.profile).to.have.all.keys(['name']); - expect(memberProgress.tasks.length).to.equal(1); - }); - it('returns the member tasks for the challenges', async () => { const group = await generateGroup(user, { type: 'party', name: generateUUID() }); const challenge = await generateChallenge(user, group); diff --git a/test/api/v3/integration/challenges/GET-challenges_group_groupid.test.js b/test/api/v3/integration/challenges/GET-challenges_group_groupid.test.js index fc26d4ce0d..8fc956a544 100644 --- a/test/api/v3/integration/challenges/GET-challenges_group_groupid.test.js +++ b/test/api/v3/integration/challenges/GET-challenges_group_groupid.test.js @@ -7,117 +7,7 @@ import { import { TAVERN_ID } from '../../../../../website/common/script/constants'; describe('GET challenges/groups/:groupId', () => { - context('Public Guild', () => { - let publicGuild; let user; let nonMember; let challenge; let - challenge2; - - before(async () => { - const { group, groupLeader } = await createAndPopulateGroup({ - groupDetails: { - name: 'TestGuild', - type: 'guild', - privacy: 'public', - }, - }); - - publicGuild = group; - user = groupLeader; - - nonMember = await generateUser(); - - challenge = await generateChallenge(user, group); - await user.post(`/challenges/${challenge._id}/join`); - challenge2 = await generateChallenge(user, group); - await user.post(`/challenges/${challenge2._id}/join`); - }); - - it('should return group challenges for non member with populated leader', async () => { - const challenges = await nonMember.get(`/challenges/groups/${publicGuild._id}`); - - const foundChallenge1 = _.find(challenges, { _id: challenge._id }); - expect(foundChallenge1).to.exist; - expect(foundChallenge1.leader).to.eql({ - _id: publicGuild.leader._id, - id: publicGuild.leader._id, - profile: { name: user.profile.name }, - auth: { - local: { - username: user.auth.local.username, - }, - }, - flags: { - verifiedUsername: true, - }, - }); - const foundChallenge2 = _.find(challenges, { _id: challenge2._id }); - expect(foundChallenge2).to.exist; - expect(foundChallenge2.leader).to.eql({ - _id: publicGuild.leader._id, - id: publicGuild.leader._id, - profile: { name: user.profile.name }, - auth: { - local: { - username: user.auth.local.username, - }, - }, - flags: { - verifiedUsername: true, - }, - }); - }); - - it('should return group challenges for member with populated leader', async () => { - const challenges = await user.get(`/challenges/groups/${publicGuild._id}`); - - const foundChallenge1 = _.find(challenges, { _id: challenge._id }); - expect(foundChallenge1).to.exist; - expect(foundChallenge1.leader).to.eql({ - _id: publicGuild.leader._id, - id: publicGuild.leader._id, - profile: { name: user.profile.name }, - auth: { - local: { - username: user.auth.local.username, - }, - }, - flags: { - verifiedUsername: true, - }, - }); - const foundChallenge2 = _.find(challenges, { _id: challenge2._id }); - expect(foundChallenge2).to.exist; - expect(foundChallenge2.leader).to.eql({ - _id: publicGuild.leader._id, - id: publicGuild.leader._id, - profile: { name: user.profile.name }, - auth: { - local: { - username: user.auth.local.username, - }, - }, - flags: { - verifiedUsername: true, - }, - }); - }); - - it('should return newest challenges first', async () => { - let challenges = await user.get(`/challenges/groups/${publicGuild._id}`); - - let foundChallengeIndex = _.findIndex(challenges, { _id: challenge2._id }); - expect(foundChallengeIndex).to.eql(0); - - const newChallenge = await generateChallenge(user, publicGuild); - await user.post(`/challenges/${newChallenge._id}/join`); - - challenges = await user.get(`/challenges/groups/${publicGuild._id}`); - - foundChallengeIndex = _.findIndex(challenges, { _id: newChallenge._id }); - expect(foundChallengeIndex).to.eql(0); - }); - }); - - context('Private Guild', () => { + context('Group Plan', () => { let privateGuild; let user; let nonMember; let challenge; let challenge2; @@ -128,6 +18,7 @@ describe('GET challenges/groups/:groupId', () => { type: 'guild', privacy: 'private', }, + upgradeToGroupPlan: true, }); privateGuild = group; @@ -186,68 +77,6 @@ describe('GET challenges/groups/:groupId', () => { }); }); - context('official challenge is present', () => { - let publicGuild; let user; let officialChallenge; let unofficialChallenges; - - before(async () => { - const { group, groupLeader } = await createAndPopulateGroup({ - groupDetails: { - name: 'TestGuild', - type: 'guild', - privacy: 'public', - }, - }); - - user = groupLeader; - publicGuild = group; - - await user.update({ - 'permissions.challengeAdmin': true, - }); - - officialChallenge = await generateChallenge(user, group, { - categories: [{ - name: 'habitica_official', - slug: 'habitica_official', - }], - }); - await user.post(`/challenges/${officialChallenge._id}/join`); - - // We add 10 extra challenges to test whether the official challenge - // (the oldest) makes it to the front page. - unofficialChallenges = []; - for (let i = 0; i < 10; i += 1) { - const challenge = await generateChallenge(user, group); // eslint-disable-line - await user.post(`/challenges/${challenge._id}/join`); // eslint-disable-line - unofficialChallenges.push(challenge); - } - }); - - it('should return official challenges first', async () => { - const challenges = await user.get(`/challenges/groups/${publicGuild._id}`); - - const foundChallengeIndex = _.findIndex(challenges, { _id: officialChallenge._id }); - expect(foundChallengeIndex).to.eql(0); - }); - - it('should return newest challenges first, after official ones', async () => { - let challenges = await user.get(`/challenges/groups/${publicGuild._id}`); - - unofficialChallenges.forEach((chal, index) => { - const foundChallengeIndex = _.findIndex(challenges, { _id: chal._id }); - expect(foundChallengeIndex).to.eql(10 - index); - }); - - const newChallenge = await generateChallenge(user, publicGuild); - await user.post(`/challenges/${newChallenge._id}/join`); - - challenges = await user.get(`/challenges/groups/${publicGuild._id}`); - - const foundChallengeIndex = _.findIndex(challenges, { _id: newChallenge._id }); - expect(foundChallengeIndex).to.eql(1); - }); - }); - context('Party', () => { let party; let user; let nonMember; let challenge; let challenge2; @@ -401,7 +230,7 @@ describe('GET challenges/groups/:groupId', () => { }); }); - it('should return tavern challenges using ID "habitrpg', async () => { + it('should return tavern challenges using ID "habitrpg"', async () => { const challenges = await user.get('/challenges/groups/habitrpg'); const foundChallenge1 = _.find(challenges, { _id: challenge._id }); @@ -435,5 +264,58 @@ describe('GET challenges/groups/:groupId', () => { }, }); }); + + context('official challenge is present', () => { + let officialChallenge; let unofficialChallenges; + + before(async () => { + await user.update({ + 'permissions.challengeAdmin': true, + balance: 3, + }); + + officialChallenge = await generateChallenge(user, tavern, { + categories: [{ + name: 'habitica_official', + slug: 'habitica_official', + }], + prize: 1, + }); + await user.post(`/challenges/${officialChallenge._id}/join`); + + // We add 10 extra challenges to test whether the official challenge + // (the oldest) makes it to the front page. + unofficialChallenges = []; + for (let i = 0; i < 10; i += 1) { + const challenge = await generateChallenge(user, tavern, { prize: 1 }); // eslint-disable-line + await user.post(`/challenges/${challenge._id}/join`); // eslint-disable-line + unofficialChallenges.push(challenge); + } + }); + + it('should return official challenges first', async () => { + const challenges = await user.get('/challenges/groups/habitrpg'); + + const foundChallengeIndex = _.findIndex(challenges, { _id: officialChallenge._id }); + expect(foundChallengeIndex).to.eql(0); + }); + + it('should return newest challenges first, after official ones', async () => { + let challenges = await user.get('/challenges/groups/habitrpg'); + + unofficialChallenges.forEach((chal, index) => { + const foundChallengeIndex = _.findIndex(challenges, { _id: chal._id }); + expect(foundChallengeIndex).to.eql(10 - index); + }); + + const newChallenge = await generateChallenge(user, tavern, { prize: 1 }); + await user.post(`/challenges/${newChallenge._id}/join`); + + challenges = await user.get('/challenges/groups/habitrpg'); + + const foundChallengeIndex = _.findIndex(challenges, { _id: newChallenge._id }); + expect(foundChallengeIndex).to.eql(1); + }); + }); }); }); diff --git a/test/api/v3/integration/challenges/GET-challenges_user.test.js b/test/api/v3/integration/challenges/GET-challenges_user.test.js index cc9c3aabd7..190024e9de 100644 --- a/test/api/v3/integration/challenges/GET-challenges_user.test.js +++ b/test/api/v3/integration/challenges/GET-challenges_user.test.js @@ -2,39 +2,44 @@ import { generateUser, generateChallenge, createAndPopulateGroup, + resetHabiticaDB, } from '../../../../helpers/api-integration/v3'; +import { TAVERN_ID } from '../../../../../website/common/script/constants'; describe('GET challenges/user', () => { context('no official challenges', () => { - let user; let member; let nonMember; let challenge; let challenge2; - let publicGuild; let userData; let groupData; + let user; let member; let nonMember; let challenge; let challenge2; let publicChallenge; + let groupPlan; let userData; let groupData; let tavern; let tavernData; before(async () => { + await resetHabiticaDB(); + const { group, groupLeader, members } = await createAndPopulateGroup({ groupDetails: { name: 'TestGuild', type: 'guild', - privacy: 'public', + privacy: 'private', }, members: 1, + upgradeToGroupPlan: true, }); - publicGuild = group; + groupPlan = group; groupData = { - _id: publicGuild._id, + _id: groupPlan._id, categories: [], - id: publicGuild._id, - type: publicGuild.type, - privacy: publicGuild.privacy, - name: publicGuild.name, - summary: publicGuild.name, - leader: publicGuild.leader._id, + id: groupPlan._id, + type: groupPlan.type, + privacy: groupPlan.privacy, + name: groupPlan.name, + summary: groupPlan.name, + leader: groupPlan.leader._id, }; user = groupLeader; userData = { - _id: publicGuild.leader._id, - id: publicGuild.leader._id, + _id: groupPlan.leader._id, + id: groupPlan.leader._id, profile: { name: user.profile.name }, auth: { local: { @@ -46,17 +51,31 @@ describe('GET challenges/user', () => { }, }; + tavern = await user.get(`/groups/${TAVERN_ID}`); + tavernData = { + _id: TAVERN_ID, + categories: [], + id: TAVERN_ID, + type: tavern.type, + privacy: tavern.privacy, + name: tavern.name, + summary: tavern.name, + leader: tavern.leader._id, + }; + member = members[0]; // eslint-disable-line prefer-destructuring nonMember = await generateUser(); challenge = await generateChallenge(user, group); challenge2 = await generateChallenge(user, group); + await user.update({ balance: 0.25 }); + publicChallenge = await generateChallenge(user, tavern, { prize: 1 }); - await nonMember.post(`/challenges/${challenge._id}/join`); + await member.post(`/challenges/${challenge._id}/join`); }); context('all challenges', () => { it('should return challenges user has joined', async () => { - const challenges = await nonMember.get('/challenges/user?page=0'); + const challenges = await member.get('/challenges/user?page=0'); const foundChallenge = _.find(challenges, { _id: challenge._id }); expect(foundChallenge).to.exist; @@ -64,11 +83,13 @@ describe('GET challenges/user', () => { expect(foundChallenge.group).to.eql(groupData); }); - it('should not return challenges a non-member has not joined', async () => { + it('should return public challenges', async () => { const challenges = await nonMember.get('/challenges/user?page=0'); - const foundChallenge2 = _.find(challenges, { _id: challenge2._id }); - expect(foundChallenge2).to.not.exist; + const foundPublicChallenge = _.find(challenges, { _id: publicChallenge._id }); + expect(foundPublicChallenge).to.exist; + expect(foundPublicChallenge.leader).to.eql(userData); + expect(foundPublicChallenge.group).to.eql(tavernData); }); it('should return challenges user has created', async () => { @@ -100,10 +121,10 @@ describe('GET challenges/user', () => { it('should return newest challenges first', async () => { let challenges = await user.get('/challenges/user?page=0'); - let foundChallengeIndex = _.findIndex(challenges, { _id: challenge2._id }); + let foundChallengeIndex = _.findIndex(challenges, { _id: publicChallenge._id }); expect(foundChallengeIndex).to.eql(0); - const newChallenge = await generateChallenge(user, publicGuild); + const newChallenge = await generateChallenge(user, groupPlan); await user.post(`/challenges/${newChallenge._id}/join`); challenges = await user.get('/challenges/user?page=0'); @@ -113,52 +134,23 @@ describe('GET challenges/user', () => { }); it('should not return challenges user doesn\'t have access to', async () => { - const { group, groupLeader } = await createAndPopulateGroup({ - groupDetails: { - name: 'TestPrivateGuild', - summary: 'summary for TestPrivateGuild', - type: 'guild', - privacy: 'private', - }, - }); - - const privateChallenge = await generateChallenge(groupLeader, group); - await groupLeader.post(`/challenges/${privateChallenge._id}/join`); - const challenges = await nonMember.get('/challenges/user?page=0'); - const foundChallenge = _.find(challenges, { _id: privateChallenge._id }); + const foundChallenge = _.find(challenges, { _id: challenge._id }); expect(foundChallenge).to.not.exist; }); it('should not return challenges user doesn\'t have access to, even with query parameters', async () => { - const { group, groupLeader } = await createAndPopulateGroup({ - groupDetails: { - name: 'TestPrivateGuild', - summary: 'summary for TestPrivateGuild', - type: 'guild', - privacy: 'private', - }, - }); - - const privateChallenge = await generateChallenge(groupLeader, group, { - categories: [{ - name: 'academics', - slug: 'academics', - }], - }); - await groupLeader.post(`/challenges/${privateChallenge._id}/join`); - const challenges = await nonMember.get('/challenges/user?page=0&categories=academics&owned=not_owned'); - const foundChallenge = _.find(challenges, { _id: privateChallenge._id }); + const foundChallenge = _.find(challenges, { _id: challenge._id }); expect(foundChallenge).to.not.exist; }); }); context('my challenges', () => { it('should return challenges user has joined', async () => { - const challenges = await nonMember.get(`/challenges/user?page=0&member=${true}`); + const challenges = await member.get(`/challenges/user?page=0&member=${true}`); const foundChallenge = _.find(challenges, { _id: challenge._id }); expect(foundChallenge).to.exist; @@ -177,6 +169,10 @@ describe('GET challenges/user', () => { expect(foundChallenge2).to.exist; expect(foundChallenge2.leader).to.eql(userData); expect(foundChallenge2.group).to.eql(groupData); + const foundPublicChallenge = _.find(challenges, { _id: publicChallenge._id }); + expect(foundPublicChallenge).to.exist; + expect(foundPublicChallenge.leader).to.eql(userData); + expect(foundPublicChallenge.group).to.eql(tavernData); }); it('should return challenges user has created if filter by owned', async () => { @@ -190,6 +186,10 @@ describe('GET challenges/user', () => { expect(foundChallenge2).to.exist; expect(foundChallenge2.leader).to.eql(userData); expect(foundChallenge2.group).to.eql(groupData); + const foundPublicChallenge = _.find(challenges, { _id: publicChallenge._id }); + expect(foundPublicChallenge).to.exist; + expect(foundPublicChallenge.leader).to.eql(userData); + expect(foundPublicChallenge.group).to.eql(tavernData); }); it('should not return challenges user has created if filter by not owned', async () => { @@ -199,36 +199,40 @@ describe('GET challenges/user', () => { expect(foundChallenge1).to.not.exist; const foundChallenge2 = _.find(challenges, { _id: challenge2._id }); expect(foundChallenge2).to.not.exist; + const foundPublicChallenge = _.find(challenges, { _id: publicChallenge._id }); + expect(foundPublicChallenge).to.not.exist; }); it('should not return challenges in user groups', async () => { const challenges = await member.get(`/challenges/user?page=0&member=${true}`); - const foundChallenge1 = _.find(challenges, { _id: challenge._id }); - expect(foundChallenge1).to.not.exist; - const foundChallenge2 = _.find(challenges, { _id: challenge2._id }); expect(foundChallenge2).to.not.exist; }); + + it('should not return public challenges', async () => { + const challenges = await member.get(`/challenges/user?page=0&member=${true}`); + + const foundPublicChallenge = _.find(challenges, { _id: publicChallenge._id }); + expect(foundPublicChallenge).to.not.exist; + }); }); }); context('official challenge is present', () => { let user; let officialChallenge; let unofficialChallenges; let - publicGuild; + group; before(async () => { - const { group, groupLeader } = await createAndPopulateGroup({ + ({ group, groupLeader: user } = await createAndPopulateGroup({ groupDetails: { name: 'TestGuild', summary: 'summary for TestGuild', type: 'guild', - privacy: 'public', + privacy: 'private', }, - }); - - user = groupLeader; - publicGuild = group; + upgradeToGroupPlan: true, + })); await user.update({ 'permissions.challengeAdmin': true, @@ -271,7 +275,7 @@ describe('GET challenges/user', () => { } }); - const newChallenge = await generateChallenge(user, publicGuild); + const newChallenge = await generateChallenge(user, group); await user.post(`/challenges/${newChallenge._id}/join`); challenges = await user.get('/challenges/user?page=0'); @@ -294,9 +298,10 @@ describe('GET challenges/user', () => { groupDetails: { name: 'TestGuild', type: 'guild', - privacy: 'public', + privacy: 'private', }, members: 1, + upgradeToGroupPlan: true, }); user = groupLeader; diff --git a/test/api/v3/integration/challenges/POST-challenges.test.js b/test/api/v3/integration/challenges/POST-challenges.test.js index 3c566b76d9..fa490725cf 100644 --- a/test/api/v3/integration/challenges/POST-challenges.test.js +++ b/test/api/v3/integration/challenges/POST-challenges.test.js @@ -42,26 +42,7 @@ describe('POST /challenges', () => { }); }); - it('returns error when creating a challenge in a public guild and you are not a member of it', async () => { - const user = await generateUser(); - const { group } = await createAndPopulateGroup({ - groupDetails: { - type: 'guild', - privacy: 'public', - }, - }); - - await expect(user.post('/challenges', { - group: group._id, - prize: 4, - })).to.eventually.be.rejected.and.eql({ - code: 401, - error: 'NotAuthorized', - message: t('mustBeGroupMember'), - }); - }); - - it('return error when creating a challenge with summary with greater than MAX_SUMMARY_SIZE_FOR_CHALLENGES characters', async () => { + it('returns error when creating a challenge with summary with greater than MAX_SUMMARY_SIZE_FOR_CHALLENGES characters', async () => { const user = await generateUser(); const summary = 'A'.repeat(MAX_SUMMARY_SIZE_FOR_CHALLENGES + 1); const group = createAndPopulateGroup({ @@ -77,7 +58,7 @@ describe('POST /challenges', () => { }); }); - context('Creating a challenge for a valid group', () => { + context('creating a Challenge for a Group Plan', () => { let groupLeader; let group; let groupMember; @@ -94,9 +75,11 @@ describe('POST /challenges', () => { challenges: true, }, }, + upgradeToGroupPlan: true, }); groupLeader = await populatedGroup.groupLeader.sync(); + await groupLeader.update({ permissions: {} }); group = populatedGroup.group; groupMember = populatedGroup.members[0]; // eslint-disable-line prefer-destructuring }); diff --git a/test/api/v3/integration/challenges/PUT-challenges_challengeId.test.js b/test/api/v3/integration/challenges/PUT-challenges_challengeId.test.js index 22e1f38b18..4e82da370a 100644 --- a/test/api/v3/integration/challenges/PUT-challenges_challengeId.test.js +++ b/test/api/v3/integration/challenges/PUT-challenges_challengeId.test.js @@ -18,6 +18,7 @@ describe('PUT /challenges/:challengeId', () => { privacy: 'private', }, members: 1, + upgradeToGroupPlan: true, }); privateGuild = group; diff --git a/test/api/v3/integration/chat/DELETE-chat_id.test.js b/test/api/v3/integration/chat/DELETE-chat_id.test.js index 7a9756e673..d0a323edfb 100644 --- a/test/api/v3/integration/chat/DELETE-chat_id.test.js +++ b/test/api/v3/integration/chat/DELETE-chat_id.test.js @@ -1,7 +1,6 @@ import { v4 as generateUUID } from 'uuid'; import { createAndPopulateGroup, - generateUser, translate as t, } from '../../../../helpers/api-integration/v3'; @@ -10,27 +9,30 @@ describe('DELETE /groups/:groupId/chat/:chatId', () => { admin; before(async () => { - const { group, groupLeader } = await createAndPopulateGroup({ + const { group, groupLeader, members } = await createAndPopulateGroup({ groupDetails: { type: 'guild', - privacy: 'public', + privacy: 'private', }, leaderDetails: { 'auth.timestamps.created': new Date('2022-01-01'), balance: 10, }, + members: 2, + upgradeToGroupPlan: true, }); groupWithChat = group; user = groupLeader; message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: 'Some message' }); message = message.message; - userThatDidNotCreateChat = await generateUser(); - admin = await generateUser({ 'permissions.moderator': true }); + userThatDidNotCreateChat = members[0]; // eslint-disable-line prefer-destructuring + admin = members[1]; // eslint-disable-line prefer-destructuring + await admin.update({ permissions: { moderator: true } }); }); context('Chat errors', () => { - it('returns an error is message does not exist', async () => { + it('returns an error if message does not exist', async () => { const fakeChatId = generateUUID(); await expect(user.del(`/groups/${groupWithChat._id}/chat/${fakeChatId}`)).to.eventually.be.rejected.and.eql({ code: 404, @@ -56,7 +58,7 @@ describe('DELETE /groups/:groupId/chat/:chatId', () => { nextMessage = nextMessage.message; }); - it('allows creator to delete a their message', async () => { + it('allows creator to delete their message', async () => { await user.del(`/groups/${groupWithChat._id}/chat/${nextMessage.id}`); const returnedMessages = await user.get(`/groups/${groupWithChat._id}/chat/`); diff --git a/test/api/v3/integration/chat/GET-chat.test.js b/test/api/v3/integration/chat/GET-chat.test.js index 910b094c40..34f4316dad 100644 --- a/test/api/v3/integration/chat/GET-chat.test.js +++ b/test/api/v3/integration/chat/GET-chat.test.js @@ -1,6 +1,6 @@ import { generateUser, - generateGroup, + createAndPopulateGroup, translate as t, } from '../../../../helpers/api-integration/v3'; @@ -11,48 +11,22 @@ describe('GET /groups/:groupId/chat', () => { user = await generateUser(); }); - context('public Guild', () => { - let group; - - before(async () => { - const leader = await generateUser({ balance: 2 }); - - group = await generateGroup(leader, { - name: 'test group', - type: 'guild', - privacy: 'public', - }, { - chat: [ - { text: 'Hello', flags: {}, id: 1 }, - { text: 'Welcome to the Guild', flags: {}, id: 2 }, - ], - }); - }); - - it('returns Guild chat', async () => { - const chat = await user.get(`/groups/${group._id}/chat`); - - expect(chat[0].id).to.eql(group.chat[0].id); - expect(chat[1].id).to.eql(group.chat[1].id); - }); - }); - context('private Guild', () => { let group; - before(async () => { - const leader = await generateUser({ balance: 2 }); - - group = await generateGroup(leader, { - name: 'test group', - type: 'guild', - privacy: 'private', - }, { + ({ group } = await createAndPopulateGroup({ + groupDetails: { + name: 'test group', + type: 'guild', + privacy: 'private', + }, + members: 1, + upgradeToGroupPlan: true, chat: [ 'Hello', 'Welcome to the Guild', ], - }); + })); }); it('returns error if user is not member of requested private group', async () => { diff --git a/test/api/v3/integration/chat/POST-chat.flag.test.js b/test/api/v3/integration/chat/POST-chat.flag.test.js index 909fbfa7a0..21dfabec92 100644 --- a/test/api/v3/integration/chat/POST-chat.flag.test.js +++ b/test/api/v3/integration/chat/POST-chat.flag.test.js @@ -1,32 +1,42 @@ -import { find } from 'lodash'; +import find from 'lodash/find'; import moment from 'moment'; -import nconf from 'nconf'; import { IncomingWebhook } from '@slack/webhook'; import { - generateUser, + createAndPopulateGroup, translate as t, } from '../../../../helpers/api-integration/v3'; -const BASE_URL = nconf.get('BASE_URL'); - describe('POST /chat/:chatId/flag', () => { let user; let admin; let anotherUser; let newUser; let - group; + group; let members; let userToDelete; const TEST_MESSAGE = 'Test Message'; const USER_AGE_FOR_FLAGGING = 3; beforeEach(async () => { - user = await generateUser({ balance: 1, 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate() }); - admin = await generateUser({ balance: 1, 'permissions.moderator': true }); - anotherUser = await generateUser({ 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate() }); - newUser = await generateUser({ 'auth.timestamps.created': moment().subtract(1, 'days').toDate() }); - sandbox.stub(IncomingWebhook.prototype, 'send').returns(Promise.resolve()); + ({ group, groupLeader: user, members } = await createAndPopulateGroup({ + groupDetails: { + name: 'Test Guild', + type: 'guild', + privacy: 'private', + }, + leaderDetails: { + 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate(), + }, + members: 4, + upgradeToGroupPlan: true, + })); - group = await user.post('/groups', { - name: 'Test Guild', - type: 'guild', - privacy: 'public', + [admin, anotherUser, newUser, userToDelete] = members; + await user.update({ permissions: {} }); + await admin.update({ permissions: { moderator: true } }); + await anotherUser.update({ 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate() }); + await newUser.update({ 'auth.timestamps.created': moment().subtract(1, 'days').toDate() }); + await userToDelete.update({ + 'auth.timestamps.created': moment().subtract(1, 'days').toDate(), + 'purchased.plan.dateTerminated': moment().subtract(1, 'minutes').toDate(), }); + + sandbox.stub(IncomingWebhook.prototype, 'send').returns(Promise.resolve()); }); afterEach(() => { @@ -69,8 +79,8 @@ describe('POST /chat/:chatId/flag', () => { fallback: 'Flag Message', color: 'danger', author_name: `@${anotherUser.auth.local.username} ${anotherUser.profile.name} (${anotherUser.auth.local.email}; ${anotherUser._id})\n${timestamp}`, - title: 'Flag in Test Guild', - title_link: `${BASE_URL}/groups/guild/${group._id}`, + title: 'Flag in Test Guild - (private guild)', + title_link: undefined, text: TEST_MESSAGE, footer: ``, mrkdwn_in: [ @@ -78,7 +88,7 @@ describe('POST /chat/:chatId/flag', () => { ], }], }); - /* eslint-ensable camelcase */ + /* eslint-enable camelcase */ }); it('Does not increment message flag count and sends different message to moderator Slack when user is new', async () => { @@ -104,8 +114,8 @@ describe('POST /chat/:chatId/flag', () => { fallback: 'Flag Message', color: 'danger', author_name: `@${newUser.auth.local.username} ${newUser.profile.name} (${newUser.auth.local.email}; ${newUser._id})\n${timestamp}`, - title: 'Flag in Test Guild', - title_link: `${BASE_URL}/groups/guild/${group._id}`, + title: 'Flag in Test Guild - (private guild)', + title_link: undefined, text: TEST_MESSAGE, footer: ` ${automatedComment}`, mrkdwn_in: [ @@ -113,15 +123,12 @@ describe('POST /chat/:chatId/flag', () => { ], }], }); - /* eslint-ensable camelcase */ + /* eslint-enable camelcase */ }); it('Flags a chat when the author\'s account was deleted', async () => { - const deletedUser = await generateUser({ - 'auth.timestamps.created': new Date('2022-01-01'), - }); - const { message } = await deletedUser.post(`/groups/${group._id}/chat`, { message: TEST_MESSAGE }); - await deletedUser.del('/user', { + const { message } = await userToDelete.post(`/groups/${group._id}/chat`, { message: TEST_MESSAGE }); + await userToDelete.del('/user', { password: 'password', }); diff --git a/test/api/v3/integration/chat/POST-chat.like.test.js b/test/api/v3/integration/chat/POST-chat.like.test.js index beab851bb0..242d2d95f6 100644 --- a/test/api/v3/integration/chat/POST-chat.like.test.js +++ b/test/api/v3/integration/chat/POST-chat.like.test.js @@ -6,27 +6,27 @@ import { describe('POST /chat/:chatId/like', () => { let user; - let groupWithChat; - const testMessage = 'Test Message'; let anotherUser; + let groupWithChat; + let members; + const testMessage = 'Test Message'; before(async () => { - const { group, groupLeader, members } = await createAndPopulateGroup({ + ({ group: groupWithChat, groupLeader: user, members } = await createAndPopulateGroup({ groupDetails: { name: 'Test Guild', type: 'guild', - privacy: 'public', + privacy: 'private', }, members: 1, leaderDetails: { 'auth.timestamps.created': new Date('2022-01-01'), balance: 10, }, - }); + upgradeToGroupPlan: true, + })); - user = groupLeader; - groupWithChat = group; - anotherUser = members[0]; // eslint-disable-line prefer-destructuring + [anotherUser] = members; await anotherUser.update({ 'auth.timestamps.created': new Date('2022-01-01') }); }); diff --git a/test/api/v3/integration/chat/POST-chat.test.js b/test/api/v3/integration/chat/POST-chat.test.js index d88ddd1d2b..10e8e80bc3 100644 --- a/test/api/v3/integration/chat/POST-chat.test.js +++ b/test/api/v3/integration/chat/POST-chat.test.js @@ -1,41 +1,33 @@ import { IncomingWebhook } from '@slack/webhook'; -import nconf from 'nconf'; import { v4 as generateUUID } from 'uuid'; import { createAndPopulateGroup, - generateUser, translate as t, sleep, server, } from '../../../../helpers/api-integration/v3'; import { - SPAM_MESSAGE_LIMIT, SPAM_MIN_EXEMPT_CONTRIB_LEVEL, - TAVERN_ID, } from '../../../../../website/server/models/group'; -import { CHAT_FLAG_FROM_SHADOW_MUTE, MAX_MESSAGE_LENGTH } from '../../../../../website/common/script/constants'; +import { MAX_MESSAGE_LENGTH } from '../../../../../website/common/script/constants'; import * as email from '../../../../../website/server/libs/email'; -const BASE_URL = nconf.get('BASE_URL'); - describe('POST /chat', () => { let user; let groupWithChat; let member; let additionalMember; const testMessage = 'Test Message'; const testBannedWordMessage = 'TESTPLACEHOLDERSWEARWORDHERE'; - const testBannedWordMessage1 = 'TESTPLACEHOLDERSWEARWORDHERE1'; const testSlurMessage = 'message with TESTPLACEHOLDERSLURWORDHERE'; - const testSlurMessage1 = 'TESTPLACEHOLDERSLURWORDHERE1'; - const bannedWordErrorMessage = t('bannedWordUsed', { swearWordsUsed: testBannedWordMessage }); before(async () => { const { group, groupLeader, members } = await createAndPopulateGroup({ groupDetails: { name: 'Test Guild', type: 'guild', - privacy: 'public', + privacy: 'private', }, members: 2, + upgradeToGroupPlan: true, }); user = groupLeader; await user.update({ @@ -43,8 +35,7 @@ describe('POST /chat', () => { 'auth.timestamps.created': new Date('2022-01-01'), }); // prevent tests accidentally throwing messageGroupChatSpam groupWithChat = group; - member = members[0]; // eslint-disable-line prefer-destructuring - additionalMember = members[1]; // eslint-disable-line prefer-destructuring + [member, additionalMember] = members; await member.update({ 'auth.timestamps.created': new Date('2022-01-01') }); await additionalMember.update({ 'auth.timestamps.created': new Date('2022-01-01') }); }); @@ -89,32 +80,12 @@ describe('POST /chat', () => { member.update({ 'flags.chatRevoked': false }); }); - it('returns an error when chat privileges are revoked when sending a message to a public guild', async () => { - const userWithChatRevoked = await member.update({ 'flags.chatRevoked': true }); - await expect(userWithChatRevoked.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage })).to.eventually.be.rejected.and.eql({ - code: 401, - error: 'NotAuthorized', - message: t('chatPrivilegesRevoked'), - }); - }); - it('does not error when chat privileges are revoked when sending a message to a private guild', async () => { - const { group, members } = await createAndPopulateGroup({ - groupDetails: { - name: 'Private Guild', - type: 'guild', - privacy: 'private', - }, - members: 1, - }); - - const privateGuildMemberWithChatsRevoked = members[0]; - await privateGuildMemberWithChatsRevoked.update({ + await member.update({ 'flags.chatRevoked': true, - 'auth.timestamps.created': new Date('2022-01-01'), }); - const message = await privateGuildMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage }); + const message = await member.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage }); expect(message.message.id).to.exist; }); @@ -152,54 +123,12 @@ describe('POST /chat', () => { member.update({ 'flags.chatShadowMuted': false }); }); - it('creates a chat with flagCount already set and notifies mods when sending a message to a public guild', async () => { - const userWithChatShadowMuted = await member.update({ 'flags.chatShadowMuted': true }); - const message = await userWithChatShadowMuted.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage }); - expect(message.message.id).to.exist; - expect(message.message.flagCount).to.eql(CHAT_FLAG_FROM_SHADOW_MUTE); - - // Email sent to mods - await sleep(0.5); - expect(email.sendTxn).to.be.calledOnce; - expect(email.sendTxn.args[0][1]).to.eql('shadow-muted-post-report-to-mods'); - - // Slack message to mods - expect(IncomingWebhook.prototype.send).to.be.calledOnce; - /* eslint-disable camelcase */ - expect(IncomingWebhook.prototype.send).to.be.calledWith({ - text: `@${member.auth.local.username} / ${member.profile.name} posted while shadow-muted`, - attachments: [{ - fallback: 'Shadow-Muted Message', - color: 'danger', - author_name: `@${member.auth.local.username} ${member.profile.name} (${member.auth.local.email}; ${member._id})`, - title: 'Shadow-Muted Post in Test Guild', - title_link: `${BASE_URL}/groups/guild/${groupWithChat.id}`, - text: testMessage, - mrkdwn_in: [ - 'text', - ], - }], - }); - /* eslint-enable camelcase */ - }); - it('creates a chat with zero flagCount when sending a message to a private guild', async () => { - const { group, members } = await createAndPopulateGroup({ - groupDetails: { - name: 'Private Guild', - type: 'guild', - privacy: 'private', - }, - members: 1, - }); - - const userWithChatShadowMuted = members[0]; - await userWithChatShadowMuted.update({ + await member.update({ 'flags.chatShadowMuted': true, - 'auth.timestamps.created': new Date('2022-01-01'), }); - const message = await userWithChatShadowMuted.post(`/groups/${group._id}/chat`, { message: testMessage }); + const message = await member.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage }); expect(message.message.id).to.exist; expect(message.message.flagCount).to.eql(0); @@ -226,100 +155,9 @@ describe('POST /chat', () => { expect(message.message.id).to.exist; expect(message.message.flagCount).to.eql(0); }); - - it('creates a chat with zero flagCount when non-shadow-muted user sends a message to a public guild', async () => { - const message = await member.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage }); - expect(message.message.id).to.exist; - expect(message.message.flagCount).to.eql(0); - }); }); context('banned word', () => { - it('returns an error when chat message contains a banned word in tavern', async () => { - await expect(user.post('/groups/habitrpg/chat', { message: testBannedWordMessage })) - .to.eventually.be.rejected.and.eql({ - code: 400, - error: 'BadRequest', - message: bannedWordErrorMessage, - }); - }); - - it('returns an error when chat message contains a banned word in a public guild', async () => { - const { group, members } = await createAndPopulateGroup({ - groupDetails: { - name: 'public guild', - type: 'guild', - privacy: 'public', - }, - members: 1, - }); - - await expect(members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage })) - .to.eventually.be.rejected.and.eql({ - code: 400, - error: 'BadRequest', - message: bannedWordErrorMessage, - }); - }); - - it('errors when word is part of a phrase', async () => { - const wordInPhrase = `phrase ${testBannedWordMessage} end`; - await expect(user.post('/groups/habitrpg/chat', { message: wordInPhrase })) - .to.eventually.be.rejected.and.eql({ - code: 400, - error: 'BadRequest', - message: bannedWordErrorMessage, - }); - }); - - it('errors when word is surrounded by non alphabet characters', async () => { - const wordInPhrase = `_!${testBannedWordMessage}@_`; - await expect(user.post('/groups/habitrpg/chat', { message: wordInPhrase })) - .to.eventually.be.rejected.and.eql({ - code: 400, - error: 'BadRequest', - message: bannedWordErrorMessage, - }); - }); - - it('errors when word is typed in mixed case', async () => { - const substrLength = Math.floor(testBannedWordMessage.length / 2); - const chatMessage = testBannedWordMessage.substring(0, substrLength).toLowerCase() - + testBannedWordMessage.substring(substrLength).toUpperCase(); - await expect(user.post('/groups/habitrpg/chat', { message: chatMessage })) - .to.eventually.be.rejected.and.eql({ - code: 400, - error: 'BadRequest', - message: t('bannedWordUsed', { swearWordsUsed: chatMessage }), - }); - }); - - it('checks error message has all the banned words used, regardless of case', async () => { - const testBannedWords = [ - testBannedWordMessage.toUpperCase(), - testBannedWordMessage1.toLowerCase(), - ]; - const chatMessage = `Mixing ${testBannedWords[0]} and ${testBannedWords[1]} is bad for you.`; - await expect(user.post('/groups/habitrpg/chat', { message: chatMessage })) - .to.eventually.be.rejected - .and.have.property('message') - .that.includes(testBannedWords.join(', ')); - }); - - it('does not error when bad word is suffix of a word', async () => { - const wordAsSuffix = `prefix${testBannedWordMessage}`; - const message = await user.post('/groups/habitrpg/chat', { message: wordAsSuffix }); - - expect(message.message.id).to.exist; - }); - - it('does not error when bad word is prefix of a word', async () => { - const wordAsPrefix = `${testBannedWordMessage}suffix`; - const message = await user.post('/groups/habitrpg/chat', { message: wordAsPrefix }); - - expect(message.message.id).to.exist; - }); - it('does not error when sending a chat message containing a banned word to a party', async () => { const { group, members } = await createAndPopulateGroup({ groupDetails: { @@ -336,37 +174,8 @@ describe('POST /chat', () => { expect(message.message.id).to.exist; }); - it('does not error when sending a chat message containing a banned word to a public guild in which banned words are allowed', async () => { - const { group, members } = await createAndPopulateGroup({ - groupDetails: { - name: 'public guild', - type: 'guild', - privacy: 'public', - }, - members: 1, - }); - - // Update the bannedWordsAllowed property for the group - group.update({ bannedWordsAllowed: true }); - await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') }); - - const message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage }); - - expect(message.message.id).to.exist; - }); - it('does not error when sending a chat message containing a banned word to a private guild', async () => { - const { group, members } = await createAndPopulateGroup({ - groupDetails: { - name: 'private guild', - type: 'guild', - privacy: 'private', - }, - members: 1, - }); - await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') }); - - const message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage }); + const message = await member.post(`/groups/${groupWithChat._id}/chat`, { message: testBannedWordMessage }); expect(message.message.id).to.exist; }); @@ -383,45 +192,6 @@ describe('POST /chat', () => { user.update({ 'flags.chatRevoked': false }); }); - it('errors and revokes privileges when chat message contains a banned slur', async () => { - await expect(user.post(`/groups/${groupWithChat._id}/chat`, { message: testSlurMessage })).to.eventually.be.rejected.and.eql({ - code: 400, - error: 'BadRequest', - message: t('bannedSlurUsed'), - }); - - // Email sent to mods - await sleep(0.5); - expect(email.sendTxn).to.be.calledOnce; - expect(email.sendTxn.args[0][1]).to.eql('slur-report-to-mods'); - - // Slack message to mods - expect(IncomingWebhook.prototype.send).to.be.calledOnce; - /* eslint-disable camelcase */ - expect(IncomingWebhook.prototype.send).to.be.calledWith({ - text: `${user.profile.name} (${user.id}) tried to post a slur`, - attachments: [{ - fallback: 'Slur Message', - color: 'danger', - author_name: `@${user.auth.local.username} ${user.profile.name} (${user.auth.local.email}; ${user._id})`, - title: 'Slur in Test Guild', - title_link: `${BASE_URL}/groups/guild/${groupWithChat.id}`, - text: testSlurMessage, - mrkdwn_in: [ - 'text', - ], - }], - }); - /* eslint-enable camelcase */ - - // Chat privileges are revoked - await expect(user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage })).to.eventually.be.rejected.and.eql({ - code: 401, - error: 'NotAuthorized', - message: t('chatPrivilegesRevoked'), - }); - }); - it('allows slurs in private groups', async () => { const { group, members } = await createAndPopulateGroup({ groupDetails: { @@ -437,28 +207,17 @@ describe('POST /chat', () => { expect(message.message.id).to.exist; }); - - it('errors when slur is typed in mixed case', async () => { - const substrLength = Math.floor(testSlurMessage1.length / 2); - const chatMessage = testSlurMessage1.substring(0, substrLength).toLowerCase() - + testSlurMessage1.substring(substrLength).toUpperCase(); - await expect(user.post('/groups/habitrpg/chat', { message: chatMessage })) - .to.eventually.be.rejected.and.eql({ - code: 400, - error: 'BadRequest', - message: t('bannedSlurUsed'), - }); - }); }); it('errors when user account is too young', async () => { - const brandNewUser = await generateUser(); - await expect(brandNewUser.post('/groups/habitrpg/chat', { message: 'hi im new' })) + await user.update({ 'auth.timestamps.created': new Date() }); + await expect(user.post(`/groups/${groupWithChat._id}/chat`, { message: 'hi im new' })) .to.eventually.be.rejected.and.eql({ code: 400, error: 'BadRequest', message: t('chatTemporarilyUnavailable'), }); + await user.update({ 'auth.timestamps.created': new Date('2022-01-01') }); }); it('creates a chat', async () => { @@ -519,54 +278,42 @@ describe('POST /chat', () => { const mount = 'test-mount'; const pet = 'test-pet'; const style = 'test-style'; - const userWithStyle = await generateUser({ + await user.update({ 'items.currentMount': mount, 'items.currentPet': pet, 'preferences.style': style, - 'auth.timestamps.created': new Date('2022-01-01'), }); - await userWithStyle.sync(); - const message = await userWithStyle.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage }); + const message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage }); expect(message.message.id).to.exist; - expect(message.message.userStyles.items.currentMount).to.eql(userWithStyle.items.currentMount); - expect(message.message.userStyles.items.currentPet).to.eql(userWithStyle.items.currentPet); - expect(message.message.userStyles.preferences.style).to.eql(userWithStyle.preferences.style); - expect(message.message.userStyles.preferences.hair).to.eql(userWithStyle.preferences.hair); - expect(message.message.userStyles.preferences.skin).to.eql(userWithStyle.preferences.skin); - expect(message.message.userStyles.preferences.shirt).to.eql(userWithStyle.preferences.shirt); - expect(message.message.userStyles.preferences.chair).to.eql(userWithStyle.preferences.chair); + expect(message.message.userStyles.items.currentMount).to.eql(user.items.currentMount); + expect(message.message.userStyles.items.currentPet).to.eql(user.items.currentPet); + expect(message.message.userStyles.preferences.style).to.eql(user.preferences.style); + expect(message.message.userStyles.preferences.hair).to.eql(user.preferences.hair); + expect(message.message.userStyles.preferences.skin).to.eql(user.preferences.skin); + expect(message.message.userStyles.preferences.shirt).to.eql(user.preferences.shirt); + expect(message.message.userStyles.preferences.chair).to.eql(user.preferences.chair); expect(message.message.userStyles.preferences.background) - .to.eql(userWithStyle.preferences.background); + .to.eql(user.preferences.background); }); it('creates equipped to user styles', async () => { - const userWithStyle = await generateUser({ - 'preferences.costume': false, - 'auth.timestamps.created': new Date('2022-01-01'), - }); - await userWithStyle.sync(); - - const message = await userWithStyle.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage }); + const message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage }); expect(message.message.id).to.exist; expect(message.message.userStyles.items.gear.equipped) - .to.eql(userWithStyle.items.gear.equipped); + .to.eql(user.items.gear.equipped); expect(message.message.userStyles.items.gear.costume).to.not.exist; }); it('creates costume to user styles', async () => { - const userWithStyle = await generateUser({ - 'preferences.costume': true, - 'auth.timestamps.created': new Date('2022-01-01'), - }); - await userWithStyle.sync(); + await user.update({ 'preferences.costume': true }); - const message = await userWithStyle.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage }); + const message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage }); expect(message.message.id).to.exist; - expect(message.message.userStyles.items.gear.costume).to.eql(userWithStyle.items.gear.costume); + expect(message.message.userStyles.items.gear.costume).to.eql(user.items.gear.costume); expect(message.message.userStyles.items.gear.equipped).to.not.exist; }); @@ -576,12 +323,11 @@ describe('POST /chat', () => { tier: 800, tokensApplied: true, }; - const backer = await generateUser({ + await user.update({ backer: backerInfo, - 'auth.timestamps.created': new Date('2022-01-01'), }); - const message = await backer.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage }); + const message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage }); const messageBackerInfo = message.message.backer; expect(messageBackerInfo.npc).to.equal(backerInfo.npc); @@ -661,43 +407,5 @@ describe('POST /chat', () => { expect(memberWithNotification.newMessages[`${group._id}`]).to.exist; expect(memberWithNotification.notifications.find(n => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === group._id)).to.exist; }); - - it('does not notify other users of a new message that is already hidden from shadow-muting', async () => { - await user.update({ 'flags.chatShadowMuted': true }); - const message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage }); - const memberWithNotification = await member.get('/user'); - - await user.update({ 'flags.chatShadowMuted': false }); - - expect(message.message.id).to.exist; - expect(memberWithNotification.newMessages[`${groupWithChat._id}`]).to.not.exist; - expect(memberWithNotification.notifications.find(n => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupWithChat._id)).to.not.exist; - }); - }); - - context('Spam prevention', () => { - it('Returns an error when the user has been posting too many messages', async () => { - // Post as many messages are needed to reach the spam limit - for (let i = 0; i < SPAM_MESSAGE_LIMIT; i += 1) { - const result = await additionalMember.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage }); // eslint-disable-line no-await-in-loop - expect(result.message.id).to.exist; - } - - await expect(additionalMember.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage })).to.eventually.be.rejected.and.eql({ - code: 401, - error: 'NotAuthorized', - message: t('messageGroupChatSpam'), - }); - }); - - it('contributor should not receive spam alert', async () => { - const userSocialite = await member.update({ 'contributor.level': SPAM_MIN_EXEMPT_CONTRIB_LEVEL }); - - // Post 1 more message than the spam limit to ensure they do not reach the limit - for (let i = 0; i < SPAM_MESSAGE_LIMIT + 1; i += 1) { - const result = await userSocialite.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage }); // eslint-disable-line no-await-in-loop - expect(result.message.id).to.exist; - } - }); }); }); diff --git a/test/api/v3/integration/chat/POST-chat_seen.test.js b/test/api/v3/integration/chat/POST-chat_seen.test.js index 9cd2103bc6..89229f178f 100644 --- a/test/api/v3/integration/chat/POST-chat_seen.test.js +++ b/test/api/v3/integration/chat/POST-chat_seen.test.js @@ -12,18 +12,19 @@ describe('POST /groups/:id/chat/seen', () => { const { group, groupLeader, members } = await createAndPopulateGroup({ groupDetails: { type: 'guild', - privacy: 'public', + privacy: 'private', }, members: 1, leaderDetails: { 'auth.timestamps.created': new Date('2022-01-01'), balance: 10, }, + upgradeToGroupPlan: true, }); guild = group; guildLeader = groupLeader; - guildMember = members[0]; // eslint-disable-line prefer-destructuring + [guildMember] = members; guildMessage = await guildLeader.post(`/groups/${guild._id}/chat`, { message: 'Some guild message' }); guildMessage = guildMessage.message; diff --git a/test/api/v3/integration/chat/POST-groups_id_chat_id_clear_flags.test.js b/test/api/v3/integration/chat/POST-groups_id_chat_id_clear_flags.test.js index 9f98d73d85..13513cda1e 100644 --- a/test/api/v3/integration/chat/POST-groups_id_chat_id_clear_flags.test.js +++ b/test/api/v3/integration/chat/POST-groups_id_chat_id_clear_flags.test.js @@ -2,7 +2,6 @@ import moment from 'moment'; import { v4 as generateUUID } from 'uuid'; import { createAndPopulateGroup, - generateUser, translate as t, } from '../../../../helpers/api-integration/v3'; import config from '../../../../../config.json'; @@ -13,21 +12,24 @@ describe('POST /groups/:id/chat/:id/clearflags', () => { admin; before(async () => { - const { group, groupLeader } = await createAndPopulateGroup({ + const { group, groupLeader, members } = await createAndPopulateGroup({ groupDetails: { type: 'guild', - privacy: 'public', + privacy: 'private', }, leaderDetails: { 'auth.timestamps.created': new Date('2022-01-01'), balance: 10, }, + upgradeToGroupPlan: true, + members: 2, }); groupWithChat = group; author = groupLeader; - nonAdmin = await generateUser({ 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate() }); - admin = await generateUser({ 'permissions.moderator': true }); + [nonAdmin, admin] = members; + await nonAdmin.update({ 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate() }); + await admin.update({ 'permissions.moderator': true }); message = await author.post(`/groups/${groupWithChat._id}/chat`, { message: 'Some message' }); message = message.message; diff --git a/test/api/v3/integration/groups/GET-group-plans.test.js b/test/api/v3/integration/groups/GET-group-plans.test.js index 42e82d133d..8c050a98a2 100644 --- a/test/api/v3/integration/groups/GET-group-plans.test.js +++ b/test/api/v3/integration/groups/GET-group-plans.test.js @@ -1,6 +1,5 @@ import { - generateUser, - generateGroup, + createAndPopulateGroup, } from '../../../../helpers/api-integration/v3'; describe('GET /group-plans', () => { @@ -8,20 +7,15 @@ describe('GET /group-plans', () => { let groupPlan; before(async () => { - user = await generateUser({ balance: 4 }); - groupPlan = await generateGroup(user, - { - name: 'public guild - is member', + ({ group: groupPlan, groupLeader: user } = await createAndPopulateGroup({ + groupDetails: { + name: 'group plan - is member', type: 'guild', - privacy: 'public', + privacy: 'private', }, - { - purchased: { - plan: { - customerId: 'existings', - }, - }, - }); + upgradeToGroupPlan: true, + leaderDetails: { balance: 4 }, + })); }); it('returns group plans for the user', async () => { diff --git a/test/api/v3/integration/groups/GET-groups.test.js b/test/api/v3/integration/groups/GET-groups.test.js index 74c988e0b1..407cc21d29 100644 --- a/test/api/v3/integration/groups/GET-groups.test.js +++ b/test/api/v3/integration/groups/GET-groups.test.js @@ -1,70 +1,63 @@ import { - generateUser, + createAndPopulateGroup, resetHabiticaDB, generateGroup, translate as t, } from '../../../../helpers/api-integration/v3'; -import { - TAVERN_ID, -} from '../../../../../website/server/models/group'; -import apiError from '../../../../../website/server/libs/apiError'; describe('GET /groups', () => { - let user; - let userInGuild; - const NUMBER_OF_PUBLIC_GUILDS = 2; - const NUMBER_OF_PUBLIC_GUILDS_USER_IS_LEADER = 2; - const NUMBER_OF_PUBLIC_GUILDS_USER_IS_MEMBER = 1; - const NUMBER_OF_USERS_PRIVATE_GUILDS = 1; - const NUMBER_OF_GROUPS_USER_CAN_VIEW = 5; - const GUILD_PER_PAGE = 30; + let user; let leader; let members; + let secondGroup; let secondLeader; + const NUMBER_OF_USERS_PRIVATE_GUILDS = 2; + const NUMBER_OF_GROUPS_USER_CAN_VIEW = 3; const categories = [{ slug: 'newCat', name: 'New Category', }]; - let publicGuildNotMember; let privateGuildUserIsMemberOf; before(async () => { await resetHabiticaDB(); - const leader = await generateUser({ balance: 10 }); - user = await generateUser({ balance: 4 }); + ({ + group: privateGuildUserIsMemberOf, + groupLeader: leader, + members, + } = await createAndPopulateGroup({ + groupDetails: { + name: 'private guild - is member', + type: 'guild', + privacy: 'private', + categories, + }, + leaderDetails: { + balance: 10, + }, + members: 1, + upgradeToGroupPlan: true, + })); + [user] = members; + await user.update({ balance: 4 }); - const publicGuildUserIsMemberOf = await generateGroup(leader, { - name: 'public guild - is member', - type: 'guild', - privacy: 'public', - summary: 'ohayou kombonwa', - description: 'oyasumi', - }); - await leader.post(`/groups/${publicGuildUserIsMemberOf._id}/invite`, { uuids: [user._id] }); - await user.post(`/groups/${publicGuildUserIsMemberOf._id}/join`); + ({ group: secondGroup, groupLeader: secondLeader } = await createAndPopulateGroup({ + groupDetails: { + name: 'c++ coders', + type: 'guild', + privacy: 'private', + }, + upgradeToGroupPlan: true, + })); - userInGuild = await generateUser({ guilds: [publicGuildUserIsMemberOf._id] }); + await secondLeader.post(`/groups/${secondGroup._id}/invite`, { uuids: [user._id] }); + await user.post(`/groups/${secondGroup._id}/join`); - publicGuildNotMember = await generateGroup(leader, { - name: 'public guild - is not member', - type: 'guild', - privacy: 'public', - summary: 'Natsume Soseki', - description: 'Kinnosuke no Hondana', - categories, - }); - - privateGuildUserIsMemberOf = await generateGroup(leader, { - name: 'private guild - is member', - type: 'guild', - privacy: 'private', - categories, - }); - await leader.post(`/groups/${privateGuildUserIsMemberOf._id}/invite`, { uuids: [user._id] }); - await user.post(`/groups/${privateGuildUserIsMemberOf._id}/join`); - - await generateGroup(leader, { - name: 'private guild - is not member', - type: 'guild', - privacy: 'private', + await createAndPopulateGroup({ + groupDetails: { + name: 'private guild - is not member', + type: 'guild', + privacy: 'private', + }, + upgradeToGroupPlan: true, }); await generateGroup(leader, { @@ -98,172 +91,16 @@ describe('GET /groups', () => { }); }); - it('returns only the tavern when tavern passed in as query', async () => { - await expect(user.get('/groups?type=tavern')) - .to.eventually.have.a.lengthOf(1) - .and.to.have.nested.property('[0]') - .and.to.have.property('_id', TAVERN_ID); - }); - it('returns only the user\'s party when party passed in as query', async () => { await expect(user.get('/groups?type=party')) .to.eventually.have.a.lengthOf(1) .and.to.have.nested.property('[0]'); }); - it('returns all public guilds when publicGuilds passed in as query', async () => { - await expect(user.get('/groups?type=publicGuilds')) - .to.eventually.have.a.lengthOf(NUMBER_OF_PUBLIC_GUILDS); - }); - - describe('filters', () => { - it('returns public guilds filtered by category', async () => { - const guilds = await user.get(`/groups?type=publicGuilds&categories=${categories[0].slug}`); - - expect(guilds[0]._id).to.equal(publicGuildNotMember._id); - }); - - it('returns private guilds filtered by category', async () => { - const guilds = await user.get(`/groups?type=privateGuilds&categories=${categories[0].slug}`); - - expect(guilds[0]._id).to.equal(privateGuildUserIsMemberOf._id); - }); - - it('filters public guilds by size', async () => { - await generateGroup(user, { - name: 'guild1', - type: 'guild', - privacy: 'public', - memberCount: 1, - }); - - // @TODO: anyway to set higher memberCount in tests right now? - - const guilds = await user.get('/groups?type=publicGuilds&minMemberCount=3'); - - expect(guilds.length).to.equal(0); - }); - - it('filters private guilds by size', async () => { - await generateGroup(user, { - name: 'guild1', - type: 'guild', - privacy: 'private', - memberCount: 1, - }); - - // @TODO: anyway to set higher memberCount in tests right now? - - const guilds = await user.get('/groups?type=privateGuilds&minMemberCount=3'); - - expect(guilds.length).to.equal(0); - }); - - it('filters public guilds by leader role', async () => { - const guilds = await user.get('/groups?type=publicGuilds&leader=true'); - expect(guilds.length).to.equal(NUMBER_OF_PUBLIC_GUILDS_USER_IS_LEADER); - }); - - it('filters public guilds by member role', async () => { - const guilds = await userInGuild.get('/groups?type=publicGuilds&member=true'); - expect(guilds.length).to.equal(1); - expect(guilds[0].name).to.have.string('is member'); - }); - - it('filters public guilds by single-word search term', async () => { - const guilds = await user.get('/groups?type=publicGuilds&search=kom'); - expect(guilds.length).to.equal(1); - expect(guilds[0].summary).to.have.string('ohayou kombonwa'); - }); - - it('filters public guilds by single-word search term left and right-padded by spaces', async () => { - const guilds = await user.get('/groups?type=publicGuilds&search=++++ohayou+kombonwa+++++'); - expect(guilds.length).to.equal(1); - expect(guilds[0].summary).to.have.string('ohayou kombonwa'); - }); - - it('filters public guilds by two-words search term separated by multiple spaces', async () => { - const guilds = await user.get('/groups?type=publicGuilds&search=kinnosuke+++++hon'); - expect(guilds.length).to.equal(1); - expect(guilds[0].description).to.have.string('Kinnosuke'); - }); - }); - - describe('public guilds pagination', () => { - it('req.query.paginate must be a boolean string', async () => { - await expect(user.get('/groups?paginate=aString&type=publicGuilds')) - .to.eventually.be.rejected.and.eql({ - code: 400, - error: 'BadRequest', - message: 'Invalid request parameters.', - }); - }); - - it('req.query.paginate can only be true when req.query.type includes publicGuilds', async () => { - await expect(user.get('/groups?paginate=true&type=notPublicGuilds')) - .to.eventually.be.rejected.and.eql({ - code: 400, - error: 'BadRequest', - message: apiError('guildsOnlyPaginate'), - }); - }); - - it('req.query.page can\'t be negative', async () => { - await expect(user.get('/groups?paginate=true&page=-1&type=publicGuilds')) - .to.eventually.be.rejected.and.eql({ - code: 400, - error: 'BadRequest', - message: 'Invalid request parameters.', - }); - }); - - it('returns 30 guilds per page ordered by number of members', async () => { - await user.update({ balance: 9000 }); - const delay = () => new Promise(resolve => setTimeout(resolve, 40)); - const promises = []; - - for (let i = 0; i < 60; i += 1) { - promises.push(generateGroup(user, { - name: `public guild ${i} - is member`, - type: 'guild', - privacy: 'public', - })); - await delay(); // eslint-disable-line no-await-in-loop - } - - const groups = await Promise.all(promises); - - // update group number 32 and not the first to make sure sorting works - await groups[32].update({ name: 'guild with most members', memberCount: 199 }); - await groups[33].update({ name: 'guild with less members', memberCount: -100 }); - - const page0 = await expect(user.get('/groups?type=publicGuilds&paginate=true')) - .to.eventually.have.a.lengthOf(GUILD_PER_PAGE); - expect(page0[0].name).to.equal('guild with most members'); - - await expect(user.get('/groups?type=publicGuilds&paginate=true&page=1')) - .to.eventually.have.a.lengthOf(GUILD_PER_PAGE); - const page2 = await expect(user.get('/groups?type=publicGuilds&paginate=true&page=2')) - // 1 created now, 4 by other tests, -1 for no more tavern. - .to.eventually.have.a.lengthOf(1 + 4 - 1); - expect(page2[3].name).to.equal('guild with less members'); - }).timeout(10000); - }); - - it('makes sure that the tavern doesn\'t show up when guilds is passed as a query', async () => { - const guilds = await user.get('/groups?type=guilds'); - expect(guilds.find(g => g.id === TAVERN_ID)).to.be.undefined; - }); - - it('makes sure that the tavern doesn\'t show up when publicGuilds is passed as a query', async () => { - const guilds = await user.get('/groups?type=publicGuilds'); - expect(guilds.find(g => g.id === TAVERN_ID)).to.be.undefined; - }); - it('returns all the user\'s guilds when guilds passed in as query', async () => { await expect(user.get('/groups?type=guilds')) .to.eventually.have.a - .lengthOf(NUMBER_OF_PUBLIC_GUILDS_USER_IS_MEMBER + NUMBER_OF_USERS_PRIVATE_GUILDS); + .lengthOf(NUMBER_OF_USERS_PRIVATE_GUILDS); }); it('returns all private guilds user is a part of when privateGuilds passed in as query', async () => { @@ -272,21 +109,21 @@ describe('GET /groups', () => { }); it('returns a list of groups user has access to', async () => { - await expect(user.get('/groups?type=privateGuilds,publicGuilds,party,tavern')) - .to.eventually.have.lengthOf(NUMBER_OF_GROUPS_USER_CAN_VIEW - 1); // -1 for no Tavern. + await expect(user.get('/groups?type=privateGuilds,party')) + .to.eventually.have.lengthOf(NUMBER_OF_GROUPS_USER_CAN_VIEW); }); - it('returns a list of groups user has access to', async () => { - const group = await generateGroup(user, { - name: 'c++ coders', - type: 'guild', - privacy: 'public', + describe('filters', () => { + it('returns private guilds filtered by category', async () => { + const guilds = await user.get(`/groups?type=privateGuilds&categories=${categories[0].slug}`); + + expect(guilds[0]._id).to.equal(privateGuildUserIsMemberOf._id); }); - // search for 'c++ coders' - await expect(user.get('/groups?type=publicGuilds&paginate=true&page=0&search=c%2B%2B+coders')) - .to.eventually.have.lengthOf(1) - .and.to.have.nested.property('[0]') - .and.to.have.property('_id', group._id); + it('filters private guilds by size', async () => { + const guilds = await user.get('/groups?type=privateGuilds&minMemberCount=3'); + + expect(guilds.length).to.equal(0); + }); }); }); diff --git a/test/api/v3/integration/groups/GET-groups_groupId_invites.test.js b/test/api/v3/integration/groups/GET-groups_groupId_invites.test.js index 8cf4e4f074..9ed08b392a 100644 --- a/test/api/v3/integration/groups/GET-groups_groupId_invites.test.js +++ b/test/api/v3/integration/groups/GET-groups_groupId_invites.test.js @@ -3,6 +3,7 @@ import { generateUser, generateGroup, translate as t, + createAndPopulateGroup, } from '../../../../helpers/api-integration/v3'; describe('GET /groups/:groupId/invites', () => { @@ -71,15 +72,16 @@ describe('GET /groups/:groupId/invites', () => { }); it('returns only first 30 invites by default (req.query.limit not specified)', async () => { - const leader = await generateUser({ balance: 4 }); - const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() }); - - const invitesToGenerate = []; - for (let i = 0; i < 31; i += 1) { - invitesToGenerate.push(generateUser()); - } - const generatedInvites = await Promise.all(invitesToGenerate); - await leader.post(`/groups/${group._id}/invite`, { uuids: generatedInvites.map(invite => invite._id) }); + const { group, groupLeader: leader } = await createAndPopulateGroup({ + groupDetails: { + type: 'guild', + privacy: 'private', + name: generateUUID(), + }, + leaderDetails: { balance: 4 }, + invites: 31, + upgradeToGroupPlan: true, + }); const res = await leader.get(`/groups/${group._id}/invites`); expect(res.length).to.equal(30); @@ -90,8 +92,16 @@ describe('GET /groups/:groupId/invites', () => { }).timeout(10000); it('returns an error if req.query.limit is over 60', async () => { - const leader = await generateUser({ balance: 4 }); - const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() }); + const { group, groupLeader: leader } = await createAndPopulateGroup({ + groupDetails: { + type: 'guild', + privacy: 'private', + name: generateUUID(), + }, + leaderDetails: { balance: 4 }, + invites: 1, + upgradeToGroupPlan: true, + }); await expect(leader.get(`/groups/${group._id}/invites?limit=61`)).to.eventually.be.rejected.and.eql({ code: 400, @@ -101,8 +111,16 @@ describe('GET /groups/:groupId/invites', () => { }); it('returns an error if req.query.limit is under 1', async () => { - const leader = await generateUser({ balance: 4 }); - const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() }); + const { group, groupLeader: leader } = await createAndPopulateGroup({ + groupDetails: { + type: 'guild', + privacy: 'private', + name: generateUUID(), + }, + leaderDetails: { balance: 4 }, + invites: 1, + upgradeToGroupPlan: true, + }); await expect(leader.get(`/groups/${group._id}/invites?limit=-1`)).to.eventually.be.rejected.and.eql({ code: 400, @@ -112,8 +130,16 @@ describe('GET /groups/:groupId/invites', () => { }); it('returns an error if req.query.limit is not an integer', async () => { - const leader = await generateUser({ balance: 4 }); - const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() }); + const { group, groupLeader: leader } = await createAndPopulateGroup({ + groupDetails: { + type: 'guild', + privacy: 'private', + name: generateUUID(), + }, + leaderDetails: { balance: 4 }, + invites: 1, + upgradeToGroupPlan: true, + }); await expect(leader.get(`/groups/${group._id}/invites?limit=1.3`)).to.eventually.be.rejected.and.eql({ code: 400, @@ -123,15 +149,16 @@ describe('GET /groups/:groupId/invites', () => { }); it('returns up to 60 invites when req.query.limit is specified', async () => { - const leader = await generateUser({ balance: 4 }); - const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() }); - - const invitesToGenerate = []; - for (let i = 0; i < 31; i += 1) { - invitesToGenerate.push(generateUser()); - } - const generatedInvites = await Promise.all(invitesToGenerate); - await leader.post(`/groups/${group._id}/invite`, { uuids: generatedInvites.map(invite => invite._id) }); + const { group, groupLeader: leader } = await createAndPopulateGroup({ + groupDetails: { + type: 'guild', + privacy: 'private', + name: generateUUID(), + }, + leaderDetails: { balance: 4 }, + invites: 31, + upgradeToGroupPlan: true, + }); let res = await leader.get(`/groups/${group._id}/invites?limit=14`); expect(res.length).to.equal(14); @@ -149,17 +176,20 @@ describe('GET /groups/:groupId/invites', () => { }).timeout(30000); it('supports using req.query.lastId to get more invites', async function test () { + let group; let invitees; this.timeout(30000); // @TODO: times out after 8 seconds - const leader = await generateUser({ balance: 4 }); - const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() }); + ({ group, groupLeader: user, invitees } = await createAndPopulateGroup({ + groupDetails: { + type: 'guild', + privacy: 'private', + name: generateUUID(), + }, + leaderDetails: { balance: 4 }, + invites: 32, + upgradeToGroupPlan: true, + })); - const invitesToGenerate = []; - for (let i = 0; i < 32; i += 1) { - invitesToGenerate.push(generateUser()); - } - const generatedInvites = await Promise.all(invitesToGenerate); // Group has 32 invites - const expectedIds = generatedInvites.map(generatedInvite => generatedInvite._id); - await user.post(`/groups/${group._id}/invite`, { uuids: expectedIds }); + const expectedIds = invitees.map(generatedInvite => generatedInvite._id); const res = await user.get(`/groups/${group._id}/invites`); expect(res.length).to.equal(30); diff --git a/test/api/v3/integration/groups/GET-groups_groupId_members.test.js b/test/api/v3/integration/groups/GET-groups_groupId_members.test.js index 5247df8197..42d02fc681 100644 --- a/test/api/v3/integration/groups/GET-groups_groupId_members.test.js +++ b/test/api/v3/integration/groups/GET-groups_groupId_members.test.js @@ -1,5 +1,6 @@ import { v4 as generateUUID } from 'uuid'; import { + createAndPopulateGroup, generateUser, generateGroup, translate as t, @@ -75,7 +76,15 @@ describe('GET /groups/:groupId/members', () => { }); it('req.query.includeAllPublicFields === true works with guilds', async () => { - const group = await generateGroup(user, { type: 'guild', name: generateUUID() }); + let group; + ({ group, groupLeader: user } = await createAndPopulateGroup({ + type: 'guild', + privacy: 'private', + name: generateUUID(), + upgradeToGroupPlan: true, + members: 1, + })); + const [memberRes] = await user.get(`/groups/${group._id}/members?includeAllPublicFields=true`); expect(memberRes).to.have.all.keys([ // works as: object has all and only these keys @@ -206,20 +215,20 @@ describe('GET /groups/:groupId/members', () => { it('supports using req.query.lastId to get more members', async function test () { this.timeout(30000); // @TODO: times out after 8 seconds - const leader = await generateUser({ balance: 4 }); - const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() }); + const { group, groupLeader: leader, members: generatedUsers } = await createAndPopulateGroup({ + type: 'guild', + privacy: 'private', + name: generateUUID(), + upgradeToGroupPlan: true, + leaderDetails: { balance: 4 }, + members: 57, + }); - const usersToGenerate = []; - for (let i = 0; i < 57; i += 1) { - usersToGenerate.push(generateUser({ guilds: [group._id] })); - } - // Group has 59 members (1 is the leader) - const generatedUsers = await Promise.all(usersToGenerate); const expectedIds = [leader._id].concat(generatedUsers.map(generatedUser => generatedUser._id)); - const res = await user.get(`/groups/${group._id}/members`); + const res = await leader.get(`/groups/${group._id}/members`); expect(res.length).to.equal(30); - const res2 = await user.get(`/groups/${group._id}/members?lastId=${res[res.length - 1]._id}`); + const res2 = await leader.get(`/groups/${group._id}/members?lastId=${res[res.length - 1]._id}`); expect(res2.length).to.equal(28); const resIds = res.concat(res2).map(member => member._id); diff --git a/test/api/v3/integration/groups/GET-groups_id.test.js b/test/api/v3/integration/groups/GET-groups_id.test.js index 7d1657db18..ea22cfe9a6 100644 --- a/test/api/v3/integration/groups/GET-groups_id.test.js +++ b/test/api/v3/integration/groups/GET-groups_id.test.js @@ -11,7 +11,6 @@ import { describe('GET /groups/:id', () => { const typesOfGroups = {}; - typesOfGroups['public guild'] = { type: 'guild', privacy: 'public' }; typesOfGroups['private guild'] = { type: 'guild', privacy: 'private' }; typesOfGroups.party = { type: 'party', privacy: 'private' }; @@ -24,10 +23,11 @@ describe('GET /groups/:id', () => { const groupData = await createAndPopulateGroup({ members: 30, groupDetails, + upgradeToGroupPlan: groupDetails.type === 'guild', }); leader = groupData.groupLeader; - member = groupData.members[0]; // eslint-disable-line prefer-destructuring + [member] = groupData.members; createdGroup = groupData.group; }); @@ -49,34 +49,6 @@ describe('GET /groups/:id', () => { }); }); - context('Non-member of a public guild', () => { - let nonMember; let - createdGroup; - - before(async () => { - const groupData = await createAndPopulateGroup({ - members: 1, - groupDetails: { - name: 'test guild', - type: 'guild', - privacy: 'public', - }, - }); - - createdGroup = groupData.group; - nonMember = await generateUser(); - }); - - it('returns the group object for a non-member', async () => { - const group = await nonMember.get(`/groups/${createdGroup._id}`); - - expect(group._id).to.eql(createdGroup._id); - expect(group.name).to.eql(createdGroup.name); - expect(group.type).to.eql(createdGroup.type); - expect(group.privacy).to.eql(createdGroup.privacy); - }); - }); - context('Non-member of a private guild', () => { let nonMember; let createdGroup; @@ -89,6 +61,7 @@ describe('GET /groups/:id', () => { type: 'guild', privacy: 'private', }, + upgradeToGroupPlan: true, }); createdGroup = groupData.group; @@ -218,7 +191,7 @@ describe('GET /groups/:id', () => { }); context('Flagged messages', () => { - let group; + let group; let members; const chat1 = { id: 'chat1', @@ -268,7 +241,7 @@ describe('GET /groups/:id', () => { groupDetails: { name: 'test guild', type: 'guild', - privacy: 'public', + privacy: 'private', chat: [ chat1, chat2, @@ -277,9 +250,11 @@ describe('GET /groups/:id', () => { chat5, ], }, + members: 1, + upgradeToGroupPlan: true, }); - group = groupData.group; + ({ group, members } = groupData); await group.addChat([chat1, chat2, chat3, chat4, chat5]); }); @@ -287,8 +262,8 @@ describe('GET /groups/:id', () => { context('non-admin', () => { let nonAdmin; - beforeEach(async () => { - nonAdmin = await generateUser(); + beforeEach(() => { + [nonAdmin] = members; }); it('does not include messages with a flag count of 2 or greater', async () => { @@ -314,9 +289,8 @@ describe('GET /groups/:id', () => { let admin; beforeEach(async () => { - admin = await generateUser({ - 'permissions.moderator': true, - }); + [admin] = members; + await admin.update({ permissions: { moderator: true } }); }); it('includes all messages', async () => { diff --git a/test/api/v3/integration/groups/POST-groups.test.js b/test/api/v3/integration/groups/POST-groups.test.js index e4b915f972..cb056c3e24 100644 --- a/test/api/v3/integration/groups/POST-groups.test.js +++ b/test/api/v3/integration/groups/POST-groups.test.js @@ -2,7 +2,6 @@ import { generateUser, translate as t, } from '../../../../helpers/api-integration/v3'; -import { model as Group } from '../../../../../website/server/models/group'; import { MAX_SUMMARY_SIZE_FOR_GUILDS } from '../../../../../website/common/script/constants'; describe('POST /group', () => { @@ -35,8 +34,8 @@ describe('POST /group', () => { it('sets the group leader to the user who created the group', async () => { const group = await user.post('/groups', { - name: 'Test Public Guild', - type: 'guild', + name: 'Test Party', + type: 'party', }); expect(group.leader).to.eql({ @@ -51,7 +50,7 @@ describe('POST /group', () => { const name = 'Test Group'; const group = await user.post('/groups', { name, - type: 'guild', + type: 'party', }); const updatedGroup = await user.get(`/groups/${group._id}`); @@ -64,7 +63,7 @@ describe('POST /group', () => { const summary = 'Test Summary'; const group = await user.post('/groups', { name, - type: 'guild', + type: 'party', summary, }); @@ -78,7 +77,7 @@ describe('POST /group', () => { const summary = 'A'.repeat(MAX_SUMMARY_SIZE_FOR_GUILDS + 1); await expect(user.post('/groups', { name, - type: 'guild', + type: 'party', summary, })).to.eventually.be.rejected.and.eql({ code: 400, @@ -88,157 +87,6 @@ describe('POST /group', () => { }); }); - context('Guilds', () => { - it('returns an error when a user with insufficient funds attempts to create a guild', async () => { - await user.update({ balance: 0 }); - - await expect( - user.post('/groups', { - name: 'Test Public Guild', - type: 'guild', - }), - ).to.eventually.be.rejected.and.eql({ - code: 401, - error: 'NotAuthorized', - message: t('messageInsufficientGems'), - }); - }); - - it('adds guild to user\'s list of guilds', async () => { - const guild = await user.post('/groups', { - name: 'some guild', - type: 'guild', - privacy: 'public', - }); - - const updatedUser = await user.get('/user'); - - expect(updatedUser.guilds).to.include(guild._id); - }); - - it('awards the Joined Guild achievement', async () => { - await user.post('/groups', { - name: 'some guild', - type: 'guild', - privacy: 'public', - }); - - const updatedUser = await user.get('/user'); - - expect(updatedUser.achievements.joinedGuild).to.eql(true); - }); - - context('public guild', () => { - it('creates a group', async () => { - const groupName = 'Test Public Guild'; - const groupType = 'guild'; - const groupPrivacy = 'public'; - - const publicGuild = await user.post('/groups', { - name: groupName, - type: groupType, - privacy: groupPrivacy, - }); - - expect(publicGuild._id).to.exist; - expect(publicGuild.name).to.equal(groupName); - expect(publicGuild.type).to.equal(groupType); - expect(publicGuild.memberCount).to.equal(1); - expect(publicGuild.privacy).to.equal(groupPrivacy); - expect(publicGuild.leader).to.eql({ - _id: user._id, - profile: { - name: user.profile.name, - }, - }); - }); - - it('returns an error when a user with no chat privileges attempts to create a public guild', async () => { - await user.update({ 'flags.chatRevoked': true }); - - await expect( - user.post('/groups', { - name: 'Test Public Guild', - type: 'guild', - privacy: 'public', - }), - ).to.eventually.be.rejected.and.eql({ - code: 401, - error: 'NotAuthorized', - message: t('chatPrivilegesRevoked'), - }); - }); - }); - - context('private guild', () => { - const groupName = 'Test Private Guild'; - const groupType = 'guild'; - const groupPrivacy = 'private'; - - it('creates a group', async () => { - const privateGuild = await user.post('/groups', { - name: groupName, - type: groupType, - privacy: groupPrivacy, - }); - - expect(privateGuild._id).to.exist; - expect(privateGuild.name).to.equal(groupName); - expect(privateGuild.type).to.equal(groupType); - expect(privateGuild.memberCount).to.equal(1); - expect(privateGuild.privacy).to.equal(groupPrivacy); - expect(privateGuild.leader).to.eql({ - _id: user._id, - profile: { - name: user.profile.name, - }, - }); - }); - - it('creates a private guild when the user has no chat privileges', async () => { - await user.update({ 'flags.chatRevoked': true }); - const privateGuild = await user.post('/groups', { - name: groupName, - type: groupType, - privacy: groupPrivacy, - }); - - expect(privateGuild._id).to.exist; - }); - - it('deducts gems from user and adds them to guild bank', async () => { - const privateGuild = await user.post('/groups', { - name: groupName, - type: groupType, - privacy: groupPrivacy, - }); - - expect(privateGuild.balance).to.eql(1); - - const updatedUser = await user.get('/user'); - - expect(updatedUser.balance).to.eql(user.balance - 1); - }); - - it('does not deduct the gems from user when guild creation fails', async () => { - const stub = sinon.stub(Group.prototype, 'save').rejects(); - const promise = user.post('/groups', { - name: groupName, - type: groupType, - privacy: groupPrivacy, - }); - - await expect(promise).to.eventually.be.rejected; - - const updatedUser = await user.get('/user'); - - expect(updatedUser.balance).to.eql(user.balance); - - stub.restore(); - }); - }); - }); - context('Parties', () => { const partyName = 'Test Party'; const partyType = 'party'; diff --git a/test/api/v3/integration/groups/POST-groups_groupId_join.test.js b/test/api/v3/integration/groups/POST-groups_groupId_join.test.js index 2c6313154b..a9bc3ed49f 100644 --- a/test/api/v3/integration/groups/POST-groups_groupId_join.test.js +++ b/test/api/v3/integration/groups/POST-groups_groupId_join.test.js @@ -18,81 +18,24 @@ describe('POST /group/:groupId/join', () => { }); }); - context('Joining a public guild', () => { - let user; let joiningUser; let - publicGuild; - - beforeEach(async () => { - const { group, groupLeader } = await createAndPopulateGroup({ - groupDetails: { - name: 'Test Guild', - type: 'guild', - privacy: 'public', - }, - }); - - publicGuild = group; - user = groupLeader; - joiningUser = await generateUser(); - }); - - it('allows non-invited users to join public guilds', async () => { - const res = await joiningUser.post(`/groups/${publicGuild._id}/join`); - - await expect(joiningUser.get('/user')).to.eventually.have.property('guilds').to.include(publicGuild._id); - expect(res.leader._id).to.eql(user._id); - expect(res.leader.profile.name).to.eql(user.profile.name); - }); - - it('returns an error if user was already a member', async () => { - await joiningUser.post(`/groups/${publicGuild._id}/join`); - await expect(joiningUser.post(`/groups/${publicGuild._id}/join`)).to.eventually.be.rejected.and.eql({ - code: 401, - error: 'NotAuthorized', - message: t('youAreAlreadyInGroup'), - }); - }); - - it('promotes joining member in a public empty guild to leader', async () => { - await user.post(`/groups/${publicGuild._id}/leave`); - - await joiningUser.post(`/groups/${publicGuild._id}/join`); - - await expect(joiningUser.get(`/groups/${publicGuild._id}`)).to.eventually.have.nested.property('leader._id', joiningUser._id); - }); - - it('increments memberCount when joining guilds', async () => { - const oldMemberCount = publicGuild.memberCount; - - await joiningUser.post(`/groups/${publicGuild._id}/join`); - - await expect(joiningUser.get(`/groups/${publicGuild._id}`)).to.eventually.have.property('memberCount', oldMemberCount + 1); - }); - - it('awards Joined Guild achievement', async () => { - await joiningUser.post(`/groups/${publicGuild._id}/join`); - - await expect(joiningUser.get('/user')).to.eventually.have.nested.property('achievements.joinedGuild', true); - }); - }); - context('Joining a private guild', () => { - let user; let invitedUser; let - guild; + let user; + let invitedUser; + let guild; + let invitees; beforeEach(async () => { - const { group, groupLeader, invitees } = await createAndPopulateGroup({ + ({ group: guild, groupLeader: user, invitees } = await createAndPopulateGroup({ groupDetails: { name: 'Test Guild', type: 'guild', privacy: 'private', }, invites: 1, - }); + upgradeToGroupPlan: true, + })); - guild = group; - user = groupLeader; - invitedUser = invitees[0]; // eslint-disable-line prefer-destructuring + [invitedUser] = invitees; }); it('returns error when user is not invited to private guild', async () => { @@ -182,7 +125,7 @@ describe('POST /group/:groupId/join', () => { party = group; user = groupLeader; - invitedUser = invitees[0]; // eslint-disable-line prefer-destructuring + [invitedUser] = invitees; }); it('returns error when user is not invited to party', async () => { diff --git a/test/api/v3/integration/groups/POST-groups_groupId_leave.test.js b/test/api/v3/integration/groups/POST-groups_groupId_leave.test.js index b7b16a7f04..523c7b9d65 100644 --- a/test/api/v3/integration/groups/POST-groups_groupId_leave.test.js +++ b/test/api/v3/integration/groups/POST-groups_groupId_leave.test.js @@ -5,7 +5,6 @@ import { generateChallenge, checkExistence, createAndPopulateGroup, - sleep, generateUser, translate as t, } from '../../../../helpers/api-integration/v3'; @@ -14,253 +13,187 @@ import payments from '../../../../../website/server/libs/payments/payments'; import calculateSubscriptionTerminationDate from '../../../../../website/server/libs/payments/calculateSubscriptionTerminationDate'; describe('POST /groups/:groupId/leave', () => { - const typesOfGroups = { - 'public guild': { type: 'guild', privacy: 'public' }, - 'private guild': { type: 'guild', privacy: 'private' }, - party: { type: 'party', privacy: 'private' }, - }; + let groupToLeave; + let leader; + let member; + let members; + let memberCount; - each(typesOfGroups, (groupDetails, groupType) => { - context(`Leaving a ${groupType}`, () => { - let groupToLeave; - let leader; - let member; - let memberCount; + context('Leaving a Group Plan', () => { + beforeEach(async () => { + ({ group: groupToLeave, groupLeader: leader, members } = await createAndPopulateGroup({ + type: 'guild', + privacy: 'private', + members: 1, + upgradeToGroupPlan: true, + })); + + [member] = members; + memberCount = groupToLeave.memberCount; + await leader.update({ 'auth.timestamps.created': new Date('2022-01-01') }); + }); + + it('prevents non members from leaving', async () => { + const user = await generateUser(); + await expect(user.post(`/groups/${groupToLeave._id}/leave`)).to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: t('groupNotFound'), + }); + }); + + it('lets user leave', async () => { + await member.post(`/groups/${groupToLeave._id}/leave`); + + const userThatLeftGroup = await member.get('/user'); + + expect(userThatLeftGroup.guilds).to.be.empty; + expect(userThatLeftGroup.party._id).to.not.exist; + await groupToLeave.sync(); + expect(groupToLeave.memberCount).to.equal(memberCount - 1); + }); + + it('removes new messages for that group from user', async () => { + await leader.post(`/groups/${groupToLeave._id}/chat`, { message: 'Some message' }); + await member.sync(); + + expect(member.notifications.find(n => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupToLeave._id)).to.exist; + expect(member.newMessages[groupToLeave._id]).to.not.be.empty; + + await member.post(`/groups/${groupToLeave._id}/leave`); + await member.sync(); + + expect(member.notifications.find(n => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupToLeave._id)).to.not.exist; + expect(member.newMessages[groupToLeave._id]).to.be.undefined; + }); + + context('with challenges', () => { + let challenge; beforeEach(async () => { - const { group, groupLeader, members } = await createAndPopulateGroup({ - groupDetails, - members: 1, - }); + challenge = await generateChallenge(leader, groupToLeave); + await member.post(`/challenges/${challenge._id}/join`); - groupToLeave = group; - leader = groupLeader; - member = members[0]; // eslint-disable-line prefer-destructuring - memberCount = group.memberCount; - await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') }); - }); - - it('prevents non members from leaving', async () => { - const user = await generateUser(); - await expect(user.post(`/groups/${groupToLeave._id}/leave`)).to.eventually.be.rejected.and.eql({ - code: 404, - error: 'NotFound', - message: t('groupNotFound'), + await leader.post(`/tasks/challenge/${challenge._id}`, { + text: 'test habit', + type: 'habit', }); }); - it(`lets user leave a ${groupType}`, async () => { + it('removes all challenge tasks when keep parameter is set to remove', async () => { + await member.post(`/groups/${groupToLeave._id}/leave?keep=remove-all`); + + const userWithoutChallengeTasks = await member.get('/user'); + + expect(userWithoutChallengeTasks.challenges).to.not.include(challenge._id); + expect(userWithoutChallengeTasks.tasksOrder.habits).to.be.empty; + }); + + it('keeps all challenge tasks when keep parameter is not set', async () => { await member.post(`/groups/${groupToLeave._id}/leave`); - const userThatLeftGroup = await member.get('/user'); + const userWithChallengeTasks = await member.get('/user'); - expect(userThatLeftGroup.guilds).to.be.empty; - expect(userThatLeftGroup.party._id).to.not.exist; - await groupToLeave.sync(); - expect(groupToLeave.memberCount).to.equal(memberCount - 1); + expect(userWithChallengeTasks.tasksOrder.habits).to.not.be.empty; }); - it(`sets a new group leader when leader leaves a ${groupType}`, async () => { - await leader.post(`/groups/${groupToLeave._id}/leave`); + it('keeps the user in the challenge when the keepChallenges parameter is set to remain-in-challenges', async () => { + await member.post(`/groups/${groupToLeave._id}/leave`, { keepChallenges: 'remain-in-challenges' }); - await groupToLeave.sync(); - expect(groupToLeave.memberCount).to.equal(memberCount - 1); - expect(groupToLeave.leader).to.equal(member._id); + const userWithChallengeTasks = await member.get('/user'); + + expect(userWithChallengeTasks.challenges).to.include(challenge._id); }); - it('removes new messages for that group from user', async () => { - await member.post(`/groups/${groupToLeave._id}/chat`, { message: 'Some message' }); + it('drops the user in the challenge when the keepChallenges parameter isn\'t set', async () => { + await member.post(`/groups/${groupToLeave._id}/leave`); - await sleep(0.5); + const userWithChallengeTasks = await member.get('/user'); - await leader.sync(); - - expect(leader.notifications.find(n => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupToLeave._id)).to.exist; - expect(leader.newMessages[groupToLeave._id]).to.not.be.empty; - - await leader.post(`/groups/${groupToLeave._id}/leave`); - await leader.sync(); - - expect(leader.notifications.find(n => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupToLeave._id)).to.not.exist; - expect(leader.newMessages[groupToLeave._id]).to.be.undefined; + expect(userWithChallengeTasks.challenges).to.not.include(challenge._id); }); - - context('with challenges', () => { - let challenge; - - beforeEach(async () => { - challenge = await generateChallenge(leader, groupToLeave); - await leader.post(`/challenges/${challenge._id}/join`); - - await leader.post(`/tasks/challenge/${challenge._id}`, { - text: 'test habit', - type: 'habit', - }); - - await sleep(0.5); - }); - - it('removes all challenge tasks when keep parameter is set to remove', async () => { - await leader.post(`/groups/${groupToLeave._id}/leave?keep=remove-all`); - - const userWithoutChallengeTasks = await leader.get('/user'); - - expect(userWithoutChallengeTasks.challenges).to.not.include(challenge._id); - expect(userWithoutChallengeTasks.tasksOrder.habits).to.be.empty; - }); - - it('keeps all challenge tasks when keep parameter is not set', async () => { - await leader.post(`/groups/${groupToLeave._id}/leave`); - - const userWithChallengeTasks = await leader.get('/user'); - - // @TODO find elegant way to assert against the task existing - expect(userWithChallengeTasks.tasksOrder.habits).to.not.be.empty; - }); - - it('keeps the user in the challenge when the keepChallenges parameter is set to remain-in-challenges', async () => { - await leader.post(`/groups/${groupToLeave._id}/leave`, { keepChallenges: 'remain-in-challenges' }); - - const userWithChallengeTasks = await leader.get('/user'); - - expect(userWithChallengeTasks.challenges).to.include(challenge._id); - }); - - it('drops the user in the challenge when the keepChallenges parameter isn\'t set', async () => { - await leader.post(`/groups/${groupToLeave._id}/leave`); - - const userWithChallengeTasks = await leader.get('/user'); - - expect(userWithChallengeTasks.challenges).to.not.include(challenge._id); - }); - }); - - it('prevents quest leader from leaving a groupToLeave'); - it('prevents a user from leaving during an active quest'); }); }); - context('Leaving a group as the last member', () => { - context('private guild', () => { - let privateGuild; - let leader; - let invitedUser; + context('Leaving a Party', () => { + let invitees; + let invitedUser; - beforeEach(async () => { - const { group, groupLeader, invitees } = await createAndPopulateGroup({ - groupDetails: { - name: 'Test Private Guild', - type: 'guild', - }, - invites: 1, - leaderDetails: { - 'auth.timestamps.created': new Date('2022-01-01'), - balance: 10, - }, - }); + beforeEach(async () => { + ({ + group: groupToLeave, + groupLeader: leader, + members, + invitees, + } = await createAndPopulateGroup({ + type: 'party', + privacy: 'private', + members: 1, + invites: 1, + })); - privateGuild = group; - leader = groupLeader; - invitedUser = invitees[0]; // eslint-disable-line prefer-destructuring + [member] = members; + [invitedUser] = invitees; + memberCount = groupToLeave.memberCount; + await leader.update({ 'auth.timestamps.created': new Date('2022-01-01') }); + }); - await leader.post(`/groups/${group._id}/chat`, { message: 'Some message' }); - }); - - it('removes a group when the last member leaves', async () => { - await leader.post(`/groups/${privateGuild._id}/leave`); - - await expect(checkExistence('groups', privateGuild._id)).to.eventually.equal(false); - }); - - it('removes invitations when the last member leaves', async () => { - await leader.post(`/groups/${privateGuild._id}/leave`); - - const userWithoutInvitation = await invitedUser.get('/user'); - - expect(userWithoutInvitation.invitations.guilds).to.be.empty; + it('prevents non members from leaving', async () => { + const user = await generateUser(); + await expect(user.post(`/groups/${groupToLeave._id}/leave`)).to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: t('groupNotFound'), }); }); - context('public guild', () => { - let publicGuild; - let leader; - let invitedUser; + it('lets user leave', async () => { + await member.post(`/groups/${groupToLeave._id}/leave`); - beforeEach(async () => { - const { group, groupLeader, invitees } = await createAndPopulateGroup({ - groupDetails: { - name: 'Test Public Guild', - type: 'guild', - privacy: 'public', - }, - invites: 1, - }); + const userThatLeftGroup = await member.get('/user'); - publicGuild = group; - leader = groupLeader; - invitedUser = invitees[0]; // eslint-disable-line prefer-destructuring - }); - - it('keeps the group when the last member leaves', async () => { - await leader.post(`/groups/${publicGuild._id}/leave`); - - await expect(checkExistence('groups', publicGuild._id)).to.eventually.equal(true); - }); - - it('keeps the invitations when the last member leaves a public guild', async () => { - await leader.post(`/groups/${publicGuild._id}/leave`); - - const userWithoutInvitation = await invitedUser.get('/user'); - - expect(userWithoutInvitation.invitations.guilds).to.not.be.empty; - }); - - it('deletes non existent guild from user when user tries to leave', async () => { - const nonExistentGuildId = generateUUID(); - const userWithNonExistentGuild = await generateUser({ guilds: [nonExistentGuildId] }); - expect(userWithNonExistentGuild.guilds).to.contain(nonExistentGuildId); - - await expect(userWithNonExistentGuild.post(`/groups/${nonExistentGuildId}/leave`)) - .to.eventually.be.rejected; - - await userWithNonExistentGuild.sync(); - - expect(userWithNonExistentGuild.guilds).to.not.contain(nonExistentGuildId); - }); + expect(userThatLeftGroup.guilds).to.be.empty; + expect(userThatLeftGroup.party._id).to.not.exist; + await groupToLeave.sync(); + expect(groupToLeave.memberCount).to.equal(memberCount - 1); }); - context('party', () => { - let party; - let leader; - let invitedUser; + it('sets a new group leader when leader leaves', async () => { + await leader.post(`/groups/${groupToLeave._id}/leave`); - beforeEach(async () => { - const { group, groupLeader, invitees } = await createAndPopulateGroup({ - groupDetails: { - name: 'Test Party', - type: 'party', - }, - invites: 1, - }); + await groupToLeave.sync(); + expect(groupToLeave.memberCount).to.equal(memberCount - 1); + expect(groupToLeave.leader).to.equal(member._id); + }); - party = group; - leader = groupLeader; - invitedUser = invitees[0]; // eslint-disable-line prefer-destructuring - }); + it('removes new messages for that group from user', async () => { + await leader.post(`/groups/${groupToLeave._id}/chat`, { message: 'Some message' }); + await member.sync(); - it('removes a group when the last member leaves a party', async () => { - await leader.post(`/groups/${party._id}/leave`); + expect(member.notifications.find(n => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupToLeave._id)).to.exist; + expect(member.newMessages[groupToLeave._id]).to.not.be.empty; - await expect(checkExistence('party', party._id)).to.eventually.equal(false); - }); + await member.post(`/groups/${groupToLeave._id}/leave`); + await member.sync(); - it('removes invitations when the last member leaves a party', async () => { - await leader.post(`/groups/${party._id}/leave`); + expect(member.notifications.find(n => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupToLeave._id)).to.not.exist; + expect(member.newMessages[groupToLeave._id]).to.be.undefined; + }); - const userWithoutInvitation = await invitedUser.get('/user'); + it('removes a party when the last member leaves', async () => { + await member.post(`/groups/${groupToLeave._id}/leave`); + await leader.post(`/groups/${groupToLeave._id}/leave`); - expect(userWithoutInvitation.invitations.parties[0]).to.be.undefined; - }); + await expect(checkExistence('party', groupToLeave._id)).to.eventually.equal(false); + }); + + it('removes invitations when the last member leaves a party', async () => { + await member.post(`/groups/${groupToLeave._id}/leave`); + await leader.post(`/groups/${groupToLeave._id}/leave`); + + const userWithoutInvitation = await invitedUser.get('/user'); + + expect(userWithoutInvitation.invitations.parties[0]).to.be.undefined; }); it('deletes non existent party from user when user tries to leave', async () => { @@ -275,23 +208,71 @@ describe('POST /groups/:groupId/leave', () => { expect(userWithNonExistentParty.party).to.eql({}); }); + + context('with challenges', () => { + let challenge; + + beforeEach(async () => { + challenge = await generateChallenge(leader, groupToLeave); + await member.post(`/challenges/${challenge._id}/join`); + + await leader.post(`/tasks/challenge/${challenge._id}`, { + text: 'test habit', + type: 'habit', + }); + }); + + it('removes all challenge tasks when keep parameter is set to remove', async () => { + await member.post(`/groups/${groupToLeave._id}/leave?keep=remove-all`); + + const userWithoutChallengeTasks = await member.get('/user'); + + expect(userWithoutChallengeTasks.challenges).to.not.include(challenge._id); + expect(userWithoutChallengeTasks.tasksOrder.habits).to.be.empty; + }); + + it('keeps all challenge tasks when keep parameter is not set', async () => { + await member.post(`/groups/${groupToLeave._id}/leave`); + + const userWithChallengeTasks = await member.get('/user'); + + expect(userWithChallengeTasks.tasksOrder.habits).to.not.be.empty; + }); + + it('keeps the user in the challenge when the keepChallenges parameter is set to remain-in-challenges', async () => { + await member.post(`/groups/${groupToLeave._id}/leave`, { keepChallenges: 'remain-in-challenges' }); + + const userWithChallengeTasks = await member.get('/user'); + + expect(userWithChallengeTasks.challenges).to.include(challenge._id); + }); + + it('drops the user in the challenge when the keepChallenges parameter isn\'t set', async () => { + await member.post(`/groups/${groupToLeave._id}/leave`); + + const userWithChallengeTasks = await member.get('/user'); + + expect(userWithChallengeTasks.challenges).to.not.include(challenge._id); + }); + }); }); + const typesOfGroups = { + 'private guild': { type: 'guild', privacy: 'private' }, + party: { type: 'party', privacy: 'private' }, + }; + each(typesOfGroups, (groupDetails, groupType) => { context(`Leaving a group plan when the group is a ${groupType}`, () => { - if (groupDetails.privacy === 'public') return; // public guilds cannot be group plans let groupWithPlan; - let leader; - let member; beforeEach(async () => { - const { group, groupLeader, members } = await createAndPopulateGroup({ + ({ group: groupWithPlan, groupLeader: leader, members } = await createAndPopulateGroup({ groupDetails, members: 1, - }); - leader = groupLeader; - member = members[0]; // eslint-disable-line prefer-destructuring - groupWithPlan = group; + upgradeToGroupPlan: true, + })); + [member] = members; const userWithFreePlan = await User.findById(leader._id).exec(); // Create subscription @@ -321,45 +302,21 @@ describe('POST /groups/:groupId/leave', () => { await member.sync(); expect(member.purchased.plan.dateTerminated).to.exist; }); - - it('preserves the free subscription when leaving a any other group without a plan', async () => { - // Joining a guild without a group plan - const { group: groupWithNoPlan } = await createAndPopulateGroup({ - groupDetails: { - name: 'Group Without Plan', - type: 'guild', - privacy: 'public', - }, - }); - - await member.post(`/groups/${groupWithNoPlan._id}/join`); - await member.sync(); - expect(member.purchased.plan.planId).to.equal('group_plan_auto'); - expect(member.purchased.plan.dateTerminated).to.not.exist; - - // Leaving the guild without a group plan - await member.post(`/groups/${groupWithNoPlan._id}/leave`); - await member.sync(); - expect(member.purchased.plan.dateTerminated).to.not.exist; - }); }); }); each(typesOfGroups, (groupDetails, groupType) => { context(`Leaving a group with extraMonths left plan when the group is a ${groupType}`, () => { - if (groupDetails.privacy === 'public') return; // public guilds cannot be group plans const extraMonths = 12; let groupWithPlan; - let member; beforeEach(async () => { - const { group, members } = await createAndPopulateGroup({ + ({ group: groupWithPlan, members } = await createAndPopulateGroup({ groupDetails, members: 1, upgradeToGroupPlan: true, - }); + })); [member] = members; - groupWithPlan = group; await member.update({ 'purchased.plan.extraMonths': extraMonths, }); diff --git a/test/api/v3/integration/groups/POST-groups_groupId_reject.test.js b/test/api/v3/integration/groups/POST-groups_groupId_reject.test.js index 68fba0e772..ccc8e761c9 100644 --- a/test/api/v3/integration/groups/POST-groups_groupId_reject.test.js +++ b/test/api/v3/integration/groups/POST-groups_groupId_reject.test.js @@ -5,43 +5,6 @@ import { } from '../../../../helpers/api-integration/v3'; describe('POST /group/:groupId/reject-invite', () => { - context('Rejecting a public guild invite', () => { - let publicGuild; let - invitedUser; - - beforeEach(async () => { - const { group, invitees } = await createAndPopulateGroup({ - groupDetails: { - name: 'Test Guild', - type: 'guild', - privacy: 'public', - }, - invites: 1, - }); - - publicGuild = group; - invitedUser = invitees[0]; // eslint-disable-line prefer-destructuring - }); - - it('returns error when user is not invited', async () => { - const userWithoutInvite = await generateUser(); - - await expect(userWithoutInvite.post(`/groups/${publicGuild._id}/reject-invite`)).to.eventually.be.rejected.and.eql({ - code: 401, - error: 'NotAuthorized', - message: t('messageGroupRequiresInvite'), - }); - }); - - it('clears invitation from user', async () => { - await invitedUser.post(`/groups/${publicGuild._id}/reject-invite`); - - await expect(invitedUser.get('/user')) - .to.eventually.have.nested.property('invitations.guilds') - .to.not.include({ id: publicGuild._id }); - }); - }); - context('Rejecting a private guild invite', () => { let invitedUser; let guild; @@ -54,6 +17,7 @@ describe('POST /group/:groupId/reject-invite', () => { privacy: 'private', }, invites: 1, + upgradeToGroupPlan: true, }); guild = group; diff --git a/test/api/v3/integration/groups/POST-groups_id_removeMember.test.js b/test/api/v3/integration/groups/POST-groups_id_removeMember.test.js index c8a903d891..6e8e7822e1 100644 --- a/test/api/v3/integration/groups/POST-groups_id_removeMember.test.js +++ b/test/api/v3/integration/groups/POST-groups_id_removeMember.test.js @@ -25,6 +25,7 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => { }, invites: 1, members: 2, + upgradeToGroupPlan: true, }); guild = group; @@ -129,9 +130,11 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => { it('sends email to removed user', async () => { await leader.post(`/groups/${guild._id}/removeMember/${member._id}`); - expect(email.sendTxn).to.be.calledOnce; + expect(email.sendTxn).to.be.calledTwice; expect(email.sendTxn.args[0][0]._id).to.eql(member._id); expect(email.sendTxn.args[0][1]).to.eql('kicked-from-guild'); + expect(email.sendTxn.args[1][0]._id).to.eql(member._id); + expect(email.sendTxn.args[1][1]).to.eql('group-member-removed'); }); }); diff --git a/test/api/v3/integration/groups/POST-groups_invite.test.js b/test/api/v3/integration/groups/POST-groups_invite.test.js index 179e6dfdee..0001e09e3a 100644 --- a/test/api/v3/integration/groups/POST-groups_invite.test.js +++ b/test/api/v3/integration/groups/POST-groups_invite.test.js @@ -3,7 +3,6 @@ import nconf from 'nconf'; import { createAndPopulateGroup, generateUser, - generateGroup, translate as t, } from '../../../../helpers/api-integration/v3'; @@ -14,13 +13,13 @@ const MAX_EMAIL_INVITES_BY_USER = 200; describe('Post /groups/:groupId/invite', () => { let inviter; let group; - const groupName = 'Test Public Guild'; + const groupName = 'Test Party'; beforeEach(async () => { inviter = await generateUser({ balance: 4 }); group = await inviter.post('/groups', { name: groupName, - type: 'guild', + type: 'party', }); }); @@ -65,45 +64,44 @@ describe('Post /groups/:groupId/invite', () => { it('invites a user to a group by username', async () => { const userToInvite = await generateUser(); - await expect(inviter.post(`/groups/${group._id}/invite`, { + const response = await inviter.post(`/groups/${group._id}/invite`, { usernames: [userToInvite.auth.local.lowerCaseUsername], - })).to.eventually.deep.equal([{ - id: group._id, - name: groupName, - inviter: inviter._id, - publicGuild: false, - }]); + }); + expect(response).to.be.an('Array'); + expect(response[0]).to.have.all.keys(['_id', 'id', 'name', 'inviter']); + expect(response[0]._id).to.be.a('String'); + expect(response[0].id).to.eql(group._id); + expect(response[0].name).to.eql(groupName); + expect(response[0].inviter).to.eql(inviter._id); await expect(userToInvite.get('/user')) - .to.eventually.have.nested.property('invitations.guilds[0].id', group._id); + .to.eventually.have.nested.property('invitations.parties[0].id', group._id); }); it('invites multiple users to a group by uuid', async () => { const userToInvite = await generateUser(); const userToInvite2 = await generateUser(); - await expect(inviter.post(`/groups/${group._id}/invite`, { + const response = await (inviter.post(`/groups/${group._id}/invite`, { usernames: [ userToInvite.auth.local.lowerCaseUsername, userToInvite2.auth.local.lowerCaseUsername, ], - })).to.eventually.deep.equal([ - { - id: group._id, - name: groupName, - inviter: inviter._id, - publicGuild: false, - }, - { - id: group._id, - name: groupName, - inviter: inviter._id, - publicGuild: false, - }, - ]); + })); + expect(response).to.be.an('Array'); + expect(response[0]).to.have.all.keys(['_id', 'id', 'name', 'inviter']); + expect(response[0]._id).to.be.a('String'); + expect(response[0].id).to.eql(group._id); + expect(response[0].name).to.eql(groupName); + expect(response[0].inviter).to.eql(inviter._id); + expect(response[1]).to.have.all.keys(['_id', 'id', 'name', 'inviter']); + expect(response[1]._id).to.be.a('String'); + expect(response[1].id).to.eql(group._id); + expect(response[1].name).to.eql(groupName); + expect(response[1].inviter).to.eql(inviter._id); - await expect(userToInvite.get('/user')).to.eventually.have.nested.property('invitations.guilds[0].id', group._id); - await expect(userToInvite2.get('/user')).to.eventually.have.nested.property('invitations.guilds[0].id', group._id); + await expect(userToInvite.get('/user')).to.eventually.have.nested.property('invitations.parties[0].id', group._id); + await expect(userToInvite2.get('/user')).to.eventually.have.nested.property('invitations.parties[0].id', group._id); }); }); @@ -214,42 +212,42 @@ describe('Post /groups/:groupId/invite', () => { it('invites a user to a group by uuid', async () => { const userToInvite = await generateUser(); - await expect(inviter.post(`/groups/${group._id}/invite`, { + const response = await inviter.post(`/groups/${group._id}/invite`, { uuids: [userToInvite._id], - })).to.eventually.deep.equal([{ - id: group._id, - name: groupName, - inviter: inviter._id, - publicGuild: false, - }]); + }); + expect(response).to.be.an('Array'); + expect(response[0]).to.have.all.keys(['_id', 'id', 'name', 'inviter']); + expect(response[0]._id).to.be.a('String'); + expect(response[0].id).to.eql(group._id); + expect(response[0].name).to.eql(groupName); + expect(response[0].inviter).to.eql(inviter._id); await expect(userToInvite.get('/user')) - .to.eventually.have.nested.property('invitations.guilds[0].id', group._id); + .to.eventually.have.nested.property('invitations.parties[0].id', group._id); }); it('invites multiple users to a group by uuid', async () => { const userToInvite = await generateUser(); const userToInvite2 = await generateUser(); - await expect(inviter.post(`/groups/${group._id}/invite`, { + const response = await inviter.post(`/groups/${group._id}/invite`, { uuids: [userToInvite._id, userToInvite2._id], - })).to.eventually.deep.equal([ - { - id: group._id, - name: groupName, - inviter: inviter._id, - publicGuild: false, - }, - { - id: group._id, - name: groupName, - inviter: inviter._id, - publicGuild: false, - }, - ]); + }); - await expect(userToInvite.get('/user')).to.eventually.have.nested.property('invitations.guilds[0].id', group._id); - await expect(userToInvite2.get('/user')).to.eventually.have.nested.property('invitations.guilds[0].id', group._id); + expect(response).to.be.an('Array'); + expect(response[0]).to.have.all.keys(['_id', 'id', 'name', 'inviter']); + expect(response[0]._id).to.be.a('String'); + expect(response[0].id).to.eql(group._id); + expect(response[0].name).to.eql(groupName); + expect(response[0].inviter).to.eql(inviter._id); + expect(response[1]).to.have.all.keys(['_id', 'id', 'name', 'inviter']); + expect(response[1]._id).to.be.a('String'); + expect(response[1].id).to.eql(group._id); + expect(response[1].name).to.eql(groupName); + expect(response[1].inviter).to.eql(inviter._id); + + await expect(userToInvite.get('/user')).to.eventually.have.nested.property('invitations.parties[0].id', group._id); + await expect(userToInvite2.get('/user')).to.eventually.have.nested.property('invitations.parties[0].id', group._id); }); it('returns an error when inviting multiple users and a user is not found', async () => { @@ -338,12 +336,8 @@ describe('Post /groups/:groupId/invite', () => { invitesSent: MAX_EMAIL_INVITES_BY_USER, balance: 4, }); - const tmpGroup = await inviterWithMax.post('/groups', { - name: groupName, - type: 'guild', - }); - await expect(inviterWithMax.post(`/groups/${tmpGroup._id}/invite`, { + await expect(inviterWithMax.post(`/groups/${group._id}/invite`, { emails: [testInvite], inviter: 'inviter name', })) @@ -419,15 +413,15 @@ describe('Post /groups/:groupId/invite', () => { }); const invitedUser = await newUser.get('/user'); - expect(invitedUser.invitations.guilds[0].id).to.equal(group._id); + expect(invitedUser.invitations.parties[0].id).to.equal(group._id); expect(invite).to.exist; }); - it('invites marks invite with cancelled plan', async () => { - const cancelledPlanGroup = await generateGroup(inviter, { - type: 'guild', - name: generateUUID(), - }); + it('invites user to group with cancelled plan', async () => { + let cancelledPlanGroup; + ({ group: cancelledPlanGroup, groupLeader: inviter } = await createAndPopulateGroup({ + upgradeToGroupPlan: true, + })); await cancelledPlanGroup.createCancelledSubscription(); const newUser = await generateUser(); @@ -437,13 +431,13 @@ describe('Post /groups/:groupId/invite', () => { }); const invitedUser = await newUser.get('/user'); - expect(invitedUser.invitations.guilds[0].id).to.equal(cancelledPlanGroup._id); - expect(invitedUser.invitations.guilds[0].cancelledPlan).to.be.true; + expect(invitedUser.invitations.parties[0].id).to.equal(cancelledPlanGroup._id); + expect(invitedUser.invitations.parties[0].cancelledPlan).to.be.true; expect(invite).to.exist; }); }); - describe('guild invites', () => { + describe('party invites', () => { it('returns an error when inviter has no chat privileges', async () => { const inviterMuted = await inviter.update({ 'flags.chatRevoked': true }); const userToInvite = await generateUser(); @@ -457,103 +451,13 @@ describe('Post /groups/:groupId/invite', () => { }); }); - it('returns an error when invited user is already invited to the group', async () => { - const userToInvite = await generateUser(); - await inviter.post(`/groups/${group._id}/invite`, { - uuids: [userToInvite._id], - }); - - await expect(inviter.post(`/groups/${group._id}/invite`, { - uuids: [userToInvite._id], - })) - .to.eventually.be.rejected.and.eql({ - code: 401, - error: 'NotAuthorized', - message: t('userAlreadyInvitedToGroup', { userId: userToInvite._id, username: userToInvite.profile.name }), - }); - }); - - it('returns an error when invited user is already in the group', async () => { - const userToInvite = await generateUser(); - await inviter.post(`/groups/${group._id}/invite`, { - uuids: [userToInvite._id], - }); - await userToInvite.post(`/groups/${group._id}/join`); - - await expect(inviter.post(`/groups/${group._id}/invite`, { - uuids: [userToInvite._id], - })) - .to.eventually.be.rejected.and.eql({ - code: 401, - error: 'NotAuthorized', - message: t('userAlreadyInGroup', { userId: userToInvite._id, username: userToInvite.profile.name }), - }); - }); - - it('allows 30+ members in a guild', async () => { - const invitesToGenerate = []; - // Generate 30 users to invite (30 + leader = 31 members) - for (let i = 0; i < PARTY_LIMIT_MEMBERS; i += 1) { - invitesToGenerate.push(generateUser()); - } - const generatedInvites = await Promise.all(invitesToGenerate); - // Invite users - expect(await inviter.post(`/groups/${group._id}/invite`, { - uuids: generatedInvites.map(invite => invite._id), - })).to.be.an('array'); - }).timeout(10000); - - // @TODO: Add this after we are able to mock the group plan route - xit('returns an error when a non-leader invites to a group plan', async () => { - const userToInvite = await generateUser(); - - const nonGroupLeader = await generateUser(); - await inviter.post(`/groups/${group._id}/invite`, { - uuids: [nonGroupLeader._id], - }); - await nonGroupLeader.post(`/groups/${group._id}/join`); - - await expect(nonGroupLeader.post(`/groups/${group._id}/invite`, { - uuids: [userToInvite._id], - })) - .to.eventually.be.rejected.and.eql({ - code: 401, - error: 'NotAuthorized', - message: t('onlyGroupLeaderCanInviteToGroupPlan'), - }); - }); - }); - - describe('party invites', () => { - let party; - - beforeEach(async () => { - party = await inviter.post('/groups', { - name: 'Test Party', - type: 'party', - }); - }); - - it('returns an error when inviter has no chat privileges', async () => { - const inviterMuted = await inviter.update({ 'flags.chatRevoked': true }); - const userToInvite = await generateUser(); - await expect(inviterMuted.post(`/groups/${party._id}/invite`, { - uuids: [userToInvite._id], - })) - .to.eventually.be.rejected.and.eql({ - code: 401, - error: 'NotAuthorized', - message: t('chatPrivilegesRevoked'), - }); - }); - it('returns an error when invited user has a pending invitation to the party', async () => { const userToInvite = await generateUser(); - await inviter.post(`/groups/${party._id}/invite`, { + await inviter.post(`/groups/${group._id}/invite`, { uuids: [userToInvite._id], }); - await expect(inviter.post(`/groups/${party._id}/invite`, { + await expect(inviter.post(`/groups/${group._id}/invite`, { uuids: [userToInvite._id], })) .to.eventually.be.rejected.and.eql({ @@ -566,13 +470,13 @@ describe('Post /groups/:groupId/invite', () => { it('returns an error when invited user is already in a party of more than 1 member', async () => { const userToInvite = await generateUser(); const userToInvite2 = await generateUser(); - await inviter.post(`/groups/${party._id}/invite`, { + await inviter.post(`/groups/${group._id}/invite`, { uuids: [userToInvite._id, userToInvite2._id], }); - await userToInvite.post(`/groups/${party._id}/join`); - await userToInvite2.post(`/groups/${party._id}/join`); + await userToInvite.post(`/groups/${group._id}/join`); + await userToInvite2.post(`/groups/${group._id}/join`); - await expect(inviter.post(`/groups/${party._id}/invite`, { + await expect(inviter.post(`/groups/${group._id}/invite`, { uuids: [userToInvite._id], })) .to.eventually.be.rejected.and.eql({ @@ -596,7 +500,7 @@ describe('Post /groups/:groupId/invite', () => { }); // Invite to first party - await inviter.post(`/groups/${party._id}/invite`, { + await inviter.post(`/groups/${group._id}/invite`, { uuids: [userToInvite._id], }); @@ -609,28 +513,27 @@ describe('Post /groups/:groupId/invite', () => { const invitedUser = await userToInvite.get('/user'); expect(invitedUser.invitations.parties.length).to.equal(2); - expect(invitedUser.invitations.parties[0].id).to.equal(party._id); + expect(invitedUser.invitations.parties[0].id).to.equal(group._id); expect(invitedUser.invitations.parties[1].id).to.equal(party2._id); }); - it('allow inviting a user if party id is not associated with a real party', async () => { + it('allows inviting a user if party id is not associated with a real party', async () => { const userToInvite = await generateUser({ party: { _id: generateUUID() }, }); - await inviter.post(`/groups/${party._id}/invite`, { + await inviter.post(`/groups/${group._id}/invite`, { uuids: [userToInvite._id], }); - expect((await userToInvite.get('/user')).invitations.parties[0].id).to.equal(party._id); + expect((await userToInvite.get('/user')).invitations.parties[0].id).to.equal(group._id); }); }); describe('party size limits', () => { - let party; let partyLeader; beforeEach(async () => { - group = await createAndPopulateGroup({ + ({ group, groupLeader: partyLeader } = await createAndPopulateGroup({ groupDetails: { name: 'Test Party', type: 'party', @@ -638,9 +541,7 @@ describe('Post /groups/:groupId/invite', () => { }, // Generate party with 20 members members: PARTY_LIMIT_MEMBERS - 10, - }); - party = group.group; - partyLeader = group.groupLeader; + })); }); it('allows 30 members in a party', async () => { @@ -651,7 +552,7 @@ describe('Post /groups/:groupId/invite', () => { } const generatedInvites = await Promise.all(invitesToGenerate); // Invite users - expect(await partyLeader.post(`/groups/${party._id}/invite`, { + expect(await partyLeader.post(`/groups/${group._id}/invite`, { uuids: generatedInvites.map(invite => invite._id), })).to.be.an('array'); }).timeout(10000); @@ -664,7 +565,7 @@ describe('Post /groups/:groupId/invite', () => { } const generatedInvites = await Promise.all(invitesToGenerate); // Invite users - await expect(partyLeader.post(`/groups/${party._id}/invite`, { + await expect(partyLeader.post(`/groups/${group._id}/invite`, { uuids: generatedInvites.map(invite => invite._id), })) .to.eventually.be.rejected.and.eql({ diff --git a/test/api/v3/integration/groups/POST-groups_manager.test.js b/test/api/v3/integration/groups/POST-groups_manager.test.js index e7b6cad9de..c191cc4d17 100644 --- a/test/api/v3/integration/groups/POST-groups_manager.test.js +++ b/test/api/v3/integration/groups/POST-groups_manager.test.js @@ -17,9 +17,10 @@ describe('POST /group/:groupId/add-manager', () => { groupDetails: { name: groupName, type: groupType, - privacy: 'public', + privacy: 'private', }, members: 1, + upgradeToGroupPlan: true, }); groupToUpdate = group; diff --git a/test/api/v3/integration/groups/PUT-groups.test.js b/test/api/v3/integration/groups/PUT-groups.test.js index 77d4ad6487..e584703fea 100644 --- a/test/api/v3/integration/groups/PUT-groups.test.js +++ b/test/api/v3/integration/groups/PUT-groups.test.js @@ -23,10 +23,11 @@ describe('PUT /group', () => { groupDetails: { name: groupName, type: groupType, - privacy: 'public', + privacy: 'private', categories: groupCategories, }, members: 1, + upgradeToGroupPlan: true, }); adminUser = await generateUser({ 'permissions.moderator': true }); groupToUpdate = group; @@ -106,14 +107,28 @@ describe('PUT /group', () => { expect(updatedGroup.name).to.equal(groupUpdatedName); }); - it('allows a leader to change leaders', async () => { - const updatedGroup = await leader.put(`/groups/${groupToUpdate._id}`, { + it('does not allow a leader to change leader of active group plan', async () => { + await expect(leader.put(`/groups/${groupToUpdate._id}`, { name: groupUpdatedName, leader: nonLeader._id, + })).to.eventually.be.rejected.and.eql({ + code: 401, + error: 'NotAuthorized', + message: t('cannotChangeLeaderWithActiveGroupPlan'), + }); + }); + + it('allows a leader of a party to change leaders', async () => { + const { group: party, groupLeader: partyLeader, members } = await createAndPopulateGroup({ + members: 1, + }); + const updatedGroup = await partyLeader.put(`/groups/${party._id}`, { + name: groupUpdatedName, + leader: members[0]._id, }); - expect(updatedGroup.leader._id).to.eql(nonLeader._id); - expect(updatedGroup.leader.profile.name).to.eql(nonLeader.profile.name); + expect(updatedGroup.leader._id).to.eql(members[0]._id); + expect(updatedGroup.leader.profile.name).to.eql(members[0].profile.name); expect(updatedGroup.name).to.equal(groupUpdatedName); }); @@ -122,15 +137,16 @@ describe('PUT /group', () => { groupDetails: { name: 'public guild', type: 'guild', - privacy: 'public', + privacy: 'private', }, + upgradeToGroupPlan: true, }); const updateGroupDetails = { id: group._id, name: 'public guild', type: 'guild', - privacy: 'public', + privacy: 'private', bannedWordsAllowed: true, }; @@ -150,9 +166,11 @@ describe('PUT /group', () => { groupDetails: { name: 'public guild', type: 'guild', - privacy: 'public', + privacy: 'private', }, + upgradeToGroupPlan: true, }); + await groupLeader.update({ permissions: {} }); const updateGroupDetails = { id: group._id, diff --git a/test/api/v3/integration/payments/amazon/GET-payments_amazon_subscribe_cancel.test.js b/test/api/v3/integration/payments/amazon/GET-payments_amazon_subscribe_cancel.test.js index 67313693be..7c2466f0f2 100644 --- a/test/api/v3/integration/payments/amazon/GET-payments_amazon_subscribe_cancel.test.js +++ b/test/api/v3/integration/payments/amazon/GET-payments_amazon_subscribe_cancel.test.js @@ -1,6 +1,6 @@ import { + createAndPopulateGroup, generateUser, - generateGroup, translate as t, } from '../../../../../helpers/api-integration/v3'; import amzLib from '../../../../../../website/server/libs/payments/amazon'; @@ -50,22 +50,21 @@ describe('payments : amazon #subscribeCancel', () => { }); it('cancels a group subscription', async () => { - user = await generateUser({ - 'profile.name': 'sender', - 'purchased.plan.customerId': 'customer-id', - 'purchased.plan.planId': 'basic_3mo', - 'purchased.plan.lastBillingDate': new Date(), - balance: 2, - }); - - group = await generateGroup(user, { - name: 'test group', - type: 'guild', - privacy: 'public', - 'purchased.plan.customerId': 'customer-id', - 'purchased.plan.planId': 'basic_3mo', - 'purchased.plan.lastBillingDate': new Date(), - }); + ({ group, groupLeader: user } = await createAndPopulateGroup({ + groupDetails: { + name: 'test group', + type: 'guild', + privacy: 'private', + }, + leaderDetails: { + 'profile.name': 'sender', + 'purchased.plan.customerId': 'customer-id', + 'purchased.plan.planId': 'basic_3mo', + 'purchased.plan.lastBillingDate': new Date(), + balance: 2, + }, + upgradeToGroupPlan: true, + })); await user.get(`${endpoint}&groupId=${group._id}`); diff --git a/test/api/v3/integration/payments/amazon/POST-payments_amazon_subscribe.test.js b/test/api/v3/integration/payments/amazon/POST-payments_amazon_subscribe.test.js index b6c09848a3..3ce1a718e2 100644 --- a/test/api/v3/integration/payments/amazon/POST-payments_amazon_subscribe.test.js +++ b/test/api/v3/integration/payments/amazon/POST-payments_amazon_subscribe.test.js @@ -70,8 +70,8 @@ describe('payments - amazon - #subscribe', () => { group = await generateGroup(user, { name: 'test group', - type: 'guild', - privacy: 'public', + type: 'party', + privacy: 'private', 'purchased.plan.customerId': 'customer-id', 'purchased.plan.planId': 'basic_3mo', 'purchased.plan.lastBillingDate': new Date(), diff --git a/test/api/v3/integration/payments/stripe/GET-payments_stripe_subscribe_cancel.test.js b/test/api/v3/integration/payments/stripe/GET-payments_stripe_subscribe_cancel.test.js index d6ec1e0312..77098a1b46 100644 --- a/test/api/v3/integration/payments/stripe/GET-payments_stripe_subscribe_cancel.test.js +++ b/test/api/v3/integration/payments/stripe/GET-payments_stripe_subscribe_cancel.test.js @@ -1,7 +1,7 @@ import { generateUser, - generateGroup, translate as t, + createAndPopulateGroup, } from '../../../../../helpers/api-integration/v3'; import stripePayments from '../../../../../../website/server/libs/payments/stripe'; @@ -48,22 +48,21 @@ describe('payments - stripe - #subscribeCancel', () => { }); it('cancels a group subscription', async () => { - user = await generateUser({ - 'profile.name': 'sender', - 'purchased.plan.customerId': 'customer-id', - 'purchased.plan.planId': 'basic_3mo', - 'purchased.plan.lastBillingDate': new Date(), - balance: 2, - }); - - group = await generateGroup(user, { - name: 'test group', - type: 'guild', - privacy: 'public', - 'purchased.plan.customerId': 'customer-id', - 'purchased.plan.planId': 'basic_3mo', - 'purchased.plan.lastBillingDate': new Date(), - }); + ({ group, groupLeader: user } = await createAndPopulateGroup({ + groupDetails: { + name: 'test group', + type: 'guild', + privacy: 'private', + }, + leaderDetails: { + 'profile.name': 'sender', + 'purchased.plan.customerId': 'customer-id', + 'purchased.plan.planId': 'basic_3mo', + 'purchased.plan.lastBillingDate': new Date(), + balance: 2, + }, + upgradeToGroupPlan: true, + })); await user.get(`${endpoint}&groupId=${group._id}`); diff --git a/test/api/v3/integration/quests/POST-groups_groupId_quests_accept.test.js b/test/api/v3/integration/quests/POST-groups_groupId_quests_accept.test.js index deb105de7c..671ff002d4 100644 --- a/test/api/v3/integration/quests/POST-groups_groupId_quests_accept.test.js +++ b/test/api/v3/integration/quests/POST-groups_groupId_quests_accept.test.js @@ -53,6 +53,7 @@ describe('POST /groups/:groupId/quests/accept', () => { it('does not accept quest for a guild', async () => { const { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({ groupDetails: { type: 'guild', privacy: 'private' }, + upgradeToGroupPlan: true, }); await expect(guildLeader.post(`/groups/${guild._id}/quests/accept`)) diff --git a/test/api/v3/integration/quests/POST-groups_groupId_quests_force-start.test.js b/test/api/v3/integration/quests/POST-groups_groupId_quests_force-start.test.js index 2769b6c00f..48c9378105 100644 --- a/test/api/v3/integration/quests/POST-groups_groupId_quests_force-start.test.js +++ b/test/api/v3/integration/quests/POST-groups_groupId_quests_force-start.test.js @@ -43,6 +43,7 @@ describe('POST /groups/:groupId/quests/force-start', () => { it('does not force start quest for a guild', async () => { const { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({ groupDetails: { type: 'guild', privacy: 'private' }, + upgradeToGroupPlan: true, }); await expect(guildLeader.post(`/groups/${guild._id}/quests/force-start`)) diff --git a/test/api/v3/integration/quests/POST-groups_groupId_quests_invite.test.js b/test/api/v3/integration/quests/POST-groups_groupId_quests_invite.test.js index 547cbd44ae..e7dd9426b0 100644 --- a/test/api/v3/integration/quests/POST-groups_groupId_quests_invite.test.js +++ b/test/api/v3/integration/quests/POST-groups_groupId_quests_invite.test.js @@ -51,14 +51,13 @@ describe('POST /groups/:groupId/quests/invite/:questKey', () => { }); it('does not issue invites for Guilds', async () => { - const { group } = await createAndPopulateGroup({ - groupDetails: { type: 'guild', privacy: 'public' }, + const { group, groupLeader } = await createAndPopulateGroup({ + groupDetails: { type: 'guild', privacy: 'private' }, members: 1, + upgradeToGroupPlan: true, }); - const alternateGroup = group; - - await expect(leader.post(`/groups/${alternateGroup._id}/quests/invite/${PET_QUEST}`)).to.eventually.be.rejected.and.eql({ + await expect(groupLeader.post(`/groups/${group._id}/quests/invite/${PET_QUEST}`)).to.eventually.be.rejected.and.eql({ code: 401, error: 'NotAuthorized', message: t('guildQuestsNotSupported'), diff --git a/test/api/v3/integration/quests/POST-groups_groupid_quests_abort.test.js b/test/api/v3/integration/quests/POST-groups_groupid_quests_abort.test.js index 7a42166a2c..3e1bf567bd 100644 --- a/test/api/v3/integration/quests/POST-groups_groupid_quests_abort.test.js +++ b/test/api/v3/integration/quests/POST-groups_groupid_quests_abort.test.js @@ -52,6 +52,7 @@ describe('POST /groups/:groupId/quests/abort', () => { it('returns an error when group is a guild', async () => { const { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({ groupDetails: { type: 'guild', privacy: 'private' }, + upgradeToGroupPlan: true, }); await expect(guildLeader.post(`/groups/${guild._id}/quests/abort`)) diff --git a/test/api/v3/integration/quests/POST-groups_groupid_quests_cancel.test.js b/test/api/v3/integration/quests/POST-groups_groupid_quests_cancel.test.js index 7df6dbd0f4..77c5a5ec17 100644 --- a/test/api/v3/integration/quests/POST-groups_groupid_quests_cancel.test.js +++ b/test/api/v3/integration/quests/POST-groups_groupid_quests_cancel.test.js @@ -52,6 +52,7 @@ describe('POST /groups/:groupId/quests/cancel', () => { it('returns an error when group is a guild', async () => { const { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({ groupDetails: { type: 'guild', privacy: 'private' }, + upgradeToGroupPlan: true, }); await expect(guildLeader.post(`/groups/${guild._id}/quests/cancel`)) diff --git a/test/api/v3/integration/quests/POST-groups_groupid_quests_leave.test.js b/test/api/v3/integration/quests/POST-groups_groupid_quests_leave.test.js index 1e2f7d6983..87d37464c1 100644 --- a/test/api/v3/integration/quests/POST-groups_groupid_quests_leave.test.js +++ b/test/api/v3/integration/quests/POST-groups_groupid_quests_leave.test.js @@ -51,6 +51,7 @@ describe('POST /groups/:groupId/quests/leave', () => { it('returns an error when group is a guild', async () => { const { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({ groupDetails: { type: 'guild', privacy: 'private' }, + upgradeToGroupPlan: true, }); await expect(guildLeader.post(`/groups/${guild._id}/quests/leave`)) diff --git a/test/api/v3/integration/quests/POST-groups_groupid_quests_reject.test.js b/test/api/v3/integration/quests/POST-groups_groupid_quests_reject.test.js index 4f897d2b2b..e956c71664 100644 --- a/test/api/v3/integration/quests/POST-groups_groupid_quests_reject.test.js +++ b/test/api/v3/integration/quests/POST-groups_groupid_quests_reject.test.js @@ -53,6 +53,7 @@ describe('POST /groups/:groupId/quests/reject', () => { it('returns an error when group is a guild', async () => { const { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({ groupDetails: { type: 'guild', privacy: 'private' }, + upgradeToGroupPlan: true, }); await expect(guildLeader.post(`/groups/${guild._id}/quests/reject`)) diff --git a/test/api/v3/integration/tasks/groups/POST-tasks_move_taskId_to_position.test.js b/test/api/v3/integration/tasks/groups/POST-tasks_move_taskId_to_position.test.js index 3255f85206..113a2f3f3c 100644 --- a/test/api/v3/integration/tasks/groups/POST-tasks_move_taskId_to_position.test.js +++ b/test/api/v3/integration/tasks/groups/POST-tasks_move_taskId_to_position.test.js @@ -1,6 +1,5 @@ import { - generateUser, - generateGroup, + createAndPopulateGroup, } from '../../../../../helpers/api-integration/v3'; describe('POST group-tasks/:taskId/move/to/:position', () => { @@ -8,8 +7,12 @@ describe('POST group-tasks/:taskId/move/to/:position', () => { guild; beforeEach(async () => { - user = await generateUser({ balance: 1 }); - guild = await generateGroup(user, { type: 'guild' }, { 'purchased.plan.customerId': 'group-unlimited' }); + const { group, groupLeader } = await createAndPopulateGroup({ + groupDetails: { type: 'guild', privacy: 'private' }, + upgradeToGroupPlan: true, + }); + guild = group; + user = groupLeader; }); it('can move task to new position', async () => { diff --git a/test/api/v3/integration/user/DELETE-user.test.js b/test/api/v3/integration/user/DELETE-user.test.js index 5c176fe141..f0dd2018c6 100644 --- a/test/api/v3/integration/user/DELETE-user.test.js +++ b/test/api/v3/integration/user/DELETE-user.test.js @@ -1,5 +1,4 @@ import { - find, each, map, } from 'lodash'; @@ -198,95 +197,6 @@ describe('DELETE /user', () => { await expect(checkExistence('party', party._id)).to.eventually.eql(false); }); }); - - context('last member of a private guild', () => { - let privateGuild; - - beforeEach(async () => { - privateGuild = await generateGroup(user, { - type: 'guild', - privacy: 'private', - }); - }); - - it('deletes guild when user is the only member', async () => { - await user.del('/user', { - password, - }); - await expect(checkExistence('groups', privateGuild._id)).to.eventually.eql(false); - }); - }); - - context('groups user is leader of', () => { - let guild; let oldLeader; let - newLeader; - - beforeEach(async () => { - const { group, groupLeader, members } = await createAndPopulateGroup({ - groupDetails: { - type: 'guild', - privacy: 'public', - }, - members: 1, - }); - - guild = group; - newLeader = members[0]; // eslint-disable-line prefer-destructuring - oldLeader = groupLeader; - }); - - it('chooses new group leader for any group user was the leader of', async () => { - await oldLeader.del('/user', { - password, - }); - - const updatedGuild = await newLeader.get(`/groups/${guild._id}`); - - expect(updatedGuild.leader).to.exist; - expect(updatedGuild.leader._id).to.not.eql(oldLeader._id); - }); - }); - - context('groups user is a part of', () => { - let group1; let group2; let userToDelete; let - otherUser; - - beforeEach(async () => { - userToDelete = await generateUser({ balance: 10 }); - - group1 = await generateGroup(userToDelete, { - type: 'guild', - privacy: 'public', - }); - - const { group, members } = await createAndPopulateGroup({ - groupDetails: { - type: 'guild', - privacy: 'public', - }, - members: 3, - }); - - group2 = group; - otherUser = members[0]; // eslint-disable-line prefer-destructuring - - await userToDelete.post(`/groups/${group2._id}/join`); - }); - - it('removes user from all groups user was a part of', async () => { - await userToDelete.del('/user', { - password, - }); - - const updatedGroup1Members = await otherUser.get(`/groups/${group1._id}/members`); - const updatedGroup2Members = await otherUser.get(`/groups/${group2._id}/members`); - const userInGroup = find(updatedGroup2Members, member => member._id === userToDelete._id); - - expect(updatedGroup1Members).to.be.empty; - expect(updatedGroup2Members).to.not.be.empty; - expect(userInGroup).to.not.exist; - }); - }); }); context('user with Google auth', async () => { diff --git a/test/api/v3/integration/user/POST-user_purchase.test.js b/test/api/v3/integration/user/POST-user_purchase.test.js index 502bcb3790..0fd8871d2b 100644 --- a/test/api/v3/integration/user/POST-user_purchase.test.js +++ b/test/api/v3/integration/user/POST-user_purchase.test.js @@ -51,6 +51,7 @@ describe('POST /user/purchase/:type/:key', () => { type: 'guild', privacy: 'private', }, + upgradeToGroupPlan: true, }); await group.update({ 'leaderOnly.getGems': true, @@ -77,6 +78,7 @@ describe('POST /user/purchase/:type/:key', () => { privacy: 'private', }, members: 1, + upgradeToGroupPlan: true, }); await group.update({ 'leaderOnly.getGems': true, diff --git a/test/api/v3/integration/user/auth/POST-register_local.test.js b/test/api/v3/integration/user/auth/POST-register_local.test.js index 6e548051cd..d97751fd91 100644 --- a/test/api/v3/integration/user/auth/POST-register_local.test.js +++ b/test/api/v3/integration/user/auth/POST-register_local.test.js @@ -714,31 +714,6 @@ describe('POST /user/auth/local/register', () => { expect(user.invitations.party).to.eql({}); }); - - it('adds a user to a guild on an invite of type other than party', async () => { - const { group, groupLeader } = await createAndPopulateGroup({ - groupDetails: { type: 'guild', privacy: 'private' }, - }); - - const invite = encrypt(JSON.stringify({ - id: group._id, - inviter: groupLeader._id, - sentAt: Date.now(), - })); - - const user = await api.post(`/user/auth/local/register?groupInvite=${invite}`, { - username, - email, - password, - confirmPassword: password, - }); - - expect(user.invitations.guilds[0]).to.eql({ - id: group._id, - name: group.name, - inviter: groupLeader._id, - }); - }); }); context('successful login via api', () => { diff --git a/test/api/v4/user/auth/POST-register_local.test.js b/test/api/v4/user/auth/POST-register_local.test.js index 919b650e25..11836cd213 100644 --- a/test/api/v4/user/auth/POST-register_local.test.js +++ b/test/api/v4/user/auth/POST-register_local.test.js @@ -665,6 +665,7 @@ describe('POST /user/auth/local/register', () => { it('adds a user to a guild on an invite of type other than party', async () => { const { group, groupLeader } = await createAndPopulateGroup({ groupDetails: { type: 'guild', privacy: 'private' }, + upgradeToGroupPlan: true, }); const invite = encrypt(JSON.stringify({ diff --git a/test/helpers/api-integration/v3/object-generators.js b/test/helpers/api-integration/v3/object-generators.js index b48dd862a9..e3a7c723c9 100644 --- a/test/helpers/api-integration/v3/object-generators.js +++ b/test/helpers/api-integration/v3/object-generators.js @@ -127,6 +127,9 @@ export async function createAndPopulateGroup (settings = {}) { const upgradeToGroupPlan = settings.upgradeToGroupPlan || false; const { groupDetails } = settings; const leaderDetails = settings.leaderDetails || { balance: 10 }; + if (upgradeToGroupPlan) { + leaderDetails.permissions = { fullAccess: true }; + } const groupLeader = await generateUser(leaderDetails); const group = await generateGroup(groupLeader, groupDetails); diff --git a/test/helpers/api-integration/v4/object-generators.js b/test/helpers/api-integration/v4/object-generators.js index 59b0642b15..92a4c50748 100644 --- a/test/helpers/api-integration/v4/object-generators.js +++ b/test/helpers/api-integration/v4/object-generators.js @@ -120,6 +120,9 @@ export async function createAndPopulateGroup (settings = {}) { const upgradeToGroupPlan = settings.upgradeToGroupPlan || false; const { groupDetails } = settings; const leaderDetails = settings.leaderDetails || { balance: 10 }; + if (upgradeToGroupPlan) { + leaderDetails.permissions = { fullAccess: true }; + } const groupLeader = await generateUser(leaderDetails); const group = await generateGroup(groupLeader, groupDetails); diff --git a/website/client/package-lock.json b/website/client/package-lock.json index 2afcc09143..819d630fdb 100644 --- a/website/client/package-lock.json +++ b/website/client/package-lock.json @@ -13318,31 +13318,11 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, - "emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==" - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, - "loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - } - }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -13374,35 +13354,6 @@ "ansi-regex": "^5.0.1" } }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { - "has-flag": "^4.0.0" - } - }, - "vue-loader-v16": { - "version": "npm:vue-loader@16.8.3", - "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.8.3.tgz", - "integrity": "sha512-7vKN45IxsKxe5GcVCbc2qFU5aWzyiLrYJyUuMz4BQLKctCj/fmCa0w6fGiiQ2cLFetNcek1ppGJQDCup0c1hpA==", - "requires": { - "chalk": "^4.1.0", - "hash-sum": "^2.0.0", - "loader-utils": "^2.0.0" - }, - "dependencies": { - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - } - } - }, "wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -30707,6 +30658,85 @@ } } }, + "vue-loader-v16": { + "version": "npm:vue-loader@16.8.3", + "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.8.3.tgz", + "integrity": "sha512-7vKN45IxsKxe5GcVCbc2qFU5aWzyiLrYJyUuMz4BQLKctCj/fmCa0w6fGiiQ2cLFetNcek1ppGJQDCup0c1hpA==", + "optional": true, + "requires": { + "chalk": "^4.1.0", + "hash-sum": "^2.0.0", + "loader-utils": "^2.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "optional": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "optional": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "optional": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "optional": true + }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "optional": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "optional": true + }, + "loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "optional": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "optional": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "vue-mugen-scroll": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/vue-mugen-scroll/-/vue-mugen-scroll-0.2.6.tgz", diff --git a/website/client/public/static/npc/normal/pixel_border.png b/website/client/public/static/npc/normal/pixel_border.png new file mode 100644 index 0000000000..8e873389ca Binary files /dev/null and b/website/client/public/static/npc/normal/pixel_border.png differ diff --git a/website/client/src/app.vue b/website/client/src/app.vue index 95cb2ba425..5166789ed3 100644 --- a/website/client/src/app.vue +++ b/website/client/src/app.vue @@ -41,6 +41,7 @@