diff --git a/migrations/archive/2024/20240318_pet_group_achievements.js b/migrations/archive/2024/20240318_pet_group_achievements.js new file mode 100644 index 0000000000..3ff3bd4280 --- /dev/null +++ b/migrations/archive/2024/20240318_pet_group_achievements.js @@ -0,0 +1,89 @@ +/* eslint-disable no-console */ +const MIGRATION_NAME = '202403_pet_group_achievements'; +import { model as User } from '../../../website/server/models/user'; + +const progressCount = 1000; +let count = 0; + +async function updateUser (user) { + count++; + + let set = { + migration: MIGRATION_NAME, + }; + + if (user && user.items && user.items.pets) { + const pets = user.items.pets; + if (pets['GuineaPig-Zombie'] > 0 + && pets['GuineaPig-Skeleton'] > 0 + && pets['GuineaPig-Base'] > 0 + && pets['GuineaPig-Desert'] > 0 + && pets['GuineaPig-Red'] > 0 + && pets['GuineaPig-Shade'] > 0 + && pets['GuineaPig-White']> 0 + && pets['GuineaPig-Golden'] > 0 + && pets['GuineaPig-CottonCandyBlue'] > 0 + && pets['GuineaPig-CottonCandyPink'] > 0 + && pets['Squirrel-Zombie'] > 0 + && pets['Squirrel-Skeleton'] > 0 + && pets['Squirrel-Base'] > 0 + && pets['Squirrel-Desert'] > 0 + && pets['Squirrel-Red'] > 0 + && pets['Squirrel-Shade'] > 0 + && pets['Squirrel-White'] > 0 + && pets['Squirrel-Golden'] > 0 + && pets['Squirrel-CottonCandyBlue'] > 0 + && pets['Squirrel-CottonCandyPink'] > 0 + && pets['Rat-Zombie'] > 0 + && pets['Rat-Skeleton'] > 0 + && pets['Rat-Base'] > 0 + && pets['Rat-Desert'] > 0 + && pets['Rat-Red'] > 0 + && pets['Rat-Shade'] > 0 + && pets['Rat-White'] > 0 + && pets['Rat-Golden'] > 0 + && pets['Rat-CottonCandyBlue'] > 0 + && pets['Rat-CottonCandyPink'] > 0 ) { + set['achievements.rodentRuler'] = true; + + } + } + + if (count % progressCount === 0) console.warn(`${count} ${user._id}`); + + return await User.updateOne({ _id: user._id }, { $set: set }).exec(); +} + +export default async function processUsers () { + let query = { + migration: { $ne: MIGRATION_NAME }, + 'auth.timestamps.loggedin': { $gt: new Date('2024-02-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) + .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]._id, + }; + } + + await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop + } +}; diff --git a/migrations/users/full-stable.js b/migrations/users/full-stable.js index df29a1fd38..01d6e04464 100644 --- a/migrations/users/full-stable.js +++ b/migrations/users/full-stable.js @@ -51,7 +51,7 @@ async function updateUser (user) { if (count % progressCount === 0) console.warn(`${count} ${user._id}`); - return User.update({ _id: user._id }, { $set: set }).exec(); + return User.updateOne({ _id: user._id }, { $set: set }).exec(); } export default async function processUsers () { diff --git a/migrations/users/pi-day.js b/migrations/users/pi-day.js index dd3f7d4f3f..211ba92c69 100644 --- a/migrations/users/pi-day.js +++ b/migrations/users/pi-day.js @@ -3,7 +3,7 @@ import { v4 as uuid } from 'uuid'; import { model as User } from '../../website/server/models/user'; -const MIGRATION_NAME = '20230314_pi_day'; +const MIGRATION_NAME = '20240314_pi_day'; const progressCount = 1000; let count = 0; diff --git a/package-lock.json b/package-lock.json index ab5eaa9946..4c2d7160ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "habitica", - "version": "5.18.1", + "version": "5.19.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 8da888d298..6a34ac3530 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": "5.18.1", + "version": "5.19.0", "main": "./website/server/index.js", "dependencies": { "@babel/core": "^7.22.10", diff --git a/test/api/v3/integration/challenges/POST-challenge_flag.test.js b/test/api/v3/integration/challenges/POST-challenge_flag.test.js index adb2f37235..702d667e6b 100644 --- a/test/api/v3/integration/challenges/POST-challenge_flag.test.js +++ b/test/api/v3/integration/challenges/POST-challenge_flag.test.js @@ -7,6 +7,7 @@ import { describe('POST /challenges/:challengeId/flag', () => { let user; + let challengeGroup; let challenge; beforeEach(async () => { @@ -20,6 +21,7 @@ describe('POST /challenges/:challengeId/flag', () => { }); user = groupLeader; + challengeGroup = group; challenge = await generateChallenge(user, group); }); @@ -59,4 +61,19 @@ describe('POST /challenges/:challengeId/flag', () => { message: t('messageChallengeFlagAlreadyReported'), }); }); + + it('returns an error when user tries to flag an official challenge', async () => { + await user.updateOne({ + permissions: { + challengeAdmin: true, + }, + }); + challenge = await generateChallenge(user, challengeGroup, { official: true }); + await expect(user.post(`/challenges/${challenge._id}/flag`)) + .to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: t('messageChallengeFlagOfficial'), + }); + }); }); diff --git a/test/api/v3/integration/challenges/POST-challenges.test.js b/test/api/v3/integration/challenges/POST-challenges.test.js index e471e0c5f2..f74e7c7422 100644 --- a/test/api/v3/integration/challenges/POST-challenges.test.js +++ b/test/api/v3/integration/challenges/POST-challenges.test.js @@ -331,5 +331,71 @@ describe('POST /challenges', () => { expect(updatedChallenge.summary).to.eql(summary); }); + + it('sets categories for challenges', async () => { + const testCategory = { _id: '65c1172997c0b24600371ea9', slug: 'test', name: 'Test' }; + const challenge = await groupLeader.post('/challenges', { + group: group._id, + name: 'Test Challenge', + shortName: 'TC Label', + categories: [testCategory], + }); + + const updatedChallenge = await groupLeader.get(`/challenges/${challenge._id}`); + + expect(updatedChallenge.categories).to.eql([testCategory]); + }); + + it('does not set habitica_official category for non-admins', async () => { + const testCategory = { _id: '65c1172997c0b24600371ea9', slug: 'habitica_official', name: 'habitica_official' }; + await expect(groupLeader.post('/challenges', { + group: group._id, + name: 'Test Challenge', + shortName: 'TC Label', + categories: [testCategory], + })).to.eventually.be.rejected.and.eql({ + code: 401, + error: 'NotAuthorized', + message: t('noPrivAccess'), + }); + }); + + it('sets habitica_official category for admins', async () => { + await groupLeader.updateOne({ + permissions: { + challengeAdmin: true, + }, + }); + + const testCategory = { _id: '65c1172997c0b24600371ea9', slug: 'habitica_official', name: 'habitica_official' }; + const challenge = await groupLeader.post('/challenges', { + group: group._id, + name: 'Test Challenge', + shortName: 'TC Label', + categories: [testCategory], + }); + + const updatedChallenge = await groupLeader.get(`/challenges/${challenge._id}`); + expect(updatedChallenge.categories).to.eql([testCategory]); + }); + + it('sets official if the habitica_official category is set for admins', async () => { + await groupLeader.updateOne({ + permissions: { + challengeAdmin: true, + }, + }); + + const testCategory = { _id: '65c1172997c0b24600371ea9', slug: 'habitica_official', name: 'habitica_official' }; + const challenge = await groupLeader.post('/challenges', { + group: group._id, + name: 'Test Challenge', + shortName: 'TC Label', + categories: [testCategory], + }); + + const updatedChallenge = await groupLeader.get(`/challenges/${challenge._id}`); + expect(updatedChallenge.official).to.eql(true); + }); }); }); diff --git a/test/common/ops/buy/purchase.js b/test/common/ops/buy/purchase.js index 962175d2ae..8f70523d1c 100644 --- a/test/common/ops/buy/purchase.js +++ b/test/common/ops/buy/purchase.js @@ -197,7 +197,7 @@ describe('shared.ops.purchase', () => { it('purchases quest bundles', async () => { const startingBalance = user.balance; - const clock = sandbox.useFakeTimers(moment('2022-03-16').valueOf()); + const clock = sandbox.useFakeTimers(moment('2024-03-20').valueOf()); const type = 'bundles'; const key = 'cuddleBuddies'; const price = 1.75; diff --git a/website/client/src/components/challenges/challengeDetail.vue b/website/client/src/components/challenges/challengeDetail.vue index 12de0133b8..dac2c52a33 100644 --- a/website/client/src/components/challenges/challengeDetail.vue +++ b/website/client/src/components/challenges/challengeDetail.vue @@ -223,6 +223,7 @@