Compare commits

..

1 Commits

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,155 +0,0 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20230731_naming_day';
import { v4 as uuid } from 'uuid';
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
let set;
let push;
const inc = {
'items.food.Cake_Base': 1,
'items.food.Cake_CottonCandyBlue': 1,
'items.food.Cake_CottonCandyPink': 1,
'items.food.Cake_Desert': 1,
'items.food.Cake_Golden': 1,
'items.food.Cake_Red': 1,
'items.food.Cake_Shade': 1,
'items.food.Cake_Skeleton': 1,
'items.food.Cake_White': 1,
'items.food.Cake_Zombie': 1,
'achievements.habiticaDays': 1,
};
if (user && user.items && user.items.gear && user.items.gear.owned && typeof user.items.gear.owned.back_special_namingDay2020 !== 'undefined') {
set = { migration: MIGRATION_NAME };
push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_namingDay_cake',
title: 'Happy Naming Day!',
text: 'To celebrate the day we became Habitica, weve awarded you some cake!',
destination: '/inventory/items',
},
seen: false,
},
};
} else if (user && user.items && user.items.gear && user.items.gear.owned && typeof user.items.gear.owned.body_special_namingDay2018 !== 'undefined') {
set = { migration: MIGRATION_NAME, 'items.gear.owned.back_special_namingDay2020': true };
push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_namingDay_back',
title: 'Happy Naming Day!',
text: 'To celebrate the day we became Habitica, weve awarded you a Royal Purple Gryphon Tail and cake!',
destination: '/inventory/equipment',
},
seen: false,
},
};
} else if (user && user.items && user.items.gear && user.items.gear.owned && typeof user.items.gear.owned.head_special_namingDay2017 !== 'undefined') {
set = { migration: MIGRATION_NAME, 'items.gear.owned.body_special_namingDay2018': true };
push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_namingDay_body',
title: 'Happy Naming Day!',
text: 'To celebrate the day we became Habitica, weve awarded you a Royal Purple Gryphon Cloak and cake!',
destination: '/inventory/equipment',
},
seen: false,
},
};
} else if (user && user.items && user.items.pets && typeof user.items.pets['Gryphon-RoyalPurple'] !== 'undefined') {
set = { migration: MIGRATION_NAME, 'items.gear.owned.head_special_namingDay2017': true };
push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_namingDay_head',
title: 'Happy Naming Day!',
text: 'To celebrate the day we became Habitica, weve awarded you a Royal Purple Gryphon Helm and cake!',
destination: '/inventory/equipment',
},
seen: false,
},
};
} else if (user && user.items && user.items.mounts && typeof user.items.mounts['Gryphon-RoyalPurple'] !== 'undefined') {
set = { migration: MIGRATION_NAME, 'items.pets.Gryphon-RoyalPurple': 5 };
push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_namingDay_pet',
title: 'Happy Naming Day!',
text: 'To celebrate the day we became Habitica, weve awarded you a Royal Purple Gryphon Pet and cake!',
destination: '/inventory/stable',
},
seen: false,
},
};
} else {
set = { migration: MIGRATION_NAME, 'items.mounts.Gryphon-RoyalPurple': true };
push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_namingDay_mount',
title: 'Happy Naming Day!',
text: 'To celebrate the day we became Habitica, weve awarded you a Royal Purple Gryphon Mount and cake!',
destination: '/inventory/stable',
},
seen: false,
},
};
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
if (push) {
return await user.updateOne({ $set: set, $inc: inc, $push: push }).exec();
} else {
return await user.updateOne({ $set: set, $inc: inc }).exec();
}
}
export default async function processUsers () {
let query = {
migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2023-07-01') },
};
const fields = {
_id: 1,
items: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1]._id,
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -1,72 +0,0 @@
/* eslint-disable no-console */
import { model as User } from '../../../website/server/models/user';
import { model as Group } from '../../../website/server/models/group';
const guildsPerRun = 500;
const progressCount = 1000;
const guildsQuery = {
type: 'guild',
};
let count = 0;
async function updateGroup (guild) {
count++;
if (count % progressCount === 0) {
console.warn(`${count} ${guild._id}`);
}
if (guild.hasActiveGroupPlan()) {
return console.warn(`Guild ${guild._id} is active Group Plan`);
}
const leader = await User
.findOne({ _id: guild.leader })
.select({ _id: true })
.exec();
if (!leader) {
return console.warn(`Leader not found for Guild ${guild._id}`);
}
if (guild.balance > 0) {
await leader.updateBalance(
guild.balance,
'create_guild',
'',
`Guild Bank refund for ${guild.name} (${guild._id})`,
);
}
return guild.updateOne({ $set: { balance: 0 } }).exec();
}
export default async function processGroups () {
const guildFields = {
_id: 1,
balance: 1,
leader: 1,
name: 1,
purchased: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const foundGroups = await Group // eslint-disable-line no-await-in-loop
.find(guildsQuery)
.limit(guildsPerRun)
.sort({ _id: 1 })
.select(guildFields)
.exec();
if (foundGroups.length === 0) {
console.warn('All appropriate Guilds found and modified.');
console.warn(`\n${count} Guilds processed\n`);
break;
} else {
guildsQuery._id = {
$gt: foundGroups[foundGroups.length - 1],
};
}
await Promise.all(foundGroups.map(guild => updateGroup(guild))); // eslint-disable-line no-await-in-loop
}
};

View File

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

View File

@@ -1,144 +0,0 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20230808_veteran_pet_ladder';
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
const set = {};
let push = { notifications: { $each: [] }};
set.migration = MIGRATION_NAME;
if (user.items.pets['Fox-Veteran']) {
set['items.pets.Dragon-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_dragon',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Dragon.',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user.items.pets['Bear-Veteran']) {
set['items.pets.Fox-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_fox',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Fox.',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user.items.pets['Lion-Veteran']) {
set['items.pets.Bear-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_bear',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Bear.',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user.items.pets['Tiger-Veteran']) {
set['items.pets.Lion-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_lion',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Lion.',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user.items.pets['Wolf-Veteran']) {
set['items.pets.Tiger-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_tiger',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Tiger.',
destination: '/inventory/stable',
},
seen: false,
});
} else {
set['items.pets.Wolf-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_wolf',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Wolf.',
destination: '/inventory/stable',
},
seen: false,
});
}
if (user.contributor.level > 0) {
set['items.gear.owned.armor_special_heroicTunic'] = true;
set['items.gear.owned.back_special_heroicAureole'] = true;
set['items.gear.owned.headAccessory_special_heroicCirclet'] = true;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'heroic_set_icon',
title: 'Youve received the Heroic Set!',
text: 'To commemorate your hard work as a contributor, weve awarded you the Heroic Circlet, Heroic Aureole, and Heroic Tunic.',
destination: '/inventory/equipment',
},
seen: false,
});
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.update({_id: user._id}, {$set: set, $push: push}).exec();
}
export default async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
// 'auth.timestamps.loggedin': { $gt: new Date('2023-07-08') },
};
const fields = {
_id: 1,
items: 1,
migration: 1,
contributor: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.lean()
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1],
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

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

3425
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,22 @@
{ {
"name": "habitica", "name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.", "description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "5.0.1", "version": "4.251.0",
"main": "./website/server/index.js", "main": "./website/server/index.js",
"dependencies": { "dependencies": {
"@babel/core": "^7.22.5", "@babel/core": "^7.19.6",
"@babel/preset-env": "^7.22.5", "@babel/preset-env": "^7.20.2",
"@babel/register": "^7.22.5", "@babel/register": "^7.18.9",
"@google-cloud/trace-agent": "^7.1.2", "@google-cloud/trace-agent": "^7.1.2",
"@parse/node-apn": "^5.1.3", "@parse/node-apn": "^5.1.3",
"@slack/webhook": "^6.1.0", "@slack/webhook": "^6.1.0",
"accepts": "^1.3.8", "accepts": "^1.3.8",
"amazon-payments": "^0.2.9", "amazon-payments": "^0.2.9",
"amplitude": "^6.0.0", "amplitude": "^6.0.0",
"apidoc": "^0.54.0", "apidoc": "^0.53.1",
"apple-auth": "^1.0.9", "apple-auth": "^1.0.7",
"bcrypt": "^5.1.0", "bcrypt": "^5.1.0",
"body-parser": "^1.20.2", "body-parser": "^1.20.1",
"bootstrap": "^4.6.0", "bootstrap": "^4.6.0",
"compression": "^1.7.4", "compression": "^1.7.4",
"cookie-session": "^2.0.0", "cookie-session": "^2.0.0",
@@ -30,7 +30,7 @@
"express": "^4.18.2", "express": "^4.18.2",
"express-basic-auth": "^1.2.1", "express-basic-auth": "^1.2.1",
"express-validator": "^5.2.0", "express-validator": "^5.2.0",
"glob": "^8.1.0", "glob": "^8.0.3",
"got": "^11.8.3", "got": "^11.8.3",
"gulp": "^4.0.0", "gulp": "^4.0.0",
"gulp-babel": "^8.0.0", "gulp-babel": "^8.0.0",
@@ -42,7 +42,7 @@
"image-size": "^1.0.2", "image-size": "^1.0.2",
"in-app-purchase": "^1.11.3", "in-app-purchase": "^1.11.3",
"js2xmlparser": "^5.0.0", "js2xmlparser": "^5.0.0",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^8.5.1",
"jwks-rsa": "^2.1.5", "jwks-rsa": "^2.1.5",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"merge-stream": "^2.0.0", "merge-stream": "^2.0.0",
@@ -54,7 +54,7 @@
"nconf": "^0.12.0", "nconf": "^0.12.0",
"node-gcm": "^1.0.5", "node-gcm": "^1.0.5",
"on-headers": "^1.0.2", "on-headers": "^1.0.2",
"passport": "^0.5.0", "passport": "^0.6.0",
"passport-facebook": "^3.0.0", "passport-facebook": "^3.0.0",
"passport-google-oauth2": "^0.2.0", "passport-google-oauth2": "^0.2.0",
"passport-google-oauth20": "2.0.0", "passport-google-oauth20": "2.0.0",
@@ -67,16 +67,16 @@
"remove-markdown": "^0.5.0", "remove-markdown": "^0.5.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"short-uuid": "^4.2.2", "short-uuid": "^4.2.2",
"stripe": "^12.9.0", "stripe": "^10.13.0",
"superagent": "^8.0.9", "superagent": "^8.0.5",
"universal-analytics": "^0.5.3", "universal-analytics": "^0.5.3",
"useragent": "^2.1.9", "useragent": "^2.1.9",
"uuid": "^9.0.0", "uuid": "^8.3.2",
"validator": "^13.9.0", "validator": "^13.7.0",
"vinyl-buffer": "^1.0.1", "vinyl-buffer": "^1.0.1",
"winston": "^3.9.0", "winston": "^3.8.2",
"winston-loggly-bulk": "^3.2.1", "winston-loggly-bulk": "^3.2.1",
"xml2js": "^0.6.0" "xml2js": "^0.4.23"
}, },
"private": true, "private": true,
"engines": { "engines": {
@@ -110,11 +110,11 @@
"apidoc": "gulp apidoc" "apidoc": "gulp apidoc"
}, },
"devDependencies": { "devDependencies": {
"axios": "^1.3.6", "axios": "^0.27.2",
"chai": "^4.3.7", "chai": "^4.3.7",
"chai-as-promised": "^7.1.1", "chai-as-promised": "^7.1.1",
"chai-moment": "^0.1.0", "chai-moment": "^0.1.0",
"chalk": "^5.2.0", "chalk": "^5.1.2",
"cross-spawn": "^7.0.3", "cross-spawn": "^7.0.3",
"expect.js": "^0.3.1", "expect.js": "^0.3.1",
"istanbul": "^1.1.0-alpha.1", "istanbul": "^1.1.0-alpha.1",
@@ -122,7 +122,7 @@
"monk": "^7.3.4", "monk": "^7.3.4",
"require-again": "^2.0.0", "require-again": "^2.0.0",
"run-rs": "^0.7.7", "run-rs": "^0.7.7",
"sinon": "^15.1.2", "sinon": "^14.0.2",
"sinon-chai": "^3.7.0", "sinon-chai": "^3.7.0",
"sinon-stub-promise": "^4.0.0" "sinon-stub-promise": "^4.0.0"
}, },

View File

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

View File

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

View File

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

View File

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

View File

@@ -203,28 +203,6 @@ describe('payments/index', () => {
expect(recipient.purchased.plan.dateCreated).to.exist; expect(recipient.purchased.plan.dateCreated).to.exist;
}); });
it('sets plan.dateCurrentTypeCreated if it did not previously exist', async () => {
expect(recipient.purchased.plan.dateCurrentTypeCreated).to.not.exist;
await api.createSubscription(data);
expect(recipient.purchased.plan.dateCurrentTypeCreated).to.exist;
});
it('keeps plan.dateCreated when changing subscription type', async () => {
await api.createSubscription(data);
const initialDate = recipient.purchased.plan.dateCreated;
await api.createSubscription(data);
expect(recipient.purchased.plan.dateCreated).to.eql(initialDate);
});
it('sets plan.dateCurrentTypeCreated when changing subscription type', async () => {
await api.createSubscription(data);
const initialDate = recipient.purchased.plan.dateCurrentTypeCreated;
await api.createSubscription(data);
expect(recipient.purchased.plan.dateCurrentTypeCreated).to.not.eql(initialDate);
});
it('does not change plan.customerId if it already exists', async () => { it('does not change plan.customerId if it already exists', async () => {
recipient.purchased.plan = plan; recipient.purchased.plan = plan;
data.customerId = 'purchaserCustomerId'; data.customerId = 'purchaserCustomerId';
@@ -235,116 +213,6 @@ describe('payments/index', () => {
expect(recipient.purchased.plan.customerId).to.eql('customer-id'); expect(recipient.purchased.plan.customerId).to.eql('customer-id');
}); });
it('sets plan.perkMonthCount to 1 if user is not subscribed', async () => {
recipient.purchased.plan = plan;
recipient.purchased.plan.perkMonthCount = 1;
recipient.purchased.plan.customerId = undefined;
data.sub.key = 'basic_earned';
data.gift.subscription.key = 'basic_earned';
data.gift.subscription.months = 1;
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
await api.createSubscription(data);
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
});
it('sets plan.perkMonthCount to 1 if field is not initialized', async () => {
recipient.purchased.plan = plan;
recipient.purchased.plan.perkMonthCount = -1;
recipient.purchased.plan.customerId = undefined;
data.sub.key = 'basic_earned';
data.gift.subscription.key = 'basic_earned';
data.gift.subscription.months = 1;
expect(recipient.purchased.plan.perkMonthCount).to.eql(-1);
await api.createSubscription(data);
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
});
it('sets plan.perkMonthCount to 1 if user had previous count but lapsed subscription', async () => {
recipient.purchased.plan = plan;
recipient.purchased.plan.perkMonthCount = 2;
recipient.purchased.plan.customerId = undefined;
data.sub.key = 'basic_earned';
data.gift.subscription.key = 'basic_earned';
data.gift.subscription.months = 1;
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
await api.createSubscription(data);
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
});
it('adds to plan.perkMonthCount if user is already subscribed', async () => {
recipient.purchased.plan = plan;
recipient.purchased.plan.perkMonthCount = 1;
data.sub.key = 'basic_earned';
data.gift.subscription.key = 'basic_earned';
data.gift.subscription.months = 1;
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
await api.createSubscription(data);
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
});
it('awards perks if plan.perkMonthCount reaches 3 with existing subscription', async () => {
recipient.purchased.plan = plan;
recipient.purchased.plan.perkMonthCount = 2;
data.sub.key = 'basic_earned';
data.gift.subscription.key = 'basic_earned';
data.gift.subscription.months = 1;
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
await api.createSubscription(data);
expect(recipient.purchased.plan.perkMonthCount).to.eql(0);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(5);
});
it('awards perks if plan.perkMonthCount reaches 3 without existing subscription', async () => {
recipient.purchased.plan.perkMonthCount = 0;
expect(recipient.purchased.plan.perkMonthCount).to.eql(0);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
await api.createSubscription(data);
expect(recipient.purchased.plan.perkMonthCount).to.eql(0);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(5);
});
it('awards perks if plan.perkMonthCount reaches 3 without initialized field', async () => {
expect(recipient.purchased.plan.perkMonthCount).to.eql(-1);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
await api.createSubscription(data);
expect(recipient.purchased.plan.perkMonthCount).to.eql(0);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(5);
});
it('awards perks if plan.perkMonthCount goes over 3', async () => {
recipient.purchased.plan = plan;
recipient.purchased.plan.perkMonthCount = 2;
data.sub.key = 'basic_earned';
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
await api.createSubscription(data);
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(5);
});
it('sets plan.customerId to "Gift" if it does not already exist', async () => { it('sets plan.customerId to "Gift" if it does not already exist', async () => {
expect(recipient.purchased.plan.customerId).to.not.exist; expect(recipient.purchased.plan.customerId).to.not.exist;
@@ -511,7 +379,6 @@ describe('payments/index', () => {
expect(user.purchased.plan.customerId).to.eql('customer-id'); expect(user.purchased.plan.customerId).to.eql('customer-id');
expect(user.purchased.plan.dateUpdated).to.exist; expect(user.purchased.plan.dateUpdated).to.exist;
expect(user.purchased.plan.gemsBought).to.eql(0); expect(user.purchased.plan.gemsBought).to.eql(0);
expect(user.purchased.plan.perkMonthCount).to.eql(0);
expect(user.purchased.plan.paymentMethod).to.eql('Payment Method'); expect(user.purchased.plan.paymentMethod).to.eql('Payment Method');
expect(user.purchased.plan.extraMonths).to.eql(0); expect(user.purchased.plan.extraMonths).to.eql(0);
expect(user.purchased.plan.dateTerminated).to.eql(null); expect(user.purchased.plan.dateTerminated).to.eql(null);
@@ -519,63 +386,6 @@ describe('payments/index', () => {
expect(user.purchased.plan.dateCreated).to.exist; expect(user.purchased.plan.dateCreated).to.exist;
}); });
it('sets plan.dateCreated if it did not previously exist', async () => {
expect(user.purchased.plan.dateCreated).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.dateCreated).to.exist;
});
it('sets plan.dateCurrentTypeCreated if it did not previously exist', async () => {
expect(user.purchased.plan.dateCurrentTypeCreated).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.dateCurrentTypeCreated).to.exist;
});
it('keeps plan.dateCreated when changing subscription type', async () => {
await api.createSubscription(data);
const initialDate = user.purchased.plan.dateCreated;
await api.createSubscription(data);
expect(user.purchased.plan.dateCreated).to.eql(initialDate);
});
it('sets plan.dateCurrentTypeCreated when changing subscription type', async () => {
await api.createSubscription(data);
const initialDate = user.purchased.plan.dateCurrentTypeCreated;
await api.createSubscription(data);
expect(user.purchased.plan.dateCurrentTypeCreated).to.not.eql(initialDate);
});
it('keeps plan.perkMonthCount when changing subscription type', async () => {
await api.createSubscription(data);
user.purchased.plan.perkMonthCount = 2;
await api.createSubscription(data);
expect(user.purchased.plan.perkMonthCount).to.eql(2);
});
it('sets plan.perkMonthCount to zero when creating new monthly subscription', async () => {
user.purchased.plan.perkMonthCount = 2;
await api.createSubscription(data);
expect(user.purchased.plan.perkMonthCount).to.eql(0);
});
it('sets plan.perkMonthCount to zero when creating new 3 month subscription', async () => {
user.purchased.plan.perkMonthCount = 2;
await api.createSubscription(data);
expect(user.purchased.plan.perkMonthCount).to.eql(0);
});
it('updates plan.consecutive.offset when changing subscription type', async () => {
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.offset).to.eql(3);
data.sub.key = 'basic_6mo';
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.offset).to.eql(6);
});
it('awards the Royal Purple Jackalope pet', async () => { it('awards the Royal Purple Jackalope pet', async () => {
await api.createSubscription(data); await api.createSubscription(data);
@@ -655,89 +465,6 @@ describe('payments/index', () => {
}, },
}); });
}); });
context('Upgrades subscription', () => {
it('from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.customerId).to.eql('customer-id');
const created = user.purchased.plan.dateCreated;
const updated = user.purchased.plan.dateUpdated;
data.sub.key = 'basic_6mo';
data.updatedFrom = { key: 'basic_earned' };
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.dateCreated).to.eql(created);
expect(user.purchased.plan.dateUpdated).to.not.eql(updated);
expect(user.purchased.plan.customerId).to.eql('customer-id');
});
it('from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.customerId).to.eql('customer-id');
const created = user.purchased.plan.dateCreated;
const updated = user.purchased.plan.dateUpdated;
data.sub.key = 'basic_12mo';
data.updatedFrom = { key: 'basic_3mo' };
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.dateCreated).to.eql(created);
expect(user.purchased.plan.dateUpdated).to.not.eql(updated);
expect(user.purchased.plan.customerId).to.eql('customer-id');
});
});
context('Downgrades subscription', () => {
it('from basic_6mo to basic_earned', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.customerId).to.eql('customer-id');
const created = user.purchased.plan.dateCreated;
const updated = user.purchased.plan.dateUpdated;
data.sub.key = 'basic_earned';
data.updatedFrom = { key: 'basic_6mo' };
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.dateCreated).to.eql(created);
expect(user.purchased.plan.dateUpdated).to.not.eql(updated);
expect(user.purchased.plan.customerId).to.eql('customer-id');
});
it('from basic_12mo to basic_3mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
data.sub.key = 'basic_12mo';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.customerId).to.eql('customer-id');
const created = user.purchased.plan.dateCreated;
const updated = user.purchased.plan.dateUpdated;
data.sub.key = 'basic_3mo';
data.updatedFrom = { key: 'basic_12mo' };
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.dateCreated).to.eql(created);
expect(user.purchased.plan.dateUpdated).to.not.eql(updated);
expect(user.purchased.plan.customerId).to.eql('customer-id');
});
});
}); });
context('Block subscription perks', () => { context('Block subscription perks', () => {
@@ -748,19 +475,9 @@ describe('payments/index', () => {
}); });
it('does not add to plans.consecutive.offset if 1 month subscription', async () => { it('does not add to plans.consecutive.offset if 1 month subscription', async () => {
data.sub.key = 'basic_earned';
await api.createSubscription(data); await api.createSubscription(data);
expect(user.purchased.plan.consecutive.offset).to.eql(0); expect(user.purchased.plan.extraMonths).to.eql(0);
});
it('resets plans.consecutive.offset if 1 month subscription', async () => {
user.purchased.plan.consecutive.offset = 1;
await user.save();
data.sub.key = 'basic_earned';
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.offset).to.eql(0);
}); });
it('adds 5 to plan.consecutive.gemCapExtra for 3 month block', async () => { it('adds 5 to plan.consecutive.gemCapExtra for 3 month block', async () => {
@@ -771,6 +488,7 @@ describe('payments/index', () => {
it('adds 10 to plan.consecutive.gemCapExtra for 6 month block', async () => { it('adds 10 to plan.consecutive.gemCapExtra for 6 month block', async () => {
data.sub.key = 'basic_6mo'; data.sub.key = 'basic_6mo';
await api.createSubscription(data); await api.createSubscription(data);
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10); expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
@@ -778,6 +496,7 @@ describe('payments/index', () => {
it('adds 20 to plan.consecutive.gemCapExtra for 12 month block', async () => { it('adds 20 to plan.consecutive.gemCapExtra for 12 month block', async () => {
data.sub.key = 'basic_12mo'; data.sub.key = 'basic_12mo';
await api.createSubscription(data); await api.createSubscription(data);
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20); expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
@@ -813,532 +532,6 @@ describe('payments/index', () => {
expect(user.purchased.plan.consecutive.trinkets).to.eql(4); expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
}); });
context('Upgrades subscription', () => {
context('Using payDifference logic', () => {
beforeEach(async () => {
data.updatedFrom = { logic: 'payDifference' };
});
it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
});
it('Adds 15 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_3mo';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
});
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
});
it('Adds 2 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_6mo';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
});
it('Adds 3 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_3mo';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
});
});
context('Using payFull logic', () => {
beforeEach(async () => {
data.updatedFrom = { logic: 'payFull' };
});
it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
});
it('Adds 20 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_3mo';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(25);
});
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
});
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_6mo';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(6);
});
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_3mo';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(5);
});
});
context('Using refundAndRepay logic', () => {
let clock;
beforeEach(async () => {
clock = sinon.useFakeTimers(new Date('2022-01-01'));
data.updatedFrom = { logic: 'refundAndRepay' };
});
context('Upgrades within first half of subscription', () => {
it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-01-10'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
});
it('Adds 15 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_3mo';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-02-05'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
});
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-01-08'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
});
it('Adds 3 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_3mo';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-01-31'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
});
it('Adds 2 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_6mo';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-01-28'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
});
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo after initial cycle', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
clock.restore();
clock = sinon.useFakeTimers(new Date('2024-01-08'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
});
it('Adds 2 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo after initial cycle', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_6mo';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-08-28'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
});
it('Adds 3 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo after initial cycle', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_3mo';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-07-31'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
});
});
context('Upgrades within second half of subscription', () => {
it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-01-20'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
});
it('Adds 20 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_3mo';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-02-24'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(25);
});
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-01-28'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
});
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_6mo';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-05-28'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(6);
});
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_3mo';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-03-03'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(5);
});
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo after initial cycle', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-05-28'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
});
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo after initial cycle', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_6mo';
clock.restore();
clock = sinon.useFakeTimers(new Date('2023-05-28'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(6);
});
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo after initial cycle', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_3mo';
clock.restore();
clock = sinon.useFakeTimers(new Date('2023-09-03'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(5);
});
});
afterEach(async () => {
if (clock !== null) clock.restore();
});
});
});
context('Downgrades subscription', () => {
it('does not remove from plan.consecutive.gemCapExtra from basic_6mo to basic_earned', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
data.sub.key = 'basic_earned';
data.updatedFrom = { key: 'basic_6mo' };
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
});
it('does not remove from plan.consecutive.gemCapExtra from basic_12mo to basic_3mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
data.sub.key = 'basic_12mo';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
data.sub.key = 'basic_3mo';
data.updatedFrom = { key: 'basic_12mo' };
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
});
it('does not remove from plan.consecutive.trinkets from basic_6mo to basic_earned', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
data.sub.key = 'basic_earned';
data.updatedFrom = { key: 'basic_6mo' };
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
});
it('does not remove from plan.consecutive.trinkets from basic_12mo to basic_3mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
data.sub.key = 'basic_12mo';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
data.sub.key = 'basic_3mo';
data.updatedFrom = { key: 'basic_12mo' };
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
});
});
}); });
context('Mystery Items', () => { context('Mystery Items', () => {

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

@@ -242,7 +242,7 @@ describe('cron middleware', () => {
sandbox.spy(cronLib, 'recoverCron'); sandbox.spy(cronLib, 'recoverCron');
sandbox.stub(User, 'updateOne') sandbox.stub(User, 'update')
.withArgs({ .withArgs({
_id: user._id, _id: user._id,
$or: [ $or: [

View File

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

View File

@@ -16,7 +16,60 @@ describe('GET /challenges/:challengeId', () => {
}); });
}); });
context('Group Plan', () => { context('public guild', () => {
let groupLeader;
let group;
let challenge;
let user;
beforeEach(async () => {
user = await generateUser();
const populatedGroup = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'public' },
});
groupLeader = populatedGroup.groupLeader;
group = populatedGroup.group;
challenge = await generateChallenge(groupLeader, group);
await groupLeader.post(`/challenges/${challenge._id}/join`);
});
it('should return challenge data', async () => {
await challenge.sync();
const chal = await user.get(`/challenges/${challenge._id}`);
expect(chal.memberCount).to.equal(challenge.memberCount);
expect(chal.name).to.equal(challenge.name);
expect(chal._id).to.equal(challenge._id);
expect(chal.leader).to.eql({
_id: groupLeader._id,
id: groupLeader._id,
profile: { name: groupLeader.profile.name },
auth: {
local: {
username: groupLeader.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
expect(chal.group).to.eql({
_id: group._id,
categories: [],
id: group.id,
name: group.name,
summary: group.name,
type: group.type,
privacy: group.privacy,
leader: groupLeader.id,
});
});
});
context('private guild', () => {
let groupLeader; let groupLeader;
let challengeLeader; let challengeLeader;
let group; let group;
@@ -31,14 +84,14 @@ describe('GET /challenges/:challengeId', () => {
const populatedGroup = await createAndPopulateGroup({ const populatedGroup = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' }, groupDetails: { type: 'guild', privacy: 'private' },
members: 2, members: 2,
upgradeToGroupPlan: true,
}); });
groupLeader = populatedGroup.groupLeader; groupLeader = populatedGroup.groupLeader;
group = populatedGroup.group; group = populatedGroup.group;
members = populatedGroup.members; members = populatedGroup.members;
[challengeLeader, otherMember] = members; challengeLeader = members[0]; // eslint-disable-line prefer-destructuring
otherMember = members[1]; // eslint-disable-line prefer-destructuring
challenge = await generateChallenge(challengeLeader, group); challenge = await generateChallenge(challengeLeader, group);
}); });

View File

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

View File

@@ -72,6 +72,20 @@ describe('GET /challenges/:challengeId/members/:memberId', () => {
}); });
}); });
it('works with challenges belonging to a public guild', async () => {
const groupLeader = await generateUser({ balance: 4 });
const group = await generateGroup(groupLeader, { type: 'guild', privacy: 'public', name: generateUUID() });
const challenge = await generateChallenge(groupLeader, group);
await groupLeader.post(`/challenges/${challenge._id}/join`);
const taskText = 'Test Text';
await groupLeader.post(`/tasks/challenge/${challenge._id}`, [{ type: 'habit', text: taskText }]);
const memberProgress = await user.get(`/challenges/${challenge._id}/members/${groupLeader._id}`);
expect(memberProgress).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile', 'tasks']);
expect(memberProgress.profile).to.have.all.keys(['name']);
expect(memberProgress.tasks.length).to.equal(1);
});
it('returns the member tasks for the challenges', async () => { it('returns the member tasks for the challenges', async () => {
const group = await generateGroup(user, { type: 'party', name: generateUUID() }); const group = await generateGroup(user, { type: 'party', name: generateUUID() });
const challenge = await generateChallenge(user, group); const challenge = await generateChallenge(user, group);

View File

@@ -7,7 +7,117 @@ import {
import { TAVERN_ID } from '../../../../../website/common/script/constants'; import { TAVERN_ID } from '../../../../../website/common/script/constants';
describe('GET challenges/groups/:groupId', () => { describe('GET challenges/groups/:groupId', () => {
context('Group Plan', () => { context('Public Guild', () => {
let publicGuild; let user; let nonMember; let challenge; let
challenge2;
before(async () => {
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'TestGuild',
type: 'guild',
privacy: 'public',
},
});
publicGuild = group;
user = groupLeader;
nonMember = await generateUser();
challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
challenge2 = await generateChallenge(user, group);
await user.post(`/challenges/${challenge2._id}/join`);
});
it('should return group challenges for non member with populated leader', async () => {
const challenges = await nonMember.get(`/challenges/groups/${publicGuild._id}`);
const foundChallenge1 = _.find(challenges, { _id: challenge._id });
expect(foundChallenge1).to.exist;
expect(foundChallenge1.leader).to.eql({
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: { name: user.profile.name },
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
const foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundChallenge2).to.exist;
expect(foundChallenge2.leader).to.eql({
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: { name: user.profile.name },
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
});
it('should return group challenges for member with populated leader', async () => {
const challenges = await user.get(`/challenges/groups/${publicGuild._id}`);
const foundChallenge1 = _.find(challenges, { _id: challenge._id });
expect(foundChallenge1).to.exist;
expect(foundChallenge1.leader).to.eql({
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: { name: user.profile.name },
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
const foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundChallenge2).to.exist;
expect(foundChallenge2.leader).to.eql({
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: { name: user.profile.name },
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
});
it('should return newest challenges first', async () => {
let challenges = await user.get(`/challenges/groups/${publicGuild._id}`);
let foundChallengeIndex = _.findIndex(challenges, { _id: challenge2._id });
expect(foundChallengeIndex).to.eql(0);
const newChallenge = await generateChallenge(user, publicGuild);
await user.post(`/challenges/${newChallenge._id}/join`);
challenges = await user.get(`/challenges/groups/${publicGuild._id}`);
foundChallengeIndex = _.findIndex(challenges, { _id: newChallenge._id });
expect(foundChallengeIndex).to.eql(0);
});
});
context('Private Guild', () => {
let privateGuild; let user; let nonMember; let challenge; let let privateGuild; let user; let nonMember; let challenge; let
challenge2; challenge2;
@@ -18,7 +128,6 @@ describe('GET challenges/groups/:groupId', () => {
type: 'guild', type: 'guild',
privacy: 'private', privacy: 'private',
}, },
upgradeToGroupPlan: true,
}); });
privateGuild = group; privateGuild = group;
@@ -77,6 +186,68 @@ describe('GET challenges/groups/:groupId', () => {
}); });
}); });
context('official challenge is present', () => {
let publicGuild; let user; let officialChallenge; let unofficialChallenges;
before(async () => {
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'TestGuild',
type: 'guild',
privacy: 'public',
},
});
user = groupLeader;
publicGuild = group;
await user.update({
'permissions.challengeAdmin': true,
});
officialChallenge = await generateChallenge(user, group, {
categories: [{
name: 'habitica_official',
slug: 'habitica_official',
}],
});
await user.post(`/challenges/${officialChallenge._id}/join`);
// We add 10 extra challenges to test whether the official challenge
// (the oldest) makes it to the front page.
unofficialChallenges = [];
for (let i = 0; i < 10; i += 1) {
const challenge = await generateChallenge(user, group); // eslint-disable-line
await user.post(`/challenges/${challenge._id}/join`); // eslint-disable-line
unofficialChallenges.push(challenge);
}
});
it('should return official challenges first', async () => {
const challenges = await user.get(`/challenges/groups/${publicGuild._id}`);
const foundChallengeIndex = _.findIndex(challenges, { _id: officialChallenge._id });
expect(foundChallengeIndex).to.eql(0);
});
it('should return newest challenges first, after official ones', async () => {
let challenges = await user.get(`/challenges/groups/${publicGuild._id}`);
unofficialChallenges.forEach((chal, index) => {
const foundChallengeIndex = _.findIndex(challenges, { _id: chal._id });
expect(foundChallengeIndex).to.eql(10 - index);
});
const newChallenge = await generateChallenge(user, publicGuild);
await user.post(`/challenges/${newChallenge._id}/join`);
challenges = await user.get(`/challenges/groups/${publicGuild._id}`);
const foundChallengeIndex = _.findIndex(challenges, { _id: newChallenge._id });
expect(foundChallengeIndex).to.eql(1);
});
});
context('Party', () => { context('Party', () => {
let party; let user; let nonMember; let challenge; let let party; let user; let nonMember; let challenge; let
challenge2; challenge2;
@@ -230,7 +401,7 @@ describe('GET challenges/groups/:groupId', () => {
}); });
}); });
it('should return tavern challenges using ID "habitrpg"', async () => { it('should return tavern challenges using ID "habitrpg', async () => {
const challenges = await user.get('/challenges/groups/habitrpg'); const challenges = await user.get('/challenges/groups/habitrpg');
const foundChallenge1 = _.find(challenges, { _id: challenge._id }); const foundChallenge1 = _.find(challenges, { _id: challenge._id });
@@ -264,58 +435,5 @@ describe('GET challenges/groups/:groupId', () => {
}, },
}); });
}); });
context('official challenge is present', () => {
let officialChallenge; let unofficialChallenges;
before(async () => {
await user.update({
'permissions.challengeAdmin': true,
balance: 3,
});
officialChallenge = await generateChallenge(user, tavern, {
categories: [{
name: 'habitica_official',
slug: 'habitica_official',
}],
prize: 1,
});
await user.post(`/challenges/${officialChallenge._id}/join`);
// We add 10 extra challenges to test whether the official challenge
// (the oldest) makes it to the front page.
unofficialChallenges = [];
for (let i = 0; i < 10; i += 1) {
const challenge = await generateChallenge(user, tavern, { prize: 1 }); // eslint-disable-line
await user.post(`/challenges/${challenge._id}/join`); // eslint-disable-line
unofficialChallenges.push(challenge);
}
});
it('should return official challenges first', async () => {
const challenges = await user.get('/challenges/groups/habitrpg');
const foundChallengeIndex = _.findIndex(challenges, { _id: officialChallenge._id });
expect(foundChallengeIndex).to.eql(0);
});
it('should return newest challenges first, after official ones', async () => {
let challenges = await user.get('/challenges/groups/habitrpg');
unofficialChallenges.forEach((chal, index) => {
const foundChallengeIndex = _.findIndex(challenges, { _id: chal._id });
expect(foundChallengeIndex).to.eql(10 - index);
});
const newChallenge = await generateChallenge(user, tavern, { prize: 1 });
await user.post(`/challenges/${newChallenge._id}/join`);
challenges = await user.get('/challenges/groups/habitrpg');
const foundChallengeIndex = _.findIndex(challenges, { _id: newChallenge._id });
expect(foundChallengeIndex).to.eql(1);
});
});
}); });
}); });

View File

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

View File

@@ -42,7 +42,26 @@ describe('POST /challenges', () => {
}); });
}); });
it('returns error when creating a challenge with summary with greater than MAX_SUMMARY_SIZE_FOR_CHALLENGES characters', async () => { it('returns error when creating a challenge in a public guild and you are not a member of it', async () => {
const user = await generateUser();
const { group } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'public',
},
});
await expect(user.post('/challenges', {
group: group._id,
prize: 4,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('mustBeGroupMember'),
});
});
it('return error when creating a challenge with summary with greater than MAX_SUMMARY_SIZE_FOR_CHALLENGES characters', async () => {
const user = await generateUser(); const user = await generateUser();
const summary = 'A'.repeat(MAX_SUMMARY_SIZE_FOR_CHALLENGES + 1); const summary = 'A'.repeat(MAX_SUMMARY_SIZE_FOR_CHALLENGES + 1);
const group = createAndPopulateGroup({ const group = createAndPopulateGroup({
@@ -58,7 +77,7 @@ describe('POST /challenges', () => {
}); });
}); });
context('creating a Challenge for a Group Plan', () => { context('Creating a challenge for a valid group', () => {
let groupLeader; let groupLeader;
let group; let group;
let groupMember; let groupMember;
@@ -75,11 +94,9 @@ describe('POST /challenges', () => {
challenges: true, challenges: true,
}, },
}, },
upgradeToGroupPlan: true,
}); });
groupLeader = await populatedGroup.groupLeader.sync(); groupLeader = await populatedGroup.groupLeader.sync();
await groupLeader.update({ permissions: {} });
group = populatedGroup.group; group = populatedGroup.group;
groupMember = populatedGroup.members[0]; // eslint-disable-line prefer-destructuring groupMember = populatedGroup.members[0]; // eslint-disable-line prefer-destructuring
}); });

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,27 +6,27 @@ import {
describe('POST /chat/:chatId/like', () => { describe('POST /chat/:chatId/like', () => {
let user; let user;
let anotherUser;
let groupWithChat; let groupWithChat;
let members;
const testMessage = 'Test Message'; const testMessage = 'Test Message';
let anotherUser;
before(async () => { before(async () => {
({ group: groupWithChat, groupLeader: user, members } = await createAndPopulateGroup({ const { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: { groupDetails: {
name: 'Test Guild', name: 'Test Guild',
type: 'guild', type: 'guild',
privacy: 'private', privacy: 'public',
}, },
members: 1, members: 1,
leaderDetails: { leaderDetails: {
'auth.timestamps.created': new Date('2022-01-01'), 'auth.timestamps.created': new Date('2022-01-01'),
balance: 10, balance: 10,
}, },
upgradeToGroupPlan: true, });
}));
[anotherUser] = members; user = groupLeader;
groupWithChat = group;
anotherUser = members[0]; // eslint-disable-line prefer-destructuring
await anotherUser.update({ 'auth.timestamps.created': new Date('2022-01-01') }); await anotherUser.update({ 'auth.timestamps.created': new Date('2022-01-01') });
}); });

View File

@@ -1,33 +1,41 @@
import { IncomingWebhook } from '@slack/webhook'; import { IncomingWebhook } from '@slack/webhook';
import nconf from 'nconf';
import { v4 as generateUUID } from 'uuid'; import { v4 as generateUUID } from 'uuid';
import { import {
createAndPopulateGroup, createAndPopulateGroup,
generateUser,
translate as t, translate as t,
sleep, sleep,
server, server,
} from '../../../../helpers/api-integration/v3'; } from '../../../../helpers/api-integration/v3';
import { import {
SPAM_MESSAGE_LIMIT,
SPAM_MIN_EXEMPT_CONTRIB_LEVEL, SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
TAVERN_ID,
} from '../../../../../website/server/models/group'; } from '../../../../../website/server/models/group';
import { MAX_MESSAGE_LENGTH } from '../../../../../website/common/script/constants'; import { CHAT_FLAG_FROM_SHADOW_MUTE, MAX_MESSAGE_LENGTH } from '../../../../../website/common/script/constants';
import * as email from '../../../../../website/server/libs/email'; import * as email from '../../../../../website/server/libs/email';
const BASE_URL = nconf.get('BASE_URL');
describe('POST /chat', () => { describe('POST /chat', () => {
let user; let groupWithChat; let member; let let user; let groupWithChat; let member; let
additionalMember; additionalMember;
const testMessage = 'Test Message'; const testMessage = 'Test Message';
const testBannedWordMessage = 'TESTPLACEHOLDERSWEARWORDHERE'; const testBannedWordMessage = 'TESTPLACEHOLDERSWEARWORDHERE';
const testBannedWordMessage1 = 'TESTPLACEHOLDERSWEARWORDHERE1';
const testSlurMessage = 'message with TESTPLACEHOLDERSLURWORDHERE'; const testSlurMessage = 'message with TESTPLACEHOLDERSLURWORDHERE';
const testSlurMessage1 = 'TESTPLACEHOLDERSLURWORDHERE1';
const bannedWordErrorMessage = t('bannedWordUsed', { swearWordsUsed: testBannedWordMessage });
before(async () => { before(async () => {
const { group, groupLeader, members } = await createAndPopulateGroup({ const { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: { groupDetails: {
name: 'Test Guild', name: 'Test Guild',
type: 'guild', type: 'guild',
privacy: 'private', privacy: 'public',
}, },
members: 2, members: 2,
upgradeToGroupPlan: true,
}); });
user = groupLeader; user = groupLeader;
await user.update({ await user.update({
@@ -35,7 +43,8 @@ describe('POST /chat', () => {
'auth.timestamps.created': new Date('2022-01-01'), 'auth.timestamps.created': new Date('2022-01-01'),
}); // prevent tests accidentally throwing messageGroupChatSpam }); // prevent tests accidentally throwing messageGroupChatSpam
groupWithChat = group; groupWithChat = group;
[member, additionalMember] = members; member = members[0]; // eslint-disable-line prefer-destructuring
additionalMember = members[1]; // eslint-disable-line prefer-destructuring
await member.update({ 'auth.timestamps.created': new Date('2022-01-01') }); await member.update({ 'auth.timestamps.created': new Date('2022-01-01') });
await additionalMember.update({ 'auth.timestamps.created': new Date('2022-01-01') }); await additionalMember.update({ 'auth.timestamps.created': new Date('2022-01-01') });
}); });
@@ -80,12 +89,32 @@ describe('POST /chat', () => {
member.update({ 'flags.chatRevoked': false }); member.update({ 'flags.chatRevoked': false });
}); });
it('returns an error when chat privileges are revoked when sending a message to a public guild', async () => {
const userWithChatRevoked = await member.update({ 'flags.chatRevoked': true });
await expect(userWithChatRevoked.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage })).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('chatPrivilegesRevoked'),
});
});
it('does not error when chat privileges are revoked when sending a message to a private guild', async () => { it('does not error when chat privileges are revoked when sending a message to a private guild', async () => {
await member.update({ const { group, members } = await createAndPopulateGroup({
'flags.chatRevoked': true, groupDetails: {
name: 'Private Guild',
type: 'guild',
privacy: 'private',
},
members: 1,
}); });
const message = await member.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage }); const privateGuildMemberWithChatsRevoked = members[0];
await privateGuildMemberWithChatsRevoked.update({
'flags.chatRevoked': true,
'auth.timestamps.created': new Date('2022-01-01'),
});
const message = await privateGuildMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage });
expect(message.message.id).to.exist; expect(message.message.id).to.exist;
}); });
@@ -123,12 +152,54 @@ describe('POST /chat', () => {
member.update({ 'flags.chatShadowMuted': false }); member.update({ 'flags.chatShadowMuted': false });
}); });
it('creates a chat with flagCount already set and notifies mods when sending a message to a public guild', async () => {
const userWithChatShadowMuted = await member.update({ 'flags.chatShadowMuted': true });
const message = await userWithChatShadowMuted.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
expect(message.message.id).to.exist;
expect(message.message.flagCount).to.eql(CHAT_FLAG_FROM_SHADOW_MUTE);
// Email sent to mods
await sleep(0.5);
expect(email.sendTxn).to.be.calledOnce;
expect(email.sendTxn.args[0][1]).to.eql('shadow-muted-post-report-to-mods');
// Slack message to mods
expect(IncomingWebhook.prototype.send).to.be.calledOnce;
/* eslint-disable camelcase */
expect(IncomingWebhook.prototype.send).to.be.calledWith({
text: `@${member.auth.local.username} / ${member.profile.name} posted while shadow-muted`,
attachments: [{
fallback: 'Shadow-Muted Message',
color: 'danger',
author_name: `@${member.auth.local.username} ${member.profile.name} (${member.auth.local.email}; ${member._id})`,
title: 'Shadow-Muted Post in Test Guild',
title_link: `${BASE_URL}/groups/guild/${groupWithChat.id}`,
text: testMessage,
mrkdwn_in: [
'text',
],
}],
});
/* eslint-enable camelcase */
});
it('creates a chat with zero flagCount when sending a message to a private guild', async () => { it('creates a chat with zero flagCount when sending a message to a private guild', async () => {
await member.update({ const { group, members } = await createAndPopulateGroup({
'flags.chatShadowMuted': true, groupDetails: {
name: 'Private Guild',
type: 'guild',
privacy: 'private',
},
members: 1,
}); });
const message = await member.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage }); const userWithChatShadowMuted = members[0];
await userWithChatShadowMuted.update({
'flags.chatShadowMuted': true,
'auth.timestamps.created': new Date('2022-01-01'),
});
const message = await userWithChatShadowMuted.post(`/groups/${group._id}/chat`, { message: testMessage });
expect(message.message.id).to.exist; expect(message.message.id).to.exist;
expect(message.message.flagCount).to.eql(0); expect(message.message.flagCount).to.eql(0);
@@ -155,9 +226,100 @@ describe('POST /chat', () => {
expect(message.message.id).to.exist; expect(message.message.id).to.exist;
expect(message.message.flagCount).to.eql(0); expect(message.message.flagCount).to.eql(0);
}); });
it('creates a chat with zero flagCount when non-shadow-muted user sends a message to a public guild', async () => {
const message = await member.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
expect(message.message.id).to.exist;
expect(message.message.flagCount).to.eql(0);
});
}); });
context('banned word', () => { context('banned word', () => {
it('returns an error when chat message contains a banned word in tavern', async () => {
await expect(user.post('/groups/habitrpg/chat', { message: testBannedWordMessage }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: bannedWordErrorMessage,
});
});
it('returns an error when chat message contains a banned word in a public guild', async () => {
const { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'public guild',
type: 'guild',
privacy: 'public',
},
members: 1,
});
await expect(members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: bannedWordErrorMessage,
});
});
it('errors when word is part of a phrase', async () => {
const wordInPhrase = `phrase ${testBannedWordMessage} end`;
await expect(user.post('/groups/habitrpg/chat', { message: wordInPhrase }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: bannedWordErrorMessage,
});
});
it('errors when word is surrounded by non alphabet characters', async () => {
const wordInPhrase = `_!${testBannedWordMessage}@_`;
await expect(user.post('/groups/habitrpg/chat', { message: wordInPhrase }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: bannedWordErrorMessage,
});
});
it('errors when word is typed in mixed case', async () => {
const substrLength = Math.floor(testBannedWordMessage.length / 2);
const chatMessage = testBannedWordMessage.substring(0, substrLength).toLowerCase()
+ testBannedWordMessage.substring(substrLength).toUpperCase();
await expect(user.post('/groups/habitrpg/chat', { message: chatMessage }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedWordUsed', { swearWordsUsed: chatMessage }),
});
});
it('checks error message has all the banned words used, regardless of case', async () => {
const testBannedWords = [
testBannedWordMessage.toUpperCase(),
testBannedWordMessage1.toLowerCase(),
];
const chatMessage = `Mixing ${testBannedWords[0]} and ${testBannedWords[1]} is bad for you.`;
await expect(user.post('/groups/habitrpg/chat', { message: chatMessage }))
.to.eventually.be.rejected
.and.have.property('message')
.that.includes(testBannedWords.join(', '));
});
it('does not error when bad word is suffix of a word', async () => {
const wordAsSuffix = `prefix${testBannedWordMessage}`;
const message = await user.post('/groups/habitrpg/chat', { message: wordAsSuffix });
expect(message.message.id).to.exist;
});
it('does not error when bad word is prefix of a word', async () => {
const wordAsPrefix = `${testBannedWordMessage}suffix`;
const message = await user.post('/groups/habitrpg/chat', { message: wordAsPrefix });
expect(message.message.id).to.exist;
});
it('does not error when sending a chat message containing a banned word to a party', async () => { it('does not error when sending a chat message containing a banned word to a party', async () => {
const { group, members } = await createAndPopulateGroup({ const { group, members } = await createAndPopulateGroup({
groupDetails: { groupDetails: {
@@ -174,8 +336,37 @@ describe('POST /chat', () => {
expect(message.message.id).to.exist; expect(message.message.id).to.exist;
}); });
it('does not error when sending a chat message containing a banned word to a public guild in which banned words are allowed', async () => {
const { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'public guild',
type: 'guild',
privacy: 'public',
},
members: 1,
});
// Update the bannedWordsAllowed property for the group
group.update({ bannedWordsAllowed: true });
await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') });
const message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage });
expect(message.message.id).to.exist;
});
it('does not error when sending a chat message containing a banned word to a private guild', async () => { it('does not error when sending a chat message containing a banned word to a private guild', async () => {
const message = await member.post(`/groups/${groupWithChat._id}/chat`, { message: testBannedWordMessage }); const { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'private guild',
type: 'guild',
privacy: 'private',
},
members: 1,
});
await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') });
const message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage });
expect(message.message.id).to.exist; expect(message.message.id).to.exist;
}); });
@@ -192,6 +383,45 @@ describe('POST /chat', () => {
user.update({ 'flags.chatRevoked': false }); user.update({ 'flags.chatRevoked': false });
}); });
it('errors and revokes privileges when chat message contains a banned slur', async () => {
await expect(user.post(`/groups/${groupWithChat._id}/chat`, { message: testSlurMessage })).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedSlurUsed'),
});
// Email sent to mods
await sleep(0.5);
expect(email.sendTxn).to.be.calledOnce;
expect(email.sendTxn.args[0][1]).to.eql('slur-report-to-mods');
// Slack message to mods
expect(IncomingWebhook.prototype.send).to.be.calledOnce;
/* eslint-disable camelcase */
expect(IncomingWebhook.prototype.send).to.be.calledWith({
text: `${user.profile.name} (${user.id}) tried to post a slur`,
attachments: [{
fallback: 'Slur Message',
color: 'danger',
author_name: `@${user.auth.local.username} ${user.profile.name} (${user.auth.local.email}; ${user._id})`,
title: 'Slur in Test Guild',
title_link: `${BASE_URL}/groups/guild/${groupWithChat.id}`,
text: testSlurMessage,
mrkdwn_in: [
'text',
],
}],
});
/* eslint-enable camelcase */
// Chat privileges are revoked
await expect(user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage })).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('chatPrivilegesRevoked'),
});
});
it('allows slurs in private groups', async () => { it('allows slurs in private groups', async () => {
const { group, members } = await createAndPopulateGroup({ const { group, members } = await createAndPopulateGroup({
groupDetails: { groupDetails: {
@@ -207,17 +437,28 @@ describe('POST /chat', () => {
expect(message.message.id).to.exist; expect(message.message.id).to.exist;
}); });
it('errors when slur is typed in mixed case', async () => {
const substrLength = Math.floor(testSlurMessage1.length / 2);
const chatMessage = testSlurMessage1.substring(0, substrLength).toLowerCase()
+ testSlurMessage1.substring(substrLength).toUpperCase();
await expect(user.post('/groups/habitrpg/chat', { message: chatMessage }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedSlurUsed'),
});
});
}); });
it('errors when user account is too young', async () => { it('errors when user account is too young', async () => {
await user.update({ 'auth.timestamps.created': new Date() }); const brandNewUser = await generateUser();
await expect(user.post(`/groups/${groupWithChat._id}/chat`, { message: 'hi im new' })) await expect(brandNewUser.post('/groups/habitrpg/chat', { message: 'hi im new' }))
.to.eventually.be.rejected.and.eql({ .to.eventually.be.rejected.and.eql({
code: 400, code: 400,
error: 'BadRequest', error: 'BadRequest',
message: t('chatTemporarilyUnavailable'), message: t('chatTemporarilyUnavailable'),
}); });
await user.update({ 'auth.timestamps.created': new Date('2022-01-01') });
}); });
it('creates a chat', async () => { it('creates a chat', async () => {
@@ -278,43 +519,26 @@ describe('POST /chat', () => {
const mount = 'test-mount'; const mount = 'test-mount';
const pet = 'test-pet'; const pet = 'test-pet';
const style = 'test-style'; const style = 'test-style';
await user.update({ const userWithStyle = await generateUser({
'items.currentMount': mount, 'items.currentMount': mount,
'items.currentPet': pet, 'items.currentPet': pet,
'preferences.style': style, 'preferences.style': style,
'auth.timestamps.created': new Date('2022-01-01'),
}); });
await userWithStyle.sync();
const message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage }); const message = await userWithStyle.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
expect(message.message.id).to.exist; expect(message.message.id).to.exist;
expect(message.message.userStyles.items.currentMount).to.eql(user.items.currentMount); expect(message.message.userStyles.items.currentMount).to.eql(userWithStyle.items.currentMount);
expect(message.message.userStyles.items.currentPet).to.eql(user.items.currentPet); expect(message.message.userStyles.items.currentPet).to.eql(userWithStyle.items.currentPet);
expect(message.message.userStyles.preferences.style).to.eql(user.preferences.style); expect(message.message.userStyles.preferences.style).to.eql(userWithStyle.preferences.style);
expect(message.message.userStyles.preferences.hair).to.eql(user.preferences.hair); expect(message.message.userStyles.preferences.hair).to.eql(userWithStyle.preferences.hair);
expect(message.message.userStyles.preferences.skin).to.eql(user.preferences.skin); expect(message.message.userStyles.preferences.skin).to.eql(userWithStyle.preferences.skin);
expect(message.message.userStyles.preferences.shirt).to.eql(user.preferences.shirt); expect(message.message.userStyles.preferences.shirt).to.eql(userWithStyle.preferences.shirt);
expect(message.message.userStyles.preferences.chair).to.eql(user.preferences.chair); expect(message.message.userStyles.preferences.chair).to.eql(userWithStyle.preferences.chair);
expect(message.message.userStyles.preferences.background) expect(message.message.userStyles.preferences.background)
.to.eql(user.preferences.background); .to.eql(userWithStyle.preferences.background);
});
it('creates equipped to user styles', async () => {
const message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
expect(message.message.id).to.exist;
expect(message.message.userStyles.items.gear.equipped)
.to.eql(user.items.gear.equipped);
expect(message.message.userStyles.items.gear.costume).to.not.exist;
});
it('creates costume to user styles', async () => {
await user.update({ 'preferences.costume': true });
const message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
expect(message.message.id).to.exist;
expect(message.message.userStyles.items.gear.costume).to.eql(user.items.gear.costume);
expect(message.message.userStyles.items.gear.equipped).to.not.exist;
}); });
it('adds backer info to chat', async () => { it('adds backer info to chat', async () => {
@@ -323,11 +547,12 @@ describe('POST /chat', () => {
tier: 800, tier: 800,
tokensApplied: true, tokensApplied: true,
}; };
await user.update({ const backer = await generateUser({
backer: backerInfo, backer: backerInfo,
'auth.timestamps.created': new Date('2022-01-01'),
}); });
const message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage }); const message = await backer.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
const messageBackerInfo = message.message.backer; const messageBackerInfo = message.message.backer;
expect(messageBackerInfo.npc).to.equal(backerInfo.npc); expect(messageBackerInfo.npc).to.equal(backerInfo.npc);
@@ -407,5 +632,43 @@ describe('POST /chat', () => {
expect(memberWithNotification.newMessages[`${group._id}`]).to.exist; expect(memberWithNotification.newMessages[`${group._id}`]).to.exist;
expect(memberWithNotification.notifications.find(n => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === group._id)).to.exist; expect(memberWithNotification.notifications.find(n => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === group._id)).to.exist;
}); });
it('does not notify other users of a new message that is already hidden from shadow-muting', async () => {
await user.update({ 'flags.chatShadowMuted': true });
const message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
const memberWithNotification = await member.get('/user');
await user.update({ 'flags.chatShadowMuted': false });
expect(message.message.id).to.exist;
expect(memberWithNotification.newMessages[`${groupWithChat._id}`]).to.not.exist;
expect(memberWithNotification.notifications.find(n => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupWithChat._id)).to.not.exist;
});
});
context('Spam prevention', () => {
it('Returns an error when the user has been posting too many messages', async () => {
// Post as many messages are needed to reach the spam limit
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i += 1) {
const result = await additionalMember.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage }); // eslint-disable-line no-await-in-loop
expect(result.message.id).to.exist;
}
await expect(additionalMember.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage })).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageGroupChatSpam'),
});
});
it('contributor should not receive spam alert', async () => {
const userSocialite = await member.update({ 'contributor.level': SPAM_MIN_EXEMPT_CONTRIB_LEVEL });
// Post 1 more message than the spam limit to ensure they do not reach the limit
for (let i = 0; i < SPAM_MESSAGE_LIMIT + 1; i += 1) {
const result = await userSocialite.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage }); // eslint-disable-line no-await-in-loop
expect(result.message.id).to.exist;
}
});
}); });
}); });

View File

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

View File

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

View File

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

View File

@@ -1,63 +1,70 @@
import { import {
createAndPopulateGroup, generateUser,
resetHabiticaDB, resetHabiticaDB,
generateGroup, generateGroup,
translate as t, translate as t,
} from '../../../../helpers/api-integration/v3'; } from '../../../../helpers/api-integration/v3';
import {
TAVERN_ID,
} from '../../../../../website/server/models/group';
import apiError from '../../../../../website/server/libs/apiError';
describe('GET /groups', () => { describe('GET /groups', () => {
let user; let leader; let members; let user;
let secondGroup; let secondLeader; let userInGuild;
const NUMBER_OF_USERS_PRIVATE_GUILDS = 2; const NUMBER_OF_PUBLIC_GUILDS = 2;
const NUMBER_OF_GROUPS_USER_CAN_VIEW = 3; const NUMBER_OF_PUBLIC_GUILDS_USER_IS_LEADER = 2;
const NUMBER_OF_PUBLIC_GUILDS_USER_IS_MEMBER = 1;
const NUMBER_OF_USERS_PRIVATE_GUILDS = 1;
const NUMBER_OF_GROUPS_USER_CAN_VIEW = 5;
const GUILD_PER_PAGE = 30;
const categories = [{ const categories = [{
slug: 'newCat', slug: 'newCat',
name: 'New Category', name: 'New Category',
}]; }];
let publicGuildNotMember;
let privateGuildUserIsMemberOf; let privateGuildUserIsMemberOf;
before(async () => { before(async () => {
await resetHabiticaDB(); await resetHabiticaDB();
({ const leader = await generateUser({ balance: 10 });
group: privateGuildUserIsMemberOf, user = await generateUser({ balance: 4 });
groupLeader: leader,
members,
} = await createAndPopulateGroup({
groupDetails: {
name: 'private guild - is member',
type: 'guild',
privacy: 'private',
categories,
},
leaderDetails: {
balance: 10,
},
members: 1,
upgradeToGroupPlan: true,
}));
[user] = members;
await user.update({ balance: 4 });
({ group: secondGroup, groupLeader: secondLeader } = await createAndPopulateGroup({ const publicGuildUserIsMemberOf = await generateGroup(leader, {
groupDetails: { name: 'public guild - is member',
name: 'c++ coders', type: 'guild',
type: 'guild', privacy: 'public',
privacy: 'private', summary: 'ohayou kombonwa',
}, description: 'oyasumi',
upgradeToGroupPlan: true, });
})); await leader.post(`/groups/${publicGuildUserIsMemberOf._id}/invite`, { uuids: [user._id] });
await user.post(`/groups/${publicGuildUserIsMemberOf._id}/join`);
await secondLeader.post(`/groups/${secondGroup._id}/invite`, { uuids: [user._id] }); userInGuild = await generateUser({ guilds: [publicGuildUserIsMemberOf._id] });
await user.post(`/groups/${secondGroup._id}/join`);
await createAndPopulateGroup({ publicGuildNotMember = await generateGroup(leader, {
groupDetails: { name: 'public guild - is not member',
name: 'private guild - is not member', type: 'guild',
type: 'guild', privacy: 'public',
privacy: 'private', summary: 'Natsume Soseki',
}, description: 'Kinnosuke no Hondana',
upgradeToGroupPlan: true, categories,
});
privateGuildUserIsMemberOf = await generateGroup(leader, {
name: 'private guild - is member',
type: 'guild',
privacy: 'private',
categories,
});
await leader.post(`/groups/${privateGuildUserIsMemberOf._id}/invite`, { uuids: [user._id] });
await user.post(`/groups/${privateGuildUserIsMemberOf._id}/join`);
await generateGroup(leader, {
name: 'private guild - is not member',
type: 'guild',
privacy: 'private',
}); });
await generateGroup(leader, { await generateGroup(leader, {
@@ -91,16 +98,172 @@ describe('GET /groups', () => {
}); });
}); });
it('returns only the tavern when tavern passed in as query', async () => {
await expect(user.get('/groups?type=tavern'))
.to.eventually.have.a.lengthOf(1)
.and.to.have.nested.property('[0]')
.and.to.have.property('_id', TAVERN_ID);
});
it('returns only the user\'s party when party passed in as query', async () => { it('returns only the user\'s party when party passed in as query', async () => {
await expect(user.get('/groups?type=party')) await expect(user.get('/groups?type=party'))
.to.eventually.have.a.lengthOf(1) .to.eventually.have.a.lengthOf(1)
.and.to.have.nested.property('[0]'); .and.to.have.nested.property('[0]');
}); });
it('returns all public guilds when publicGuilds passed in as query', async () => {
await expect(user.get('/groups?type=publicGuilds'))
.to.eventually.have.a.lengthOf(NUMBER_OF_PUBLIC_GUILDS);
});
describe('filters', () => {
it('returns public guilds filtered by category', async () => {
const guilds = await user.get(`/groups?type=publicGuilds&categories=${categories[0].slug}`);
expect(guilds[0]._id).to.equal(publicGuildNotMember._id);
});
it('returns private guilds filtered by category', async () => {
const guilds = await user.get(`/groups?type=privateGuilds&categories=${categories[0].slug}`);
expect(guilds[0]._id).to.equal(privateGuildUserIsMemberOf._id);
});
it('filters public guilds by size', async () => {
await generateGroup(user, {
name: 'guild1',
type: 'guild',
privacy: 'public',
memberCount: 1,
});
// @TODO: anyway to set higher memberCount in tests right now?
const guilds = await user.get('/groups?type=publicGuilds&minMemberCount=3');
expect(guilds.length).to.equal(0);
});
it('filters private guilds by size', async () => {
await generateGroup(user, {
name: 'guild1',
type: 'guild',
privacy: 'private',
memberCount: 1,
});
// @TODO: anyway to set higher memberCount in tests right now?
const guilds = await user.get('/groups?type=privateGuilds&minMemberCount=3');
expect(guilds.length).to.equal(0);
});
it('filters public guilds by leader role', async () => {
const guilds = await user.get('/groups?type=publicGuilds&leader=true');
expect(guilds.length).to.equal(NUMBER_OF_PUBLIC_GUILDS_USER_IS_LEADER);
});
it('filters public guilds by member role', async () => {
const guilds = await userInGuild.get('/groups?type=publicGuilds&member=true');
expect(guilds.length).to.equal(1);
expect(guilds[0].name).to.have.string('is member');
});
it('filters public guilds by single-word search term', async () => {
const guilds = await user.get('/groups?type=publicGuilds&search=kom');
expect(guilds.length).to.equal(1);
expect(guilds[0].summary).to.have.string('ohayou kombonwa');
});
it('filters public guilds by single-word search term left and right-padded by spaces', async () => {
const guilds = await user.get('/groups?type=publicGuilds&search=++++ohayou+kombonwa+++++');
expect(guilds.length).to.equal(1);
expect(guilds[0].summary).to.have.string('ohayou kombonwa');
});
it('filters public guilds by two-words search term separated by multiple spaces', async () => {
const guilds = await user.get('/groups?type=publicGuilds&search=kinnosuke+++++hon');
expect(guilds.length).to.equal(1);
expect(guilds[0].description).to.have.string('Kinnosuke');
});
});
describe('public guilds pagination', () => {
it('req.query.paginate must be a boolean string', async () => {
await expect(user.get('/groups?paginate=aString&type=publicGuilds'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Invalid request parameters.',
});
});
it('req.query.paginate can only be true when req.query.type includes publicGuilds', async () => {
await expect(user.get('/groups?paginate=true&type=notPublicGuilds'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: apiError('guildsOnlyPaginate'),
});
});
it('req.query.page can\'t be negative', async () => {
await expect(user.get('/groups?paginate=true&page=-1&type=publicGuilds'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Invalid request parameters.',
});
});
it('returns 30 guilds per page ordered by number of members', async () => {
await user.update({ balance: 9000 });
const delay = () => new Promise(resolve => setTimeout(resolve, 40));
const promises = [];
for (let i = 0; i < 60; i += 1) {
promises.push(generateGroup(user, {
name: `public guild ${i} - is member`,
type: 'guild',
privacy: 'public',
}));
await delay(); // eslint-disable-line no-await-in-loop
}
const groups = await Promise.all(promises);
// update group number 32 and not the first to make sure sorting works
await groups[32].update({ name: 'guild with most members', memberCount: 199 });
await groups[33].update({ name: 'guild with less members', memberCount: -100 });
const page0 = await expect(user.get('/groups?type=publicGuilds&paginate=true'))
.to.eventually.have.a.lengthOf(GUILD_PER_PAGE);
expect(page0[0].name).to.equal('guild with most members');
await expect(user.get('/groups?type=publicGuilds&paginate=true&page=1'))
.to.eventually.have.a.lengthOf(GUILD_PER_PAGE);
const page2 = await expect(user.get('/groups?type=publicGuilds&paginate=true&page=2'))
// 1 created now, 4 by other tests, -1 for no more tavern.
.to.eventually.have.a.lengthOf(1 + 4 - 1);
expect(page2[3].name).to.equal('guild with less members');
}).timeout(10000);
});
it('makes sure that the tavern doesn\'t show up when guilds is passed as a query', async () => {
const guilds = await user.get('/groups?type=guilds');
expect(guilds.find(g => g.id === TAVERN_ID)).to.be.undefined;
});
it('makes sure that the tavern doesn\'t show up when publicGuilds is passed as a query', async () => {
const guilds = await user.get('/groups?type=publicGuilds');
expect(guilds.find(g => g.id === TAVERN_ID)).to.be.undefined;
});
it('returns all the user\'s guilds when guilds passed in as query', async () => { it('returns all the user\'s guilds when guilds passed in as query', async () => {
await expect(user.get('/groups?type=guilds')) await expect(user.get('/groups?type=guilds'))
.to.eventually.have.a .to.eventually.have.a
.lengthOf(NUMBER_OF_USERS_PRIVATE_GUILDS); .lengthOf(NUMBER_OF_PUBLIC_GUILDS_USER_IS_MEMBER + NUMBER_OF_USERS_PRIVATE_GUILDS);
}); });
it('returns all private guilds user is a part of when privateGuilds passed in as query', async () => { it('returns all private guilds user is a part of when privateGuilds passed in as query', async () => {
@@ -109,21 +272,21 @@ describe('GET /groups', () => {
}); });
it('returns a list of groups user has access to', async () => { it('returns a list of groups user has access to', async () => {
await expect(user.get('/groups?type=privateGuilds,party')) await expect(user.get('/groups?type=privateGuilds,publicGuilds,party,tavern'))
.to.eventually.have.lengthOf(NUMBER_OF_GROUPS_USER_CAN_VIEW); .to.eventually.have.lengthOf(NUMBER_OF_GROUPS_USER_CAN_VIEW - 1); // -1 for no Tavern.
}); });
describe('filters', () => { it('returns a list of groups user has access to', async () => {
it('returns private guilds filtered by category', async () => { const group = await generateGroup(user, {
const guilds = await user.get(`/groups?type=privateGuilds&categories=${categories[0].slug}`); name: 'c++ coders',
type: 'guild',
expect(guilds[0]._id).to.equal(privateGuildUserIsMemberOf._id); privacy: 'public',
}); });
it('filters private guilds by size', async () => { // search for 'c++ coders'
const guilds = await user.get('/groups?type=privateGuilds&minMemberCount=3'); await expect(user.get('/groups?type=publicGuilds&paginate=true&page=0&search=c%2B%2B+coders'))
.to.eventually.have.lengthOf(1)
expect(guilds.length).to.equal(0); .and.to.have.nested.property('[0]')
}); .and.to.have.property('_id', group._id);
}); });
}); });

View File

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

View File

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

View File

@@ -11,6 +11,7 @@ import {
describe('GET /groups/:id', () => { describe('GET /groups/:id', () => {
const typesOfGroups = {}; const typesOfGroups = {};
typesOfGroups['public guild'] = { type: 'guild', privacy: 'public' };
typesOfGroups['private guild'] = { type: 'guild', privacy: 'private' }; typesOfGroups['private guild'] = { type: 'guild', privacy: 'private' };
typesOfGroups.party = { type: 'party', privacy: 'private' }; typesOfGroups.party = { type: 'party', privacy: 'private' };
@@ -23,11 +24,10 @@ describe('GET /groups/:id', () => {
const groupData = await createAndPopulateGroup({ const groupData = await createAndPopulateGroup({
members: 30, members: 30,
groupDetails, groupDetails,
upgradeToGroupPlan: groupDetails.type === 'guild',
}); });
leader = groupData.groupLeader; leader = groupData.groupLeader;
[member] = groupData.members; member = groupData.members[0]; // eslint-disable-line prefer-destructuring
createdGroup = groupData.group; createdGroup = groupData.group;
}); });
@@ -49,6 +49,34 @@ describe('GET /groups/:id', () => {
}); });
}); });
context('Non-member of a public guild', () => {
let nonMember; let
createdGroup;
before(async () => {
const groupData = await createAndPopulateGroup({
members: 1,
groupDetails: {
name: 'test guild',
type: 'guild',
privacy: 'public',
},
});
createdGroup = groupData.group;
nonMember = await generateUser();
});
it('returns the group object for a non-member', async () => {
const group = await nonMember.get(`/groups/${createdGroup._id}`);
expect(group._id).to.eql(createdGroup._id);
expect(group.name).to.eql(createdGroup.name);
expect(group.type).to.eql(createdGroup.type);
expect(group.privacy).to.eql(createdGroup.privacy);
});
});
context('Non-member of a private guild', () => { context('Non-member of a private guild', () => {
let nonMember; let let nonMember; let
createdGroup; createdGroup;
@@ -61,7 +89,6 @@ describe('GET /groups/:id', () => {
type: 'guild', type: 'guild',
privacy: 'private', privacy: 'private',
}, },
upgradeToGroupPlan: true,
}); });
createdGroup = groupData.group; createdGroup = groupData.group;
@@ -191,7 +218,7 @@ describe('GET /groups/:id', () => {
}); });
context('Flagged messages', () => { context('Flagged messages', () => {
let group; let members; let group;
const chat1 = { const chat1 = {
id: 'chat1', id: 'chat1',
@@ -241,7 +268,7 @@ describe('GET /groups/:id', () => {
groupDetails: { groupDetails: {
name: 'test guild', name: 'test guild',
type: 'guild', type: 'guild',
privacy: 'private', privacy: 'public',
chat: [ chat: [
chat1, chat1,
chat2, chat2,
@@ -250,11 +277,9 @@ describe('GET /groups/:id', () => {
chat5, chat5,
], ],
}, },
members: 1,
upgradeToGroupPlan: true,
}); });
({ group, members } = groupData); group = groupData.group;
await group.addChat([chat1, chat2, chat3, chat4, chat5]); await group.addChat([chat1, chat2, chat3, chat4, chat5]);
}); });
@@ -262,8 +287,8 @@ describe('GET /groups/:id', () => {
context('non-admin', () => { context('non-admin', () => {
let nonAdmin; let nonAdmin;
beforeEach(() => { beforeEach(async () => {
[nonAdmin] = members; nonAdmin = await generateUser();
}); });
it('does not include messages with a flag count of 2 or greater', async () => { it('does not include messages with a flag count of 2 or greater', async () => {
@@ -289,8 +314,9 @@ describe('GET /groups/:id', () => {
let admin; let admin;
beforeEach(async () => { beforeEach(async () => {
[admin] = members; admin = await generateUser({
await admin.update({ permissions: { moderator: true } }); 'permissions.moderator': true,
});
}); });
it('includes all messages', async () => { it('includes all messages', async () => {

View File

@@ -2,6 +2,7 @@ import {
generateUser, generateUser,
translate as t, translate as t,
} from '../../../../helpers/api-integration/v3'; } from '../../../../helpers/api-integration/v3';
import { model as Group } from '../../../../../website/server/models/group';
import { MAX_SUMMARY_SIZE_FOR_GUILDS } from '../../../../../website/common/script/constants'; import { MAX_SUMMARY_SIZE_FOR_GUILDS } from '../../../../../website/common/script/constants';
describe('POST /group', () => { describe('POST /group', () => {
@@ -34,8 +35,8 @@ describe('POST /group', () => {
it('sets the group leader to the user who created the group', async () => { it('sets the group leader to the user who created the group', async () => {
const group = await user.post('/groups', { const group = await user.post('/groups', {
name: 'Test Party', name: 'Test Public Guild',
type: 'party', type: 'guild',
}); });
expect(group.leader).to.eql({ expect(group.leader).to.eql({
@@ -50,7 +51,7 @@ describe('POST /group', () => {
const name = 'Test Group'; const name = 'Test Group';
const group = await user.post('/groups', { const group = await user.post('/groups', {
name, name,
type: 'party', type: 'guild',
}); });
const updatedGroup = await user.get(`/groups/${group._id}`); const updatedGroup = await user.get(`/groups/${group._id}`);
@@ -63,7 +64,7 @@ describe('POST /group', () => {
const summary = 'Test Summary'; const summary = 'Test Summary';
const group = await user.post('/groups', { const group = await user.post('/groups', {
name, name,
type: 'party', type: 'guild',
summary, summary,
}); });
@@ -77,7 +78,7 @@ describe('POST /group', () => {
const summary = 'A'.repeat(MAX_SUMMARY_SIZE_FOR_GUILDS + 1); const summary = 'A'.repeat(MAX_SUMMARY_SIZE_FOR_GUILDS + 1);
await expect(user.post('/groups', { await expect(user.post('/groups', {
name, name,
type: 'party', type: 'guild',
summary, summary,
})).to.eventually.be.rejected.and.eql({ })).to.eventually.be.rejected.and.eql({
code: 400, code: 400,
@@ -87,6 +88,157 @@ describe('POST /group', () => {
}); });
}); });
context('Guilds', () => {
it('returns an error when a user with insufficient funds attempts to create a guild', async () => {
await user.update({ balance: 0 });
await expect(
user.post('/groups', {
name: 'Test Public Guild',
type: 'guild',
}),
).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageInsufficientGems'),
});
});
it('adds guild to user\'s list of guilds', async () => {
const guild = await user.post('/groups', {
name: 'some guild',
type: 'guild',
privacy: 'public',
});
const updatedUser = await user.get('/user');
expect(updatedUser.guilds).to.include(guild._id);
});
it('awards the Joined Guild achievement', async () => {
await user.post('/groups', {
name: 'some guild',
type: 'guild',
privacy: 'public',
});
const updatedUser = await user.get('/user');
expect(updatedUser.achievements.joinedGuild).to.eql(true);
});
context('public guild', () => {
it('creates a group', async () => {
const groupName = 'Test Public Guild';
const groupType = 'guild';
const groupPrivacy = 'public';
const publicGuild = await user.post('/groups', {
name: groupName,
type: groupType,
privacy: groupPrivacy,
});
expect(publicGuild._id).to.exist;
expect(publicGuild.name).to.equal(groupName);
expect(publicGuild.type).to.equal(groupType);
expect(publicGuild.memberCount).to.equal(1);
expect(publicGuild.privacy).to.equal(groupPrivacy);
expect(publicGuild.leader).to.eql({
_id: user._id,
profile: {
name: user.profile.name,
},
});
});
it('returns an error when a user with no chat privileges attempts to create a public guild', async () => {
await user.update({ 'flags.chatRevoked': true });
await expect(
user.post('/groups', {
name: 'Test Public Guild',
type: 'guild',
privacy: 'public',
}),
).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('chatPrivilegesRevoked'),
});
});
});
context('private guild', () => {
const groupName = 'Test Private Guild';
const groupType = 'guild';
const groupPrivacy = 'private';
it('creates a group', async () => {
const privateGuild = await user.post('/groups', {
name: groupName,
type: groupType,
privacy: groupPrivacy,
});
expect(privateGuild._id).to.exist;
expect(privateGuild.name).to.equal(groupName);
expect(privateGuild.type).to.equal(groupType);
expect(privateGuild.memberCount).to.equal(1);
expect(privateGuild.privacy).to.equal(groupPrivacy);
expect(privateGuild.leader).to.eql({
_id: user._id,
profile: {
name: user.profile.name,
},
});
});
it('creates a private guild when the user has no chat privileges', async () => {
await user.update({ 'flags.chatRevoked': true });
const privateGuild = await user.post('/groups', {
name: groupName,
type: groupType,
privacy: groupPrivacy,
});
expect(privateGuild._id).to.exist;
});
it('deducts gems from user and adds them to guild bank', async () => {
const privateGuild = await user.post('/groups', {
name: groupName,
type: groupType,
privacy: groupPrivacy,
});
expect(privateGuild.balance).to.eql(1);
const updatedUser = await user.get('/user');
expect(updatedUser.balance).to.eql(user.balance - 1);
});
it('does not deduct the gems from user when guild creation fails', async () => {
const stub = sinon.stub(Group.prototype, 'save').rejects();
const promise = user.post('/groups', {
name: groupName,
type: groupType,
privacy: groupPrivacy,
});
await expect(promise).to.eventually.be.rejected;
const updatedUser = await user.get('/user');
expect(updatedUser.balance).to.eql(user.balance);
stub.restore();
});
});
});
context('Parties', () => { context('Parties', () => {
const partyName = 'Test Party'; const partyName = 'Test Party';
const partyType = 'party'; const partyType = 'party';

View File

@@ -2,6 +2,7 @@ import { v4 as generateUUID } from 'uuid';
import { import {
generateUser, generateUser,
createAndPopulateGroup, createAndPopulateGroup,
checkExistence,
translate as t, translate as t,
} from '../../../../helpers/api-integration/v3'; } from '../../../../helpers/api-integration/v3';
@@ -18,24 +19,81 @@ describe('POST /group/:groupId/join', () => {
}); });
}); });
context('Joining a private guild', () => { context('Joining a public guild', () => {
let user; let user; let joiningUser; let
let invitedUser; publicGuild;
let guild;
let invitees;
beforeEach(async () => { beforeEach(async () => {
({ group: guild, groupLeader: user, invitees } = await createAndPopulateGroup({ const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
privacy: 'public',
},
});
publicGuild = group;
user = groupLeader;
joiningUser = await generateUser();
});
it('allows non-invited users to join public guilds', async () => {
const res = await joiningUser.post(`/groups/${publicGuild._id}/join`);
await expect(joiningUser.get('/user')).to.eventually.have.property('guilds').to.include(publicGuild._id);
expect(res.leader._id).to.eql(user._id);
expect(res.leader.profile.name).to.eql(user.profile.name);
});
it('returns an error if user was already a member', async () => {
await joiningUser.post(`/groups/${publicGuild._id}/join`);
await expect(joiningUser.post(`/groups/${publicGuild._id}/join`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('youAreAlreadyInGroup'),
});
});
it('promotes joining member in a public empty guild to leader', async () => {
await user.post(`/groups/${publicGuild._id}/leave`);
await joiningUser.post(`/groups/${publicGuild._id}/join`);
await expect(joiningUser.get(`/groups/${publicGuild._id}`)).to.eventually.have.nested.property('leader._id', joiningUser._id);
});
it('increments memberCount when joining guilds', async () => {
const oldMemberCount = publicGuild.memberCount;
await joiningUser.post(`/groups/${publicGuild._id}/join`);
await expect(joiningUser.get(`/groups/${publicGuild._id}`)).to.eventually.have.property('memberCount', oldMemberCount + 1);
});
it('awards Joined Guild achievement', async () => {
await joiningUser.post(`/groups/${publicGuild._id}/join`);
await expect(joiningUser.get('/user')).to.eventually.have.nested.property('achievements.joinedGuild', true);
});
});
context('Joining a private guild', () => {
let user; let invitedUser; let
guild;
beforeEach(async () => {
const { group, groupLeader, invitees } = await createAndPopulateGroup({
groupDetails: { groupDetails: {
name: 'Test Guild', name: 'Test Guild',
type: 'guild', type: 'guild',
privacy: 'private', privacy: 'private',
}, },
invites: 1, invites: 1,
upgradeToGroupPlan: true, });
}));
[invitedUser] = invitees; guild = group;
user = groupLeader;
invitedUser = invitees[0]; // eslint-disable-line prefer-destructuring
}); });
it('returns error when user is not invited to private guild', async () => { it('returns error when user is not invited to private guild', async () => {
@@ -125,7 +183,7 @@ describe('POST /group/:groupId/join', () => {
party = group; party = group;
user = groupLeader; user = groupLeader;
[invitedUser] = invitees; invitedUser = invitees[0]; // eslint-disable-line prefer-destructuring
}); });
it('returns error when user is not invited to party', async () => { it('returns error when user is not invited to party', async () => {
@@ -200,6 +258,47 @@ describe('POST /group/:groupId/join', () => {
await expect(user.get('/user')).to.eventually.have.nested.property('items.quests.basilist', 2); await expect(user.get('/user')).to.eventually.have.nested.property('items.quests.basilist', 2);
}); });
it('deletes previous party where the user was the only member', async () => {
const userToInvite = await generateUser();
const oldParty = await userToInvite.post('/groups', { // add user to a party
name: 'Another Test Party',
type: 'party',
});
await expect(checkExistence('groups', oldParty._id)).to.eventually.equal(true);
await user.post(`/groups/${party._id}/invite`, {
uuids: [userToInvite._id],
});
await userToInvite.post(`/groups/${party._id}/join`);
await expect(user.get('/user')).to.eventually.have.nested.property('party._id', party._id);
await expect(checkExistence('groups', oldParty._id)).to.eventually.equal(false);
});
it('does not allow user to leave a party if a quest was active and they were the only member', async () => {
const userToInvite = await generateUser();
const oldParty = await userToInvite.post('/groups', { // add user to a party
name: 'Another Test Party',
type: 'party',
});
await userToInvite.update({
[`items.quests.${PET_QUEST}`]: 1,
});
await userToInvite.post(`/groups/${oldParty._id}/quests/invite/${PET_QUEST}`);
await expect(checkExistence('groups', oldParty._id)).to.eventually.equal(true);
await user.post(`/groups/${party._id}/invite`, {
uuids: [userToInvite._id],
});
await expect(userToInvite.post(`/groups/${party._id}/join`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageCannotLeaveWhileQuesting'),
});
});
it('invites joining member to active quest', async () => { it('invites joining member to active quest', async () => {
await user.update({ await user.update({
[`items.quests.${PET_QUEST}`]: 1, [`items.quests.${PET_QUEST}`]: 1,

View File

@@ -5,6 +5,7 @@ import {
generateChallenge, generateChallenge,
checkExistence, checkExistence,
createAndPopulateGroup, createAndPopulateGroup,
sleep,
generateUser, generateUser,
translate as t, translate as t,
} from '../../../../helpers/api-integration/v3'; } from '../../../../helpers/api-integration/v3';
@@ -13,187 +14,253 @@ import payments from '../../../../../website/server/libs/payments/payments';
import calculateSubscriptionTerminationDate from '../../../../../website/server/libs/payments/calculateSubscriptionTerminationDate'; import calculateSubscriptionTerminationDate from '../../../../../website/server/libs/payments/calculateSubscriptionTerminationDate';
describe('POST /groups/:groupId/leave', () => { describe('POST /groups/:groupId/leave', () => {
let groupToLeave; const typesOfGroups = {
let leader; 'public guild': { type: 'guild', privacy: 'public' },
let member; 'private guild': { type: 'guild', privacy: 'private' },
let members; party: { type: 'party', privacy: 'private' },
let memberCount; };
context('Leaving a Group Plan', () => { each(typesOfGroups, (groupDetails, groupType) => {
beforeEach(async () => { context(`Leaving a ${groupType}`, () => {
({ group: groupToLeave, groupLeader: leader, members } = await createAndPopulateGroup({ let groupToLeave;
type: 'guild', let leader;
privacy: 'private', let member;
members: 1, let memberCount;
upgradeToGroupPlan: true,
}));
[member] = members;
memberCount = groupToLeave.memberCount;
await leader.update({ 'auth.timestamps.created': new Date('2022-01-01') });
});
it('prevents non members from leaving', async () => {
const user = await generateUser();
await expect(user.post(`/groups/${groupToLeave._id}/leave`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('groupNotFound'),
});
});
it('lets user leave', async () => {
await member.post(`/groups/${groupToLeave._id}/leave`);
const userThatLeftGroup = await member.get('/user');
expect(userThatLeftGroup.guilds).to.be.empty;
expect(userThatLeftGroup.party._id).to.not.exist;
await groupToLeave.sync();
expect(groupToLeave.memberCount).to.equal(memberCount - 1);
});
it('removes new messages for that group from user', async () => {
await leader.post(`/groups/${groupToLeave._id}/chat`, { message: 'Some message' });
await member.sync();
expect(member.notifications.find(n => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupToLeave._id)).to.exist;
expect(member.newMessages[groupToLeave._id]).to.not.be.empty;
await member.post(`/groups/${groupToLeave._id}/leave`);
await member.sync();
expect(member.notifications.find(n => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupToLeave._id)).to.not.exist;
expect(member.newMessages[groupToLeave._id]).to.be.undefined;
});
context('with challenges', () => {
let challenge;
beforeEach(async () => { beforeEach(async () => {
challenge = await generateChallenge(leader, groupToLeave); const { group, groupLeader, members } = await createAndPopulateGroup({
await member.post(`/challenges/${challenge._id}/join`); groupDetails,
members: 1,
});
await leader.post(`/tasks/challenge/${challenge._id}`, { groupToLeave = group;
text: 'test habit', leader = groupLeader;
type: 'habit', 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 () => {
const user = await generateUser();
await expect(user.post(`/groups/${groupToLeave._id}/leave`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('groupNotFound'),
}); });
}); });
it('removes all challenge tasks when keep parameter is set to remove', async () => { it(`lets user leave a ${groupType}`, async () => {
await member.post(`/groups/${groupToLeave._id}/leave?keep=remove-all`);
const userWithoutChallengeTasks = await member.get('/user');
expect(userWithoutChallengeTasks.challenges).to.not.include(challenge._id);
expect(userWithoutChallengeTasks.tasksOrder.habits).to.be.empty;
});
it('keeps all challenge tasks when keep parameter is not set', async () => {
await member.post(`/groups/${groupToLeave._id}/leave`); await member.post(`/groups/${groupToLeave._id}/leave`);
const userWithChallengeTasks = await member.get('/user'); const userThatLeftGroup = await member.get('/user');
expect(userWithChallengeTasks.tasksOrder.habits).to.not.be.empty; expect(userThatLeftGroup.guilds).to.be.empty;
expect(userThatLeftGroup.party._id).to.not.exist;
await groupToLeave.sync();
expect(groupToLeave.memberCount).to.equal(memberCount - 1);
}); });
it('keeps the user in the challenge when the keepChallenges parameter is set to remain-in-challenges', async () => { it(`sets a new group leader when leader leaves a ${groupType}`, async () => {
await member.post(`/groups/${groupToLeave._id}/leave`, { keepChallenges: 'remain-in-challenges' }); await leader.post(`/groups/${groupToLeave._id}/leave`);
const userWithChallengeTasks = await member.get('/user'); await groupToLeave.sync();
expect(groupToLeave.memberCount).to.equal(memberCount - 1);
expect(userWithChallengeTasks.challenges).to.include(challenge._id); expect(groupToLeave.leader).to.equal(member._id);
}); });
it('drops the user in the challenge when the keepChallenges parameter isn\'t set', async () => { it('removes new messages for that group from user', async () => {
await member.post(`/groups/${groupToLeave._id}/leave`); await member.post(`/groups/${groupToLeave._id}/chat`, { message: 'Some message' });
const userWithChallengeTasks = await member.get('/user'); await sleep(0.5);
expect(userWithChallengeTasks.challenges).to.not.include(challenge._id); await leader.sync();
expect(leader.notifications.find(n => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupToLeave._id)).to.exist;
expect(leader.newMessages[groupToLeave._id]).to.not.be.empty;
await leader.post(`/groups/${groupToLeave._id}/leave`);
await leader.sync();
expect(leader.notifications.find(n => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupToLeave._id)).to.not.exist;
expect(leader.newMessages[groupToLeave._id]).to.be.undefined;
}); });
context('with challenges', () => {
let challenge;
beforeEach(async () => {
challenge = await generateChallenge(leader, groupToLeave);
await leader.post(`/challenges/${challenge._id}/join`);
await leader.post(`/tasks/challenge/${challenge._id}`, {
text: 'test habit',
type: 'habit',
});
await sleep(0.5);
});
it('removes all challenge tasks when keep parameter is set to remove', async () => {
await leader.post(`/groups/${groupToLeave._id}/leave?keep=remove-all`);
const userWithoutChallengeTasks = await leader.get('/user');
expect(userWithoutChallengeTasks.challenges).to.not.include(challenge._id);
expect(userWithoutChallengeTasks.tasksOrder.habits).to.be.empty;
});
it('keeps all challenge tasks when keep parameter is not set', async () => {
await leader.post(`/groups/${groupToLeave._id}/leave`);
const userWithChallengeTasks = await leader.get('/user');
// @TODO find elegant way to assert against the task existing
expect(userWithChallengeTasks.tasksOrder.habits).to.not.be.empty;
});
it('keeps the user in the challenge when the keepChallenges parameter is set to remain-in-challenges', async () => {
await leader.post(`/groups/${groupToLeave._id}/leave`, { keepChallenges: 'remain-in-challenges' });
const userWithChallengeTasks = await leader.get('/user');
expect(userWithChallengeTasks.challenges).to.include(challenge._id);
});
it('drops the user in the challenge when the keepChallenges parameter isn\'t set', async () => {
await leader.post(`/groups/${groupToLeave._id}/leave`);
const userWithChallengeTasks = await leader.get('/user');
expect(userWithChallengeTasks.challenges).to.not.include(challenge._id);
});
});
it('prevents quest leader from leaving a groupToLeave');
it('prevents a user from leaving during an active quest');
}); });
}); });
context('Leaving a Party', () => { context('Leaving a group as the last member', () => {
let invitees; context('private guild', () => {
let invitedUser; let privateGuild;
let leader;
let invitedUser;
beforeEach(async () => { beforeEach(async () => {
({ const { group, groupLeader, invitees } = await createAndPopulateGroup({
group: groupToLeave, groupDetails: {
groupLeader: leader, name: 'Test Private Guild',
members, type: 'guild',
invitees, },
} = await createAndPopulateGroup({ invites: 1,
type: 'party', leaderDetails: {
privacy: 'private', 'auth.timestamps.created': new Date('2022-01-01'),
members: 1, balance: 10,
invites: 1, },
})); });
[member] = members; privateGuild = group;
[invitedUser] = invitees; leader = groupLeader;
memberCount = groupToLeave.memberCount; invitedUser = invitees[0]; // eslint-disable-line prefer-destructuring
await leader.update({ 'auth.timestamps.created': new Date('2022-01-01') });
});
it('prevents non members from leaving', async () => { await leader.post(`/groups/${group._id}/chat`, { message: 'Some message' });
const user = await generateUser(); });
await expect(user.post(`/groups/${groupToLeave._id}/leave`)).to.eventually.be.rejected.and.eql({
code: 404, it('removes a group when the last member leaves', async () => {
error: 'NotFound', await leader.post(`/groups/${privateGuild._id}/leave`);
message: t('groupNotFound'),
await expect(checkExistence('groups', privateGuild._id)).to.eventually.equal(false);
});
it('removes invitations when the last member leaves', async () => {
await leader.post(`/groups/${privateGuild._id}/leave`);
const userWithoutInvitation = await invitedUser.get('/user');
expect(userWithoutInvitation.invitations.guilds).to.be.empty;
}); });
}); });
it('lets user leave', async () => { context('public guild', () => {
await member.post(`/groups/${groupToLeave._id}/leave`); let publicGuild;
let leader;
let invitedUser;
const userThatLeftGroup = await member.get('/user'); beforeEach(async () => {
const { group, groupLeader, invitees } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Public Guild',
type: 'guild',
privacy: 'public',
},
invites: 1,
});
expect(userThatLeftGroup.guilds).to.be.empty; publicGuild = group;
expect(userThatLeftGroup.party._id).to.not.exist; leader = groupLeader;
await groupToLeave.sync(); invitedUser = invitees[0]; // eslint-disable-line prefer-destructuring
expect(groupToLeave.memberCount).to.equal(memberCount - 1); });
it('keeps the group when the last member leaves', async () => {
await leader.post(`/groups/${publicGuild._id}/leave`);
await expect(checkExistence('groups', publicGuild._id)).to.eventually.equal(true);
});
it('keeps the invitations when the last member leaves a public guild', async () => {
await leader.post(`/groups/${publicGuild._id}/leave`);
const userWithoutInvitation = await invitedUser.get('/user');
expect(userWithoutInvitation.invitations.guilds).to.not.be.empty;
});
it('deletes non existent guild from user when user tries to leave', async () => {
const nonExistentGuildId = generateUUID();
const userWithNonExistentGuild = await generateUser({ guilds: [nonExistentGuildId] });
expect(userWithNonExistentGuild.guilds).to.contain(nonExistentGuildId);
await expect(userWithNonExistentGuild.post(`/groups/${nonExistentGuildId}/leave`))
.to.eventually.be.rejected;
await userWithNonExistentGuild.sync();
expect(userWithNonExistentGuild.guilds).to.not.contain(nonExistentGuildId);
});
}); });
it('sets a new group leader when leader leaves', async () => { context('party', () => {
await leader.post(`/groups/${groupToLeave._id}/leave`); let party;
let leader;
let invitedUser;
await groupToLeave.sync(); beforeEach(async () => {
expect(groupToLeave.memberCount).to.equal(memberCount - 1); const { group, groupLeader, invitees } = await createAndPopulateGroup({
expect(groupToLeave.leader).to.equal(member._id); groupDetails: {
}); name: 'Test Party',
type: 'party',
},
invites: 1,
});
it('removes new messages for that group from user', async () => { party = group;
await leader.post(`/groups/${groupToLeave._id}/chat`, { message: 'Some message' }); leader = groupLeader;
await member.sync(); invitedUser = invitees[0]; // eslint-disable-line prefer-destructuring
});
expect(member.notifications.find(n => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupToLeave._id)).to.exist; it('removes a group when the last member leaves a party', async () => {
expect(member.newMessages[groupToLeave._id]).to.not.be.empty; await leader.post(`/groups/${party._id}/leave`);
await member.post(`/groups/${groupToLeave._id}/leave`); await expect(checkExistence('party', party._id)).to.eventually.equal(false);
await member.sync(); });
expect(member.notifications.find(n => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupToLeave._id)).to.not.exist; it('removes invitations when the last member leaves a party', async () => {
expect(member.newMessages[groupToLeave._id]).to.be.undefined; await leader.post(`/groups/${party._id}/leave`);
});
it('removes a party when the last member leaves', async () => { const userWithoutInvitation = await invitedUser.get('/user');
await member.post(`/groups/${groupToLeave._id}/leave`);
await leader.post(`/groups/${groupToLeave._id}/leave`);
await expect(checkExistence('party', groupToLeave._id)).to.eventually.equal(false); expect(userWithoutInvitation.invitations.parties[0]).to.be.undefined;
}); });
it('removes invitations when the last member leaves a party', async () => {
await member.post(`/groups/${groupToLeave._id}/leave`);
await leader.post(`/groups/${groupToLeave._id}/leave`);
const userWithoutInvitation = await invitedUser.get('/user');
expect(userWithoutInvitation.invitations.parties[0]).to.be.undefined;
}); });
it('deletes non existent party from user when user tries to leave', async () => { it('deletes non existent party from user when user tries to leave', async () => {
@@ -208,71 +275,23 @@ describe('POST /groups/:groupId/leave', () => {
expect(userWithNonExistentParty.party).to.eql({}); expect(userWithNonExistentParty.party).to.eql({});
}); });
context('with challenges', () => {
let challenge;
beforeEach(async () => {
challenge = await generateChallenge(leader, groupToLeave);
await member.post(`/challenges/${challenge._id}/join`);
await leader.post(`/tasks/challenge/${challenge._id}`, {
text: 'test habit',
type: 'habit',
});
});
it('removes all challenge tasks when keep parameter is set to remove', async () => {
await member.post(`/groups/${groupToLeave._id}/leave?keep=remove-all`);
const userWithoutChallengeTasks = await member.get('/user');
expect(userWithoutChallengeTasks.challenges).to.not.include(challenge._id);
expect(userWithoutChallengeTasks.tasksOrder.habits).to.be.empty;
});
it('keeps all challenge tasks when keep parameter is not set', async () => {
await member.post(`/groups/${groupToLeave._id}/leave`);
const userWithChallengeTasks = await member.get('/user');
expect(userWithChallengeTasks.tasksOrder.habits).to.not.be.empty;
});
it('keeps the user in the challenge when the keepChallenges parameter is set to remain-in-challenges', async () => {
await member.post(`/groups/${groupToLeave._id}/leave`, { keepChallenges: 'remain-in-challenges' });
const userWithChallengeTasks = await member.get('/user');
expect(userWithChallengeTasks.challenges).to.include(challenge._id);
});
it('drops the user in the challenge when the keepChallenges parameter isn\'t set', async () => {
await member.post(`/groups/${groupToLeave._id}/leave`);
const userWithChallengeTasks = await member.get('/user');
expect(userWithChallengeTasks.challenges).to.not.include(challenge._id);
});
});
}); });
const typesOfGroups = {
'private guild': { type: 'guild', privacy: 'private' },
party: { type: 'party', privacy: 'private' },
};
each(typesOfGroups, (groupDetails, groupType) => { each(typesOfGroups, (groupDetails, groupType) => {
context(`Leaving a group plan when the group is a ${groupType}`, () => { context(`Leaving a group plan when the group is a ${groupType}`, () => {
if (groupDetails.privacy === 'public') return; // public guilds cannot be group plans
let groupWithPlan; let groupWithPlan;
let leader;
let member;
beforeEach(async () => { beforeEach(async () => {
({ group: groupWithPlan, groupLeader: leader, members } = await createAndPopulateGroup({ const { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails, groupDetails,
members: 1, members: 1,
upgradeToGroupPlan: true, });
})); leader = groupLeader;
[member] = members; member = members[0]; // eslint-disable-line prefer-destructuring
groupWithPlan = group;
const userWithFreePlan = await User.findById(leader._id).exec(); const userWithFreePlan = await User.findById(leader._id).exec();
// Create subscription // Create subscription
@@ -302,21 +321,45 @@ describe('POST /groups/:groupId/leave', () => {
await member.sync(); await member.sync();
expect(member.purchased.plan.dateTerminated).to.exist; expect(member.purchased.plan.dateTerminated).to.exist;
}); });
it('preserves the free subscription when leaving a any other group without a plan', async () => {
// Joining a guild without a group plan
const { group: groupWithNoPlan } = await createAndPopulateGroup({
groupDetails: {
name: 'Group Without Plan',
type: 'guild',
privacy: 'public',
},
});
await member.post(`/groups/${groupWithNoPlan._id}/join`);
await member.sync();
expect(member.purchased.plan.planId).to.equal('group_plan_auto');
expect(member.purchased.plan.dateTerminated).to.not.exist;
// Leaving the guild without a group plan
await member.post(`/groups/${groupWithNoPlan._id}/leave`);
await member.sync();
expect(member.purchased.plan.dateTerminated).to.not.exist;
});
}); });
}); });
each(typesOfGroups, (groupDetails, groupType) => { each(typesOfGroups, (groupDetails, groupType) => {
context(`Leaving a group with extraMonths left plan when the group is a ${groupType}`, () => { context(`Leaving a group with extraMonths left plan when the group is a ${groupType}`, () => {
if (groupDetails.privacy === 'public') return; // public guilds cannot be group plans
const extraMonths = 12; const extraMonths = 12;
let groupWithPlan; let groupWithPlan;
let member;
beforeEach(async () => { beforeEach(async () => {
({ group: groupWithPlan, members } = await createAndPopulateGroup({ const { group, members } = await createAndPopulateGroup({
groupDetails, groupDetails,
members: 1, members: 1,
upgradeToGroupPlan: true, upgradeToGroupPlan: true,
})); });
[member] = members; [member] = members;
groupWithPlan = group;
await member.update({ await member.update({
'purchased.plan.extraMonths': extraMonths, 'purchased.plan.extraMonths': extraMonths,
}); });

View File

@@ -5,6 +5,43 @@ import {
} from '../../../../helpers/api-integration/v3'; } from '../../../../helpers/api-integration/v3';
describe('POST /group/:groupId/reject-invite', () => { describe('POST /group/:groupId/reject-invite', () => {
context('Rejecting a public guild invite', () => {
let publicGuild; let
invitedUser;
beforeEach(async () => {
const { group, invitees } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
privacy: 'public',
},
invites: 1,
});
publicGuild = group;
invitedUser = invitees[0]; // eslint-disable-line prefer-destructuring
});
it('returns error when user is not invited', async () => {
const userWithoutInvite = await generateUser();
await expect(userWithoutInvite.post(`/groups/${publicGuild._id}/reject-invite`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageGroupRequiresInvite'),
});
});
it('clears invitation from user', async () => {
await invitedUser.post(`/groups/${publicGuild._id}/reject-invite`);
await expect(invitedUser.get('/user'))
.to.eventually.have.nested.property('invitations.guilds')
.to.not.include({ id: publicGuild._id });
});
});
context('Rejecting a private guild invite', () => { context('Rejecting a private guild invite', () => {
let invitedUser; let let invitedUser; let
guild; guild;
@@ -17,7 +54,6 @@ describe('POST /group/:groupId/reject-invite', () => {
privacy: 'private', privacy: 'private',
}, },
invites: 1, invites: 1,
upgradeToGroupPlan: true,
}); });
guild = group; guild = group;

View File

@@ -25,7 +25,6 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
}, },
invites: 1, invites: 1,
members: 2, members: 2,
upgradeToGroupPlan: true,
}); });
guild = group; guild = group;
@@ -130,11 +129,9 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
it('sends email to removed user', async () => { it('sends email to removed user', async () => {
await leader.post(`/groups/${guild._id}/removeMember/${member._id}`); await leader.post(`/groups/${guild._id}/removeMember/${member._id}`);
expect(email.sendTxn).to.be.calledTwice; expect(email.sendTxn).to.be.calledOnce;
expect(email.sendTxn.args[0][0]._id).to.eql(member._id); expect(email.sendTxn.args[0][0]._id).to.eql(member._id);
expect(email.sendTxn.args[0][1]).to.eql('kicked-from-guild'); expect(email.sendTxn.args[0][1]).to.eql('kicked-from-guild');
expect(email.sendTxn.args[1][0]._id).to.eql(member._id);
expect(email.sendTxn.args[1][1]).to.eql('group-member-removed');
}); });
}); });

View File

@@ -1,8 +1,8 @@
import { v4 as generateUUID } from 'uuid'; import { v4 as generateUUID } from 'uuid';
import nconf from 'nconf'; import nconf from 'nconf';
import { import {
createAndPopulateGroup,
generateUser, generateUser,
generateGroup,
translate as t, translate as t,
} from '../../../../helpers/api-integration/v3'; } from '../../../../helpers/api-integration/v3';
@@ -13,13 +13,13 @@ const MAX_EMAIL_INVITES_BY_USER = 200;
describe('Post /groups/:groupId/invite', () => { describe('Post /groups/:groupId/invite', () => {
let inviter; let inviter;
let group; let group;
const groupName = 'Test Party'; const groupName = 'Test Public Guild';
beforeEach(async () => { beforeEach(async () => {
inviter = await generateUser({ balance: 4 }); inviter = await generateUser({ balance: 4 });
group = await inviter.post('/groups', { group = await inviter.post('/groups', {
name: groupName, name: groupName,
type: 'party', type: 'guild',
}); });
}); });
@@ -48,60 +48,48 @@ 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 () => { it('invites a user to a group by username', async () => {
const userToInvite = await generateUser(); const userToInvite = await generateUser();
const response = await inviter.post(`/groups/${group._id}/invite`, { await expect(inviter.post(`/groups/${group._id}/invite`, {
usernames: [userToInvite.auth.local.lowerCaseUsername], usernames: [userToInvite.auth.local.lowerCaseUsername],
}); })).to.eventually.deep.equal([{
expect(response).to.be.an('Array'); id: group._id,
expect(response[0]).to.have.all.keys(['_id', 'id', 'name', 'inviter']); name: groupName,
expect(response[0]._id).to.be.a('String'); inviter: inviter._id,
expect(response[0].id).to.eql(group._id); publicGuild: false,
expect(response[0].name).to.eql(groupName); }]);
expect(response[0].inviter).to.eql(inviter._id);
await expect(userToInvite.get('/user')) await expect(userToInvite.get('/user'))
.to.eventually.have.nested.property('invitations.parties[0].id', group._id); .to.eventually.have.nested.property('invitations.guilds[0].id', group._id);
}); });
it('invites multiple users to a group by uuid', async () => { it('invites multiple users to a group by uuid', async () => {
const userToInvite = await generateUser(); const userToInvite = await generateUser();
const userToInvite2 = await generateUser(); const userToInvite2 = await generateUser();
const response = await (inviter.post(`/groups/${group._id}/invite`, { await expect(inviter.post(`/groups/${group._id}/invite`, {
usernames: [ usernames: [
userToInvite.auth.local.lowerCaseUsername, userToInvite.auth.local.lowerCaseUsername,
userToInvite2.auth.local.lowerCaseUsername, userToInvite2.auth.local.lowerCaseUsername,
], ],
})); })).to.eventually.deep.equal([
expect(response).to.be.an('Array'); {
expect(response[0]).to.have.all.keys(['_id', 'id', 'name', 'inviter']); id: group._id,
expect(response[0]._id).to.be.a('String'); name: groupName,
expect(response[0].id).to.eql(group._id); inviter: inviter._id,
expect(response[0].name).to.eql(groupName); publicGuild: false,
expect(response[0].inviter).to.eql(inviter._id); },
expect(response[1]).to.have.all.keys(['_id', 'id', 'name', 'inviter']); {
expect(response[1]._id).to.be.a('String'); id: group._id,
expect(response[1].id).to.eql(group._id); name: groupName,
expect(response[1].name).to.eql(groupName); inviter: inviter._id,
expect(response[1].inviter).to.eql(inviter._id); publicGuild: false,
},
]);
await expect(userToInvite.get('/user')).to.eventually.have.nested.property('invitations.parties[0].id', group._id); await expect(userToInvite.get('/user')).to.eventually.have.nested.property('invitations.guilds[0].id', group._id);
await expect(userToInvite2.get('/user')).to.eventually.have.nested.property('invitations.parties[0].id', group._id); await expect(userToInvite2.get('/user')).to.eventually.have.nested.property('invitations.guilds[0].id', group._id);
}); });
}); });
@@ -212,42 +200,42 @@ describe('Post /groups/:groupId/invite', () => {
it('invites a user to a group by uuid', async () => { it('invites a user to a group by uuid', async () => {
const userToInvite = await generateUser(); const userToInvite = await generateUser();
const response = await inviter.post(`/groups/${group._id}/invite`, { await expect(inviter.post(`/groups/${group._id}/invite`, {
uuids: [userToInvite._id], uuids: [userToInvite._id],
}); })).to.eventually.deep.equal([{
expect(response).to.be.an('Array'); id: group._id,
expect(response[0]).to.have.all.keys(['_id', 'id', 'name', 'inviter']); name: groupName,
expect(response[0]._id).to.be.a('String'); inviter: inviter._id,
expect(response[0].id).to.eql(group._id); publicGuild: false,
expect(response[0].name).to.eql(groupName); }]);
expect(response[0].inviter).to.eql(inviter._id);
await expect(userToInvite.get('/user')) await expect(userToInvite.get('/user'))
.to.eventually.have.nested.property('invitations.parties[0].id', group._id); .to.eventually.have.nested.property('invitations.guilds[0].id', group._id);
}); });
it('invites multiple users to a group by uuid', async () => { it('invites multiple users to a group by uuid', async () => {
const userToInvite = await generateUser(); const userToInvite = await generateUser();
const userToInvite2 = await generateUser(); const userToInvite2 = await generateUser();
const response = await inviter.post(`/groups/${group._id}/invite`, { await expect(inviter.post(`/groups/${group._id}/invite`, {
uuids: [userToInvite._id, userToInvite2._id], uuids: [userToInvite._id, userToInvite2._id],
}); })).to.eventually.deep.equal([
{
id: group._id,
name: groupName,
inviter: inviter._id,
publicGuild: false,
},
{
id: group._id,
name: groupName,
inviter: inviter._id,
publicGuild: false,
},
]);
expect(response).to.be.an('Array'); await expect(userToInvite.get('/user')).to.eventually.have.nested.property('invitations.guilds[0].id', group._id);
expect(response[0]).to.have.all.keys(['_id', 'id', 'name', 'inviter']); await expect(userToInvite2.get('/user')).to.eventually.have.nested.property('invitations.guilds[0].id', group._id);
expect(response[0]._id).to.be.a('String');
expect(response[0].id).to.eql(group._id);
expect(response[0].name).to.eql(groupName);
expect(response[0].inviter).to.eql(inviter._id);
expect(response[1]).to.have.all.keys(['_id', 'id', 'name', 'inviter']);
expect(response[1]._id).to.be.a('String');
expect(response[1].id).to.eql(group._id);
expect(response[1].name).to.eql(groupName);
expect(response[1].inviter).to.eql(inviter._id);
await expect(userToInvite.get('/user')).to.eventually.have.nested.property('invitations.parties[0].id', group._id);
await expect(userToInvite2.get('/user')).to.eventually.have.nested.property('invitations.parties[0].id', group._id);
}); });
it('returns an error when inviting multiple users and a user is not found', async () => { it('returns an error when inviting multiple users and a user is not found', async () => {
@@ -336,8 +324,12 @@ describe('Post /groups/:groupId/invite', () => {
invitesSent: MAX_EMAIL_INVITES_BY_USER, invitesSent: MAX_EMAIL_INVITES_BY_USER,
balance: 4, balance: 4,
}); });
const tmpGroup = await inviterWithMax.post('/groups', {
name: groupName,
type: 'guild',
});
await expect(inviterWithMax.post(`/groups/${group._id}/invite`, { await expect(inviterWithMax.post(`/groups/${tmpGroup._id}/invite`, {
emails: [testInvite], emails: [testInvite],
inviter: 'inviter name', inviter: 'inviter name',
})) }))
@@ -413,15 +405,15 @@ describe('Post /groups/:groupId/invite', () => {
}); });
const invitedUser = await newUser.get('/user'); const invitedUser = await newUser.get('/user');
expect(invitedUser.invitations.parties[0].id).to.equal(group._id); expect(invitedUser.invitations.guilds[0].id).to.equal(group._id);
expect(invite).to.exist; expect(invite).to.exist;
}); });
it('invites user to group with cancelled plan', async () => { it('invites marks invite with cancelled plan', async () => {
let cancelledPlanGroup; const cancelledPlanGroup = await generateGroup(inviter, {
({ group: cancelledPlanGroup, groupLeader: inviter } = await createAndPopulateGroup({ type: 'guild',
upgradeToGroupPlan: true, name: generateUUID(),
})); });
await cancelledPlanGroup.createCancelledSubscription(); await cancelledPlanGroup.createCancelledSubscription();
const newUser = await generateUser(); const newUser = await generateUser();
@@ -431,13 +423,13 @@ describe('Post /groups/:groupId/invite', () => {
}); });
const invitedUser = await newUser.get('/user'); const invitedUser = await newUser.get('/user');
expect(invitedUser.invitations.parties[0].id).to.equal(cancelledPlanGroup._id); expect(invitedUser.invitations.guilds[0].id).to.equal(cancelledPlanGroup._id);
expect(invitedUser.invitations.parties[0].cancelledPlan).to.be.true; expect(invitedUser.invitations.guilds[0].cancelledPlan).to.be.true;
expect(invite).to.exist; expect(invite).to.exist;
}); });
}); });
describe('party invites', () => { describe('guild invites', () => {
it('returns an error when inviter has no chat privileges', async () => { it('returns an error when inviter has no chat privileges', async () => {
const inviterMuted = await inviter.update({ 'flags.chatRevoked': true }); const inviterMuted = await inviter.update({ 'flags.chatRevoked': true });
const userToInvite = await generateUser(); const userToInvite = await generateUser();
@@ -451,7 +443,7 @@ describe('Post /groups/:groupId/invite', () => {
}); });
}); });
it('returns an error when invited user has a pending invitation to the party', async () => { it('returns an error when invited user is already invited to the group', async () => {
const userToInvite = await generateUser(); const userToInvite = await generateUser();
await inviter.post(`/groups/${group._id}/invite`, { await inviter.post(`/groups/${group._id}/invite`, {
uuids: [userToInvite._id], uuids: [userToInvite._id],
@@ -459,6 +451,96 @@ describe('Post /groups/:groupId/invite', () => {
await expect(inviter.post(`/groups/${group._id}/invite`, { await expect(inviter.post(`/groups/${group._id}/invite`, {
uuids: [userToInvite._id], uuids: [userToInvite._id],
}))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('userAlreadyInvitedToGroup', { userId: userToInvite._id, username: userToInvite.profile.name }),
});
});
it('returns an error when invited user is already in the group', async () => {
const userToInvite = await generateUser();
await inviter.post(`/groups/${group._id}/invite`, {
uuids: [userToInvite._id],
});
await userToInvite.post(`/groups/${group._id}/join`);
await expect(inviter.post(`/groups/${group._id}/invite`, {
uuids: [userToInvite._id],
}))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('userAlreadyInGroup', { userId: userToInvite._id, username: userToInvite.profile.name }),
});
});
it('allows 30+ members in a guild', async () => {
const invitesToGenerate = [];
// Generate 30 users to invite (30 + leader = 31 members)
for (let i = 0; i < PARTY_LIMIT_MEMBERS; i += 1) {
invitesToGenerate.push(generateUser());
}
const generatedInvites = await Promise.all(invitesToGenerate);
// Invite users
expect(await inviter.post(`/groups/${group._id}/invite`, {
uuids: generatedInvites.map(invite => invite._id),
})).to.be.an('array');
}).timeout(10000);
// @TODO: Add this after we are able to mock the group plan route
xit('returns an error when a non-leader invites to a group plan', async () => {
const userToInvite = await generateUser();
const nonGroupLeader = await generateUser();
await inviter.post(`/groups/${group._id}/invite`, {
uuids: [nonGroupLeader._id],
});
await nonGroupLeader.post(`/groups/${group._id}/join`);
await expect(nonGroupLeader.post(`/groups/${group._id}/invite`, {
uuids: [userToInvite._id],
}))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlyGroupLeaderCanInviteToGroupPlan'),
});
});
});
describe('party invites', () => {
let party;
beforeEach(async () => {
party = await inviter.post('/groups', {
name: 'Test Party',
type: 'party',
});
});
it('returns an error when inviter has no chat privileges', async () => {
const inviterMuted = await inviter.update({ 'flags.chatRevoked': true });
const userToInvite = await generateUser();
await expect(inviterMuted.post(`/groups/${party._id}/invite`, {
uuids: [userToInvite._id],
}))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('chatPrivilegesRevoked'),
});
});
it('returns an error when invited user has a pending invitation to the party', async () => {
const userToInvite = await generateUser();
await inviter.post(`/groups/${party._id}/invite`, {
uuids: [userToInvite._id],
});
await expect(inviter.post(`/groups/${party._id}/invite`, {
uuids: [userToInvite._id],
})) }))
.to.eventually.be.rejected.and.eql({ .to.eventually.be.rejected.and.eql({
code: 401, code: 401,
@@ -470,13 +552,13 @@ describe('Post /groups/:groupId/invite', () => {
it('returns an error when invited user is already in a party of more than 1 member', async () => { it('returns an error when invited user is already in a party of more than 1 member', async () => {
const userToInvite = await generateUser(); const userToInvite = await generateUser();
const userToInvite2 = await generateUser(); const userToInvite2 = await generateUser();
await inviter.post(`/groups/${group._id}/invite`, { await inviter.post(`/groups/${party._id}/invite`, {
uuids: [userToInvite._id, userToInvite2._id], uuids: [userToInvite._id, userToInvite2._id],
}); });
await userToInvite.post(`/groups/${group._id}/join`); await userToInvite.post(`/groups/${party._id}/join`);
await userToInvite2.post(`/groups/${group._id}/join`); await userToInvite2.post(`/groups/${party._id}/join`);
await expect(inviter.post(`/groups/${group._id}/invite`, { await expect(inviter.post(`/groups/${party._id}/invite`, {
uuids: [userToInvite._id], uuids: [userToInvite._id],
})) }))
.to.eventually.be.rejected.and.eql({ .to.eventually.be.rejected.and.eql({
@@ -486,7 +568,20 @@ describe('Post /groups/:groupId/invite', () => {
}); });
}); });
it('allows inviting a user to 2 different parties', async () => { it('allow inviting a user to a party if they are partying solo', async () => {
const userToInvite = await generateUser();
await userToInvite.post('/groups', { // add user to a party
name: 'Another Test Party',
type: 'party',
});
await inviter.post(`/groups/${party._id}/invite`, {
uuids: [userToInvite._id],
});
expect((await userToInvite.get('/user')).invitations.parties[0].id).to.equal(party._id);
});
it('allow inviting a user to 2 different parties', async () => {
// Create another inviter // Create another inviter
const inviter2 = await generateUser(); const inviter2 = await generateUser();
@@ -500,7 +595,7 @@ describe('Post /groups/:groupId/invite', () => {
}); });
// Invite to first party // Invite to first party
await inviter.post(`/groups/${group._id}/invite`, { await inviter.post(`/groups/${party._id}/invite`, {
uuids: [userToInvite._id], uuids: [userToInvite._id],
}); });
@@ -513,59 +608,43 @@ describe('Post /groups/:groupId/invite', () => {
const invitedUser = await userToInvite.get('/user'); const invitedUser = await userToInvite.get('/user');
expect(invitedUser.invitations.parties.length).to.equal(2); expect(invitedUser.invitations.parties.length).to.equal(2);
expect(invitedUser.invitations.parties[0].id).to.equal(group._id); expect(invitedUser.invitations.parties[0].id).to.equal(party._id);
expect(invitedUser.invitations.parties[1].id).to.equal(party2._id); expect(invitedUser.invitations.parties[1].id).to.equal(party2._id);
}); });
it('allows inviting a user if party id is not associated with a real party', async () => { it('allow inviting a user if party id is not associated with a real party', async () => {
const userToInvite = await generateUser({ const userToInvite = await generateUser({
party: { _id: generateUUID() }, party: { _id: generateUUID() },
}); });
await inviter.post(`/groups/${group._id}/invite`, { await inviter.post(`/groups/${party._id}/invite`, {
uuids: [userToInvite._id], uuids: [userToInvite._id],
}); });
expect((await userToInvite.get('/user')).invitations.parties[0].id).to.equal(group._id); expect((await userToInvite.get('/user')).invitations.parties[0].id).to.equal(party._id);
});
});
describe('party size limits', () => {
let partyLeader;
beforeEach(async () => {
({ group, groupLeader: partyLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Party',
type: 'party',
privacy: 'private',
},
// Generate party with 20 members
members: PARTY_LIMIT_MEMBERS - 10,
}));
}); });
it('allows 30 members in a party', async () => { it('allows 30 members in a party', async () => {
const invitesToGenerate = []; const invitesToGenerate = [];
// Generate 10 new invites // Generate 29 users to invite (29 + leader = 30 members)
for (let i = 1; i < 10; i += 1) { for (let i = 0; i < PARTY_LIMIT_MEMBERS - 1; i += 1) {
invitesToGenerate.push(generateUser()); invitesToGenerate.push(generateUser());
} }
const generatedInvites = await Promise.all(invitesToGenerate); const generatedInvites = await Promise.all(invitesToGenerate);
// Invite users // Invite users
expect(await partyLeader.post(`/groups/${group._id}/invite`, { expect(await inviter.post(`/groups/${party._id}/invite`, {
uuids: generatedInvites.map(invite => invite._id), uuids: generatedInvites.map(invite => invite._id),
})).to.be.an('array'); })).to.be.an('array');
}).timeout(10000); }).timeout(10000);
it('does not allow >30 members in a party', async () => { it('does not allow 30+ members in a party', async () => {
const invitesToGenerate = []; const invitesToGenerate = [];
// Generate 11 invites // Generate 30 users to invite (30 + leader = 31 members)
for (let i = 1; i < 11; i += 1) { for (let i = 0; i < PARTY_LIMIT_MEMBERS; i += 1) {
invitesToGenerate.push(generateUser()); invitesToGenerate.push(generateUser());
} }
const generatedInvites = await Promise.all(invitesToGenerate); const generatedInvites = await Promise.all(invitesToGenerate);
// Invite users // Invite users
await expect(partyLeader.post(`/groups/${group._id}/invite`, { await expect(inviter.post(`/groups/${party._id}/invite`, {
uuids: generatedInvites.map(invite => invite._id), uuids: generatedInvites.map(invite => invite._id),
})) }))
.to.eventually.be.rejected.and.eql({ .to.eventually.be.rejected.and.eql({

View File

@@ -17,10 +17,9 @@ describe('POST /group/:groupId/add-manager', () => {
groupDetails: { groupDetails: {
name: groupName, name: groupName,
type: groupType, type: groupType,
privacy: 'private', privacy: 'public',
}, },
members: 1, members: 1,
upgradeToGroupPlan: true,
}); });
groupToUpdate = group; groupToUpdate = group;

View File

@@ -23,11 +23,10 @@ describe('PUT /group', () => {
groupDetails: { groupDetails: {
name: groupName, name: groupName,
type: groupType, type: groupType,
privacy: 'private', privacy: 'public',
categories: groupCategories, categories: groupCategories,
}, },
members: 1, members: 1,
upgradeToGroupPlan: true,
}); });
adminUser = await generateUser({ 'permissions.moderator': true }); adminUser = await generateUser({ 'permissions.moderator': true });
groupToUpdate = group; groupToUpdate = group;
@@ -107,28 +106,14 @@ describe('PUT /group', () => {
expect(updatedGroup.name).to.equal(groupUpdatedName); expect(updatedGroup.name).to.equal(groupUpdatedName);
}); });
it('does not allow a leader to change leader of active group plan', async () => { it('allows a leader to change leaders', async () => {
await expect(leader.put(`/groups/${groupToUpdate._id}`, { const updatedGroup = await leader.put(`/groups/${groupToUpdate._id}`, {
name: groupUpdatedName, name: groupUpdatedName,
leader: nonLeader._id, leader: nonLeader._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('cannotChangeLeaderWithActiveGroupPlan'),
});
});
it('allows a leader of a party to change leaders', async () => {
const { group: party, groupLeader: partyLeader, members } = await createAndPopulateGroup({
members: 1,
});
const updatedGroup = await partyLeader.put(`/groups/${party._id}`, {
name: groupUpdatedName,
leader: members[0]._id,
}); });
expect(updatedGroup.leader._id).to.eql(members[0]._id); expect(updatedGroup.leader._id).to.eql(nonLeader._id);
expect(updatedGroup.leader.profile.name).to.eql(members[0].profile.name); expect(updatedGroup.leader.profile.name).to.eql(nonLeader.profile.name);
expect(updatedGroup.name).to.equal(groupUpdatedName); expect(updatedGroup.name).to.equal(groupUpdatedName);
}); });
@@ -137,16 +122,15 @@ describe('PUT /group', () => {
groupDetails: { groupDetails: {
name: 'public guild', name: 'public guild',
type: 'guild', type: 'guild',
privacy: 'private', privacy: 'public',
}, },
upgradeToGroupPlan: true,
}); });
const updateGroupDetails = { const updateGroupDetails = {
id: group._id, id: group._id,
name: 'public guild', name: 'public guild',
type: 'guild', type: 'guild',
privacy: 'private', privacy: 'public',
bannedWordsAllowed: true, bannedWordsAllowed: true,
}; };
@@ -166,11 +150,9 @@ describe('PUT /group', () => {
groupDetails: { groupDetails: {
name: 'public guild', name: 'public guild',
type: 'guild', type: 'guild',
privacy: 'private', privacy: 'public',
}, },
upgradeToGroupPlan: true,
}); });
await groupLeader.update({ permissions: {} });
const updateGroupDetails = { const updateGroupDetails = {
id: group._id, id: group._id,

View File

@@ -1,6 +1,6 @@
import { import {
createAndPopulateGroup,
generateUser, generateUser,
generateGroup,
translate as t, translate as t,
} from '../../../../../helpers/api-integration/v3'; } from '../../../../../helpers/api-integration/v3';
import amzLib from '../../../../../../website/server/libs/payments/amazon'; import amzLib from '../../../../../../website/server/libs/payments/amazon';
@@ -50,21 +50,22 @@ describe('payments : amazon #subscribeCancel', () => {
}); });
it('cancels a group subscription', async () => { it('cancels a group subscription', async () => {
({ group, groupLeader: user } = await createAndPopulateGroup({ user = await generateUser({
groupDetails: { 'profile.name': 'sender',
name: 'test group', 'purchased.plan.customerId': 'customer-id',
type: 'guild', 'purchased.plan.planId': 'basic_3mo',
privacy: 'private', 'purchased.plan.lastBillingDate': new Date(),
}, balance: 2,
leaderDetails: { });
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id', group = await generateGroup(user, {
'purchased.plan.planId': 'basic_3mo', name: 'test group',
'purchased.plan.lastBillingDate': new Date(), type: 'guild',
balance: 2, privacy: 'public',
}, 'purchased.plan.customerId': 'customer-id',
upgradeToGroupPlan: true, 'purchased.plan.planId': 'basic_3mo',
})); 'purchased.plan.lastBillingDate': new Date(),
});
await user.get(`${endpoint}&groupId=${group._id}`); await user.get(`${endpoint}&groupId=${group._id}`);

View File

@@ -70,8 +70,8 @@ describe('payments - amazon - #subscribe', () => {
group = await generateGroup(user, { group = await generateGroup(user, {
name: 'test group', name: 'test group',
type: 'party', type: 'guild',
privacy: 'private', privacy: 'public',
'purchased.plan.customerId': 'customer-id', 'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo', 'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(), 'purchased.plan.lastBillingDate': new Date(),

View File

@@ -45,10 +45,11 @@ describe('payments : apple #subscribe', () => {
}); });
expect(subscribeStub).to.be.calledOnce; expect(subscribeStub).to.be.calledOnce;
expect(subscribeStub.args[0][0]._id).to.eql(user._id); expect(subscribeStub.args[0][0]).to.eql(sku);
expect(subscribeStub.args[0][1]).to.eql('receipt'); expect(subscribeStub.args[0][1]._id).to.eql(user._id);
expect(subscribeStub.args[0][2]['x-api-key']).to.eql(user.apiToken); expect(subscribeStub.args[0][2]).to.eql('receipt');
expect(subscribeStub.args[0][2]['x-api-user']).to.eql(user._id); 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; let verifyStub;
beforeEach(async () => { beforeEach(async () => {
verifyStub = sinon.stub(applePayments, 'verifyPurchase').resolves({}); verifyStub = sinon.stub(applePayments, 'verifyGemPurchase').resolves({});
}); });
afterEach(() => { afterEach(() => {
applePayments.verifyPurchase.restore(); applePayments.verifyGemPurchase.restore();
}); });
it('makes a purchase', async () => { it('makes a purchase', async () => {

View File

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

View File

@@ -1,7 +1,7 @@
import { import {
generateUser, generateUser,
generateGroup,
translate as t, translate as t,
createAndPopulateGroup,
} from '../../../../../helpers/api-integration/v3'; } from '../../../../../helpers/api-integration/v3';
import stripePayments from '../../../../../../website/server/libs/payments/stripe'; import stripePayments from '../../../../../../website/server/libs/payments/stripe';
@@ -48,21 +48,22 @@ describe('payments - stripe - #subscribeCancel', () => {
}); });
it('cancels a group subscription', async () => { it('cancels a group subscription', async () => {
({ group, groupLeader: user } = await createAndPopulateGroup({ user = await generateUser({
groupDetails: { 'profile.name': 'sender',
name: 'test group', 'purchased.plan.customerId': 'customer-id',
type: 'guild', 'purchased.plan.planId': 'basic_3mo',
privacy: 'private', 'purchased.plan.lastBillingDate': new Date(),
}, balance: 2,
leaderDetails: { });
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id', group = await generateGroup(user, {
'purchased.plan.planId': 'basic_3mo', name: 'test group',
'purchased.plan.lastBillingDate': new Date(), type: 'guild',
balance: 2, privacy: 'public',
}, 'purchased.plan.customerId': 'customer-id',
upgradeToGroupPlan: true, 'purchased.plan.planId': 'basic_3mo',
})); 'purchased.plan.lastBillingDate': new Date(),
});
await user.get(`${endpoint}&groupId=${group._id}`); await user.get(`${endpoint}&groupId=${group._id}`);

View File

@@ -53,7 +53,6 @@ describe('POST /groups/:groupId/quests/accept', () => {
it('does not accept quest for a guild', async () => { it('does not accept quest for a guild', async () => {
const { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({ const { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' }, groupDetails: { type: 'guild', privacy: 'private' },
upgradeToGroupPlan: true,
}); });
await expect(guildLeader.post(`/groups/${guild._id}/quests/accept`)) await expect(guildLeader.post(`/groups/${guild._id}/quests/accept`))

View File

@@ -43,7 +43,6 @@ describe('POST /groups/:groupId/quests/force-start', () => {
it('does not force start quest for a guild', async () => { it('does not force start quest for a guild', async () => {
const { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({ const { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' }, groupDetails: { type: 'guild', privacy: 'private' },
upgradeToGroupPlan: true,
}); });
await expect(guildLeader.post(`/groups/${guild._id}/quests/force-start`)) await expect(guildLeader.post(`/groups/${guild._id}/quests/force-start`))

View File

@@ -51,13 +51,14 @@ describe('POST /groups/:groupId/quests/invite/:questKey', () => {
}); });
it('does not issue invites for Guilds', async () => { it('does not issue invites for Guilds', async () => {
const { group, groupLeader } = await createAndPopulateGroup({ const { group } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' }, groupDetails: { type: 'guild', privacy: 'public' },
members: 1, members: 1,
upgradeToGroupPlan: true,
}); });
await expect(groupLeader.post(`/groups/${group._id}/quests/invite/${PET_QUEST}`)).to.eventually.be.rejected.and.eql({ const alternateGroup = group;
await expect(leader.post(`/groups/${alternateGroup._id}/quests/invite/${PET_QUEST}`)).to.eventually.be.rejected.and.eql({
code: 401, code: 401,
error: 'NotAuthorized', error: 'NotAuthorized',
message: t('guildQuestsNotSupported'), message: t('guildQuestsNotSupported'),

View File

@@ -52,7 +52,6 @@ describe('POST /groups/:groupId/quests/abort', () => {
it('returns an error when group is a guild', async () => { it('returns an error when group is a guild', async () => {
const { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({ const { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' }, groupDetails: { type: 'guild', privacy: 'private' },
upgradeToGroupPlan: true,
}); });
await expect(guildLeader.post(`/groups/${guild._id}/quests/abort`)) await expect(guildLeader.post(`/groups/${guild._id}/quests/abort`))

View File

@@ -52,7 +52,6 @@ describe('POST /groups/:groupId/quests/cancel', () => {
it('returns an error when group is a guild', async () => { it('returns an error when group is a guild', async () => {
const { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({ const { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' }, groupDetails: { type: 'guild', privacy: 'private' },
upgradeToGroupPlan: true,
}); });
await expect(guildLeader.post(`/groups/${guild._id}/quests/cancel`)) await expect(guildLeader.post(`/groups/${guild._id}/quests/cancel`))

View File

@@ -51,7 +51,6 @@ describe('POST /groups/:groupId/quests/leave', () => {
it('returns an error when group is a guild', async () => { it('returns an error when group is a guild', async () => {
const { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({ const { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' }, groupDetails: { type: 'guild', privacy: 'private' },
upgradeToGroupPlan: true,
}); });
await expect(guildLeader.post(`/groups/${guild._id}/quests/leave`)) await expect(guildLeader.post(`/groups/${guild._id}/quests/leave`))

View File

@@ -53,7 +53,6 @@ describe('POST /groups/:groupId/quests/reject', () => {
it('returns an error when group is a guild', async () => { it('returns an error when group is a guild', async () => {
const { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({ const { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' }, groupDetails: { type: 'guild', privacy: 'private' },
upgradeToGroupPlan: true,
}); });
await expect(guildLeader.post(`/groups/${guild._id}/quests/reject`)) await expect(guildLeader.post(`/groups/${guild._id}/quests/reject`))

View File

@@ -1,5 +1,6 @@
import { import {
createAndPopulateGroup, generateUser,
generateGroup,
} from '../../../../../helpers/api-integration/v3'; } from '../../../../../helpers/api-integration/v3';
describe('POST group-tasks/:taskId/move/to/:position', () => { describe('POST group-tasks/:taskId/move/to/:position', () => {
@@ -7,12 +8,8 @@ describe('POST group-tasks/:taskId/move/to/:position', () => {
guild; guild;
beforeEach(async () => { beforeEach(async () => {
const { group, groupLeader } = await createAndPopulateGroup({ user = await generateUser({ balance: 1 });
groupDetails: { type: 'guild', privacy: 'private' }, guild = await generateGroup(user, { type: 'guild' }, { 'purchased.plan.customerId': 'group-unlimited' });
upgradeToGroupPlan: true,
});
guild = group;
user = groupLeader;
}); });
it('can move task to new position', async () => { it('can move task to new position', async () => {

View File

@@ -1,4 +1,5 @@
import { import {
find,
each, each,
map, map,
} from 'lodash'; } from 'lodash';
@@ -197,6 +198,95 @@ describe('DELETE /user', () => {
await expect(checkExistence('party', party._id)).to.eventually.eql(false); await expect(checkExistence('party', party._id)).to.eventually.eql(false);
}); });
}); });
context('last member of a private guild', () => {
let privateGuild;
beforeEach(async () => {
privateGuild = await generateGroup(user, {
type: 'guild',
privacy: 'private',
});
});
it('deletes guild when user is the only member', async () => {
await user.del('/user', {
password,
});
await expect(checkExistence('groups', privateGuild._id)).to.eventually.eql(false);
});
});
context('groups user is leader of', () => {
let guild; let oldLeader; let
newLeader;
beforeEach(async () => {
const { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'public',
},
members: 1,
});
guild = group;
newLeader = members[0]; // eslint-disable-line prefer-destructuring
oldLeader = groupLeader;
});
it('chooses new group leader for any group user was the leader of', async () => {
await oldLeader.del('/user', {
password,
});
const updatedGuild = await newLeader.get(`/groups/${guild._id}`);
expect(updatedGuild.leader).to.exist;
expect(updatedGuild.leader._id).to.not.eql(oldLeader._id);
});
});
context('groups user is a part of', () => {
let group1; let group2; let userToDelete; let
otherUser;
beforeEach(async () => {
userToDelete = await generateUser({ balance: 10 });
group1 = await generateGroup(userToDelete, {
type: 'guild',
privacy: 'public',
});
const { group, members } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'public',
},
members: 3,
});
group2 = group;
otherUser = members[0]; // eslint-disable-line prefer-destructuring
await userToDelete.post(`/groups/${group2._id}/join`);
});
it('removes user from all groups user was a part of', async () => {
await userToDelete.del('/user', {
password,
});
const updatedGroup1Members = await otherUser.get(`/groups/${group1._id}/members`);
const updatedGroup2Members = await otherUser.get(`/groups/${group2._id}/members`);
const userInGroup = find(updatedGroup2Members, member => member._id === userToDelete._id);
expect(updatedGroup1Members).to.be.empty;
expect(updatedGroup2Members).to.not.be.empty;
expect(userInGroup).to.not.exist;
});
});
}); });
context('user with Google auth', async () => { context('user with Google auth', async () => {

View File

@@ -51,7 +51,6 @@ describe('POST /user/purchase/:type/:key', () => {
type: 'guild', type: 'guild',
privacy: 'private', privacy: 'private',
}, },
upgradeToGroupPlan: true,
}); });
await group.update({ await group.update({
'leaderOnly.getGems': true, 'leaderOnly.getGems': true,
@@ -78,7 +77,6 @@ describe('POST /user/purchase/:type/:key', () => {
privacy: 'private', privacy: 'private',
}, },
members: 1, members: 1,
upgradeToGroupPlan: true,
}); });
await group.update({ await group.update({
'leaderOnly.getGems': true, 'leaderOnly.getGems': true,

View File

@@ -714,6 +714,31 @@ describe('POST /user/auth/local/register', () => {
expect(user.invitations.party).to.eql({}); expect(user.invitations.party).to.eql({});
}); });
it('adds a user to a guild on an invite of type other than party', async () => {
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
});
const invite = encrypt(JSON.stringify({
id: group._id,
inviter: groupLeader._id,
sentAt: Date.now(),
}));
const user = await api.post(`/user/auth/local/register?groupInvite=${invite}`, {
username,
email,
password,
confirmPassword: password,
});
expect(user.invitations.guilds[0]).to.eql({
id: group._id,
name: group.name,
inviter: groupLeader._id,
});
});
}); });
context('successful login via api', () => { context('successful login via api', () => {

View File

@@ -96,20 +96,6 @@ describe('PUT /user/auth/update-password', async () => {
}); });
}); });
it('returns an error when newPassword is too long', async () => {
const body = {
password,
newPassword: '12345678910111213141516171819202122232425262728293031323334353637383940',
confirmPassword: '12345678910111213141516171819202122232425262728293031323334353637383940',
};
await expect(user.put(ENDPOINT, body)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('returns an error when confirmPassword is missing', async () => { it('returns an error when confirmPassword is missing', async () => {
const body = { const body = {
password, password,

View File

@@ -35,6 +35,13 @@ describe('GET /world-state', () => {
}); });
}); });
it('returns a string representing the current season for NPC sprites', async () => {
const res = await requester().get('/world-state');
expect(res).to.have.nested.property('npcImageSuffix');
expect(res.npcImageSuffix).to.be.a('string');
});
context('no current event', () => { context('no current event', () => {
beforeEach(async () => { beforeEach(async () => {
sinon.stub(worldState, 'getCurrentEvent').returns(null); sinon.stub(worldState, 'getCurrentEvent').returns(null);

View File

@@ -37,8 +37,6 @@ describe('GET /faq', () => {
expect(res).to.have.property('questions'); expect(res).to.have.property('questions');
expect(res.questions[0]).to.eql({ expect(res.questions[0]).to.eql({
exclusions: [],
heading: 'overview',
question: translate('faqQuestion0'), question: translate('faqQuestion0'),
ios: translate('iosFaqAnswer0'), ios: translate('iosFaqAnswer0'),
}); });
@@ -59,8 +57,6 @@ describe('GET /faq', () => {
expect(res).to.have.property('questions'); expect(res).to.have.property('questions');
expect(res.questions[0]).to.eql({ expect(res.questions[0]).to.eql({
exclusions: [],
heading: 'overview',
question: translate('faqQuestion0'), question: translate('faqQuestion0'),
android: translate('androidFaqAnswer0'), android: translate('androidFaqAnswer0'),
}); });

View File

@@ -202,86 +202,18 @@ describe('POST /user/class/cast/:spellId', () => {
await group.groupLeader.post('/user/class/cast/mpheal'); await group.groupLeader.post('/user/class/cast/mpheal');
promises = []; promises = [];
promises.push(group.groupLeader.sync());
promises.push(group.members[0].sync()); promises.push(group.members[0].sync());
promises.push(group.members[1].sync()); promises.push(group.members[1].sync());
promises.push(group.members[2].sync()); promises.push(group.members[2].sync());
promises.push(group.members[3].sync()); promises.push(group.members[3].sync());
await Promise.all(promises); await Promise.all(promises);
expect(group.groupLeader.stats.mp).to.be.equal(170); // spell caster
expect(group.members[0].stats.mp).to.be.greaterThan(0); // warrior expect(group.members[0].stats.mp).to.be.greaterThan(0); // warrior
expect(group.members[1].stats.mp).to.equal(0); // wizard expect(group.members[1].stats.mp).to.equal(0); // wizard
expect(group.members[2].stats.mp).to.be.greaterThan(0); // rogue expect(group.members[2].stats.mp).to.be.greaterThan(0); // rogue
expect(group.members[3].stats.mp).to.be.greaterThan(0); // healer expect(group.members[3].stats.mp).to.be.greaterThan(0); // healer
}); });
const spellList = [
{
className: 'warrior',
spells: [['smash', 'task'], ['defensiveStance'], ['valorousPresence'], ['intimidate']],
},
{
className: 'wizard',
spells: [['fireball', 'task'], ['mpheal'], ['earth'], ['frost']],
},
{
className: 'healer',
spells: [['heal'], ['brightness'], ['protectAura'], ['healAll']],
},
{
className: 'rogue',
spells: [['pickPocket', 'task'], ['backStab', 'task'], ['toolsOfTrade'], ['stealth']],
},
];
spellList.forEach(async habitClass => {
describe(`For a ${habitClass.className}`, async () => {
habitClass.spells.forEach(async spell => {
describe(`Using ${spell[0]}`, async () => {
it('Deducts MP from spell caster', async () => {
const { groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
members: 3,
});
await groupLeader.update({
'stats.mp': 200, 'stats.class': habitClass.className, 'stats.lvl': 20, 'stats.hp': 40,
});
// need this for task spells and for stealth
const task = await groupLeader.post('/tasks/user', {
text: 'test habit',
type: 'daily',
});
if (spell.length === 2 && spell[1] === 'task') {
await groupLeader.post(`/user/class/cast/${spell[0]}?targetId=${task._id}`);
} else {
await groupLeader.post(`/user/class/cast/${spell[0]}`);
}
await groupLeader.sync();
expect(groupLeader.stats.mp).to.be.lessThan(200);
});
it('works without a party', async () => {
await user.update({
'stats.mp': 200, 'stats.class': habitClass.className, 'stats.lvl': 20, 'stats.hp': 40,
});
// need this for task spells and for stealth
const task = await user.post('/tasks/user', {
text: 'test habit',
type: 'daily',
});
if (spell.length === 2 && spell[1] === 'task') {
await user.post(`/user/class/cast/${spell[0]}?targetId=${task._id}`);
} else {
await user.post(`/user/class/cast/${spell[0]}`);
}
await user.sync();
expect(user.stats.mp).to.be.lessThan(200);
});
});
});
});
});
it('cast bulk', async () => { it('cast bulk', async () => {
let { group, groupLeader } = await createAndPopulateGroup({ // eslint-disable-line prefer-const let { group, groupLeader } = await createAndPopulateGroup({ // eslint-disable-line prefer-const
groupDetails: { type: 'party', privacy: 'private' }, groupDetails: { type: 'party', privacy: 'private' },

View File

@@ -665,7 +665,6 @@ describe('POST /user/auth/local/register', () => {
it('adds a user to a guild on an invite of type other than party', async () => { it('adds a user to a guild on an invite of type other than party', async () => {
const { group, groupLeader } = await createAndPopulateGroup({ const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' }, groupDetails: { type: 'guild', privacy: 'private' },
upgradeToGroupPlan: true,
}); });
const invite = encrypt(JSON.stringify({ const invite = encrypt(JSON.stringify({

View File

@@ -215,7 +215,6 @@ describe('cron utility functions', () => {
it('monthly plan, next date in 3 months', () => { it('monthly plan, next date in 3 months', () => {
const user = baseUserData(60, 0, 'group_plan_auto'); const user = baseUserData(60, 0, 'group_plan_auto');
user.purchased.plan.perkMonthCount = 0;
const planContext = getPlanContext(user, now); const planContext = getPlanContext(user, now);
@@ -225,7 +224,6 @@ describe('cron utility functions', () => {
it('monthly plan, next date in 1 month', () => { it('monthly plan, next date in 1 month', () => {
const user = baseUserData(62, 0, 'group_plan_auto'); const user = baseUserData(62, 0, 'group_plan_auto');
user.purchased.plan.perkMonthCount = 2;
const planContext = getPlanContext(user, now); const planContext = getPlanContext(user, now);
@@ -250,15 +248,5 @@ describe('cron utility functions', () => {
expect(planContext.nextHourglassDate) expect(planContext.nextHourglassDate)
.to.be.sameMoment('2022-07-10T02:00:00.144Z'); .to.be.sameMoment('2022-07-10T02:00:00.144Z');
}); });
it('multi-month plan with perk count', () => {
const user = baseUserData(60, 1, 'basic_3mo');
user.purchased.plan.perkMonthCount = 2;
const planContext = getPlanContext(user, now);
expect(planContext.nextHourglassDate)
.to.be.sameMoment('2022-07-10T02:00:00.144Z');
});
}); });
}); });

View File

@@ -12,9 +12,8 @@ const webhookData = {};
app.use(bodyParser.urlencoded({ app.use(bodyParser.urlencoded({
extended: true, extended: true,
limit: '10mb',
})); }));
app.use(bodyParser.json({ limit: '10mb' })); app.use(bodyParser.json());
app.post('/webhooks/:id', (req, res) => { app.post('/webhooks/:id', (req, res) => {
const { id } = req.params; const { id } = req.params;

View File

@@ -53,8 +53,7 @@ function _requestMaker (user, method, additionalSets = {}) {
if (user && user._id && user.apiToken) { if (user && user._id && user.apiToken) {
request request
.set('x-api-user', user._id) .set('x-api-user', user._id)
.set('x-api-key', user.apiToken) .set('x-api-key', user.apiToken);
.set('x-client', 'habitica-web');
} }
if (!isEmpty(additionalSets)) { if (!isEmpty(additionalSets)) {

View File

@@ -127,9 +127,6 @@ export async function createAndPopulateGroup (settings = {}) {
const upgradeToGroupPlan = settings.upgradeToGroupPlan || false; const upgradeToGroupPlan = settings.upgradeToGroupPlan || false;
const { groupDetails } = settings; const { groupDetails } = settings;
const leaderDetails = settings.leaderDetails || { balance: 10 }; const leaderDetails = settings.leaderDetails || { balance: 10 };
if (upgradeToGroupPlan) {
leaderDetails.permissions = { fullAccess: true };
}
const groupLeader = await generateUser(leaderDetails); const groupLeader = await generateUser(leaderDetails);
const group = await generateGroup(groupLeader, groupDetails); const group = await generateGroup(groupLeader, groupDetails);

View File

@@ -120,9 +120,6 @@ export async function createAndPopulateGroup (settings = {}) {
const upgradeToGroupPlan = settings.upgradeToGroupPlan || false; const upgradeToGroupPlan = settings.upgradeToGroupPlan || false;
const { groupDetails } = settings; const { groupDetails } = settings;
const leaderDetails = settings.leaderDetails || { balance: 10 }; const leaderDetails = settings.leaderDetails || { balance: 10 };
if (upgradeToGroupPlan) {
leaderDetails.permissions = { fullAccess: true };
}
const groupLeader = await generateUser(leaderDetails); const groupLeader = await generateUser(leaderDetails);
const group = await generateGroup(groupLeader, groupDetails); const group = await generateGroup(groupLeader, groupDetails);

File diff suppressed because it is too large Load Diff

View File

@@ -18,44 +18,44 @@
"@storybook/addon-links": "6.5.8", "@storybook/addon-links": "6.5.8",
"@storybook/addon-notes": "5.3.21", "@storybook/addon-notes": "5.3.21",
"@storybook/addons": "6.5.9", "@storybook/addons": "6.5.9",
"@storybook/vue": "6.5.14", "@storybook/vue": "6.3.13",
"@vue/cli-plugin-babel": "^5.0.8", "@vue/cli-plugin-babel": "^4.5.15",
"@vue/cli-plugin-eslint": "^4.5.19", "@vue/cli-plugin-eslint": "^4.5.19",
"@vue/cli-plugin-router": "^5.0.8", "@vue/cli-plugin-router": "^5.0.8",
"@vue/cli-plugin-unit-mocha": "^5.0.8", "@vue/cli-plugin-unit-mocha": "^4.5.15",
"@vue/cli-service": "^4.5.15", "@vue/cli-service": "^4.5.15",
"@vue/test-utils": "1.0.0-beta.29", "@vue/test-utils": "1.0.0-beta.29",
"amplitude-js": "^8.21.3", "amplitude-js": "^8.21.1",
"axios": "^0.27.2", "axios": "^0.27.2",
"axios-progress-bar": "^1.2.0", "axios-progress-bar": "^1.2.0",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"bootstrap": "^4.6.0", "bootstrap": "^4.6.0",
"bootstrap-vue": "^2.23.1", "bootstrap-vue": "^2.22.0",
"chai": "^4.3.7", "chai": "^4.3.6",
"core-js": "^3.31.0", "core-js": "^3.26.0",
"dompurify": "^3.0.3", "dompurify": "^2.4.1",
"eslint": "^6.8.0", "eslint": "^6.8.0",
"eslint-config-habitrpg": "^6.2.0", "eslint-config-habitrpg": "^6.2.0",
"eslint-plugin-mocha": "^5.3.0", "eslint-plugin-mocha": "^5.3.0",
"eslint-plugin-vue": "^6.2.2", "eslint-plugin-vue": "^6.2.2",
"habitica-markdown": "^3.0.0", "habitica-markdown": "^3.0.0",
"hellojs": "^1.20.0", "hellojs": "^1.19.5",
"inspectpack": "^4.7.1", "inspectpack": "^4.7.1",
"intro.js": "^7.0.1", "intro.js": "^6.0.0",
"jquery": "^3.7.0", "jquery": "^3.6.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moment": "^2.29.4", "moment": "^2.29.4",
"nconf": "^0.12.0", "nconf": "^0.12.0",
"sass": "^1.63.4", "sass": "^1.34.0",
"sass-loader": "^8.0.2", "sass-loader": "^8.0.2",
"smartbanner.js": "^1.19.2", "smartbanner.js": "^1.19.1",
"stopword": "^2.0.8", "stopword": "^2.0.5",
"svg-inline-loader": "^0.8.2", "svg-inline-loader": "^0.8.2",
"svg-url-loader": "^7.1.1", "svg-url-loader": "^7.1.1",
"svgo": "^1.3.2", "svgo": "^1.3.2",
"svgo-loader": "^2.2.1", "svgo-loader": "^2.2.1",
"uuid": "^9.0.0", "uuid": "^8.3.2",
"validator": "^13.9.0", "validator": "^13.7.0",
"vue": "^2.7.10", "vue": "^2.7.10",
"vue-cli-plugin-storybook": "2.1.0", "vue-cli-plugin-storybook": "2.1.0",
"vue-mugen-scroll": "^0.2.6", "vue-mugen-scroll": "^0.2.6",
@@ -66,6 +66,6 @@
"webpack": "^4.46.0" "webpack": "^4.46.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.21.0" "@babel/plugin-proposal-optional-chaining": "^7.18.9"
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 B

View File

@@ -35,17 +35,13 @@
<sub-canceled-modal v-if="isUserLoaded" /> <sub-canceled-modal v-if="isUserLoaded" />
<bug-report-modal v-if="isUserLoaded" /> <bug-report-modal v-if="isUserLoaded" />
<bug-report-success-modal v-if="isUserLoaded" /> <bug-report-success-modal v-if="isUserLoaded" />
<external-link-modal />
<birthday-modal />
<snackbars /> <snackbars />
<router-view v-if="!isUserLoggedIn || isStaticPage" /> <router-view v-if="!isUserLoggedIn || isStaticPage" />
<template v-else> <template v-else>
<template v-if="isUserLoaded"> <template v-if="isUserLoaded">
<chat-banner />
<damage-paused-banner /> <damage-paused-banner />
<gems-promo-banner /> <gems-promo-banner />
<gift-promo-banner /> <gift-promo-banner />
<birthday-banner />
<notifications-display /> <notifications-display />
<app-menu /> <app-menu />
<div <div
@@ -157,14 +153,11 @@
import axios from 'axios'; import axios from 'axios';
import { loadProgressBar } from 'axios-progress-bar'; import { loadProgressBar } from 'axios-progress-bar';
import birthdayModal from '@/components/news/birthdayModal';
import AppMenu from './components/header/menu'; import AppMenu from './components/header/menu';
import AppHeader from './components/header/index'; import AppHeader from './components/header/index';
import ChatBanner from './components/header/banners/chatBanner';
import DamagePausedBanner from './components/header/banners/damagePaused'; import DamagePausedBanner from './components/header/banners/damagePaused';
import GemsPromoBanner from './components/header/banners/gemsPromo'; import GemsPromoBanner from './components/header/banners/gemsPromo';
import GiftPromoBanner from './components/header/banners/giftPromo'; import GiftPromoBanner from './components/header/banners/giftPromo';
import BirthdayBanner from './components/header/banners/birthdayBanner';
import AppFooter from './components/appFooter'; import AppFooter from './components/appFooter';
import notificationsDisplay from './components/notifications'; import notificationsDisplay from './components/notifications';
import snackbars from './components/snackbars/notifications'; import snackbars from './components/snackbars/notifications';
@@ -178,7 +171,6 @@ import amazonPaymentsModal from '@/components/payments/amazonModal';
import paymentsSuccessModal from '@/components/payments/successModal'; import paymentsSuccessModal from '@/components/payments/successModal';
import subCancelModalConfirm from '@/components/payments/cancelModalConfirm'; import subCancelModalConfirm from '@/components/payments/cancelModalConfirm';
import subCanceledModal from '@/components/payments/canceledModal'; import subCanceledModal from '@/components/payments/canceledModal';
import externalLinkModal from '@/components/externalLinkModal.vue';
import spellsMixin from '@/mixins/spells'; import spellsMixin from '@/mixins/spells';
import { import {
@@ -199,12 +191,9 @@ export default {
AppMenu, AppMenu,
AppHeader, AppHeader,
AppFooter, AppFooter,
birthdayModal,
ChatBanner,
DamagePausedBanner, DamagePausedBanner,
GemsPromoBanner, GemsPromoBanner,
GiftPromoBanner, GiftPromoBanner,
BirthdayBanner,
notificationsDisplay, notificationsDisplay,
snackbars, snackbars,
BuyModal, BuyModal,
@@ -215,7 +204,6 @@ export default {
subCanceledModal, subCanceledModal,
bugReportModal, bugReportModal,
bugReportSuccessModal, bugReportSuccessModal,
externalLinkModal,
}, },
mixins: [notifications, spellsMixin], mixins: [notifications, spellsMixin],
data () { data () {

View File

@@ -94,12 +94,6 @@
height: 90px; height: 90px;
} }
.back_special_heroicAureole {
width: 114px;
height: 90px;
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_special_heroicAureole.gif") no-repeat;
}
.head_special_0 { .head_special_0 {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-ShadeHelmet.gif") no-repeat; background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-ShadeHelmet.gif") no-repeat;
} }
@@ -162,12 +156,6 @@
height: 99px; height: 99px;
} }
.Pet-Gryphatrice-Jubilant {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Gryphatrice-Jubilant.gif") no-repeat;
width: 81px;
height: 96px;
}
.Mount_Head_Gryphon-Gryphatrice, .Mount_Body_Gryphon-Gryphatrice { .Mount_Head_Gryphon-Gryphatrice, .Mount_Body_Gryphon-Gryphatrice {
width: 135px; width: 135px;
height: 135px; height: 135px;
@@ -198,6 +186,14 @@
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_steamworks.gif") no-repeat; background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_steamworks.gif") no-repeat;
} }
/* FIXME figure out how to handle customize menu!!
.customize-menu .f_head_0 {
width: 60px;
height: 60px;
background-position: -1917px -9px;
}
*/
[class*="Mount_Head_"], [class*="Mount_Head_"],
[class*="Mount_Body_"] { [class*="Mount_Body_"] {
margin-top:18px; /* Sprite accommodates 105x123 box */ margin-top:18px; /* Sprite accommodates 105x123 box */

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 850 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -19,12 +19,8 @@
top: -16px !important; top: -16px !important;
} }
$foolPets: Veggie, Dessert, VirtualPet, TeaShop; .Pet.Pet-FlyingPig-Veggie, .Pet.Pet-FlyingPig-Dessert, .Pet.Pet-FlyingPig-VirtualPet {
top: -28px !important;
@each $foolPet in $foolPets {
.Pet.Pet-FlyingPig-#{$foolPet} {
top: -28px !important;
}
} }
.Pet[class*="Virtual"] { .Pet[class*="Virtual"] {

View File

@@ -24,9 +24,9 @@
} }
} }
.icon-10 { .icon-16 {
width: 10px; width: 16px;
height: 10px; height: 16px;
} }
.icon-12 { .icon-12 {
@@ -34,26 +34,21 @@
height: 12px; height: 12px;
} }
.icon-16 {
width: 16px;
height: 16px;
}
.icon-24 { .icon-24 {
width: 24px; width: 24px;
height: 24px; height: 24px;
} }
.icon-32 {
width: 32px;
height: 32px;
}
.icon-48 { .icon-48 {
width: 48px; width: 48px;
height: 48px; height: 48px;
} }
.icon-10 {
width: 10px;
height: 10px;
}
.inline { .inline {
display: inline-block; display: inline-block;
} }

View File

@@ -50,7 +50,10 @@ h3.markdown {
} }
a { a {
color: $blue-10;
&:hover, &:active, &:focus { &:hover, &:active, &:focus {
color: $blue-10;
text-decoration: underline; text-decoration: underline;
} }
} }

View File

@@ -26,17 +26,19 @@ a:not([href]), a:not([href]):hover {
a, a:not([href]):not([tabindex]) { a, a:not([href]):not([tabindex]) {
cursor: pointer; cursor: pointer;
color: $purple-300;
&:hover, &:active, &:focus { &.standard-link {
text-decoration: underline; color: $blue-10;
color: $purple-300;
}
&[disabled="disabled"] { &:hover, &:active, &:focus {
color: $gray-300; text-decoration: underline;
text-decoration: none; }
cursor: default;
&[disabled="disabled"] {
color: $gray-300;
text-decoration: none;
cursor: default;
}
} }
&.small-link { &.small-link {

View File

@@ -1,61 +0,0 @@
<svg width="199" height="24" viewBox="0 0 199 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#c19w6aye5a)" fill="#fff">
<path d="M56.47 18.83V6.003L56 3.662l.47-1.405h8.942c1.773 0 3.193.344 4.26 1.03 1.066.687 1.6 1.733 1.6 3.137 0 .765-.142 1.397-.424 1.896a4.175 4.175 0 0 1-1.035 1.24 4.14 4.14 0 0 1 1.505.703c.471.327.855.772 1.154 1.334.297.546.447 1.225.447 2.036 0 1.639-.487 2.918-1.46 3.839-.956.905-2.502 1.358-4.635 1.358H56.471zm5.177-10.136h2.777c.533 0 .918-.078 1.153-.234.235-.171.353-.429.353-.772 0-.359-.173-.609-.518-.75-.345-.155-.753-.233-1.223-.233h-2.542v1.99zm0 5.688h4c.628 0 1.067-.093 1.318-.28a.974.974 0 0 0 .377-.796c0-.75-.486-1.124-1.46-1.124h-4.235v2.2zM75.515 18.83V6.003l-.236-2.341.236-1.405h5.2V18.83h-5.2zM84 18.83V6.026l-.471-2.364.47-1.405h8.472c1.27 0 2.36.203 3.27.61.91.405 1.608 1.06 2.095 1.965.486.89.73 2.068.73 3.535 0 1.357-.228 2.473-.683 3.347a4.552 4.552 0 0 1-2 1.99l.4.327 1.623 2.2 1.341 1.194v1.405h-6.235l-2.588-4.284h-1.248v.515l.236 2.34-.236 1.429H84zm5.176-8.66h1.365c.55 0 1.02-.024 1.412-.071.408-.047.722-.187.941-.421.22-.25.33-.648.33-1.194 0-.578-.118-.991-.353-1.24-.236-.25-.565-.399-.989-.446a9.614 9.614 0 0 0-1.435-.093h-1.506l.235 3.464zM104.666 18.83V6.728l-4.706.234V2.257h14.707v4.705l-4.824-.234v8.357l.259 2.34-.259 1.405h-5.177zM116.785 18.83V6.026l-.235-2.34.235-1.429h5.177v6.344h4.918V2.257h5.177v8.802l.235 1.615v6.156h-5.412v-5.946h-4.918v1.639l.235 4.307h-5.412zM135.588 18.83V6.026l-.471-2.34.471-1.429h7.977c1.114 0 2.188.11 3.223.328 1.051.203 1.985.593 2.801 1.17.831.578 1.49 1.405 1.976 2.482.486 1.076.73 2.473.73 4.19 0 1.716-.251 3.128-.753 4.236-.487 1.092-1.146 1.943-1.977 2.552a7.477 7.477 0 0 1-2.8 1.264 14.463 14.463 0 0 1-3.2.35h-7.977zm5.224-4.448h1.788c.926 0 1.702-.101 2.33-.304a2.498 2.498 0 0 0 1.458-1.147c.33-.577.495-1.412.495-2.504 0-1.108-.173-1.92-.518-2.435-.33-.53-.816-.874-1.459-1.03-.628-.171-1.396-.257-2.306-.257h-1.788v7.677zM153.013 18.83l1.13-3.956V11.9l1.741-.702 3.294-8.918h7.083l4.024 10.486 1.812 3.324v2.739h-5.106l-1.177-3.488h-6.377l-1.012 3.488h-5.412zm7.977-7.584h3.53l-1.553-4.658h-.494l-1.483 4.658zM176.04 18.83v-6.788l-5.835-8.38V2.257h5.906l2.353 4.822h.47l2.33-4.822h5.883v1.405l-6.001 8.52.141 2.364v4.284h-5.247zM191.923 12.72l-2.07-8.847L192.676 2l2.8 1.896-2.141 8.824h-1.412zm.518 7.28-3.059-3.043 3.059-3.043 3.059 3.043L192.441 20z"/>
</g>
<g filter="url(#s1alkvv8kb)">
<path d="M5.87 18.825V7.601H3V3.17l8.228-.937.239 1.406-.24 2.344v12.841H5.87z" fill="url(#xidihnl5xc)"/>
<path d="M21.258 19.06a9.043 9.043 0 0 1-2.87-.446 6.484 6.484 0 0 1-2.369-1.453c-.67-.671-1.195-1.546-1.578-2.624-.383-1.094-.574-2.43-.574-4.007 0-1.562.191-2.883.574-3.96.382-1.094.909-1.977 1.578-2.648a6.092 6.092 0 0 1 2.368-1.453A8.63 8.63 0 0 1 21.257 2c1.356 0 2.584.281 3.684.844 1.116.562 2.001 1.468 2.655 2.718.67 1.234 1.004 2.89 1.004 4.968s-.335 3.741-1.004 4.991c-.654 1.25-1.539 2.156-2.655 2.718-1.1.547-2.328.82-3.683.82zm0-5.039c.701 0 1.187-.25 1.459-.75.27-.5.406-1.413.406-2.741 0-1.313-.136-2.219-.407-2.719-.27-.515-.757-.773-1.459-.773-.685 0-1.18.258-1.483.773-.287.516-.43 1.422-.43 2.719 0 1.312.143 2.226.43 2.742.303.5.798.75 1.483.75z" fill="url(#9hqzmmkygd)"/>
<path d="M32.721 12.014V4.745l-2.87.14V2.06h8.97v2.826l-2.943-.141v5.02l.158 1.405-.158.844h-3.157z" fill="url(#bzq8gpt5ve)"/>
<path d="M40.543 12.014v-7.69l-.144-1.407.144-.857H43.7v3.81h3V2.06h3.156v5.286l.144.97v3.698h-3.3V8.443h-3v.984l.143 2.587h-3.3z" fill="url(#4t6arxwa4f)"/>
</g>
<defs>
<linearGradient id="xidihnl5xc" x1="3" y1="2" x2="29.822" y2="35.308" gradientUnits="userSpaceOnUse">
<stop stop-color="#6133B4"/>
<stop offset="1" stop-color="#4F2A93"/>
</linearGradient>
<linearGradient id="9hqzmmkygd" x1="3" y1="2" x2="29.822" y2="35.308" gradientUnits="userSpaceOnUse">
<stop stop-color="#6133B4"/>
<stop offset="1" stop-color="#4F2A93"/>
</linearGradient>
<linearGradient id="bzq8gpt5ve" x1="3" y1="2" x2="29.822" y2="35.308" gradientUnits="userSpaceOnUse">
<stop stop-color="#6133B4"/>
<stop offset="1" stop-color="#4F2A93"/>
</linearGradient>
<linearGradient id="4t6arxwa4f" x1="3" y1="2" x2="29.822" y2="35.308" gradientUnits="userSpaceOnUse">
<stop stop-color="#6133B4"/>
<stop offset="1" stop-color="#4F2A93"/>
</linearGradient>
<filter id="c19w6aye5a" x="53" y="0" width="145.5" height="24" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="1"/>
<feGaussianBlur stdDeviation="1.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix values="0 0 0 0 0.101961 0 0 0 0 0.0941176 0 0 0 0 0.113725 0 0 0 0.12 0"/>
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_45_799"/>
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="1"/>
<feGaussianBlur stdDeviation="1"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix values="0 0 0 0 0.101961 0 0 0 0 0.0941176 0 0 0 0 0.113725 0 0 0 0.24 0"/>
<feBlend in2="effect1_dropShadow_45_799" result="effect2_dropShadow_45_799"/>
<feBlend in="SourceGraphic" in2="effect2_dropShadow_45_799" result="shape"/>
</filter>
<filter id="s1alkvv8kb" x="0" y="0" width="53" height="23.059" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="1"/>
<feGaussianBlur stdDeviation="1.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix values="0 0 0 0 0.101961 0 0 0 0 0.0941176 0 0 0 0 0.113725 0 0 0 0.12 0"/>
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_45_799"/>
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="1"/>
<feGaussianBlur stdDeviation="1"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix values="0 0 0 0 0.101961 0 0 0 0 0.0941176 0 0 0 0 0.113725 0 0 0 0.24 0"/>
<feBlend in2="effect1_dropShadow_45_799" result="effect2_dropShadow_45_799"/>
<feBlend in="SourceGraphic" in2="effect2_dropShadow_45_799" result="shape"/>
</filter>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 6.8 KiB

View File

@@ -1,22 +0,0 @@
<svg width="58" height="48" viewBox="0 0 58 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="m16.853 4.36 7.959-1.453-2.71 7.556-2.708 7.557-5.25-6.103-5.25-6.103 7.959-1.453z" fill="#5DDEAB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.771 1.454 40.731 0l-2.71 7.556-2.709 7.556-5.25-6.102-5.25-6.103 7.96-1.453z" fill="#5DDEAB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="m43.272 13.659-7.96 1.453 2.71-7.556L40.73 0l5.25 6.103 5.25 6.102-7.96 1.454z" fill="#38C38D"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="m27.353 16.566-7.96 1.453 2.71-7.556 2.709-7.556 5.25 6.103 5.25 6.102-7.96 1.454zM11.434 19.473l-7.959 1.453 2.71-7.556 2.708-7.556 5.25 6.103 5.25 6.102-7.959 1.454z" fill="#B0F1D7"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="m3.475 20.926 28.05 18.662L19.394 18.02 3.475 20.926z" fill="#38C38D"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M51.249 12.202 31.525 39.588l3.805-24.48 15.919-2.906z" fill="#B0F1D7"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="m19.394 18.02 12.131 21.568 3.787-24.476-15.918 2.907z" fill="#5DDEAB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="m51.904 26.44-3.832-.897 1.132 3.736 1.132 3.737 2.7-2.84 2.7-2.84-3.832-.896zM44.24 24.647l-3.832-.897 1.132 3.736 1.132 3.736 2.7-2.84 2.7-2.839-3.832-.896z" fill="#87E3E1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="m38.84 30.326 3.832.896-1.132-3.736-1.132-3.736-2.7 2.84-2.7 2.839 3.832.897zM46.504 32.12l3.832.896-1.132-3.736-1.132-3.737-2.7 2.84-2.7 2.84 3.832.896z" fill="#C0FBFA"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M54.168 33.912 58 34.81l-1.132-3.736-1.133-3.736-2.7 2.84-2.7 2.839 3.833.896z" fill="#5EC5C2"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="m58 34.81-14.084 8.395 6.42-10.19L58 34.81z" fill="#C0FBFA"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="m35 29.427 8.916 13.779-1.252-11.986L35 29.427z" fill="#5EC5C2"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="m50.336 33.016-6.42 10.19-1.244-11.984 7.664 1.794z" fill="#87E3E1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="m16.877 22.666-5.078 1.971 4.262 3.372 4.262 3.37.816-5.341.816-5.343-5.078 1.971zM6.721 26.609l-5.078 1.97 4.262 3.372 4.262 3.371.816-5.342.816-5.343-5.078 1.972z" fill="#7BE3CF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="m5.09 37.294 5.077-1.972-4.262-3.371-4.261-3.371-.817 5.342-.816 5.343 5.078-1.971z" fill="#C5F3EA"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="m15.245 33.351 5.078-1.971-4.262-3.371-4.262-3.371-.816 5.342-.816 5.342 5.078-1.97z" fill="#C5F3EA"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="m25.4 29.41 5.078-1.972-4.262-3.371-4.261-3.372-.816 5.343-.816 5.342 5.078-1.97z" fill="#41C7AF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M30.478 27.438 21.117 48l-.794-16.62 10.155-3.942z" fill="#C5F3EA"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="m0 39.269 21.117 8.73-10.961-12.672L0 39.269z" fill="#41C7AF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.323 31.38 21.117 48l-10.95-12.678 10.156-3.942z" fill="#7BE3CF"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -1,22 +0,0 @@
<svg width="518" height="152" viewBox="0 0 518 152" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M144.48 65.487v5.042h-1.772v-5.042h1.772zm1.621 6.671h5.013v1.782h-5.013v-1.782zm-10.027 0h5.013v1.782h-5.013v-1.782zm8.406 3.412v5.041h-1.772V75.57h1.772z" fill="#36205D" style="mix-blend-mode:multiply" opacity=".5"/>
<path opacity=".92" fill-rule="evenodd" clip-rule="evenodd" d="m9.504 29.894 2.707-4.715 1.658.962-2.707 4.715-1.658-.962zm2.066-7.12-4.689-2.722.958-1.667 4.688 2.723-.957 1.667zm9.378 5.445-4.689-2.722.957-1.667 4.69 2.722-.958 1.667zm-6.03-7.755 2.707-4.715 1.658.962-2.707 4.715-1.658-.962z" fill="#fff"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="m60.85 11.508.708-3.662 1.288.251-.708 3.662-1.288-.252zm-.24-5.076-3.642-.712.25-1.295 3.642.712-.25 1.295zm7.283 1.423-3.642-.711.25-1.295 3.642.712-.25 1.294zm-5.627-3.671.708-3.662 1.287.251-.708 3.662-1.287-.251z" fill="#36205D" style="mix-blend-mode:multiply" opacity=".81"/>
<path opacity=".76" fill-rule="evenodd" clip-rule="evenodd" d="m107.034 22.162.493 5.675-1.995.175-.494-5.674 1.996-.176zm2.477 7.349 5.643-.497.175 2.007-5.644.496-.174-2.006zm-11.287.993 5.644-.497.174 2.007-5.643.496-.175-2.006zm9.797 3.008.494 5.675-1.996.175-.493-5.675 1.995-.175z" fill="#fff"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="m16.191 92.492-5.006 1.636-.575-1.78 5.006-1.636.575 1.78zm-6.098 3.792 1.626 5.034-1.77.578-1.626-5.034 1.77-.578zM6.839 86.215l1.627 5.035-1.77.578-1.627-5.034 1.77-.579zm-.66 9.549-5.006 1.635-.576-1.78 5.007-1.635.575 1.78z" fill="#36205D" style="mix-blend-mode:multiply" opacity=".91"/>
<path opacity=".92" fill-rule="evenodd" clip-rule="evenodd" d="m35.176 59.176 5.102-1.97.692 1.814-5.101 1.97-.693-1.814zm6.118-4.264-1.958-5.13 1.803-.696 1.958 5.13-1.803.696zm3.916 10.26-1.958-5.13 1.804-.696 1.958 5.13-1.804.696zm.17-9.935 5.1-1.969.693 1.814-5.101 1.969-.693-1.814z" fill="#fff"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="m95.733 86.583-4.383 2.649-.931-1.559 4.383-2.648.93 1.558zm-4.949 4.93 2.634 4.407-1.55.936-2.633-4.407 1.55-.937zm-5.267-8.816 2.633 4.408-1.55.936-2.633-4.408 1.55-.936zm1.45 9.183-4.384 2.648-.93-1.558 4.382-2.648.931 1.558z" fill="#36205D" style="mix-blend-mode:multiply"/>
<path opacity=".98" fill-rule="evenodd" clip-rule="evenodd" d="m24.804 132.406-2.1-3.015 1.06-.746 2.1 3.014-1.06.747zm-3.747-3.307-2.998 2.111-.742-1.066 2.998-2.111.742 1.066zm5.996-4.222-2.998 2.111-.742-1.066 2.998-2.11.742 1.065zm-6.447 1.5-2.1-3.015 1.06-.746 2.1 3.014-1.06.747z" fill="#fff"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="m60.65 142.295-1.594 3.144-1.105-.567 1.593-3.144 1.105.567zm-1.098 4.678 3.126 1.602-.563 1.112-3.127-1.602.564-1.112zm-6.254-3.204 3.127 1.602-.563 1.112-3.127-1.603.563-1.111zm4.165 4.814-1.593 3.144-1.106-.566 1.593-3.144 1.106.566z" fill="#36205D" style="mix-blend-mode:multiply" opacity=".82"/>
<path opacity=".71" fill-rule="evenodd" clip-rule="evenodd" d="m110.507 140.233 2.321-4.582 1.611.826-2.321 4.581-1.611-.825zm1.599-6.817-4.556-2.335.821-1.62 4.556 2.335-.821 1.62zm9.112 4.669-4.556-2.335.821-1.62 4.556 2.335-.821 1.62zm-6.068-7.015 2.321-4.582 1.611.825-2.321 4.582-1.611-.825z" fill="#fff"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M373.52 65.487v5.042h1.772v-5.042h-1.772zm-1.621 6.671h-5.013v1.782h5.013v-1.782zm10.027 0h-5.013v1.782h5.013v-1.782zm-8.406 3.412v5.041h1.772V75.57h-1.772z" fill="#36205D" style="mix-blend-mode:multiply" opacity=".5"/>
<path opacity=".92" fill-rule="evenodd" clip-rule="evenodd" d="m508.496 29.894-2.707-4.715-1.658.962 2.707 4.715 1.658-.962zm-2.066-7.12 4.689-2.722-.958-1.667-4.689 2.723.958 1.667zm-9.378 5.445 4.689-2.722-.957-1.667-4.689 2.722.957 1.667zm6.03-7.755-2.707-4.715-1.658.962 2.707 4.715 1.658-.962z" fill="#fff"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="m457.15 11.508-.708-3.662-1.287.251.707 3.662 1.288-.252zm.24-5.076 3.642-.712-.25-1.295-3.642.712.25 1.295zm-7.283 1.423 3.642-.711-.251-1.295-3.641.712.25 1.294zm5.627-3.671-.708-3.662-1.287.251.708 3.662 1.287-.251z" fill="#36205D" style="mix-blend-mode:multiply" opacity=".81"/>
<path opacity=".76" fill-rule="evenodd" clip-rule="evenodd" d="m410.966 22.162-.493 5.675 1.995.175.494-5.674-1.996-.176zm-2.477 7.349-5.643-.497-.175 2.007 5.644.496.174-2.006zm11.287.993-5.644-.497-.174 2.007 5.643.496.175-2.006zm-9.797 3.008-.494 5.675 1.996.175.493-5.675-1.995-.175z" fill="#fff"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="m501.809 92.492 5.006 1.636.575-1.78-5.006-1.636-.575 1.78zm6.098 3.792-1.626 5.034 1.77.578 1.626-5.034-1.77-.578zm3.254-10.069-1.627 5.035 1.77.578 1.627-5.034-1.77-.579zm.66 9.549 5.006 1.635.576-1.78-5.007-1.635-.575 1.78z" fill="#36205D" style="mix-blend-mode:multiply" opacity=".91"/>
<path opacity=".92" fill-rule="evenodd" clip-rule="evenodd" d="m482.824 59.176-5.102-1.97-.692 1.814 5.101 1.97.693-1.814zm-6.118-4.264 1.958-5.13-1.803-.696-1.958 5.13 1.803.696zm-3.916 10.26 1.958-5.13-1.804-.696-1.958 5.13 1.804.696zm-.169-9.935-5.102-1.969-.692 1.814 5.101 1.969.693-1.814z" fill="#fff"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="m422.267 86.583 4.383 2.649.932-1.559-4.384-2.648-.931 1.558zm4.949 4.93-2.634 4.407 1.55.936 2.634-4.407-1.55-.937zm5.267-8.816-2.633 4.408 1.549.936 2.634-4.408-1.55-.936zm-1.449 9.183 4.383 2.648.931-1.558-4.383-2.648-.931 1.558z" fill="#36205D" style="mix-blend-mode:multiply"/>
<path opacity=".98" fill-rule="evenodd" clip-rule="evenodd" d="m493.196 132.406 2.099-3.015-1.06-.746-2.099 3.014 1.06.747zm3.747-3.307 2.998 2.111.742-1.066-2.998-2.111-.742 1.066zm-5.996-4.222 2.998 2.111.742-1.066-2.998-2.11-.742 1.065zm6.447 1.5 2.1-3.015-1.06-.746-2.099 3.014 1.059.747z" fill="#fff"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="m457.351 142.295 1.593 3.144 1.105-.567-1.593-3.144-1.105.567zm1.097 4.678-3.126 1.602.563 1.112 3.127-1.602-.564-1.112zm6.254-3.204-3.127 1.602.563 1.112 3.127-1.603-.563-1.111zm-4.165 4.814 1.593 3.144 1.106-.566-1.593-3.144-1.106.566z" fill="#36205D" style="mix-blend-mode:multiply" opacity=".82"/>
<path opacity=".71" fill-rule="evenodd" clip-rule="evenodd" d="m407.493 140.233-2.321-4.582-1.611.826 2.321 4.581 1.611-.825zm-1.599-6.817 4.556-2.335-.821-1.62-4.556 2.335.821 1.62zm-9.112 4.669 4.556-2.335-.821-1.62-4.556 2.335.821 1.62zm6.068-7.015-2.321-4.582-1.611.825 2.321 4.582 1.611-.825z" fill="#fff"/>
</svg>

Before

Width:  |  Height:  |  Size: 6.4 KiB

View File

@@ -1,3 +0,0 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.26512 0L4.84341 3.57829L3.57829 4.84341L0 1.26512L1.26512 0ZM7.15659 3.57829L10.7349 5.33207e-08L12 1.26512L8.42171 4.84341L7.15659 3.57829ZM5.33207e-08 10.7349L3.57829 7.15659L4.84341 8.42171L1.26512 12L5.33207e-08 10.7349ZM8.42171 7.15659L12 10.7349L10.7349 12L7.15659 8.42171L8.42171 7.15659Z" fill="#FFB445"/>
</svg>

Before

Width:  |  Height:  |  Size: 469 B

View File

@@ -1,4 +0,0 @@
<svg width="138" height="12" viewBox="0 0 138 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="m127.265 0 3.578 3.578-1.265 1.265L126 1.265 127.265 0zm5.892 3.578L136.735 0 138 1.265l-3.578 3.578-1.265-1.265zM126 10.735l3.578-3.578 1.265 1.265L127.265 12 126 10.735zm8.422-3.578L138 10.735 136.735 12l-3.578-3.578 1.265-1.265z" fill="#FFB445"/>
<path d="M114.445 4.555 112.5 1l-1.945 3.555L107.914 6h-3.828l-1.349-.737L101.5 3l-1.237 2.263L98.914 6H0v1h98.914l1.349.737L101.5 10l1.237-2.263L104.086 7h3.828l2.641 1.445L112.5 12l1.945-3.555L118 6.5l-3.555-1.945z" fill="#36205D"/>
</svg>

Before

Width:  |  Height:  |  Size: 647 B

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