Skip to content

Commit

Permalink
Merge branch 'release' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
SabreCat committed Oct 9, 2023
2 parents 6a5097c + 565f38f commit 71e565e
Show file tree
Hide file tree
Showing 158 changed files with 9,078 additions and 4,347 deletions.
2 changes: 1 addition & 1 deletion habitica-images
Submodule habitica-images updated 55 files
+ achievements/achievement-duneBuddy2x.png
+ backgrounds/background_jack_o_lantern_stacks.png
+ backgrounds/background_monstrous_cave.png
+ backgrounds/background_spectral_candle_room.png
+ backgrounds/icons/icon_background_jack_o_lantern_stacks.png
+ backgrounds/icons/icon_background_monstrous_cave.png
+ backgrounds/icons/icon_background_spectral_candle_room.png
+ gear/armoire/head_armoire_blackSpookySorceryHat.png
+ gear/armoire/head_armoire_purpleSpookySorceryHat.png
+ gear/armoire/shop/shop_head_armoire_blackSpookySorceryHat.png
+ gear/armoire/shop/shop_head_armoire_purpleSpookySorceryHat.png
+ gear/armoire/shop/shop_weapon_armoire_ridingBroom.png
+ gear/armoire/weapon_armoire_ridingBroom.png
+ gear/events/fall/broad_armor_special_fall2023Healer.png
+ gear/events/fall/broad_armor_special_fall2023Mage.png
+ gear/events/fall/broad_armor_special_fall2023Rogue.png
+ gear/events/fall/broad_armor_special_fall2023Warrior.png
+ gear/events/fall/head_special_fall2023Healer.png
+ gear/events/fall/head_special_fall2023Mage.png
+ gear/events/fall/head_special_fall2023Rogue.png
+ gear/events/fall/head_special_fall2023Warrior.png
+ gear/events/fall/shield_special_fall2023Healer.png
+ gear/events/fall/shield_special_fall2023Rogue.png
+ gear/events/fall/shield_special_fall2023Warrior.png
+ gear/events/fall/shop/shop_armor_special_fall2023Healer.png
+ gear/events/fall/shop/shop_armor_special_fall2023Mage.png
+ gear/events/fall/shop/shop_armor_special_fall2023Rogue.png
+ gear/events/fall/shop/shop_armor_special_fall2023Warrior.png
+ gear/events/fall/shop/shop_head_special_fall2023Healer.png
+ gear/events/fall/shop/shop_head_special_fall2023Mage.png
+ gear/events/fall/shop/shop_head_special_fall2023Warrior.png
+ gear/events/fall/shop/shop_shield_special_fall2023Healer.png
+ gear/events/fall/shop/shop_shield_special_fall2023Rogue.png
+ gear/events/fall/shop/shop_shield_special_fall2023Warrior.png
+ gear/events/fall/shop/shop_weapon_special_fall2023Healer.png
+ gear/events/fall/shop/shop_weapon_special_fall2023Rogue.png
+ gear/events/fall/shop_head_special_fall2023Rogue.png
+ gear/events/fall/shop_weapon_special_fall2023Mage.png
+ gear/events/fall/shop_weapon_special_fall2023Warrior.png
+ gear/events/fall/slim_armor_special_fall2023Healer.png
+ gear/events/fall/slim_armor_special_fall2023Mage.png
+ gear/events/fall/slim_armor_special_fall2023Rogue.png
+ gear/events/fall/slim_armor_special_fall2023Warrior.png
+ gear/events/fall/weapon_special_fall2023Healer.png
+ gear/events/fall/weapon_special_fall2023Mage.png
+ gear/events/fall/weapon_special_fall2023Rogue.png
+ gear/events/fall/weapon_special_fall2023Warrior.png
+ gear/events/mystery_202310/broad_armor_mystery_202310.png
+ gear/events/mystery_202310/headAccessory_mystery_202310.png
+ gear/events/mystery_202310/head_mystery_202310.png
+ gear/events/mystery_202310/shop_armor_mystery_202310.png
+ gear/events/mystery_202310/shop_headAccessory_mystery_202310.png
+ gear/events/mystery_202310/shop_head_mystery_202310.png
+ gear/events/mystery_202310/shop_set_mystery_202310.png
+ gear/events/mystery_202310/slim_armor_mystery_202310.png
1,147 changes: 428 additions & 719 deletions package-lock.json

Large diffs are not rendered by default.

21 changes: 11 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "5.3.0",
"version": "5.7.0",
"main": "./website/server/index.js",
"dependencies": {
"@babel/core": "^7.22.5",
"@babel/core": "^7.22.10",
"@babel/preset-env": "^7.22.10",
"@babel/register": "^7.22.5",
"@google-cloud/trace-agent": "^7.1.2",
"@parse/node-apn": "^5.1.3",
"@parse/node-apn": "^5.2.3",
"@slack/webhook": "^6.1.0",
"accepts": "^1.3.8",
"amazon-payments": "^0.2.9",
Expand All @@ -17,7 +17,7 @@
"apple-auth": "^1.0.9",
"bcrypt": "^5.1.1",
"body-parser": "^1.20.2",
"bootstrap": "^4.6.0",
"bootstrap": "^4.6.2",
"compression": "^1.7.4",
"cookie-session": "^2.0.0",
"coupon-code": "^0.4.5",
Expand All @@ -31,11 +31,12 @@
"express-basic-auth": "^1.2.1",
"express-validator": "^5.2.0",
"glob": "^8.1.0",
"got": "^11.8.3",
"got": "^11.8.6",
"gulp": "^4.0.0",
"gulp-babel": "^8.0.0",
"gulp-imagemin": "^7.1.0",
"gulp-nodemon": "^2.5.0",
"nodemon": "^2.0.20",
"gulp.spritesmith": "^6.13.0",
"habitica-markdown": "^3.0.0",
"helmet": "^4.6.0",
Expand All @@ -49,12 +50,12 @@
"method-override": "^3.0.0",
"moment": "^2.29.4",
"moment-recur": "^1.0.7",
"mongoose": "^5.13.7",
"mongoose": "^5.13.20",
"morgan": "^1.10.0",
"nconf": "^0.12.0",
"node-gcm": "^1.0.5",
"on-headers": "^1.0.2",
"passport": "^0.5.0",
"passport": "^0.5.3",
"passport-facebook": "^3.0.0",
"passport-google-oauth2": "^0.2.0",
"passport-google-oauth20": "2.0.0",
Expand All @@ -67,12 +68,12 @@
"remove-markdown": "^0.5.0",
"rimraf": "^3.0.2",
"short-uuid": "^4.2.2",
"stripe": "^12.9.0",
"stripe": "^12.18.0",
"superagent": "^8.1.2",
"universal-analytics": "^0.5.3",
"useragent": "^2.1.9",
"uuid": "^9.0.0",
"validator": "^13.9.0",
"validator": "^13.11.0",
"vinyl-buffer": "^1.0.1",
"winston": "^3.10.0",
"winston-loggly-bulk": "^3.2.1",
Expand Down Expand Up @@ -110,7 +111,7 @@
"apidoc": "gulp apidoc"
},
"devDependencies": {
"axios": "^1.3.6",
"axios": "^1.4.0",
"chai": "^4.3.7",
"chai-as-promised": "^7.1.1",
"chai-moment": "^0.1.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { v4 as generateUUID } from 'uuid';
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';

describe('POST /members/:memberId/clear-flags', () => {
let reporter;
let admin;
let moderator;

beforeEach(async () => {
reporter = await generateUser();
admin = await generateUser({ permissions: { userSupport: true } });
moderator = await generateUser({ permissions: { moderator: true } });
await reporter.post(`/members/${admin._id}/flag`);
});

context('error cases', () => {
it('returns error when memberId is not a UUID', async () => {
await expect(moderator.post('/members/gribbly/clear-flags'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});

it('returns error when member with UUID is not found', async () => {
const randomId = generateUUID();

await expect(moderator.post(`/members/${randomId}/clear-flags`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('userWithIDNotFound', { userId: randomId }),
});
});

it('returns error when requesting user is not a moderator', async () => {
await expect(reporter.post(`/members/${admin._id}/clear-flags`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Only a moderator may clear reports from a profile.',
});
});
});

context('valid request', () => {
it('removes a single flag from user', async () => {
await expect(moderator.post(`/members/${admin._id}/clear-flags`)).to.eventually.be.ok;
const updatedTarget = await admin.get(`/hall/heroes/${admin._id}`);
expect(updatedTarget.profile.flags).to.eql({});
});

it('removes multiple flags from user', async () => {
await moderator.post(`/members/${admin._id}/flag`);
await expect(moderator.post(`/members/${admin._id}/clear-flags`)).to.eventually.be.ok;
const updatedTarget = await admin.get(`/hall/heroes/${admin._id}`);
expect(updatedTarget.profile.flags).to.eql({});
});
});
});
151 changes: 151 additions & 0 deletions test/api/v3/integration/members/POST-members_memberId_flag.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { v4 as generateUUID } from 'uuid';
import moment from 'moment';
import nconf from 'nconf';
import { IncomingWebhook } from '@slack/webhook';
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';

describe('POST /members/:memberId/flag', () => {
let reporter;
let target;

beforeEach(async () => {
reporter = await generateUser();
target = await generateUser({
'profile.blurb': 'Naughty Text',
'profile.imageUrl': 'https://evil.com/',
});
});

context('error cases', () => {
it('returns error when memberId is not a UUID', async () => {
await expect(reporter.post('/members/gribbly/flag'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});

it('returns error when member with UUID is not found', async () => {
const randomId = generateUUID();

await expect(reporter.post(`/members/${randomId}/flag`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('userWithIDNotFound', { userId: randomId }),
});
});

it('returns error when non-admin flags same profile twice', async () => {
await reporter.post(`/members/${target._id}/flag`);
await expect(reporter.post(`/members/${target._id}/flag`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'A profile can not be flagged more than once by the same user.',
});
});
});

context('valid request', () => {
let admin;
const comment = 'this profile is bad';
const source = 'Third Party Script';

beforeEach(async () => {
admin = await generateUser({ 'permissions.userSupport': true });
sandbox.stub(IncomingWebhook.prototype, 'send').returns(Promise.resolve());
});

afterEach(() => {
sandbox.restore();
});

it('adds flags object to target user', async () => {
await reporter.post(`/members/${target._id}/flag`);
const updatedTarget = await admin.get(`/hall/heroes/${target._id}`);
expect(updatedTarget.profile.flags[reporter._id]).to.have.all.keys([
'comment',
'source',
'timestamp',
]);
expect(moment(updatedTarget.profile.flags[reporter._id].timestamp).toDate()).to.be.a('date');
});

it('allows addition of a comment and source', async () => {
await reporter.post(`/members/${target._id}/flag`, {
comment,
source,
});
const updatedTarget = await admin.get(`/hall/heroes/${target._id}`);
expect(updatedTarget.profile.flags[reporter._id].comment).to.eql(comment);
expect(updatedTarget.profile.flags[reporter._id].source).to.eql(source);
});

it('allows moderator to flag twice', async () => {
const moderator = await generateUser({ 'permissions.moderator': true });
await moderator.post(`/members/${target._id}/flag`);
await expect(moderator.post(`/members/${target._id}/flag`)).to.eventually.be.ok;
});

it('allows multiple non-moderators to flag individually', async () => {
await admin.post(`/members/${target._id}/flag`);
await reporter.post(`/members/${target._id}/flag`);
const updatedTarget = await admin.get(`/hall/heroes/${target._id}`);
expect(updatedTarget.profile.flags[admin._id]).to.exist;
expect(updatedTarget.profile.flags[reporter._id]).to.exist;
});

it('sends a flag report to moderation Slack', async () => {
const BASE_URL = nconf.get('BASE_URL');
await reporter.post(`/members/${target._id}/flag`, {
comment,
source,
});

/* eslint-disable camelcase */
expect(IncomingWebhook.prototype.send).to.be.calledWith({
text: `@${reporter.auth.local.username} (${reporter._id}; language: ${reporter.preferences.language}) flagged @${target.auth.local.username}'s profile from ${source} and commented: ${comment}`,
attachments: [{
fallback: 'Flag Profile',
color: 'danger',
title: 'User Profile Report',
title_link: `${BASE_URL}/profile/${target._id}`,
text: `Display Name: ${target.profile.name}\n\nImage URL: ${target.profile.imageUrl}\n\nAbout: ${target.profile.blurb}`,
mrkdwn_in: [
'text',
],
}],
});
/* eslint-enable camelcase */
});

it('excludes empty fields when sending Slack message', async () => {
const BASE_URL = nconf.get('BASE_URL');
await reporter.post(`/members/${admin._id}/flag`, {
comment,
source,
});

/* eslint-disable camelcase */
expect(IncomingWebhook.prototype.send).to.be.calledWith({
text: `@${reporter.auth.local.username} (${reporter._id}; language: ${reporter.preferences.language}) flagged @${admin.auth.local.username}'s profile from ${source} and commented: ${comment}`,
attachments: [{
fallback: 'Flag Profile',
color: 'danger',
title: 'User Profile Report',
title_link: `${BASE_URL}/profile/${admin._id}`,
text: `Display Name: ${admin.profile.name}`,
mrkdwn_in: [
'text',
],
}],
});
/* eslint-enable camelcase */
});
});
});
44 changes: 38 additions & 6 deletions test/api/v4/user/POST-user_reset.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ describe('POST /user/reset', () => {
type: 'habit',
});

await user.post('/user/reset');
await user.post('/user/reset', {
password: 'password',
});
await user.sync();

await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
Expand All @@ -39,7 +41,9 @@ describe('POST /user/reset', () => {
type: 'daily',
});

await user.post('/user/reset');
await user.post('/user/reset', {
password: 'password',
});
await user.sync();

await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
Expand All @@ -57,7 +61,9 @@ describe('POST /user/reset', () => {
type: 'todo',
});

await user.post('/user/reset');
await user.post('/user/reset', {
password: 'password',
});
await user.sync();

await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
Expand All @@ -75,7 +81,9 @@ describe('POST /user/reset', () => {
type: 'reward',
});

await user.post('/user/reset');
await user.post('/user/reset', {
password: 'password',
});
await user.sync();

await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
Expand All @@ -87,6 +95,26 @@ describe('POST /user/reset', () => {
expect(user.tasksOrder.rewards).to.be.empty;
});

it('does not allow to reset if the password is missing', async () => {
await expect(user.post('/user/reset', {
password: '',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingPassword'),
});
});

it('does not allow to reset if the password is wrong', async () => {
await expect(user.post('/user/reset', {
password: 'passdw',
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('wrongPassword'),
});
});

it('does not delete challenge or group tasks', async () => {
const guild = await generateGroup(user, {}, { 'purchased.plan.customerId': 'group-unlimited' });
const challenge = await generateChallenge(user, guild);
Expand All @@ -102,7 +130,9 @@ describe('POST /user/reset', () => {
});
await user.post(`/tasks/${groupTask._id}/assign`, [user._id]);

await user.post('/user/reset');
await user.post('/user/reset', {
password: 'password',
});
await user.sync();

await user.put('/user', {
Expand Down Expand Up @@ -133,7 +163,9 @@ describe('POST /user/reset', () => {
},
});

await hero.post('/user/reset');
await user.post('/user/reset', {
password: 'password',
});

const heroRes = await admin.get(`/hall/heroes/${hero.auth.local.username}`);

Expand Down
Loading

0 comments on commit 71e565e

Please sign in to comment.