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
2837 changed files with 318020 additions and 75176 deletions
+2 -7
View File
@@ -1,11 +1,6 @@
/* eslint-disable import/no-commonjs */
module.exports = {
root: true,
extends: [
'habitrpg/lib/node',
'habitrpg/lib/node'
],
rules: {
'prefer-regex-literals': 'warn',
'import/no-extraneous-dependencies': 'off',
},
};
}
+36
View File
@@ -104,6 +104,42 @@ updates:
- 7.7.0
- 7.8.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
versions:
- 3.10.0
+39 -49
View File
@@ -10,20 +10,19 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [21.x]
node-version: [14.x]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt-get -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
npm i
npm ci
env:
CI: true
NODE_ENV: test
@@ -32,20 +31,19 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [21.x]
node-version: [14.x]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt-get -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
npm i
npm ci
env:
CI: true
NODE_ENV: test
@@ -54,20 +52,19 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [21.x]
node-version: [14.x]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt-get -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
npm i
npm ci
env:
CI: true
NODE_ENV: test
@@ -77,20 +74,19 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [21.x]
node-version: [14.x]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt-get -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
npm i
npm ci
env:
CI: true
NODE_ENV: test
@@ -99,20 +95,19 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [21.x]
node-version: [14.x]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt-get -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
npm i
npm ci
env:
CI: true
NODE_ENV: test
@@ -122,14 +117,14 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [21.x]
node-version: [14.x]
mongodb-version: [4.2]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Start MongoDB ${{ matrix.mongodb-version }} Replica Set
@@ -137,11 +132,10 @@ jobs:
with:
mongodb-version: ${{ matrix.mongodb-version }}
mongodb-replica-set: rs
- run: sudo apt-get -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
npm i
npm ci
env:
CI: true
NODE_ENV: test
@@ -152,14 +146,14 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [21.x]
node-version: [14.x]
mongodb-version: [4.2]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Start MongoDB ${{ matrix.mongodb-version }} Replica Set
@@ -167,11 +161,10 @@ jobs:
with:
mongodb-version: ${{ matrix.mongodb-version }}
mongodb-replica-set: rs
- run: sudo apt-get -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
npm i
npm ci
env:
CI: true
NODE_ENV: test
@@ -182,14 +175,14 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [21.x]
node-version: [14.x]
mongodb-version: [4.2]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Start MongoDB ${{ matrix.mongodb-version }} Replica Set
@@ -197,11 +190,10 @@ jobs:
with:
mongodb-version: ${{ matrix.mongodb-version }}
mongodb-replica-set: rs
- run: sudo apt-get -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
npm i
npm ci
env:
CI: true
NODE_ENV: test
@@ -213,20 +205,19 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [21.x]
node-version: [14.x]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt-get -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
npm i
npm ci
env:
CI: true
NODE_ENV: test
@@ -237,16 +228,15 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [21.x]
node-version: [14.x]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt-get -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
-1
View File
@@ -40,7 +40,6 @@ yarn.lock
!.elasticbeanstalk/*.global.yml
/.vscode
habitica.code-workspace
# webstorm fake webpack for path intellisense
webpack.webstorm.config
+1 -1
View File
@@ -1 +1 @@
20
14
+30
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"]
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:20
FROM node:14
# Install global packages
RUN npm install -g gulp-cli mocha
+1 -2
View File
@@ -86,6 +86,5 @@
"RATE_LIMITER_ENABLED": "false",
"REDIS_HOST": "aaabbbcccdddeeefff",
"REDIS_PORT": "1234",
"REDIS_PASSWORD": "12345678",
"TRUSTED_DOMAINS": "localhost,https://habitica.com"
"REDIS_PASSWORD": "12345678"
}
+1 -1
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
// 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) {
const stringChunk = chunk.toString();
+5
View File
@@ -2,6 +2,7 @@ import mongoose from 'mongoose';
import nconf from 'nconf';
import repl from 'repl';
import gulp from 'gulp';
import logger from '../website/server/libs/logger';
import {
getDevelopmentConnectionUrl,
getDefaultConnectionOptions,
@@ -38,6 +39,10 @@ const improveRepl = context => {
mongoose.connect(
connectionUrl,
mongooseOptions,
err => {
if (err) throw err;
logger.info('Connected with Mongoose');
},
);
};
+14 -24
View File
@@ -59,15 +59,13 @@ gulp.task('test:prepare:mongo', cb => {
const mongooseOptions = getDefaultConnectionOptions();
const connectionUrl = getDevelopmentConnectionUrl(TEST_DB_URI);
mongoose.connect(connectionUrl, mongooseOptions)
.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}`);
throw err;
mongoose.connect(connectionUrl, mongooseOptions, err => {
if (err) return cb(`Unable to connect to mongo database. Are you sure it's running? \n\n${err}`);
return mongoose.connection.dropDatabase(err2 => {
if (err2) return cb(err2);
return mongoose.connection.close(cb);
});
});
});
gulp.task('test:prepare:server', gulp.series('test:prepare:mongo', done => {
@@ -118,10 +116,8 @@ gulp.task('test:common:safe', gulp.series('test:prepare:build', cb => {
pipe(runner);
}));
gulp.task('test:content', gulp.series(
'test:prepare:build',
runInChildProcess(CONTENT_TEST_COMMAND, LIMIT_MAX_BUFFER_OPTIONS),
));
gulp.task('test:content', gulp.series('test:prepare:build',
runInChildProcess(CONTENT_TEST_COMMAND, LIMIT_MAX_BUFFER_OPTIONS)));
gulp.task('test:content:clean', 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);
}));
gulp.task(
'test:api:unit:run',
runInChildProcess(integrationTestCommand('test/api/unit', 'coverage/api-unit')),
);
gulp.task('test:api:unit:run',
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-v3:integration', gulp.series(
'test:prepare:mongo',
gulp.task('test:api-v3:integration', gulp.series('test:prepare:mongo',
runInChildProcess(
integrationTestCommand('test/api/v3/integration', 'coverage/api-v3-integration'),
LIMIT_MAX_BUFFER_OPTIONS,
),
));
)));
gulp.task('test:api-v3:integration:watch', () => gulp.watch([
'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',
));
gulp.task('test:api-v4:integration', gulp.series(
'test:prepare:mongo',
gulp.task('test:api-v4:integration', gulp.series('test:prepare:mongo',
runInChildProcess(
integrationTestCommand('test/api/v4', 'api-v4-integration'),
LIMIT_MAX_BUFFER_OPTIONS,
),
));
)));
gulp.task('test:api-v4:integration:separate-server', runInChildProcess(
'mocha test/api/v4 --recursive --require ./test/helpers/start-server',
+3 -3
View File
@@ -3,6 +3,6 @@ module.exports = {
root: false,
rules: {
'no-console': 0,
'no-use-before-define': ['error', { functions: false }],
},
};
'no-use-before-define': ['error', { functions: false }]
}
}
@@ -61,7 +61,7 @@ async function updateUser (user) {
export default async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
// migration: {$ne: MIGRATION_NAME},
'auth.timestamps.loggedin': {$gt: new Date('2021-01-01')},
};
@@ -105,7 +105,7 @@ async function updateUser (user) {
export default async function processUsers () {
let query = {
migration: { $ne: MIGRATION_NAME },
// migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2021-08-01') },
};
@@ -145,7 +145,7 @@ async function updateUser (user) {
export default async function processUsers () {
let query = {
migration: { $ne: MIGRATION_NAME },
// migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2021-08-01') },
};
@@ -105,7 +105,7 @@ async function updateUser (user) {
export default async function processUsers () {
let query = {
migration: { $ne: MIGRATION_NAME },
// migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2021-08-01') },
};
@@ -95,7 +95,7 @@ async function updateUser (user) {
export default async function processUsers () {
let query = {
migration: { $ne: MIGRATION_NAME },
// migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2022-01-01') },
};
@@ -86,7 +86,7 @@ async function updateUser (user) {
export default async function processUsers () {
let query = {
migration: { $ne: MIGRATION_NAME },
// migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2022-01-01') },
};
@@ -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
}
};
-144
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
}
};
@@ -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
}
};
@@ -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
}
};
@@ -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
}
};
@@ -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
}
};
@@ -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
}
};
@@ -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
}
};
@@ -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
}
};
@@ -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
}
};
@@ -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
}
};
@@ -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
}
};
@@ -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
}
};
-87
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
}
};
@@ -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
}
};
@@ -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
}
};
+118
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
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
+6 -8
View File
@@ -21,14 +21,12 @@ async function handOutJackalopes () {
if (user.party._id) groupList.push(user.party._id);
groupList = groupList.concat(user.guilds);
const subscribedGroup = await Group.findOne(
{
_id: { $in: groupList },
'purchased.plan.planId': 'group_monthly',
'purchased.plan.dateTerminated': null,
},
{ _id: 1 },
);
const subscribedGroup = await Group.findOne({
_id: { $in: groupList },
'purchased.plan.planId': 'group_monthly',
'purchased.plan.dateTerminated': null,
},
{ _id: 1 });
if (subscribedGroup) {
User.update({ _id: user._id }, { $set: { 'items.mounts.Jackalope-RoyalPurple': true } }).exec();
@@ -51,8 +51,7 @@ function getAchievementUpdate (newUser, oldUser) {
// Rebirth level
if (achievementsUpdate.rebirthLevel) {
achievementsUpdate.rebirthLevel = Math.max(
achievementsUpdate.rebirthLevel,
oldAchievements.rebirthLevel,
achievementsUpdate.rebirthLevel, oldAchievements.rebirthLevel,
);
} else if (oldAchievements.rebirthLevel) {
achievementsUpdate.rebirthLevel = oldAchievements.rebirthLevel;
-1
View File
@@ -16,7 +16,6 @@ async function updateUser (user) {
if (count % progressCount === 0) {
console.warn(`${count} ${user._id}`);
// eslint-disable-next-line no-promise-executor-return
await new Promise(resolve => setTimeout(resolve, 5000));
}
+1 -1
View File
@@ -51,7 +51,7 @@ async function updateUser (user) {
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 () {
+2 -2
View File
@@ -3,7 +3,7 @@ import { v4 as uuid } from 'uuid';
import { model as User } from '../../website/server/models/user';
const MIGRATION_NAME = '20240314_pi_day';
const MIGRATION_NAME = '20220314_pi_day';
const progressCount = 1000;
let count = 0;
@@ -54,7 +54,7 @@ async function updateUser (user) {
export default async function processUsers () {
const query = {
migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2023-02-15') },
'auth.timestamps.loggedin': { $gt: new Date('2022-02-15') },
};
const fields = {
+10682 -15011
View File
File diff suppressed because it is too large Load Diff
+37 -34
View File
@@ -1,37 +1,37 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "5.22.1",
"version": "4.251.0",
"main": "./website/server/index.js",
"dependencies": {
"@babel/core": "^7.22.10",
"@babel/preset-env": "^7.22.10",
"@babel/register": "^7.22.15",
"@babel/core": "^7.19.6",
"@babel/preset-env": "^7.20.2",
"@babel/register": "^7.18.9",
"@google-cloud/trace-agent": "^7.1.2",
"@parse/node-apn": "^5.2.3",
"@parse/node-apn": "^5.1.3",
"@slack/webhook": "^6.1.0",
"accepts": "^1.3.8",
"amazon-payments": "^0.2.9",
"amplitude": "^6.0.0",
"apidoc": "^0.54.0",
"apple-auth": "^1.0.9",
"bcrypt": "^5.1.1",
"body-parser": "^1.20.2",
"bootstrap": "^4.6.2",
"apidoc": "^0.53.1",
"apple-auth": "^1.0.7",
"bcrypt": "^5.1.0",
"body-parser": "^1.20.1",
"bootstrap": "^4.6.0",
"compression": "^1.7.4",
"cookie-session": "^2.0.0",
"coupon-code": "^0.4.5",
"csv-stringify": "^5.6.5",
"cwait": "^1.1.1",
"domain-middleware": "~0.1.0",
"eslint": "^8.55.0",
"eslint-config-habitrpg": "^6.2.3",
"eslint": "^6.8.0",
"eslint-config-habitrpg": "^6.2.0",
"eslint-plugin-mocha": "^5.0.0",
"express": "^4.18.2",
"express-basic-auth": "^1.2.1",
"express-validator": "^5.2.0",
"glob": "^8.1.0",
"got": "^11.8.6",
"glob": "^8.0.3",
"got": "^11.8.3",
"gulp": "^4.0.0",
"gulp-babel": "^8.0.0",
"gulp-imagemin": "^7.1.0",
@@ -39,47 +39,49 @@
"gulp.spritesmith": "^6.13.0",
"habitica-markdown": "^3.0.0",
"helmet": "^4.6.0",
"image-size": "^1.0.2",
"in-app-purchase": "^1.11.3",
"js2xmlparser": "^5.0.0",
"jsonwebtoken": "^9.0.2",
"jsonwebtoken": "^8.5.1",
"jwks-rsa": "^2.1.5",
"lodash": "^4.17.21",
"merge-stream": "^2.0.0",
"method-override": "^3.0.0",
"moment": "^2.29.4",
"moment-recur": "^1.0.7",
"mongoose": "^7.6.3",
"mongoose": "^5.13.7",
"morgan": "^1.10.0",
"nconf": "^0.12.1",
"nconf": "^0.12.0",
"node-gcm": "^1.0.5",
"nodemon": "^2.0.20",
"on-headers": "^1.0.2",
"passport": "^0.5.3",
"passport": "^0.6.0",
"passport-facebook": "^3.0.0",
"passport-google-oauth2": "^0.2.0",
"passport-google-oauth20": "2.0.0",
"paypal-rest-sdk": "^1.8.1",
"pp-ipn": "^1.1.0",
"ps-tree": "^1.0.0",
"rate-limiter-flexible": "^2.4.2",
"rate-limiter-flexible": "^2.4.0",
"redis": "^3.1.2",
"regenerator-runtime": "^0.13.11",
"remove-markdown": "^0.5.0",
"rimraf": "^3.0.2",
"short-uuid": "^4.2.2",
"stripe": "^12.18.0",
"superagent": "^8.1.2",
"stripe": "^10.13.0",
"superagent": "^8.0.5",
"universal-analytics": "^0.5.3",
"useragent": "^2.1.9",
"uuid": "^9.0.0",
"validator": "^13.11.0",
"winston": "^3.10.0",
"winston-loggly-bulk": "^3.3.0",
"xml2js": "^0.6.2"
"uuid": "^8.3.2",
"validator": "^13.7.0",
"vinyl-buffer": "^1.0.1",
"winston": "^3.8.2",
"winston-loggly-bulk": "^3.2.1",
"xml2js": "^0.4.23"
},
"private": true,
"engines": {
"node": "20",
"npm": "^10"
"node": "^14",
"npm": "^6"
},
"scripts": {
"lint": "eslint --ext .js --fix . && cd website/client && npm run lint",
@@ -103,16 +105,16 @@
"client:unit": "cd website/client && npm run test:unit",
"start": "gulp nodemon",
"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",
"apidoc": "gulp apidoc"
},
"devDependencies": {
"axios": "^1.4.0",
"axios": "^0.27.2",
"chai": "^4.3.7",
"chai-as-promised": "^7.1.1",
"chai-moment": "^0.1.0",
"chalk": "^5.3.0",
"chalk": "^5.1.2",
"cross-spawn": "^7.0.3",
"expect.js": "^0.3.1",
"istanbul": "^1.1.0-alpha.1",
@@ -120,8 +122,9 @@
"monk": "^7.3.4",
"require-again": "^2.0.0",
"run-rs": "^0.7.7",
"sinon": "^15.2.0",
"sinon": "^14.0.2",
"sinon-chai": "^3.7.0",
"sinon-stub-promise": "^4.0.0"
}
},
"optionalDependencies": {}
}
-2
View File
@@ -44,7 +44,6 @@ async function deleteHabiticaData (user, email) {
{ _id: user._id },
{ $set: set },
);
// eslint-disable-next-line no-promise-executor-return
await new Promise(resolve => setTimeout(resolve, 1000));
const response = await axios.delete(
`${BASE_URL}/api/v3/user`,
@@ -97,7 +96,6 @@ async function processEmailAddress (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));
return Promise.all(users.map(user => (async () => {
await deleteAmplitudeData(user._id, email); // eslint-disable-line no-await-in-loop
+2 -7
View File
@@ -1,5 +1,3 @@
/* eslint-disable import/no-commonjs */
module.exports = {
extends: [
'habitrpg/lib/mocha',
@@ -9,9 +7,6 @@ module.exports = {
chai: true,
expect: true,
sinon: true,
sandbox: true,
sandbox: true
},
rules: {
'import/no-extraneous-dependencies': 'off',
},
};
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { apiError } from '../../../../website/server/libs/apiError';
import apiError from '../../../../website/server/libs/apiError';
describe('API Messages', () => {
const message = 'Only public guilds support pagination.';
+3 -1
View File
@@ -26,7 +26,9 @@ describe('bug-report', () => {
_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(result).to.deep.equal({
+21 -100
View File
@@ -231,16 +231,13 @@ describe('cron', async () => {
},
});
// user1 has a 1-month recurring subscription starting today
beforeEach(async () => {
user1.purchased.plan.customerId = 'subscribedId';
user1.purchased.plan.dateUpdated = moment().toDate();
user1.purchased.plan.planId = 'basic';
user1.purchased.plan.consecutive.count = 0;
user1.purchased.plan.perkMonthCount = 0;
user1.purchased.plan.consecutive.offset = 0;
user1.purchased.plan.consecutive.trinkets = 0;
user1.purchased.plan.consecutive.gemCapExtra = 0;
});
user1.purchased.plan.customerId = 'subscribedId';
user1.purchased.plan.dateUpdated = moment().toDate();
user1.purchased.plan.planId = 'basic';
user1.purchased.plan.consecutive.count = 0;
user1.purchased.plan.consecutive.offset = 0;
user1.purchased.plan.consecutive.trinkets = 0;
user1.purchased.plan.consecutive.gemCapExtra = 0;
it('does not increment consecutive benefits after the first month', async () => {
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);
});
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 () => {
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(3, 'months')
.add(2, 'days')
@@ -336,30 +315,6 @@ describe('cron', async () => {
expect(user1.purchased.plan.consecutive.trinkets).to.equal(3);
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 () => {
@@ -375,16 +330,13 @@ describe('cron', async () => {
},
});
// user3 has a 3-month recurring subscription starting today
beforeEach(async () => {
user3.purchased.plan.customerId = 'subscribedId';
user3.purchased.plan.dateUpdated = moment().toDate();
user3.purchased.plan.planId = 'basic_3mo';
user3.purchased.plan.perkMonthCount = 0;
user3.purchased.plan.consecutive.count = 0;
user3.purchased.plan.consecutive.offset = 3;
user3.purchased.plan.consecutive.trinkets = 1;
user3.purchased.plan.consecutive.gemCapExtra = 5;
});
user3.purchased.plan.customerId = 'subscribedId';
user3.purchased.plan.dateUpdated = moment().toDate();
user3.purchased.plan.planId = 'basic_3mo';
user3.purchased.plan.consecutive.count = 0;
user3.purchased.plan.consecutive.offset = 3;
user3.purchased.plan.consecutive.trinkets = 1;
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 () => {
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);
});
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 () => {
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(5, 'months')
.add(2, 'days')
@@ -519,16 +456,13 @@ describe('cron', async () => {
},
});
// user6 has a 6-month recurring subscription starting today
beforeEach(async () => {
user6.purchased.plan.customerId = 'subscribedId';
user6.purchased.plan.dateUpdated = moment().toDate();
user6.purchased.plan.planId = 'google_6mo';
user6.purchased.plan.perkMonthCount = 0;
user6.purchased.plan.consecutive.count = 0;
user6.purchased.plan.consecutive.offset = 6;
user6.purchased.plan.consecutive.trinkets = 2;
user6.purchased.plan.consecutive.gemCapExtra = 10;
});
user6.purchased.plan.customerId = 'subscribedId';
user6.purchased.plan.dateUpdated = moment().toDate();
user6.purchased.plan.planId = 'google_6mo';
user6.purchased.plan.consecutive.count = 0;
user6.purchased.plan.consecutive.offset = 6;
user6.purchased.plan.consecutive.trinkets = 2;
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 () => {
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);
});
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 () => {
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(13, 'months')
.add(2, 'days')
+1 -1
View File
@@ -44,7 +44,7 @@ describe('mongodb', () => {
const mongoLibOverride = requireAgain(pathToMongoLib);
const options = mongoLibOverride.getDefaultConnectionOptions();
expect(options).to.have.all.keys(['useNewUrlParser', 'useUnifiedTopology']);
expect(options).to.have.all.keys(['useNewUrlParser', 'useUnifiedTopology', 'keepAlive', 'keepAliveInitialDelay']);
});
});
});
+3 -3
View File
@@ -227,7 +227,7 @@ describe('Password Utilities', () => {
expiresAt: moment().subtract({ minutes: 1 }),
}));
await user.updateOne({
await user.update({
'auth.local.passwordResetCode': code,
});
@@ -264,7 +264,7 @@ describe('Password Utilities', () => {
expiresAt: moment().add({ days: 1 }),
}));
await user.updateOne({
await user.update({
'auth.local.passwordResetCode': 'invalid',
});
@@ -280,7 +280,7 @@ describe('Password Utilities', () => {
expiresAt: moment().add({ days: 1 }),
}));
await user.updateOne({
await user.update({
'auth.local.passwordResetCode': code,
});
@@ -2,7 +2,7 @@ import { model as User } from '../../../../../../website/server/models/user';
import amzLib from '../../../../../../website/server/libs/payments/amazon';
import payments from '../../../../../../website/server/libs/payments/payments';
import common from '../../../../../../website/common';
import { apiError } from '../../../../../../website/server/libs/apiError';
import apiError from '../../../../../../website/server/libs/apiError';
import * as gems from '../../../../../../website/server/libs/payments/gems';
const { i18n } = common;
@@ -17,7 +17,7 @@ describe('Amazon Payments - Checkout', () => {
let closeOrderReferenceSpy;
let paymentBuyGemsStub;
let paymentCreateSubscriptionStub;
let paymentCreateSubscritionStub;
let amount = gemsBlock.price / 100;
function expectOrderReferenceSpy () {
@@ -85,8 +85,8 @@ describe('Amazon Payments - Checkout', () => {
paymentBuyGemsStub = sinon.stub(payments, 'buyGems');
paymentBuyGemsStub.resolves({});
paymentCreateSubscriptionStub = sinon.stub(payments, 'createSubscription');
paymentCreateSubscriptionStub.resolves({});
paymentCreateSubscritionStub = sinon.stub(payments, 'createSubscription');
paymentCreateSubscritionStub.resolves({});
sinon.stub(common, 'uuid').returns('uuid-generated');
sandbox.stub(gems, 'validateGiftMessage');
@@ -109,7 +109,6 @@ describe('Amazon Payments - Checkout', () => {
user,
paymentMethod,
headers,
sku: undefined,
};
if (gift) {
expectedArgs.gift = gift;
@@ -216,14 +215,13 @@ describe('Amazon Payments - Checkout', () => {
});
gift.member = receivingUser;
expect(paymentCreateSubscriptionStub).to.be.calledOnce;
expect(paymentCreateSubscriptionStub).to.be.calledWith({
expect(paymentCreateSubscritionStub).to.be.calledOnce;
expect(paymentCreateSubscritionStub).to.be.calledWith({
user,
paymentMethod: amzLib.constants.PAYMENT_METHOD_GIFT,
headers,
gift,
gemsBlock: undefined,
sku: undefined,
});
expectAmazonStubs();
});
+54 -321
View File
@@ -12,10 +12,10 @@ const { i18n } = common;
describe('Apple Payments', () => {
const subKey = 'basic_3mo';
describe('verifyPurchase', () => {
describe('verifyGemPurchase', () => {
let sku; let user; let token; let receipt; let
headers;
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let paymentBuySkuStub; let
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let paymentBuyGemsStub; let
iapGetPurchaseDataStub; let validateGiftMessageStub;
beforeEach(() => {
@@ -29,15 +29,14 @@ describe('Apple Payments', () => {
.resolves();
iapValidateStub = sinon.stub(iap, 'validate')
.resolves({});
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
sinon.stub(iap, 'isExpired').returns(false);
sinon.stub(iap, 'isCanceled').returns(false);
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
.returns(true);
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
productId: 'com.habitrpg.ios.Habitica.21gems',
transactionId: token,
}]);
paymentBuySkuStub = sinon.stub(payments, 'buySkuItem').resolves({});
paymentBuyGemsStub = sinon.stub(payments, 'buyGems').resolves({});
validateGiftMessageStub = sinon.stub(gems, 'validateGiftMessage');
});
@@ -45,10 +44,8 @@ describe('Apple Payments', () => {
iap.setup.restore();
iap.validate.restore();
iap.isValidated.restore();
iap.isExpired.restore();
iap.isCanceled.restore();
iap.getPurchaseData.restore();
payments.buySkuItem.restore();
payments.buyGems.restore();
gems.validateGiftMessage.restore();
});
@@ -57,7 +54,7 @@ describe('Apple Payments', () => {
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
.returns(false);
await expect(applePayments.verifyPurchase({ user, receipt, headers }))
await expect(applePayments.verifyGemPurchase({ user, receipt, headers }))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
@@ -69,7 +66,7 @@ describe('Apple Payments', () => {
iapGetPurchaseDataStub.restore();
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({
httpCode: 401,
name: 'NotAuthorized',
@@ -79,7 +76,7 @@ describe('Apple Payments', () => {
it('errors if the user cannot purchase gems', async () => {
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({
httpCode: 401,
name: 'NotAuthorized',
@@ -97,16 +94,14 @@ describe('Apple Payments', () => {
productId: 'badProduct',
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({
httpCode: 400,
name: 'BadRequest',
httpCode: 401,
name: 'NotAuthorized',
message: applePayments.constants.RESPONSE_INVALID_ITEM,
});
paymentBuySkuStub = sinon.stub(payments, 'buySkuItem').resolves({});
user.canGetGems.restore();
});
@@ -143,7 +138,7 @@ describe('Apple Payments', () => {
}]);
sinon.stub(user, 'canGetGems').resolves(true);
await applePayments.verifyPurchase({ user, receipt, headers });
await applePayments.verifyGemPurchase({ user, receipt, headers });
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
@@ -153,13 +148,13 @@ describe('Apple Payments', () => {
expect(iapGetPurchaseDataStub).to.be.calledOnce;
expect(validateGiftMessageStub).to.not.be.called;
expect(paymentBuySkuStub).to.be.calledOnce;
expect(paymentBuySkuStub).to.be.calledWith({
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,
gift: undefined,
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
sku: gemTest.productId,
gemsBlock: common.content.gems[gemTest.gemsBlock],
headers,
gift: undefined,
});
expect(user.canGetGems).to.be.calledOnce;
user.canGetGems.restore();
@@ -178,7 +173,7 @@ describe('Apple Payments', () => {
}]);
const gift = { uuid: receivingUser._id };
await applePayments.verifyPurchase({
await applePayments.verifyGemPurchase({
user, gift, receipt, headers,
});
@@ -192,16 +187,18 @@ describe('Apple Payments', () => {
expect(validateGiftMessageStub).to.be.calledOnce;
expect(validateGiftMessageStub).to.be.calledWith(gift, user);
expect(paymentBuySkuStub).to.be.calledOnce;
expect(paymentBuySkuStub).to.be.calledWith({
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,
gift: {
uuid: receivingUser._id,
member: sinon.match({ _id: receivingUser._id }),
},
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
sku: 'com.habitrpg.ios.Habitica.4gems',
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 = {};
receipt = `{"token": "${token}"}`;
nextPaymentProcessing = moment.utc().add({ days: 2 });
user = new User();
iapSetupStub = sinon.stub(iap, 'setup')
.resolves();
@@ -232,17 +228,14 @@ describe('Apple Payments', () => {
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().subtract({ day: 1 }).toDate(),
purchaseDate: moment.utc().valueOf(),
productId: sku,
transactionId: token,
}, {
expirationDate: moment.utc().add({ day: 1 }).toDate(),
purchaseDate: moment.utc().valueOf(),
productId: 'wrongsku',
transactionId: token,
}, {
expirationDate: moment.utc().add({ day: 1 }).toDate(),
purchaseDate: moment.utc().valueOf(),
productId: sku,
transactionId: token,
}]);
@@ -257,12 +250,21 @@ describe('Apple Payments', () => {
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 () => {
iap.isValidated.restore();
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
.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({
httpCode: 401,
name: 'NotAuthorized',
@@ -293,15 +295,13 @@ describe('Apple Payments', () => {
iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().add({ day: 2 }).toDate(),
purchaseDate: new Date(),
expirationDate: moment.utc().add({ day: 1 }).toDate(),
productId: option.sku,
transactionId: token,
originalTransactionId: token,
}]);
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(iapValidateStub).to.be.calledOnce;
@@ -321,253 +321,21 @@ describe('Apple Payments', () => {
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 () => {
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;
it('errors when a user is already subscribed', async () => {
payments.createSubscription.restore();
user = new User();
await user.save();
await applePayments.subscribe(user, receipt, headers, nextPaymentProcessing);
await applePayments.subscribe(sku, 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();
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();
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 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 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,
});
});
await expect(applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: applePayments.constants.RESPONSE_ALREADY_USED,
});
});
});
@@ -592,9 +360,9 @@ describe('Apple Payments', () => {
});
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{ expirationDate: expirationDate.toDate() }]);
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
sinon.stub(iap, 'isCanceled').returns(false);
sinon.stub(iap, 'isExpired').returns(true);
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
.returns(true);
user = new User();
user.profile.name = 'sender';
user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE;
@@ -609,8 +377,6 @@ describe('Apple Payments', () => {
iap.setup.restore();
iap.validate.restore();
iap.isValidated.restore();
iap.isExpired.restore();
iap.isCanceled.restore();
iap.getPurchaseData.restore();
payments.cancelSubscription.restore();
});
@@ -630,8 +396,6 @@ describe('Apple Payments', () => {
iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{ expirationDate: expirationDate.add({ day: 1 }).toDate() }]);
iap.isExpired.restore();
sinon.stub(iap, 'isExpired').returns(false);
await expect(applePayments.cancelSubscribe(user, headers))
.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 () => {
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 () => {
it('should cancel a user subscription', async () => {
await applePayments.cancelSubscribe(user, headers);
expect(iapSetupStub).to.be.calledOnce;
+29 -34
View File
@@ -12,11 +12,11 @@ const { i18n } = common;
describe('Google Payments', () => {
const subKey = 'basic_3mo';
describe('verifyPurchase', () => {
describe('verifyGemPurchase', () => {
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
paymentBuySkuStub; let validateGiftMessageStub;
paymentBuyGemsStub; let validateGiftMessageStub;
beforeEach(() => {
sku = 'com.habitrpg.android.habitica.iap.21gems';
@@ -27,10 +27,11 @@ describe('Google Payments', () => {
iapSetupStub = sinon.stub(iap, 'setup')
.resolves();
iapValidateStub = sinon.stub(iap, 'validate').resolves({ productId: sku });
iapValidateStub = sinon.stub(iap, 'validate')
.resolves({});
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
.returns(true);
paymentBuySkuStub = sinon.stub(payments, 'buySkuItem').resolves({});
paymentBuyGemsStub = sinon.stub(payments, 'buyGems').resolves({});
validateGiftMessageStub = sinon.stub(gems, 'validateGiftMessage');
});
@@ -38,7 +39,7 @@ describe('Google Payments', () => {
iap.setup.restore();
iap.validate.restore();
iap.isValidated.restore();
payments.buySkuItem.restore();
payments.buyGems.restore();
gems.validateGiftMessage.restore();
});
@@ -47,7 +48,7 @@ describe('Google Payments', () => {
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
.returns(false);
await expect(googlePayments.verifyPurchase({
await expect(googlePayments.verifyGemPurchase({
user, receipt, signature, headers,
}))
.to.eventually.be.rejected.and.to.eql({
@@ -59,25 +60,21 @@ describe('Google Payments', () => {
it('should throw an error if productId is invalid', async () => {
receipt = `{"token": "${token}", "productId": "invalid"}`;
iapValidateStub.restore();
iapValidateStub = sinon.stub(iap, 'validate').resolves({});
paymentBuySkuStub.restore();
await expect(googlePayments.verifyPurchase({
await expect(googlePayments.verifyGemPurchase({
user, receipt, signature, headers,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
httpCode: 401,
name: 'NotAuthorized',
message: googlePayments.constants.RESPONSE_INVALID_ITEM,
});
paymentBuySkuStub = sinon.stub(payments, 'buySkuItem').resolves({});
});
it('should throw an error if user cannot purchase gems', async () => {
sinon.stub(user, 'canGetGems').resolves(false);
await expect(googlePayments.verifyPurchase({
await expect(googlePayments.verifyGemPurchase({
user, receipt, signature, headers,
}))
.to.eventually.be.rejected.and.to.eql({
@@ -91,7 +88,7 @@ describe('Google Payments', () => {
it('purchases gems', async () => {
sinon.stub(user, 'canGetGems').resolves(true);
await googlePayments.verifyPurchase({
await googlePayments.verifyGemPurchase({
user, receipt, signature, headers,
});
@@ -104,17 +101,15 @@ describe('Google Payments', () => {
signature,
});
expect(iapIsValidatedStub).to.be.calledOnce;
expect(iapIsValidatedStub).to.be.calledWith(
{ productId: sku },
);
expect(iapIsValidatedStub).to.be.calledWith({});
expect(paymentBuySkuStub).to.be.calledOnce;
expect(paymentBuySkuStub).to.be.calledWith({
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,
gift: undefined,
paymentMethod: googlePayments.constants.PAYMENT_METHOD_GOOGLE,
sku,
gemsBlock,
headers,
gift: undefined,
});
expect(user.canGetGems).to.be.calledOnce;
user.canGetGems.restore();
@@ -125,7 +120,7 @@ describe('Google Payments', () => {
await receivingUser.save();
const gift = { uuid: receivingUser._id };
await googlePayments.verifyPurchase({
await googlePayments.verifyGemPurchase({
user, gift, receipt, signature, headers,
});
@@ -139,20 +134,20 @@ describe('Google Payments', () => {
signature,
});
expect(iapIsValidatedStub).to.be.calledOnce;
expect(iapIsValidatedStub).to.be.calledWith(
{ productId: sku },
);
expect(iapIsValidatedStub).to.be.calledWith({});
expect(paymentBuySkuStub).to.be.calledOnce;
expect(paymentBuySkuStub).to.be.calledWith({
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,
gift: {
uuid: receivingUser._id,
member: sinon.match({ _id: receivingUser._id }),
},
paymentMethod: googlePayments.constants.PAYMENT_METHOD_GOOGLE,
sku,
gemsBlock,
headers,
gift: {
type: 'gems',
gems: { amount: 21 },
member: sinon.match({ _id: receivingUser._id }),
uuid: receivingUser._id,
},
});
});
});
+17 -812
View File
@@ -203,28 +203,6 @@ describe('payments/index', () => {
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 () => {
recipient.purchased.plan = plan;
data.customerId = 'purchaserCustomerId';
@@ -235,116 +213,6 @@ describe('payments/index', () => {
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 () => {
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.dateUpdated).to.exist;
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.extraMonths).to.eql(0);
expect(user.purchased.plan.dateTerminated).to.eql(null);
@@ -519,63 +386,6 @@ describe('payments/index', () => {
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 () => {
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', () => {
@@ -748,19 +475,9 @@ describe('payments/index', () => {
});
it('does not add to plans.consecutive.offset if 1 month subscription', async () => {
data.sub.key = 'basic_earned';
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.offset).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);
expect(user.purchased.plan.extraMonths).to.eql(0);
});
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 () => {
data.sub.key = 'basic_6mo';
await api.createSubscription(data);
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 () => {
data.sub.key = 'basic_12mo';
await api.createSubscription(data);
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);
});
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', () => {
@@ -1382,6 +575,18 @@ describe('payments/index', () => {
expect(user.purchased.plan.mysteryItems).to.have.a.lengthOf(1);
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 () => {
// TODO using english for both users because other languages are not loaded
// for api.buyGems
await recipient.updateOne({
await recipient.update({
'preferences.language': 'en',
});
await user.updateOne({
await user.update({
'preferences.language': 'en',
});
await api.buyGems(data);
@@ -4,7 +4,7 @@ import nconf from 'nconf';
import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
import { model as User } from '../../../../../../website/server/models/user';
import common from '../../../../../../website/common';
import { apiError } from '../../../../../../website/server/libs/apiError';
import apiError from '../../../../../../website/server/libs/apiError';
import * as gems from '../../../../../../website/server/libs/payments/gems';
const BASE_URL = nconf.get('BASE_URL');
@@ -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;
});
});
});
});
@@ -1,4 +1,4 @@
import { apiError } from '../../../../../../website/server/libs/apiError';
import apiError from '../../../../../../website/server/libs/apiError';
import common from '../../../../../../website/common';
import {
getOneTimePaymentInfo,
+23 -14
View File
@@ -53,9 +53,11 @@ describe('cron middleware', () => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
Tasks.Task.findOne({ _id: task }).then(foundTask => {
expect(foundTask).to.not.exist;
resolve();
Tasks.Task.findOne({ _id: task }, (secondErr, taskFound) => {
if (secondErr) return reject(err);
expect(secondErr).to.not.exist;
expect(taskFound).to.not.exist;
return resolve();
});
return null;
@@ -76,8 +78,10 @@ describe('cron middleware', () => {
await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
Tasks.Task.findOne({ _id: task }).then(foundTask => {
expect(foundTask).to.exist;
Tasks.Task.findOne({ _id: task }, (secondErr, taskFound) => {
if (secondErr) return reject(secondErr);
expect(secondErr).to.not.exist;
expect(taskFound).to.exist;
return resolve();
});
return null;
@@ -99,8 +103,10 @@ describe('cron middleware', () => {
await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
Tasks.Task.findOne({ _id: task }).then(foundTask => {
expect(foundTask).to.not.exist;
Tasks.Task.findOne({ _id: task }, (secondErr, taskFound) => {
if (secondErr) return reject(secondErr);
expect(secondErr).to.not.exist;
expect(taskFound).to.not.exist;
return resolve();
});
return null;
@@ -164,7 +170,8 @@ describe('cron middleware', () => {
await new Promise((resolve, reject) => {
cronMiddleware(req, res, 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);
return resolve();
});
@@ -181,7 +188,8 @@ describe('cron middleware', () => {
await new Promise((resolve, reject) => {
cronMiddleware(req, res, 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);
return resolve();
});
@@ -216,7 +224,8 @@ describe('cron middleware', () => {
await new Promise((resolve, reject) => {
cronMiddleware(req, res, 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);
return resolve();
});
@@ -229,11 +238,11 @@ describe('cron middleware', () => {
await user.save();
const updatedUser = user.toObject();
updatedUser.matchedCount = 0;
updatedUser.nMatched = 0;
sandbox.spy(cronLib, 'recoverCron');
sandbox.stub(User, 'updateOne')
sandbox.stub(User, 'update')
.withArgs({
_id: user._id,
$or: [
@@ -260,7 +269,7 @@ describe('cron middleware', () => {
it('cronSignature less than an hour ago should error', async () => {
user.lastCron = moment(new Date()).subtract({ days: 2 });
const now = new Date();
await User.updateOne({
await User.update({
_id: user._id,
}, {
$set: {
@@ -282,7 +291,7 @@ describe('cron middleware', () => {
it('cronSignature longer than an hour ago should allow cron', async () => {
user.lastCron = moment(new Date()).subtract({ days: 2 });
const now = new Date();
await User.updateOne({
await User.update({
_id: user._id,
}, {
$set: {
@@ -6,7 +6,7 @@ import {
} from '../../../helpers/api-unit.helper';
import { ensurePermission } from '../../../../website/server/middlewares/ensureAccessRight';
import { NotAuthorized } from '../../../../website/server/libs/errors';
import { apiError } from '../../../../website/server/libs/apiError';
import apiError from '../../../../website/server/libs/apiError';
describe('ensure access middlewares', () => {
let res; let req; let
+1 -1
View File
@@ -6,7 +6,7 @@ import {
generateNext,
} from '../../../helpers/api-unit.helper';
import { Forbidden } from '../../../../website/server/libs/errors';
import { apiError } from '../../../../website/server/libs/apiError';
import apiError from '../../../../website/server/libs/apiError';
function checkErrorThrown (next) {
expect(next).to.have.been.calledOnce;
@@ -7,7 +7,7 @@ import {
generateNext,
} from '../../../helpers/api-unit.helper';
import { TooManyRequests } from '../../../../website/server/libs/errors';
import { apiError } from '../../../../website/server/libs/apiError';
import apiError from '../../../../website/server/libs/apiError';
import logger from '../../../../website/server/libs/logger';
describe('rateLimiter middleware', () => {
+27 -28
View File
@@ -1358,8 +1358,7 @@ describe('Group Model', () => {
describe('#sendChat', () => {
beforeEach(() => {
sandbox.spy(User, 'updateOne');
sandbox.spy(User, 'updateMany');
sandbox.spy(User, 'update');
});
it('formats message', () => {
@@ -1414,8 +1413,8 @@ describe('Group Model', () => {
it('updates users about new messages in party', () => {
party.sendChat({ message: 'message' });
expect(User.updateMany).to.be.calledOnce;
expect(User.updateMany).to.be.calledWithMatch({
expect(User.update).to.be.calledOnce;
expect(User.update).to.be.calledWithMatch({
'party._id': party._id,
_id: { $ne: '' },
});
@@ -1428,8 +1427,8 @@ describe('Group Model', () => {
group.sendChat({ message: 'message' });
expect(User.updateMany).to.be.calledOnce;
expect(User.updateMany).to.be.calledWithMatch({
expect(User.update).to.be.calledOnce;
expect(User.update).to.be.calledWithMatch({
guilds: group._id,
_id: { $ne: '' },
});
@@ -1438,8 +1437,8 @@ describe('Group Model', () => {
it('does not send update to user that sent the message', () => {
party.sendChat({ message: 'message', user: { _id: 'user-id', profile: { name: 'user' } } });
expect(User.updateMany).to.be.calledOnce;
expect(User.updateMany).to.be.calledWithMatch({
expect(User.update).to.be.calledOnce;
expect(User.update).to.be.calledWithMatch({
'party._id': party._id,
_id: { $ne: 'user-id' },
});
@@ -1450,7 +1449,7 @@ describe('Group Model', () => {
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', () => {
@@ -1458,7 +1457,7 @@ describe('Group Model', () => {
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 () => {
sandbox.spy(User, 'updateMany');
sandbox.spy(User, 'update');
await party.startQuest(nonParticipatingMember);
@@ -1740,7 +1739,7 @@ describe('Group Model', () => {
questLeader._id, participatingMember._id, sleepingParticipatingMember._id,
];
expect(User.updateMany).to.be.calledWith(
expect(User.update).to.be.calledWith(
{ _id: { $in: members } },
{
$set: {
@@ -1753,11 +1752,11 @@ describe('Group Model', () => {
});
it('updates non-user quest leader and decrements quest scroll', async () => {
sandbox.spy(User, 'updateOne');
sandbox.spy(User, 'update');
await party.startQuest(participatingMember);
expect(User.updateOne).to.be.calledWith(
expect(User.update).to.be.calledWith(
{ _id: questLeader._id },
{
$inc: {
@@ -1819,29 +1818,29 @@ describe('Group Model', () => {
};
it('doesn\'t retry successful operations', async () => {
sandbox.stub(User, 'updateOne').returns(successfulMock);
sandbox.stub(User, 'update').returns(successfulMock);
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 () => {
const updateStub = sandbox.stub(User, 'updateOne');
const updateStub = sandbox.stub(User, 'update');
updateStub.onCall(0).returns(failedMock);
updateStub.returns(successfulMock);
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 () => {
sandbox.stub(User, 'updateOne').returns(failedMock);
sandbox.stub(User, 'update').returns(failedMock);
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', () => {
it('updates participating members with rewards', async () => {
sandbox.spy(User, 'updateOne');
sandbox.spy(User, 'update');
await party.finishQuest(quest);
expect(User.updateOne).to.be.calledThrice;
expect(User.updateOne).to.be.calledWithMatch({
expect(User.update).to.be.calledThrice;
expect(User.update).to.be.calledWithMatch({
_id: questLeader._id,
});
expect(User.updateOne).to.be.calledWithMatch({
expect(User.update).to.be.calledWithMatch({
_id: participatingMember._id,
});
expect(User.updateOne).to.be.calledWithMatch({
expect(User.update).to.be.calledWithMatch({
_id: sleepingParticipatingMember._id,
});
});
@@ -2173,11 +2172,11 @@ describe('Group Model', () => {
});
it('updates all users with rewards', async () => {
sandbox.spy(User, 'updateMany');
sandbox.spy(User, 'update');
await party.finishQuest(tavernQuest);
expect(User.updateMany).to.be.calledOnce;
expect(User.updateMany).to.be.calledWithMatch({});
expect(User.update).to.be.calledOnce;
expect(User.update).to.be.calledWithMatch({});
});
it('sets quest completed to the world quest key', async () => {
+2 -2
View File
@@ -103,7 +103,7 @@ describe('NewsPost Model', () => {
beforeEach(async () => {
// Delete all existing posts from the database
await NewsPost.deleteMany();
await NewsPost.remove();
});
afterEach(() => {
@@ -116,7 +116,7 @@ describe('NewsPost Model', () => {
_id: v4(), publishDate: new Date(), published: true,
};
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
const newPost = await NewsPost.create({
+3 -6
View File
@@ -221,8 +221,7 @@ describe('Task Model', () => {
it('returns task by alias', async () => {
const foundTasks = await Tasks.Task.findMultipleByIdOrAlias(
[taskWithAlias.alias],
user._id,
[taskWithAlias.alias], user._id,
);
expect(foundTasks[0].text).to.eql(taskWithAlias.text);
@@ -230,8 +229,7 @@ describe('Task Model', () => {
it('returns multiple tasks', async () => {
const foundTasks = await Tasks.Task.findMultipleByIdOrAlias(
[taskWithAlias.alias, secondTask._id],
user._id,
[taskWithAlias.alias, secondTask._id], user._id,
);
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 () => {
const foundTasks = await Tasks.Task.findMultipleByIdOrAlias(
[taskWithAlias.alias, taskWithAlias._id],
user._id,
[taskWithAlias.alias, taskWithAlias._id], user._id,
);
expect(foundTasks.length).to.eql(1);
+5 -5
View File
@@ -188,7 +188,7 @@ describe('User Model', () => {
it('removes invalid tags when loading the user', async () => {
let user = new User();
await user.save();
await user.updateOne({
await user.update({
$set: {
tags: [
null, // invalid, not an object
@@ -212,7 +212,7 @@ describe('User Model', () => {
it('removes invalid push devices when loading the user', async () => {
let user = new User();
await user.save();
await user.updateOne({
await user.update({
$set: {
pushDevices: [
null, // invalid, not an object
@@ -236,7 +236,7 @@ describe('User Model', () => {
it('removes duplicate push devices when loading the user', async () => {
let user = new User();
await user.save();
await user.updateOne({
await user.update({
$set: {
pushDevices: [
{ type: 'android', regId: '1234' },
@@ -258,7 +258,7 @@ describe('User Model', () => {
it('removes invalid notifications when loading the user', async () => {
let user = new User();
await user.save();
await user.updateOne({
await user.update({
$set: {
notifications: [
null, // invalid, not an object
@@ -284,7 +284,7 @@ describe('User Model', () => {
it('removes multiple NEW_CHAT_MESSAGE for the same group', async () => {
let user = new User();
await user.save();
await user.updateOne({
await user.update({
$set: {
notifications: [
{
+1 -1
View File
@@ -1,7 +1,7 @@
import { v4 as generateUUID } from 'uuid';
import { model as Webhook } from '../../../../website/server/models/webhook';
import { BadRequest } from '../../../../website/server/libs/errors';
import { apiError } from '../../../../website/server/libs/apiError';
import apiError from '../../../../website/server/libs/apiError';
describe('Webhook Model', () => {
context('Instance Methods', () => {
@@ -87,7 +87,7 @@ describe('DELETE /challenges/:challengeId', () => {
const testTask = _.find(tasks, task => task.text === taskText);
expect(testTask.challenge.broken).to.eql('CHALLENGE_DELETED');
expect(testTask.challenge.winner).to.be.undefined;
expect(testTask.challenge.winner).to.be.null;
});
});
});
@@ -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 challengeLeader;
let group;
@@ -31,14 +84,14 @@ describe('GET /challenges/:challengeId', () => {
const populatedGroup = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
members: 2,
upgradeToGroupPlan: true,
});
groupLeader = populatedGroup.groupLeader;
group = populatedGroup.group;
members = populatedGroup.members;
[challengeLeader, otherMember] = members;
challengeLeader = members[0]; // eslint-disable-line prefer-destructuring
otherMember = members[1]; // eslint-disable-line prefer-destructuring
challenge = await generateChallenge(challengeLeader, group);
});
@@ -74,7 +74,7 @@ describe('GET /challenges/:challengeId/export/csv', () => {
});
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 sortedMembers = _.sortBy([members[1], members[2], groupLeader], '_id');
const splitRes = res.split('\n');
@@ -71,18 +71,42 @@ describe('GET /challenges/:challengeId/members', () => {
});
});
it('populates only some fields', async () => {
const group = await generateGroup(user, { type: 'party', privacy: 'private', name: generateUUID() });
const challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
it('works with challenges belonging to public guild', async () => {
const leader = await generateUser({ balance: 4 });
const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() });
const challenge = await generateChallenge(leader, group);
await leader.post(`/challenges/${challenge._id}/join`);
const res = await user.get(`/challenges/${challenge._id}/members`);
expect(res[0]).to.eql({
_id: user._id,
id: user._id,
profile: { name: user.profile.name },
_id: leader._id,
id: leader._id,
profile: { name: leader.profile.name },
auth: {
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: {
@@ -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 () => {
const group = await generateGroup(user, { type: 'party', name: generateUUID() });
const challenge = await generateChallenge(user, group);
@@ -7,7 +7,117 @@ import {
import { TAVERN_ID } from '../../../../../website/common/script/constants';
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
challenge2;
@@ -18,7 +128,6 @@ describe('GET challenges/groups/:groupId', () => {
type: 'guild',
privacy: 'private',
},
upgradeToGroupPlan: true,
});
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', () => {
let party; let user; let nonMember; let challenge; let
challenge2;
@@ -186,7 +357,7 @@ describe('GET challenges/groups/:groupId', () => {
before(async () => {
user = await generateUser();
await user.updateOne({ balance: 0.5 });
await user.update({ balance: 0.5 });
tavern = await user.get(`/groups/${TAVERN_ID}`);
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 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);
});
});
});
});
@@ -2,44 +2,39 @@ import {
generateUser,
generateChallenge,
createAndPopulateGroup,
resetHabiticaDB,
} from '../../../../helpers/api-integration/v3';
import { TAVERN_ID } from '../../../../../website/common/script/constants';
describe('GET challenges/user', () => {
context('no official challenges', () => {
let user; let member; let nonMember; let challenge; let challenge2; let publicChallenge;
let groupPlan; let userData; let groupData; let tavern; let tavernData;
let user; let member; let nonMember; let challenge; let challenge2;
let publicGuild; let userData; let groupData;
before(async () => {
await resetHabiticaDB();
const { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: 'TestGuild',
type: 'guild',
privacy: 'private',
privacy: 'public',
},
members: 1,
upgradeToGroupPlan: true,
});
groupPlan = group;
publicGuild = group;
groupData = {
_id: groupPlan._id,
_id: publicGuild._id,
categories: [],
id: groupPlan._id,
type: groupPlan.type,
privacy: groupPlan.privacy,
name: groupPlan.name,
summary: groupPlan.name,
leader: groupPlan.leader._id,
id: publicGuild._id,
type: publicGuild.type,
privacy: publicGuild.privacy,
name: publicGuild.name,
summary: publicGuild.name,
leader: publicGuild.leader._id,
};
user = groupLeader;
userData = {
_id: groupPlan.leader._id,
id: groupPlan.leader._id,
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: { name: user.profile.name },
auth: {
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
nonMember = await generateUser();
challenge = 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', () => {
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 });
expect(foundChallenge).to.exist;
@@ -83,13 +64,11 @@ describe('GET challenges/user', () => {
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 foundPublicChallenge = _.find(challenges, { _id: publicChallenge._id });
expect(foundPublicChallenge).to.exist;
expect(foundPublicChallenge.leader).to.eql(userData);
expect(foundPublicChallenge.group).to.eql(tavernData);
const foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundChallenge2).to.not.exist;
});
it('should return challenges user has created', async () => {
@@ -121,10 +100,10 @@ describe('GET challenges/user', () => {
it('should return newest challenges first', async () => {
let challenges = await user.get('/challenges/user?page=0');
let foundChallengeIndex = _.findIndex(challenges, { _id: publicChallenge._id });
let foundChallengeIndex = _.findIndex(challenges, { _id: challenge2._id });
expect(foundChallengeIndex).to.eql(0);
const newChallenge = await generateChallenge(user, groupPlan);
const newChallenge = await generateChallenge(user, publicGuild);
await user.post(`/challenges/${newChallenge._id}/join`);
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 () => {
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'TestPrivateGuild',
summary: 'summary for TestPrivateGuild',
type: 'guild',
privacy: 'private',
},
});
const privateChallenge = await generateChallenge(groupLeader, group);
await groupLeader.post(`/challenges/${privateChallenge._id}/join`);
const challenges = await nonMember.get('/challenges/user?page=0');
const foundChallenge = _.find(challenges, { _id: challenge._id });
const foundChallenge = _.find(challenges, { _id: privateChallenge._id });
expect(foundChallenge).to.not.exist;
});
it('should not return challenges user doesn\'t have access to, even with query parameters', async () => {
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'TestPrivateGuild',
summary: 'summary for TestPrivateGuild',
type: 'guild',
privacy: 'private',
},
});
const privateChallenge = await generateChallenge(groupLeader, group, {
categories: [{
name: 'academics',
slug: 'academics',
}],
});
await groupLeader.post(`/challenges/${privateChallenge._id}/join`);
const challenges = await nonMember.get('/challenges/user?page=0&categories=academics&owned=not_owned');
const foundChallenge = _.find(challenges, { _id: challenge._id });
const foundChallenge = _.find(challenges, { _id: privateChallenge._id });
expect(foundChallenge).to.not.exist;
});
});
context('my challenges', () => {
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 });
expect(foundChallenge).to.exist;
@@ -169,10 +177,6 @@ describe('GET challenges/user', () => {
expect(foundChallenge2).to.exist;
expect(foundChallenge2.leader).to.eql(userData);
expect(foundChallenge2.group).to.eql(groupData);
const foundPublicChallenge = _.find(challenges, { _id: publicChallenge._id });
expect(foundPublicChallenge).to.exist;
expect(foundPublicChallenge.leader).to.eql(userData);
expect(foundPublicChallenge.group).to.eql(tavernData);
});
it('should return challenges user has created if filter by owned', async () => {
@@ -186,10 +190,6 @@ describe('GET challenges/user', () => {
expect(foundChallenge2).to.exist;
expect(foundChallenge2.leader).to.eql(userData);
expect(foundChallenge2.group).to.eql(groupData);
const foundPublicChallenge = _.find(challenges, { _id: publicChallenge._id });
expect(foundPublicChallenge).to.exist;
expect(foundPublicChallenge.leader).to.eql(userData);
expect(foundPublicChallenge.group).to.eql(tavernData);
});
it('should not return challenges user has created if filter by not owned', async () => {
@@ -199,42 +199,38 @@ describe('GET challenges/user', () => {
expect(foundChallenge1).to.not.exist;
const foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundChallenge2).to.not.exist;
const foundPublicChallenge = _.find(challenges, { _id: publicChallenge._id });
expect(foundPublicChallenge).to.not.exist;
});
it('should not return challenges in user groups', async () => {
const challenges = await member.get(`/challenges/user?page=0&member=${true}`);
const foundChallenge1 = _.find(challenges, { _id: challenge._id });
expect(foundChallenge1).to.not.exist;
const foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundChallenge2).to.not.exist;
});
it('should not return public challenges', async () => {
const challenges = await member.get(`/challenges/user?page=0&member=${true}`);
const foundPublicChallenge = _.find(challenges, { _id: publicChallenge._id });
expect(foundPublicChallenge).to.not.exist;
});
});
});
context('official challenge is present', () => {
let user; let officialChallenge; let unofficialChallenges; let
group;
publicGuild;
before(async () => {
({ group, groupLeader: user } = await createAndPopulateGroup({
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'TestGuild',
summary: 'summary for TestGuild',
type: 'guild',
privacy: 'private',
privacy: 'public',
},
upgradeToGroupPlan: true,
}));
});
await user.updateOne({
user = groupLeader;
publicGuild = group;
await user.update({
'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`);
challenges = await user.get('/challenges/user?page=0');
@@ -298,17 +294,16 @@ describe('GET challenges/user', () => {
groupDetails: {
name: 'TestGuild',
type: 'guild',
privacy: 'private',
privacy: 'public',
},
members: 1,
upgradeToGroupPlan: true,
});
user = groupLeader;
guild = group;
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) {
let challenge = await generateChallenge(user, group); // eslint-disable-line
@@ -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'),
});
});
});
@@ -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);
});
});
@@ -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 summary = 'A'.repeat(MAX_SUMMARY_SIZE_FOR_CHALLENGES + 1);
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 group;
let groupMember;
@@ -75,11 +94,9 @@ describe('POST /challenges', () => {
challenges: true,
},
},
upgradeToGroupPlan: true,
});
groupLeader = await populatedGroup.groupLeader.sync();
await groupLeader.updateOne({ permissions: {} });
group = populatedGroup.group;
groupMember = populatedGroup.members[0]; // eslint-disable-line prefer-destructuring
});
@@ -177,7 +194,7 @@ describe('POST /challenges', () => {
const oldUserBalance = groupLeader.balance;
const prize = 8;
await group.updateOne({ balance: 0 });
await group.update({ balance: 0 });
await groupLeader.post('/challenges', {
group: group._id,
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 () => {
await groupLeader.updateOne({
await groupLeader.update({
permissions: {
challengeAdmin: true,
},
@@ -331,71 +348,5 @@ describe('POST /challenges', () => {
expect(updatedChallenge.summary).to.eql(summary);
});
it('sets categories for challenges', async () => {
const testCategory = { _id: '65c1172997c0b24600371ea9', slug: 'test', name: 'Test' };
const challenge = await groupLeader.post('/challenges', {
group: group._id,
name: 'Test Challenge',
shortName: 'TC Label',
categories: [testCategory],
});
const updatedChallenge = await groupLeader.get(`/challenges/${challenge._id}`);
expect(updatedChallenge.categories).to.eql([testCategory]);
});
it('does not set habitica_official category for non-admins', async () => {
const testCategory = { _id: '65c1172997c0b24600371ea9', slug: 'habitica_official', name: 'habitica_official' };
await expect(groupLeader.post('/challenges', {
group: group._id,
name: 'Test Challenge',
shortName: 'TC Label',
categories: [testCategory],
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('noPrivAccess'),
});
});
it('sets habitica_official category for admins', async () => {
await groupLeader.updateOne({
permissions: {
challengeAdmin: true,
},
});
const testCategory = { _id: '65c1172997c0b24600371ea9', slug: 'habitica_official', name: 'habitica_official' };
const challenge = await groupLeader.post('/challenges', {
group: group._id,
name: 'Test Challenge',
shortName: 'TC Label',
categories: [testCategory],
});
const updatedChallenge = await groupLeader.get(`/challenges/${challenge._id}`);
expect(updatedChallenge.categories).to.eql([testCategory]);
});
it('sets official if the habitica_official category is set for admins', async () => {
await groupLeader.updateOne({
permissions: {
challengeAdmin: true,
},
});
const testCategory = { _id: '65c1172997c0b24600371ea9', slug: 'habitica_official', name: 'habitica_official' };
const challenge = await groupLeader.post('/challenges', {
group: group._id,
name: 'Test Challenge',
shortName: 'TC Label',
categories: [testCategory],
});
const updatedChallenge = await groupLeader.get(`/challenges/${challenge._id}`);
expect(updatedChallenge.official).to.eql(true);
});
});
});
@@ -128,10 +128,10 @@ describe('POST /challenges/:challengeId/winner/:winnerId', () => {
const oldBalance = winningUser.balance;
const oldLeaderBalance = (await groupLeader.sync()).balance;
await winningUser.updateOne({
await winningUser.update({
'purchased.plan.customerId': 'group-plan',
});
await group.updateOne({
await group.update({
'leaderOnly.getGems': true,
'purchased.plan.customerId': 123,
});
@@ -18,7 +18,6 @@ describe('PUT /challenges/:challengeId', () => {
privacy: 'private',
},
members: 1,
upgradeToGroupPlan: true,
});
privateGuild = group;
@@ -1,6 +1,7 @@
import { v4 as generateUUID } from 'uuid';
import {
createAndPopulateGroup,
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
@@ -9,30 +10,27 @@ describe('DELETE /groups/:groupId/chat/:chatId', () => {
admin;
before(async () => {
const { group, groupLeader, members } = await createAndPopulateGroup({
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'private',
privacy: 'public',
},
leaderDetails: {
'auth.timestamps.created': new Date('2022-01-01'),
balance: 10,
},
members: 2,
upgradeToGroupPlan: true,
});
groupWithChat = group;
user = groupLeader;
message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: 'Some message' });
message = message.message;
userThatDidNotCreateChat = members[0]; // eslint-disable-line prefer-destructuring
admin = members[1]; // eslint-disable-line prefer-destructuring
await admin.updateOne({ permissions: { moderator: true } });
userThatDidNotCreateChat = await generateUser();
admin = await generateUser({ 'permissions.moderator': true });
});
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();
await expect(user.del(`/groups/${groupWithChat._id}/chat/${fakeChatId}`)).to.eventually.be.rejected.and.eql({
code: 404,
@@ -58,7 +56,7 @@ describe('DELETE /groups/:groupId/chat/:chatId', () => {
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}`);
const returnedMessages = await user.get(`/groups/${groupWithChat._id}/chat/`);
+36 -10
View File
@@ -1,6 +1,6 @@
import {
generateUser,
createAndPopulateGroup,
generateGroup,
translate as t,
} from '../../../../helpers/api-integration/v3';
@@ -11,22 +11,48 @@ describe('GET /groups/:groupId/chat', () => {
user = await generateUser();
});
context('public Guild', () => {
let group;
before(async () => {
const leader = await generateUser({ balance: 2 });
group = await generateGroup(leader, {
name: 'test group',
type: 'guild',
privacy: 'public',
}, {
chat: [
{ text: 'Hello', flags: {}, id: 1 },
{ text: 'Welcome to the Guild', flags: {}, id: 2 },
],
});
});
it('returns Guild chat', async () => {
const chat = await user.get(`/groups/${group._id}/chat`);
expect(chat[0].id).to.eql(group.chat[0].id);
expect(chat[1].id).to.eql(group.chat[1].id);
});
});
context('private Guild', () => {
let group;
before(async () => {
({ group } = await createAndPopulateGroup({
groupDetails: {
name: 'test group',
type: 'guild',
privacy: 'private',
},
members: 1,
upgradeToGroupPlan: true,
const leader = await generateUser({ balance: 2 });
group = await generateGroup(leader, {
name: 'test group',
type: 'guild',
privacy: 'private',
}, {
chat: [
'Hello',
'Welcome to the Guild',
],
}));
});
});
it('returns error if user is not member of requested private group', async () => {
@@ -1,42 +1,32 @@
import find from 'lodash/find';
import { find } from 'lodash';
import moment from 'moment';
import nconf from 'nconf';
import { IncomingWebhook } from '@slack/webhook';
import {
createAndPopulateGroup,
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
const BASE_URL = nconf.get('BASE_URL');
describe('POST /chat/:chatId/flag', () => {
let user; let admin; let anotherUser; let newUser; let
group; let members; let userToDelete;
group;
const TEST_MESSAGE = 'Test Message';
const USER_AGE_FOR_FLAGGING = 3;
beforeEach(async () => {
({ group, groupLeader: user, members } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
privacy: 'private',
},
leaderDetails: {
'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate(),
},
members: 4,
upgradeToGroupPlan: true,
}));
[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(),
});
user = await generateUser({ balance: 1, 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate() });
admin = await generateUser({ balance: 1, 'permissions.moderator': true });
anotherUser = await generateUser({ 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate() });
newUser = await generateUser({ 'auth.timestamps.created': moment().subtract(1, 'days').toDate() });
sandbox.stub(IncomingWebhook.prototype, 'send').returns(Promise.resolve());
group = await user.post('/groups', {
name: 'Test Guild',
type: 'guild',
privacy: 'public',
});
});
afterEach(() => {
@@ -79,8 +69,8 @@ describe('POST /chat/:chatId/flag', () => {
fallback: 'Flag Message',
color: 'danger',
author_name: `@${anotherUser.auth.local.username} ${anotherUser.profile.name} (${anotherUser.auth.local.email}; ${anotherUser._id})\n${timestamp}`,
title: 'Flag in Test Guild - (private guild)',
title_link: undefined,
title: 'Flag in Test Guild',
title_link: `${BASE_URL}/groups/guild/${group._id}`,
text: TEST_MESSAGE,
footer: `<https://habitrpg.github.io/flag-o-rama/?groupId=${group._id}&chatId=${message.id}|Flag this message.>`,
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 () => {
@@ -114,8 +104,8 @@ describe('POST /chat/:chatId/flag', () => {
fallback: 'Flag Message',
color: 'danger',
author_name: `@${newUser.auth.local.username} ${newUser.profile.name} (${newUser.auth.local.email}; ${newUser._id})\n${timestamp}`,
title: 'Flag in Test Guild - (private guild)',
title_link: undefined,
title: 'Flag in Test Guild',
title_link: `${BASE_URL}/groups/guild/${group._id}`,
text: TEST_MESSAGE,
footer: `<https://habitrpg.github.io/flag-o-rama/?groupId=${group._id}&chatId=${message.id}|Flag this message.> ${automatedComment}`,
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 () => {
const { message } = await userToDelete.post(`/groups/${group._id}/chat`, { message: TEST_MESSAGE });
await userToDelete.del('/user', {
const deletedUser = await generateUser({
'auth.timestamps.created': new Date('2022-01-01'),
});
const { message } = await deletedUser.post(`/groups/${group._id}/chat`, { message: TEST_MESSAGE });
await deletedUser.del('/user', {
password: 'password',
});
@@ -6,28 +6,28 @@ import {
describe('POST /chat/:chatId/like', () => {
let user;
let anotherUser;
let groupWithChat;
let members;
const testMessage = 'Test Message';
let anotherUser;
before(async () => {
({ group: groupWithChat, groupLeader: user, members } = await createAndPopulateGroup({
const { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
privacy: 'private',
privacy: 'public',
},
members: 1,
leaderDetails: {
'auth.timestamps.created': new Date('2022-01-01'),
balance: 10,
},
upgradeToGroupPlan: true,
}));
});
[anotherUser] = members;
await anotherUser.updateOne({ 'auth.timestamps.created': new Date('2022-01-01') });
user = groupLeader;
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 () => {
@@ -39,21 +39,19 @@ describe('POST /chat/:chatId/like', () => {
});
});
it('Likes a chat', async () => {
const message = await anotherUser.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
it('Returns an error when user tries to like their own message', async () => {
const message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
const likeResult = await user.post(`/groups/${groupWithChat._id}/chat/${message.message.id}/like`);
expect(likeResult.likes[user._id]).to.equal(true);
const groupWithChatLikes = await user.get(`/groups/${groupWithChat._id}`);
const messageToCheck = find(groupWithChatLikes.chat, { id: message.message.id });
expect(messageToCheck.likes[user._id]).to.equal(true);
await expect(user.post(`/groups/${groupWithChat._id}/chat/${message.message.id}/like`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('messageGroupChatLikeOwnMessage'),
});
});
it('Allows to likes their own chat message', async () => {
const message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
it('Likes a chat', async () => {
const message = await anotherUser.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
const likeResult = await user.post(`/groups/${groupWithChat._id}/chat/${message.message.id}/like`);
+320 -57
View File
@@ -1,43 +1,52 @@
import { IncomingWebhook } from '@slack/webhook';
import nconf from 'nconf';
import { v4 as generateUUID } from 'uuid';
import {
createAndPopulateGroup,
generateUser,
translate as t,
sleep,
server,
} from '../../../../helpers/api-integration/v3';
import {
SPAM_MESSAGE_LIMIT,
SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
TAVERN_ID,
} from '../../../../../website/server/models/group';
import { 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';
const BASE_URL = nconf.get('BASE_URL');
describe('POST /chat', () => {
let user; let groupWithChat; let member; let
additionalMember;
const testMessage = 'Test Message';
const testBannedWordMessage = 'TESTPLACEHOLDERSWEARWORDHERE';
const testBannedWordMessage1 = 'TESTPLACEHOLDERSWEARWORDHERE1';
const testSlurMessage = 'message with TESTPLACEHOLDERSLURWORDHERE';
const testSlurMessage1 = 'TESTPLACEHOLDERSLURWORDHERE1';
const bannedWordErrorMessage = t('bannedWordUsed', { swearWordsUsed: testBannedWordMessage });
before(async () => {
const { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
privacy: 'private',
privacy: 'public',
},
members: 2,
upgradeToGroupPlan: true,
});
user = groupLeader;
await user.updateOne({
await user.update({
'contributor.level': SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
'auth.timestamps.created': new Date('2022-01-01'),
}); // prevent tests accidentally throwing messageGroupChatSpam
groupWithChat = group;
[member, additionalMember] = members;
await member.updateOne({ 'auth.timestamps.created': new Date('2022-01-01') });
await additionalMember.updateOne({ 'auth.timestamps.created': new Date('2022-01-01') });
member = members[0]; // eslint-disable-line prefer-destructuring
additionalMember = members[1]; // eslint-disable-line prefer-destructuring
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 () => {
@@ -77,15 +86,35 @@ describe('POST /chat', () => {
describe('mute user', () => {
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 () => {
await member.updateOne({
'flags.chatRevoked': true,
const { group, members } = await createAndPopulateGroup({
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;
});
@@ -101,7 +130,7 @@ describe('POST /chat', () => {
});
const privatePartyMemberWithChatsRevoked = members[0];
await privatePartyMemberWithChatsRevoked.updateOne({
await privatePartyMemberWithChatsRevoked.update({
'flags.chatRevoked': true,
'auth.timestamps.created': new Date('2022-01-01'),
});
@@ -120,15 +149,57 @@ describe('POST /chat', () => {
afterEach(() => {
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 () => {
await member.updateOne({
'flags.chatShadowMuted': true,
const { group, members } = await createAndPopulateGroup({
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.flagCount).to.eql(0);
@@ -145,7 +216,7 @@ describe('POST /chat', () => {
});
const userWithChatShadowMuted = members[0];
await userWithChatShadowMuted.updateOne({
await userWithChatShadowMuted.update({
'flags.chatShadowMuted': true,
'auth.timestamps.created': new Date('2022-01-01'),
});
@@ -155,9 +226,100 @@ describe('POST /chat', () => {
expect(message.message.id).to.exist;
expect(message.message.flagCount).to.eql(0);
});
it('creates a chat with zero flagCount when non-shadow-muted user sends a message to a public guild', async () => {
const message = await member.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
expect(message.message.id).to.exist;
expect(message.message.flagCount).to.eql(0);
});
});
context('banned word', () => {
it('returns an error when chat message contains a banned word in tavern', async () => {
await expect(user.post('/groups/habitrpg/chat', { message: testBannedWordMessage }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: bannedWordErrorMessage,
});
});
it('returns an error when chat message contains a banned word in a public guild', async () => {
const { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'public guild',
type: 'guild',
privacy: 'public',
},
members: 1,
});
await expect(members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: bannedWordErrorMessage,
});
});
it('errors when word is part of a phrase', async () => {
const wordInPhrase = `phrase ${testBannedWordMessage} end`;
await expect(user.post('/groups/habitrpg/chat', { message: wordInPhrase }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: bannedWordErrorMessage,
});
});
it('errors when word is surrounded by non alphabet characters', async () => {
const wordInPhrase = `_!${testBannedWordMessage}@_`;
await expect(user.post('/groups/habitrpg/chat', { message: wordInPhrase }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: bannedWordErrorMessage,
});
});
it('errors when word is typed in mixed case', async () => {
const substrLength = Math.floor(testBannedWordMessage.length / 2);
const chatMessage = testBannedWordMessage.substring(0, substrLength).toLowerCase()
+ testBannedWordMessage.substring(substrLength).toUpperCase();
await expect(user.post('/groups/habitrpg/chat', { message: chatMessage }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedWordUsed', { swearWordsUsed: chatMessage }),
});
});
it('checks error message has all the banned words used, regardless of case', async () => {
const testBannedWords = [
testBannedWordMessage.toUpperCase(),
testBannedWordMessage1.toLowerCase(),
];
const chatMessage = `Mixing ${testBannedWords[0]} and ${testBannedWords[1]} is bad for you.`;
await expect(user.post('/groups/habitrpg/chat', { message: chatMessage }))
.to.eventually.be.rejected
.and.have.property('message')
.that.includes(testBannedWords.join(', '));
});
it('does not error when bad word is suffix of a word', async () => {
const wordAsSuffix = `prefix${testBannedWordMessage}`;
const message = await user.post('/groups/habitrpg/chat', { message: wordAsSuffix });
expect(message.message.id).to.exist;
});
it('does not error when bad word is prefix of a word', async () => {
const wordAsPrefix = `${testBannedWordMessage}suffix`;
const message = await user.post('/groups/habitrpg/chat', { message: wordAsPrefix });
expect(message.message.id).to.exist;
});
it('does not error when sending a chat message containing a banned word to a party', async () => {
const { group, members } = await createAndPopulateGroup({
groupDetails: {
@@ -167,7 +329,26 @@ describe('POST /chat', () => {
},
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 });
@@ -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 () => {
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;
});
@@ -189,7 +380,46 @@ describe('POST /chat', () => {
afterEach(() => {
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 () => {
@@ -201,23 +431,34 @@ describe('POST /chat', () => {
},
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 });
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 () => {
await user.updateOne({ 'auth.timestamps.created': new Date() });
await expect(user.post(`/groups/${groupWithChat._id}/chat`, { message: 'hi im new' }))
const brandNewUser = await generateUser();
await expect(brandNewUser.post('/groups/habitrpg/chat', { message: 'hi im new' }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('chatTemporarilyUnavailable'),
});
await user.updateOne({ 'auth.timestamps.created': new Date('2022-01-01') });
});
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 () => {
const memberUsername = 'memberUsername';
await member.updateOne({ 'auth.local.username': memberUsername });
await member.update({ 'auth.local.username': memberUsername });
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.`;
@@ -278,43 +519,26 @@ describe('POST /chat', () => {
const mount = 'test-mount';
const pet = 'test-pet';
const style = 'test-style';
await user.updateOne({
const userWithStyle = await generateUser({
'items.currentMount': mount,
'items.currentPet': pet,
'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.userStyles.items.currentMount).to.eql(user.items.currentMount);
expect(message.message.userStyles.items.currentPet).to.eql(user.items.currentPet);
expect(message.message.userStyles.preferences.style).to.eql(user.preferences.style);
expect(message.message.userStyles.preferences.hair).to.eql(user.preferences.hair);
expect(message.message.userStyles.preferences.skin).to.eql(user.preferences.skin);
expect(message.message.userStyles.preferences.shirt).to.eql(user.preferences.shirt);
expect(message.message.userStyles.preferences.chair).to.eql(user.preferences.chair);
expect(message.message.userStyles.items.currentMount).to.eql(userWithStyle.items.currentMount);
expect(message.message.userStyles.items.currentPet).to.eql(userWithStyle.items.currentPet);
expect(message.message.userStyles.preferences.style).to.eql(userWithStyle.preferences.style);
expect(message.message.userStyles.preferences.hair).to.eql(userWithStyle.preferences.hair);
expect(message.message.userStyles.preferences.skin).to.eql(userWithStyle.preferences.skin);
expect(message.message.userStyles.preferences.shirt).to.eql(userWithStyle.preferences.shirt);
expect(message.message.userStyles.preferences.chair).to.eql(userWithStyle.preferences.chair);
expect(message.message.userStyles.preferences.background)
.to.eql(user.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;
.to.eql(userWithStyle.preferences.background);
});
it('adds backer info to chat', async () => {
@@ -323,11 +547,12 @@ describe('POST /chat', () => {
tier: 800,
tokensApplied: true,
};
await user.updateOne({
const backer = await generateUser({
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;
expect(messageBackerInfo.npc).to.equal(backerInfo.npc);
@@ -375,7 +600,7 @@ describe('POST /chat', () => {
context('chat notifications', () => {
beforeEach(() => {
member.updateOne({ newMessages: {}, notifications: [] });
member.update({ newMessages: {}, notifications: [] });
});
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.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;
}
});
});
});
@@ -12,19 +12,18 @@ describe('POST /groups/:id/chat/seen', () => {
const { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'private',
privacy: 'public',
},
members: 1,
leaderDetails: {
'auth.timestamps.created': new Date('2022-01-01'),
balance: 10,
},
upgradeToGroupPlan: true,
});
guild = group;
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 = guildMessage.message;

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