Compare commits

..

1 Commits

Author SHA1 Message Date
Phillip Thelen
a018588021 Banning a user now automatically hides all their posts 2022-12-09 13:02:49 +01:00
2607 changed files with 297140 additions and 59253 deletions

View File

@@ -1,10 +1,6 @@
/* eslint-disable import/no-commonjs */
module.exports = { module.exports = {
root: true, root: true,
extends: [ extends: [
'habitrpg/lib/node', 'habitrpg/lib/node'
], ],
rules: { }
'import/no-extraneous-dependencies': 'off',
},
};

View File

@@ -104,6 +104,42 @@ updates:
- 7.7.0 - 7.7.0
- 7.8.0 - 7.8.0
- 7.9.0 - 7.9.0
- dependency-name: "@storybook/addon-knobs"
versions:
- 6.1.17
- 6.1.18
- 6.1.20
- 6.1.21
- 6.2.2
- 6.2.3
- 6.2.7
- dependency-name: "@storybook/addon-links"
versions:
- 6.1.17
- 6.1.18
- 6.1.20
- 6.1.21
- 6.2.2
- 6.2.3
- 6.2.7
- dependency-name: "@storybook/vue"
versions:
- 6.1.17
- 6.1.18
- 6.1.20
- 6.1.21
- 6.2.2
- 6.2.3
- 6.2.7
- dependency-name: "@storybook/addon-actions"
versions:
- 6.1.17
- 6.1.18
- 6.1.20
- 6.1.21
- 6.2.2
- 6.2.3
- 6.2.7
- dependency-name: core-js - dependency-name: core-js
versions: versions:
- 3.10.0 - 3.10.0

1
.gitignore vendored
View File

@@ -40,7 +40,6 @@ yarn.lock
!.elasticbeanstalk/*.global.yml !.elasticbeanstalk/*.global.yml
/.vscode /.vscode
habitica.code-workspace
# webstorm fake webpack for path intellisense # webstorm fake webpack for path intellisense
webpack.webstorm.config webpack.webstorm.config

30
Dockerfile Normal file
View File

@@ -0,0 +1,30 @@
FROM node:14
ENV ADMIN_EMAIL admin@habitica.com
ENV EMAILS_COMMUNITY_MANAGER_EMAIL admin@habitica.com
ENV AMAZON_PAYMENTS_CLIENT_ID amzn1.application-oa2-client.68ed9e6904ef438fbc1bf86bf494056e
ENV AMAZON_PAYMENTS_SELLER_ID AMQ3SB4SG5E91
ENV AMPLITUDE_KEY e8d4c24b3d6ef3ee73eeba715023dd43
ENV BASE_URL https://habitica.com
ENV FACEBOOK_KEY 128307497299777
ENV GA_ID UA-33510635-1
ENV GOOGLE_CLIENT_ID 1035232791481-32vtplgnjnd1aufv3mcu1lthf31795fq.apps.googleusercontent.com
ENV LOGGLY_CLIENT_TOKEN ab5663bf-241f-4d14-8783-7d80db77089a
ENV NODE_ENV production
ENV STRIPE_PUB_KEY pk_85fQ0yMECHNfHTSsZoxZXlPSwSNfA
ENV APPLE_AUTH_CLIENT_ID 9Q9SMRMCNN.com.habitrpg.ios.Habitica
# Install global packages
RUN npm install -g gulp-cli mocha
# Clone Habitica repo and install dependencies
RUN mkdir -p /usr/src/habitrpg
WORKDIR /usr/src/habitrpg
RUN git clone --branch release --depth 1 https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
RUN git config --global url."https://".insteadOf git://
RUN npm set unsafe-perm true
RUN npm install
# Start Habitica
EXPOSE 80 8080 36612
CMD ["node", "./website/transpiled-babel/index.js"]

View File

@@ -86,6 +86,5 @@
"RATE_LIMITER_ENABLED": "false", "RATE_LIMITER_ENABLED": "false",
"REDIS_HOST": "aaabbbcccdddeeefff", "REDIS_HOST": "aaabbbcccdddeeefff",
"REDIS_PORT": "1234", "REDIS_PORT": "1234",
"REDIS_PASSWORD": "12345678", "REDIS_PASSWORD": "12345678"
"TRUSTED_DOMAINS": "localhost,habitica.com"
} }

View File

@@ -51,7 +51,7 @@ gulp.task('build:prepare-mongo', async () => {
console.log('MongoDB data folder is missing, setting up.'); // eslint-disable-line no-console console.log('MongoDB data folder is missing, setting up.'); // eslint-disable-line no-console
// use run-rs without --keep, kill it as soon as the replica set starts // use run-rs without --keep, kill it as soon as the replica set starts
const runRsProcess = spawn('run-rs', ['-v', '4.1.1', '-l', 'ubuntu1804', '--dbpath', 'mongodb-data', '--number', '1', '--quiet']); const runRsProcess = spawn('run-rs', ['-v', '4.2.8', '-l', 'ubuntu1804', '--dbpath', 'mongodb-data', '--number', '1', '--quiet']);
for await (const chunk of runRsProcess.stdout) { for await (const chunk of runRsProcess.stdout) {
const stringChunk = chunk.toString(); const stringChunk = chunk.toString();

View File

@@ -2,6 +2,7 @@ import mongoose from 'mongoose';
import nconf from 'nconf'; import nconf from 'nconf';
import repl from 'repl'; import repl from 'repl';
import gulp from 'gulp'; import gulp from 'gulp';
import logger from '../website/server/libs/logger';
import { import {
getDevelopmentConnectionUrl, getDevelopmentConnectionUrl,
getDefaultConnectionOptions, getDefaultConnectionOptions,
@@ -38,6 +39,10 @@ const improveRepl = context => {
mongoose.connect( mongoose.connect(
connectionUrl, connectionUrl,
mongooseOptions, mongooseOptions,
err => {
if (err) throw err;
logger.info('Connected with Mongoose');
},
); );
}; };

View File

@@ -59,14 +59,12 @@ gulp.task('test:prepare:mongo', cb => {
const mongooseOptions = getDefaultConnectionOptions(); const mongooseOptions = getDefaultConnectionOptions();
const connectionUrl = getDevelopmentConnectionUrl(TEST_DB_URI); const connectionUrl = getDevelopmentConnectionUrl(TEST_DB_URI);
mongoose.connect(connectionUrl, mongooseOptions) mongoose.connect(connectionUrl, mongooseOptions, err => {
.then(() => mongoose.connection.dropDatabase())
.then(() => mongoose.connection.close()).then(() => {
cb();
})
.catch(err => {
if (err) return cb(`Unable to connect to mongo database. Are you sure it's running? \n\n${err}`); if (err) return cb(`Unable to connect to mongo database. Are you sure it's running? \n\n${err}`);
throw err; return mongoose.connection.dropDatabase(err2 => {
if (err2) return cb(err2);
return mongoose.connection.close(cb);
});
}); });
}); });
@@ -118,10 +116,8 @@ gulp.task('test:common:safe', gulp.series('test:prepare:build', cb => {
pipe(runner); pipe(runner);
})); }));
gulp.task('test:content', gulp.series( gulp.task('test:content', gulp.series('test:prepare:build',
'test:prepare:build', runInChildProcess(CONTENT_TEST_COMMAND, LIMIT_MAX_BUFFER_OPTIONS)));
runInChildProcess(CONTENT_TEST_COMMAND, LIMIT_MAX_BUFFER_OPTIONS),
));
gulp.task('test:content:clean', cb => { gulp.task('test:content:clean', cb => {
pipe(exec(testBin(CONTENT_TEST_COMMAND), LIMIT_MAX_BUFFER_OPTIONS, () => cb())); pipe(exec(testBin(CONTENT_TEST_COMMAND), LIMIT_MAX_BUFFER_OPTIONS, () => cb()));
@@ -146,20 +142,16 @@ gulp.task('test:content:safe', gulp.series('test:prepare:build', cb => {
pipe(runner); pipe(runner);
})); }));
gulp.task( gulp.task('test:api:unit:run',
'test:api:unit:run', runInChildProcess(integrationTestCommand('test/api/unit', 'coverage/api-unit')));
runInChildProcess(integrationTestCommand('test/api/unit', 'coverage/api-unit')),
);
gulp.task('test:api:unit:watch', () => gulp.watch(['website/server/libs/*', 'test/api/unit/**/*', 'website/server/controllers/**/*'], gulp.series('test:api:unit:run', done => done()))); gulp.task('test:api:unit:watch', () => gulp.watch(['website/server/libs/*', 'test/api/unit/**/*', 'website/server/controllers/**/*'], gulp.series('test:api:unit:run', done => done())));
gulp.task('test:api-v3:integration', gulp.series( gulp.task('test:api-v3:integration', gulp.series('test:prepare:mongo',
'test:prepare:mongo',
runInChildProcess( runInChildProcess(
integrationTestCommand('test/api/v3/integration', 'coverage/api-v3-integration'), integrationTestCommand('test/api/v3/integration', 'coverage/api-v3-integration'),
LIMIT_MAX_BUFFER_OPTIONS, LIMIT_MAX_BUFFER_OPTIONS,
), )));
));
gulp.task('test:api-v3:integration:watch', () => gulp.watch([ gulp.task('test:api-v3:integration:watch', () => gulp.watch([
'website/server/controllers/api-v3/**/*', 'common/script/ops/*', 'website/server/libs/*.js', 'website/server/controllers/api-v3/**/*', 'common/script/ops/*', 'website/server/libs/*.js',
@@ -172,13 +164,11 @@ gulp.task('test:api-v3:integration:separate-server', runInChildProcess(
'LOAD_SERVER=0', 'LOAD_SERVER=0',
)); ));
gulp.task('test:api-v4:integration', gulp.series( gulp.task('test:api-v4:integration', gulp.series('test:prepare:mongo',
'test:prepare:mongo',
runInChildProcess( runInChildProcess(
integrationTestCommand('test/api/v4', 'api-v4-integration'), integrationTestCommand('test/api/v4', 'api-v4-integration'),
LIMIT_MAX_BUFFER_OPTIONS, LIMIT_MAX_BUFFER_OPTIONS,
), )));
));
gulp.task('test:api-v4:integration:separate-server', runInChildProcess( gulp.task('test:api-v4:integration:separate-server', runInChildProcess(
'mocha test/api/v4 --recursive --require ./test/helpers/start-server', 'mocha test/api/v4 --recursive --require ./test/helpers/start-server',

View File

@@ -3,6 +3,6 @@ module.exports = {
root: false, root: false,
rules: { rules: {
'no-console': 0, 'no-console': 0,
'no-use-before-define': ['error', { functions: false }], 'no-use-before-define': ['error', { functions: false }]
}, }
}; }

View File

@@ -61,7 +61,7 @@ async function updateUser (user) {
export default async function processUsers () { export default async function processUsers () {
let query = { let query = {
migration: {$ne: MIGRATION_NAME}, // migration: {$ne: MIGRATION_NAME},
'auth.timestamps.loggedin': {$gt: new Date('2021-01-01')}, 'auth.timestamps.loggedin': {$gt: new Date('2021-01-01')},
}; };

View File

@@ -105,7 +105,7 @@ async function updateUser (user) {
export default async function processUsers () { export default async function processUsers () {
let query = { let query = {
migration: { $ne: MIGRATION_NAME }, // migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2021-08-01') }, 'auth.timestamps.loggedin': { $gt: new Date('2021-08-01') },
}; };

View File

@@ -145,7 +145,7 @@ async function updateUser (user) {
export default async function processUsers () { export default async function processUsers () {
let query = { let query = {
migration: { $ne: MIGRATION_NAME }, // migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2021-08-01') }, 'auth.timestamps.loggedin': { $gt: new Date('2021-08-01') },
}; };

View File

@@ -105,7 +105,7 @@ async function updateUser (user) {
export default async function processUsers () { export default async function processUsers () {
let query = { let query = {
migration: { $ne: MIGRATION_NAME }, // migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2021-08-01') }, 'auth.timestamps.loggedin': { $gt: new Date('2021-08-01') },
}; };

View File

@@ -95,7 +95,7 @@ async function updateUser (user) {
export default async function processUsers () { export default async function processUsers () {
let query = { let query = {
migration: { $ne: MIGRATION_NAME }, // migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2022-01-01') }, 'auth.timestamps.loggedin': { $gt: new Date('2022-01-01') },
}; };

View File

@@ -86,7 +86,7 @@ async function updateUser (user) {
export default async function processUsers () { export default async function processUsers () {
let query = { let query = {
migration: { $ne: MIGRATION_NAME }, // migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2022-01-01') }, 'auth.timestamps.loggedin': { $gt: new Date('2022-01-01') },
}; };

View File

@@ -1,108 +0,0 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20221213_pet_group_achievements';
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
const set = {
migration: MIGRATION_NAME,
};
if (user && user.items && user.items.pets) {
const pets = user.items.pets;
if (pets['BearCub-Base']
&& pets['BearCub-CottonCandyBlue']
&& pets['BearCub-CottonCandyPink']
&& pets['BearCub-Desert']
&& pets['BearCub-Golden']
&& pets['BearCub-Red']
&& pets['BearCub-Shade']
&& pets['BearCub-Skeleton']
&& pets['BearCub-White']
&& pets['BearCub-Zombie']
&& pets['Fox-Base']
&& pets['Fox-CottonCandyBlue']
&& pets['Fox-CottonCandyPink']
&& pets['Fox-Desert']
&& pets['Fox-Golden']
&& pets['Fox-Red']
&& pets['Fox-Shade']
&& pets['Fox-Skeleton']
&& pets['Fox-White']
&& pets['Fox-Zombie']
&& pets['Penguin-Base']
&& pets['Penguin-CottonCandyBlue']
&& pets['Penguin-CottonCandyPink']
&& pets['Penguin-Desert']
&& pets['Penguin-Golden']
&& pets['Penguin-Red']
&& pets['Penguin-Shade']
&& pets['Penguin-Skeleton']
&& pets['Penguin-White']
&& pets['Penguin-Zombie']
&& pets['Whale-Base']
&& pets['Whale-CottonCandyBlue']
&& pets['Whale-CottonCandyPink']
&& pets['Whale-Desert']
&& pets['Whale-Golden']
&& pets['Whale-Red']
&& pets['Whale-Shade']
&& pets['Whale-Skeleton']
&& pets['Whale-White']
&& pets['Whale-Zombie']
&& pets['Wolf-Base']
&& pets['Wolf-CottonCandyBlue']
&& pets['Wolf-CottonCandyPink']
&& pets['Wolf-Desert']
&& pets['Wolf-Golden']
&& pets['Wolf-Red']
&& pets['Wolf-Shade']
&& pets['Wolf-Skeleton']
&& pets['Wolf-White']
&& pets['Wolf-Zombie']) {
set['achievements.polarPro'] = true;
}
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.update({ _id: user._id }, { $set: set }).exec();
}
export default async function processUsers () {
let query = {
migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2022-11-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
}
};

View File

@@ -1,144 +0,0 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20221227_nye';
import { model as User } from '../../../website/server/models/user';
import { v4 as uuid } from 'uuid';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
const set = { migration: MIGRATION_NAME };
let push;
if (typeof user.items.gear.owned.head_special_nye2021 !== 'undefined') {
set['items.gear.owned.head_special_nye2022'] = false;
push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_nye2022',
_id: uuid(),
},
];
} else if (typeof user.items.gear.owned.head_special_nye2020 !== 'undefined') {
set['items.gear.owned.head_special_nye2021'] = false;
push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_nye2021',
_id: uuid(),
},
];
} else if (typeof user.items.gear.owned.head_special_nye2019 !== 'undefined') {
set['items.gear.owned.head_special_nye2020'] = false;
push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_nye2020',
_id: uuid(),
},
];
} else if (typeof user.items.gear.owned.head_special_nye2018 !== 'undefined') {
set['items.gear.owned.head_special_nye2019'] = false;
push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_nye2019',
_id: uuid(),
},
];
} else if (typeof user.items.gear.owned.head_special_nye2017 !== 'undefined') {
set['items.gear.owned.head_special_nye2018'] = false;
push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_nye2018',
_id: uuid(),
},
];
} else if (typeof user.items.gear.owned.head_special_nye2016 !== 'undefined') {
set['items.gear.owned.head_special_nye2017'] = false;
push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_nye2017',
_id: uuid(),
},
];
} else if (typeof user.items.gear.owned.head_special_nye2015 !== 'undefined') {
set['items.gear.owned.head_special_nye2016'] = false;
push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_nye2016',
_id: uuid(),
},
];
} else if (typeof user.items.gear.owned.head_special_nye2014 !== 'undefined') {
set['items.gear.owned.head_special_nye2015'] = false;
push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_nye2015',
_id: uuid(),
},
];
} else if (typeof user.items.gear.owned.head_special_nye !== 'undefined') {
set['items.gear.owned.head_special_nye2014'] = false;
push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_nye2014',
_id: uuid(),
},
];
} else {
set['items.gear.owned.head_special_nye'] = false;
push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_nye',
_id: uuid(),
},
];
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.update({_id: user._id}, {$set: set, $push: {pinnedItems: {$each: push}}}).exec();
}
export default async function processUsers () {
let query = {
'auth.timestamps.loggedin': {$gt: new Date('2022-12-01')},
migration: {$ne: MIGRATION_NAME},
};
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],
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -1,88 +0,0 @@
/* eslint-disable no-console */
import { v4 as uuid } from 'uuid';
import { model as User } from '../../../website/server/models/user';
const MIGRATION_NAME = '20230123_habit_birthday';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count += 1;
const inc = { 'balance': 5 };
const set = {};
const push = {};
set.migration = MIGRATION_NAME;
if (typeof user.items.gear.owned.armor_special_birthday2022 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2023'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday2021 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2022'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday2020 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2021'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday2019 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2020'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday2018 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2019'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday2017 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2018'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday2016 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2017'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday2015 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2016'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday !== 'undefined') {
set['items.gear.owned.armor_special_birthday2015'] = true;
} else {
set['items.gear.owned.armor_special_birthday'] = true;
}
push.notifications = {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_head_special_nye',
title: 'Birthday Bash Day 1!',
text: 'Enjoy your new Birthday Robe and 20 Gems on us!',
destination: 'equipment',
},
seen: false,
};
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.update({_id: user._id}, {$inc: inc, $set: set, $push: push}).exec();
}
export default async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
'auth.timestamps.loggedin': {$gt: new Date('2022-12-23')},
};
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],
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -1,69 +0,0 @@
/* eslint-disable no-console */
import { v4 as uuid } from 'uuid';
import { model as User } from '../../../website/server/models/user';
const MIGRATION_NAME = '20230127_habit_birthday_day5';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count += 1;
const set = {};
const push = {};
set.migration = MIGRATION_NAME;
set['items.gear.owned.back_special_anniversary'] = true;
set['items.gear.owned.body_special_anniversary'] = true;
set['items.gear.owned.eyewear_special_anniversary'] = true;
push.notifications = {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_head_special_nye',
title: 'Birthday Bash Day 5!',
text: 'Come celebrate by wearing your new Habitica Hero Cape, Collar, and Mask!',
destination: '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('2022-12-23')},
};
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],
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -1,79 +0,0 @@
/* eslint-disable no-console */
import { v4 as uuid } from 'uuid';
import { model as User } from '../../../website/server/models/user';
const MIGRATION_NAME = '20230201_habit_birthday_day10';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count += 1;
const set = {
migration: MIGRATION_NAME,
'purchased.background.birthday_bash': true,
};
const push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_head_special_nye',
title: 'Birthday Bash Day 10!',
text: 'Join in for the end of our birthday celebrations with 10th Birthday background, Cake, and achievement!',
destination: 'backgrounds',
},
seen: false,
},
};
const inc = {
'items.food.Cake_Skeleton': 1,
'items.food.Cake_Base': 1,
'items.food.Cake_CottonCandyBlue': 1,
'items.food.Cake_CottonCandyPink': 1,
'items.food.Cake_Shade': 1,
'items.food.Cake_White': 1,
'items.food.Cake_Golden': 1,
'items.food.Cake_Zombie': 1,
'items.food.Cake_Desert': 1,
'items.food.Cake_Red': 1,
'achievements.habitBirthdays': 1,
};
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.update({_id: user._id}, {$set: set, $push: push, $inc: inc }).exec();
}
export default async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
'auth.timestamps.loggedin': {$gt: new Date('2022-12-23')},
};
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],
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -1,158 +0,0 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20230522_pet_group_achievements';
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
const set = {
migration: MIGRATION_NAME,
};
if (user && user.items && user.items.pets) {
const pets = user.items.pets;
if (pets['Parrot-Base']
&& pets['Parrot-CottonCandyBlue']
&& pets['Parrot-CottonCandyPink']
&& pets['Parrot-Desert']
&& pets['Parrot-Golden']
&& pets['Parrot-Red']
&& pets['Parrot-Shade']
&& pets['Parrot-Skeleton']
&& pets['Parrot-White']
&& pets['Parrot-Zombie']
&& pets['Rooster-Base']
&& pets['Rooster-CottonCandyBlue']
&& pets['Rooster-CottonCandyPink']
&& pets['Rooster-Desert']
&& pets['Rooster-Golden']
&& pets['Rooster-Red']
&& pets['Rooster-Shade']
&& pets['Rooster-Skeleton']
&& pets['Rooster-White']
&& pets['Rooster-Zombie']
&& pets['Triceratops-Base']
&& pets['Triceratops-CottonCandyBlue']
&& pets['Triceratops-CottonCandyPink']
&& pets['Triceratops-Desert']
&& pets['Triceratops-Golden']
&& pets['Triceratops-Red']
&& pets['Triceratops-Shade']
&& pets['Triceratops-Skeleton']
&& pets['Triceratops-White']
&& pets['Triceratops-Zombie']
&& pets['TRex-Base']
&& pets['TRex-CottonCandyBlue']
&& pets['TRex-CottonCandyPink']
&& pets['TRex-Desert']
&& pets['TRex-Golden']
&& pets['TRex-Red']
&& pets['TRex-Shade']
&& pets['TRex-Skeleton']
&& pets['TRex-White']
&& pets['TRex-Zombie']
&& pets['Pterodactyl-Base']
&& pets['Pterodactyl-CottonCandyBlue']
&& pets['Pterodactyl-CottonCandyPink']
&& pets['Pterodactyl-Desert']
&& pets['Pterodactyl-Golden']
&& pets['Pterodactyl-Red']
&& pets['Pterodactyl-Shade']
&& pets['Pterodactyl-Skeleton']
&& pets['Pterodactyl-White']
&& pets['Pterodactyl-Zombie']
&& pets['Owl-Base']
&& pets['Owl-CottonCandyBlue']
&& pets['Owl-CottonCandyPink']
&& pets['Owl-Desert']
&& pets['Owl-Golden']
&& pets['Owl-Red']
&& pets['Owl-Shade']
&& pets['Owl-Skeleton']
&& pets['Owl-White']
&& pets['Owl-Zombie']
&& pets['Velociraptor-Base']
&& pets['Velociraptor-CottonCandyBlue']
&& pets['Velociraptor-CottonCandyPink']
&& pets['Velociraptor-Desert']
&& pets['Velociraptor-Golden']
&& pets['Velociraptor-Red']
&& pets['Velociraptor-Shade']
&& pets['Velociraptor-Skeleton']
&& pets['Velociraptor-White']
&& pets['Velociraptor-Zombie']
&& pets['Penguin-Base']
&& pets['Penguin-CottonCandyBlue']
&& pets['Penguin-CottonCandyPink']
&& pets['Penguin-Desert']
&& pets['Penguin-Golden']
&& pets['Penguin-Red']
&& pets['Penguin-Shade']
&& pets['Penguin-Skeleton']
&& pets['Penguin-White']
&& pets['Penguin-Zombie']
&& pets['Falcon-Base']
&& pets['Falcon-CottonCandyBlue']
&& pets['Falcon-CottonCandyPink']
&& pets['Falcon-Desert']
&& pets['Falcon-Golden']
&& pets['Falcon-Red']
&& pets['Falcon-Shade']
&& pets['Falcon-Skeleton']
&& pets['Falcon-White']
&& pets['Falcon-Zombie']
&& pets['Peacock-Base']
&& pets['Peacock-CottonCandyBlue']
&& pets['Peacock-CottonCandyPink']
&& pets['Peacock-Desert']
&& pets['Peacock-Golden']
&& pets['Peacock-Red']
&& pets['Peacock-Shade']
&& pets['Peacock-Skeleton']
&& pets['Peacock-White']
&& pets['Peacock-Zombie']) {
set['achievements.dinosaurDynasty'] = true;
}
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.update({ _id: user._id }, { $set: set }).exec();
}
export default async function processUsers () {
let query = {
migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2023-04-15') },
};
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
}
};

View File

@@ -1,79 +0,0 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20230718_summer_splash_orcas';
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
const set = { migration: MIGRATION_NAME };
const push = {};
if (user && user.items && user.items.pets && typeof user.items.pets['Orca-Base'] !== 'undefined') {
return;
} else if (user && user.items && user.items.mounts && typeof user.items.mounts['Orca-Base'] !== 'undefined') {
set['items.pets.Orca-Base'] = 5;
push.notifications = {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_orca_pet',
title: 'Orcas for Summer Splash!',
text: 'To celebrate Summer Splash, we\'ve given you an Orca Pet!',
destination: 'stable',
},
seen: false,
};
} else {
set['items.mounts.Orca-Base'] = true;
push.notifications = {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_orca_mount',
title: 'Orcas for Summer Splash!',
text: 'To celebrate Summer Splash, we\'ve given you an Orca Mount!',
destination: 'stable',
},
seen: false,
};
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await user.updateOne({ $set: set, $push: push }).exec();
}
export default async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
'auth.timestamps.loggedin': {$gt: new Date('2023-06-18')},
};
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],
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -1,155 +0,0 @@
/* 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, weve 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, weve 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, weve 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, weve 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, weve 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, weve 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
}
};

View File

@@ -1,72 +0,0 @@
/* 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
}
};

View File

@@ -1,62 +0,0 @@
/* eslint-disable no-console */
import { model as User } from '../../../website/server/models/user';
import { TransactionModel as Transaction } from '../../../website/server/models/transaction';
const transactionsPerRun = 500;
const progressCount = 1000;
const transactionsQuery = {
transactionType: 'create_guild',
amount: { $gt: 0 },
};
let count = 0;
async function updateTransaction (transaction) {
count++;
if (count % progressCount === 0) {
console.warn(`${count} ${transaction._id}`);
}
const leader = await User
.findOne({ _id: transaction.userId })
.select({ _id: true })
.exec();
if (!leader) {
return console.warn(`User not found for transaction ${transaction._id}`);
}
return leader.updateOne(
{ $inc: { balance: transaction.amount }},
).exec();
}
export default async function processTransactions () {
const transactionFields = {
_id: 1,
userId: 1,
currency: 1,
amount: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const foundTransactions = await Transaction // eslint-disable-line no-await-in-loop
.find(transactionsQuery)
.limit(transactionsPerRun)
.sort({ _id: 1 })
.select(transactionFields)
.lean()
.exec();
if (foundTransactions.length === 0) {
console.warn('All appropriate transactions found and modified.');
console.warn(`\n${count} transactions processed\n`);
break;
} else {
transactionsQuery._id = {
$gt: foundTransactions[foundTransactions.length - 1],
};
}
await Promise.all(foundTransactions.map(txn => updateTransaction(txn))); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -1,144 +0,0 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20230808_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: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve 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: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve 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: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve 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: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve 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: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve 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: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve 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: 'Youve received the Heroic Set!',
text: 'To commemorate your hard work as a contributor, weve 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-08') },
};
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
}
};

View File

@@ -1,118 +0,0 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20231017_pet_group_achievements';
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
const set = {
migration: MIGRATION_NAME,
};
if (user && user.items && user.items.pets) {
const pets = user.items.pets;
if (pets['Armadillo-Base']
&& pets['Armadillo-CottonCandyBlue']
&& pets['Armadillo-CottonCandyPink']
&& pets['Armadillo-Desert']
&& pets['Armadillo-Golden']
&& pets['Armadillo-Red']
&& pets['Armadillo-Shade']
&& pets['Armadillo-Skeleton']
&& pets['Armadillo-White']
&& pets['Armadillo-Zombie']
&& pets['Cactus-Base']
&& pets['Cactus-CottonCandyBlue']
&& pets['Cactus-CottonCandyPink']
&& pets['Cactus-Desert']
&& pets['Cactus-Golden']
&& pets['Cactus-Red']
&& pets['Cactus-Shade']
&& pets['Cactus-Skeleton']
&& pets['Cactus-White']
&& pets['Cactus-Zombie']
&& pets['Fox-Base']
&& pets['Fox-CottonCandyBlue']
&& pets['Fox-CottonCandyPink']
&& pets['Fox-Desert']
&& pets['Fox-Golden']
&& pets['Fox-Red']
&& pets['Fox-Shade']
&& pets['Fox-Skeleton']
&& pets['Fox-White']
&& pets['Fox-Zombie']
&& pets['Frog-Base']
&& pets['Frog-CottonCandyBlue']
&& pets['Frog-CottonCandyPink']
&& pets['Frog-Desert']
&& pets['Frog-Golden']
&& pets['Frog-Red']
&& pets['Frog-Shade']
&& pets['Frog-Skeleton']
&& pets['Frog-White']
&& pets['Frog-Zombie']
&& pets['Snake-Base']
&& pets['Snake-CottonCandyBlue']
&& pets['Snake-CottonCandyPink']
&& pets['Snake-Desert']
&& pets['Snake-Golden']
&& pets['Snake-Red']
&& pets['Snake-Shade']
&& pets['Snake-Skeleton']
&& pets['Snake-White']
&& pets['Snake-Zombie']
&& pets['Spider-Base']
&& pets['Spider-CottonCandyBlue']
&& pets['Spider-CottonCandyPink']
&& pets['Spider-Desert']
&& pets['Spider-Golden']
&& pets['Spider-Red']
&& pets['Spider-Shade']
&& pets['Spider-Skeleton']
&& pets['Spider-White']
&& pets['Spider-Zombie']) {
set['achievements.duneBuddy'] = 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('2023-09-16') },
};
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
}
};

View File

@@ -1,124 +0,0 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20231114_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['Cactus-Zombie'] > 0
&& pets['Cactus-Skeleton'] > 0
&& pets['Cactus-Base'] > 0
&& pets['Cactus-Desert'] > 0
&& pets['Cactus-Red'] > 0
&& pets['Cactus-Shade'] > 0
&& pets['Cactus-White']> 0
&& pets['Cactus-Golden'] > 0
&& pets['Cactus-CottonCandyBlue'] > 0
&& pets['Cactus-CottonCandyPink'] > 0
&& pets['Hedgehog-Zombie'] > 0
&& pets['Hedgehog-Skeleton'] > 0
&& pets['Hedgehog-Base'] > 0
&& pets['Hedgehog-Desert'] > 0
&& pets['Hedgehog-Red'] > 0
&& pets['Hedgehog-Shade'] > 0
&& pets['Hedgehog-White'] > 0
&& pets['Hedgehog-Golder'] > 0
&& pets['Hedgehog-CottonCandyBlue'] > 0
&& pets['Hedgehog-CottonCandyPink'] > 0
&& pets['Rock-Zombie'] > 0
&& pets['Rock-Skeleton'] > 0
&& pets['Rock-Base'] > 0
&& pets['Rock-Desert'] > 0
&& pets['Rock-Red'] > 0
&& pets['Rock-Shade'] > 0
&& pets['Rock-White'] > 0
&& pets['Rock-Golden'] > 0
&& pets['Rock-CottonCandyBlue'] > 0
&& pets['Rock-CottonCandyPink'] > 0 ) {
set['achievements.roughRider'] = true;
}
}
if (user && user.items && user.items.mounts) {
const mounts = user.items.mounts;
if (mounts['Cactus-Zombie']
&& mounts['Cactus-Skeleton']
&& mounts['Cactus-Base']
&& mounts['Cactus-Desert']
&& mounts['Cactus-Red']
&& mounts['Cactus-Shade']
&& mounts['Cactus-White']
&& mounts['Cactus-Golden']
&& mounts['Cactus-CottonCandyPink']
&& mounts['Cactus-CottonCandyBlue']
&& mounts['Hedgehog-Zombie']
&& mounts['Hedgehog-Skeleton']
&& mounts['Hedgehog-Base']
&& mounts['Hedgehog-Desert']
&& mounts['Hedgehog-Red']
&& mounts['Hedgehog-Shade']
&& mounts['Hedgehog-White']
&& mounts['Hedgehog-Golden']
&& mounts['Hedgehog-CottonCandyPink']
&& mounts['Hedgehog-CottonCandyBlue']
&& mounts['Rock-Zombie']
&& mounts['Rock-Skeleton']
&& mounts['Rock-Base']
&& mounts['Rock-Desert']
&& mounts['Rock-Red']
&& mounts['Rock-Shade']
&& mounts['Rock-White']
&& mounts['Rock-Golden']
&& mounts['Rock-CottonCandyPink']
&& mounts['Rock-CottonCandyBlue'] ) {
set['achievements.roughRider'] = true;
}
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.update({ _id: user._id }, { $set: set }).exec();
}
module.exports = async function processUsers () {
let query = {
migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2023-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
}
};

View File

@@ -1,87 +0,0 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20231228_nye';
import { model as User } from '../../../website/server/models/user';
import { v4 as uuid } from 'uuid';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
const set = { migration: MIGRATION_NAME };
let push = {};
if (typeof user.items.gear.owned.head_special_nye2022 !== 'undefined') {
set['items.gear.owned.head_special_nye2023'] = true;
} else if (typeof user.items.gear.owned.head_special_nye2021 !== 'undefined') {
set['items.gear.owned.head_special_nye2022'] = true;
} else if (typeof user.items.gear.owned.head_special_nye2020 !== 'undefined') {
set['items.gear.owned.head_special_nye2021'] = true;
} else if (typeof user.items.gear.owned.head_special_nye2019 !== 'undefined') {
set['items.gear.owned.head_special_nye2020'] = true;
} else if (typeof user.items.gear.owned.head_special_nye2018 !== 'undefined') {
set['items.gear.owned.head_special_nye2019'] = true;
} else if (typeof user.items.gear.owned.head_special_nye2017 !== 'undefined') {
set['items.gear.owned.head_special_nye2018'] = true;
} else if (typeof user.items.gear.owned.head_special_nye2016 !== 'undefined') {
set['items.gear.owned.head_special_nye2017'] = true;
} else if (typeof user.items.gear.owned.head_special_nye2015 !== 'undefined') {
set['items.gear.owned.head_special_nye2016'] = true;
} else if (typeof user.items.gear.owned.head_special_nye2014 !== 'undefined') {
set['items.gear.owned.head_special_nye2015'] = true;
} else if (typeof user.items.gear.owned.head_special_nye !== 'undefined') {
set['items.gear.owned.head_special_nye2014'] = true;
} else {
set['items.gear.owned.head_special_nye'] = true;
}
push.notifications = {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_head_special_nye',
title: 'Happy New Year!',
text: 'Check your Equipment for this year\'s party hat!',
destination: 'inventory/equipment',
},
seen: false,
};
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.updateOne({_id: user._id}, {$set: set, $push: push}).exec();
}
export default async function processUsers () {
let query = {
'auth.timestamps.loggedin': { $gt: new Date('2023-12-01') },
migration: { $ne: MIGRATION_NAME },
};
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],
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -1,102 +0,0 @@
/* eslint-disable no-console */
import { v4 as uuid } from 'uuid';
import { model as User } from '../../../website/server/models/user';
const MIGRATION_NAME = '20240131_habit_birthday';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count += 1;
const inc = {
'items.food.Cake_Skeleton': 1,
'items.food.Cake_Base': 1,
'items.food.Cake_CottonCandyBlue': 1,
'items.food.Cake_CottonCandyPink': 1,
'items.food.Cake_Shade': 1,
'items.food.Cake_White': 1,
'items.food.Cake_Golden': 1,
'items.food.Cake_Zombie': 1,
'items.food.Cake_Desert': 1,
'items.food.Cake_Red': 1,
'achievements.habitBirthdays': 1,
};
const set = {};
const push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_namingDay_cake',
title: 'Happy Habit Birthday!',
text: 'Habitica turns 11 today! Enjoy free party robes and cake!',
destination: 'inventory/equipment',
},
seen: false,
},
};
set.migration = MIGRATION_NAME;
if (typeof user.items.gear.owned.armor_special_birthday2023 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2024'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday2022 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2023'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday2021 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2022'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday2020 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2021'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday2019 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2020'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday2018 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2019'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday2017 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2018'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday2016 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2017'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday2015 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2016'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday !== 'undefined') {
set['items.gear.owned.armor_special_birthday2015'] = true;
} else {
set['items.gear.owned.armor_special_birthday'] = true;
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.updateOne({_id: user._id}, {$inc: inc, $set: set, $push: push}).exec();
}
export default async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
'auth.timestamps.loggedin': {$gt: new Date('2023-12-23')},
};
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],
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -1,89 +0,0 @@
/* 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
}
};

View File

@@ -0,0 +1,118 @@
let migrationName = '20180904_takeThis.js'; // Update per month
let authorName = 'Sabe'; // in case script author needs to know when their ...
let authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
/*
* Award Take This ladder items to participants in this month's challenge
*/
import monk from 'monk';
import nconf from 'nconf';
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING'); // FOR TEST DATABASE
let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false });
function processUsers (lastId) {
// specify a query to limit the affected users (empty for all users):
let query = {
migration: {$ne: migrationName},
challenges: {$in: ['1044ec0c-4a85-48c5-9f36-d51c0c62c7d3']}, // Update per month
};
if (lastId) {
query._id = {
$gt: lastId,
};
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 250,
fields: [
'items.gear.owned',
], // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
})
.then(updateUsers)
.catch((err) => {
console.log(err);
return exiting(1, `ERROR! ${ err}`);
});
}
let progressCount = 1000;
let count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
displayData();
return;
}
let userPromises = users.map(updateUser);
let lastUser = users[users.length - 1];
return Promise.all(userPromises)
.then(() => {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
let set = {};
let push;
if (typeof user.items.gear.owned.back_special_takeThis !== 'undefined') {
set = {migration: migrationName};
} else if (typeof user.items.gear.owned.body_special_takeThis !== 'undefined') {
set = {migration: migrationName, 'items.gear.owned.back_special_takeThis': false};
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.back_special_takeThis', _id: monk.id()}};
} else if (typeof user.items.gear.owned.head_special_takeThis !== 'undefined') {
set = {migration: migrationName, 'items.gear.owned.body_special_takeThis': false};
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.body_special_takeThis', _id: monk.id()}};
} else if (typeof user.items.gear.owned.armor_special_takeThis !== 'undefined') {
set = {migration: migrationName, 'items.gear.owned.head_special_takeThis': false};
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.head_special_takeThis', _id: monk.id()}};
} else if (typeof user.items.gear.owned.weapon_special_takeThis !== 'undefined') {
set = {migration: migrationName, 'items.gear.owned.armor_special_takeThis': false};
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.armor_special_takeThis', _id: monk.id()}};
} else if (typeof user.items.gear.owned.shield_special_takeThis !== 'undefined') {
set = {migration: migrationName, 'items.gear.owned.weapon_special_takeThis': false};
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.weapon_special_takeThis', _id: monk.id()}};
} else {
set = {migration: migrationName, 'items.gear.owned.shield_special_takeThis': false};
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.shield_special_takeThis', _id: monk.id()}};
}
if (push) {
dbUsers.update({_id: user._id}, {$set: set, $push: push});
} else {
dbUsers.update({_id: user._id}, {$set: set});
}
if (count % progressCount === 0) console.warn(`${count } ${ user._id}`);
if (user._id === authorUuid) console.warn(`${authorName } processed`);
}
function displayData () {
console.warn(`\n${ count } users processed\n`);
return exiting(0);
}
function exiting (code, msg) {
code = code || 0; // 0 = success
if (code && !msg) {
msg = 'ERROR!';
}
if (msg) {
if (code) {
console.error(msg);
} else {
console.log(msg);
}
}
process.exit(code);
}
module.exports = processUsers;

10
migrations/csvexport.py Normal file
View File

@@ -0,0 +1,10 @@
import csv
with open(r"/home/slappybag/Documents/SurveyScrape.csv") as f:
reader = csv.reader(f, delimiter=',', quotechar='"')
column = []
for row in reader:
if row:
column.append(row[4])
print column

View File

@@ -21,14 +21,12 @@ async function handOutJackalopes () {
if (user.party._id) groupList.push(user.party._id); if (user.party._id) groupList.push(user.party._id);
groupList = groupList.concat(user.guilds); groupList = groupList.concat(user.guilds);
const subscribedGroup = await Group.findOne( const subscribedGroup = await Group.findOne({
{
_id: { $in: groupList }, _id: { $in: groupList },
'purchased.plan.planId': 'group_monthly', 'purchased.plan.planId': 'group_monthly',
'purchased.plan.dateTerminated': null, 'purchased.plan.dateTerminated': null,
}, },
{ _id: 1 }, { _id: 1 });
);
if (subscribedGroup) { if (subscribedGroup) {
User.update({ _id: user._id }, { $set: { 'items.mounts.Jackalope-RoyalPurple': true } }).exec(); User.update({ _id: user._id }, { $set: { 'items.mounts.Jackalope-RoyalPurple': true } }).exec();

View File

@@ -51,8 +51,7 @@ function getAchievementUpdate (newUser, oldUser) {
// Rebirth level // Rebirth level
if (achievementsUpdate.rebirthLevel) { if (achievementsUpdate.rebirthLevel) {
achievementsUpdate.rebirthLevel = Math.max( achievementsUpdate.rebirthLevel = Math.max(
achievementsUpdate.rebirthLevel, achievementsUpdate.rebirthLevel, oldAchievements.rebirthLevel,
oldAchievements.rebirthLevel,
); );
} else if (oldAchievements.rebirthLevel) { } else if (oldAchievements.rebirthLevel) {
achievementsUpdate.rebirthLevel = oldAchievements.rebirthLevel; achievementsUpdate.rebirthLevel = oldAchievements.rebirthLevel;

View File

@@ -16,7 +16,6 @@ async function updateUser (user) {
if (count % progressCount === 0) { if (count % progressCount === 0) {
console.warn(`${count} ${user._id}`); console.warn(`${count} ${user._id}`);
// eslint-disable-next-line no-promise-executor-return
await new Promise(resolve => setTimeout(resolve, 5000)); await new Promise(resolve => setTimeout(resolve, 5000));
} }

View File

@@ -51,7 +51,7 @@ async function updateUser (user) {
if (count % progressCount === 0) console.warn(`${count} ${user._id}`); if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return User.updateOne({ _id: user._id }, { $set: set }).exec(); return User.update({ _id: user._id }, { $set: set }).exec();
} }
export default async function processUsers () { export default async function processUsers () {

View File

@@ -3,7 +3,7 @@ import { v4 as uuid } from 'uuid';
import { model as User } from '../../website/server/models/user'; import { model as User } from '../../website/server/models/user';
const MIGRATION_NAME = '20240314_pi_day'; const MIGRATION_NAME = '20220314_pi_day';
const progressCount = 1000; const progressCount = 1000;
let count = 0; let count = 0;
@@ -54,7 +54,7 @@ async function updateUser (user) {
export default async function processUsers () { export default async function processUsers () {
const query = { const query = {
migration: { $ne: MIGRATION_NAME }, migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2023-02-15') }, 'auth.timestamps.loggedin': { $gt: new Date('2022-02-15') },
}; };
const fields = { const fields = {

8302
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,37 +1,37 @@
{ {
"name": "habitica", "name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.", "description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "5.19.1", "version": "4.251.0",
"main": "./website/server/index.js", "main": "./website/server/index.js",
"dependencies": { "dependencies": {
"@babel/core": "^7.22.10", "@babel/core": "^7.19.6",
"@babel/preset-env": "^7.22.10", "@babel/preset-env": "^7.20.2",
"@babel/register": "^7.22.15", "@babel/register": "^7.18.9",
"@google-cloud/trace-agent": "^7.1.2", "@google-cloud/trace-agent": "^7.1.2",
"@parse/node-apn": "^5.2.3", "@parse/node-apn": "^5.1.3",
"@slack/webhook": "^6.1.0", "@slack/webhook": "^6.1.0",
"accepts": "^1.3.8", "accepts": "^1.3.8",
"amazon-payments": "^0.2.9", "amazon-payments": "^0.2.9",
"amplitude": "^6.0.0", "amplitude": "^6.0.0",
"apidoc": "^0.54.0", "apidoc": "^0.53.1",
"apple-auth": "^1.0.9", "apple-auth": "^1.0.7",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.0",
"body-parser": "^1.20.2", "body-parser": "^1.20.1",
"bootstrap": "^4.6.2", "bootstrap": "^4.6.0",
"compression": "^1.7.4", "compression": "^1.7.4",
"cookie-session": "^2.0.0", "cookie-session": "^2.0.0",
"coupon-code": "^0.4.5", "coupon-code": "^0.4.5",
"csv-stringify": "^5.6.5", "csv-stringify": "^5.6.5",
"cwait": "^1.1.1", "cwait": "^1.1.1",
"domain-middleware": "~0.1.0", "domain-middleware": "~0.1.0",
"eslint": "^8.55.0", "eslint": "^6.8.0",
"eslint-config-habitrpg": "^6.2.3", "eslint-config-habitrpg": "^6.2.0",
"eslint-plugin-mocha": "^5.0.0", "eslint-plugin-mocha": "^5.0.0",
"express": "^4.18.2", "express": "^4.18.2",
"express-basic-auth": "^1.2.1", "express-basic-auth": "^1.2.1",
"express-validator": "^5.2.0", "express-validator": "^5.2.0",
"glob": "^8.1.0", "glob": "^8.0.3",
"got": "^11.8.6", "got": "^11.8.3",
"gulp": "^4.0.0", "gulp": "^4.0.0",
"gulp-babel": "^8.0.0", "gulp-babel": "^8.0.0",
"gulp-imagemin": "^7.1.0", "gulp-imagemin": "^7.1.0",
@@ -39,42 +39,44 @@
"gulp.spritesmith": "^6.13.0", "gulp.spritesmith": "^6.13.0",
"habitica-markdown": "^3.0.0", "habitica-markdown": "^3.0.0",
"helmet": "^4.6.0", "helmet": "^4.6.0",
"image-size": "^1.0.2",
"in-app-purchase": "^1.11.3", "in-app-purchase": "^1.11.3",
"js2xmlparser": "^5.0.0", "js2xmlparser": "^5.0.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^8.5.1",
"jwks-rsa": "^2.1.5", "jwks-rsa": "^2.1.5",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"merge-stream": "^2.0.0", "merge-stream": "^2.0.0",
"method-override": "^3.0.0", "method-override": "^3.0.0",
"moment": "^2.29.4", "moment": "^2.29.4",
"moment-recur": "^1.0.7", "moment-recur": "^1.0.7",
"mongoose": "^7.6.3", "mongoose": "^5.13.7",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"nconf": "^0.12.1", "nconf": "^0.12.0",
"node-gcm": "^1.0.5", "node-gcm": "^1.0.5",
"nodemon": "^2.0.20",
"on-headers": "^1.0.2", "on-headers": "^1.0.2",
"passport": "^0.5.3", "passport": "^0.6.0",
"passport-facebook": "^3.0.0", "passport-facebook": "^3.0.0",
"passport-google-oauth2": "^0.2.0", "passport-google-oauth2": "^0.2.0",
"passport-google-oauth20": "2.0.0", "passport-google-oauth20": "2.0.0",
"paypal-rest-sdk": "^1.8.1", "paypal-rest-sdk": "^1.8.1",
"pp-ipn": "^1.1.0", "pp-ipn": "^1.1.0",
"ps-tree": "^1.0.0", "ps-tree": "^1.0.0",
"rate-limiter-flexible": "^2.4.2", "rate-limiter-flexible": "^2.4.0",
"redis": "^3.1.2", "redis": "^3.1.2",
"regenerator-runtime": "^0.13.11",
"remove-markdown": "^0.5.0", "remove-markdown": "^0.5.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"short-uuid": "^4.2.2", "short-uuid": "^4.2.2",
"stripe": "^12.18.0", "stripe": "^10.13.0",
"superagent": "^8.1.2", "superagent": "^8.0.5",
"universal-analytics": "^0.5.3", "universal-analytics": "^0.5.3",
"useragent": "^2.1.9", "useragent": "^2.1.9",
"uuid": "^9.0.0", "uuid": "^8.3.2",
"validator": "^13.11.0", "validator": "^13.7.0",
"winston": "^3.10.0", "vinyl-buffer": "^1.0.1",
"winston-loggly-bulk": "^3.3.0", "winston": "^3.8.2",
"xml2js": "^0.6.2" "winston-loggly-bulk": "^3.2.1",
"xml2js": "^0.4.23"
}, },
"private": true, "private": true,
"engines": { "engines": {
@@ -103,16 +105,16 @@
"client:unit": "cd website/client && npm run test:unit", "client:unit": "cd website/client && npm run test:unit",
"start": "gulp nodemon", "start": "gulp nodemon",
"debug": "gulp nodemon --inspect", "debug": "gulp nodemon --inspect",
"mongo:dev": "run-rs -v 5.0.23 -l ubuntu1804 --keep --dbpath mongodb-data --number 1 --quiet", "mongo:dev": "run-rs -v 4.2.8 -l ubuntu1804 --keep --dbpath mongodb-data --number 1 --quiet",
"postinstall": "git config --global url.\"https://\".insteadOf git:// && gulp build && cd website/client && npm install", "postinstall": "git config --global url.\"https://\".insteadOf git:// && gulp build && cd website/client && npm install",
"apidoc": "gulp apidoc" "apidoc": "gulp apidoc"
}, },
"devDependencies": { "devDependencies": {
"axios": "^1.4.0", "axios": "^0.27.2",
"chai": "^4.3.7", "chai": "^4.3.7",
"chai-as-promised": "^7.1.1", "chai-as-promised": "^7.1.1",
"chai-moment": "^0.1.0", "chai-moment": "^0.1.0",
"chalk": "^5.3.0", "chalk": "^5.1.2",
"cross-spawn": "^7.0.3", "cross-spawn": "^7.0.3",
"expect.js": "^0.3.1", "expect.js": "^0.3.1",
"istanbul": "^1.1.0-alpha.1", "istanbul": "^1.1.0-alpha.1",
@@ -120,7 +122,7 @@
"monk": "^7.3.4", "monk": "^7.3.4",
"require-again": "^2.0.0", "require-again": "^2.0.0",
"run-rs": "^0.7.7", "run-rs": "^0.7.7",
"sinon": "^15.2.0", "sinon": "^14.0.2",
"sinon-chai": "^3.7.0", "sinon-chai": "^3.7.0",
"sinon-stub-promise": "^4.0.0" "sinon-stub-promise": "^4.0.0"
}, },

View File

@@ -44,7 +44,6 @@ async function deleteHabiticaData (user, email) {
{ _id: user._id }, { _id: user._id },
{ $set: set }, { $set: set },
); );
// eslint-disable-next-line no-promise-executor-return
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000));
const response = await axios.delete( const response = await axios.delete(
`${BASE_URL}/api/v3/user`, `${BASE_URL}/api/v3/user`,
@@ -97,7 +96,6 @@ async function processEmailAddress (email) {
return console.log(`No users found with email address ${email}`); return console.log(`No users found with email address ${email}`);
} }
// eslint-disable-next-line no-promise-executor-return
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000));
return Promise.all(users.map(user => (async () => { return Promise.all(users.map(user => (async () => {
await deleteAmplitudeData(user._id, email); // eslint-disable-line no-await-in-loop await deleteAmplitudeData(user._id, email); // eslint-disable-line no-await-in-loop

View File

@@ -1,5 +1,3 @@
/* eslint-disable import/no-commonjs */
module.exports = { module.exports = {
extends: [ extends: [
'habitrpg/lib/mocha', 'habitrpg/lib/mocha',
@@ -9,9 +7,6 @@ module.exports = {
chai: true, chai: true,
expect: true, expect: true,
sinon: true, sinon: true,
sandbox: true, sandbox: true
}, },
rules: { }
'import/no-extraneous-dependencies': 'off',
},
};

View File

@@ -26,7 +26,9 @@ describe('bug-report', () => {
_id: userId, _id: userId,
}); });
const result = await bugReportLogic(user, userMail, userMessage, userAgent); const result = await bugReportLogic(
user, userMail, userMessage, userAgent,
);
expect(emailLib.sendTxn).to.be.called; expect(emailLib.sendTxn).to.be.called;
expect(result).to.deep.equal({ expect(result).to.deep.equal({

View File

@@ -231,16 +231,13 @@ describe('cron', async () => {
}, },
}); });
// user1 has a 1-month recurring subscription starting today // user1 has a 1-month recurring subscription starting today
beforeEach(async () => {
user1.purchased.plan.customerId = 'subscribedId'; user1.purchased.plan.customerId = 'subscribedId';
user1.purchased.plan.dateUpdated = moment().toDate(); user1.purchased.plan.dateUpdated = moment().toDate();
user1.purchased.plan.planId = 'basic'; user1.purchased.plan.planId = 'basic';
user1.purchased.plan.consecutive.count = 0; user1.purchased.plan.consecutive.count = 0;
user1.purchased.plan.perkMonthCount = 0;
user1.purchased.plan.consecutive.offset = 0; user1.purchased.plan.consecutive.offset = 0;
user1.purchased.plan.consecutive.trinkets = 0; user1.purchased.plan.consecutive.trinkets = 0;
user1.purchased.plan.consecutive.gemCapExtra = 0; user1.purchased.plan.consecutive.gemCapExtra = 0;
});
it('does not increment consecutive benefits after the first month', async () => { it('does not increment consecutive benefits after the first month', async () => {
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months') clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
@@ -274,24 +271,6 @@ describe('cron', async () => {
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(0); expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(0);
}); });
it('increments consecutive benefits after the second month if they also received a 1 month gift subscription', async () => {
user1.purchased.plan.perkMonthCount = 1;
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(2, 'months')
.add(2, 'days')
.toDate());
// Add 1 month to simulate what happens a month after the subscription was created.
// Add 2 days so that we're sure we're not affected by any start-of-month effects
// e.g., from time zone oddness.
await cron({
user: user1, tasksByType, daysMissed, analytics,
});
expect(user1.purchased.plan.perkMonthCount).to.equal(0);
expect(user1.purchased.plan.consecutive.count).to.equal(2);
expect(user1.purchased.plan.consecutive.offset).to.equal(0);
expect(user1.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(5);
});
it('increments consecutive benefits after the third month', async () => { it('increments consecutive benefits after the third month', async () => {
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(3, 'months') clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(3, 'months')
.add(2, 'days') .add(2, 'days')
@@ -336,30 +315,6 @@ describe('cron', async () => {
expect(user1.purchased.plan.consecutive.trinkets).to.equal(3); expect(user1.purchased.plan.consecutive.trinkets).to.equal(3);
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(15); expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(15);
}); });
it('initializes plan.perkMonthCount if necessary', async () => {
user.purchased.plan.perkMonthCount = undefined;
clock = sinon.useFakeTimers(moment(user.purchased.plan.dateUpdated)
.utcOffset(0)
.startOf('month')
.add(1, 'months')
.add(2, 'days')
.toDate());
await cron({
user, tasksByType, daysMissed, analytics,
});
expect(user.purchased.plan.perkMonthCount).to.equal(1);
user.purchased.plan.perkMonthCount = undefined;
user.purchased.plan.consecutive.count = 8;
clock.restore();
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(2, 'months')
.add(2, 'days')
.toDate());
await cron({
user, tasksByType, daysMissed, analytics,
});
expect(user.purchased.plan.perkMonthCount).to.equal(2);
});
}); });
describe('for a 3-month recurring subscription', async () => { describe('for a 3-month recurring subscription', async () => {
@@ -375,16 +330,13 @@ describe('cron', async () => {
}, },
}); });
// user3 has a 3-month recurring subscription starting today // user3 has a 3-month recurring subscription starting today
beforeEach(async () => {
user3.purchased.plan.customerId = 'subscribedId'; user3.purchased.plan.customerId = 'subscribedId';
user3.purchased.plan.dateUpdated = moment().toDate(); user3.purchased.plan.dateUpdated = moment().toDate();
user3.purchased.plan.planId = 'basic_3mo'; user3.purchased.plan.planId = 'basic_3mo';
user3.purchased.plan.perkMonthCount = 0;
user3.purchased.plan.consecutive.count = 0; user3.purchased.plan.consecutive.count = 0;
user3.purchased.plan.consecutive.offset = 3; user3.purchased.plan.consecutive.offset = 3;
user3.purchased.plan.consecutive.trinkets = 1; user3.purchased.plan.consecutive.trinkets = 1;
user3.purchased.plan.consecutive.gemCapExtra = 5; user3.purchased.plan.consecutive.gemCapExtra = 5;
});
it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', async () => { it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', async () => {
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months') clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
@@ -438,21 +390,6 @@ describe('cron', async () => {
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(10); expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(10);
}); });
it('keeps existing plan.perkMonthCount intact when incrementing consecutive benefits', async () => {
user3.purchased.plan.perkMonthCount = 2;
user3.purchased.plan.consecutive.trinkets = 1;
user3.purchased.plan.consecutive.gemCapExtra = 5;
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(4, 'months')
.add(2, 'days')
.toDate());
await cron({
user: user3, tasksByType, daysMissed, analytics,
});
expect(user3.purchased.plan.perkMonthCount).to.equal(2);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(2);
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(10);
});
it('does not increment consecutive benefits in the second month of the second period that they already have benefits for', async () => { it('does not increment consecutive benefits in the second month of the second period that they already have benefits for', async () => {
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(5, 'months') clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(5, 'months')
.add(2, 'days') .add(2, 'days')
@@ -519,16 +456,13 @@ describe('cron', async () => {
}, },
}); });
// user6 has a 6-month recurring subscription starting today // user6 has a 6-month recurring subscription starting today
beforeEach(async () => {
user6.purchased.plan.customerId = 'subscribedId'; user6.purchased.plan.customerId = 'subscribedId';
user6.purchased.plan.dateUpdated = moment().toDate(); user6.purchased.plan.dateUpdated = moment().toDate();
user6.purchased.plan.planId = 'google_6mo'; user6.purchased.plan.planId = 'google_6mo';
user6.purchased.plan.perkMonthCount = 0;
user6.purchased.plan.consecutive.count = 0; user6.purchased.plan.consecutive.count = 0;
user6.purchased.plan.consecutive.offset = 6; user6.purchased.plan.consecutive.offset = 6;
user6.purchased.plan.consecutive.trinkets = 2; user6.purchased.plan.consecutive.trinkets = 2;
user6.purchased.plan.consecutive.gemCapExtra = 10; user6.purchased.plan.consecutive.gemCapExtra = 10;
});
it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', async () => { it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', async () => {
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months') clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
@@ -569,19 +503,6 @@ describe('cron', async () => {
expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(20); expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(20);
}); });
it('keeps existing plan.perkMonthCount intact when incrementing consecutive benefits', async () => {
user6.purchased.plan.perkMonthCount = 2;
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(7, 'months')
.add(2, 'days')
.toDate());
await cron({
user: user6, tasksByType, daysMissed, analytics,
});
expect(user6.purchased.plan.perkMonthCount).to.equal(2);
expect(user6.purchased.plan.consecutive.trinkets).to.equal(4);
expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(20);
});
it('increments consecutive benefits the month after the third paid period has started', async () => { it('increments consecutive benefits the month after the third paid period has started', async () => {
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(13, 'months') clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(13, 'months')
.add(2, 'days') .add(2, 'days')

View File

@@ -44,7 +44,7 @@ describe('mongodb', () => {
const mongoLibOverride = requireAgain(pathToMongoLib); const mongoLibOverride = requireAgain(pathToMongoLib);
const options = mongoLibOverride.getDefaultConnectionOptions(); const options = mongoLibOverride.getDefaultConnectionOptions();
expect(options).to.have.all.keys(['useNewUrlParser', 'useUnifiedTopology']); expect(options).to.have.all.keys(['useNewUrlParser', 'useUnifiedTopology', 'keepAlive', 'keepAliveInitialDelay']);
}); });
}); });
}); });

View File

@@ -227,7 +227,7 @@ describe('Password Utilities', () => {
expiresAt: moment().subtract({ minutes: 1 }), expiresAt: moment().subtract({ minutes: 1 }),
})); }));
await user.updateOne({ await user.update({
'auth.local.passwordResetCode': code, 'auth.local.passwordResetCode': code,
}); });
@@ -264,7 +264,7 @@ describe('Password Utilities', () => {
expiresAt: moment().add({ days: 1 }), expiresAt: moment().add({ days: 1 }),
})); }));
await user.updateOne({ await user.update({
'auth.local.passwordResetCode': 'invalid', 'auth.local.passwordResetCode': 'invalid',
}); });
@@ -280,7 +280,7 @@ describe('Password Utilities', () => {
expiresAt: moment().add({ days: 1 }), expiresAt: moment().add({ days: 1 }),
})); }));
await user.updateOne({ await user.update({
'auth.local.passwordResetCode': code, 'auth.local.passwordResetCode': code,
}); });

View File

@@ -17,7 +17,7 @@ describe('Amazon Payments - Checkout', () => {
let closeOrderReferenceSpy; let closeOrderReferenceSpy;
let paymentBuyGemsStub; let paymentBuyGemsStub;
let paymentCreateSubscriptionStub; let paymentCreateSubscritionStub;
let amount = gemsBlock.price / 100; let amount = gemsBlock.price / 100;
function expectOrderReferenceSpy () { function expectOrderReferenceSpy () {
@@ -85,8 +85,8 @@ describe('Amazon Payments - Checkout', () => {
paymentBuyGemsStub = sinon.stub(payments, 'buyGems'); paymentBuyGemsStub = sinon.stub(payments, 'buyGems');
paymentBuyGemsStub.resolves({}); paymentBuyGemsStub.resolves({});
paymentCreateSubscriptionStub = sinon.stub(payments, 'createSubscription'); paymentCreateSubscritionStub = sinon.stub(payments, 'createSubscription');
paymentCreateSubscriptionStub.resolves({}); paymentCreateSubscritionStub.resolves({});
sinon.stub(common, 'uuid').returns('uuid-generated'); sinon.stub(common, 'uuid').returns('uuid-generated');
sandbox.stub(gems, 'validateGiftMessage'); sandbox.stub(gems, 'validateGiftMessage');
@@ -109,7 +109,6 @@ describe('Amazon Payments - Checkout', () => {
user, user,
paymentMethod, paymentMethod,
headers, headers,
sku: undefined,
}; };
if (gift) { if (gift) {
expectedArgs.gift = gift; expectedArgs.gift = gift;
@@ -216,14 +215,13 @@ describe('Amazon Payments - Checkout', () => {
}); });
gift.member = receivingUser; gift.member = receivingUser;
expect(paymentCreateSubscriptionStub).to.be.calledOnce; expect(paymentCreateSubscritionStub).to.be.calledOnce;
expect(paymentCreateSubscriptionStub).to.be.calledWith({ expect(paymentCreateSubscritionStub).to.be.calledWith({
user, user,
paymentMethod: amzLib.constants.PAYMENT_METHOD_GIFT, paymentMethod: amzLib.constants.PAYMENT_METHOD_GIFT,
headers, headers,
gift, gift,
gemsBlock: undefined, gemsBlock: undefined,
sku: undefined,
}); });
expectAmazonStubs(); expectAmazonStubs();
}); });

View File

@@ -12,10 +12,10 @@ const { i18n } = common;
describe('Apple Payments', () => { describe('Apple Payments', () => {
const subKey = 'basic_3mo'; const subKey = 'basic_3mo';
describe('verifyPurchase', () => { describe('verifyGemPurchase', () => {
let sku; let user; let token; let receipt; let let sku; let user; let token; let receipt; let
headers; headers;
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let paymentBuySkuStub; let let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let paymentBuyGemsStub; let
iapGetPurchaseDataStub; let validateGiftMessageStub; iapGetPurchaseDataStub; let validateGiftMessageStub;
beforeEach(() => { beforeEach(() => {
@@ -29,15 +29,14 @@ describe('Apple Payments', () => {
.resolves(); .resolves();
iapValidateStub = sinon.stub(iap, 'validate') iapValidateStub = sinon.stub(iap, 'validate')
.resolves({}); .resolves({});
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true); iapIsValidatedStub = sinon.stub(iap, 'isValidated')
sinon.stub(iap, 'isExpired').returns(false); .returns(true);
sinon.stub(iap, 'isCanceled').returns(false);
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData') iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{ .returns([{
productId: 'com.habitrpg.ios.Habitica.21gems', productId: 'com.habitrpg.ios.Habitica.21gems',
transactionId: token, transactionId: token,
}]); }]);
paymentBuySkuStub = sinon.stub(payments, 'buySkuItem').resolves({}); paymentBuyGemsStub = sinon.stub(payments, 'buyGems').resolves({});
validateGiftMessageStub = sinon.stub(gems, 'validateGiftMessage'); validateGiftMessageStub = sinon.stub(gems, 'validateGiftMessage');
}); });
@@ -45,10 +44,8 @@ describe('Apple Payments', () => {
iap.setup.restore(); iap.setup.restore();
iap.validate.restore(); iap.validate.restore();
iap.isValidated.restore(); iap.isValidated.restore();
iap.isExpired.restore();
iap.isCanceled.restore();
iap.getPurchaseData.restore(); iap.getPurchaseData.restore();
payments.buySkuItem.restore(); payments.buyGems.restore();
gems.validateGiftMessage.restore(); gems.validateGiftMessage.restore();
}); });
@@ -57,7 +54,7 @@ describe('Apple Payments', () => {
iapIsValidatedStub = sinon.stub(iap, 'isValidated') iapIsValidatedStub = sinon.stub(iap, 'isValidated')
.returns(false); .returns(false);
await expect(applePayments.verifyPurchase({ user, receipt, headers })) await expect(applePayments.verifyGemPurchase({ user, receipt, headers }))
.to.eventually.be.rejected.and.to.eql({ .to.eventually.be.rejected.and.to.eql({
httpCode: 401, httpCode: 401,
name: 'NotAuthorized', name: 'NotAuthorized',
@@ -69,7 +66,7 @@ describe('Apple Payments', () => {
iapGetPurchaseDataStub.restore(); iapGetPurchaseDataStub.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData').returns([]); iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData').returns([]);
await expect(applePayments.verifyPurchase({ user, receipt, headers })) await expect(applePayments.verifyGemPurchase({ user, receipt, headers }))
.to.eventually.be.rejected.and.to.eql({ .to.eventually.be.rejected.and.to.eql({
httpCode: 401, httpCode: 401,
name: 'NotAuthorized', name: 'NotAuthorized',
@@ -79,7 +76,7 @@ describe('Apple Payments', () => {
it('errors if the user cannot purchase gems', async () => { it('errors if the user cannot purchase gems', async () => {
sinon.stub(user, 'canGetGems').resolves(false); sinon.stub(user, 'canGetGems').resolves(false);
await expect(applePayments.verifyPurchase({ user, receipt, headers })) await expect(applePayments.verifyGemPurchase({ user, receipt, headers }))
.to.eventually.be.rejected.and.to.eql({ .to.eventually.be.rejected.and.to.eql({
httpCode: 401, httpCode: 401,
name: 'NotAuthorized', name: 'NotAuthorized',
@@ -97,16 +94,14 @@ describe('Apple Payments', () => {
productId: 'badProduct', productId: 'badProduct',
transactionId: token, transactionId: token,
}]); }]);
paymentBuySkuStub.restore();
await expect(applePayments.verifyPurchase({ user, receipt, headers })) await expect(applePayments.verifyGemPurchase({ user, receipt, headers }))
.to.eventually.be.rejected.and.to.eql({ .to.eventually.be.rejected.and.to.eql({
httpCode: 400, httpCode: 401,
name: 'BadRequest', name: 'NotAuthorized',
message: applePayments.constants.RESPONSE_INVALID_ITEM, message: applePayments.constants.RESPONSE_INVALID_ITEM,
}); });
paymentBuySkuStub = sinon.stub(payments, 'buySkuItem').resolves({});
user.canGetGems.restore(); user.canGetGems.restore();
}); });
@@ -143,7 +138,7 @@ describe('Apple Payments', () => {
}]); }]);
sinon.stub(user, 'canGetGems').resolves(true); sinon.stub(user, 'canGetGems').resolves(true);
await applePayments.verifyPurchase({ user, receipt, headers }); await applePayments.verifyGemPurchase({ user, receipt, headers });
expect(iapSetupStub).to.be.calledOnce; expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce; expect(iapValidateStub).to.be.calledOnce;
@@ -153,13 +148,13 @@ describe('Apple Payments', () => {
expect(iapGetPurchaseDataStub).to.be.calledOnce; expect(iapGetPurchaseDataStub).to.be.calledOnce;
expect(validateGiftMessageStub).to.not.be.called; expect(validateGiftMessageStub).to.not.be.called;
expect(paymentBuySkuStub).to.be.calledOnce; expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuySkuStub).to.be.calledWith({ expect(paymentBuyGemsStub).to.be.calledWith({
user, user,
gift: undefined,
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE, paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
sku: gemTest.productId, gemsBlock: common.content.gems[gemTest.gemsBlock],
headers, headers,
gift: undefined,
}); });
expect(user.canGetGems).to.be.calledOnce; expect(user.canGetGems).to.be.calledOnce;
user.canGetGems.restore(); user.canGetGems.restore();
@@ -178,7 +173,7 @@ describe('Apple Payments', () => {
}]); }]);
const gift = { uuid: receivingUser._id }; const gift = { uuid: receivingUser._id };
await applePayments.verifyPurchase({ await applePayments.verifyGemPurchase({
user, gift, receipt, headers, user, gift, receipt, headers,
}); });
@@ -192,16 +187,18 @@ describe('Apple Payments', () => {
expect(validateGiftMessageStub).to.be.calledOnce; expect(validateGiftMessageStub).to.be.calledOnce;
expect(validateGiftMessageStub).to.be.calledWith(gift, user); expect(validateGiftMessageStub).to.be.calledWith(gift, user);
expect(paymentBuySkuStub).to.be.calledOnce; expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuySkuStub).to.be.calledWith({ expect(paymentBuyGemsStub).to.be.calledWith({
user, user,
gift: {
uuid: receivingUser._id,
member: sinon.match({ _id: receivingUser._id }),
},
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE, paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
sku: 'com.habitrpg.ios.Habitica.4gems',
headers, headers,
gift: {
type: 'gems',
gems: { amount: 4 },
member: sinon.match({ _id: receivingUser._id }),
uuid: receivingUser._id,
},
gemsBlock: common.content.gems['4gems'],
}); });
}); });
}); });
@@ -221,7 +218,6 @@ describe('Apple Payments', () => {
headers = {}; headers = {};
receipt = `{"token": "${token}"}`; receipt = `{"token": "${token}"}`;
nextPaymentProcessing = moment.utc().add({ days: 2 }); nextPaymentProcessing = moment.utc().add({ days: 2 });
user = new User();
iapSetupStub = sinon.stub(iap, 'setup') iapSetupStub = sinon.stub(iap, 'setup')
.resolves(); .resolves();
@@ -232,17 +228,14 @@ describe('Apple Payments', () => {
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData') iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{ .returns([{
expirationDate: moment.utc().subtract({ day: 1 }).toDate(), expirationDate: moment.utc().subtract({ day: 1 }).toDate(),
purchaseDate: moment.utc().valueOf(),
productId: sku, productId: sku,
transactionId: token, transactionId: token,
}, { }, {
expirationDate: moment.utc().add({ day: 1 }).toDate(), expirationDate: moment.utc().add({ day: 1 }).toDate(),
purchaseDate: moment.utc().valueOf(),
productId: 'wrongsku', productId: 'wrongsku',
transactionId: token, transactionId: token,
}, { }, {
expirationDate: moment.utc().add({ day: 1 }).toDate(), expirationDate: moment.utc().add({ day: 1 }).toDate(),
purchaseDate: moment.utc().valueOf(),
productId: sku, productId: sku,
transactionId: token, transactionId: token,
}]); }]);
@@ -257,12 +250,21 @@ describe('Apple Payments', () => {
if (payments.createSubscription.restore) payments.createSubscription.restore(); if (payments.createSubscription.restore) payments.createSubscription.restore();
}); });
it('should throw an error if sku is empty', async () => {
await expect(applePayments.subscribe('', user, receipt, headers, nextPaymentProcessing))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
message: i18n.t('missingSubscriptionCode'),
});
});
it('should throw an error if receipt is invalid', async () => { it('should throw an error if receipt is invalid', async () => {
iap.isValidated.restore(); iap.isValidated.restore();
iapIsValidatedStub = sinon.stub(iap, 'isValidated') iapIsValidatedStub = sinon.stub(iap, 'isValidated')
.returns(false); .returns(false);
await expect(applePayments.subscribe(user, receipt, headers, nextPaymentProcessing)) await expect(applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing))
.to.eventually.be.rejected.and.to.eql({ .to.eventually.be.rejected.and.to.eql({
httpCode: 401, httpCode: 401,
name: 'NotAuthorized', name: 'NotAuthorized',
@@ -293,15 +295,13 @@ describe('Apple Payments', () => {
iap.getPurchaseData.restore(); iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData') iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{ .returns([{
expirationDate: moment.utc().add({ day: 2 }).toDate(), expirationDate: moment.utc().add({ day: 1 }).toDate(),
purchaseDate: new Date(),
productId: option.sku, productId: option.sku,
transactionId: token, transactionId: token,
originalTransactionId: token,
}]); }]);
sub = common.content.subscriptionBlocks[option.subKey]; sub = common.content.subscriptionBlocks[option.subKey];
await applePayments.subscribe(user, receipt, headers, nextPaymentProcessing); await applePayments.subscribe(option.sku, user, receipt, headers, nextPaymentProcessing);
expect(iapSetupStub).to.be.calledOnce; expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce; expect(iapValidateStub).to.be.calledOnce;
@@ -321,254 +321,22 @@ describe('Apple Payments', () => {
nextPaymentProcessing, nextPaymentProcessing,
}); });
}); });
if (option !== subOptions[3]) {
const newOption = subOptions[3];
it(`upgrades a subscription from ${option.sku} to ${newOption.sku}`, async () => {
const oldSub = common.content.subscriptionBlocks[option.subKey];
oldSub.logic = 'refundAndRepay';
user.profile.name = 'sender';
user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE;
user.purchased.plan.customerId = token;
user.purchased.plan.planId = option.subKey;
user.purchased.plan.additionalData = receipt;
iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().add({ day: 2 }).toDate(),
purchaseDate: moment.utc().valueOf(),
productId: newOption.sku,
transactionId: `${token}new`,
originalTransactionId: token,
}]);
sub = common.content.subscriptionBlocks[newOption.subKey];
await applePayments.subscribe(
user,
receipt,
headers,
nextPaymentProcessing,
);
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt);
expect(iapIsValidatedStub).to.be.calledOnce;
expect(iapIsValidatedStub).to.be.calledWith({});
expect(iapGetPurchaseDataStub).to.be.calledOnce;
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
expect(paymentsCreateSubscritionStub).to.be.calledWith({
user,
customerId: token,
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
sub,
headers,
additionalData: receipt,
nextPaymentProcessing,
updatedFrom: oldSub,
});
});
}
if (option !== subOptions[0]) {
const newOption = subOptions[0];
it(`downgrades a subscription from ${option.sku} to ${newOption.sku}`, async () => {
const oldSub = common.content.subscriptionBlocks[option.subKey];
user.profile.name = 'sender';
user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE;
user.purchased.plan.customerId = token;
user.purchased.plan.planId = option.subKey;
user.purchased.plan.additionalData = receipt;
iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().add({ day: 2 }).toDate(),
purchaseDate: moment.utc().valueOf(),
productId: newOption.sku,
transactionId: `${token}new`,
originalTransactionId: token,
}]);
sub = common.content.subscriptionBlocks[newOption.subKey];
await applePayments.subscribe(
user,
receipt,
headers,
nextPaymentProcessing,
);
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt);
expect(iapIsValidatedStub).to.be.calledOnce;
expect(iapIsValidatedStub).to.be.calledWith({});
expect(iapGetPurchaseDataStub).to.be.calledOnce;
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
expect(paymentsCreateSubscritionStub).to.be.calledWith({
user,
customerId: token,
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
sub,
headers,
additionalData: receipt,
nextPaymentProcessing,
updatedFrom: oldSub,
});
});
}
}); });
it('uses the most recent subscription data', async () => { it('errors when a user is already subscribed', async () => {
iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().add({ day: 4 }).toDate(),
purchaseDate: moment.utc().subtract({ day: 5 }).toDate(),
productId: 'com.habitrpg.ios.habitica.subscription.3month',
transactionId: `${token}oldest`,
originalTransactionId: `${token}evenOlder`,
}, {
expirationDate: moment.utc().add({ day: 2 }).toDate(),
purchaseDate: moment.utc().subtract({ day: 1 }).toDate(),
productId: 'com.habitrpg.ios.habitica.subscription.12month',
transactionId: `${token}newest`,
originalTransactionId: `${token}newest`,
}, {
expirationDate: moment.utc().add({ day: 1 }).toDate(),
purchaseDate: moment.utc().subtract({ day: 2 }).toDate(),
productId: 'com.habitrpg.ios.habitica.subscription.6month',
transactionId: token,
originalTransactionId: token,
}]);
sub = common.content.subscriptionBlocks.basic_12mo;
await applePayments.subscribe(user, receipt, headers, nextPaymentProcessing);
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
expect(paymentsCreateSubscritionStub).to.be.calledWith({
user,
customerId: `${token}newest`,
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
sub,
headers,
additionalData: receipt,
nextPaymentProcessing,
});
});
describe('does not apply multiple times', async () => {
it('errors when a user is using the same subscription', async () => {
payments.createSubscription.restore(); payments.createSubscription.restore();
iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().add({ day: 1 }).toDate(),
purchaseDate: moment.utc().toDate(),
productId: sku,
transactionId: token,
originalTransactionId: token,
}]);
await applePayments.subscribe(user, receipt, headers, nextPaymentProcessing);
await expect(applePayments.subscribe(user, receipt, headers, nextPaymentProcessing))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: applePayments.constants.RESPONSE_ALREADY_USED,
});
});
it('errors when a user is using a rebill of the same subscription', async () => {
user = new User(); user = new User();
await user.save(); await user.save();
payments.createSubscription.restore();
iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().add({ day: 1 }).toDate(),
purchaseDate: moment.utc().toDate(),
productId: sku,
transactionId: `${token}renew`,
originalTransactionId: token,
}]);
await applePayments.subscribe(user, receipt, headers, nextPaymentProcessing); await applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing);
await expect(applePayments.subscribe(user, receipt, headers, nextPaymentProcessing)) await expect(applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing))
.to.eventually.be.rejected.and.to.eql({ .to.eventually.be.rejected.and.to.eql({
httpCode: 401, httpCode: 401,
name: 'NotAuthorized', name: 'NotAuthorized',
message: applePayments.constants.RESPONSE_ALREADY_USED, message: applePayments.constants.RESPONSE_ALREADY_USED,
}); });
}); });
it('errors when a different user is using the subscription', async () => {
user = new User();
await user.save();
payments.createSubscription.restore();
iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().add({ day: 1 }).toDate(),
purchaseDate: moment.utc().toDate(),
productId: sku,
transactionId: token,
originalTransactionId: token,
}]);
await applePayments.subscribe(user, receipt, headers, nextPaymentProcessing);
const secondUser = new User();
await secondUser.save();
await expect(applePayments.subscribe(secondUser, receipt, headers, nextPaymentProcessing))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: applePayments.constants.RESPONSE_ALREADY_USED,
});
});
it('errors when a multiple users exist using the subscription', async () => {
user = new User();
await user.save();
payments.createSubscription.restore();
iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().add({ day: 1 }).toDate(),
purchaseDate: moment.utc().toDate(),
productId: sku,
transactionId: token,
originalTransactionId: token,
}]);
await applePayments.subscribe(user, receipt, headers, nextPaymentProcessing);
const secondUser = new User();
secondUser.purchased.plan = user.purchased.plan;
secondUser.purchased.plan.dateTerminate = new Date();
secondUser.save();
iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().add({ day: 1 }).toDate(),
purchaseDate: moment.utc().toDate(),
productId: sku,
transactionId: `${token}new`,
originalTransactionId: token,
}]);
const thirdUser = new User();
await thirdUser.save();
await expect(applePayments.subscribe(thirdUser, receipt, headers, nextPaymentProcessing))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: applePayments.constants.RESPONSE_ALREADY_USED,
});
});
});
}); });
describe('cancelSubscribe ', () => { describe('cancelSubscribe ', () => {
@@ -592,9 +360,9 @@ describe('Apple Payments', () => {
}); });
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData') iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{ expirationDate: expirationDate.toDate() }]); .returns([{ expirationDate: expirationDate.toDate() }]);
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true); iapIsValidatedStub = sinon.stub(iap, 'isValidated')
sinon.stub(iap, 'isCanceled').returns(false); .returns(true);
sinon.stub(iap, 'isExpired').returns(true);
user = new User(); user = new User();
user.profile.name = 'sender'; user.profile.name = 'sender';
user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE; user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE;
@@ -609,8 +377,6 @@ describe('Apple Payments', () => {
iap.setup.restore(); iap.setup.restore();
iap.validate.restore(); iap.validate.restore();
iap.isValidated.restore(); iap.isValidated.restore();
iap.isExpired.restore();
iap.isCanceled.restore();
iap.getPurchaseData.restore(); iap.getPurchaseData.restore();
payments.cancelSubscription.restore(); payments.cancelSubscription.restore();
}); });
@@ -630,8 +396,6 @@ describe('Apple Payments', () => {
iap.getPurchaseData.restore(); iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData') iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{ expirationDate: expirationDate.add({ day: 1 }).toDate() }]); .returns([{ expirationDate: expirationDate.add({ day: 1 }).toDate() }]);
iap.isExpired.restore();
sinon.stub(iap, 'isExpired').returns(false);
await expect(applePayments.cancelSubscribe(user, headers)) await expect(applePayments.cancelSubscribe(user, headers))
.to.eventually.be.rejected.and.to.eql({ .to.eventually.be.rejected.and.to.eql({
@@ -654,38 +418,7 @@ describe('Apple Payments', () => {
}); });
}); });
it('should cancel a cancelled subscription with termination date in the future', async () => { it('should cancel a user subscription', async () => {
const futureDate = expirationDate.add({ day: 1 });
iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{ expirationDate: futureDate }]);
iap.isExpired.restore();
sinon.stub(iap, 'isExpired').returns(false);
iap.isCanceled.restore();
sinon.stub(iap, 'isCanceled').returns(true);
await applePayments.cancelSubscribe(user, headers);
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt);
expect(iapIsValidatedStub).to.be.calledOnce;
expect(iapIsValidatedStub).to.be.calledWith({
expirationDate: futureDate,
});
expect(iapGetPurchaseDataStub).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
user,
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
nextBill: futureDate.toDate(),
headers,
});
});
it('should cancel an expired subscription', async () => {
await applePayments.cancelSubscribe(user, headers); await applePayments.cancelSubscribe(user, headers);
expect(iapSetupStub).to.be.calledOnce; expect(iapSetupStub).to.be.calledOnce;

View File

@@ -12,11 +12,11 @@ const { i18n } = common;
describe('Google Payments', () => { describe('Google Payments', () => {
const subKey = 'basic_3mo'; const subKey = 'basic_3mo';
describe('verifyPurchase', () => { describe('verifyGemPurchase', () => {
let sku; let user; let token; let receipt; let signature; let let sku; let user; let token; let receipt; let signature; let
headers; headers; const gemsBlock = common.content.gems['21gems'];
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let
paymentBuySkuStub; let validateGiftMessageStub; paymentBuyGemsStub; let validateGiftMessageStub;
beforeEach(() => { beforeEach(() => {
sku = 'com.habitrpg.android.habitica.iap.21gems'; sku = 'com.habitrpg.android.habitica.iap.21gems';
@@ -27,10 +27,11 @@ describe('Google Payments', () => {
iapSetupStub = sinon.stub(iap, 'setup') iapSetupStub = sinon.stub(iap, 'setup')
.resolves(); .resolves();
iapValidateStub = sinon.stub(iap, 'validate').resolves({ productId: sku }); iapValidateStub = sinon.stub(iap, 'validate')
.resolves({});
iapIsValidatedStub = sinon.stub(iap, 'isValidated') iapIsValidatedStub = sinon.stub(iap, 'isValidated')
.returns(true); .returns(true);
paymentBuySkuStub = sinon.stub(payments, 'buySkuItem').resolves({}); paymentBuyGemsStub = sinon.stub(payments, 'buyGems').resolves({});
validateGiftMessageStub = sinon.stub(gems, 'validateGiftMessage'); validateGiftMessageStub = sinon.stub(gems, 'validateGiftMessage');
}); });
@@ -38,7 +39,7 @@ describe('Google Payments', () => {
iap.setup.restore(); iap.setup.restore();
iap.validate.restore(); iap.validate.restore();
iap.isValidated.restore(); iap.isValidated.restore();
payments.buySkuItem.restore(); payments.buyGems.restore();
gems.validateGiftMessage.restore(); gems.validateGiftMessage.restore();
}); });
@@ -47,7 +48,7 @@ describe('Google Payments', () => {
iapIsValidatedStub = sinon.stub(iap, 'isValidated') iapIsValidatedStub = sinon.stub(iap, 'isValidated')
.returns(false); .returns(false);
await expect(googlePayments.verifyPurchase({ await expect(googlePayments.verifyGemPurchase({
user, receipt, signature, headers, user, receipt, signature, headers,
})) }))
.to.eventually.be.rejected.and.to.eql({ .to.eventually.be.rejected.and.to.eql({
@@ -59,25 +60,21 @@ describe('Google Payments', () => {
it('should throw an error if productId is invalid', async () => { it('should throw an error if productId is invalid', async () => {
receipt = `{"token": "${token}", "productId": "invalid"}`; receipt = `{"token": "${token}", "productId": "invalid"}`;
iapValidateStub.restore();
iapValidateStub = sinon.stub(iap, 'validate').resolves({});
paymentBuySkuStub.restore(); await expect(googlePayments.verifyGemPurchase({
await expect(googlePayments.verifyPurchase({
user, receipt, signature, headers, user, receipt, signature, headers,
})) }))
.to.eventually.be.rejected.and.to.eql({ .to.eventually.be.rejected.and.to.eql({
httpCode: 400, httpCode: 401,
name: 'BadRequest', name: 'NotAuthorized',
message: googlePayments.constants.RESPONSE_INVALID_ITEM, message: googlePayments.constants.RESPONSE_INVALID_ITEM,
}); });
paymentBuySkuStub = sinon.stub(payments, 'buySkuItem').resolves({});
}); });
it('should throw an error if user cannot purchase gems', async () => { it('should throw an error if user cannot purchase gems', async () => {
sinon.stub(user, 'canGetGems').resolves(false); sinon.stub(user, 'canGetGems').resolves(false);
await expect(googlePayments.verifyPurchase({ await expect(googlePayments.verifyGemPurchase({
user, receipt, signature, headers, user, receipt, signature, headers,
})) }))
.to.eventually.be.rejected.and.to.eql({ .to.eventually.be.rejected.and.to.eql({
@@ -91,7 +88,7 @@ describe('Google Payments', () => {
it('purchases gems', async () => { it('purchases gems', async () => {
sinon.stub(user, 'canGetGems').resolves(true); sinon.stub(user, 'canGetGems').resolves(true);
await googlePayments.verifyPurchase({ await googlePayments.verifyGemPurchase({
user, receipt, signature, headers, user, receipt, signature, headers,
}); });
@@ -104,17 +101,15 @@ describe('Google Payments', () => {
signature, signature,
}); });
expect(iapIsValidatedStub).to.be.calledOnce; expect(iapIsValidatedStub).to.be.calledOnce;
expect(iapIsValidatedStub).to.be.calledWith( expect(iapIsValidatedStub).to.be.calledWith({});
{ productId: sku },
);
expect(paymentBuySkuStub).to.be.calledOnce; expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuySkuStub).to.be.calledWith({ expect(paymentBuyGemsStub).to.be.calledWith({
user, user,
gift: undefined,
paymentMethod: googlePayments.constants.PAYMENT_METHOD_GOOGLE, paymentMethod: googlePayments.constants.PAYMENT_METHOD_GOOGLE,
sku, gemsBlock,
headers, headers,
gift: undefined,
}); });
expect(user.canGetGems).to.be.calledOnce; expect(user.canGetGems).to.be.calledOnce;
user.canGetGems.restore(); user.canGetGems.restore();
@@ -125,7 +120,7 @@ describe('Google Payments', () => {
await receivingUser.save(); await receivingUser.save();
const gift = { uuid: receivingUser._id }; const gift = { uuid: receivingUser._id };
await googlePayments.verifyPurchase({ await googlePayments.verifyGemPurchase({
user, gift, receipt, signature, headers, user, gift, receipt, signature, headers,
}); });
@@ -139,20 +134,20 @@ describe('Google Payments', () => {
signature, signature,
}); });
expect(iapIsValidatedStub).to.be.calledOnce; expect(iapIsValidatedStub).to.be.calledOnce;
expect(iapIsValidatedStub).to.be.calledWith( expect(iapIsValidatedStub).to.be.calledWith({});
{ productId: sku },
);
expect(paymentBuySkuStub).to.be.calledOnce; expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuySkuStub).to.be.calledWith({ expect(paymentBuyGemsStub).to.be.calledWith({
user, user,
gift: {
uuid: receivingUser._id,
member: sinon.match({ _id: receivingUser._id }),
},
paymentMethod: googlePayments.constants.PAYMENT_METHOD_GOOGLE, paymentMethod: googlePayments.constants.PAYMENT_METHOD_GOOGLE,
sku, gemsBlock,
headers, headers,
gift: {
type: 'gems',
gems: { amount: 21 },
member: sinon.match({ _id: receivingUser._id }),
uuid: receivingUser._id,
},
}); });
}); });
}); });

View File

@@ -203,28 +203,6 @@ describe('payments/index', () => {
expect(recipient.purchased.plan.dateCreated).to.exist; expect(recipient.purchased.plan.dateCreated).to.exist;
}); });
it('sets plan.dateCurrentTypeCreated if it did not previously exist', async () => {
expect(recipient.purchased.plan.dateCurrentTypeCreated).to.not.exist;
await api.createSubscription(data);
expect(recipient.purchased.plan.dateCurrentTypeCreated).to.exist;
});
it('keeps plan.dateCreated when changing subscription type', async () => {
await api.createSubscription(data);
const initialDate = recipient.purchased.plan.dateCreated;
await api.createSubscription(data);
expect(recipient.purchased.plan.dateCreated).to.eql(initialDate);
});
it('sets plan.dateCurrentTypeCreated when changing subscription type', async () => {
await api.createSubscription(data);
const initialDate = recipient.purchased.plan.dateCurrentTypeCreated;
await api.createSubscription(data);
expect(recipient.purchased.plan.dateCurrentTypeCreated).to.not.eql(initialDate);
});
it('does not change plan.customerId if it already exists', async () => { it('does not change plan.customerId if it already exists', async () => {
recipient.purchased.plan = plan; recipient.purchased.plan = plan;
data.customerId = 'purchaserCustomerId'; data.customerId = 'purchaserCustomerId';
@@ -235,116 +213,6 @@ describe('payments/index', () => {
expect(recipient.purchased.plan.customerId).to.eql('customer-id'); expect(recipient.purchased.plan.customerId).to.eql('customer-id');
}); });
it('sets plan.perkMonthCount to 1 if user is not subscribed', async () => {
recipient.purchased.plan = plan;
recipient.purchased.plan.perkMonthCount = 1;
recipient.purchased.plan.customerId = undefined;
data.sub.key = 'basic_earned';
data.gift.subscription.key = 'basic_earned';
data.gift.subscription.months = 1;
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
await api.createSubscription(data);
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
});
it('sets plan.perkMonthCount to 1 if field is not initialized', async () => {
recipient.purchased.plan = plan;
recipient.purchased.plan.perkMonthCount = -1;
recipient.purchased.plan.customerId = undefined;
data.sub.key = 'basic_earned';
data.gift.subscription.key = 'basic_earned';
data.gift.subscription.months = 1;
expect(recipient.purchased.plan.perkMonthCount).to.eql(-1);
await api.createSubscription(data);
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
});
it('sets plan.perkMonthCount to 1 if user had previous count but lapsed subscription', async () => {
recipient.purchased.plan = plan;
recipient.purchased.plan.perkMonthCount = 2;
recipient.purchased.plan.customerId = undefined;
data.sub.key = 'basic_earned';
data.gift.subscription.key = 'basic_earned';
data.gift.subscription.months = 1;
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
await api.createSubscription(data);
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
});
it('adds to plan.perkMonthCount if user is already subscribed', async () => {
recipient.purchased.plan = plan;
recipient.purchased.plan.perkMonthCount = 1;
data.sub.key = 'basic_earned';
data.gift.subscription.key = 'basic_earned';
data.gift.subscription.months = 1;
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
await api.createSubscription(data);
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
});
it('awards perks if plan.perkMonthCount reaches 3 with existing subscription', async () => {
recipient.purchased.plan = plan;
recipient.purchased.plan.perkMonthCount = 2;
data.sub.key = 'basic_earned';
data.gift.subscription.key = 'basic_earned';
data.gift.subscription.months = 1;
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
await api.createSubscription(data);
expect(recipient.purchased.plan.perkMonthCount).to.eql(0);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(5);
});
it('awards perks if plan.perkMonthCount reaches 3 without existing subscription', async () => {
recipient.purchased.plan.perkMonthCount = 0;
expect(recipient.purchased.plan.perkMonthCount).to.eql(0);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
await api.createSubscription(data);
expect(recipient.purchased.plan.perkMonthCount).to.eql(0);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(5);
});
it('awards perks if plan.perkMonthCount reaches 3 without initialized field', async () => {
expect(recipient.purchased.plan.perkMonthCount).to.eql(-1);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
await api.createSubscription(data);
expect(recipient.purchased.plan.perkMonthCount).to.eql(0);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(5);
});
it('awards perks if plan.perkMonthCount goes over 3', async () => {
recipient.purchased.plan = plan;
recipient.purchased.plan.perkMonthCount = 2;
data.sub.key = 'basic_earned';
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
await api.createSubscription(data);
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(5);
});
it('sets plan.customerId to "Gift" if it does not already exist', async () => { it('sets plan.customerId to "Gift" if it does not already exist', async () => {
expect(recipient.purchased.plan.customerId).to.not.exist; expect(recipient.purchased.plan.customerId).to.not.exist;
@@ -511,7 +379,6 @@ describe('payments/index', () => {
expect(user.purchased.plan.customerId).to.eql('customer-id'); expect(user.purchased.plan.customerId).to.eql('customer-id');
expect(user.purchased.plan.dateUpdated).to.exist; expect(user.purchased.plan.dateUpdated).to.exist;
expect(user.purchased.plan.gemsBought).to.eql(0); expect(user.purchased.plan.gemsBought).to.eql(0);
expect(user.purchased.plan.perkMonthCount).to.eql(0);
expect(user.purchased.plan.paymentMethod).to.eql('Payment Method'); expect(user.purchased.plan.paymentMethod).to.eql('Payment Method');
expect(user.purchased.plan.extraMonths).to.eql(0); expect(user.purchased.plan.extraMonths).to.eql(0);
expect(user.purchased.plan.dateTerminated).to.eql(null); expect(user.purchased.plan.dateTerminated).to.eql(null);
@@ -519,63 +386,6 @@ describe('payments/index', () => {
expect(user.purchased.plan.dateCreated).to.exist; expect(user.purchased.plan.dateCreated).to.exist;
}); });
it('sets plan.dateCreated if it did not previously exist', async () => {
expect(user.purchased.plan.dateCreated).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.dateCreated).to.exist;
});
it('sets plan.dateCurrentTypeCreated if it did not previously exist', async () => {
expect(user.purchased.plan.dateCurrentTypeCreated).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.dateCurrentTypeCreated).to.exist;
});
it('keeps plan.dateCreated when changing subscription type', async () => {
await api.createSubscription(data);
const initialDate = user.purchased.plan.dateCreated;
await api.createSubscription(data);
expect(user.purchased.plan.dateCreated).to.eql(initialDate);
});
it('sets plan.dateCurrentTypeCreated when changing subscription type', async () => {
await api.createSubscription(data);
const initialDate = user.purchased.plan.dateCurrentTypeCreated;
await api.createSubscription(data);
expect(user.purchased.plan.dateCurrentTypeCreated).to.not.eql(initialDate);
});
it('keeps plan.perkMonthCount when changing subscription type', async () => {
await api.createSubscription(data);
user.purchased.plan.perkMonthCount = 2;
await api.createSubscription(data);
expect(user.purchased.plan.perkMonthCount).to.eql(2);
});
it('sets plan.perkMonthCount to zero when creating new monthly subscription', async () => {
user.purchased.plan.perkMonthCount = 2;
await api.createSubscription(data);
expect(user.purchased.plan.perkMonthCount).to.eql(0);
});
it('sets plan.perkMonthCount to zero when creating new 3 month subscription', async () => {
user.purchased.plan.perkMonthCount = 2;
await api.createSubscription(data);
expect(user.purchased.plan.perkMonthCount).to.eql(0);
});
it('updates plan.consecutive.offset when changing subscription type', async () => {
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.offset).to.eql(3);
data.sub.key = 'basic_6mo';
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.offset).to.eql(6);
});
it('awards the Royal Purple Jackalope pet', async () => { it('awards the Royal Purple Jackalope pet', async () => {
await api.createSubscription(data); await api.createSubscription(data);
@@ -655,89 +465,6 @@ describe('payments/index', () => {
}, },
}); });
}); });
context('Upgrades subscription', () => {
it('from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.customerId).to.eql('customer-id');
const created = user.purchased.plan.dateCreated;
const updated = user.purchased.plan.dateUpdated;
data.sub.key = 'basic_6mo';
data.updatedFrom = { key: 'basic_earned' };
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.dateCreated).to.eql(created);
expect(user.purchased.plan.dateUpdated).to.not.eql(updated);
expect(user.purchased.plan.customerId).to.eql('customer-id');
});
it('from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.customerId).to.eql('customer-id');
const created = user.purchased.plan.dateCreated;
const updated = user.purchased.plan.dateUpdated;
data.sub.key = 'basic_12mo';
data.updatedFrom = { key: 'basic_3mo' };
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.dateCreated).to.eql(created);
expect(user.purchased.plan.dateUpdated).to.not.eql(updated);
expect(user.purchased.plan.customerId).to.eql('customer-id');
});
});
context('Downgrades subscription', () => {
it('from basic_6mo to basic_earned', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.customerId).to.eql('customer-id');
const created = user.purchased.plan.dateCreated;
const updated = user.purchased.plan.dateUpdated;
data.sub.key = 'basic_earned';
data.updatedFrom = { key: 'basic_6mo' };
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.dateCreated).to.eql(created);
expect(user.purchased.plan.dateUpdated).to.not.eql(updated);
expect(user.purchased.plan.customerId).to.eql('customer-id');
});
it('from basic_12mo to basic_3mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
data.sub.key = 'basic_12mo';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.customerId).to.eql('customer-id');
const created = user.purchased.plan.dateCreated;
const updated = user.purchased.plan.dateUpdated;
data.sub.key = 'basic_3mo';
data.updatedFrom = { key: 'basic_12mo' };
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.dateCreated).to.eql(created);
expect(user.purchased.plan.dateUpdated).to.not.eql(updated);
expect(user.purchased.plan.customerId).to.eql('customer-id');
});
});
}); });
context('Block subscription perks', () => { context('Block subscription perks', () => {
@@ -748,19 +475,9 @@ describe('payments/index', () => {
}); });
it('does not add to plans.consecutive.offset if 1 month subscription', async () => { it('does not add to plans.consecutive.offset if 1 month subscription', async () => {
data.sub.key = 'basic_earned';
await api.createSubscription(data); await api.createSubscription(data);
expect(user.purchased.plan.consecutive.offset).to.eql(0); expect(user.purchased.plan.extraMonths).to.eql(0);
});
it('resets plans.consecutive.offset if 1 month subscription', async () => {
user.purchased.plan.consecutive.offset = 1;
await user.save();
data.sub.key = 'basic_earned';
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.offset).to.eql(0);
}); });
it('adds 5 to plan.consecutive.gemCapExtra for 3 month block', async () => { it('adds 5 to plan.consecutive.gemCapExtra for 3 month block', async () => {
@@ -771,6 +488,7 @@ describe('payments/index', () => {
it('adds 10 to plan.consecutive.gemCapExtra for 6 month block', async () => { it('adds 10 to plan.consecutive.gemCapExtra for 6 month block', async () => {
data.sub.key = 'basic_6mo'; data.sub.key = 'basic_6mo';
await api.createSubscription(data); await api.createSubscription(data);
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10); expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
@@ -778,6 +496,7 @@ describe('payments/index', () => {
it('adds 20 to plan.consecutive.gemCapExtra for 12 month block', async () => { it('adds 20 to plan.consecutive.gemCapExtra for 12 month block', async () => {
data.sub.key = 'basic_12mo'; data.sub.key = 'basic_12mo';
await api.createSubscription(data); await api.createSubscription(data);
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20); expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
@@ -813,532 +532,6 @@ describe('payments/index', () => {
expect(user.purchased.plan.consecutive.trinkets).to.eql(4); expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
}); });
context('Upgrades subscription', () => {
context('Using payDifference logic', () => {
beforeEach(async () => {
data.updatedFrom = { logic: 'payDifference' };
});
it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
});
it('Adds 15 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_3mo';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
});
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
});
it('Adds 2 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_6mo';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
});
it('Adds 3 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_3mo';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
});
});
context('Using payFull logic', () => {
beforeEach(async () => {
data.updatedFrom = { logic: 'payFull' };
});
it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
});
it('Adds 20 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_3mo';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(25);
});
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
});
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_6mo';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(6);
});
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_3mo';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(5);
});
});
context('Using refundAndRepay logic', () => {
let clock;
beforeEach(async () => {
clock = sinon.useFakeTimers(new Date('2022-01-01'));
data.updatedFrom = { logic: 'refundAndRepay' };
});
context('Upgrades within first half of subscription', () => {
it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-01-10'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
});
it('Adds 15 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_3mo';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-02-05'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
});
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-01-08'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
});
it('Adds 3 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_3mo';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-01-31'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
});
it('Adds 2 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_6mo';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-01-28'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
});
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo after initial cycle', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
clock.restore();
clock = sinon.useFakeTimers(new Date('2024-01-08'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
});
it('Adds 2 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo after initial cycle', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_6mo';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-08-28'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
});
it('Adds 3 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo after initial cycle', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_3mo';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-07-31'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
});
});
context('Upgrades within second half of subscription', () => {
it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-01-20'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
});
it('Adds 20 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_3mo';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-02-24'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(25);
});
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-01-28'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
});
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_6mo';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-05-28'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(6);
});
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_3mo';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-03-03'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(5);
});
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo after initial cycle', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-05-28'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
});
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo after initial cycle', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_6mo';
clock.restore();
clock = sinon.useFakeTimers(new Date('2023-05-28'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(6);
});
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo after initial cycle', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_3mo';
clock.restore();
clock = sinon.useFakeTimers(new Date('2023-09-03'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(5);
});
});
afterEach(async () => {
if (clock !== null) clock.restore();
});
});
});
context('Downgrades subscription', () => {
it('does not remove from plan.consecutive.gemCapExtra from basic_6mo to basic_earned', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
data.sub.key = 'basic_earned';
data.updatedFrom = { key: 'basic_6mo' };
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
});
it('does not remove from plan.consecutive.gemCapExtra from basic_12mo to basic_3mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
data.sub.key = 'basic_12mo';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
data.sub.key = 'basic_3mo';
data.updatedFrom = { key: 'basic_12mo' };
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
});
it('does not remove from plan.consecutive.trinkets from basic_6mo to basic_earned', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
data.sub.key = 'basic_earned';
data.updatedFrom = { key: 'basic_6mo' };
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
});
it('does not remove from plan.consecutive.trinkets from basic_12mo to basic_3mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
data.sub.key = 'basic_12mo';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
data.sub.key = 'basic_3mo';
data.updatedFrom = { key: 'basic_12mo' };
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
});
});
}); });
context('Mystery Items', () => { context('Mystery Items', () => {
@@ -1382,6 +575,18 @@ describe('payments/index', () => {
expect(user.purchased.plan.mysteryItems).to.have.a.lengthOf(1); expect(user.purchased.plan.mysteryItems).to.have.a.lengthOf(1);
expect(user.purchased.plan.mysteryItems).to.include('head_mystery_201605'); expect(user.purchased.plan.mysteryItems).to.include('head_mystery_201605');
}); });
it('does not award mystery item when user already has the item in the mystery box', async () => {
user.purchased.plan.mysteryItems = [mayMysteryItem];
sandbox.spy(user.purchased.plan.mysteryItems, 'push');
data = { paymentMethod: 'PaymentMethod', user, sub: { key: 'basic_3mo' } };
await api.createSubscription(data);
expect(user.purchased.plan.mysteryItems.push).to.be.calledOnce;
expect(user.purchased.plan.mysteryItems.push).to.be.calledWith('head_mystery_201605');
});
}); });
}); });
@@ -1587,10 +792,10 @@ describe('payments/index', () => {
it('sends gem donation message in each participant\'s language', async () => { it('sends gem donation message in each participant\'s language', async () => {
// TODO using english for both users because other languages are not loaded // TODO using english for both users because other languages are not loaded
// for api.buyGems // for api.buyGems
await recipient.updateOne({ await recipient.update({
'preferences.language': 'en', 'preferences.language': 'en',
}); });
await user.updateOne({ await user.update({
'preferences.language': 'en', 'preferences.language': 'en',
}); });
await api.buyGems(data); await api.buyGems(data);

View File

@@ -1,40 +0,0 @@
import {
canBuySkuItem,
} from '../../../../../website/server/libs/payments/skuItem';
import { model as User } from '../../../../../website/server/models/user';
describe('payments/skuItems', () => {
let user;
let clock;
beforeEach(() => {
user = new User();
clock = null;
});
afterEach(() => {
if (clock !== null) clock.restore();
});
describe('#canBuySkuItem', () => {
it('returns true for random sku', () => {
expect(canBuySkuItem('something', user)).to.be.true;
});
describe('#gryphatrice', () => {
const sku = 'Pet-Gryphatrice-Jubilant';
it('returns true during birthday week', () => {
clock = sinon.useFakeTimers(new Date('2023-01-31'));
expect(canBuySkuItem(sku, user)).to.be.true;
});
it('returns false outside of birthday week', () => {
clock = sinon.useFakeTimers(new Date('2023-01-20'));
expect(canBuySkuItem(sku, user)).to.be.false;
});
it('returns false if user already owns it', () => {
clock = sinon.useFakeTimers(new Date('2023-02-01'));
user.items.pets['Gryphatrice-Jubilant'] = 5;
expect(canBuySkuItem(sku, user)).to.be.false;
});
});
});
});

View File

@@ -53,9 +53,11 @@ describe('cron middleware', () => {
cronMiddleware(req, res, err => { cronMiddleware(req, res, err => {
if (err) return reject(err); if (err) return reject(err);
Tasks.Task.findOne({ _id: task }).then(foundTask => { Tasks.Task.findOne({ _id: task }, (secondErr, taskFound) => {
expect(foundTask).to.not.exist; if (secondErr) return reject(err);
resolve(); expect(secondErr).to.not.exist;
expect(taskFound).to.not.exist;
return resolve();
}); });
return null; return null;
@@ -76,8 +78,10 @@ describe('cron middleware', () => {
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => { cronMiddleware(req, res, err => {
if (err) return reject(err); if (err) return reject(err);
Tasks.Task.findOne({ _id: task }).then(foundTask => { Tasks.Task.findOne({ _id: task }, (secondErr, taskFound) => {
expect(foundTask).to.exist; if (secondErr) return reject(secondErr);
expect(secondErr).to.not.exist;
expect(taskFound).to.exist;
return resolve(); return resolve();
}); });
return null; return null;
@@ -99,8 +103,10 @@ describe('cron middleware', () => {
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => { cronMiddleware(req, res, err => {
if (err) return reject(err); if (err) return reject(err);
Tasks.Task.findOne({ _id: task }).then(foundTask => { Tasks.Task.findOne({ _id: task }, (secondErr, taskFound) => {
expect(foundTask).to.not.exist; if (secondErr) return reject(secondErr);
expect(secondErr).to.not.exist;
expect(taskFound).to.not.exist;
return resolve(); return resolve();
}); });
return null; return null;
@@ -164,7 +170,8 @@ describe('cron middleware', () => {
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => { cronMiddleware(req, res, err => {
if (err) return reject(err); if (err) return reject(err);
return User.findOne({ _id: user._id }).then(updatedUser => { return User.findOne({ _id: user._id }, (secondErr, updatedUser) => {
if (secondErr) return reject(secondErr);
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore); expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
return resolve(); return resolve();
}); });
@@ -181,7 +188,8 @@ describe('cron middleware', () => {
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => { cronMiddleware(req, res, err => {
if (err) return reject(err); if (err) return reject(err);
return Tasks.Task.findOne({ _id: todo._id }).then(todoFound => { return Tasks.Task.findOne({ _id: todo._id }, (secondErr, todoFound) => {
if (secondErr) return reject(secondErr);
expect(todoFound.value).to.be.lessThan(todoValueBefore); expect(todoFound.value).to.be.lessThan(todoValueBefore);
return resolve(); return resolve();
}); });
@@ -216,7 +224,8 @@ describe('cron middleware', () => {
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => { cronMiddleware(req, res, err => {
if (err) return reject(err); if (err) return reject(err);
return User.findOne({ _id: user._id }).then(updatedUser => { return User.findOne({ _id: user._id }, (secondErr, updatedUser) => {
if (secondErr) return reject(secondErr);
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore); expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
return resolve(); return resolve();
}); });
@@ -229,11 +238,11 @@ describe('cron middleware', () => {
await user.save(); await user.save();
const updatedUser = user.toObject(); const updatedUser = user.toObject();
updatedUser.matchedCount = 0; updatedUser.nMatched = 0;
sandbox.spy(cronLib, 'recoverCron'); sandbox.spy(cronLib, 'recoverCron');
sandbox.stub(User, 'updateOne') sandbox.stub(User, 'update')
.withArgs({ .withArgs({
_id: user._id, _id: user._id,
$or: [ $or: [
@@ -260,7 +269,7 @@ describe('cron middleware', () => {
it('cronSignature less than an hour ago should error', async () => { it('cronSignature less than an hour ago should error', async () => {
user.lastCron = moment(new Date()).subtract({ days: 2 }); user.lastCron = moment(new Date()).subtract({ days: 2 });
const now = new Date(); const now = new Date();
await User.updateOne({ await User.update({
_id: user._id, _id: user._id,
}, { }, {
$set: { $set: {
@@ -282,7 +291,7 @@ describe('cron middleware', () => {
it('cronSignature longer than an hour ago should allow cron', async () => { it('cronSignature longer than an hour ago should allow cron', async () => {
user.lastCron = moment(new Date()).subtract({ days: 2 }); user.lastCron = moment(new Date()).subtract({ days: 2 });
const now = new Date(); const now = new Date();
await User.updateOne({ await User.update({
_id: user._id, _id: user._id,
}, { }, {
$set: { $set: {

View File

@@ -1358,8 +1358,7 @@ describe('Group Model', () => {
describe('#sendChat', () => { describe('#sendChat', () => {
beforeEach(() => { beforeEach(() => {
sandbox.spy(User, 'updateOne'); sandbox.spy(User, 'update');
sandbox.spy(User, 'updateMany');
}); });
it('formats message', () => { it('formats message', () => {
@@ -1414,8 +1413,8 @@ describe('Group Model', () => {
it('updates users about new messages in party', () => { it('updates users about new messages in party', () => {
party.sendChat({ message: 'message' }); party.sendChat({ message: 'message' });
expect(User.updateMany).to.be.calledOnce; expect(User.update).to.be.calledOnce;
expect(User.updateMany).to.be.calledWithMatch({ expect(User.update).to.be.calledWithMatch({
'party._id': party._id, 'party._id': party._id,
_id: { $ne: '' }, _id: { $ne: '' },
}); });
@@ -1428,8 +1427,8 @@ describe('Group Model', () => {
group.sendChat({ message: 'message' }); group.sendChat({ message: 'message' });
expect(User.updateMany).to.be.calledOnce; expect(User.update).to.be.calledOnce;
expect(User.updateMany).to.be.calledWithMatch({ expect(User.update).to.be.calledWithMatch({
guilds: group._id, guilds: group._id,
_id: { $ne: '' }, _id: { $ne: '' },
}); });
@@ -1438,8 +1437,8 @@ describe('Group Model', () => {
it('does not send update to user that sent the message', () => { it('does not send update to user that sent the message', () => {
party.sendChat({ message: 'message', user: { _id: 'user-id', profile: { name: 'user' } } }); party.sendChat({ message: 'message', user: { _id: 'user-id', profile: { name: 'user' } } });
expect(User.updateMany).to.be.calledOnce; expect(User.update).to.be.calledOnce;
expect(User.updateMany).to.be.calledWithMatch({ expect(User.update).to.be.calledWithMatch({
'party._id': party._id, 'party._id': party._id,
_id: { $ne: 'user-id' }, _id: { $ne: 'user-id' },
}); });
@@ -1450,7 +1449,7 @@ describe('Group Model', () => {
party.sendChat({ message: 'message' }); party.sendChat({ message: 'message' });
expect(User.updateMany).to.not.be.called; expect(User.update).to.not.be.called;
}); });
it('skips sending messages to the tavern', () => { it('skips sending messages to the tavern', () => {
@@ -1458,7 +1457,7 @@ describe('Group Model', () => {
party.sendChat({ message: 'message' }); party.sendChat({ message: 'message' });
expect(User.updateMany).to.not.be.called; expect(User.update).to.not.be.called;
}); });
}); });
@@ -1732,7 +1731,7 @@ describe('Group Model', () => {
}); });
it('updates participting members (not including user)', async () => { it('updates participting members (not including user)', async () => {
sandbox.spy(User, 'updateMany'); sandbox.spy(User, 'update');
await party.startQuest(nonParticipatingMember); await party.startQuest(nonParticipatingMember);
@@ -1740,7 +1739,7 @@ describe('Group Model', () => {
questLeader._id, participatingMember._id, sleepingParticipatingMember._id, questLeader._id, participatingMember._id, sleepingParticipatingMember._id,
]; ];
expect(User.updateMany).to.be.calledWith( expect(User.update).to.be.calledWith(
{ _id: { $in: members } }, { _id: { $in: members } },
{ {
$set: { $set: {
@@ -1753,11 +1752,11 @@ describe('Group Model', () => {
}); });
it('updates non-user quest leader and decrements quest scroll', async () => { it('updates non-user quest leader and decrements quest scroll', async () => {
sandbox.spy(User, 'updateOne'); sandbox.spy(User, 'update');
await party.startQuest(participatingMember); await party.startQuest(participatingMember);
expect(User.updateOne).to.be.calledWith( expect(User.update).to.be.calledWith(
{ _id: questLeader._id }, { _id: questLeader._id },
{ {
$inc: { $inc: {
@@ -1819,29 +1818,29 @@ describe('Group Model', () => {
}; };
it('doesn\'t retry successful operations', async () => { it('doesn\'t retry successful operations', async () => {
sandbox.stub(User, 'updateOne').returns(successfulMock); sandbox.stub(User, 'update').returns(successfulMock);
await party.finishQuest(quest); await party.finishQuest(quest);
expect(User.updateOne).to.be.calledThrice; expect(User.update).to.be.calledThrice;
}); });
it('stops retrying when a successful update has occurred', async () => { it('stops retrying when a successful update has occurred', async () => {
const updateStub = sandbox.stub(User, 'updateOne'); const updateStub = sandbox.stub(User, 'update');
updateStub.onCall(0).returns(failedMock); updateStub.onCall(0).returns(failedMock);
updateStub.returns(successfulMock); updateStub.returns(successfulMock);
await party.finishQuest(quest); await party.finishQuest(quest);
expect(User.updateOne.callCount).to.equal(4); expect(User.update.callCount).to.equal(4);
}); });
it('retries failed updates at most five times per user', async () => { it('retries failed updates at most five times per user', async () => {
sandbox.stub(User, 'updateOne').returns(failedMock); sandbox.stub(User, 'update').returns(failedMock);
await expect(party.finishQuest(quest)).to.eventually.be.rejected; await expect(party.finishQuest(quest)).to.eventually.be.rejected;
expect(User.updateOne.callCount).to.eql(15); // for 3 users expect(User.update.callCount).to.eql(15); // for 3 users
}); });
}); });
@@ -2088,17 +2087,17 @@ describe('Group Model', () => {
context('Party quests', () => { context('Party quests', () => {
it('updates participating members with rewards', async () => { it('updates participating members with rewards', async () => {
sandbox.spy(User, 'updateOne'); sandbox.spy(User, 'update');
await party.finishQuest(quest); await party.finishQuest(quest);
expect(User.updateOne).to.be.calledThrice; expect(User.update).to.be.calledThrice;
expect(User.updateOne).to.be.calledWithMatch({ expect(User.update).to.be.calledWithMatch({
_id: questLeader._id, _id: questLeader._id,
}); });
expect(User.updateOne).to.be.calledWithMatch({ expect(User.update).to.be.calledWithMatch({
_id: participatingMember._id, _id: participatingMember._id,
}); });
expect(User.updateOne).to.be.calledWithMatch({ expect(User.update).to.be.calledWithMatch({
_id: sleepingParticipatingMember._id, _id: sleepingParticipatingMember._id,
}); });
}); });
@@ -2173,11 +2172,11 @@ describe('Group Model', () => {
}); });
it('updates all users with rewards', async () => { it('updates all users with rewards', async () => {
sandbox.spy(User, 'updateMany'); sandbox.spy(User, 'update');
await party.finishQuest(tavernQuest); await party.finishQuest(tavernQuest);
expect(User.updateMany).to.be.calledOnce; expect(User.update).to.be.calledOnce;
expect(User.updateMany).to.be.calledWithMatch({}); expect(User.update).to.be.calledWithMatch({});
}); });
it('sets quest completed to the world quest key', async () => { it('sets quest completed to the world quest key', async () => {

View File

@@ -103,7 +103,7 @@ describe('NewsPost Model', () => {
beforeEach(async () => { beforeEach(async () => {
// Delete all existing posts from the database // Delete all existing posts from the database
await NewsPost.deleteMany(); await NewsPost.remove();
}); });
afterEach(() => { afterEach(() => {
@@ -116,7 +116,7 @@ describe('NewsPost Model', () => {
_id: v4(), publishDate: new Date(), published: true, _id: v4(), publishDate: new Date(), published: true,
}; };
NewsPost.updateLastNewsPost(previousPost); NewsPost.updateLastNewsPost(previousPost);
intervalId = refreshNewsPost(100); // refreshes every 100ms intervalId = refreshNewsPost(50); // refreshes every 50ms
await sleep(0.1); // wait 100ms to make sure the new post has a more recent publishDate await sleep(0.1); // wait 100ms to make sure the new post has a more recent publishDate
const newPost = await NewsPost.create({ const newPost = await NewsPost.create({

View File

@@ -221,8 +221,7 @@ describe('Task Model', () => {
it('returns task by alias', async () => { it('returns task by alias', async () => {
const foundTasks = await Tasks.Task.findMultipleByIdOrAlias( const foundTasks = await Tasks.Task.findMultipleByIdOrAlias(
[taskWithAlias.alias], [taskWithAlias.alias], user._id,
user._id,
); );
expect(foundTasks[0].text).to.eql(taskWithAlias.text); expect(foundTasks[0].text).to.eql(taskWithAlias.text);
@@ -230,8 +229,7 @@ describe('Task Model', () => {
it('returns multiple tasks', async () => { it('returns multiple tasks', async () => {
const foundTasks = await Tasks.Task.findMultipleByIdOrAlias( const foundTasks = await Tasks.Task.findMultipleByIdOrAlias(
[taskWithAlias.alias, secondTask._id], [taskWithAlias.alias, secondTask._id], user._id,
user._id,
); );
expect(foundTasks.length).to.eql(2); expect(foundTasks.length).to.eql(2);
@@ -241,8 +239,7 @@ describe('Task Model', () => {
it('returns a task only once if searched by both id and alias', async () => { it('returns a task only once if searched by both id and alias', async () => {
const foundTasks = await Tasks.Task.findMultipleByIdOrAlias( const foundTasks = await Tasks.Task.findMultipleByIdOrAlias(
[taskWithAlias.alias, taskWithAlias._id], [taskWithAlias.alias, taskWithAlias._id], user._id,
user._id,
); );
expect(foundTasks.length).to.eql(1); expect(foundTasks.length).to.eql(1);

View File

@@ -188,7 +188,7 @@ describe('User Model', () => {
it('removes invalid tags when loading the user', async () => { it('removes invalid tags when loading the user', async () => {
let user = new User(); let user = new User();
await user.save(); await user.save();
await user.updateOne({ await user.update({
$set: { $set: {
tags: [ tags: [
null, // invalid, not an object null, // invalid, not an object
@@ -212,7 +212,7 @@ describe('User Model', () => {
it('removes invalid push devices when loading the user', async () => { it('removes invalid push devices when loading the user', async () => {
let user = new User(); let user = new User();
await user.save(); await user.save();
await user.updateOne({ await user.update({
$set: { $set: {
pushDevices: [ pushDevices: [
null, // invalid, not an object null, // invalid, not an object
@@ -236,7 +236,7 @@ describe('User Model', () => {
it('removes duplicate push devices when loading the user', async () => { it('removes duplicate push devices when loading the user', async () => {
let user = new User(); let user = new User();
await user.save(); await user.save();
await user.updateOne({ await user.update({
$set: { $set: {
pushDevices: [ pushDevices: [
{ type: 'android', regId: '1234' }, { type: 'android', regId: '1234' },
@@ -258,7 +258,7 @@ describe('User Model', () => {
it('removes invalid notifications when loading the user', async () => { it('removes invalid notifications when loading the user', async () => {
let user = new User(); let user = new User();
await user.save(); await user.save();
await user.updateOne({ await user.update({
$set: { $set: {
notifications: [ notifications: [
null, // invalid, not an object null, // invalid, not an object
@@ -284,7 +284,7 @@ describe('User Model', () => {
it('removes multiple NEW_CHAT_MESSAGE for the same group', async () => { it('removes multiple NEW_CHAT_MESSAGE for the same group', async () => {
let user = new User(); let user = new User();
await user.save(); await user.save();
await user.updateOne({ await user.update({
$set: { $set: {
notifications: [ notifications: [
{ {

View File

@@ -87,7 +87,7 @@ describe('DELETE /challenges/:challengeId', () => {
const testTask = _.find(tasks, task => task.text === taskText); const testTask = _.find(tasks, task => task.text === taskText);
expect(testTask.challenge.broken).to.eql('CHALLENGE_DELETED'); expect(testTask.challenge.broken).to.eql('CHALLENGE_DELETED');
expect(testTask.challenge.winner).to.be.undefined; expect(testTask.challenge.winner).to.be.null;
}); });
}); });
}); });

View File

@@ -16,7 +16,60 @@ describe('GET /challenges/:challengeId', () => {
}); });
}); });
context('Group Plan', () => { 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', () => {
let groupLeader; let groupLeader;
let challengeLeader; let challengeLeader;
let group; let group;
@@ -31,14 +84,14 @@ describe('GET /challenges/:challengeId', () => {
const populatedGroup = await createAndPopulateGroup({ const populatedGroup = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' }, groupDetails: { type: 'guild', privacy: 'private' },
members: 2, members: 2,
upgradeToGroupPlan: true,
}); });
groupLeader = populatedGroup.groupLeader; groupLeader = populatedGroup.groupLeader;
group = populatedGroup.group; group = populatedGroup.group;
members = populatedGroup.members; members = populatedGroup.members;
[challengeLeader, otherMember] = members; challengeLeader = members[0]; // eslint-disable-line prefer-destructuring
otherMember = members[1]; // eslint-disable-line prefer-destructuring
challenge = await generateChallenge(challengeLeader, group); challenge = await generateChallenge(challengeLeader, group);
}); });

View File

@@ -74,7 +74,7 @@ describe('GET /challenges/:challengeId/export/csv', () => {
}); });
it('should successfully return when it contains erroneous residue user data', async () => { it('should successfully return when it contains erroneous residue user data', async () => {
await members[0].updateOne({ challenges: [] }); await members[0].update({ challenges: [] });
const res = await members[1].get(`/challenges/${challenge._id}/export/csv`); const res = await members[1].get(`/challenges/${challenge._id}/export/csv`);
const sortedMembers = _.sortBy([members[1], members[2], groupLeader], '_id'); const sortedMembers = _.sortBy([members[1], members[2], groupLeader], '_id');
const splitRes = res.split('\n'); const splitRes = res.split('\n');

View File

@@ -71,18 +71,42 @@ describe('GET /challenges/:challengeId/members', () => {
}); });
}); });
it('populates only some fields', async () => { it('works with challenges belonging to public guild', async () => {
const group = await generateGroup(user, { type: 'party', privacy: 'private', name: generateUUID() }); const leader = await generateUser({ balance: 4 });
const challenge = await generateChallenge(user, group); const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() });
await user.post(`/challenges/${challenge._id}/join`); const challenge = await generateChallenge(leader, group);
await leader.post(`/challenges/${challenge._id}/join`);
const res = await user.get(`/challenges/${challenge._id}/members`); const res = await user.get(`/challenges/${challenge._id}/members`);
expect(res[0]).to.eql({ expect(res[0]).to.eql({
_id: user._id, _id: leader._id,
id: user._id, id: leader._id,
profile: { name: user.profile.name }, profile: { name: leader.profile.name },
auth: { auth: {
local: { local: {
username: user.auth.local.username, 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 res = await user.get(`/challenges/${challenge._id}/members`);
expect(res[0]).to.eql({
_id: anotherUser._id,
id: anotherUser._id,
profile: { name: anotherUser.profile.name },
auth: {
local: {
username: anotherUser.auth.local.username,
}, },
}, },
flags: { flags: {

View File

@@ -72,6 +72,20 @@ 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 () => { it('returns the member tasks for the challenges', async () => {
const group = await generateGroup(user, { type: 'party', name: generateUUID() }); const group = await generateGroup(user, { type: 'party', name: generateUUID() });
const challenge = await generateChallenge(user, group); const challenge = await generateChallenge(user, group);

View File

@@ -7,7 +7,117 @@ import {
import { TAVERN_ID } from '../../../../../website/common/script/constants'; import { TAVERN_ID } from '../../../../../website/common/script/constants';
describe('GET challenges/groups/:groupId', () => { describe('GET challenges/groups/:groupId', () => {
context('Group Plan', () => { 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', () => {
let privateGuild; let user; let nonMember; let challenge; let let privateGuild; let user; let nonMember; let challenge; let
challenge2; challenge2;
@@ -18,7 +128,6 @@ describe('GET challenges/groups/:groupId', () => {
type: 'guild', type: 'guild',
privacy: 'private', privacy: 'private',
}, },
upgradeToGroupPlan: true,
}); });
privateGuild = group; privateGuild = group;
@@ -77,6 +186,68 @@ 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', () => { context('Party', () => {
let party; let user; let nonMember; let challenge; let let party; let user; let nonMember; let challenge; let
challenge2; challenge2;
@@ -186,7 +357,7 @@ describe('GET challenges/groups/:groupId', () => {
before(async () => { before(async () => {
user = await generateUser(); user = await generateUser();
await user.updateOne({ balance: 0.5 }); await user.update({ balance: 0.5 });
tavern = await user.get(`/groups/${TAVERN_ID}`); tavern = await user.get(`/groups/${TAVERN_ID}`);
challenge = await generateChallenge(user, tavern, { prize: 1 }); challenge = await generateChallenge(user, tavern, { prize: 1 });
@@ -230,7 +401,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 challenges = await user.get('/challenges/groups/habitrpg');
const foundChallenge1 = _.find(challenges, { _id: challenge._id }); const foundChallenge1 = _.find(challenges, { _id: challenge._id });
@@ -264,58 +435,5 @@ describe('GET challenges/groups/:groupId', () => {
}, },
}); });
}); });
context('official challenge is present', () => {
let officialChallenge; let unofficialChallenges;
before(async () => {
await user.updateOne({
'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);
});
});
}); });
}); });

View File

@@ -2,44 +2,39 @@ import {
generateUser, generateUser,
generateChallenge, generateChallenge,
createAndPopulateGroup, createAndPopulateGroup,
resetHabiticaDB,
} from '../../../../helpers/api-integration/v3'; } from '../../../../helpers/api-integration/v3';
import { TAVERN_ID } from '../../../../../website/common/script/constants';
describe('GET challenges/user', () => { describe('GET challenges/user', () => {
context('no official challenges', () => { context('no official challenges', () => {
let user; let member; let nonMember; let challenge; let challenge2; let publicChallenge; let user; let member; let nonMember; let challenge; let challenge2;
let groupPlan; let userData; let groupData; let tavern; let tavernData; let publicGuild; let userData; let groupData;
before(async () => { before(async () => {
await resetHabiticaDB();
const { group, groupLeader, members } = await createAndPopulateGroup({ const { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: { groupDetails: {
name: 'TestGuild', name: 'TestGuild',
type: 'guild', type: 'guild',
privacy: 'private', privacy: 'public',
}, },
members: 1, members: 1,
upgradeToGroupPlan: true,
}); });
groupPlan = group; publicGuild = group;
groupData = { groupData = {
_id: groupPlan._id, _id: publicGuild._id,
categories: [], categories: [],
id: groupPlan._id, id: publicGuild._id,
type: groupPlan.type, type: publicGuild.type,
privacy: groupPlan.privacy, privacy: publicGuild.privacy,
name: groupPlan.name, name: publicGuild.name,
summary: groupPlan.name, summary: publicGuild.name,
leader: groupPlan.leader._id, leader: publicGuild.leader._id,
}; };
user = groupLeader; user = groupLeader;
userData = { userData = {
_id: groupPlan.leader._id, _id: publicGuild.leader._id,
id: groupPlan.leader._id, id: publicGuild.leader._id,
profile: { name: user.profile.name }, profile: { name: user.profile.name },
auth: { auth: {
local: { local: {
@@ -51,31 +46,17 @@ 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 member = members[0]; // eslint-disable-line prefer-destructuring
nonMember = await generateUser(); nonMember = await generateUser();
challenge = await generateChallenge(user, group); challenge = await generateChallenge(user, group);
challenge2 = await generateChallenge(user, group); challenge2 = await generateChallenge(user, group);
await user.updateOne({ balance: 0.25 });
publicChallenge = await generateChallenge(user, tavern, { prize: 1 });
await member.post(`/challenges/${challenge._id}/join`); await nonMember.post(`/challenges/${challenge._id}/join`);
}); });
context('all challenges', () => { context('all challenges', () => {
it('should return challenges user has joined', async () => { it('should return challenges user has joined', async () => {
const challenges = await member.get('/challenges/user?page=0'); const challenges = await nonMember.get('/challenges/user?page=0');
const foundChallenge = _.find(challenges, { _id: challenge._id }); const foundChallenge = _.find(challenges, { _id: challenge._id });
expect(foundChallenge).to.exist; expect(foundChallenge).to.exist;
@@ -83,13 +64,11 @@ describe('GET challenges/user', () => {
expect(foundChallenge.group).to.eql(groupData); expect(foundChallenge.group).to.eql(groupData);
}); });
it('should return public challenges', async () => { it('should not return challenges a non-member has not joined', async () => {
const challenges = await nonMember.get('/challenges/user?page=0'); const challenges = await nonMember.get('/challenges/user?page=0');
const foundPublicChallenge = _.find(challenges, { _id: publicChallenge._id }); const foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundPublicChallenge).to.exist; expect(foundChallenge2).to.not.exist;
expect(foundPublicChallenge.leader).to.eql(userData);
expect(foundPublicChallenge.group).to.eql(tavernData);
}); });
it('should return challenges user has created', async () => { it('should return challenges user has created', async () => {
@@ -121,10 +100,10 @@ describe('GET challenges/user', () => {
it('should return newest challenges first', async () => { it('should return newest challenges first', async () => {
let challenges = await user.get('/challenges/user?page=0'); let challenges = await user.get('/challenges/user?page=0');
let foundChallengeIndex = _.findIndex(challenges, { _id: publicChallenge._id }); let foundChallengeIndex = _.findIndex(challenges, { _id: challenge2._id });
expect(foundChallengeIndex).to.eql(0); expect(foundChallengeIndex).to.eql(0);
const newChallenge = await generateChallenge(user, groupPlan); const newChallenge = await generateChallenge(user, publicGuild);
await user.post(`/challenges/${newChallenge._id}/join`); await user.post(`/challenges/${newChallenge._id}/join`);
challenges = await user.get('/challenges/user?page=0'); challenges = await user.get('/challenges/user?page=0');
@@ -134,23 +113,52 @@ describe('GET challenges/user', () => {
}); });
it('should not return challenges user doesn\'t have access to', async () => { 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 challenges = await nonMember.get('/challenges/user?page=0');
const foundChallenge = _.find(challenges, { _id: challenge._id }); const foundChallenge = _.find(challenges, { _id: privateChallenge._id });
expect(foundChallenge).to.not.exist; expect(foundChallenge).to.not.exist;
}); });
it('should not return challenges user doesn\'t have access to, even with query parameters', async () => { 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 challenges = await nonMember.get('/challenges/user?page=0&categories=academics&owned=not_owned');
const foundChallenge = _.find(challenges, { _id: challenge._id }); const foundChallenge = _.find(challenges, { _id: privateChallenge._id });
expect(foundChallenge).to.not.exist; expect(foundChallenge).to.not.exist;
}); });
}); });
context('my challenges', () => { context('my challenges', () => {
it('should return challenges user has joined', async () => { it('should return challenges user has joined', async () => {
const challenges = await member.get(`/challenges/user?page=0&member=${true}`); const challenges = await nonMember.get(`/challenges/user?page=0&member=${true}`);
const foundChallenge = _.find(challenges, { _id: challenge._id }); const foundChallenge = _.find(challenges, { _id: challenge._id });
expect(foundChallenge).to.exist; expect(foundChallenge).to.exist;
@@ -169,10 +177,6 @@ describe('GET challenges/user', () => {
expect(foundChallenge2).to.exist; expect(foundChallenge2).to.exist;
expect(foundChallenge2.leader).to.eql(userData); expect(foundChallenge2.leader).to.eql(userData);
expect(foundChallenge2.group).to.eql(groupData); 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 () => { it('should return challenges user has created if filter by owned', async () => {
@@ -186,10 +190,6 @@ describe('GET challenges/user', () => {
expect(foundChallenge2).to.exist; expect(foundChallenge2).to.exist;
expect(foundChallenge2.leader).to.eql(userData); expect(foundChallenge2.leader).to.eql(userData);
expect(foundChallenge2.group).to.eql(groupData); 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 () => { it('should not return challenges user has created if filter by not owned', async () => {
@@ -199,42 +199,38 @@ describe('GET challenges/user', () => {
expect(foundChallenge1).to.not.exist; expect(foundChallenge1).to.not.exist;
const foundChallenge2 = _.find(challenges, { _id: challenge2._id }); const foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundChallenge2).to.not.exist; 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 () => { it('should not return challenges in user groups', async () => {
const challenges = await member.get(`/challenges/user?page=0&member=${true}`); 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 }); const foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundChallenge2).to.not.exist; 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', () => { context('official challenge is present', () => {
let user; let officialChallenge; let unofficialChallenges; let let user; let officialChallenge; let unofficialChallenges; let
group; publicGuild;
before(async () => { before(async () => {
({ group, groupLeader: user } = await createAndPopulateGroup({ const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { groupDetails: {
name: 'TestGuild', name: 'TestGuild',
summary: 'summary for TestGuild', summary: 'summary for TestGuild',
type: 'guild', type: 'guild',
privacy: 'private', privacy: 'public',
}, },
upgradeToGroupPlan: true, });
}));
await user.updateOne({ user = groupLeader;
publicGuild = group;
await user.update({
'permissions.challengeAdmin': true, 'permissions.challengeAdmin': true,
}); });
@@ -275,7 +271,7 @@ describe('GET challenges/user', () => {
} }
}); });
const newChallenge = await generateChallenge(user, group); const newChallenge = await generateChallenge(user, publicGuild);
await user.post(`/challenges/${newChallenge._id}/join`); await user.post(`/challenges/${newChallenge._id}/join`);
challenges = await user.get('/challenges/user?page=0'); challenges = await user.get('/challenges/user?page=0');
@@ -298,17 +294,16 @@ describe('GET challenges/user', () => {
groupDetails: { groupDetails: {
name: 'TestGuild', name: 'TestGuild',
type: 'guild', type: 'guild',
privacy: 'private', privacy: 'public',
}, },
members: 1, members: 1,
upgradeToGroupPlan: true,
}); });
user = groupLeader; user = groupLeader;
guild = group; guild = group;
member = members[0]; // eslint-disable-line prefer-destructuring member = members[0]; // eslint-disable-line prefer-destructuring
await user.updateOne({ balance: 20 }); await user.update({ balance: 20 });
for (let i = 0; i < 11; i += 1) { for (let i = 0; i < 11; i += 1) {
let challenge = await generateChallenge(user, group); // eslint-disable-line let challenge = await generateChallenge(user, group); // eslint-disable-line

View File

@@ -1,79 +0,0 @@
import { v4 as generateUUID } from 'uuid';
import {
generateChallenge,
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('POST /challenges/:challengeId/flag', () => {
let user;
let challengeGroup;
let challenge;
beforeEach(async () => {
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'TestParty',
type: 'party',
privacy: 'private',
},
members: 1,
});
user = groupLeader;
challengeGroup = group;
challenge = await generateChallenge(user, group);
});
it('returns an error when challenge is not found', async () => {
await expect(user.post(`/challenges/${generateUUID()}/flag`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('challengeNotFound'),
});
});
it('flags a challenge', async () => {
const flagResult = await user.post(`/challenges/${challenge._id}/flag`);
expect(flagResult.challenge.flags[user._id]).to.equal(true);
expect(flagResult.challenge.flagCount).to.equal(1);
});
it('flags a challenge with a higher count when from an admin', async () => {
await user.updateOne({ 'contributor.admin': true });
const flagResult = await user.post(`/challenges/${challenge._id}/flag`);
expect(flagResult.challenge.flags[user._id]).to.equal(true);
expect(flagResult.challenge.flagCount).to.equal(5);
});
it('returns an error when user tries to flag a challenge that is already flagged', async () => {
await user.post(`/challenges/${challenge._id}/flag`);
await expect(user.post(`/challenges/${challenge._id}/flag`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
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'),
});
});
});

View File

@@ -1,58 +0,0 @@
import { v4 as generateUUID } from 'uuid';
import {
generateChallenge,
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('POST /challenges/:challengeId/clearflags', () => {
let admin;
let nonAdmin;
let challenge;
beforeEach(async () => {
const { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: 'TestParty',
type: 'party',
privacy: 'private',
},
members: 1,
});
admin = groupLeader;
[nonAdmin] = members;
await admin.updateOne({ 'permissions.moderator': true });
challenge = await generateChallenge(admin, group);
await admin.post(`/challenges/${challenge._id}/flag`);
});
it('returns error when non-admin attempts to clear flags', async () => {
await expect(nonAdmin.post(`/challenges/${generateUUID()}/clearflags`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageGroupChatAdminClearFlagCount'),
});
});
it('returns an error when challenge is not found', async () => {
await expect(admin.post(`/challenges/${generateUUID()}/clearflags`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('challengeNotFound'),
});
});
it('clears flags and sets mod flag to false', async () => {
await nonAdmin.post(`/challenges/${challenge._id}/flag`);
const flagResult = await admin.post(`/challenges/${challenge._id}/clearflags`);
expect(flagResult.challenge.flagCount).to.eql(0);
expect(flagResult.challenge.flags).to.have.property(admin._id, false);
expect(flagResult.challenge.flags).to.have.property(nonAdmin._id, true);
});
});

View File

@@ -42,7 +42,26 @@ describe('POST /challenges', () => {
}); });
}); });
it('returns error when creating a challenge with summary with greater than MAX_SUMMARY_SIZE_FOR_CHALLENGES characters', async () => { 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 () => {
const user = await generateUser(); const user = await generateUser();
const summary = 'A'.repeat(MAX_SUMMARY_SIZE_FOR_CHALLENGES + 1); const summary = 'A'.repeat(MAX_SUMMARY_SIZE_FOR_CHALLENGES + 1);
const group = createAndPopulateGroup({ const group = createAndPopulateGroup({
@@ -58,7 +77,7 @@ describe('POST /challenges', () => {
}); });
}); });
context('creating a Challenge for a Group Plan', () => { context('Creating a challenge for a valid group', () => {
let groupLeader; let groupLeader;
let group; let group;
let groupMember; let groupMember;
@@ -75,11 +94,9 @@ describe('POST /challenges', () => {
challenges: true, challenges: true,
}, },
}, },
upgradeToGroupPlan: true,
}); });
groupLeader = await populatedGroup.groupLeader.sync(); groupLeader = await populatedGroup.groupLeader.sync();
await groupLeader.updateOne({ permissions: {} });
group = populatedGroup.group; group = populatedGroup.group;
groupMember = populatedGroup.members[0]; // eslint-disable-line prefer-destructuring groupMember = populatedGroup.members[0]; // eslint-disable-line prefer-destructuring
}); });
@@ -177,7 +194,7 @@ describe('POST /challenges', () => {
const oldUserBalance = groupLeader.balance; const oldUserBalance = groupLeader.balance;
const prize = 8; const prize = 8;
await group.updateOne({ balance: 0 }); await group.update({ balance: 0 });
await groupLeader.post('/challenges', { await groupLeader.post('/challenges', {
group: group._id, group: group._id,
name: 'Test Challenge', name: 'Test Challenge',
@@ -202,7 +219,7 @@ describe('POST /challenges', () => {
}); });
it('sets challenge as official if created by admin and official flag is set', async () => { it('sets challenge as official if created by admin and official flag is set', async () => {
await groupLeader.updateOne({ await groupLeader.update({
permissions: { permissions: {
challengeAdmin: true, challengeAdmin: true,
}, },
@@ -331,71 +348,5 @@ describe('POST /challenges', () => {
expect(updatedChallenge.summary).to.eql(summary); 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);
});
}); });
}); });

View File

@@ -128,10 +128,10 @@ describe('POST /challenges/:challengeId/winner/:winnerId', () => {
const oldBalance = winningUser.balance; const oldBalance = winningUser.balance;
const oldLeaderBalance = (await groupLeader.sync()).balance; const oldLeaderBalance = (await groupLeader.sync()).balance;
await winningUser.updateOne({ await winningUser.update({
'purchased.plan.customerId': 'group-plan', 'purchased.plan.customerId': 'group-plan',
}); });
await group.updateOne({ await group.update({
'leaderOnly.getGems': true, 'leaderOnly.getGems': true,
'purchased.plan.customerId': 123, 'purchased.plan.customerId': 123,
}); });

View File

@@ -18,7 +18,6 @@ describe('PUT /challenges/:challengeId', () => {
privacy: 'private', privacy: 'private',
}, },
members: 1, members: 1,
upgradeToGroupPlan: true,
}); });
privateGuild = group; privateGuild = group;

View File

@@ -1,6 +1,7 @@
import { v4 as generateUUID } from 'uuid'; import { v4 as generateUUID } from 'uuid';
import { import {
createAndPopulateGroup, createAndPopulateGroup,
generateUser,
translate as t, translate as t,
} from '../../../../helpers/api-integration/v3'; } from '../../../../helpers/api-integration/v3';
@@ -9,30 +10,27 @@ describe('DELETE /groups/:groupId/chat/:chatId', () => {
admin; admin;
before(async () => { before(async () => {
const { group, groupLeader, members } = await createAndPopulateGroup({ const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { groupDetails: {
type: 'guild', type: 'guild',
privacy: 'private', privacy: 'public',
}, },
leaderDetails: { leaderDetails: {
'auth.timestamps.created': new Date('2022-01-01'), 'auth.timestamps.created': new Date('2022-01-01'),
balance: 10, balance: 10,
}, },
members: 2,
upgradeToGroupPlan: true,
}); });
groupWithChat = group; groupWithChat = group;
user = groupLeader; user = groupLeader;
message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: 'Some message' }); message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: 'Some message' });
message = message.message; message = message.message;
userThatDidNotCreateChat = members[0]; // eslint-disable-line prefer-destructuring userThatDidNotCreateChat = await generateUser();
admin = members[1]; // eslint-disable-line prefer-destructuring admin = await generateUser({ 'permissions.moderator': true });
await admin.updateOne({ permissions: { moderator: true } });
}); });
context('Chat errors', () => { context('Chat errors', () => {
it('returns an error if message does not exist', async () => { it('returns an error is message does not exist', async () => {
const fakeChatId = generateUUID(); const fakeChatId = generateUUID();
await expect(user.del(`/groups/${groupWithChat._id}/chat/${fakeChatId}`)).to.eventually.be.rejected.and.eql({ await expect(user.del(`/groups/${groupWithChat._id}/chat/${fakeChatId}`)).to.eventually.be.rejected.and.eql({
code: 404, code: 404,
@@ -58,7 +56,7 @@ describe('DELETE /groups/:groupId/chat/:chatId', () => {
nextMessage = nextMessage.message; nextMessage = nextMessage.message;
}); });
it('allows creator to delete their message', async () => { it('allows creator to delete a their message', async () => {
await user.del(`/groups/${groupWithChat._id}/chat/${nextMessage.id}`); await user.del(`/groups/${groupWithChat._id}/chat/${nextMessage.id}`);
const returnedMessages = await user.get(`/groups/${groupWithChat._id}/chat/`); const returnedMessages = await user.get(`/groups/${groupWithChat._id}/chat/`);

View File

@@ -1,6 +1,6 @@
import { import {
generateUser, generateUser,
createAndPopulateGroup, generateGroup,
translate as t, translate as t,
} from '../../../../helpers/api-integration/v3'; } from '../../../../helpers/api-integration/v3';
@@ -11,22 +11,48 @@ describe('GET /groups/:groupId/chat', () => {
user = await generateUser(); 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', () => { context('private Guild', () => {
let group; let group;
before(async () => { before(async () => {
({ group } = await createAndPopulateGroup({ const leader = await generateUser({ balance: 2 });
groupDetails: {
group = await generateGroup(leader, {
name: 'test group', name: 'test group',
type: 'guild', type: 'guild',
privacy: 'private', privacy: 'private',
}, }, {
members: 1,
upgradeToGroupPlan: true,
chat: [ chat: [
'Hello', 'Hello',
'Welcome to the Guild', 'Welcome to the Guild',
], ],
})); });
}); });
it('returns error if user is not member of requested private group', async () => { it('returns error if user is not member of requested private group', async () => {

View File

@@ -1,42 +1,32 @@
import find from 'lodash/find'; import { find } from 'lodash';
import moment from 'moment'; import moment from 'moment';
import nconf from 'nconf';
import { IncomingWebhook } from '@slack/webhook'; import { IncomingWebhook } from '@slack/webhook';
import { import {
createAndPopulateGroup, generateUser,
translate as t, translate as t,
} from '../../../../helpers/api-integration/v3'; } from '../../../../helpers/api-integration/v3';
const BASE_URL = nconf.get('BASE_URL');
describe('POST /chat/:chatId/flag', () => { describe('POST /chat/:chatId/flag', () => {
let user; let admin; let anotherUser; let newUser; let let user; let admin; let anotherUser; let newUser; let
group; let members; let userToDelete; group;
const TEST_MESSAGE = 'Test Message'; const TEST_MESSAGE = 'Test Message';
const USER_AGE_FOR_FLAGGING = 3; const USER_AGE_FOR_FLAGGING = 3;
beforeEach(async () => { beforeEach(async () => {
({ group, groupLeader: user, members } = await createAndPopulateGroup({ user = await generateUser({ balance: 1, 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate() });
groupDetails: { 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 = await user.post('/groups', {
name: 'Test Guild', name: 'Test Guild',
type: 'guild', type: 'guild',
privacy: 'private', privacy: 'public',
},
leaderDetails: {
'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate(),
},
members: 4,
upgradeToGroupPlan: true,
}));
[admin, anotherUser, newUser, userToDelete] = members;
await user.updateOne({ permissions: {} });
await admin.updateOne({ permissions: { moderator: true } });
await anotherUser.updateOne({ 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate() });
await newUser.updateOne({ 'auth.timestamps.created': moment().subtract(1, 'days').toDate() });
await userToDelete.updateOne({
'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(() => { afterEach(() => {
@@ -79,8 +69,8 @@ describe('POST /chat/:chatId/flag', () => {
fallback: 'Flag Message', fallback: 'Flag Message',
color: 'danger', color: 'danger',
author_name: `@${anotherUser.auth.local.username} ${anotherUser.profile.name} (${anotherUser.auth.local.email}; ${anotherUser._id})\n${timestamp}`, author_name: `@${anotherUser.auth.local.username} ${anotherUser.profile.name} (${anotherUser.auth.local.email}; ${anotherUser._id})\n${timestamp}`,
title: 'Flag in Test Guild - (private guild)', title: 'Flag in Test Guild',
title_link: undefined, title_link: `${BASE_URL}/groups/guild/${group._id}`,
text: TEST_MESSAGE, text: TEST_MESSAGE,
footer: `<https://habitrpg.github.io/flag-o-rama/?groupId=${group._id}&chatId=${message.id}|Flag this message.>`, footer: `<https://habitrpg.github.io/flag-o-rama/?groupId=${group._id}&chatId=${message.id}|Flag this message.>`,
mrkdwn_in: [ mrkdwn_in: [
@@ -88,7 +78,7 @@ describe('POST /chat/:chatId/flag', () => {
], ],
}], }],
}); });
/* eslint-enable camelcase */ /* eslint-ensable camelcase */
}); });
it('Does not increment message flag count and sends different message to moderator Slack when user is new', async () => { it('Does not increment message flag count and sends different message to moderator Slack when user is new', async () => {
@@ -114,8 +104,8 @@ describe('POST /chat/:chatId/flag', () => {
fallback: 'Flag Message', fallback: 'Flag Message',
color: 'danger', color: 'danger',
author_name: `@${newUser.auth.local.username} ${newUser.profile.name} (${newUser.auth.local.email}; ${newUser._id})\n${timestamp}`, author_name: `@${newUser.auth.local.username} ${newUser.profile.name} (${newUser.auth.local.email}; ${newUser._id})\n${timestamp}`,
title: 'Flag in Test Guild - (private guild)', title: 'Flag in Test Guild',
title_link: undefined, title_link: `${BASE_URL}/groups/guild/${group._id}`,
text: TEST_MESSAGE, text: TEST_MESSAGE,
footer: `<https://habitrpg.github.io/flag-o-rama/?groupId=${group._id}&chatId=${message.id}|Flag this message.> ${automatedComment}`, footer: `<https://habitrpg.github.io/flag-o-rama/?groupId=${group._id}&chatId=${message.id}|Flag this message.> ${automatedComment}`,
mrkdwn_in: [ mrkdwn_in: [
@@ -123,12 +113,15 @@ describe('POST /chat/:chatId/flag', () => {
], ],
}], }],
}); });
/* eslint-enable camelcase */ /* eslint-ensable camelcase */
}); });
it('Flags a chat when the author\'s account was deleted', async () => { it('Flags a chat when the author\'s account was deleted', async () => {
const { message } = await userToDelete.post(`/groups/${group._id}/chat`, { message: TEST_MESSAGE }); const deletedUser = await generateUser({
await userToDelete.del('/user', { 'auth.timestamps.created': new Date('2022-01-01'),
});
const { message } = await deletedUser.post(`/groups/${group._id}/chat`, { message: TEST_MESSAGE });
await deletedUser.del('/user', {
password: 'password', password: 'password',
}); });

View File

@@ -6,28 +6,28 @@ import {
describe('POST /chat/:chatId/like', () => { describe('POST /chat/:chatId/like', () => {
let user; let user;
let anotherUser;
let groupWithChat; let groupWithChat;
let members;
const testMessage = 'Test Message'; const testMessage = 'Test Message';
let anotherUser;
before(async () => { before(async () => {
({ group: groupWithChat, groupLeader: user, members } = await createAndPopulateGroup({ const { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: { groupDetails: {
name: 'Test Guild', name: 'Test Guild',
type: 'guild', type: 'guild',
privacy: 'private', privacy: 'public',
}, },
members: 1, members: 1,
leaderDetails: { leaderDetails: {
'auth.timestamps.created': new Date('2022-01-01'), 'auth.timestamps.created': new Date('2022-01-01'),
balance: 10, balance: 10,
}, },
upgradeToGroupPlan: true, });
}));
[anotherUser] = members; user = groupLeader;
await anotherUser.updateOne({ 'auth.timestamps.created': new Date('2022-01-01') }); groupWithChat = group;
anotherUser = members[0]; // eslint-disable-line prefer-destructuring
await anotherUser.update({ 'auth.timestamps.created': new Date('2022-01-01') });
}); });
it('Returns an error when chat message is not found', async () => { it('Returns an error when chat message is not found', async () => {
@@ -39,21 +39,19 @@ describe('POST /chat/:chatId/like', () => {
}); });
}); });
it('Likes a chat', async () => { it('Returns an error when user tries to like their own message', async () => {
const message = await anotherUser.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage }); const message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
const likeResult = await user.post(`/groups/${groupWithChat._id}/chat/${message.message.id}/like`); await expect(user.post(`/groups/${groupWithChat._id}/chat/${message.message.id}/like`))
.to.eventually.be.rejected.and.eql({
expect(likeResult.likes[user._id]).to.equal(true); code: 404,
error: 'NotFound',
const groupWithChatLikes = await user.get(`/groups/${groupWithChat._id}`); message: t('messageGroupChatLikeOwnMessage'),
});
const messageToCheck = find(groupWithChatLikes.chat, { id: message.message.id });
expect(messageToCheck.likes[user._id]).to.equal(true);
}); });
it('Allows to likes their own chat message', async () => { it('Likes a chat', async () => {
const message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage }); const message = await anotherUser.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
const likeResult = await user.post(`/groups/${groupWithChat._id}/chat/${message.message.id}/like`); const likeResult = await user.post(`/groups/${groupWithChat._id}/chat/${message.message.id}/like`);

View File

@@ -1,43 +1,52 @@
import { IncomingWebhook } from '@slack/webhook'; import { IncomingWebhook } from '@slack/webhook';
import nconf from 'nconf';
import { v4 as generateUUID } from 'uuid'; import { v4 as generateUUID } from 'uuid';
import { import {
createAndPopulateGroup, createAndPopulateGroup,
generateUser,
translate as t, translate as t,
sleep, sleep,
server, server,
} from '../../../../helpers/api-integration/v3'; } from '../../../../helpers/api-integration/v3';
import { import {
SPAM_MESSAGE_LIMIT,
SPAM_MIN_EXEMPT_CONTRIB_LEVEL, SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
TAVERN_ID,
} from '../../../../../website/server/models/group'; } from '../../../../../website/server/models/group';
import { MAX_MESSAGE_LENGTH } from '../../../../../website/common/script/constants'; import { CHAT_FLAG_FROM_SHADOW_MUTE, MAX_MESSAGE_LENGTH } from '../../../../../website/common/script/constants';
import * as email from '../../../../../website/server/libs/email'; import * as email from '../../../../../website/server/libs/email';
const BASE_URL = nconf.get('BASE_URL');
describe('POST /chat', () => { describe('POST /chat', () => {
let user; let groupWithChat; let member; let let user; let groupWithChat; let member; let
additionalMember; additionalMember;
const testMessage = 'Test Message'; const testMessage = 'Test Message';
const testBannedWordMessage = 'TESTPLACEHOLDERSWEARWORDHERE'; const testBannedWordMessage = 'TESTPLACEHOLDERSWEARWORDHERE';
const testBannedWordMessage1 = 'TESTPLACEHOLDERSWEARWORDHERE1';
const testSlurMessage = 'message with TESTPLACEHOLDERSLURWORDHERE'; const testSlurMessage = 'message with TESTPLACEHOLDERSLURWORDHERE';
const testSlurMessage1 = 'TESTPLACEHOLDERSLURWORDHERE1';
const bannedWordErrorMessage = t('bannedWordUsed', { swearWordsUsed: testBannedWordMessage });
before(async () => { before(async () => {
const { group, groupLeader, members } = await createAndPopulateGroup({ const { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: { groupDetails: {
name: 'Test Guild', name: 'Test Guild',
type: 'guild', type: 'guild',
privacy: 'private', privacy: 'public',
}, },
members: 2, members: 2,
upgradeToGroupPlan: true,
}); });
user = groupLeader; user = groupLeader;
await user.updateOne({ await user.update({
'contributor.level': SPAM_MIN_EXEMPT_CONTRIB_LEVEL, 'contributor.level': SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
'auth.timestamps.created': new Date('2022-01-01'), 'auth.timestamps.created': new Date('2022-01-01'),
}); // prevent tests accidentally throwing messageGroupChatSpam }); // prevent tests accidentally throwing messageGroupChatSpam
groupWithChat = group; groupWithChat = group;
[member, additionalMember] = members; member = members[0]; // eslint-disable-line prefer-destructuring
await member.updateOne({ 'auth.timestamps.created': new Date('2022-01-01') }); additionalMember = members[1]; // eslint-disable-line prefer-destructuring
await additionalMember.updateOne({ 'auth.timestamps.created': new Date('2022-01-01') }); await member.update({ 'auth.timestamps.created': new Date('2022-01-01') });
await additionalMember.update({ 'auth.timestamps.created': new Date('2022-01-01') });
}); });
it('Returns an error when no message is provided', async () => { it('Returns an error when no message is provided', async () => {
@@ -77,15 +86,35 @@ describe('POST /chat', () => {
describe('mute user', () => { describe('mute user', () => {
afterEach(() => { afterEach(() => {
member.updateOne({ 'flags.chatRevoked': false }); 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 () => { it('does not error when chat privileges are revoked when sending a message to a private guild', async () => {
await member.updateOne({ const { group, members } = await createAndPopulateGroup({
'flags.chatRevoked': true, groupDetails: {
name: 'Private Guild',
type: 'guild',
privacy: 'private',
},
members: 1,
}); });
const message = await member.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage }); const privateGuildMemberWithChatsRevoked = members[0];
await privateGuildMemberWithChatsRevoked.update({
'flags.chatRevoked': true,
'auth.timestamps.created': new Date('2022-01-01'),
});
const message = await privateGuildMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage });
expect(message.message.id).to.exist; expect(message.message.id).to.exist;
}); });
@@ -101,7 +130,7 @@ describe('POST /chat', () => {
}); });
const privatePartyMemberWithChatsRevoked = members[0]; const privatePartyMemberWithChatsRevoked = members[0];
await privatePartyMemberWithChatsRevoked.updateOne({ await privatePartyMemberWithChatsRevoked.update({
'flags.chatRevoked': true, 'flags.chatRevoked': true,
'auth.timestamps.created': new Date('2022-01-01'), 'auth.timestamps.created': new Date('2022-01-01'),
}); });
@@ -120,15 +149,57 @@ describe('POST /chat', () => {
afterEach(() => { afterEach(() => {
sandbox.restore(); sandbox.restore();
member.updateOne({ 'flags.chatShadowMuted': false }); 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 () => { it('creates a chat with zero flagCount when sending a message to a private guild', async () => {
await member.updateOne({ const { group, members } = await createAndPopulateGroup({
'flags.chatShadowMuted': true, groupDetails: {
name: 'Private Guild',
type: 'guild',
privacy: 'private',
},
members: 1,
}); });
const message = await member.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage }); const userWithChatShadowMuted = members[0];
await userWithChatShadowMuted.update({
'flags.chatShadowMuted': true,
'auth.timestamps.created': new Date('2022-01-01'),
});
const message = await userWithChatShadowMuted.post(`/groups/${group._id}/chat`, { message: testMessage });
expect(message.message.id).to.exist; expect(message.message.id).to.exist;
expect(message.message.flagCount).to.eql(0); expect(message.message.flagCount).to.eql(0);
@@ -145,7 +216,7 @@ describe('POST /chat', () => {
}); });
const userWithChatShadowMuted = members[0]; const userWithChatShadowMuted = members[0];
await userWithChatShadowMuted.updateOne({ await userWithChatShadowMuted.update({
'flags.chatShadowMuted': true, 'flags.chatShadowMuted': true,
'auth.timestamps.created': new Date('2022-01-01'), 'auth.timestamps.created': new Date('2022-01-01'),
}); });
@@ -155,9 +226,100 @@ describe('POST /chat', () => {
expect(message.message.id).to.exist; expect(message.message.id).to.exist;
expect(message.message.flagCount).to.eql(0); 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', () => { 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 () => { it('does not error when sending a chat message containing a banned word to a party', async () => {
const { group, members } = await createAndPopulateGroup({ const { group, members } = await createAndPopulateGroup({
groupDetails: { groupDetails: {
@@ -167,7 +329,26 @@ describe('POST /chat', () => {
}, },
members: 1, members: 1,
}); });
await members[0].updateOne({ 'auth.timestamps.created': new Date('2022-01-01') }); 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 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 }); const message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage });
@@ -175,7 +356,17 @@ describe('POST /chat', () => {
}); });
it('does not error when sending a chat message containing a banned word to a private guild', async () => { it('does not error when sending a chat message containing a banned word to a private guild', async () => {
const message = await member.post(`/groups/${groupWithChat._id}/chat`, { message: testBannedWordMessage }); 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 });
expect(message.message.id).to.exist; expect(message.message.id).to.exist;
}); });
@@ -189,7 +380,46 @@ describe('POST /chat', () => {
afterEach(() => { afterEach(() => {
sandbox.restore(); sandbox.restore();
user.updateOne({ 'flags.chatRevoked': false }); 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 () => { it('allows slurs in private groups', async () => {
@@ -201,23 +431,34 @@ describe('POST /chat', () => {
}, },
members: 1, members: 1,
}); });
await members[0].updateOne({ 'auth.timestamps.created': new Date('2022-01-01') }); await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') });
const message = await members[0].post(`/groups/${group._id}/chat`, { message: testSlurMessage }); const message = await members[0].post(`/groups/${group._id}/chat`, { message: testSlurMessage });
expect(message.message.id).to.exist; 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 () => { it('errors when user account is too young', async () => {
await user.updateOne({ 'auth.timestamps.created': new Date() }); const brandNewUser = await generateUser();
await expect(user.post(`/groups/${groupWithChat._id}/chat`, { message: 'hi im new' })) await expect(brandNewUser.post('/groups/habitrpg/chat', { message: 'hi im new' }))
.to.eventually.be.rejected.and.eql({ .to.eventually.be.rejected.and.eql({
code: 400, code: 400,
error: 'BadRequest', error: 'BadRequest',
message: t('chatTemporarilyUnavailable'), message: t('chatTemporarilyUnavailable'),
}); });
await user.updateOne({ 'auth.timestamps.created': new Date('2022-01-01') });
}); });
it('creates a chat', async () => { it('creates a chat', async () => {
@@ -258,7 +499,7 @@ describe('POST /chat', () => {
it('chat message with mentions - mention link should not count towards 3000 chars limit', async () => { it('chat message with mentions - mention link should not count towards 3000 chars limit', async () => {
const memberUsername = 'memberUsername'; const memberUsername = 'memberUsername';
await member.updateOne({ 'auth.local.username': memberUsername }); await member.update({ 'auth.local.username': memberUsername });
const messageWithMentions = `hi @${memberUsername} 123456789 const messageWithMentions = `hi @${memberUsername} 123456789
123456789 123456789 123456789 123456789 123456789 123456789 89 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 12345678 END.`; 123456789 123456789 123456789 123456789 123456789 123456789 89 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 12345678 END.`;
@@ -278,43 +519,26 @@ describe('POST /chat', () => {
const mount = 'test-mount'; const mount = 'test-mount';
const pet = 'test-pet'; const pet = 'test-pet';
const style = 'test-style'; const style = 'test-style';
await user.updateOne({ const userWithStyle = await generateUser({
'items.currentMount': mount, 'items.currentMount': mount,
'items.currentPet': pet, 'items.currentPet': pet,
'preferences.style': style, 'preferences.style': style,
'auth.timestamps.created': new Date('2022-01-01'),
}); });
await userWithStyle.sync();
const message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage }); const message = await userWithStyle.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
expect(message.message.id).to.exist; expect(message.message.id).to.exist;
expect(message.message.userStyles.items.currentMount).to.eql(user.items.currentMount); expect(message.message.userStyles.items.currentMount).to.eql(userWithStyle.items.currentMount);
expect(message.message.userStyles.items.currentPet).to.eql(user.items.currentPet); expect(message.message.userStyles.items.currentPet).to.eql(userWithStyle.items.currentPet);
expect(message.message.userStyles.preferences.style).to.eql(user.preferences.style); expect(message.message.userStyles.preferences.style).to.eql(userWithStyle.preferences.style);
expect(message.message.userStyles.preferences.hair).to.eql(user.preferences.hair); expect(message.message.userStyles.preferences.hair).to.eql(userWithStyle.preferences.hair);
expect(message.message.userStyles.preferences.skin).to.eql(user.preferences.skin); expect(message.message.userStyles.preferences.skin).to.eql(userWithStyle.preferences.skin);
expect(message.message.userStyles.preferences.shirt).to.eql(user.preferences.shirt); expect(message.message.userStyles.preferences.shirt).to.eql(userWithStyle.preferences.shirt);
expect(message.message.userStyles.preferences.chair).to.eql(user.preferences.chair); expect(message.message.userStyles.preferences.chair).to.eql(userWithStyle.preferences.chair);
expect(message.message.userStyles.preferences.background) expect(message.message.userStyles.preferences.background)
.to.eql(user.preferences.background); .to.eql(userWithStyle.preferences.background);
});
it('creates equipped to user styles', async () => {
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(user.items.gear.equipped);
expect(message.message.userStyles.items.gear.costume).to.not.exist;
});
it('creates costume to user styles', async () => {
await user.updateOne({ 'preferences.costume': true });
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(user.items.gear.costume);
expect(message.message.userStyles.items.gear.equipped).to.not.exist;
}); });
it('adds backer info to chat', async () => { it('adds backer info to chat', async () => {
@@ -323,11 +547,12 @@ describe('POST /chat', () => {
tier: 800, tier: 800,
tokensApplied: true, tokensApplied: true,
}; };
await user.updateOne({ const backer = await generateUser({
backer: backerInfo, backer: backerInfo,
'auth.timestamps.created': new Date('2022-01-01'),
}); });
const message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage }); const message = await backer.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
const messageBackerInfo = message.message.backer; const messageBackerInfo = message.message.backer;
expect(messageBackerInfo.npc).to.equal(backerInfo.npc); expect(messageBackerInfo.npc).to.equal(backerInfo.npc);
@@ -375,7 +600,7 @@ describe('POST /chat', () => {
context('chat notifications', () => { context('chat notifications', () => {
beforeEach(() => { beforeEach(() => {
member.updateOne({ newMessages: {}, notifications: [] }); member.update({ newMessages: {}, notifications: [] });
}); });
it('notifies other users of new messages for a guild', async () => { it('notifies other users of new messages for a guild', async () => {
@@ -407,5 +632,43 @@ describe('POST /chat', () => {
expect(memberWithNotification.newMessages[`${group._id}`]).to.exist; 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; 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;
}
});
}); });
}); });

View File

@@ -12,19 +12,18 @@ describe('POST /groups/:id/chat/seen', () => {
const { group, groupLeader, members } = await createAndPopulateGroup({ const { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: { groupDetails: {
type: 'guild', type: 'guild',
privacy: 'private', privacy: 'public',
}, },
members: 1, members: 1,
leaderDetails: { leaderDetails: {
'auth.timestamps.created': new Date('2022-01-01'), 'auth.timestamps.created': new Date('2022-01-01'),
balance: 10, balance: 10,
}, },
upgradeToGroupPlan: true,
}); });
guild = group; guild = group;
guildLeader = groupLeader; guildLeader = groupLeader;
[guildMember] = members; guildMember = members[0]; // eslint-disable-line prefer-destructuring
guildMessage = await guildLeader.post(`/groups/${guild._id}/chat`, { message: 'Some guild message' }); guildMessage = await guildLeader.post(`/groups/${guild._id}/chat`, { message: 'Some guild message' });
guildMessage = guildMessage.message; guildMessage = guildMessage.message;

View File

@@ -2,6 +2,7 @@ import moment from 'moment';
import { v4 as generateUUID } from 'uuid'; import { v4 as generateUUID } from 'uuid';
import { import {
createAndPopulateGroup, createAndPopulateGroup,
generateUser,
translate as t, translate as t,
} from '../../../../helpers/api-integration/v3'; } from '../../../../helpers/api-integration/v3';
import config from '../../../../../config.json'; import config from '../../../../../config.json';
@@ -12,24 +13,21 @@ describe('POST /groups/:id/chat/:id/clearflags', () => {
admin; admin;
before(async () => { before(async () => {
const { group, groupLeader, members } = await createAndPopulateGroup({ const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { groupDetails: {
type: 'guild', type: 'guild',
privacy: 'private', privacy: 'public',
}, },
leaderDetails: { leaderDetails: {
'auth.timestamps.created': new Date('2022-01-01'), 'auth.timestamps.created': new Date('2022-01-01'),
balance: 10, balance: 10,
}, },
upgradeToGroupPlan: true,
members: 2,
}); });
groupWithChat = group; groupWithChat = group;
author = groupLeader; author = groupLeader;
[nonAdmin, admin] = members; nonAdmin = await generateUser({ 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate() });
await nonAdmin.updateOne({ 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate() }); admin = await generateUser({ 'permissions.moderator': true });
await admin.updateOne({ 'permissions.moderator': true });
message = await author.post(`/groups/${groupWithChat._id}/chat`, { message: 'Some message' }); message = await author.post(`/groups/${groupWithChat._id}/chat`, { message: 'Some message' });
message = message.message; message = message.message;
@@ -68,27 +66,22 @@ describe('POST /groups/:id/chat/:id/clearflags', () => {
type: 'party', type: 'party',
privacy: 'private', privacy: 'private',
}, },
members: 2, members: 1,
}); });
await members[0].updateOne({ 'auth.timestamps.created': new Date('2022-01-01') }); await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') });
let privateMessage = await members[0].post(`/groups/${group._id}/chat`, { message: 'Some message' }); let privateMessage = await members[0].post(`/groups/${group._id}/chat`, { message: 'Some message' });
privateMessage = privateMessage.message; privateMessage = privateMessage.message;
await admin.post(`/groups/${group._id}/chat/${privateMessage.id}/flag`); await admin.post(`/groups/${group._id}/chat/${privateMessage.id}/flag`);
// first test that the flag was actually successful // first test that the flag was actually successful
// author always sees own message; flag count is hidden from non-admins
let messages = await members[0].get(`/groups/${group._id}/chat`); let messages = await members[0].get(`/groups/${group._id}/chat`);
expect(messages[0].flagCount).to.eql(0); expect(messages[0].flagCount).to.eql(5);
messages = await members[1].get(`/groups/${group._id}/chat`);
expect(messages.length).to.eql(0);
// admin cannot directly request private group chat, but after unflag,
// message should be revealed again and still have flagCount of 0
await admin.post(`/groups/${group._id}/chat/${privateMessage.id}/clearflags`); await admin.post(`/groups/${group._id}/chat/${privateMessage.id}/clearflags`);
messages = await members[1].get(`/groups/${group._id}/chat`);
expect(messages.length).to.eql(1); messages = await members[0].get(`/groups/${group._id}/chat`);
expect(messages[0].flagCount).to.eql(0); expect(messages[0].flagCount).to.eql(0);
}); });
@@ -104,7 +97,7 @@ describe('POST /groups/:id/chat/:id/clearflags', () => {
const member = members[0]; const member = members[0];
// make member that can use skills // make member that can use skills
await member.updateOne({ await member.update({
'stats.lvl': 100, 'stats.lvl': 100,
'stats.mp': 400, 'stats.mp': 400,
'stats.class': 'wizard', 'stats.class': 'wizard',

View File

@@ -24,7 +24,7 @@ describe('GET /coupons/', () => {
}); });
it('should return the coupons in CSV format ordered by creation date', async () => { it('should return the coupons in CSV format ordered by creation date', async () => {
await user.updateOne({ await user.update({
'permissions.coupons': true, 'permissions.coupons': true,
}); });

View File

@@ -19,7 +19,7 @@ describe('POST /coupons/generate/:event', () => {
}); });
it('returns an error if user has no coupons permission', async () => { it('returns an error if user has no coupons permission', async () => {
await user.updateOne({ await user.update({
'permissions.coupons': false, 'permissions.coupons': false,
}); });
@@ -47,7 +47,7 @@ describe('POST /coupons/generate/:event', () => {
}); });
it('should generate coupons', async () => { it('should generate coupons', async () => {
await user.updateOne({ await user.update({
'permissions.coupons': true, 'permissions.coupons': true,
}); });

View File

@@ -24,7 +24,7 @@ describe('POST /debug/quest-progress', () => {
}); });
it('increases boss quest progress by 1000', async () => { it('increases boss quest progress by 1000', async () => {
await user.updateOne({ await user.update({
'party.quest.key': 'whale', 'party.quest.key': 'whale',
}); });
@@ -36,7 +36,7 @@ describe('POST /debug/quest-progress', () => {
}); });
it('increases collection quest progress by 300 items', async () => { it('increases collection quest progress by 300 items', async () => {
await user.updateOne({ await user.update({
'party.quest.key': 'evilsanta2', 'party.quest.key': 'evilsanta2',
}); });

View File

@@ -1,5 +1,6 @@
import { import {
createAndPopulateGroup, generateUser,
generateGroup,
} from '../../../../helpers/api-integration/v3'; } from '../../../../helpers/api-integration/v3';
describe('GET /group-plans', () => { describe('GET /group-plans', () => {
@@ -7,15 +8,20 @@ describe('GET /group-plans', () => {
let groupPlan; let groupPlan;
before(async () => { before(async () => {
({ group: groupPlan, groupLeader: user } = await createAndPopulateGroup({ user = await generateUser({ balance: 4 });
groupDetails: { groupPlan = await generateGroup(user,
name: 'group plan - is member', {
name: 'public guild - is member',
type: 'guild', type: 'guild',
privacy: 'private', privacy: 'public',
}, },
upgradeToGroupPlan: true, {
leaderDetails: { balance: 4 }, purchased: {
})); plan: {
customerId: 'existings',
},
},
});
}); });
it('returns group plans for the user', async () => { it('returns group plans for the user', async () => {

View File

@@ -1,63 +1,70 @@
import { import {
createAndPopulateGroup, generateUser,
resetHabiticaDB, resetHabiticaDB,
generateGroup, generateGroup,
translate as t, translate as t,
} from '../../../../helpers/api-integration/v3'; } from '../../../../helpers/api-integration/v3';
import {
TAVERN_ID,
} from '../../../../../website/server/models/group';
import apiError from '../../../../../website/server/libs/apiError';
describe('GET /groups', () => { describe('GET /groups', () => {
let user; let leader; let members; let user;
let secondGroup; let secondLeader; let userInGuild;
const NUMBER_OF_USERS_PRIVATE_GUILDS = 2; const NUMBER_OF_PUBLIC_GUILDS = 2;
const NUMBER_OF_GROUPS_USER_CAN_VIEW = 3; 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;
const categories = [{ const categories = [{
slug: 'newCat', slug: 'newCat',
name: 'New Category', name: 'New Category',
}]; }];
let publicGuildNotMember;
let privateGuildUserIsMemberOf; let privateGuildUserIsMemberOf;
before(async () => { before(async () => {
await resetHabiticaDB(); await resetHabiticaDB();
({ const leader = await generateUser({ balance: 10 });
group: privateGuildUserIsMemberOf, user = await generateUser({ balance: 4 });
groupLeader: leader,
members, const publicGuildUserIsMemberOf = await generateGroup(leader, {
} = await createAndPopulateGroup({ name: 'public guild - is member',
groupDetails: { 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`);
userInGuild = await generateUser({ guilds: [publicGuildUserIsMemberOf._id] });
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', name: 'private guild - is member',
type: 'guild', type: 'guild',
privacy: 'private', privacy: 'private',
categories, categories,
}, });
leaderDetails: { await leader.post(`/groups/${privateGuildUserIsMemberOf._id}/invite`, { uuids: [user._id] });
balance: 10, await user.post(`/groups/${privateGuildUserIsMemberOf._id}/join`);
},
members: 1,
upgradeToGroupPlan: true,
}));
[user] = members;
await user.updateOne({ balance: 4 });
({ group: secondGroup, groupLeader: secondLeader } = await createAndPopulateGroup({ await generateGroup(leader, {
groupDetails: {
name: 'c++ coders',
type: 'guild',
privacy: 'private',
},
upgradeToGroupPlan: true,
}));
await secondLeader.post(`/groups/${secondGroup._id}/invite`, { uuids: [user._id] });
await user.post(`/groups/${secondGroup._id}/join`);
await createAndPopulateGroup({
groupDetails: {
name: 'private guild - is not member', name: 'private guild - is not member',
type: 'guild', type: 'guild',
privacy: 'private', privacy: 'private',
},
upgradeToGroupPlan: true,
}); });
await generateGroup(leader, { await generateGroup(leader, {
@@ -91,16 +98,172 @@ 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 () => { it('returns only the user\'s party when party passed in as query', async () => {
await expect(user.get('/groups?type=party')) await expect(user.get('/groups?type=party'))
.to.eventually.have.a.lengthOf(1) .to.eventually.have.a.lengthOf(1)
.and.to.have.nested.property('[0]'); .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 () => { it('returns all the user\'s guilds when guilds passed in as query', async () => {
await expect(user.get('/groups?type=guilds')) await expect(user.get('/groups?type=guilds'))
.to.eventually.have.a .to.eventually.have.a
.lengthOf(NUMBER_OF_USERS_PRIVATE_GUILDS); .lengthOf(NUMBER_OF_PUBLIC_GUILDS_USER_IS_MEMBER + NUMBER_OF_USERS_PRIVATE_GUILDS);
}); });
it('returns all private guilds user is a part of when privateGuilds passed in as query', async () => { it('returns all private guilds user is a part of when privateGuilds passed in as query', async () => {
@@ -109,21 +272,21 @@ describe('GET /groups', () => {
}); });
it('returns a list of groups user has access to', async () => { it('returns a list of groups user has access to', async () => {
await expect(user.get('/groups?type=privateGuilds,party')) await expect(user.get('/groups?type=privateGuilds,publicGuilds,party,tavern'))
.to.eventually.have.lengthOf(NUMBER_OF_GROUPS_USER_CAN_VIEW); .to.eventually.have.lengthOf(NUMBER_OF_GROUPS_USER_CAN_VIEW - 1); // -1 for no Tavern.
}); });
describe('filters', () => { it('returns a list of groups user has access to', async () => {
it('returns private guilds filtered by category', async () => { const group = await generateGroup(user, {
const guilds = await user.get(`/groups?type=privateGuilds&categories=${categories[0].slug}`); name: 'c++ coders',
type: 'guild',
expect(guilds[0]._id).to.equal(privateGuildUserIsMemberOf._id); privacy: 'public',
}); });
it('filters private guilds by size', async () => { // search for 'c++ coders'
const guilds = await user.get('/groups?type=privateGuilds&minMemberCount=3'); await expect(user.get('/groups?type=publicGuilds&paginate=true&page=0&search=c%2B%2B+coders'))
.to.eventually.have.lengthOf(1)
expect(guilds.length).to.equal(0); .and.to.have.nested.property('[0]')
}); .and.to.have.property('_id', group._id);
}); });
}); });

View File

@@ -3,7 +3,6 @@ import {
generateUser, generateUser,
generateGroup, generateGroup,
translate as t, translate as t,
createAndPopulateGroup,
} from '../../../../helpers/api-integration/v3'; } from '../../../../helpers/api-integration/v3';
describe('GET /groups/:groupId/invites', () => { describe('GET /groups/:groupId/invites', () => {
@@ -72,16 +71,15 @@ describe('GET /groups/:groupId/invites', () => {
}); });
it('returns only first 30 invites by default (req.query.limit not specified)', async () => { it('returns only first 30 invites by default (req.query.limit not specified)', async () => {
const { group, groupLeader: leader } = await createAndPopulateGroup({ const leader = await generateUser({ balance: 4 });
groupDetails: { const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() });
type: 'guild',
privacy: 'private', const invitesToGenerate = [];
name: generateUUID(), for (let i = 0; i < 31; i += 1) {
}, invitesToGenerate.push(generateUser());
leaderDetails: { balance: 4 }, }
invites: 31, const generatedInvites = await Promise.all(invitesToGenerate);
upgradeToGroupPlan: true, await leader.post(`/groups/${group._id}/invite`, { uuids: generatedInvites.map(invite => invite._id) });
});
const res = await leader.get(`/groups/${group._id}/invites`); const res = await leader.get(`/groups/${group._id}/invites`);
expect(res.length).to.equal(30); expect(res.length).to.equal(30);
@@ -92,16 +90,8 @@ describe('GET /groups/:groupId/invites', () => {
}).timeout(10000); }).timeout(10000);
it('returns an error if req.query.limit is over 60', async () => { it('returns an error if req.query.limit is over 60', async () => {
const { group, groupLeader: leader } = await createAndPopulateGroup({ const leader = await generateUser({ balance: 4 });
groupDetails: { const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() });
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({ await expect(leader.get(`/groups/${group._id}/invites?limit=61`)).to.eventually.be.rejected.and.eql({
code: 400, code: 400,
@@ -111,16 +101,8 @@ describe('GET /groups/:groupId/invites', () => {
}); });
it('returns an error if req.query.limit is under 1', async () => { it('returns an error if req.query.limit is under 1', async () => {
const { group, groupLeader: leader } = await createAndPopulateGroup({ const leader = await generateUser({ balance: 4 });
groupDetails: { const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() });
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({ await expect(leader.get(`/groups/${group._id}/invites?limit=-1`)).to.eventually.be.rejected.and.eql({
code: 400, code: 400,
@@ -130,16 +112,8 @@ describe('GET /groups/:groupId/invites', () => {
}); });
it('returns an error if req.query.limit is not an integer', async () => { it('returns an error if req.query.limit is not an integer', async () => {
const { group, groupLeader: leader } = await createAndPopulateGroup({ const leader = await generateUser({ balance: 4 });
groupDetails: { const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() });
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({ await expect(leader.get(`/groups/${group._id}/invites?limit=1.3`)).to.eventually.be.rejected.and.eql({
code: 400, code: 400,
@@ -149,16 +123,15 @@ describe('GET /groups/:groupId/invites', () => {
}); });
it('returns up to 60 invites when req.query.limit is specified', async () => { it('returns up to 60 invites when req.query.limit is specified', async () => {
const { group, groupLeader: leader } = await createAndPopulateGroup({ const leader = await generateUser({ balance: 4 });
groupDetails: { const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() });
type: 'guild',
privacy: 'private', const invitesToGenerate = [];
name: generateUUID(), for (let i = 0; i < 31; i += 1) {
}, invitesToGenerate.push(generateUser());
leaderDetails: { balance: 4 }, }
invites: 31, const generatedInvites = await Promise.all(invitesToGenerate);
upgradeToGroupPlan: true, await leader.post(`/groups/${group._id}/invite`, { uuids: generatedInvites.map(invite => invite._id) });
});
let res = await leader.get(`/groups/${group._id}/invites?limit=14`); let res = await leader.get(`/groups/${group._id}/invites?limit=14`);
expect(res.length).to.equal(14); expect(res.length).to.equal(14);
@@ -176,20 +149,17 @@ describe('GET /groups/:groupId/invites', () => {
}).timeout(30000); }).timeout(30000);
it('supports using req.query.lastId to get more invites', async function test () { 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 this.timeout(30000); // @TODO: times out after 8 seconds
({ group, groupLeader: user, invitees } = await createAndPopulateGroup({ const leader = await generateUser({ balance: 4 });
groupDetails: { const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() });
type: 'guild',
privacy: 'private',
name: generateUUID(),
},
leaderDetails: { balance: 4 },
invites: 32,
upgradeToGroupPlan: true,
}));
const expectedIds = invitees.map(generatedInvite => generatedInvite._id); 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 res = await user.get(`/groups/${group._id}/invites`); const res = await user.get(`/groups/${group._id}/invites`);
expect(res.length).to.equal(30); expect(res.length).to.equal(30);

View File

@@ -1,6 +1,5 @@
import { v4 as generateUUID } from 'uuid'; import { v4 as generateUUID } from 'uuid';
import { import {
createAndPopulateGroup,
generateUser, generateUser,
generateGroup, generateGroup,
translate as t, translate as t,
@@ -76,15 +75,7 @@ describe('GET /groups/:groupId/members', () => {
}); });
it('req.query.includeAllPublicFields === true works with guilds', async () => { it('req.query.includeAllPublicFields === true works with guilds', async () => {
let group; const group = await generateGroup(user, { type: 'guild', name: generateUUID() });
({ 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`); 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 expect(memberRes).to.have.all.keys([ // works as: object has all and only these keys
@@ -215,20 +206,20 @@ describe('GET /groups/:groupId/members', () => {
it('supports using req.query.lastId to get more members', async function test () { it('supports using req.query.lastId to get more members', async function test () {
this.timeout(30000); // @TODO: times out after 8 seconds this.timeout(30000); // @TODO: times out after 8 seconds
const { group, groupLeader: leader, members: generatedUsers } = await createAndPopulateGroup({ const leader = await generateUser({ balance: 4 });
type: 'guild', const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() });
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 expectedIds = [leader._id].concat(generatedUsers.map(generatedUser => generatedUser._id));
const res = await leader.get(`/groups/${group._id}/members`); const res = await user.get(`/groups/${group._id}/members`);
expect(res.length).to.equal(30); expect(res.length).to.equal(30);
const res2 = await leader.get(`/groups/${group._id}/members?lastId=${res[res.length - 1]._id}`); const res2 = await user.get(`/groups/${group._id}/members?lastId=${res[res.length - 1]._id}`);
expect(res2.length).to.equal(28); expect(res2.length).to.equal(28);
const resIds = res.concat(res2).map(member => member._id); const resIds = res.concat(res2).map(member => member._id);

View File

@@ -11,6 +11,7 @@ import {
describe('GET /groups/:id', () => { describe('GET /groups/:id', () => {
const typesOfGroups = {}; const typesOfGroups = {};
typesOfGroups['public guild'] = { type: 'guild', privacy: 'public' };
typesOfGroups['private guild'] = { type: 'guild', privacy: 'private' }; typesOfGroups['private guild'] = { type: 'guild', privacy: 'private' };
typesOfGroups.party = { type: 'party', privacy: 'private' }; typesOfGroups.party = { type: 'party', privacy: 'private' };
@@ -23,11 +24,10 @@ describe('GET /groups/:id', () => {
const groupData = await createAndPopulateGroup({ const groupData = await createAndPopulateGroup({
members: 30, members: 30,
groupDetails, groupDetails,
upgradeToGroupPlan: groupDetails.type === 'guild',
}); });
leader = groupData.groupLeader; leader = groupData.groupLeader;
[member] = groupData.members; member = groupData.members[0]; // eslint-disable-line prefer-destructuring
createdGroup = groupData.group; createdGroup = groupData.group;
}); });
@@ -49,6 +49,34 @@ 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', () => { context('Non-member of a private guild', () => {
let nonMember; let let nonMember; let
createdGroup; createdGroup;
@@ -61,7 +89,6 @@ describe('GET /groups/:id', () => {
type: 'guild', type: 'guild',
privacy: 'private', privacy: 'private',
}, },
upgradeToGroupPlan: true,
}); });
createdGroup = groupData.group; createdGroup = groupData.group;
@@ -153,7 +180,7 @@ describe('GET /groups/:id', () => {
it('removes non-existent guild from user\'s guild list', async () => { it('removes non-existent guild from user\'s guild list', async () => {
const guildId = generateUUID(); const guildId = generateUUID();
await user.updateOne({ await user.update({
guilds: [guildId, generateUUID()], guilds: [guildId, generateUUID()],
}); });
@@ -173,7 +200,7 @@ describe('GET /groups/:id', () => {
it('removes non-existent party from user\'s party object', async () => { it('removes non-existent party from user\'s party object', async () => {
const partyId = generateUUID(); const partyId = generateUUID();
await user.updateOne({ await user.update({
party: { _id: partyId }, party: { _id: partyId },
}); });
@@ -191,7 +218,7 @@ describe('GET /groups/:id', () => {
}); });
context('Flagged messages', () => { context('Flagged messages', () => {
let group; let members; let group;
const chat1 = { const chat1 = {
id: 'chat1', id: 'chat1',
@@ -241,7 +268,7 @@ describe('GET /groups/:id', () => {
groupDetails: { groupDetails: {
name: 'test guild', name: 'test guild',
type: 'guild', type: 'guild',
privacy: 'private', privacy: 'public',
chat: [ chat: [
chat1, chat1,
chat2, chat2,
@@ -250,11 +277,9 @@ describe('GET /groups/:id', () => {
chat5, chat5,
], ],
}, },
members: 1,
upgradeToGroupPlan: true,
}); });
({ group, members } = groupData); group = groupData.group;
await group.addChat([chat1, chat2, chat3, chat4, chat5]); await group.addChat([chat1, chat2, chat3, chat4, chat5]);
}); });
@@ -262,8 +287,8 @@ describe('GET /groups/:id', () => {
context('non-admin', () => { context('non-admin', () => {
let nonAdmin; let nonAdmin;
beforeEach(() => { beforeEach(async () => {
[nonAdmin] = members; nonAdmin = await generateUser();
}); });
it('does not include messages with a flag count of 2 or greater', async () => { it('does not include messages with a flag count of 2 or greater', async () => {
@@ -289,8 +314,9 @@ describe('GET /groups/:id', () => {
let admin; let admin;
beforeEach(async () => { beforeEach(async () => {
[admin] = members; admin = await generateUser({
await admin.updateOne({ permissions: { moderator: true } }); 'permissions.moderator': true,
});
}); });
it('includes all messages', async () => { it('includes all messages', async () => {

View File

@@ -2,6 +2,7 @@ import {
generateUser, generateUser,
translate as t, translate as t,
} from '../../../../helpers/api-integration/v3'; } 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'; import { MAX_SUMMARY_SIZE_FOR_GUILDS } from '../../../../../website/common/script/constants';
describe('POST /group', () => { describe('POST /group', () => {
@@ -34,8 +35,8 @@ describe('POST /group', () => {
it('sets the group leader to the user who created the group', async () => { it('sets the group leader to the user who created the group', async () => {
const group = await user.post('/groups', { const group = await user.post('/groups', {
name: 'Test Party', name: 'Test Public Guild',
type: 'party', type: 'guild',
}); });
expect(group.leader).to.eql({ expect(group.leader).to.eql({
@@ -50,7 +51,7 @@ describe('POST /group', () => {
const name = 'Test Group'; const name = 'Test Group';
const group = await user.post('/groups', { const group = await user.post('/groups', {
name, name,
type: 'party', type: 'guild',
}); });
const updatedGroup = await user.get(`/groups/${group._id}`); const updatedGroup = await user.get(`/groups/${group._id}`);
@@ -63,7 +64,7 @@ describe('POST /group', () => {
const summary = 'Test Summary'; const summary = 'Test Summary';
const group = await user.post('/groups', { const group = await user.post('/groups', {
name, name,
type: 'party', type: 'guild',
summary, summary,
}); });
@@ -77,7 +78,7 @@ describe('POST /group', () => {
const summary = 'A'.repeat(MAX_SUMMARY_SIZE_FOR_GUILDS + 1); const summary = 'A'.repeat(MAX_SUMMARY_SIZE_FOR_GUILDS + 1);
await expect(user.post('/groups', { await expect(user.post('/groups', {
name, name,
type: 'party', type: 'guild',
summary, summary,
})).to.eventually.be.rejected.and.eql({ })).to.eventually.be.rejected.and.eql({
code: 400, code: 400,
@@ -87,6 +88,157 @@ 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', () => { context('Parties', () => {
const partyName = 'Test Party'; const partyName = 'Test Party';
const partyType = 'party'; const partyType = 'party';
@@ -110,7 +262,7 @@ describe('POST /group', () => {
}); });
it('creates a party when the user has no chat privileges', async () => { it('creates a party when the user has no chat privileges', async () => {
await user.updateOne({ 'flags.chatRevoked': true }); await user.update({ 'flags.chatRevoked': true });
const party = await user.post('/groups', { const party = await user.post('/groups', {
name: partyName, name: partyName,
type: partyType, type: partyType,
@@ -120,7 +272,7 @@ describe('POST /group', () => {
}); });
it('does not require gems to create a party', async () => { it('does not require gems to create a party', async () => {
await user.updateOne({ balance: 0 }); await user.update({ balance: 0 });
const party = await user.post('/groups', { const party = await user.post('/groups', {
name: partyName, name: partyName,

Some files were not shown because too many files have changed in this diff Show More