Compare commits

..

2 Commits

Author SHA1 Message Date
SabreCat
eb967dae8b 4.221.2 2022-02-09 16:45:06 -06:00
SabreCat
d55c2af5fd fix(tz): remove moment-timezone completely 2022-02-09 16:44:40 -06:00
1103 changed files with 91835 additions and 61756 deletions

View File

@@ -2,9 +2,6 @@ name: Test
on: [push, pull_request]
permissions:
contents: read
jobs:
lint:
runs-on: ubuntu-latest

View File

@@ -1,7 +1,3 @@
# Files not included in deployments to Heroku, to save on file size.
/habitica-images
/test
/migrations
/scripts
/database_reports

View File

@@ -21,7 +21,6 @@ RUN npm install -g gulp-cli mocha
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

View File

@@ -2,7 +2,6 @@
"name": "Habitica V3 API Documentation",
"title": "Habitica",
"url": "https://habitica.com",
"version": "3.0.0",
"sampleUrl": null,
"header": {
"title": "Introduction",

View File

@@ -1,5 +1,4 @@
{
"ACCOUNT_MIN_CHAT_AGE": "0",
"ADMIN_EMAIL": "you@example.com",
"AMAZON_PAYMENTS_CLIENT_ID": "CLIENT_ID",
"AMAZON_PAYMENTS_MODE": "sandbox",
@@ -86,6 +85,5 @@
"RATE_LIMITER_ENABLED": "false",
"REDIS_HOST": "aaabbbcccdddeeefff",
"REDIS_PORT": "1234",
"REDIS_PASSWORD": "12345678",
"TRUSTED_DOMAINS": "https://localhost,https://habitica.com"
"REDIS_PASSWORD": "12345678"
}

View File

@@ -119,7 +119,7 @@ function path(obj, path, def) {
* @param {String} path dot separated
* @param {*} def default value ( if result undefined )
* @returns {*}
* https://stackoverflow.com/a/16190716
* http://stackoverflow.com/a/16190716
* Usage: console.log(path(someObject, pathname));
*/
for(var i = 0,path = path.split('.'),len = path.length; i < len; i++){

View File

@@ -1,3 +1,4 @@
version: "3"
services:
client:
build:
@@ -8,6 +9,7 @@ services:
- server
environment:
- BASE_URL=http://server:3000
image: habitica
networks:
- habitica
ports:
@@ -25,6 +27,7 @@ services:
- mongo
environment:
- NODE_DB_URI=mongodb://mongo/habitrpg
image: habitica
networks:
- habitica
ports:

View File

@@ -20,25 +20,17 @@ function cssVarMap (sprite) {
if (requiresSpecialTreatment) {
sprite.custom = {
px: {
offsetX: '-25px',
offsetY: '-15px',
offsetX: `-${sprite.x + 25}px`,
offsetY: `-${sprite.y + 15}px`,
width: '60px',
height: '60px',
},
};
}
// even more for shirts
if (sprite.name.indexOf('shirt') !== -1) {
sprite.custom.px.offsetX = '-29px';
sprite.custom.px.offsetY = '-42px';
}
if (sprite.name.indexOf('shirt') !== -1) sprite.custom.px.offsetY = `-${sprite.y + 35}px`; // even more for shirts
if (sprite.name.indexOf('hair_base') !== -1) {
const styleArray = sprite.name.split('_').slice(2, 3);
if (Number(styleArray[0]) > 14) {
sprite.custom.px.offsetY = '0'; // don't crop updos
}
if (Number(styleArray[0]) > 14) sprite.custom.px.offsetY = `-${sprite.y}px`; // don't crop updos
}
}

View File

@@ -2,7 +2,7 @@
// TODO it might be better we just find() and save() all user objects using mongoose, and rely on our defined pre('save')
// and default values to "migrate" users. This way we can make sure those parts are working properly too
// @see https://stackoverflow.com/questions/14867697/mongoose-full-collection-scan
// @see http://stackoverflow.com/questions/14867697/mongoose-full-collection-scan
// Also, what do we think of a Mongoose Migration module? something like https://github.com/madhums/mongoose-migrate
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.

View File

@@ -19,7 +19,7 @@ const Timer = require('./utils/timer');
const connectToDb = require('./utils/connect').connectToDb;
const closeDb = require('./utils/connect').closeDb;
const message = '`This party\'s collection quest has been made easier! For details, refer to https://habitica.fandom.com/wiki/User_blog:LadyAlys/Collection_Quests_are_Now_Easier`';
const message = '`This party\'s collection quest has been made easier! For details, refer to http://habitica.fandom.com/wiki/User_blog:LadyAlys/Collection_Quests_are_Now_Easier`';
const timer = new Timer();

View File

@@ -1,138 +0,0 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20220309_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['FlyingPig-Base']
&& pets['FlyingPig-CottonCandyBlue']
&& pets['FlyingPig-CottonCandyPink']
&& pets['FlyingPig-Desert']
&& pets['FlyingPig-Golden']
&& pets['FlyingPig-Red']
&& pets['FlyingPig-Shade']
&& pets['FlyingPig-Skeleton']
&& pets['FlyingPig-White']
&& pets['FlyingPig-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['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['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['Gryphon-Base']
&& pets['Gryphon-CottonCandyBlue']
&& pets['Gryphon-CottonCandyPink']
&& pets['Gryphon-Desert']
&& pets['Gryphon-Golden']
&& pets['Gryphon-Red']
&& pets['Gryphon-Shade']
&& pets['Gryphon-Skeleton']
&& pets['Gryphon-White']
&& pets['Gryphon-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.birdsOfAFeather'] = 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('2021-08-01') },
};
const fields = {
_id: 1,
items: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.lean()
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1]._id,
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -1,128 +0,0 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20220524_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['Alligator-Base']
&& pets['Alligator-CottonCandyBlue']
&& pets['Alligator-CottonCandyPink']
&& pets['Alligator-Desert']
&& pets['Alligator-Golden']
&& pets['Alligator-Red']
&& pets['Alligator-Shade']
&& pets['Alligator-Skeleton']
&& pets['Alligator-White']
&& pets['Alligator-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['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['Turtle-Base']
&& pets['Turtle-CottonCandyBlue']
&& pets['Turtle-CottonCandyPink']
&& pets['Turtle-Desert']
&& pets['Turtle-Golden']
&& pets['Turtle-Red']
&& pets['Turtle-Shade']
&& pets['Turtle-Skeleton']
&& pets['Turtle-White']
&& pets['Turtle-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']) {
set['achievements.reptacularRumble'] = 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-01-01') },
};
const fields = {
_id: 1,
items: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.lean()
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1]._id,
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -1,97 +0,0 @@
/* eslint-disable no-console */
import { model as UserModel } from '../../../website/server/models/user';
import { TransactionModel } from '../../../website/server/models/transaction';
const MIGRATION_NAME = '20220915_transactions_user_name';
/* transaction config */
const transactionPerRun = 500;
const progressCount = 1000;
const transactionQuery = {
migration: { $ne: MIGRATION_NAME }, // skip already migrated entries
'transactionType': { $in: ['gift_send', 'gift_receive'] },
};
let count = 0;
async function updateTransaction (transaction, userNameMap) {
count++;
const set = {
migration: MIGRATION_NAME,
};
if (userNameMap.has(transaction.reference)) {
set['referenceText'] = userNameMap.get(transaction.reference);
} else {
set['referenceText'] = 'Account not found';
}
if (count % progressCount === 0) {
console.warn(`${count} ${transaction._id}`);
}
return TransactionModel.updateOne({
_id: transaction._id
}, { $set: set }).exec();
}
export default async function processTransactions () {
const fields = {
_id: 1,
reference: 1,
referenceText: 1,
};
const userNameMap = new Map();
while (true) { // eslint-disable-line no-constant-condition
const foundTransactions = await TransactionModel // eslint-disable-line no-await-in-loop
.find(transactionQuery)
.limit(transactionPerRun)
.sort({reference: 1})
.select(fields)
.lean()
.exec();
if (foundTransactions.length === 0) {
console.warn('All appropriate transactions found and modified.');
console.warn(`\n${count} transactions processed\n`);
break;
}
// check for unknown users and load the names
const userIdsToLoad = [];
for (const foundTransaction of foundTransactions) {
const userId = foundTransaction.reference;
if (userNameMap.has(userId)) {
continue;
}
userIdsToLoad.push(userId);
}
const users = await UserModel // eslint-disable-line no-await-in-loop
.find({
_id: { $in: userIdsToLoad }
})
.select({
_id: 1,
'auth.local.username': 1,
})
.lean()
.exec();
for (const user of users) {
const localUserName = user.auth?.local?.username;
if (!localUserName) {
console.warn(`\nNo Username found for ID: ${user._id}\n`);
continue;
}
userNameMap.set(user._id, localUserName)
}
await Promise.all(foundTransactions.map(t => updateTransaction(t, userNameMap))); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -1,86 +0,0 @@
/*
* Award Habitoween ladder items to participants in this month's Habitoween festivities
*/
/* eslint-disable no-console */
const MIGRATION_NAME = '20221031_habitoween_ladder'; // Update when running in future years
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
const set = {};
const inc = {
'items.food.Candy_Skeleton': 1,
'items.food.Candy_Base': 1,
'items.food.Candy_CottonCandyBlue': 1,
'items.food.Candy_CottonCandyPink': 1,
'items.food.Candy_Shade': 1,
'items.food.Candy_White': 1,
'items.food.Candy_Golden': 1,
'items.food.Candy_Zombie': 1,
'items.food.Candy_Desert': 1,
'items.food.Candy_Red': 1,
};
set.migration = MIGRATION_NAME;
if (user && user.items && user.items.pets && user.items.pets['JackOLantern-RoyalPurple']) {
set['items.mounts.JackOLantern-RoyalPurple'] = true;
} else if (user && user.items && user.items.mounts && user.items.mounts['JackOLantern-Glow']) {
set['items.pets.JackOLantern-RoyalPurple'] = 5;
} else if (user && user.items && user.items.pets && user.items.pets['JackOLantern-Glow']) {
set['items.mounts.JackOLantern-Glow'] = true;
} else if (user && user.items && user.items.mounts && user.items.mounts['JackOLantern-Ghost']) {
set['items.pets.JackOLantern-Glow'] = 5;
} else if (user && user.items && user.items.pets && user.items.pets['JackOLantern-Ghost']) {
set['items.mounts.JackOLantern-Ghost'] = true;
} else if (user && user.items && user.items.mounts && user.items.mounts['JackOLantern-Base']) {
set['items.pets.JackOLantern-Ghost'] = 5;
} else if (user && user.items && user.items.pets && user.items.pets['JackOLantern-Base']) {
set['items.mounts.JackOLantern-Base'] = true;
} else {
set['items.pets.JackOLantern-Base'] = 5;
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.update({_id: user._id}, {$inc: inc, $set: set}).exec();
}
export default async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
'auth.timestamps.loggedin': {$gt: new Date('2022-10-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],
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -1,119 +0,0 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20221031_pet_set_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['Wolf-Skeleton']
&& pets['TigerCub-Skeleton']
&& pets['PandaCub-Skeleton']
&& pets['LionCub-Skeleton']
&& pets['Fox-Skeleton']
&& pets['FlyingPig-Skeleton']
&& pets['Dragon-Skeleton']
&& pets['Cactus-Skeleton']
&& pets['BearCub-Skeleton']
&& pets['Gryphon-Skeleton']
&& pets['Hedgehog-Skeleton']
&& pets['Deer-Skeleton']
&& pets['Egg-Skeleton']
&& pets['Rat-Skeleton']
&& pets['Octopus-Skeleton']
&& pets['Seahorse-Skeleton']
&& pets['Parrot-Skeleton']
&& pets['Rooster-Skeleton']
&& pets['Spider-Skeleton']
&& pets['Owl-Skeleton']
&& pets['Penguin-Skeleton']
&& pets['TRex-Skeleton']
&& pets['Rock-Skeleton']
&& pets['Bunny-Skeleton']
&& pets['Slime-Skeleton']
&& pets['Sheep-Skeleton']
&& pets['Cuttlefish-Skeleton']
&& pets['Whale-Skeleton']
&& pets['Cheetah-Skeleton']
&& pets['Horse-Skeleton']
&& pets['Frog-Skeleton']
&& pets['Snake-Skeleton']
&& pets['Unicorn-Skeleton']
&& pets['Sabretooth-Skeleton']
&& pets['Monkey-Skeleton']
&& pets['Snail-Skeleton']
&& pets['Falcon-Skeleton']
&& pets['Treeling-Skeleton']
&& pets['Axolotl-Skeleton']
&& pets['Turtle-Skeleton']
&& pets['Armadillo-Skeleton']
&& pets['Cow-Skeleton']
&& pets['Beetle-Skeleton']
&& pets['Ferret-Skeleton']
&& pets['Sloth-Skeleton']
&& pets['Triceratops-Skeleton']
&& pets['GuineaPig-Skeleton']
&& pets['Peacock-Skeleton']
&& pets['Butterfly-Skeleton']
&& pets['Nudibranch-Skeleton']
&& pets['Hippo-Skeleton']
&& pets['Yarn-Skeleton']
&& pets['Pterodactyl-Skeleton']
&& pets['Badger-Skeleton']
&& pets['Squirrel-Skeleton']
&& pets['SeaSerpent-Skeleton']
&& pets['Kangaroo-Skeleton']
&& pets['Alligator-Skeleton']
&& pets['Velociraptor-Skeleton']
&& pets['Dolphin-Skeleton']
&& pets['Robot-Skeleton']) {
set['achievements.boneToPick'] = 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-01-01') },
};
const fields = {
_id: 1,
items: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.lean()
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1]._id,
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -1,108 +0,0 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20221213_pet_group_achievements';
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
const set = {
migration: MIGRATION_NAME,
};
if (user && user.items && user.items.pets) {
const pets = user.items.pets;
if (pets['BearCub-Base']
&& pets['BearCub-CottonCandyBlue']
&& pets['BearCub-CottonCandyPink']
&& pets['BearCub-Desert']
&& pets['BearCub-Golden']
&& pets['BearCub-Red']
&& pets['BearCub-Shade']
&& pets['BearCub-Skeleton']
&& pets['BearCub-White']
&& pets['BearCub-Zombie']
&& pets['Fox-Base']
&& pets['Fox-CottonCandyBlue']
&& pets['Fox-CottonCandyPink']
&& pets['Fox-Desert']
&& pets['Fox-Golden']
&& pets['Fox-Red']
&& pets['Fox-Shade']
&& pets['Fox-Skeleton']
&& pets['Fox-White']
&& pets['Fox-Zombie']
&& pets['Penguin-Base']
&& pets['Penguin-CottonCandyBlue']
&& pets['Penguin-CottonCandyPink']
&& pets['Penguin-Desert']
&& pets['Penguin-Golden']
&& pets['Penguin-Red']
&& pets['Penguin-Shade']
&& pets['Penguin-Skeleton']
&& pets['Penguin-White']
&& pets['Penguin-Zombie']
&& pets['Whale-Base']
&& pets['Whale-CottonCandyBlue']
&& pets['Whale-CottonCandyPink']
&& pets['Whale-Desert']
&& pets['Whale-Golden']
&& pets['Whale-Red']
&& pets['Whale-Shade']
&& pets['Whale-Skeleton']
&& pets['Whale-White']
&& pets['Whale-Zombie']
&& pets['Wolf-Base']
&& pets['Wolf-CottonCandyBlue']
&& pets['Wolf-CottonCandyPink']
&& pets['Wolf-Desert']
&& pets['Wolf-Golden']
&& pets['Wolf-Red']
&& pets['Wolf-Shade']
&& pets['Wolf-Skeleton']
&& pets['Wolf-White']
&& pets['Wolf-Zombie'] {
set['achievements.polarPro'] = true;
}
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.update({ _id: user._id }, { $set: set }).exec();
}
export default async function processUsers () {
let query = {
// migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2022-11-01') },
};
const fields = {
_id: 1,
items: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.lean()
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1]._id,
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -1,144 +0,0 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20221227_nye';
import { model as User } from '../../../website/server/models/user';
import { v4 as uuid } from 'uuid';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
const set = { migration: MIGRATION_NAME };
let push;
if (typeof user.items.gear.owned.head_special_nye2021 !== 'undefined') {
set['items.gear.owned.head_special_nye2022'] = false;
push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_nye2022',
_id: uuid(),
},
];
} else if (typeof user.items.gear.owned.head_special_nye2020 !== 'undefined') {
set['items.gear.owned.head_special_nye2021'] = false;
push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_nye2021',
_id: uuid(),
},
];
} else if (typeof user.items.gear.owned.head_special_nye2019 !== 'undefined') {
set['items.gear.owned.head_special_nye2020'] = false;
push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_nye2020',
_id: uuid(),
},
];
} else if (typeof user.items.gear.owned.head_special_nye2018 !== 'undefined') {
set['items.gear.owned.head_special_nye2019'] = false;
push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_nye2019',
_id: uuid(),
},
];
} else if (typeof user.items.gear.owned.head_special_nye2017 !== 'undefined') {
set['items.gear.owned.head_special_nye2018'] = false;
push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_nye2018',
_id: uuid(),
},
];
} else if (typeof user.items.gear.owned.head_special_nye2016 !== 'undefined') {
set['items.gear.owned.head_special_nye2017'] = false;
push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_nye2017',
_id: uuid(),
},
];
} else if (typeof user.items.gear.owned.head_special_nye2015 !== 'undefined') {
set['items.gear.owned.head_special_nye2016'] = false;
push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_nye2016',
_id: uuid(),
},
];
} else if (typeof user.items.gear.owned.head_special_nye2014 !== 'undefined') {
set['items.gear.owned.head_special_nye2015'] = false;
push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_nye2015',
_id: uuid(),
},
];
} else if (typeof user.items.gear.owned.head_special_nye !== 'undefined') {
set['items.gear.owned.head_special_nye2014'] = false;
push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_nye2014',
_id: uuid(),
},
];
} else {
set['items.gear.owned.head_special_nye'] = false;
push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_nye',
_id: uuid(),
},
];
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.update({_id: user._id}, {$set: set, $push: {pinnedItems: {$each: push}}}).exec();
}
export default async function processUsers () {
let query = {
'auth.timestamps.loggedin': {$gt: new Date('2022-12-01')},
migration: {$ne: MIGRATION_NAME},
};
const fields = {
_id: 1,
items: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.lean()
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1],
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

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

View File

@@ -1,69 +0,0 @@
/* eslint-disable no-console */
import { v4 as uuid } from 'uuid';
import { model as User } from '../../../website/server/models/user';
const MIGRATION_NAME = '20230127_habit_birthday_day5';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count += 1;
const set = {};
const push = {};
set.migration = MIGRATION_NAME;
set['items.gear.owned.back_special_anniversary'] = true;
set['items.gear.owned.body_special_anniversary'] = true;
set['items.gear.owned.eyewear_special_anniversary'] = true;
push.notifications = {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_head_special_nye',
title: 'Birthday Bash Day 5!',
text: 'Come celebrate by wearing your new Habitica Hero Cape, Collar, and Mask!',
destination: 'equipment',
},
seen: false,
};
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.update({_id: user._id}, {$set: set, $push: push}).exec();
}
export default async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
'auth.timestamps.loggedin': {$gt: new Date('2022-12-23')},
};
const fields = {
_id: 1,
items: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.lean()
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1],
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

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

View File

@@ -16,7 +16,7 @@ AWS.config.update({
const BUCKET_NAME = config.S3.bucket;
const s3 = new AWS.S3();
// Adapted from https://stackoverflow.com/a/22210077/2601552
// Adapted from http://stackoverflow.com/a/22210077/2601552
function uploadFile (buffer, fileName) {
return new Promise((resolve, reject) => {
s3.putObject({

View File

@@ -2,7 +2,7 @@
// For some reason people often to contact me to cancel their sub,
// rather than do it online. Even when I point them to
// the FAQ (https://habitica.fandom.com/wiki/FAQ) they insist...
// the FAQ (http://goo.gl/1uoPGQ) they insist...
db.users.update(
{ _id: '' },

View File

@@ -1,71 +0,0 @@
import filter from 'lodash/filter';
import find from 'lodash/find';
import isArray from 'lodash/isArray';
import { model as Group } from '../../website/server/models/group';
import { model as User } from '../../website/server/models/user';
import * as Tasks from '../../website/server/models/task';
async function updateTeamTasks (team) {
const toSave = [];
const teamTasks = await Tasks.Task.find({
'group.id': team._id,
}).exec();
const teamBoardTasks = filter(teamTasks, task => !task.userId);
const teamUserTasks = filter(teamTasks, task => task.userId);
for (const boardTask of teamBoardTasks) {
if (isArray(boardTask.group.assignedUsers)) {
boardTask.group.approval = undefined;
boardTask.group.assignedDate = undefined;
boardTask.group.assigningUsername = undefined;
boardTask.group.sharedCompletion = undefined;
for (const assignedUserId of boardTask.group.assignedUsers) {
const assignedUser = await User.findById(assignedUserId, 'auth'); // eslint-disable-line no-await-in-loop
const userTask = find(teamUserTasks, task => task.userId === assignedUserId
&& task.group.taskId === boardTask._id);
if (!boardTask.group.assignedUsersDetail) boardTask.group.assignedUsersDetail = {};
if (userTask && assignedUser) {
boardTask.group.assignedUsersDetail[assignedUserId] = {
assignedDate: userTask.group.assignedDate,
assignedUsername: assignedUser.auth.local.username,
assigningUsername: userTask.group.assigningUsername,
completed: userTask.completed || false,
completedDate: userTask.dateCompleted,
};
} else if (assignedUser) {
boardTask.group.assignedUsersDetail[assignedUserId] = {
assignedDate: new Date(),
assignedUsername: assignedUser.auth.local.username,
assigningUsername: null,
completed: false,
completedDate: null,
};
} else {
const taskIndex = boardTask.group.assignedUsers.indexOf(assignedUserId);
boardTask.group.assignedUsers.splice(taskIndex, 1);
}
if (userTask) toSave.push(Tasks.Task.findByIdAndDelete(userTask._id));
}
boardTask.markModified('group');
toSave.push(boardTask.save());
}
}
return Promise.all(toSave);
}
export default async function processTeams () {
const activeTeams = await Group.find({
'purchased.plan.customerId': { $exists: true },
$or: [
{ 'purchased.plan.dateTerminated': { $exists: false } },
{ 'purchased.plan.dateTerminated': null },
{ 'purchased.plan.dateTerminated': { $gt: new Date() } },
],
}).exec();
const taskPromises = activeTeams.map(updateTeamTasks);
return Promise.all(taskPromises);
}

View File

@@ -3,13 +3,13 @@ import { v4 as uuid } from 'uuid';
import { model as User } from '../../website/server/models/user';
const MIGRATION_NAME = '20230314_pi_day';
const MIGRATION_NAME = '20210314_pi_day';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count += 1;
count *= 1;
const inc = {
'items.food.Pie_Skeleton': 1,
@@ -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('2021-02-15') },
};
const fields = {

24253
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,22 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "4.267.1",
"version": "4.221.2",
"main": "./website/server/index.js",
"dependencies": {
"@babel/core": "^7.20.12",
"@babel/preset-env": "^7.20.2",
"@babel/register": "^7.18.9",
"@google-cloud/trace-agent": "^7.1.2",
"@parse/node-apn": "^5.1.3",
"@babel/core": "^7.16.12",
"@babel/preset-env": "^7.16.11",
"@babel/register": "^7.17.0",
"@google-cloud/trace-agent": "^5.1.6",
"@parse/node-apn": "^5.1.0",
"@slack/webhook": "^6.1.0",
"accepts": "^1.3.8",
"amazon-payments": "^0.2.9",
"amplitude": "^6.0.0",
"apidoc": "^0.54.0",
"amplitude": "^5.2.0",
"apidoc": "^0.50.3",
"apple-auth": "^1.0.7",
"bcrypt": "^5.1.0",
"body-parser": "^1.20.1",
"bcrypt": "^5.0.1",
"body-parser": "^1.19.1",
"bootstrap": "^4.6.0",
"compression": "^1.7.4",
"cookie-session": "^2.0.0",
@@ -27,31 +27,31 @@
"eslint": "^6.8.0",
"eslint-config-habitrpg": "^6.2.0",
"eslint-plugin-mocha": "^5.0.0",
"express": "^4.18.2",
"express": "^4.17.2",
"express-basic-auth": "^1.2.1",
"express-validator": "^5.2.0",
"glob": "^8.1.0",
"glob": "^7.2.0",
"got": "^11.8.3",
"gulp": "^4.0.0",
"gulp-babel": "^8.0.0",
"gulp-imagemin": "^7.1.0",
"gulp-nodemon": "^2.5.0",
"gulp.spritesmith": "^6.13.0",
"gulp.spritesmith": "^6.12.1",
"habitica-markdown": "^3.0.0",
"helmet": "^4.6.0",
"image-size": "^1.0.2",
"image-size": "^1.0.1",
"in-app-purchase": "^1.11.3",
"js2xmlparser": "^5.0.0",
"js2xmlparser": "^4.0.2",
"jsonwebtoken": "^8.5.1",
"jwks-rsa": "^2.1.5",
"jwks-rsa": "^2.0.5",
"lodash": "^4.17.21",
"merge-stream": "^2.0.0",
"method-override": "^3.0.0",
"moment": "^2.29.4",
"moment": "^2.29.1",
"moment-recur": "^1.0.7",
"mongoose": "^5.13.7",
"morgan": "^1.10.0",
"nconf": "^0.12.0",
"nconf": "^0.11.3",
"node-gcm": "^1.0.5",
"on-headers": "^1.0.2",
"passport": "^0.5.0",
@@ -61,20 +61,20 @@
"paypal-rest-sdk": "^1.8.1",
"pp-ipn": "^1.1.0",
"ps-tree": "^1.0.0",
"rate-limiter-flexible": "^2.4.0",
"rate-limiter-flexible": "^2.3.6",
"redis": "^3.1.2",
"regenerator-runtime": "^0.13.11",
"remove-markdown": "^0.5.0",
"regenerator-runtime": "^0.13.9",
"remove-markdown": "^0.3.0",
"rimraf": "^3.0.2",
"short-uuid": "^4.2.2",
"stripe": "^11.10.0",
"superagent": "^8.0.6",
"short-uuid": "^4.2.0",
"stripe": "^8.202.0",
"superagent": "^7.1.1",
"universal-analytics": "^0.5.3",
"useragent": "^2.1.9",
"uuid": "^9.0.0",
"validator": "^13.9.0",
"uuid": "^8.3.2",
"validator": "^13.7.0",
"vinyl-buffer": "^1.0.1",
"winston": "^3.8.2",
"winston": "^3.5.1",
"winston-loggly-bulk": "^3.2.1",
"xml2js": "^0.4.23"
},
@@ -106,23 +106,23 @@
"start": "gulp nodemon",
"debug": "gulp nodemon --inspect",
"mongo:dev": "run-rs -v 4.2.8 -l ubuntu1804 --keep --dbpath mongodb-data --number 1 --quiet",
"postinstall": "git config --global url.\"https://\".insteadOf git:// && gulp build && cd website/client && npm install",
"postinstall": "gulp build && cd website/client && npm install",
"apidoc": "gulp apidoc"
},
"devDependencies": {
"axios": "^1.2.2",
"chai": "^4.3.7",
"axios": "^0.25.0",
"chai": "^4.3.6",
"chai-as-promised": "^7.1.1",
"chai-moment": "^0.1.0",
"chalk": "^5.2.0",
"chalk": "^4.1.2",
"cross-spawn": "^7.0.3",
"expect.js": "^0.3.1",
"istanbul": "^1.1.0-alpha.1",
"mocha": "^5.1.1",
"monk": "^7.3.4",
"require-again": "^2.0.0",
"run-rs": "^0.7.7",
"sinon": "^15.0.1",
"run-rs": "^0.7.6",
"sinon": "^12.0.1",
"sinon-chai": "^3.7.0",
"sinon-stub-promise": "^4.0.0"
},

View File

@@ -40,7 +40,7 @@ async function deleteHabiticaData (user, email) {
'auth.local.passwordHashMethod': 'bcrypt',
};
if (!user.auth.local.email) set['auth.local.email'] = `${user._id}@example.com`;
await User.updateOne(
await User.update(
{ _id: user._id },
{ $set: set },
);

View File

@@ -1,105 +0,0 @@
import forEach from 'lodash/forEach';
import { model as Group } from '../website/server/models/group';
import { model as User } from '../website/server/models/user';
import * as Tasks from '../website/server/models/task';
import { daysSince, shouldDo } from '../website/common/script/cron';
const TASK_VALUE_CHANGE_FACTOR = 0.9747;
const MIN_TASK_VALUE = -47.27;
async function updateTeamTasks (team) {
const toSave = [];
let teamLeader = await User.findOne({ _id: team.leader }, 'preferences').exec();
if (!teamLeader) { // why would this happen?
teamLeader = {
preferences: { }, // when options are sanitized this becomes CDS 0 at UTC
};
}
if (
!team.cron || !team.cron.lastProcessed
|| daysSince(team.cron.lastProcessed, teamLeader.preferences) > 0
) {
const tasks = await Tasks.Task.find({
'group.id': team._id,
userId: { $exists: false },
$or: [
{ type: 'todo', completed: false },
{ type: { $in: ['habit', 'daily'] } },
],
}).exec();
const tasksByType = {
habits: [], dailys: [], todos: [], rewards: [],
};
forEach(tasks, task => tasksByType[`${task.type}s`].push(task));
forEach(tasksByType.habits, habit => {
if (!(habit.up && habit.down) && habit.value !== 0) {
habit.value *= 0.5;
if (Math.abs(habit.value) < 0.1) habit.value = 0;
toSave.push(habit.save());
}
});
forEach(tasksByType.todos, todo => {
if (!todo.completed) {
const delta = TASK_VALUE_CHANGE_FACTOR ** todo.value;
todo.value -= delta;
if (todo.value < MIN_TASK_VALUE) todo.value = MIN_TASK_VALUE;
toSave.push(todo.save());
}
});
forEach(tasksByType.dailys, daily => {
let processChecklist = false;
let assignments = 0;
let completions = 0;
for (const assignedUser in daily.group.assignedUsersDetail) {
if (Object.prototype.hasOwnProperty.call(daily.group.assignedUsersDetail, assignedUser)) {
assignments += 1;
if (daily.group.assignedUsersDetail[assignedUser].completed) {
completions += 1;
daily.group.assignedUsersDetail[assignedUser].completed = false;
}
}
}
if (completions > 0) daily.markModified('group.assignedUsersDetail');
if (daily.completed) {
processChecklist = true;
daily.completed = false;
} else if (shouldDo(team.cron.lastProcessed, daily, teamLeader.preferences)) {
processChecklist = true;
const delta = TASK_VALUE_CHANGE_FACTOR ** daily.value;
if (assignments > 0) {
daily.value -= ((completions / assignments) * delta);
}
if (daily.value < MIN_TASK_VALUE) daily.value = MIN_TASK_VALUE;
}
daily.isDue = shouldDo(new Date(), daily, teamLeader.preferences);
if (processChecklist && daily.checklist.length > 0) {
daily.checklist.forEach(i => { i.completed = false; });
}
toSave.push(daily.save());
});
if (!team.cron) team.cron = {};
team.cron.lastProcessed = new Date();
toSave.push(team.save());
}
return Promise.all(toSave);
}
export default async function processTeamsCron () {
const activeTeams = await Group.find({
'purchased.plan.customerId': { $exists: true },
$or: [
{ 'purchased.plan.dateTerminated': { $exists: false } },
{ 'purchased.plan.dateTerminated': null },
{ 'purchased.plan.dateTerminated': { $gt: new Date() } },
],
}).exec();
const cronPromises = activeTeams.map(updateTeamTasks);
return Promise.all(cronPromises);
}

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')

View File

@@ -13,6 +13,11 @@ function getUser () {
username: 'username',
email: 'email@email',
},
facebook: {
emails: [{
value: 'email@facebook',
}],
},
google: {
emails: [{
value: 'email@google',
@@ -57,12 +62,30 @@ describe('emails', () => {
expect(data).to.have.property('canSend', true);
});
it('returns correct user data [facebook users]', () => {
const attachEmail = requireAgain(pathToEmailLib);
const { getUserInfo } = attachEmail;
const user = getUser();
delete user.profile.name;
delete user.auth.local.email;
delete user.auth.google.emails;
delete user.auth.apple.emails;
const data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
expect(data).to.have.property('name', user.auth.local.username);
expect(data).to.have.property('email', user.auth.facebook.emails[0].value);
expect(data).to.have.property('_id', user._id);
expect(data).to.have.property('canSend', true);
});
it('returns correct user data [google users]', () => {
const attachEmail = requireAgain(pathToEmailLib);
const { getUserInfo } = attachEmail;
const user = getUser();
delete user.profile.name;
delete user.auth.local.email;
delete user.auth.facebook.emails;
delete user.auth.apple.emails;
const data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
@@ -80,6 +103,7 @@ describe('emails', () => {
delete user.profile.name;
delete user.auth.local.email;
delete user.auth.google.emails;
delete user.auth.facebook.emails;
const data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
@@ -94,6 +118,7 @@ describe('emails', () => {
const { getUserInfo } = attachEmail;
const user = getUser();
delete user.auth.local.email;
delete user.auth.facebook;
delete user.auth.google;
delete user.auth.apple;

View File

@@ -99,26 +99,23 @@ describe('Items Utils', () => {
expect(castItemVal('items.food.Cake_Invalid', '5')).to.equal(5);
});
it('converts values for mounts paths to numbers', () => {
expect(castItemVal('items.mounts.Cactus-Base', 'true')).to.equal(true);
expect(castItemVal('items.mounts.Aether-Invisible', 'false')).to.equal(false);
expect(castItemVal('items.mounts.Aether-Invalid', 'true')).to.equal(true);
expect(castItemVal('items.mounts.Aether-Invalid', 'truish')).to.equal(true);
expect(castItemVal('items.mounts.Aether-Invalid', 0)).to.equal(false);
});
it('converts values for quests paths to numbers', () => {
expect(castItemVal('items.quests.atom3', '5')).to.equal(5);
expect(castItemVal('items.quests.invalid', '5')).to.equal(5);
});
it('converts values for mounts paths to true/null', () => {
// mounts are never false but can be null (function contains more details)
expect(castItemVal('items.mounts.Cactus-Base', 'true')).to.equal(true);
expect(castItemVal('items.mounts.Aether-Invisible', 'null')).to.equal(null);
expect(castItemVal('items.mounts.Aether-Invisible', 'false')).to.equal(null);
expect(castItemVal('items.mounts.Aether-Invalid', 'true')).to.equal(true);
expect(castItemVal('items.mounts.Aether-Invalid', 'truthy')).to.equal(true);
expect(castItemVal('items.mounts.Aether-Invalid', 0)).to.equal(null);
});
it('converts values for owned gear to true/false', () => {
it('converts values for owned gear', () => {
expect(castItemVal('items.gear.owned.shield_warrior_0', 'true')).to.equal(true);
expect(castItemVal('items.gear.owned.invalid', 'false')).to.equal(false);
expect(castItemVal('items.gear.owned.invalid', 'null')).to.equal(false);
expect(castItemVal('items.gear.owned.invalid', 'truthy')).to.equal(true);
expect(castItemVal('items.gear.owned.invalid', 'thruthy')).to.equal(true);
expect(castItemVal('items.gear.owned.invalid', 0)).to.equal(false);
});
});

View File

@@ -246,7 +246,7 @@ describe('Password Utilities', () => {
it('returns false if the user has no local auth', async () => {
const user = await generateUser({
auth: {
google: {},
facebook: {},
},
});
const res = await validatePasswordResetCodeAndFindUser(encrypt(JSON.stringify({

View File

@@ -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();
});

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,20 @@ 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 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 +359,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 +376,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 +395,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 +417,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;

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,
},
});
});
});
@@ -261,7 +256,7 @@ describe('Google Payments', () => {
expirationDate,
});
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{ expirationDate: expirationDate.toDate(), autoRenewing: false }]);
.returns([{ expirationDate: expirationDate.toDate() }]);
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
.returns(true);
@@ -330,79 +325,5 @@ describe('Google Payments', () => {
headers,
});
});
it('should cancel a user subscription with multiple inactive subscriptions', async () => {
const laterDate = moment.utc().add(7, 'days');
iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{ expirationDate, autoRenewing: false },
{ expirationDate: laterDate, autoRenewing: false },
]);
await googlePayments.cancelSubscribe(user, headers);
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledWith(iap.GOOGLE, {
data: receipt,
signature,
});
expect(iapIsValidatedStub).to.be.calledOnce;
expect(iapIsValidatedStub).to.be.calledWith({
expirationDate,
});
expect(iapGetPurchaseDataStub).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
user,
paymentMethod: googlePayments.constants.PAYMENT_METHOD_GOOGLE,
nextBill: laterDate.toDate(),
headers,
});
});
it('should not cancel a user subscription with autorenew', async () => {
iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{ autoRenewing: true }]);
await googlePayments.cancelSubscribe(user, headers);
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledWith(iap.GOOGLE, {
data: receipt,
signature,
});
expect(iapIsValidatedStub).to.be.calledOnce;
expect(iapIsValidatedStub).to.be.calledWith({
expirationDate,
});
expect(iapGetPurchaseDataStub).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy).to.not.be.called;
});
it('should not cancel a user subscription with multiple subscriptions with one autorenew', async () => {
iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{ expirationDate, autoRenewing: false },
{ autoRenewing: true },
{ expirationDate, autoRenewing: false }]);
await googlePayments.cancelSubscribe(user, headers);
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledWith(iap.GOOGLE, {
data: receipt,
signature,
});
expect(iapIsValidatedStub).to.be.calledOnce;
expect(iapIsValidatedStub).to.be.calledWith({
expirationDate,
});
expect(iapGetPurchaseDataStub).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy).to.not.be.called;
});
});
});

View File

@@ -11,13 +11,10 @@ import {
generateGroup,
} from '../../../../helpers/api-unit.helper';
import * as worldState from '../../../../../website/server/libs/worldState';
import { TransactionModel } from '../../../../../website/server/models/transaction';
describe('payments/index', () => {
let user;
let group;
let data;
let plan;
let user; let group; let data; let
plan;
beforeEach(async () => {
user = new User();
@@ -107,23 +104,6 @@ describe('payments/index', () => {
expect(recipient.purchased.plan.extraMonths).to.eql(3);
});
it('add a transaction entry to the recipient', async () => {
recipient.purchased.plan = plan;
expect(recipient.purchased.plan.extraMonths).to.eql(0);
await api.createSubscription(data);
expect(recipient.purchased.plan.extraMonths).to.eql(3);
const transactions = await TransactionModel
.find({ userId: recipient._id })
.sort({ createdAt: -1 })
.exec();
expect(transactions).to.have.lengthOf(1);
});
it('does not set negative extraMonths if plan has past dateTerminated date', async () => {
const dateTerminated = moment().subtract(2, 'months').toDate();
recipient.purchased.plan.dateTerminated = dateTerminated;
@@ -203,28 +183,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 +193,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 +359,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 +366,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 +445,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', () => {
@@ -761,6 +468,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);
@@ -768,6 +476,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);
@@ -803,532 +512,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', () => {
@@ -1489,12 +672,10 @@ describe('payments/index', () => {
context('No Active Promotion', () => {
beforeEach(() => {
sinon.stub(worldState, 'getCurrentEvent').returns(null);
sinon.stub(worldState, 'getCurrentEventList').returns([]);
});
afterEach(() => {
worldState.getCurrentEvent.restore();
worldState.getCurrentEventList.restore();
});
it('does not apply a discount', async () => {
@@ -1511,14 +692,14 @@ describe('payments/index', () => {
context('Active Promotion', () => {
beforeEach(() => {
sinon.stub(worldState, 'getCurrentEventList').returns([{
sinon.stub(worldState, 'getCurrentEvent').returns({
...common.content.events.fall2020,
event: 'fall2020',
}]);
});
});
afterEach(() => {
worldState.getCurrentEventList.restore();
worldState.getCurrentEvent.restore();
});
it('applies a discount', async () => {

View File

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

View File

@@ -42,109 +42,3 @@ describe('xml marshaller marshalls user data', () => {
</user>`);
});
});
describe('xml marshaller marshalls user data (with purchases)', () => {
const minimumUser = {
pinnedItems: [],
unpinnedItems: [],
inbox: {},
};
function userDataWith (fields) {
return { ...minimumUser, ...fields };
}
it('maps the purchases field with data that begins with a number', () => {
const userData = userDataWith({
purchased: {
ads: false,
txnCount: 0,
skin: {
eb052b: true,
'0ff591': true,
'2b43f6': true,
d7a9f7: true,
'800ed0': true,
rainbow: true,
},
},
});
const xml = xmlMarshaller.marshallUserData(userData);
expect(xml).to.equal(`<user>
<inbox/>
<purchased>
<ads>false</ads>
<txnCount>0</txnCount>
<skin>eb052b</skin>
<skin>0ff591</skin>
<skin>2b43f6</skin>
<skin>d7a9f7</skin>
<skin>800ed0</skin>
<skin>rainbow</skin>
</purchased>
</user>`);
});
});
describe('xml marshaller marshalls user data (with purchases nested)', () => {
const minimumUser = {
pinnedItems: [],
unpinnedItems: [],
inbox: {},
};
function userDataWith (fields) {
return { ...minimumUser, ...fields };
}
it('maps the purchases field with data that begins with a number and nested objects', () => {
const userData = userDataWith({
purchased: {
ads: false,
txnCount: 0,
skin: {
eb052b: true,
'0ff591': true,
'2b43f6': true,
d7a9f7: true,
'800ed0': true,
rainbow: true,
},
plan: {
consecutive: {
count: 0,
offset: 0,
gemCapExtra: 0,
trinkets: 0,
},
},
},
});
const xml = xmlMarshaller.marshallUserData(userData);
expect(xml).to.equal(`<user>
<inbox/>
<purchased>
<ads>false</ads>
<txnCount>0</txnCount>
<skin>eb052b</skin>
<skin>0ff591</skin>
<skin>2b43f6</skin>
<skin>d7a9f7</skin>
<skin>800ed0</skin>
<skin>rainbow</skin>
<plan>
<item>
<count>0</count>
<offset>0</offset>
<gemCapExtra>0</gemCapExtra>
<trinkets>0</trinkets>
</item>
</plan>
</purchased>
</user>`);
});
});

View File

@@ -128,22 +128,6 @@ describe('cron middleware', () => {
});
});
it('runs cron if previous cron was incomplete', async () => {
user.lastCron = moment(new Date()).subtract({ days: 1 });
user.auth.timestamps.loggedin = moment(new Date()).subtract({ days: 4 });
const now = new Date();
await user.save();
await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
expect(moment(now).isSame(user.lastCron, 'day'));
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
return resolve();
});
});
});
it('updates user.auth.timestamps.loggedin and lastCron', async () => {
user.lastCron = moment(new Date()).subtract({ days: 2 });
const now = new Date();
@@ -309,33 +293,4 @@ describe('cron middleware', () => {
});
});
});
it('cron should not run more than once', async () => {
user.lastCron = moment(new Date()).subtract({ days: 2 });
await user.save();
sandbox.spy(cronLib, 'cron');
await Promise.all([new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
return resolve();
});
}), new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
return resolve();
});
}), new Promise((resolve, reject) => {
setTimeout(() => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
return resolve();
});
}, 400);
}),
]);
expect(cronLib.cron).to.be.calledOnce;
});
});

View File

@@ -4,7 +4,8 @@ import {
generateReq,
generateNext,
} from '../../../helpers/api-unit.helper';
import { ensurePermission } from '../../../../website/server/middlewares/ensureAccessRight';
import i18n from '../../../../website/common/script/i18n';
import { ensureAdmin, ensureSudo, ensureNewsPoster } from '../../../../website/server/middlewares/ensureAccessRight';
import { NotAuthorized } from '../../../../website/server/libs/errors';
import apiError from '../../../../website/server/libs/apiError';
@@ -19,20 +20,20 @@ describe('ensure access middlewares', () => {
});
context('ensure admin', () => {
it('returns not authorized when user is not in userSupport', () => {
res.locals = { user: { permissions: { userSupport: false } } };
it('returns not authorized when user is not an admin', () => {
res.locals = { user: { contributor: { admin: false } } };
ensurePermission('userSupport')(req, res, next);
ensureAdmin(req, res, next);
const calledWith = next.getCall(0).args;
expect(calledWith[0].message).to.equal(apiError('noPrivAccess'));
expect(calledWith[0].message).to.equal(i18n.t('noAdminAccess'));
expect(calledWith[0] instanceof NotAuthorized).to.equal(true);
});
it('passes when user is an userSuppor', () => {
res.locals = { user: { permissions: { userSupport: true } } };
it('passes when user is an admin', () => {
res.locals = { user: { contributor: { admin: true } } };
ensurePermission('userSupport')(req, res, next);
ensureAdmin(req, res, next);
expect(next).to.be.calledOnce;
expect(next.args[0]).to.be.empty;
@@ -41,40 +42,40 @@ describe('ensure access middlewares', () => {
context('ensure newsPoster', () => {
it('returns not authorized when user is not a newsPoster', () => {
res.locals = { user: { permissions: { news: false } } };
res.locals = { user: { contributor: { newsPoster: false } } };
ensurePermission('news')(req, res, next);
ensureNewsPoster(req, res, next);
const calledWith = next.getCall(0).args;
expect(calledWith[0].message).to.equal(apiError('noPrivAccess'));
expect(calledWith[0].message).to.equal(apiError('noNewsPosterAccess'));
expect(calledWith[0] instanceof NotAuthorized).to.equal(true);
});
it('passes when user is a newsPoster', () => {
res.locals = { user: { permissions: { news: true } } };
res.locals = { user: { contributor: { newsPoster: true } } };
ensurePermission('news')(req, res, next);
ensureNewsPoster(req, res, next);
expect(next).to.be.calledOnce;
expect(next.args[0]).to.be.empty;
});
});
context('ensure coupons', () => {
it('returns not authorized when user does not have access to coupon calls', () => {
res.locals = { user: { permissions: { coupons: false } } };
context('ensure sudo', () => {
it('returns not authorized when user is not a sudo user', () => {
res.locals = { user: { contributor: { sudo: false } } };
ensurePermission('coupons')(req, res, next);
ensureSudo(req, res, next);
const calledWith = next.getCall(0).args;
expect(calledWith[0].message).to.equal(apiError('noPrivAccess'));
expect(calledWith[0].message).to.equal(apiError('noSudoAccess'));
expect(calledWith[0] instanceof NotAuthorized).to.equal(true);
});
it('passes when user has access to coupon calls', () => {
res.locals = { user: { permissions: { coupons: true } } };
it('passes when user is a sudo user', () => {
res.locals = { user: { contributor: { sudo: true } } };
ensurePermission('coupons')(req, res, next);
ensureSudo(req, res, next);
expect(next).to.be.calledOnce;
expect(next.args[0]).to.be.empty;

View File

@@ -1029,7 +1029,7 @@ describe('Group Model', () => {
expect(toJSON.chat.length).to.equal(1);
});
it('shows messages with >= 2 flag to moderators', async () => {
it('shows messages with >= 2 flag to admins', async () => {
party.chat = [{
flagCount: 3,
info: {
@@ -1037,12 +1037,12 @@ describe('Group Model', () => {
quest: 'basilist',
},
}];
const admin = new User({ 'permissions.moderator': true });
const admin = new User({ 'contributor.admin': true });
const toJSON = await Group.toJSONCleanChat(party, admin);
expect(toJSON.chat.length).to.equal(1);
});
it('doesn\'t show flagged messages to non-moderators', async () => {
it('doesn\'t show flagged messages to non-admins', async () => {
party.chat = [{
flagCount: 3,
info: {
@@ -1359,7 +1359,6 @@ describe('Group Model', () => {
describe('#sendChat', () => {
beforeEach(() => {
sandbox.spy(User, 'update');
sandbox.spy(User, 'updateMany');
});
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' },
});

View File

@@ -1,10 +1,12 @@
import { each, findIndex } from 'lodash';
import { each, find, findIndex } from 'lodash';
import { model as Challenge } from '../../../../website/server/models/challenge';
import { model as Group } from '../../../../website/server/models/group';
import { model as User } from '../../../../website/server/models/user';
import * as Tasks from '../../../../website/server/models/task';
describe('Group Task Methods', () => {
let guild; let leader; let task;
let guild; let leader; let challenge; let
task;
const tasksToTest = {
habit: {
text: 'test habit',
@@ -29,6 +31,10 @@ describe('Group Task Methods', () => {
},
};
function findLinkedTask (updatedLeadersTask) {
return updatedLeadersTask.group.taskId === task._id;
}
beforeEach(async () => {
guild = new Group({
name: 'test party',
@@ -41,9 +47,19 @@ describe('Group Task Methods', () => {
guild.leader = leader._id;
challenge = new Challenge({
name: 'Test Challenge',
shortName: 'Test',
leader: leader._id,
group: guild._id,
});
leader.challenges = [challenge._id];
await Promise.all([
guild.save(),
leader.save(),
challenge.save(),
]);
});
@@ -62,15 +78,7 @@ describe('Group Task Methods', () => {
});
it('syncs an assigned task to a user', async () => {
await guild.syncTask(task, [leader], leader);
const updatedTask = await Tasks.Task.findOne({ _id: task._id });
expect(updatedTask.group.assignedUsers).to.contain(leader._id);
expect(updatedTask.group.assignedUsersDetail[leader._id]).to.exist;
});
it('creates tags for a user when task is synced', async () => {
await guild.syncTask(task, [leader], leader);
await guild.syncTask(task, leader);
const updatedLeader = await User.findOne({ _id: leader._id });
const tagIndex = findIndex(updatedLeader.tags, { id: guild._id });
@@ -80,6 +88,197 @@ describe('Group Task Methods', () => {
expect(newTag.name).to.equal(guild.name);
expect(newTag.group).to.equal(guild._id);
});
it('create tags for a user when task is synced', async () => {
await guild.syncTask(task, leader);
const updatedLeader = await User.findOne({ _id: leader._id });
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
expect(task.group.assignedUsers).to.contain(leader._id);
expect(syncedTask).to.exist;
});
it('syncs updated info for assigned task to a user', async () => {
await guild.syncTask(task, leader);
const updatedTaskName = 'Update Task name';
task.text = updatedTaskName;
await guild.syncTask(task, leader);
const updatedLeader = await User.findOne({ _id: leader._id });
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
expect(task.group.assignedUsers).to.contain(leader._id);
expect(syncedTask).to.exist;
expect(syncedTask.text).to.equal(task.text);
});
it('syncs checklist items to an assigned user', async () => {
await guild.syncTask(task, leader);
const updatedLeader = await User.findOne({ _id: leader._id });
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
if (task.type !== 'daily' && task.type !== 'todo') return;
expect(syncedTask.checklist.length).to.equal(task.checklist.length);
expect(syncedTask.checklist[0].text).to.equal(task.checklist[0].text);
});
describe('syncs updated info', async () => {
let newMember;
beforeEach(async () => {
newMember = new User({
guilds: [guild._id],
});
await newMember.save();
await guild.syncTask(task, leader);
await guild.syncTask(task, newMember);
});
it('syncs updated info for assigned task to all users', async () => {
const updatedTaskName = 'Update Task name';
task.text = updatedTaskName;
task.group.approval.required = true;
await guild.updateTask(task);
const updatedLeader = await User.findOne({ _id: leader._id });
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
const updatedMember = await User.findOne({ _id: newMember._id });
const updatedMemberTasks = await Tasks.Task.find({ _id: { $in: updatedMember.tasksOrder[`${taskType}s`] } });
const syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
expect(task.group.assignedUsers).to.contain(leader._id);
expect(syncedTask).to.exist;
expect(syncedTask.text).to.equal(task.text);
expect(syncedTask.group.approval.required).to.equal(true);
expect(task.group.assignedUsers).to.contain(newMember._id);
expect(syncedMemberTask).to.exist;
expect(syncedMemberTask.text).to.equal(task.text);
expect(syncedMemberTask.group.approval.required).to.equal(true);
});
it('syncs a new checklist item to all assigned users', async () => {
if (task.type !== 'daily' && task.type !== 'todo') return;
const newCheckListItem = {
text: 'Checklist Item 1',
completed: false,
};
task.checklist.push(newCheckListItem);
await guild.updateTask(task, { newCheckListItem });
const updatedLeader = await User.findOne({ _id: leader._id });
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
const updatedMember = await User.findOne({ _id: newMember._id });
const updatedMemberTasks = await Tasks.Task.find({ _id: { $in: updatedMember.tasksOrder[`${taskType}s`] } });
const syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
expect(syncedTask.checklist.length).to.equal(task.checklist.length);
expect(syncedTask.checklist[1].text).to.equal(task.checklist[1].text);
expect(syncedMemberTask.checklist.length).to.equal(task.checklist.length);
expect(syncedMemberTask.checklist[1].text).to.equal(task.checklist[1].text);
});
it('syncs updated info for checklist in assigned task to all users when flag is passed', async () => {
if (task.type !== 'daily' && task.type !== 'todo') return;
const updateCheckListText = 'Updated checklist item';
if (task.checklist) {
task.checklist[0].text = updateCheckListText;
}
await guild.updateTask(task, { updateCheckListItems: [task.checklist[0]] });
const updatedLeader = await User.findOne({ _id: leader._id });
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
const updatedMember = await User.findOne({ _id: newMember._id });
const updatedMemberTasks = await Tasks.Task.find({ _id: { $in: updatedMember.tasksOrder[`${taskType}s`] } });
const syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
expect(syncedTask.checklist.length).to.equal(task.checklist.length);
expect(syncedTask.checklist[0].text).to.equal(updateCheckListText);
expect(syncedMemberTask.checklist.length).to.equal(task.checklist.length);
expect(syncedMemberTask.checklist[0].text).to.equal(updateCheckListText);
});
it('removes a checklist item in assigned task to all users when flag is passed with checklist id', async () => {
if (task.type !== 'daily' && task.type !== 'todo') return;
await guild.updateTask(task, { removedCheckListItemId: task.checklist[0].id });
const updatedLeader = await User.findOne({ _id: leader._id });
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
const updatedMember = await User.findOne({ _id: newMember._id });
const updatedMemberTasks = await Tasks.Task.find({ _id: { $in: updatedMember.tasksOrder[`${taskType}s`] } });
const syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
expect(syncedTask.checklist.length).to.equal(0);
expect(syncedMemberTask.checklist.length).to.equal(0);
});
});
it('removes assigned tasks when master task is deleted', async () => {
await guild.syncTask(task, leader);
await guild.removeTask(task);
const updatedLeader = await User.findOne({ _id: leader._id });
const updatedLeadersTasks = await Tasks.Task.find({ userId: leader._id, type: taskType });
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
expect(updatedLeader.tasksOrder[`${taskType}s`]).to.not.include(task._id);
expect(syncedTask).to.not.exist;
});
it('unlinks and deletes group tasks for a user when remove-all is specified', async () => {
await guild.syncTask(task, leader);
await guild.unlinkTask(task, leader, 'remove-all');
const updatedLeader = await User.findOne({ _id: leader._id });
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
expect(task.group.assignedUsers).to.not.contain(leader._id);
expect(syncedTask).to.not.exist;
});
it('unlinks and keeps group tasks for a user when keep-all is specified', async () => {
await guild.syncTask(task, leader);
let updatedLeader = await User.findOne({ _id: leader._id });
let updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
await guild.unlinkTask(task, leader, 'keep-all');
updatedLeader = await User.findOne({ _id: leader._id });
updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
const updatedSyncedTask = find(
updatedLeadersTasks,
updatedLeadersTask => updatedLeadersTask._id === syncedTask._id,
);
expect(task.group.assignedUsers).to.not.contain(leader._id);
expect(updatedSyncedTask).to.exist;
expect(updatedSyncedTask.group._id).to.be.undefined;
});
});
});
});

View File

@@ -246,23 +246,13 @@ describe('Task Model', () => {
expect(foundTasks[0].text).to.eql(taskWithAlias.text);
});
it('scopes alias lookup to user when querying aliases only', async () => {
it('scopes alias lookup to user', async () => {
await Tasks.Task.findMultipleByIdOrAlias([taskWithAlias.alias], user._id);
expect(Tasks.Task.find).to.be.calledOnce;
expect(Tasks.Task.find).to.be.calledWithMatch({
alias: { $in: [taskWithAlias.alias] },
userId: user._id,
});
});
it('scopes alias lookup to user when querying aliases and IDs', async () => {
await Tasks.Task.findMultipleByIdOrAlias([taskWithAlias.alias, secondTask._id], user._id);
expect(Tasks.Task.find).to.be.calledOnce;
expect(Tasks.Task.find).to.be.calledWithMatch({
$or: [
{ _id: { $in: [secondTask._id] } },
{ _id: { $in: [] } },
{ alias: { $in: [taskWithAlias.alias] } },
],
userId: user._id,
@@ -280,7 +270,10 @@ describe('Task Model', () => {
expect(Tasks.Task.find).to.be.calledOnce;
expect(Tasks.Task.find).to.be.calledWithMatch({
alias: { $in: [taskWithAlias.alias] },
$or: [
{ _id: { $in: [] } },
{ alias: { $in: [taskWithAlias.alias] } },
],
userId: user._id,
foo: 'bar',
});

View File

@@ -634,7 +634,7 @@ describe('User Model', () => {
user = await user.save();
// verify that it's been awarded
expect(user.achievements.beastMaster).to.equal(true);
expect(user.notifications.find(notification => notification.type === 'ACHIEVEMENT_STABLE')).to.exist;
expect(user.notifications.find(notification => notification.type === 'ACHIEVEMENT_BEAST_MASTER')).to.exist;
// reset the user
user.achievements.beastMasterCount = 0;
@@ -683,9 +683,9 @@ describe('User Model', () => {
user = await user.save();
// verify that it's been awarded
expect(user.notifications.find(
notification => notification.type === 'ACHIEVEMENT_STABLE',
)).to.exist;
expect(user.notifications.find(notification => notification.type === 'ACHIEVEMENT_BEAST_MASTER')).to.exist;
expect(user.notifications.find(notification => notification.type === 'ACHIEVEMENT_MOUNT_MASTER')).to.exist;
expect(user.notifications.find(notification => notification.type === 'ACHIEVEMENT_TRIAD_BINGO')).to.exist;
});
context('manage unallocated stats points notifications', () => {
@@ -811,16 +811,6 @@ describe('User Model', () => {
expect(daysMissed).to.eql(5);
});
it('correctly handles a cron that did not complete', () => {
const now = moment();
user.lastCron = moment(now).subtract(2, 'days');
user.auth.timestamps.loggedIn = moment(now).subtract(5, 'days');
const { daysMissed } = user.daysUserHasMissed(now);
expect(daysMissed).to.eql(5);
});
it('uses timezone from preferences to calculate days missed', () => {
const now = moment('2017-07-08 01:00:00Z');
user.lastCron = moment('2017-07-04 13:00:00Z');
@@ -877,7 +867,7 @@ describe('User Model', () => {
expect(user.isNewsPoster()).to.equal(false);
user.permissions = { news: true };
user.contributor.newsPoster = true;
expect(user.isNewsPoster()).to.equal(true);
});

View File

@@ -202,7 +202,7 @@ describe('GET challenges/groups/:groupId', () => {
publicGuild = group;
await user.update({
'permissions.challengeAdmin': true,
'contributor.admin': true,
});
officialChallenge = await generateChallenge(user, group, {

View File

@@ -231,7 +231,7 @@ describe('GET challenges/user', () => {
publicGuild = group;
await user.update({
'permissions.challengeAdmin': true,
'contributor.admin': true,
});
officialChallenge = await generateChallenge(user, group, {

View File

@@ -4,7 +4,6 @@ import {
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-integration/v3';
import { MAX_SUMMARY_SIZE_FOR_CHALLENGES } from '../../../../../website/common/script/constants';
describe('POST /challenges', () => {
it('returns error when group is empty', async () => {
@@ -61,22 +60,6 @@ describe('POST /challenges', () => {
});
});
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({
members: 1,
});
await expect(user.post('/challenges', {
group: group._id,
summary,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
context('Creating a challenge for a valid group', () => {
let groupLeader;
let group;
@@ -220,8 +203,8 @@ describe('POST /challenges', () => {
it('sets challenge as official if created by admin and official flag is set', async () => {
await groupLeader.update({
permissions: {
challengeAdmin: true,
contributor: {
admin: true,
},
});

View File

@@ -4,7 +4,6 @@ import {
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-integration/v3';
import { MAX_SUMMARY_SIZE_FOR_CHALLENGES } from '../../../../../website/common/script/constants';
describe('PUT /challenges/:challengeId', () => {
let privateGuild; let user; let nonMember; let challenge; let
@@ -92,15 +91,4 @@ describe('PUT /challenges/:challengeId', () => {
expect(res.name).to.equal('New Challenge Name');
expect(res.description).to.equal('New challenge description.');
});
it('return error when challenge summary is greater than MAX_SUMMARY_SIZE_FOR_CHALLENGES characters', async () => {
const summary = 'A'.repeat(MAX_SUMMARY_SIZE_FOR_CHALLENGES + 1);
await expect(user.put(`/challenges/${challenge._id}`, {
summary,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
});

View File

@@ -15,10 +15,6 @@ describe('DELETE /groups/:groupId/chat/:chatId', () => {
type: 'guild',
privacy: 'public',
},
leaderDetails: {
'auth.timestamps.created': new Date('2022-01-01'),
balance: 10,
},
});
groupWithChat = group;
@@ -26,7 +22,7 @@ describe('DELETE /groups/:groupId/chat/:chatId', () => {
message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: 'Some message' });
message = message.message;
userThatDidNotCreateChat = await generateUser();
admin = await generateUser({ 'permissions.moderator': true });
admin = await generateUser({ 'contributor.admin': true });
});
context('Chat errors', () => {

View File

@@ -17,7 +17,7 @@ describe('POST /chat/:chatId/flag', () => {
beforeEach(async () => {
user = await generateUser({ balance: 1, 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate() });
admin = await generateUser({ balance: 1, 'permissions.moderator': true });
admin = await generateUser({ balance: 1, 'contributor.admin': 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());
@@ -117,9 +117,7 @@ describe('POST /chat/:chatId/flag', () => {
});
it('Flags a chat when the author\'s account was deleted', async () => {
const deletedUser = await generateUser({
'auth.timestamps.created': new Date('2022-01-01'),
});
const deletedUser = await generateUser();
const { message } = await deletedUser.post(`/groups/${group._id}/chat`, { message: TEST_MESSAGE });
await deletedUser.del('/user', {
password: 'password',

View File

@@ -18,16 +18,11 @@ describe('POST /chat/:chatId/like', () => {
privacy: 'public',
},
members: 1,
leaderDetails: {
'auth.timestamps.created': new Date('2022-01-01'),
balance: 10,
},
});
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 () => {

View File

@@ -38,15 +38,10 @@ describe('POST /chat', () => {
members: 2,
});
user = groupLeader;
await user.update({
'contributor.level': SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
'auth.timestamps.created': new Date('2022-01-01'),
}); // prevent tests accidentally throwing messageGroupChatSpam
await user.update({ 'contributor.level': SPAM_MIN_EXEMPT_CONTRIB_LEVEL }); // prevent tests accidentally throwing messageGroupChatSpam
groupWithChat = group;
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 () => {
@@ -109,10 +104,7 @@ describe('POST /chat', () => {
});
const privateGuildMemberWithChatsRevoked = members[0];
await privateGuildMemberWithChatsRevoked.update({
'flags.chatRevoked': true,
'auth.timestamps.created': new Date('2022-01-01'),
});
await privateGuildMemberWithChatsRevoked.update({ 'flags.chatRevoked': true });
const message = await privateGuildMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage });
@@ -130,10 +122,7 @@ describe('POST /chat', () => {
});
const privatePartyMemberWithChatsRevoked = members[0];
await privatePartyMemberWithChatsRevoked.update({
'flags.chatRevoked': true,
'auth.timestamps.created': new Date('2022-01-01'),
});
await privatePartyMemberWithChatsRevoked.update({ 'flags.chatRevoked': true });
const message = await privatePartyMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage });
@@ -194,10 +183,7 @@ describe('POST /chat', () => {
});
const userWithChatShadowMuted = members[0];
await userWithChatShadowMuted.update({
'flags.chatShadowMuted': true,
'auth.timestamps.created': new Date('2022-01-01'),
});
await userWithChatShadowMuted.update({ 'flags.chatShadowMuted': true });
const message = await userWithChatShadowMuted.post(`/groups/${group._id}/chat`, { message: testMessage });
@@ -216,10 +202,7 @@ describe('POST /chat', () => {
});
const userWithChatShadowMuted = members[0];
await userWithChatShadowMuted.update({
'flags.chatShadowMuted': true,
'auth.timestamps.created': new Date('2022-01-01'),
});
await userWithChatShadowMuted.update({ 'flags.chatShadowMuted': true });
const message = await userWithChatShadowMuted.post(`/groups/${group._id}/chat`, { message: testMessage });
@@ -329,7 +312,6 @@ describe('POST /chat', () => {
},
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 });
@@ -348,7 +330,6 @@ describe('POST /chat', () => {
// 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 });
@@ -364,7 +345,6 @@ describe('POST /chat', () => {
},
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 });
@@ -422,7 +402,7 @@ describe('POST /chat', () => {
});
});
it('allows slurs in private groups', async () => {
it('does not allow slurs in private groups', async () => {
const { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'Party',
@@ -431,11 +411,43 @@ describe('POST /chat', () => {
},
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: testSlurMessage });
await expect(members[0].post(`/groups/${group._id}/chat`, { message: testSlurMessage })).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedSlurUsed'),
});
expect(message.message.id).to.exist;
// Email sent to mods
await sleep(0.5);
expect(email.sendTxn).to.be.calledThrice;
expect(email.sendTxn.args[2][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: `${members[0].profile.name} (${members[0].id}) tried to post a slur`,
attachments: [{
fallback: 'Slur Message',
color: 'danger',
author_name: `@${members[0].auth.local.username} ${members[0].profile.name} (${members[0].auth.local.email}; ${members[0]._id})`,
title: 'Slur in Party - (private party)',
title_link: undefined,
text: testSlurMessage,
mrkdwn_in: [
'text',
],
}],
});
/* eslint-enable camelcase */
// Chat privileges are revoked
await expect(members[0].post(`/groups/${groupWithChat._id}/chat`, { message: testMessage })).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('chatPrivilegesRevoked'),
});
});
it('errors when slur is typed in mixed case', async () => {
@@ -451,16 +463,6 @@ describe('POST /chat', () => {
});
});
it('errors when user account is too young', async () => {
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'),
});
});
it('creates a chat', async () => {
const newMessage = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
const groupMessages = await user.get(`/groups/${groupWithChat._id}/chat`);
@@ -523,7 +525,6 @@ describe('POST /chat', () => {
'items.currentMount': mount,
'items.currentPet': pet,
'preferences.style': style,
'auth.timestamps.created': new Date('2022-01-01'),
});
await userWithStyle.sync();
@@ -541,35 +542,6 @@ describe('POST /chat', () => {
.to.eql(userWithStyle.preferences.background);
});
it('creates equipped to user styles', async () => {
const userWithStyle = await generateUser({
'preferences.costume': false,
'auth.timestamps.created': new Date('2022-01-01'),
});
await userWithStyle.sync();
const message = await userWithStyle.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
expect(message.message.id).to.exist;
expect(message.message.userStyles.items.gear.equipped)
.to.eql(userWithStyle.items.gear.equipped);
expect(message.message.userStyles.items.gear.costume).to.not.exist;
});
it('creates costume to user styles', async () => {
const userWithStyle = await generateUser({
'preferences.costume': true,
'auth.timestamps.created': new Date('2022-01-01'),
});
await userWithStyle.sync();
const message = await userWithStyle.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
expect(message.message.id).to.exist;
expect(message.message.userStyles.items.gear.costume).to.eql(userWithStyle.items.gear.costume);
expect(message.message.userStyles.items.gear.equipped).to.not.exist;
});
it('adds backer info to chat', async () => {
const backerInfo = {
npc: 'Town Crier',
@@ -578,7 +550,6 @@ describe('POST /chat', () => {
};
const backer = await generateUser({
backer: backerInfo,
'auth.timestamps.created': new Date('2022-01-01'),
});
const message = await backer.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
@@ -649,9 +620,6 @@ describe('POST /chat', () => {
privacy: 'private',
},
members: 1,
leaderDetails: {
'auth.timestamps.created': new Date('2022-01-01'),
},
});
const message = await groupLeader.post(`/groups/${group._id}/chat`, { message: testMessage });

View File

@@ -15,10 +15,6 @@ describe('POST /groups/:id/chat/seen', () => {
privacy: 'public',
},
members: 1,
leaderDetails: {
'auth.timestamps.created': new Date('2022-01-01'),
balance: 10,
},
});
guild = group;
@@ -55,9 +51,6 @@ describe('POST /groups/:id/chat/seen', () => {
privacy: 'private',
},
members: 1,
leaderDetails: {
'auth.timestamps.created': new Date('2022-01-01'),
},
});
party = group;

View File

@@ -18,16 +18,12 @@ describe('POST /groups/:id/chat/:id/clearflags', () => {
type: 'guild',
privacy: 'public',
},
leaderDetails: {
'auth.timestamps.created': new Date('2022-01-01'),
balance: 10,
},
});
groupWithChat = group;
author = groupLeader;
nonAdmin = await generateUser({ 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate() });
admin = await generateUser({ 'permissions.moderator': true });
admin = await generateUser({ 'contributor.admin': true });
message = await author.post(`/groups/${groupWithChat._id}/chat`, { message: 'Some message' });
message = message.message;
@@ -66,27 +62,21 @@ describe('POST /groups/:id/chat/:id/clearflags', () => {
type: 'party',
privacy: 'private',
},
members: 2,
members: 1,
});
await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') });
let privateMessage = await members[0].post(`/groups/${group._id}/chat`, { message: 'Some message' });
privateMessage = privateMessage.message;
await admin.post(`/groups/${group._id}/chat/${privateMessage.id}/flag`);
// first test that the flag was actually successful
// author always sees own message; flag count is hidden from non-admins
let messages = await members[0].get(`/groups/${group._id}/chat`);
expect(messages[0].flagCount).to.eql(0);
messages = await members[1].get(`/groups/${group._id}/chat`);
expect(messages.length).to.eql(0);
expect(messages[0].flagCount).to.eql(5);
// admin cannot directly request private group chat, but after unflag,
// message should be revealed again and still have flagCount of 0
await admin.post(`/groups/${group._id}/chat/${privateMessage.id}/clearflags`);
messages = await members[1].get(`/groups/${group._id}/chat`);
expect(messages.length).to.eql(1);
messages = await members[0].get(`/groups/${group._id}/chat`);
expect(messages[0].flagCount).to.eql(0);
});

View File

@@ -14,18 +14,18 @@ describe('GET /coupons/', () => {
user = await generateUser();
});
it('returns an error if user has no coupons permission', async () => {
it('returns an error if user has no sudo permission', async () => {
await user.get('/user'); // needed so the request after this will authenticate with the correct cookie session
await expect(user.get('/coupons')).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: apiError('noPrivAccess'),
message: apiError('noSudoAccess'),
});
});
it('should return the coupons in CSV format ordered by creation date', async () => {
await user.update({
'permissions.coupons': true,
'contributor.sudo': true,
});
const coupons = await user.post('/coupons/generate/wondercon?count=11');

View File

@@ -15,7 +15,7 @@ describe('POST /coupons/enter/:code', () => {
beforeEach(async () => {
user = await generateUser();
sudoUser = await generateUser({
'permissions.coupons': true,
'contributor.sudo': true,
});
});

View File

@@ -14,19 +14,19 @@ describe('POST /coupons/generate/:event', () => {
beforeEach(async () => {
user = await generateUser({
'permissions.coupons': true,
'contributor.sudo': true,
});
});
it('returns an error if user has no coupons permission', async () => {
it('returns an error if user has no sudo permission', async () => {
await user.update({
'permissions.coupons': false,
'contributor.sudo': false,
});
await expect(user.post('/coupons/generate/aaa')).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: apiError('noPrivAccess'),
message: apiError('noSudoAccess'),
});
});
@@ -48,7 +48,7 @@ describe('POST /coupons/generate/:event', () => {
it('should generate coupons', async () => {
await user.update({
'permissions.coupons': true,
'contributor.sudo': true,
});
const coupons = await user.post('/coupons/generate/wondercon?count=2');

View File

@@ -21,7 +21,7 @@ describe('POST /coupons/validate/:code', () => {
it('returns true if coupon code is valid', async () => {
const sudoUser = await generateUser({
'permissions.coupons': true,
'contributor.sudo': true,
});
const [coupon] = await sudoUser.post('/coupons/generate/wondercon?count=1');

View File

@@ -3,7 +3,7 @@ import {
generateUser,
} from '../../../../helpers/api-integration/v3';
describe('POST /debug/make-admin', () => {
describe('POST /debug/make-admin (pended for v3 prod testing)', () => {
let user;
before(async () => {
@@ -14,12 +14,12 @@ describe('POST /debug/make-admin', () => {
nconf.set('IS_PROD', false);
});
it('makes user an admin', async () => {
it('makes user an admine', async () => {
await user.post('/debug/make-admin');
await user.sync();
expect(user.permissions.fullAccess).to.eql(true);
expect(user.contributor.admin).to.eql(true);
});
it('returns error when not in production mode', async () => {

View File

@@ -219,19 +219,11 @@ describe('GET /groups', () => {
it('returns 30 guilds per page ordered by number of members', async () => {
await user.update({ balance: 9000 });
const delay = () => new Promise(resolve => setTimeout(resolve, 40));
const promises = [];
for (let i = 0; i < 60; i += 1) {
promises.push(generateGroup(user, {
name: `public guild ${i} - is member`,
type: 'guild',
privacy: 'public',
}));
await delay(); // eslint-disable-line no-await-in-loop
}
const groups = await Promise.all(promises);
const groups = await Promise.all(_.times(60, i => generateGroup(user, {
name: `public guild ${i} - is member`,
type: 'guild',
privacy: 'public',
})));
// update group number 32 and not the first to make sure sorting works
await groups[32].update({ name: 'guild with most members', memberCount: 199 });

View File

@@ -315,7 +315,7 @@ describe('GET /groups/:id', () => {
beforeEach(async () => {
admin = await generateUser({
'permissions.moderator': true,
'contributor.admin': true,
});
});

View File

@@ -1,3 +1,4 @@
import { find } from 'lodash';
import {
createAndPopulateGroup,
translate as t,
@@ -6,19 +7,22 @@ import {
describe('POST /group/:groupId/remove-manager', () => {
let leader; let nonLeader; let
groupToUpdate;
const groupName = 'Test Private Guild';
const groupName = 'Test Public Guild';
const groupType = 'guild';
let nonManager;
function findAssignedTask (memberTask) {
return memberTask.group.id === groupToUpdate._id;
}
beforeEach(async () => {
const { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: groupName,
type: groupType,
privacy: 'private',
privacy: 'public',
},
members: 2,
upgradeToGroupPlan: true,
});
groupToUpdate = group;
@@ -58,4 +62,28 @@ describe('POST /group/:groupId/remove-manager', () => {
expect(updatedGroup.managers[nonLeader._id]).to.not.exist;
});
it('removes group approval notifications from a manager that is removed', async () => {
await leader.post(`/groups/${groupToUpdate._id}/add-manager`, {
managerId: nonLeader._id,
});
const task = await leader.post(`/tasks/group/${groupToUpdate._id}`, {
text: 'test todo',
type: 'todo',
requiresApproval: true,
});
await nonLeader.post(`/tasks/${task._id}/assign/${nonManager._id}`);
const memberTasks = await nonManager.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await nonManager.post(`/tasks/${syncedTask._id}/score/up`);
const updatedGroup = await leader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
managerId: nonLeader._id,
});
await nonLeader.sync();
expect(nonLeader.notifications.length).to.equal(0);
expect(updatedGroup.managers[nonLeader._id]).to.not.exist;
});
});

View File

@@ -2,8 +2,6 @@ import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
import { model as Group } from '../../../../../website/server/models/group';
import { MAX_SUMMARY_SIZE_FOR_GUILDS } from '../../../../../website/common/script/constants';
describe('POST /group', () => {
let user;
@@ -72,20 +70,6 @@ describe('POST /group', () => {
expect(updatedGroup.summary).to.eql(summary);
});
it('returns error when summary is longer than MAX_SUMMARY_SIZE_FOR_GUILDS characters', async () => {
const name = 'Test Group';
const summary = 'A'.repeat(MAX_SUMMARY_SIZE_FOR_GUILDS + 1);
await expect(user.post('/groups', {
name,
type: 'guild',
summary,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
});
context('Guilds', () => {
@@ -219,23 +203,6 @@ describe('POST /group', () => {
expect(updatedUser.balance).to.eql(user.balance - 1);
});
it('does not deduct the gems from user when guild creation fails', async () => {
const stub = sinon.stub(Group.prototype, 'save').rejects();
const promise = user.post('/groups', {
name: groupName,
type: groupType,
privacy: groupPrivacy,
});
await expect(promise).to.eventually.be.rejected;
const updatedUser = await user.get('/user');
expect(updatedUser.balance).to.eql(user.balance);
stub.restore();
});
});
});

View File

@@ -37,7 +37,6 @@ describe('POST /groups/:groupId/leave', () => {
leader = groupLeader;
member = members[0]; // eslint-disable-line prefer-destructuring
memberCount = group.memberCount;
await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') });
});
it('prevents non members from leaving', async () => {
@@ -153,10 +152,6 @@ describe('POST /groups/:groupId/leave', () => {
type: 'guild',
},
invites: 1,
leaderDetails: {
'auth.timestamps.created': new Date('2022-01-01'),
balance: 10,
},
});
privateGuild = group;

View File

@@ -32,7 +32,7 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
invitedUser = invitees[0]; // eslint-disable-line prefer-destructuring
member = members[0]; // eslint-disable-line prefer-destructuring
member2 = members[1]; // eslint-disable-line prefer-destructuring
adminUser = await generateUser({ 'permissions.moderator': true });
adminUser = await generateUser({ 'contributor.admin': true });
});
context('All Groups', () => {
@@ -153,7 +153,6 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
},
invites: 1,
members: 2,
leaderDetails: { 'auth.timestamps.created': new Date('2022-01-01') },
});
party = group;

View File

@@ -48,19 +48,6 @@ describe('Post /groups/:groupId/invite', () => {
});
});
it('returns error when recipient has blocked the senders', async () => {
const inviterNoBlocks = await inviter.update({ 'inbox.blocks': [] });
const userWithBlockedInviter = await generateUser({ 'inbox.blocks': [inviter._id] });
await expect(inviterNoBlocks.post(`/groups/${group._id}/invite`, {
usernames: [userWithBlockedInviter.auth.local.lowerCaseUsername],
}))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('notAuthorizedToSendMessageToThisUser'),
});
});
it('invites a user to a group by username', async () => {
const userToInvite = await generateUser();

View File

@@ -3,7 +3,6 @@ import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
import { MAX_SUMMARY_SIZE_FOR_GUILDS } from '../../../../../website/common/script/constants';
describe('PUT /group', () => {
let leader; let nonLeader; let groupToUpdate; let
@@ -11,12 +10,6 @@ describe('PUT /group', () => {
const groupName = 'Test Public Guild';
const groupType = 'guild';
const groupUpdatedName = 'Test Public Guild Updated';
const groupCategories = [
{
slug: 'initialCat',
name: 'Initial Category',
},
];
beforeEach(async () => {
const { group, groupLeader, members } = await createAndPopulateGroup({
@@ -24,11 +17,10 @@ describe('PUT /group', () => {
name: groupName,
type: groupType,
privacy: 'public',
categories: groupCategories,
},
members: 1,
});
adminUser = await generateUser({ 'permissions.moderator': true });
adminUser = await generateUser({ 'contributor.admin': true });
groupToUpdate = group;
leader = groupLeader;
nonLeader = members[0]; // eslint-disable-line prefer-destructuring
@@ -68,35 +60,6 @@ describe('PUT /group', () => {
expect(updatedGroup.categories[0].name).to.eql(categories[0].name);
});
it('removes the initial group category', async () => {
const categories = [];
const updatedGroup = await leader.put(`/groups/${groupToUpdate._id}`, {
categories,
});
expect(updatedGroup.categories.length).to.equal(0);
});
it('removes duplicate group categories', async () => {
const categories = [
{
slug: 'newCat',
name: 'New Category',
},
{
slug: 'newCat',
name: 'New Category',
},
];
const updatedGroup = await leader.put(`/groups/${groupToUpdate._id}`, {
categories,
});
expect(updatedGroup.categories.length).to.equal(1);
});
it('allows an admin to update a guild', async () => {
const updatedGroup = await adminUser.put(`/groups/${groupToUpdate._id}`, {
name: groupUpdatedName,
@@ -141,11 +104,11 @@ describe('PUT /group', () => {
// Update the bannedWordsAllowed property for the group
const response = await groupLeader.put(`/groups/${group._id}`, updateGroupDetails);
expect(groupLeader.permissions.fullAccess).to.eql(true);
expect(groupLeader.contributor.admin).to.eql(true);
expect(response.bannedWordsAllowed).to.eql(true);
});
it('does not allow for a non-moderator to update the bannedWordsAllow property for an existing guild', async () => {
it('does not allow for a non-admin to update the bannedWordsAllow property for an existing guild', async () => {
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'public guild',
@@ -165,17 +128,7 @@ describe('PUT /group', () => {
// Update the bannedWordsAllowed property for the group
const response = await groupLeader.put(`/groups/${group._id}`, updateGroupDetails);
expect(groupLeader.contributor.admin).to.eql(undefined);
expect(response.bannedWordsAllowed).to.eql(undefined);
});
it('returns error when summary is longer than MAX_SUMMARY_SIZE_FOR_GUILDS characters', async () => {
const summary = 'A'.repeat(MAX_SUMMARY_SIZE_FOR_GUILDS + 1);
await expect(leader.put(`/groups/${groupToUpdate._id}`, {
summary,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
});

View File

@@ -7,14 +7,9 @@ import {
describe('GET /heroes/:heroId', () => {
let user;
const heroFields = [
'_id', 'id', 'auth', 'balance', 'contributor', 'flags', 'items',
'lastCron', 'party', 'preferences', 'profile', 'purchased', 'secret',
];
before(async () => {
user = await generateUser({
permissions: { userSupport: true },
contributor: { admin: true },
});
});
@@ -24,7 +19,7 @@ describe('GET /heroes/:heroId', () => {
await expect(nonAdmin.get(`/hall/heroes/${user._id}`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('noPrivAccess'),
message: t('noAdminAccess'),
});
});
@@ -54,7 +49,10 @@ describe('GET /heroes/:heroId', () => {
});
const heroRes = await user.get(`/hall/heroes/${hero._id}`);
expect(heroRes).to.have.all.keys(heroFields); // works as: object has all and only these keys
expect(heroRes).to.have.all.keys([ // works as: object has all and only these keys
'_id', 'id', 'balance', 'profile', 'purchased',
'contributor', 'auth', 'items', 'secret',
]);
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
expect(heroRes.profile).to.have.all.keys(['name']);
expect(heroRes.secret.text).to.be.eq('Super Hero');
@@ -66,7 +64,10 @@ describe('GET /heroes/:heroId', () => {
});
const heroRes = await user.get(`/hall/heroes/${hero.auth.local.username}`);
expect(heroRes).to.have.all.keys(heroFields);
expect(heroRes).to.have.all.keys([ // works as: object has all and only these keys
'_id', 'id', 'balance', 'profile', 'purchased',
'contributor', 'auth', 'items', 'secret',
]);
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
expect(heroRes.profile).to.have.all.keys(['name']);
});

View File

@@ -1,61 +0,0 @@
import { v4 as generateUUID } from 'uuid';
import {
generateUser,
generateGroup,
translate as t,
} from '../../../../helpers/api-integration/v3';
import apiError from '../../../../../website/server/libs/apiError';
describe('GET /heroes/party/:groupId', () => {
let user; // admin user
before(async () => {
user = await generateUser({
'permissions.userSupport': true,
});
});
it('requires the caller to be an admin', async () => {
const nonAdmin = await generateUser();
const party = await generateGroup(nonAdmin, { type: 'party', privacy: 'private' });
await expect(nonAdmin.get(`/hall/heroes/party/${party._id}`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: apiError('noPrivAccess'),
});
});
it('validates req.params.groupId', async () => {
await expect(user.get('/hall/heroes/party/invalidUUID')).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('handles non-existing party', async () => {
const dummyId = generateUUID();
await expect(user.get(`/hall/heroes/party/${dummyId}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: apiError('groupWithIDNotFound', { groupId: dummyId }),
});
});
it('returns only necessary party data given group id', async () => {
const nonAdmin = await generateUser();
const party = await generateGroup(nonAdmin, { type: 'party', privacy: 'private' });
const partyRes = await user.get(`/hall/heroes/party/${party._id}`);
expect(partyRes).to.have.all.keys([ // works as: object has all and only these keys
'_id', 'id', 'balance', 'challengeCount', 'leader', 'leaderOnly', 'memberCount',
'purchased', 'quest', 'summary',
]);
expect(partyRes.summary).to.eq(' ');
// NB: 'summary' is NOT a field that the API route retrieves!
// It must not be retrieved for privacy reasons.
// However the group model automatically adds a summary for reasons given here:
// https://github.com/HabitRPG/habitica/blob/8da36bf27c62ba0397a6af260c20d35a17f3d911/website/server/models/group.js#L161-L170
});
});

View File

@@ -1,5 +1,4 @@
import { v4 as generateUUID } from 'uuid';
import { model as User } from '../../../../../website/server/models/user';
import {
generateUser,
translate as t,
@@ -9,12 +8,15 @@ describe('PUT /heroes/:heroId', () => {
let user;
const heroFields = [
'_id', 'auth', 'balance', 'contributor', 'flags', 'items', 'lastCron',
'party', 'preferences', 'profile', 'purchased', 'secret', 'permissions',
'_id', 'balance', 'profile', 'purchased',
'contributor', 'auth', 'items', 'flags',
'secret',
];
before(async () => {
user = await generateUser({ 'permissions.userSupport': true });
user = await generateUser({
contributor: { admin: true },
});
});
it('requires the caller to be an admin', async () => {
@@ -23,7 +25,7 @@ describe('PUT /heroes/:heroId', () => {
await expect(nonAdmin.put(`/hall/heroes/${user._id}`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('noPrivAccess'),
message: t('noAdminAccess'),
});
});
@@ -55,7 +57,8 @@ describe('PUT /heroes/:heroId', () => {
});
// test response
expect(heroRes).to.have.all.keys(heroFields); // works as: object has all and only these keys
// works as: object has all and only these keys
expect(heroRes).to.have.all.keys(heroFields);
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
expect(heroRes.profile).to.have.all.keys(['name']);
@@ -131,6 +134,7 @@ describe('PUT /heroes/:heroId', () => {
});
// test response
// works as: object has all and only these keys
expect(heroRes).to.have.all.keys(heroFields);
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
expect(heroRes.profile).to.have.all.keys(['name']);
@@ -155,6 +159,7 @@ describe('PUT /heroes/:heroId', () => {
});
// test response
// works as: object has all and only these keys
expect(heroRes).to.have.all.keys(heroFields);
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
expect(heroRes.profile).to.have.all.keys(['name']);
@@ -210,6 +215,7 @@ describe('PUT /heroes/:heroId', () => {
});
// test response
// works as: object has all and only these keys
expect(heroRes).to.have.all.keys(heroFields);
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
expect(heroRes.profile).to.have.all.keys(['name']);
@@ -220,35 +226,4 @@ describe('PUT /heroes/:heroId', () => {
await hero.sync();
expect(hero.items.special.snowball).to.equal(5);
});
it('does not accidentally update API Token', async () => {
// This test has been included because hall.js will contain code to produce
// a truncated version of the API Token, and we want to be sure that
// the real Token is not modified by bugs in that code.
const hero = await generateUser();
const originalToken = hero.apiToken;
// make any change to the user except the Token
await user.put(`/hall/heroes/${hero._id}`, {
contributor: { text: 'Astronaut' },
});
const updatedHero = await User.findById(hero._id).exec();
expect(updatedHero.apiToken).to.equal(originalToken);
expect(updatedHero.apiTokenObscured).to.not.exist;
});
it('does update API Token when admin changes it', async () => {
const hero = await generateUser();
const originalToken = hero.apiToken;
// change the user's API Token
await user.put(`/hall/heroes/${hero._id}`, {
changeApiToken: true,
});
const updatedHero = await User.findById(hero._id).exec();
expect(updatedHero.apiToken).to.not.equal(originalToken);
expect(updatedHero.apiTokenObscured).to.not.exist;
});
});

View File

@@ -176,7 +176,7 @@ describe('POST /members/send-private-message', () => {
it('allows admin to send when sender has blocked the admin', async () => {
userToSendMessage = await generateUser({
'permissions.moderator': true,
'contributor.admin': 1,
});
const receiver = await generateUser({ 'inbox.blocks': [userToSendMessage._id] });
@@ -204,7 +204,7 @@ describe('POST /members/send-private-message', () => {
it('allows admin to send when to user has opted out of messaging', async () => {
userToSendMessage = await generateUser({
'permissions.moderator': true,
'contributor.admin': 1,
});
const receiver = await generateUser({ 'inbox.optOut': true });

View File

@@ -25,7 +25,6 @@ describe('Prevent multiple notifications', () => {
for (let i = 0; i < 4; i += 1) {
for (let memberIndex = 0; memberIndex < partyMembers.length; memberIndex += 1) {
await partyMembers[memberIndex].update({ 'auth.timestamps.created': new Date('2022-01-01') }); // eslint-disable-line no-await-in-loop
multipleChatMessages.push(
partyMembers[memberIndex].post(`/groups/${party._id}/chat`, { message: `Message ${i}_${memberIndex}` }),
);

View File

@@ -45,10 +45,11 @@ describe('payments : apple #subscribe', () => {
});
expect(subscribeStub).to.be.calledOnce;
expect(subscribeStub.args[0][0]._id).to.eql(user._id);
expect(subscribeStub.args[0][1]).to.eql('receipt');
expect(subscribeStub.args[0][2]['x-api-key']).to.eql(user.apiToken);
expect(subscribeStub.args[0][2]['x-api-user']).to.eql(user._id);
expect(subscribeStub.args[0][0]).to.eql(sku);
expect(subscribeStub.args[0][1]._id).to.eql(user._id);
expect(subscribeStub.args[0][2]).to.eql('receipt');
expect(subscribeStub.args[0][3]['x-api-key']).to.eql(user.apiToken);
expect(subscribeStub.args[0][3]['x-api-user']).to.eql(user._id);
});
});
});

View File

@@ -21,11 +21,11 @@ describe('payments : apple #verify', () => {
let verifyStub;
beforeEach(async () => {
verifyStub = sinon.stub(applePayments, 'verifyPurchase').resolves({});
verifyStub = sinon.stub(applePayments, 'verifyGemPurchase').resolves({});
});
afterEach(() => {
applePayments.verifyPurchase.restore();
applePayments.verifyGemPurchase.restore();
});
it('makes a purchase', async () => {

View File

@@ -21,11 +21,11 @@ describe('payments : google #verify', () => {
let verifyStub;
beforeEach(async () => {
verifyStub = sinon.stub(googlePayments, 'verifyPurchase').resolves({});
verifyStub = sinon.stub(googlePayments, 'verifyGemPurchase').resolves({});
});
afterEach(() => {
googlePayments.verifyPurchase.restore();
googlePayments.verifyGemPurchase.restore();
});
it('makes a purchase', async () => {

View File

@@ -0,0 +1,33 @@
import superagent from 'superagent';
import nconf from 'nconf';
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
const API_TEST_SERVER_PORT = nconf.get('PORT');
xdescribe('GET /qr-code/user/:memberId', () => {
let user;
before(async () => {
user = await generateUser();
});
it('validates req.params.memberId', async () => {
await expect(user.get('/qr-code/user/invalidUUID')).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('redirects to profile page', async () => {
const url = `http://localhost:${API_TEST_SERVER_PORT}/qr-code/user/${user._id}`;
const response = await superagent.get(url).end((err, res) => {
expect(err).to.be(undefined);
return res;
});
expect(response.status).to.eql(200);
expect(response.request.url).to.eql(`http://localhost:${API_TEST_SERVER_PORT}/static/front/#?memberId=${user._id}`);
});
});

View File

@@ -105,7 +105,7 @@ describe('GET /tasks/:id', () => {
it('can get challenge task if admin', async () => {
const admin = await generateUser({
'permissions.challengeAdmin': true,
'contributor.admin': true,
});
const getTask = await admin.get(`/tasks/${task._id}`);
@@ -134,7 +134,6 @@ describe('GET /tasks/:id', () => {
type: 'guild',
},
members: 1,
upgradeToGroupPlan: true,
});
group = groupData.group;

View File

@@ -7,15 +7,8 @@ import {
describe('POST /tasks/clearCompletedTodos', () => {
it('deletes all completed todos except the ones from a challenge and group', async () => {
const user = await generateUser({ balance: 1 });
const guild = await generateGroup(
user,
{},
{ 'purchased.plan.customerId': 'group-unlimited' },
);
const guild = await generateGroup(user);
const challenge = await generateChallenge(user, guild);
await user.put('/user', {
'preferences.tasks.mirrorGroupTasks': [guild._id],
});
await user.post(`/challenges/${challenge._id}/join`);
const initialTodoCount = user.tasksOrder.todos.length;
@@ -36,7 +29,7 @@ describe('POST /tasks/clearCompletedTodos', () => {
text: 'todo 7',
type: 'todo',
});
await user.post(`/tasks/${groupTask._id}/assign`, [user._id]);
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
const tasks = await user.get('/tasks/user?type=todos');
expect(tasks.length).to.equal(initialTodoCount + 7);

View File

@@ -60,7 +60,7 @@ describe('POST /tasks/challenge/:challengeId', () => {
});
it('allows non-leader admin to add tasks to a challenge when not a member', async () => {
const admin = await generateUser({ 'permissions.challengeAdmin': true });
const admin = await generateUser({ 'contributor.admin': true });
const task = await admin.post(`/tasks/challenge/${challenge._id}`, {
text: 'test habit from admin',
type: 'habit',

View File

@@ -30,7 +30,7 @@ describe('POST /tasks/:taskId/checklist/:itemId/score', () => {
expect(savedTask.checklist[0].completed).to.equal(true);
});
it('can use an alias to score a checklist item', async () => {
it('can use a alias to score a checklist item', async () => {
const task = await user.post('/tasks/user', {
type: 'daily',
text: 'Daily with checklist',

View File

@@ -1,3 +1,4 @@
import { find } from 'lodash';
import {
translate as t,
createAndPopulateGroup,
@@ -7,6 +8,10 @@ describe('Groups DELETE /tasks/:id', () => {
let user; let guild; let member; let member2; let
task;
function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
}
beforeEach(async () => {
const { group, members, groupLeader } = await createAndPopulateGroup({
groupDetails: {
@@ -14,7 +19,6 @@ describe('Groups DELETE /tasks/:id', () => {
type: 'guild',
},
members: 2,
upgradeToGroupPlan: true,
});
guild = group;
@@ -30,7 +34,8 @@ describe('Groups DELETE /tasks/:id', () => {
notes: 1976,
});
await user.post(`/tasks/${task._id}/assign`, [member._id, member2._id]);
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await user.post(`/tasks/${task._id}/assign/${member2._id}`);
});
it('deletes a group task', async () => {
@@ -58,4 +63,81 @@ describe('Groups DELETE /tasks/:id', () => {
message: t('messageTaskNotFound'),
});
});
it('removes deleted taskʾs approval pending notifications from managers', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await user.put(`/tasks/${task._id}/`, {
requiresApproval: true,
});
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(2);
expect(user.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
expect(member2.notifications.length).to.equal(2);
expect(member2.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
await member2.del(`/tasks/${task._id}`);
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(1);
expect(member2.notifications.length).to.equal(1);
});
it('deletes task from assigned user', async () => {
await user.del(`/tasks/${task._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
expect(syncedTask).to.not.exist;
});
it('deletes task from all assigned users', async () => {
await user.del(`/tasks/${task._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
const member2Tasks = await member2.get('/tasks/user');
const member2SyncedTask = find(member2Tasks, findAssignedTask);
expect(syncedTask).to.not.exist;
expect(member2SyncedTask).to.not.exist;
});
it('prevents a user from deleting a task they are assigned to', async () => {
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await expect(member.del(`/tasks/${syncedTask._id}`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('cantDeleteAssignedGroupTasks'),
});
});
it('allows a user to delete a task after leaving a group', async () => {
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/groups/${guild._id}/leave`);
await member.del(`/tasks/${syncedTask._id}`);
await expect(member.get(`/tasks/${syncedTask._id}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: 'Task not found.',
});
});
});

View File

@@ -0,0 +1,77 @@
import { find } from 'lodash';
import {
createAndPopulateGroup,
} from '../../../../../helpers/api-integration/v3';
describe('GET /approvals/group/:groupId', () => {
let user; let guild; let member; let addlMember; let task; let syncedTask; let
addlSyncedTask;
function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
}
beforeEach(async () => {
const { group, members, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
},
members: 2,
});
guild = group;
user = groupLeader;
member = members[0]; // eslint-disable-line prefer-destructuring
addlMember = members[1]; // eslint-disable-line prefer-destructuring
task = await user.post(`/tasks/group/${guild._id}`, {
text: 'test todo',
type: 'todo',
requiresApproval: true,
});
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await user.post(`/tasks/${task._id}/assign/${addlMember._id}`);
const memberTasks = await member.get('/tasks/user');
syncedTask = find(memberTasks, findAssignedTask);
const addlMemberTasks = await addlMember.get('/tasks/user');
addlSyncedTask = find(addlMemberTasks, findAssignedTask);
try {
await member.post(`/tasks/${syncedTask._id}/score/up`);
} catch (e) {
// eslint-disable-next-line no-empty
}
try {
await addlMember.post(`/tasks/${addlSyncedTask._id}/score/up`);
} catch (e) {
// eslint-disable-next-line no-empty
}
});
it('provides only user\'s own tasks when user is not the group leader', async () => {
const approvals = await member.get(`/approvals/group/${guild._id}`);
expect(approvals[0]._id).to.equal(syncedTask._id);
expect(approvals[1]).to.not.exist;
});
it('allows group leaders to get a list of tasks that need approval', async () => {
const approvals = await user.get(`/approvals/group/${guild._id}`);
expect(approvals[0]._id).to.equal(syncedTask._id);
expect(approvals[1]._id).to.equal(addlSyncedTask._id);
});
it('allows managers to get a list of tasks that need approval', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member._id,
});
const approvals = await member.get(`/approvals/group/${guild._id}`);
expect(approvals[0]._id).to.equal(syncedTask._id);
expect(approvals[1]._id).to.equal(addlSyncedTask._id);
});
});

View File

@@ -36,7 +36,7 @@ describe('GET /tasks/group/:groupId', () => {
before(async () => {
user = await generateUser();
group = await generateGroup(user, {}, { 'purchased.plan.customerId': 'group-unlimited' });
group = await generateGroup(user);
});
it('returns error when group is not found', async () => {

View File

@@ -0,0 +1,260 @@
import { find } from 'lodash';
import {
createAndPopulateGroup,
translate as t,
} from '../../../../../helpers/api-integration/v3';
describe('POST /tasks/:id/approve/:userId', () => {
let user; let guild; let member; let member2; let
task;
function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
}
beforeEach(async () => {
const { group, members, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
},
members: 2,
});
guild = group;
user = groupLeader;
member = members[0]; // eslint-disable-line prefer-destructuring
member2 = members[1]; // eslint-disable-line prefer-destructuring
task = await user.post(`/tasks/group/${guild._id}`, {
text: 'test todo',
type: 'todo',
requiresApproval: true,
});
});
it('errors when user is not assigned', async () => {
await expect(user.post(`/tasks/${task._id}/approve/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 404,
error: 'NotFound',
message: t('messageTaskNotFound'),
});
});
it('errors when user is not the group leader', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await expect(member.post(`/tasks/${task._id}/approve/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlyGroupLeaderCanEditTasks'),
});
});
it('approves an assigned user', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await user.post(`/tasks/${task._id}/approve/${member._id}`);
await member.sync();
expect(member.notifications.length).to.equal(2);
expect(member.notifications[1].type).to.equal('GROUP_TASK_APPROVED');
expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', { taskText: task.text }));
memberTasks = await member.get('/tasks/user');
syncedTask = find(memberTasks, findAssignedTask);
expect(syncedTask.group.approval.approved).to.be.true;
expect(syncedTask.group.approval.approvingUser).to.equal(user._id);
expect(syncedTask.group.approval.dateApproved).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
});
it('allows a manager to approve an assigned user', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await member.sync();
expect(member.notifications.length).to.equal(2);
expect(member.notifications[1].type).to.equal('GROUP_TASK_APPROVED');
expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', { taskText: task.text }));
memberTasks = await member.get('/tasks/user');
syncedTask = find(memberTasks, findAssignedTask);
expect(syncedTask.group.approval.approved).to.be.true;
expect(syncedTask.group.approval.approvingUser).to.equal(member2._id);
expect(syncedTask.group.approval.dateApproved).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
});
it('removes approval pending notifications from managers', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(2);
expect(user.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
expect(member2.notifications.length).to.equal(1);
expect(member2.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(1);
expect(member2.notifications.length).to.equal(0);
});
it('prevents double approval on a task', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await expect(user.post(`/tasks/${task._id}/approve/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('canOnlyApproveTaskOnce'),
});
});
it('prevents approving a task if it is not waiting for approval', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await expect(user.post(`/tasks/${task._id}/approve/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalWasNotRequested'),
});
});
it('completes master task when single-completion task is approved', async () => {
const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: true,
sharedCompletion: 'singleCompletion',
});
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
const groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
const masterTask = find(groupTasks, groupTask => groupTask._id === sharedCompletionTask._id);
expect(masterTask.completed).to.equal(true);
});
it('deletes other assigned user tasks when single-completion task is approved', async () => {
const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: true,
sharedCompletion: 'singleCompletion',
});
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
const member2Tasks = await member2.get('/tasks/user');
const syncedTask2 = find(
member2Tasks,
memberTask => memberTask.group.taskId === sharedCompletionTask._id,
);
expect(syncedTask2).to.equal(undefined);
});
it('does not complete master task when not all user tasks are approved if all assigned must complete', async () => {
const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: true,
sharedCompletion: 'allAssignedCompletion',
});
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
const groupTasks = await user.get(`/tasks/group/${guild._id}`);
const masterTask = find(groupTasks, groupTask => groupTask._id === sharedCompletionTask._id);
expect(masterTask.completed).to.equal(false);
});
it('completes master task when all user tasks are approved if all assigned must complete', async () => {
const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: true,
sharedCompletion: 'allAssignedCompletion',
});
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
const member2Tasks = await member2.get('/tasks/user');
const member2SyncedTask = find(member2Tasks, findAssignedTask);
await member2.post(`/tasks/${member2SyncedTask._id}/score/up`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member2._id}`);
const groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
const masterTask = find(groupTasks, groupTask => groupTask._id === sharedCompletionTask._id);
expect(masterTask.completed).to.equal(true);
});
});

View File

@@ -1,3 +1,4 @@
import { find } from 'lodash';
import {
createAndPopulateGroup,
translate as t,
@@ -7,6 +8,10 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
let user; let guild; let member; let member2; let
task;
function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
}
beforeEach(async () => {
const { group, members, groupLeader } = await createAndPopulateGroup({
groupDetails: {
@@ -14,7 +19,6 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
type: 'guild',
},
members: 2,
upgradeToGroupPlan: true,
});
guild = group;
@@ -32,15 +36,14 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
it('errors when user is not assigned', async () => {
await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 400,
error: 'BadRequest',
message: 'Task not completed by this user.',
code: 404,
error: 'NotFound',
message: t('messageTaskNotFound'),
});
});
it('errors when user is not the group leader', async () => {
await user.post(`/tasks/${task._id}/assign`, [member._id]);
await member.post(`/tasks/${task._id}/score/up`);
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await expect(member.post(`/tasks/${task._id}/needs-work/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
@@ -50,64 +53,132 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
});
it('marks a task as needing more work', async () => {
await member.sync();
const initialNotifications = member.notifications.length;
await user.post(`/tasks/${task._id}/assign`, [member._id]);
await user.post(`/tasks/${task._id}/assign/${member._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
// score task to require approval
await member.post(`/tasks/${task._id}/score/up`);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await user.post(`/tasks/${task._id}/needs-work/${member._id}`);
[memberTasks] = await Promise.all([member.get('/tasks/user'), member.sync()]);
syncedTask = find(memberTasks, findAssignedTask);
// Check that the notification approval request has been removed
expect(syncedTask.group.approval.requested).to.equal(false);
expect(syncedTask.group.approval.requestedDate).to.equal(undefined);
// Check that the notification is correct
await member.sync();
expect(member.notifications.length).to.equal(initialNotifications + 3);
expect(member.notifications.length).to.equal(initialNotifications + 2);
const notification = member.notifications[member.notifications.length - 1];
expect(notification.type).to.equal('GROUP_TASK_NEEDS_WORK');
const taskText = task.text;
const managerName = user.auth.local.username;
const taskText = syncedTask.text;
const managerName = user.profile.name;
expect(notification.data.message).to.equal(t('taskNeedsWork', { taskText, managerName }));
expect(notification.data.task.id).to.equal(task._id);
expect(notification.data.task.id).to.equal(syncedTask._id);
expect(notification.data.task.text).to.equal(taskText);
expect(notification.data.group.id).to.equal(task.group.id);
expect(notification.data.group.id).to.equal(syncedTask.group.id);
expect(notification.data.group.name).to.equal(guild.name);
expect(notification.data.manager.id).to.equal(user._id);
expect(notification.data.manager.name).to.equal(managerName);
// Check that the managers' GROUP_TASK_APPROVAL notifications have been removed
await user.sync();
expect(user.notifications.find(n => { // eslint-disable-line arrow-body-style
return n.data.taskId === syncedTask._id && n.type === 'GROUP_TASK_APPROVAL';
})).to.equal(undefined);
});
it('allows a manager to mark a task as needing work', async () => {
await member.sync();
const initialNotifications = member.notifications.length;
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/assign`, [member._id]);
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
// score task to require approval
await member.post(`/tasks/${task._id}/score/up`);
await member.post(`/tasks/${syncedTask._id}/score/up`);
const initialNotifications = member.notifications.length;
await member2.post(`/tasks/${task._id}/needs-work/${member._id}`);
await member.sync();
expect(member.notifications.length).to.equal(initialNotifications + 3);
[memberTasks] = await Promise.all([member.get('/tasks/user'), member.sync()]);
syncedTask = find(memberTasks, findAssignedTask);
// Check that the notification approval request has been removed
expect(syncedTask.group.approval.requested).to.equal(false);
expect(syncedTask.group.approval.requestedDate).to.equal(undefined);
expect(member.notifications.length).to.equal(initialNotifications + 2);
const notification = member.notifications[member.notifications.length - 1];
expect(notification.type).to.equal('GROUP_TASK_NEEDS_WORK');
const taskText = task.text;
const managerName = member2.auth.local.username;
const taskText = syncedTask.text;
const managerName = member2.profile.name;
expect(notification.data.message).to.equal(t('taskNeedsWork', { taskText, managerName }));
expect(notification.data.task.id).to.equal(task._id);
expect(notification.data.task.id).to.equal(syncedTask._id);
expect(notification.data.task.text).to.equal(taskText);
expect(notification.data.group.id).to.equal(task.group.id);
expect(notification.data.group.id).to.equal(syncedTask.group.id);
expect(notification.data.group.name).to.equal(guild.name);
expect(notification.data.manager.id).to.equal(member2._id);
expect(notification.data.manager.name).to.equal(managerName);
// Check that the managers' GROUP_TASK_APPROVAL notifications have been removed
await Promise.all([user.sync(), member2.sync()]);
expect(user.notifications.find(n => { // eslint-disable-line arrow-body-style
return n.data.taskId === syncedTask._id && n.type === 'GROUP_TASK_APPROVAL';
})).to.equal(undefined);
expect(member2.notifications.find(n => { // eslint-disable-line arrow-body-style
return n.data.taskId === syncedTask._id && n.type === 'GROUP_TASK_APPROVAL';
})).to.equal(undefined);
});
it('prevents marking a task as needing work if it was already approved', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('canOnlyApproveTaskOnce'),
});
});
it('prevents marking a task as needing work if it is not waiting for approval', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalWasNotRequested'),
});
});
});

View File

@@ -8,6 +8,10 @@ describe('POST /tasks/:id/score/:direction', () => {
let user; let guild; let member; let member2; let
task;
function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
}
beforeEach(async () => {
const { group, members, groupLeader } = await createAndPopulateGroup({
groupDetails: {
@@ -15,7 +19,6 @@ describe('POST /tasks/:id/score/:direction', () => {
type: 'guild',
},
members: 2,
upgradeToGroupPlan: true,
});
guild = group;
@@ -26,50 +29,209 @@ describe('POST /tasks/:id/score/:direction', () => {
task = await user.post(`/tasks/group/${guild._id}`, {
text: 'test todo',
type: 'todo',
requiresApproval: true,
});
await user.post(`/tasks/${task._id}/assign`, [member._id]);
await user.post(`/tasks/${task._id}/assign/${member._id}`);
});
it('completes single-assigned task', async () => {
await member.post(`/tasks/${task._id}/score/up`);
it('prevents user from scoring a task that needs to be approved', async () => {
await user.update({
'preferences.language': 'cs',
});
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
const direction = 'up';
const response = await member.post(`/tasks/${syncedTask._id}/score/${direction}`);
expect(response.data.requiresApproval).to.equal(true);
expect(response.message).to.equal(t('taskApprovalHasBeenRequested'));
const updatedTask = await member.get(`/tasks/${syncedTask._id}`);
await user.sync();
expect(user.notifications.length).to.equal(2);
expect(user.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
expect(user.notifications[1].data.message).to.equal(t('userHasRequestedTaskApproval', {
user: member.auth.local.username,
taskName: updatedTask.text,
taskId: updatedTask._id,
direction,
}, 'cs')); // This test only works if we have the notification translated
expect(user.notifications[1].data.groupId).to.equal(guild._id);
expect(updatedTask.group.approval.requested).to.equal(true);
expect(updatedTask.group.approval.requestedDate).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
});
it('sends notifications to all managers', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
const direction = 'up';
await member.post(`/tasks/${syncedTask._id}/score/${direction}`);
const updatedTask = await member.get(`/tasks/${syncedTask._id}`);
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(2);
expect(user.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
expect(user.notifications[1].data.message).to.equal(t('userHasRequestedTaskApproval', {
user: member.auth.local.username,
taskName: updatedTask.text,
taskId: updatedTask._id,
direction,
}));
expect(user.notifications[1].data.groupId).to.equal(guild._id);
expect(member2.notifications.length).to.equal(1);
expect(member2.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
expect(member2.notifications[0].data.message).to.equal(t('userHasRequestedTaskApproval', {
user: member.auth.local.username,
taskName: updatedTask.text,
taskId: updatedTask._id,
direction,
}));
expect(member2.notifications[0].data.groupId).to.equal(guild._id);
});
it('errors when approval has already been requested', async () => {
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
const response = await member.post(`/tasks/${syncedTask._id}/score/up`);
expect(response.data.requiresApproval).to.equal(true);
expect(response.message).to.equal(t('taskRequiresApproval'));
});
it('allows a user to score an approved task', async () => {
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await user.post(`/tasks/${task._id}/approve/${member._id}`);
await member.post(`/tasks/${syncedTask._id}/score/up`);
const updatedTask = await member.get(`/tasks/${syncedTask._id}`);
expect(updatedTask.completed).to.equal(true);
expect(updatedTask.dateCompleted).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
});
it('completes master task when single-completion task is completed', async () => {
const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: false,
sharedCompletion: 'singleCompletion',
});
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(
memberTasks,
memberTask => memberTask.group.taskId === sharedCompletionTask._id,
);
await member.post(`/tasks/${syncedTask._id}/score/up`);
const groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
const sourceTask = find(groupTasks, groupTask => groupTask._id === task._id);
const masterTask = find(groupTasks, groupTask => groupTask._id === sharedCompletionTask._id);
expect(sourceTask.completed).to.equal(true);
expect(masterTask.completed).to.equal(true);
});
it('errors when task has already been completed', async () => {
await member.post(`/tasks/${task._id}/score/up`);
await expect(member.post(`/tasks/${task._id}/score/up`)).to.be.rejected.and.to.eventually.eql({
code: 401,
error: 'NotAuthorized',
message: t('sessionOutdated'),
it('deletes other assigned user tasks when single-completion task is completed', async () => {
const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: false,
sharedCompletion: 'singleCompletion',
});
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(
memberTasks,
memberTask => memberTask.group.taskId === sharedCompletionTask._id,
);
await member.post(`/tasks/${syncedTask._id}/score/up`);
const member2Tasks = await member2.get('/tasks/user');
const syncedTask2 = find(
member2Tasks,
memberTask => memberTask.group.taskId === sharedCompletionTask._id,
);
expect(syncedTask2).to.equal(undefined);
});
it('does not complete multi-assigned task when not all assignees have completed', async () => {
await user.post(`/tasks/${task._id}/assign`, [member2._id]);
it('does not complete master task when not all user tasks are completed if all assigned must complete', async () => {
const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: false,
sharedCompletion: 'allAssignedCompletion',
});
await member.post(`/tasks/${task._id}/score/up`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(
memberTasks,
memberTask => memberTask.group.taskId === sharedCompletionTask._id,
);
await member.post(`/tasks/${syncedTask._id}/score/up`);
const groupTasks = await user.get(`/tasks/group/${guild._id}`);
const sourceTask = find(groupTasks, groupTask => groupTask._id === task._id);
const masterTask = find(groupTasks, groupTask => groupTask._id === sharedCompletionTask._id);
expect(sourceTask.completed).to.equal(false);
expect(masterTask.completed).to.equal(false);
});
it('completes multi-assigned task when all assignees have completed', async () => {
await user.post(`/tasks/${task._id}/assign`, [member2._id]);
it('completes master task when all user tasks are completed if all assigned must complete', async () => {
const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: false,
sharedCompletion: 'allAssignedCompletion',
});
await member.post(`/tasks/${task._id}/score/up`);
await member2.post(`/tasks/${task._id}/score/up`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
const memberTasks = await member.get('/tasks/user');
const member2Tasks = await member2.get('/tasks/user');
const syncedTask = find(
memberTasks,
memberTask => memberTask.group.taskId === sharedCompletionTask._id,
);
const syncedTask2 = find(
member2Tasks,
memberTask => memberTask.group.taskId === sharedCompletionTask._id,
);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await member2.post(`/tasks/${syncedTask2._id}/score/up`);
const groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
const sourceTask = find(groupTasks, groupTask => groupTask._id === task._id);
const masterTask = find(groupTasks, groupTask => groupTask._id === sharedCompletionTask._id);
expect(sourceTask.completed).to.equal(true);
expect(masterTask.completed).to.equal(true);
});
});

View File

@@ -26,7 +26,6 @@ describe('POST /tasks/group/:groupid', () => {
privacy: 'private',
},
members: 1,
upgradeToGroupPlan: true,
});
guild = group;

View File

@@ -21,7 +21,6 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
type: 'guild',
},
members: 2,
upgradeToGroupPlan: true,
});
guild = group;
@@ -39,7 +38,7 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
});
it('returns error when task is not found', async () => {
await expect(user.post(`/tasks/${generateUUID()}/assign`, [member._id]))
await expect(user.post(`/tasks/${generateUUID()}/assign/${member._id}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
@@ -56,7 +55,7 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
notes: 1976,
});
await expect(user.post(`/tasks/${nonGroupTask._id}/assign`, [member._id]))
await expect(user.post(`/tasks/${nonGroupTask._id}/assign/${member._id}`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
@@ -67,7 +66,7 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
it('returns error when user is not a member of the group', async () => {
const nonUser = await generateUser();
await expect(nonUser.post(`/tasks/${task._id}/assign`, [member._id]))
await expect(nonUser.post(`/tasks/${task._id}/assign/${member._id}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
@@ -76,7 +75,7 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
});
it('returns error when non leader tries to create a task', async () => {
await expect(member2.post(`/tasks/${task._id}/assign`, [member._id]))
await expect(member2.post(`/tasks/${task._id}/assign/${member._id}`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
@@ -84,54 +83,73 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
});
});
it('assigns a task to a user', async () => {
await user.post(`/tasks/${task._id}/assign`, [member._id]);
it('allows user to assign themselves (claim)', async () => {
await member.post(`/tasks/${task._id}/assign/${member._id}`);
const groupTask = await user.get(`/tasks/group/${guild._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
expect(groupTask[0].group.assignedUsers).to.contain(member._id);
expect(syncedTask).to.exist;
});
it('sends notifications to group leader and managers when a task is claimed', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member.post(`/tasks/${task._id}/assign/${member._id}`);
await user.sync();
await member2.sync();
const groupTask = await user.get(`/tasks/group/${guild._id}`);
expect(user.notifications.length).to.equal(2); // includes Guild Joined achievement
expect(user.notifications[1].type).to.equal('GROUP_TASK_CLAIMED');
expect(user.notifications[1].data.taskId).to.equal(groupTask[0]._id);
expect(user.notifications[1].data.groupId).to.equal(guild._id);
expect(member2.notifications.length).to.equal(1);
expect(member2.notifications[0].type).to.equal('GROUP_TASK_CLAIMED');
expect(member2.notifications[0].data.taskId).to.equal(groupTask[0]._id);
expect(member2.notifications[0].data.groupId).to.equal(guild._id);
});
it('assigns a task to a user', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
const groupTask = await user.get(`/tasks/group/${guild._id}`);
await member.put('/user', {
'preferences.tasks.mirrorGroupTasks': [guild._id],
});
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
expect(groupTask[0].group.assignedUsers).to.contain(member._id);
expect(groupTask[0].group.assignedUsersDetail[member._id]).to.exist;
expect(syncedTask).to.exist;
});
it('sends a notification to assigned user', async () => {
await user.post(`/tasks/${task._id}/assign`, [member._id]);
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await member.sync();
const groupTask = await user.get(`/tasks/group/${guild._id}`);
expect(member.notifications.length).to.equal(2);
expect(member.notifications[1].type).to.equal('GROUP_TASK_ASSIGNED');
expect(member.notifications[1].taskId).to.equal(groupTask._id);
expect(member.notifications.length).to.equal(1);
expect(member.notifications[0].type).to.equal('GROUP_TASK_ASSIGNED');
expect(member.notifications[0].taskId).to.equal(groupTask._id);
});
it('assigns a task to multiple users', async () => {
await user.post(`/tasks/${task._id}/assign`, [member._id, member2._id]);
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await user.post(`/tasks/${task._id}/assign/${member2._id}`);
const groupTask = await user.get(`/tasks/group/${guild._id}`);
await member.put('/user', {
'preferences.tasks.mirrorGroupTasks': [guild._id],
});
const memberTasks = await member.get('/tasks/user');
const member1SyncedTask = find(memberTasks, findAssignedTask);
await member2.put('/user', {
'preferences.tasks.mirrorGroupTasks': [guild._id],
});
const member2Tasks = await member2.get('/tasks/user');
const member2SyncedTask = find(member2Tasks, findAssignedTask);
expect(groupTask[0].group.assignedUsers).to.contain(member._id);
expect(groupTask[0].group.assignedUsersDetail[member._id]).to.exist;
expect(member1SyncedTask).to.exist;
expect(groupTask[0].group.assignedUsers).to.contain(member2._id);
expect(groupTask[0].group.assignedUsersDetail[member2._id]).to.exist;
expect(member1SyncedTask).to.exist;
expect(member2SyncedTask).to.exist;
});
@@ -140,17 +158,13 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/assign`, [member._id]);
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
const groupTask = await member2.get(`/tasks/group/${guild._id}`);
await member.put('/user', {
'preferences.tasks.mirrorGroupTasks': [guild._id],
});
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
expect(groupTask[0].group.assignedUsers).to.contain(member._id);
expect(groupTask[0].group.assignedUsersDetail[member._id]).to.exist;
expect(syncedTask).to.exist;
});
});

View File

@@ -9,7 +9,7 @@ describe('POST group-tasks/:taskId/move/to/:position', () => {
beforeEach(async () => {
user = await generateUser({ balance: 1 });
guild = await generateGroup(user, { type: 'guild' }, { 'purchased.plan.customerId': 'group-unlimited' });
guild = await generateGroup(user, { type: 'guild' });
});
it('can move task to new position', async () => {

View File

@@ -21,7 +21,6 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
type: 'guild',
},
members: 2,
upgradeToGroupPlan: true,
});
guild = group;
@@ -37,7 +36,7 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
notes: 1976,
});
await user.post(`/tasks/${task._id}/assign`, [member._id]);
await user.post(`/tasks/${task._id}/assign/${member._id}`);
});
it('returns error when task is not found', async () => {
@@ -92,11 +91,11 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
await user.post(`/tasks/${task._id}/unassign/${member._id}`);
await member.sync();
expect(member.notifications.length).to.equal(1); // mystery items
expect(member.notifications.length).to.equal(0);
});
it('unassigns a user and only that user from a task', async () => {
await user.post(`/tasks/${task._id}/assign`, [member2._id]);
await user.post(`/tasks/${task._id}/assign/${member2._id}`);
await user.post(`/tasks/${task._id}/unassign/${member._id}`);
@@ -105,9 +104,6 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
const memberTasks = await member.get('/tasks/user');
const member1SyncedTask = find(memberTasks, findAssignedTask);
await member2.put('/user', {
'preferences.tasks.mirrorGroupTasks': [guild._id],
});
const member2Tasks = await member2.get('/tasks/user');
const member2SyncedTask = find(member2Tasks, findAssignedTask);
@@ -133,7 +129,20 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
expect(syncedTask).to.not.exist;
});
it('returns error when non leader tries to unassign a task', async () => {
it('allows a user to unassign themselves', async () => {
await member.post(`/tasks/${task._id}/unassign/${member._id}`);
const groupTask = await user.get(`/tasks/group/${guild._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
expect(groupTask[0].group.assignedUsers).to.not.contain(member._id);
expect(syncedTask).to.not.exist;
});
// @TODO: Which do we want? The user to unassign themselves or not. This test was in
// here, but then we had a request to allow to unaissgn.
xit('returns error when non leader tries to unassign their a task', async () => {
await expect(member.post(`/tasks/${task._id}/unassign/${member._id}`))
.to.eventually.be.rejected.and.eql({
code: 401,

View File

@@ -1,3 +1,4 @@
import { find } from 'lodash';
import {
createAndPopulateGroup, translate as t,
} from '../../../../../helpers/api-integration/v3';
@@ -10,6 +11,10 @@ describe('PUT /tasks/:id', () => {
let habit;
let todo;
function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
}
beforeEach(async () => {
const { group, members, groupLeader } = await createAndPopulateGroup({
groupDetails: {
@@ -17,7 +22,6 @@ describe('PUT /tasks/:id', () => {
type: 'guild',
},
members: 2,
upgradeToGroupPlan: true,
});
guild = group;
@@ -39,7 +43,8 @@ describe('PUT /tasks/:id', () => {
notes: 1976,
});
await user.post(`/tasks/${habit._id}/assign`, [member._id, member2._id]);
await user.post(`/tasks/${habit._id}/assign/${member._id}`);
await user.post(`/tasks/${habit._id}/assign/${member2._id}`);
});
it('updates a group task', async () => {
@@ -50,6 +55,28 @@ describe('PUT /tasks/:id', () => {
expect(savedHabit.notes).to.eql('some new notes');
});
it('updates a group task - approval is required', async () => {
// allow to manage
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member._id,
});
// change the habit
habit = await member.put(`/tasks/${habit._id}`, {
text: 'new text!',
requiresApproval: true,
});
const memberTasks = await member2.get('/tasks/user');
const syncedTask = find(memberTasks, memberTask => memberTask.group.taskId === habit._id);
// score up to trigger approval
const response = await member2.post(`/tasks/${syncedTask._id}/score/up`);
expect(response.data.requiresApproval).to.equal(true);
expect(response.message).to.equal(t('taskApprovalHasBeenRequested'));
});
it('member updates a group task value - not allowed', async () => {
// change the todo
await expect(member.put(`/tasks/${habit._id}`, {
@@ -92,7 +119,7 @@ describe('PUT /tasks/:id', () => {
],
});
await user.post(`/tasks/${habit._id}/assign`, [member._id]);
await user.post(`/tasks/${habit._id}/assign/${member._id}`);
// change the checklist text
habit = await user.put(`/tasks/${habit._id}`, {
@@ -109,4 +136,63 @@ describe('PUT /tasks/:id', () => {
expect(habit.checklist.length).to.eql(2);
});
it('updates the linked tasks', async () => {
await user.put(`/tasks/${habit._id}`, {
text: 'some new text',
up: false,
down: false,
notes: 'some new notes',
});
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
expect(syncedTask.text).to.eql('some new text');
expect(syncedTask.up).to.eql(false);
expect(syncedTask.down).to.eql(false);
});
it('updates the linked tasks for all assigned users', async () => {
await user.put(`/tasks/${habit._id}`, {
text: 'some new text',
up: false,
down: false,
notes: 'some new notes',
});
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
const member2Tasks = await member2.get('/tasks/user');
const member2SyncedTask = find(member2Tasks, findAssignedTask);
expect(syncedTask.text).to.eql('some new text');
expect(syncedTask.up).to.eql(false);
expect(syncedTask.down).to.eql(false);
expect(member2SyncedTask.text).to.eql('some new text');
expect(member2SyncedTask.up).to.eql(false);
expect(member2SyncedTask.down).to.eql(false);
});
it('updates the linked tasks', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.put(`/tasks/${habit._id}`, {
text: 'some new text',
up: false,
down: false,
notes: 'some new notes',
});
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
expect(syncedTask.text).to.eql('some new text');
expect(syncedTask.up).to.eql(false);
expect(syncedTask.down).to.eql(false);
});
});

View File

@@ -15,7 +15,6 @@ describe('DELETE group /tasks/:taskId/checklist/:itemId', () => {
type: 'guild',
},
members: 2,
upgradeToGroupPlan: true,
});
guild = group;

View File

@@ -15,7 +15,6 @@ describe('POST group /tasks/:taskId/checklist/', () => {
type: 'guild',
},
members: 2,
upgradeToGroupPlan: true,
});
guild = group;

View File

@@ -15,7 +15,6 @@ describe('PUT group /tasks/:taskId/checklist/:itemId', () => {
type: 'guild',
},
members: 2,
upgradeToGroupPlan: true,
});
guild = group;

View File

@@ -289,6 +289,45 @@ describe('DELETE /user', () => {
});
});
context('user with Facebook auth', async () => {
beforeEach(async () => {
user = await generateUser({
auth: {
facebook: {
id: 'facebook-id',
},
},
});
});
it('returns an error if confirmation phrase is wrong', async () => {
await expect(user.del('/user', {
password: 'just-do-it',
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('incorrectDeletePhrase', { magicWord: 'DELETE' }),
});
});
it('returns an error if confirmation phrase is not supplied', async () => {
await expect(user.del('/user', {
password: '',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingPassword'),
});
});
it('deletes a Facebook user', async () => {
await user.del('/user', {
password: DELETE_CONFIRMATION,
});
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
});
});
context('user with Google auth', async () => {
beforeEach(async () => {
user = await generateUser({

View File

@@ -139,22 +139,25 @@ describe('POST /user/class/cast/:spellId', () => {
});
it('returns an error if a group task was targeted', async () => {
const { group, groupLeader } = await createAndPopulateGroup({ upgradeToGroupPlan: true });
const { group, groupLeader } = await createAndPopulateGroup();
const groupTask = await groupLeader.post(`/tasks/group/${group._id}`, {
text: 'todo group',
type: 'todo',
});
await groupLeader.post(`/tasks/${groupTask._id}/assign`, [groupLeader._id]);
await groupLeader.post(`/tasks/${groupTask._id}/assign/${groupLeader._id}`);
const memberTasks = await groupLeader.get('/tasks/user');
const syncedGroupTask = find(memberTasks, memberTask => memberTask.group.id === group._id);
await groupLeader.update({ 'stats.class': 'rogue', 'stats.lvl': 11 });
await sleep(0.5);
await groupLeader.sync();
await expect(groupLeader.post(`/user/class/cast/pickPocket?targetId=${groupTask._id}`))
await expect(groupLeader.post(`/user/class/cast/pickPocket?targetId=${syncedGroupTask._id}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('messageTaskNotFound'),
code: 400,
error: 'BadRequest',
message: t('groupTasksNoCast'),
});
});
@@ -263,7 +266,7 @@ describe('POST /user/class/cast/:spellId', () => {
});
it('searing brightness does not affect challenge or group tasks', async () => {
const guild = await generateGroup(user, {}, { 'purchased.plan.customerId': 'group-unlimited' });
const guild = await generateGroup(user);
const challenge = await generateChallenge(user, guild);
await user.post(`/challenges/${challenge._id}/join`);
await user.post(`/tasks/challenge/${challenge._id}`, {
@@ -276,10 +279,7 @@ describe('POST /user/class/cast/:spellId', () => {
type: 'todo',
});
await user.update({ 'stats.class': 'healer', 'stats.mp': 200, 'stats.lvl': 15 });
await user.post(`/tasks/${groupTask._id}/assign`, [user._id]);
await user.put('/user', {
'preferences.tasks.mirrorGroupTasks': [guild._id],
});
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
await user.post('/user/class/cast/brightness');
await user.sync();

View File

@@ -88,7 +88,7 @@ describe('POST /user/reset', () => {
});
it('does not delete challenge or group tasks', async () => {
const guild = await generateGroup(user, {}, { 'purchased.plan.customerId': 'group-unlimited' });
const guild = await generateGroup(user);
const challenge = await generateChallenge(user, guild);
await user.post(`/challenges/${challenge._id}/join`);
await user.post(`/tasks/challenge/${challenge._id}`, {
@@ -100,14 +100,11 @@ describe('POST /user/reset', () => {
text: 'todo group',
type: 'todo',
});
await user.post(`/tasks/${groupTask._id}/assign`, [user._id]);
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
await user.post('/user/reset');
await user.sync();
await user.put('/user', {
'preferences.tasks.mirrorGroupTasks': [guild._id],
});
const memberTasks = await user.get('/tasks/user');
const syncedGroupTask = find(memberTasks, memberTask => memberTask.group.id === guild._id);
@@ -123,7 +120,7 @@ describe('POST /user/reset', () => {
it('does not delete secret', async () => {
const admin = await generateUser({
permissions: { userSupport: true },
contributor: { admin: true },
});
const hero = await generateUser({

View File

@@ -35,7 +35,7 @@ describe('PUT /user', () => {
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Tag list must be an array.',
message: 'mustBeArray',
});
});
@@ -135,7 +135,6 @@ describe('PUT /user', () => {
'gem balance': { balance: 100 },
auth: { 'auth.blocked': true, 'auth.timestamps.created': new Date() },
contributor: { 'contributor.level': 9, 'contributor.admin': true, 'contributor.text': 'some text' },
permissions: { 'permissions.fullAccess': true, 'permissions.news': true, 'permissions.moderator': 'some text' },
backer: { 'backer.tier': 10, 'backer.npc': 'Bilbo' },
subscriptions: { 'purchased.plan.extraMonths': 500, 'purchased.plan.consecutive.trinkets': 1000 },
'customization gem purchases': { 'purchased.background.tavern': true, 'purchased.skin.bear': true },

View File

@@ -20,6 +20,44 @@ describe('DELETE social registration', () => {
});
});
context('Facebook', () => {
it('fails if user does not have an alternative registration method', async () => {
await user.update({
'auth.facebook.id': 'some-fb-id',
'auth.local': { ok: true },
});
await expect(user.del('/user/auth/social/facebook')).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('cantDetachSocial'),
});
});
it('succeeds if user has a local registration', async () => {
await user.update({
'auth.facebook.id': 'some-fb-id',
});
const response = await user.del('/user/auth/social/facebook');
expect(response).to.eql({});
await user.sync();
expect(user.auth.facebook).to.be.undefined;
});
it('succeeds if user has a google registration', async () => {
await user.update({
'auth.facebook.id': 'some-fb-id',
'auth.google.id': 'some-google-id',
'auth.local': { ok: true },
});
const response = await user.del('/user/auth/social/facebook');
expect(response).to.eql({});
await user.sync();
expect(user.auth.facebook).to.be.undefined;
});
});
context('Google', () => {
it('fails if user does not have an alternative registration method', async () => {
await user.update({
@@ -43,6 +81,19 @@ describe('DELETE social registration', () => {
await user.sync();
expect(user.auth.google).to.be.undefined;
});
it('succeeds if user has a facebook registration', async () => {
await user.update({
'auth.google.id': 'some-google-id',
'auth.facebook.id': 'some-facebook-id',
'auth.local': { ok: true },
});
const response = await user.del('/user/auth/social/google');
expect(response).to.eql({});
await user.sync();
expect(user.auth.goodl).to.be.undefined;
});
});
context('Apple', () => {
@@ -68,5 +119,18 @@ describe('DELETE social registration', () => {
await user.sync();
expect(user.auth.apple).to.be.undefined;
});
it('succeeds if user has a facebook registration', async () => {
await user.update({
'auth.apple.id': 'some-apple-id',
'auth.facebook.id': 'some-facebook-id',
'auth.local': { ok: true },
});
const response = await user.del('/user/auth/social/apple');
expect(response).to.eql({});
await user.sync();
expect(user.auth.goodl).to.be.undefined;
});
});
});

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