Merge branch 'develop' into issue-11615
@@ -1,3 +1,2 @@
|
||||
node_modules
|
||||
.git
|
||||
website
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
FROM node:12
|
||||
WORKDIR /code
|
||||
COPY package*.json /code/
|
||||
RUN npm install
|
||||
RUN npm install -g gulp-cli mocha
|
||||
FROM node:12
|
||||
|
||||
# Install global packages
|
||||
RUN npm install -g gulp-cli mocha
|
||||
|
||||
# Copy Habitica code into container and install dependencies
|
||||
WORKDIR /usr/src/habitica
|
||||
COPY . /usr/src/habitica
|
||||
|
||||
RUN npm install
|
||||
RUN npm run postinstall
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Award Onboarding Achievements for existing users
|
||||
*/
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import { model as User } from '../../website/server/models/user';
|
||||
|
||||
const MIGRATION_NAME = '20191218_onboarding_achievements';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count += 1;
|
||||
|
||||
const set = {};
|
||||
|
||||
set.migration = MIGRATION_NAME;
|
||||
|
||||
const hasPet = Object.keys(user.items.pets).find(petKey => {
|
||||
const pet = user.items.pets[petKey];
|
||||
|
||||
if (pet >= 5) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
if (hasPet) {
|
||||
set['achievements.hatchedPet'] = true;
|
||||
}
|
||||
|
||||
const hasFedPet = Object.keys(user.items.pets).find(petKey => {
|
||||
const pet = user.items.pets[petKey];
|
||||
|
||||
if (pet > 5) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
if (hasFedPet) {
|
||||
set['achievements.fedPet'] = true;
|
||||
}
|
||||
|
||||
const hasGear = Object.keys(user.items.gear.owned).find(gearKey => {
|
||||
const gear = user.items.gear.owned[gearKey];
|
||||
|
||||
if (gear === true && gearKey.indexOf('_special_') === -1) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
if (hasGear) {
|
||||
set['achievements.purchasedEquipment'] = true;
|
||||
}
|
||||
|
||||
if (user.tasksOrder) {
|
||||
const hasTask = Object.keys(user.tasksOrder).find(tasksOrderType => {
|
||||
const order = user.tasksOrder[tasksOrderType];
|
||||
|
||||
if (order && order.length > 0) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
if (hasTask) {
|
||||
set['achievements.createdTask'] = true;
|
||||
}
|
||||
|
||||
const hasExperience = user.stats && user.stats.exp && user.stats.exp > 0;
|
||||
if (hasTask && hasExperience) {
|
||||
set['achievements.completedTask'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
return User.update({ _id: user._id }, { $set: set }).exec();
|
||||
}
|
||||
|
||||
module.exports = async function processUsers () { // eslint-disable-line import/no-commonjs
|
||||
const query = {
|
||||
migration: { $ne: MIGRATION_NAME },
|
||||
};
|
||||
|
||||
const fields = {
|
||||
_id: 1,
|
||||
stats: 1,
|
||||
items: 1,
|
||||
achievements: 1,
|
||||
tasksOrder: 1,
|
||||
};
|
||||
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
const users = await User // eslint-disable-line no-await-in-loop
|
||||
.find(query)
|
||||
.limit(100)
|
||||
.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
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,126 @@
|
||||
/* eslint-disable no-console */
|
||||
const MIGRATION_NAME = '20191127_harvest_feast';
|
||||
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++;
|
||||
|
||||
const set = {};
|
||||
let inc;
|
||||
let push;
|
||||
|
||||
set.migration = MIGRATION_NAME;
|
||||
|
||||
if (typeof user.items.gear.owned.head_special_turkeyHelmGilded !== 'undefined') {
|
||||
inc = {
|
||||
'items.food.Pie_Base': 1,
|
||||
'items.food.Pie_CottonCandyBlue': 1,
|
||||
'items.food.Pie_CottonCandyPink': 1,
|
||||
'items.food.Pie_Desert': 1,
|
||||
'items.food.Pie_Golden': 1,
|
||||
'items.food.Pie_Red': 1,
|
||||
'items.food.Pie_Shade': 1,
|
||||
'items.food.Pie_Skeleton': 1,
|
||||
'items.food.Pie_Zombie': 1,
|
||||
'items.food.Pie_White': 1,
|
||||
}
|
||||
} else if (typeof user.items.gear.owned.armor_special_turkeyArmorBase !== 'undefined') {
|
||||
set['items.gear.owned.head_special_turkeyHelmGilded'] = false;
|
||||
set['items.gear.owned.armor_special_turkeyArmorGilded'] = false;
|
||||
set['items.gear.owned.back_special_turkeyTailGilded'] = false;
|
||||
push = [
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.head_special_turkeyHelmGilded',
|
||||
_id: uuid(),
|
||||
},
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.armor_special_turkeyArmorGilded',
|
||||
_id: uuid(),
|
||||
},
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.back_special_turkeyTailGilded',
|
||||
_id: uuid(),
|
||||
},
|
||||
];
|
||||
} else if (user.items && user.items.mounts && user.items.mounts['Turkey-Gilded']) {
|
||||
set['items.gear.owned.head_special_turkeyHelmBase'] = false;
|
||||
set['items.gear.owned.armor_special_turkeyArmorBase'] = false;
|
||||
set['items.gear.owned.back_special_turkeyTailBase'] = false;
|
||||
push = [
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.head_special_turkeyHelmBase',
|
||||
_id: uuid(),
|
||||
},
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.armor_special_turkeyArmorBase',
|
||||
_id: uuid(),
|
||||
},
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.back_special_turkeyTailBase',
|
||||
_id: uuid(),
|
||||
},
|
||||
];
|
||||
} else if (user.items && user.items.pets && user.items.pets['Turkey-Gilded']) {
|
||||
set['items.mounts.Turkey-Gilded'] = true;
|
||||
} else if (user.items && user.items.mounts && user.items.mounts['Turkey-Base']) {
|
||||
set['items.pets.Turkey-Gilded'] = 5;
|
||||
} else if (user.items && user.items.pets && user.items.pets['Turkey-Base']) {
|
||||
set['items.mounts.Turkey-Base'] = true;
|
||||
} else {
|
||||
set['items.pets.Turkey-Base'] = 5;
|
||||
}
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
if (inc) {
|
||||
return await User.update({_id: user._id}, {$inc: inc, $set: set}).exec();
|
||||
} else if (push) {
|
||||
return await User.update({_id: user._id}, {$set: set, $push: {pinnedItems: {$each: push}}}).exec();
|
||||
} else {
|
||||
return await User.update({_id: user._id}, {$set: set}).exec();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = async function processUsers () {
|
||||
let query = {
|
||||
migration: {$ne: MIGRATION_NAME},
|
||||
'auth.timestamps.loggedin': {$gt: new Date('2019-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],
|
||||
};
|
||||
}
|
||||
|
||||
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
/* eslint-disable no-console */
|
||||
const MIGRATION_NAME = '20191210_pet_color_achievements';
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count++;
|
||||
|
||||
let set = {
|
||||
migration: MIGRATION_NAME,
|
||||
};
|
||||
|
||||
if (user && user.items && user.items.pets) {
|
||||
const pets = user.items.pets;
|
||||
if (pets['Wolf-White'] > 0
|
||||
&& pets['TigerCub-White'] > 0
|
||||
&& pets['PandaCub-White'] > 0
|
||||
&& pets['LionCub-White'] > 0
|
||||
&& pets['Fox-White'] > 0
|
||||
&& pets['FlyingPig-White'] > 0
|
||||
&& pets['Dragon-White'] > 0
|
||||
&& pets['Cactus-White'] > 0
|
||||
&& pets['BearCub-White'] > 0) {
|
||||
set['achievements.primedForPainting'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (user && user.items && user.items.mounts) {
|
||||
const mounts = user.items.mounts;
|
||||
if (mounts['Wolf-White']
|
||||
&& mounts['TigerCub-White']
|
||||
&& mounts['PandaCub-White']
|
||||
&& mounts['LionCub-White']
|
||||
&& mounts['Fox-White']
|
||||
&& mounts['FlyingPig-White']
|
||||
&& mounts['Dragon-White']
|
||||
&& mounts['Cactus-White']
|
||||
&& mounts['BearCub-White'] ) {
|
||||
set['achievements.pearlyPro'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
return await User.update({ _id: user._id }, { $set: set }).exec();
|
||||
}
|
||||
|
||||
module.exports = async function processUsers () {
|
||||
let query = {
|
||||
migration: { $ne: MIGRATION_NAME },
|
||||
'auth.timestamps.loggedin': { $gt: new Date('2019-12-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
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,118 @@
|
||||
/* eslint-disable no-console */
|
||||
const MIGRATION_NAME = '20191231_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 = {'flags.newStuff': true};
|
||||
let push;
|
||||
|
||||
set.migration = MIGRATION_NAME;
|
||||
|
||||
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 = {
|
||||
migration: {$ne: MIGRATION_NAME},
|
||||
};
|
||||
|
||||
const fields = {
|
||||
_id: 1,
|
||||
items: 1,
|
||||
};
|
||||
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
const users = await User // eslint-disable-line no-await-in-loop
|
||||
.find(query)
|
||||
.limit(250)
|
||||
.sort({_id: 1})
|
||||
.select(fields)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
console.warn(`\n${count} users processed\n`);
|
||||
break;
|
||||
} else {
|
||||
query._id = {
|
||||
$gt: users[users.length - 1],
|
||||
};
|
||||
}
|
||||
|
||||
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
};
|
||||
@@ -1,110 +0,0 @@
|
||||
import monk from 'monk';
|
||||
import nconf from 'nconf';
|
||||
|
||||
const migrationName = 'mystery-items-201808.js'; // Update per month
|
||||
const authorName = 'Sabe'; // in case script author needs to know when their ...
|
||||
const authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
|
||||
|
||||
/*
|
||||
* Award this month's mystery items to subscribers
|
||||
*/
|
||||
const MYSTERY_ITEMS = ['armor_mystery_201810', 'head_mystery_201810'];
|
||||
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING');
|
||||
|
||||
let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false });
|
||||
let UserNotification = require('../../website/server/models/userNotification').model;
|
||||
|
||||
function processUsers (lastId) {
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
let query = {
|
||||
migration: {$ne: migrationName},
|
||||
'purchased.plan.customerId': { $ne: null },
|
||||
$or: [
|
||||
{ 'purchased.plan.dateTerminated': { $gte: new Date() } },
|
||||
{ 'purchased.plan.dateTerminated': { $exists: false } },
|
||||
{ 'purchased.plan.dateTerminated': { $eq: null } },
|
||||
],
|
||||
};
|
||||
|
||||
if (lastId) {
|
||||
query._id = {
|
||||
$gt: lastId,
|
||||
};
|
||||
}
|
||||
|
||||
dbUsers.find(query, {
|
||||
sort: {_id: 1},
|
||||
limit: 250,
|
||||
fields: [
|
||||
], // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
|
||||
})
|
||||
.then(updateUsers)
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
return exiting(1, `ERROR! ${ err}`);
|
||||
});
|
||||
}
|
||||
|
||||
let progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
function updateUsers (users) {
|
||||
if (!users || users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
displayData();
|
||||
return;
|
||||
}
|
||||
|
||||
let userPromises = users.map(updateUser);
|
||||
let lastUser = users[users.length - 1];
|
||||
|
||||
return Promise.all(userPromises)
|
||||
.then(() => {
|
||||
processUsers(lastUser._id);
|
||||
});
|
||||
}
|
||||
|
||||
function updateUser (user) {
|
||||
count++;
|
||||
|
||||
const addToSet = {
|
||||
'purchased.plan.mysteryItems': {
|
||||
$each: MYSTERY_ITEMS,
|
||||
},
|
||||
};
|
||||
const push = {
|
||||
notifications: (new UserNotification({
|
||||
type: 'NEW_MYSTERY_ITEMS',
|
||||
data: {
|
||||
MYSTERY_ITEMS,
|
||||
},
|
||||
})).toJSON(),
|
||||
};
|
||||
|
||||
dbUsers.update({_id: user._id}, {$addToSet: addToSet, $push: push});
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count } ${ user._id}`);
|
||||
if (user._id === authorUuid) console.warn(`${authorName } processed`);
|
||||
}
|
||||
|
||||
function displayData () {
|
||||
console.warn(`\n${ count } users processed\n`);
|
||||
return exiting(0);
|
||||
}
|
||||
|
||||
function exiting (code, msg) {
|
||||
code = code || 0; // 0 = success
|
||||
if (code && !msg) {
|
||||
msg = 'ERROR!';
|
||||
}
|
||||
if (msg) {
|
||||
if (code) {
|
||||
console.error(msg);
|
||||
} else {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
module.exports = processUsers;
|
||||
@@ -10,7 +10,7 @@ async function syncChallengeToMembers (challenges) {
|
||||
|
||||
const promises = [];
|
||||
users.forEach(user => {
|
||||
promises.push(challenge.syncToUser(user));
|
||||
promises.push(challenge.syncTasksToUser(user));
|
||||
promises.push(challenge.save());
|
||||
promises.push(user.save());
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@ function uploadFile (buffer, fileName) {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
// console.info(`${fileName} uploaded to ${BUCKET_NAME} succesfully.`);
|
||||
// console.info(`${fileName} uploaded to ${BUCKET_NAME} successfully.`);
|
||||
resolve(fileName);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ async function updateUser (user) {
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
sendTxn(
|
||||
await sendTxn(
|
||||
user,
|
||||
EMAIL_SLUG,
|
||||
[{ name: 'BASE_URL', content: BASE_URL }], // Add variables from template
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
/* eslint-disable no-console */
|
||||
import { model as User } from '../../website/server/models/user';
|
||||
import { model as UserNotification } from '../../website/server/models/userNotification';
|
||||
|
||||
const MIGRATION_NAME = 'mystery_items_201910';
|
||||
const MYSTERY_ITEMS = ['armor_mystery_201910', 'head_mystery_201910'];
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count += 1;
|
||||
|
||||
const addToSet = {
|
||||
'purchased.plan.mysteryItems': {
|
||||
$each: MYSTERY_ITEMS,
|
||||
},
|
||||
};
|
||||
const push = {
|
||||
notifications: (new UserNotification({
|
||||
type: 'NEW_MYSTERY_ITEMS',
|
||||
data: {
|
||||
MYSTERY_ITEMS,
|
||||
},
|
||||
})).toJSON(),
|
||||
};
|
||||
const set = {
|
||||
migration: MIGRATION_NAME,
|
||||
};
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
return User.update({ _id: user._id }, { $set: set, $push: push, $addToSet: addToSet }).exec();
|
||||
}
|
||||
|
||||
export default async function processUsers () {
|
||||
const query = {
|
||||
migration: { $ne: MIGRATION_NAME },
|
||||
'purchased.plan.customerId': { $ne: null },
|
||||
$or: [
|
||||
{ 'purchased.plan.dateTerminated': { $gte: new Date() } },
|
||||
{ 'purchased.plan.dateTerminated': { $exists: false } },
|
||||
{ 'purchased.plan.dateTerminated': { $eq: null } },
|
||||
],
|
||||
};
|
||||
|
||||
const fields = {
|
||||
_id: 1,
|
||||
};
|
||||
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
const users = await User // eslint-disable-line no-await-in-loop
|
||||
.find(query)
|
||||
.limit(250)
|
||||
.sort({ _id: 1 })
|
||||
.select(fields)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
console.warn(`\n${count} users processed\n`);
|
||||
break;
|
||||
} else {
|
||||
query._id = {
|
||||
$gt: users[users.length - 1],
|
||||
};
|
||||
}
|
||||
|
||||
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,29 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "4.121.0",
|
||||
"version": "4.129.2",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.7.2",
|
||||
"@babel/preset-env": "^7.7.1",
|
||||
"@babel/register": "^7.7.0",
|
||||
"@google-cloud/trace-agent": "^4.2.3",
|
||||
"@babel/core": "^7.7.7",
|
||||
"@babel/preset-env": "^7.7.7",
|
||||
"@babel/register": "^7.7.7",
|
||||
"@google-cloud/trace-agent": "^4.2.4",
|
||||
"@slack/client": "^3.8.1",
|
||||
"accepts": "^1.3.5",
|
||||
"amazon-payments": "^0.2.7",
|
||||
"amplitude": "^3.5.0",
|
||||
"apidoc": "^0.17.5",
|
||||
"apn": "^2.2.0",
|
||||
"aws-sdk": "^2.573.0",
|
||||
"bcrypt": "^3.0.6",
|
||||
"aws-sdk": "^2.597.0",
|
||||
"bcrypt": "^3.0.7",
|
||||
"body-parser": "^1.18.3",
|
||||
"compression": "^1.7.4",
|
||||
"cookie-session": "^1.3.3",
|
||||
"coupon-code": "^0.4.5",
|
||||
"csv-stringify": "^5.1.0",
|
||||
"csv-stringify": "^5.3.6",
|
||||
"cwait": "^1.1.1",
|
||||
"domain-middleware": "~0.1.0",
|
||||
"eslint": "^6.6.0",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-habitrpg": "^6.2.0",
|
||||
"eslint-plugin-mocha": "^5.0.0",
|
||||
"express": "^4.16.3",
|
||||
@@ -36,7 +36,7 @@
|
||||
"gulp-imagemin": "^6.2.0",
|
||||
"gulp-nodemon": "^2.4.1",
|
||||
"gulp.spritesmith": "^6.9.0",
|
||||
"habitica-markdown": "^1.3.0",
|
||||
"habitica-markdown": "^1.3.2",
|
||||
"helmet": "^3.21.2",
|
||||
"image-size": "^0.8.3",
|
||||
"in-app-purchase": "^1.11.3",
|
||||
@@ -46,12 +46,12 @@
|
||||
"method-override": "^3.0.0",
|
||||
"moment": "^2.24.0",
|
||||
"moment-recur": "^1.0.7",
|
||||
"mongoose": "^5.7.11",
|
||||
"mongoose": "^5.8.4",
|
||||
"morgan": "^1.7.0",
|
||||
"nconf": "^0.10.0",
|
||||
"node-gcm": "^1.0.2",
|
||||
"pageres": "^5.1.0",
|
||||
"passport": "^0.4.0",
|
||||
"passport": "^0.4.1",
|
||||
"passport-facebook": "^3.0.0",
|
||||
"passport-google-oauth2": "^0.2.0",
|
||||
"passport-google-oauth20": "1.0.0",
|
||||
@@ -59,10 +59,11 @@
|
||||
"paypal-rest-sdk": "^1.8.1",
|
||||
"ps-tree": "^1.0.0",
|
||||
"regenerator-runtime": "^0.13.3",
|
||||
"remove-markdown": "^0.3.0",
|
||||
"rimraf": "^3.0.0",
|
||||
"short-uuid": "^3.0.0",
|
||||
"stripe": "^7.13.0",
|
||||
"superagent": "^5.0.2",
|
||||
"stripe": "^7.15.0",
|
||||
"superagent": "^5.1.3",
|
||||
"universal-analytics": "^0.4.17",
|
||||
"useragent": "^2.1.9",
|
||||
"uuid": "^3.3.3",
|
||||
@@ -70,7 +71,7 @@
|
||||
"vinyl-buffer": "^1.0.1",
|
||||
"winston": "^2.4.3",
|
||||
"winston-loggly-bulk": "^2.0.2",
|
||||
"xml2js": "^0.4.4"
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"private": true,
|
||||
"engines": {
|
||||
@@ -112,7 +113,7 @@
|
||||
"monk": "^7.1.1",
|
||||
"require-again": "^2.0.0",
|
||||
"sinon": "^7.2.4",
|
||||
"sinon-chai": "^3.0.0",
|
||||
"sinon-chai": "^3.4.0",
|
||||
"sinon-stub-promise": "^4.0.0"
|
||||
},
|
||||
"optionalDependencies": {}
|
||||
|
||||
@@ -24,7 +24,13 @@ async function deleteAmplitudeData (userId, email) {
|
||||
console.log(err.response.data);
|
||||
});
|
||||
|
||||
if (response) console.log(`${response.status} ${response.statusText}`);
|
||||
if (response) {
|
||||
if (response.status === 200) {
|
||||
console.log(`${userId} (${email}) Amplitude deletion request OK.`);
|
||||
} else {
|
||||
console.log(`${userId} (${email}) Amplitude response: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteHabiticaData (user, email) {
|
||||
@@ -54,39 +60,46 @@ async function deleteHabiticaData (user, email) {
|
||||
});
|
||||
|
||||
if (response) {
|
||||
console.log(`${response.status} ${response.statusText}`);
|
||||
if (response.status === 200) console.log(`${user._id} (${email}) removed. Last login: ${user.auth.timestamps.loggedin}`);
|
||||
if (response.status === 200) {
|
||||
console.log(`${user._id} (${email}) removed from Habitica. Last login: ${user.auth.timestamps.loggedin}`);
|
||||
} else {
|
||||
console.log(`${user._id} (${email}) Habitica response: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function processEmailAddress (email) {
|
||||
const emailRegex = new RegExp(`^${email}$`, 'i');
|
||||
const users = await User.find({
|
||||
$or: [
|
||||
{ 'auth.local.email': emailRegex },
|
||||
{ 'auth.facebook.emails.value': emailRegex },
|
||||
{ 'auth.google.emails.value': emailRegex },
|
||||
],
|
||||
},
|
||||
{
|
||||
_id: 1,
|
||||
apiToken: 1,
|
||||
auth: 1,
|
||||
}).exec();
|
||||
const localUsers = await User.find(
|
||||
{ 'auth.local.email': emailRegex },
|
||||
{ _id: 1, apiToken: 1, auth: 1 },
|
||||
).exec();
|
||||
|
||||
const socialUsers = await User.find(
|
||||
{
|
||||
$or: [
|
||||
{ 'auth.facebook.emails.value': email },
|
||||
{ 'auth.google.emails.value': email },
|
||||
],
|
||||
},
|
||||
{ _id: 1, apiToken: 1, auth: 1 },
|
||||
).collation(
|
||||
{ locale: 'en', strength: 1 },
|
||||
).exec();
|
||||
|
||||
const users = localUsers.concat(socialUsers);
|
||||
|
||||
if (users.length < 1) {
|
||||
console.log(`No users found with email address ${email}`);
|
||||
} else {
|
||||
Promise.all(users.map(user => (async () => {
|
||||
await deleteAmplitudeData(user._id, email); // eslint-disable-line no-await-in-loop
|
||||
await deleteHabiticaData(user, email); // eslint-disable-line no-await-in-loop
|
||||
})()));
|
||||
return console.log(`No users found with email address ${email}`);
|
||||
}
|
||||
|
||||
return Promise.all(users.map(user => (async () => {
|
||||
await deleteAmplitudeData(user._id, email); // eslint-disable-line no-await-in-loop
|
||||
await deleteHabiticaData(user, email); // eslint-disable-line no-await-in-loop
|
||||
})()));
|
||||
}
|
||||
|
||||
function deleteUserData (emails) {
|
||||
export default function deleteUserData (emails) {
|
||||
const emailPromises = emails.map(processEmailAddress);
|
||||
return Promise.all(emailPromises);
|
||||
}
|
||||
|
||||
module.exports = deleteUserData;
|
||||
|
||||
@@ -155,7 +155,7 @@ describe('analyticsService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('sets Unkown if headers are not passed in', () => {
|
||||
it('sets Unknown if headers are not passed in', () => {
|
||||
delete data.headers;
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
@@ -476,7 +476,7 @@ describe('analyticsService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('sets Unkown if headers are not passed in', () => {
|
||||
it('sets Unknown if headers are not passed in', () => {
|
||||
delete data.headers;
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
|
||||
@@ -88,6 +88,28 @@ describe('cron', () => {
|
||||
user.purchased.plan.dateUpdated = moment().subtract(1, 'months').toDate();
|
||||
});
|
||||
|
||||
it('awards current mystery items to subscriber', () => {
|
||||
user.purchased.plan.dateUpdated = new Date('2018-12-11');
|
||||
clock = sinon.useFakeTimers(new Date('2019-01-29'));
|
||||
cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.mysteryItems.length).to.eql(2);
|
||||
const filteredNotifications = user.notifications.filter(n => n.type === 'NEW_MYSTERY_ITEMS');
|
||||
expect(filteredNotifications.length).to.equal(1);
|
||||
});
|
||||
|
||||
it('awards multiple mystery item sets if user skipped months between logins', () => {
|
||||
user.purchased.plan.dateUpdated = new Date('2018-11-11');
|
||||
clock = sinon.useFakeTimers(new Date('2019-01-29'));
|
||||
cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.mysteryItems.length).to.eql(4);
|
||||
const filteredNotifications = user.notifications.filter(n => n.type === 'NEW_MYSTERY_ITEMS');
|
||||
expect(filteredNotifications.length).to.equal(1);
|
||||
});
|
||||
|
||||
it('resets plan.gemsBought on a new month', () => {
|
||||
user.purchased.plan.gemsBought = 10;
|
||||
cron({
|
||||
|
||||
@@ -11,6 +11,7 @@ import { model as User } from '../../../../../../website/server/models/user';
|
||||
import { model as Group } from '../../../../../../website/server/models/group';
|
||||
import {
|
||||
generateGroup,
|
||||
sleep,
|
||||
} from '../../../../../helpers/api-unit.helper';
|
||||
|
||||
describe('Purchasing a group plan for group', () => {
|
||||
@@ -307,6 +308,7 @@ describe('Purchasing a group plan for group', () => {
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
await sleep(0.5);
|
||||
|
||||
expect(sender.sendTxn).to.have.callCount(4);
|
||||
expect(sender.sendTxn.args[0][0]._id).to.equal(TECH_ASSISTANCE_EMAIL);
|
||||
@@ -340,6 +342,7 @@ describe('Purchasing a group plan for group', () => {
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
await sleep(0.5);
|
||||
|
||||
expect(sender.sendTxn).to.have.callCount(4);
|
||||
expect(sender.sendTxn.args[0][0]._id).to.equal(TECH_ASSISTANCE_EMAIL);
|
||||
|
||||
@@ -439,31 +439,6 @@ describe('payments/index', () => {
|
||||
fakeClock.restore();
|
||||
});
|
||||
|
||||
it('does not awards mystery items when not within the timeframe for a mystery item', async () => {
|
||||
const noMysteryItemTimeframe = 1462183920000; // May 2nd 2016
|
||||
const fakeClock = sinon.useFakeTimers(noMysteryItemTimeframe);
|
||||
data = { paymentMethod: 'PaymentMethod', user, sub: { key: 'basic_3mo' } };
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.mysteryItems).to.have.a.lengthOf(0);
|
||||
|
||||
fakeClock.restore();
|
||||
});
|
||||
|
||||
it('does not add a notification for mystery items if none was awarded', async () => {
|
||||
const noMysteryItemTimeframe = 1462183920000; // May 2nd 2016
|
||||
const fakeClock = sinon.useFakeTimers(noMysteryItemTimeframe);
|
||||
data = { paymentMethod: 'PaymentMethod', user, sub: { key: 'basic_3mo' } };
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.mysteryItems).to.have.a.lengthOf(0);
|
||||
expect(user.notifications.find(n => n.type === 'NEW_MYSTERY_ITEMS')).to.be.undefined;
|
||||
|
||||
fakeClock.restore();
|
||||
});
|
||||
|
||||
it('does not award mystery item when user already owns the item', async () => {
|
||||
const mayMysteryItemTimeframe = 1464725113000; // May 31st 2016
|
||||
const fakeClock = sinon.useFakeTimers(mayMysteryItemTimeframe);
|
||||
@@ -651,7 +626,7 @@ describe('payments/index', () => {
|
||||
.calledWith(recipient, { receiverMsg: msg, senderMsg: msg, save: false });
|
||||
});
|
||||
|
||||
it('sends a message from purchaser to recipient wtih custom message', async () => {
|
||||
it('sends a message from purchaser to recipient with custom message', async () => {
|
||||
data.gift.message = 'giftmessage';
|
||||
|
||||
await api.buyGems(data);
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
moveTask,
|
||||
} from '../../../../website/server/libs/taskManager';
|
||||
import i18n from '../../../../website/common/script/i18n';
|
||||
import shared from '../../../../website/common/script';
|
||||
import {
|
||||
generateUser,
|
||||
generateGroup,
|
||||
@@ -58,6 +59,51 @@ describe('taskManager', () => {
|
||||
expect(newTask.createdAt).to.exist;
|
||||
});
|
||||
|
||||
describe('onboarding', () => {
|
||||
beforeEach(() => {
|
||||
user.addAchievement = sinon.spy();
|
||||
sinon.stub(shared.onboarding, 'checkOnboardingStatus');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
shared.onboarding.checkOnboardingStatus.restore();
|
||||
});
|
||||
|
||||
it('adds the onboarding achievement to the user and checks the onboarding status', async () => {
|
||||
req.body = testHabit;
|
||||
res.t = i18n.t;
|
||||
user.flags.welcomed = true;
|
||||
|
||||
await createTasks(req, res, { user });
|
||||
|
||||
expect(user.addAchievement).to.be.calledOnce;
|
||||
expect(user.addAchievement).to.be.calledWith('createdTask');
|
||||
|
||||
expect(shared.onboarding.checkOnboardingStatus).to.be.calledOnce;
|
||||
expect(shared.onboarding.checkOnboardingStatus).to.be.calledWith(user);
|
||||
});
|
||||
|
||||
it('does not add the onboarding achievement to the user if flags.welcomed is false', async () => {
|
||||
req.body = testHabit;
|
||||
res.t = i18n.t;
|
||||
user.flags.welcomed = false;
|
||||
|
||||
await createTasks(req, res, { user });
|
||||
|
||||
expect(user.addAchievement).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not add the onboarding achievement to the user if it\'s already been awarded', async () => {
|
||||
req.body = testHabit;
|
||||
res.t = i18n.t;
|
||||
user.achievements.createdTask = true;
|
||||
|
||||
await createTasks(req, res, { user });
|
||||
|
||||
expect(user.addAchievement).to.not.be.called;
|
||||
});
|
||||
});
|
||||
|
||||
it('gets user tasks', async () => {
|
||||
req.body = testHabit;
|
||||
res.t = i18n.t;
|
||||
|
||||
@@ -90,7 +90,37 @@ describe('Challenge Model', () => {
|
||||
expect(syncedTask.tags[0]).to.eql(challenge._id);
|
||||
});
|
||||
|
||||
it('syncs a challenge to a user', async () => {
|
||||
it('adds a challenge to a user', async () => {
|
||||
const newMember = new User({
|
||||
guilds: [guild._id],
|
||||
});
|
||||
await newMember.save();
|
||||
|
||||
const addedSuccessfully = await challenge.addToUser(newMember);
|
||||
|
||||
const updatedNewMember = await User.findById(newMember._id);
|
||||
|
||||
expect(addedSuccessfully).to.eql(true);
|
||||
expect(updatedNewMember.challenges).to.contain(challenge._id);
|
||||
});
|
||||
|
||||
it('does not add a challenge to a user that already in the challenge', async () => {
|
||||
const newMember = new User({
|
||||
guilds: [guild._id],
|
||||
challenges: [challenge._id],
|
||||
});
|
||||
await newMember.save();
|
||||
|
||||
const addedSuccessfully = await challenge.addToUser(newMember);
|
||||
|
||||
const updatedNewMember = await User.findById(newMember._id);
|
||||
|
||||
expect(addedSuccessfully).to.eql(false);
|
||||
expect(updatedNewMember.challenges).to.contain(challenge._id);
|
||||
expect(updatedNewMember.challenges.length).to.eql(1);
|
||||
});
|
||||
|
||||
it('syncs challenge tasks to a user', async () => {
|
||||
await challenge.addTasks([task]);
|
||||
|
||||
const newMember = new User({
|
||||
@@ -98,7 +128,7 @@ describe('Challenge Model', () => {
|
||||
});
|
||||
await newMember.save();
|
||||
|
||||
await challenge.syncToUser(newMember);
|
||||
await challenge.syncTasksToUser(newMember);
|
||||
|
||||
const updatedNewMember = await User.findById(newMember._id);
|
||||
const updatedNewMemberTasks = await Tasks.Task.find({ _id: { $in: updatedNewMember.tasksOrder[`${taskType}s`] } });
|
||||
@@ -110,14 +140,13 @@ describe('Challenge Model', () => {
|
||||
),
|
||||
);
|
||||
|
||||
expect(updatedNewMember.challenges).to.contain(challenge._id);
|
||||
expect(updatedNewMember.tags[7].id).to.equal(challenge._id);
|
||||
expect(updatedNewMember.tags[7].name).to.equal(challenge.shortName);
|
||||
expect(syncedTask).to.exist;
|
||||
expect(syncedTask.attribute).to.eql('str');
|
||||
});
|
||||
|
||||
it('syncs a challenge to a user with the existing task', async () => {
|
||||
it('syncs challenge tasks to a user with the existing task', async () => {
|
||||
await challenge.addTasks([task]);
|
||||
|
||||
let updatedLeader = await User.findOne({ _id: leader._id });
|
||||
@@ -134,7 +163,7 @@ describe('Challenge Model', () => {
|
||||
task.text = newTitle;
|
||||
task.attribute = 'int';
|
||||
await task.save();
|
||||
await challenge.syncToUser(leader);
|
||||
await challenge.syncTasksToUser(leader);
|
||||
|
||||
updatedLeader = await User.findOne({ _id: leader._id });
|
||||
updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
|
||||
|
||||
@@ -1317,7 +1317,7 @@ describe('Group Model', () => {
|
||||
|
||||
it('formats message', () => {
|
||||
const chatMessage = party.sendChat({
|
||||
message: 'a new message',
|
||||
message: 'a _new_ message with *markdown*',
|
||||
user: {
|
||||
_id: 'user-id',
|
||||
profile: { name: 'user name' },
|
||||
@@ -1336,7 +1336,8 @@ describe('Group Model', () => {
|
||||
|
||||
const chat = chatMessage;
|
||||
|
||||
expect(chat.text).to.eql('a new message');
|
||||
expect(chat.text).to.eql('a _new_ message with *markdown*');
|
||||
expect(chat.unformattedText).to.eql('a new message with markdown');
|
||||
expect(validator.isUUID(chat.id)).to.eql(true);
|
||||
expect(chat.timestamp).to.be.a('date');
|
||||
expect(chat.likes).to.eql({});
|
||||
@@ -1878,6 +1879,8 @@ describe('Group Model', () => {
|
||||
await questLeader.save();
|
||||
await party.finishQuest(quest);
|
||||
|
||||
await sleep(0.5);
|
||||
|
||||
const [
|
||||
updatedLeader,
|
||||
updatedParticipatingMember,
|
||||
|
||||
@@ -84,6 +84,103 @@ describe('User Model', () => {
|
||||
expect(userToJSON.stats.toNextLevel).to.equal(common.tnl(user.stats.lvl));
|
||||
});
|
||||
|
||||
context('achievements', () => {
|
||||
it('can add an achievement', () => {
|
||||
const user = new User();
|
||||
const originalUserToJSON = user.toJSON({ minimize: false });
|
||||
expect(originalUserToJSON.achievements.createdTask).to.not.eql(true);
|
||||
const notificationsN = originalUserToJSON.notifications.length;
|
||||
|
||||
user.addAchievement('createdTask');
|
||||
|
||||
const userToJSON = user.toJSON();
|
||||
expect(user.notifications.length).to.equal(notificationsN + 1);
|
||||
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type', 'seen']);
|
||||
expect(userToJSON.notifications[0].type).to.equal('ACHIEVEMENT');
|
||||
expect(userToJSON.notifications[0].data).to.eql({
|
||||
achievement: 'createdTask',
|
||||
});
|
||||
expect(userToJSON.notifications[0].seen).to.eql(false);
|
||||
|
||||
expect(userToJSON.achievements.createdTask).to.eql(true);
|
||||
});
|
||||
|
||||
it('throws an error if the achievement is not valid', () => {
|
||||
const user = new User();
|
||||
expect(() => user.addAchievement('notAnAchievement')).to.throw;
|
||||
});
|
||||
|
||||
context('static push method', () => {
|
||||
it('throws an error if the achievement is not valid', async () => {
|
||||
const user = new User();
|
||||
await user.save();
|
||||
|
||||
await expect(User.addAchievementUpdate({ _id: user._id }, 'notAnAchievement'))
|
||||
.to.eventually.be.rejected;
|
||||
|
||||
expect(() => user.addAchievement('notAnAchievement')).to.throw;
|
||||
});
|
||||
|
||||
it('adds an achievement for a single member via static method', async () => {
|
||||
let user = new User();
|
||||
await user.save();
|
||||
|
||||
const originalUserToJSON = user.toJSON({ minimize: false });
|
||||
expect(originalUserToJSON.achievements.createdTask).to.not.eql(true);
|
||||
const notificationsN = originalUserToJSON.notifications.length;
|
||||
|
||||
await User.addAchievementUpdate({ _id: user._id }, 'createdTask');
|
||||
|
||||
user = await User.findOne({ _id: user._id }).exec();
|
||||
|
||||
const userToJSON = user.toJSON();
|
||||
expect(user.notifications.length).to.equal(notificationsN + 1);
|
||||
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type', 'seen']);
|
||||
expect(userToJSON.notifications[0].type).to.equal('ACHIEVEMENT');
|
||||
expect(userToJSON.notifications[0].data).to.eql({
|
||||
achievement: 'createdTask',
|
||||
});
|
||||
expect(userToJSON.notifications[0].seen).to.eql(false);
|
||||
|
||||
expect(userToJSON.achievements.createdTask).to.eql(true);
|
||||
});
|
||||
|
||||
it('adds an achievement for all given users via static method', async () => {
|
||||
let user = new User();
|
||||
const otherUser = new User();
|
||||
await Promise.all([user.save(), otherUser.save()]);
|
||||
|
||||
await User.addAchievementUpdate({ _id: { $in: [user._id, otherUser._id] } }, 'createdTask');
|
||||
|
||||
user = await User.findOne({ _id: user._id }).exec();
|
||||
|
||||
let userToJSON = user.toJSON();
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type', 'seen']);
|
||||
expect(userToJSON.notifications[0].type).to.equal('ACHIEVEMENT');
|
||||
expect(userToJSON.notifications[0].data).to.eql({
|
||||
achievement: 'createdTask',
|
||||
});
|
||||
expect(userToJSON.notifications[0].seen).to.eql(false);
|
||||
|
||||
expect(userToJSON.achievements.createdTask).to.eql(true);
|
||||
|
||||
user = await User.findOne({ _id: otherUser._id }).exec();
|
||||
|
||||
userToJSON = user.toJSON();
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type', 'seen']);
|
||||
expect(userToJSON.notifications[0].type).to.equal('ACHIEVEMENT');
|
||||
expect(userToJSON.notifications[0].data).to.eql({
|
||||
achievement: 'createdTask',
|
||||
});
|
||||
expect(userToJSON.notifications[0].seen).to.eql(false);
|
||||
|
||||
expect(userToJSON.achievements.createdTask).to.eql(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('notifications', () => {
|
||||
it('can add notifications without data', () => {
|
||||
const user = new User();
|
||||
|
||||
@@ -251,7 +251,7 @@ describe('POST /challenges', () => {
|
||||
expect(groupLeader.balance).to.eql(oldUserBalance);
|
||||
});
|
||||
|
||||
it('sets all properites of the challenge as passed', async () => {
|
||||
it('sets all properties of the challenge as passed', async () => {
|
||||
const name = 'Test Challenge';
|
||||
const shortName = 'TC Label';
|
||||
const description = 'Test Description';
|
||||
|
||||
@@ -177,7 +177,7 @@ describe('GET /groups/:id', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('removes non-existant guild from user\'s guild list', async () => {
|
||||
it('removes non-existent guild from user\'s guild list', async () => {
|
||||
const guildId = generateUUID();
|
||||
|
||||
await user.update({
|
||||
@@ -197,7 +197,7 @@ describe('GET /groups/:id', () => {
|
||||
expect(user.guilds).to.not.include(guildId);
|
||||
});
|
||||
|
||||
it('removes non-existant party from user\'s party object', async () => {
|
||||
it('removes non-existent party from user\'s party object', async () => {
|
||||
const partyId = generateUUID();
|
||||
|
||||
await user.update({
|
||||
|
||||
@@ -210,7 +210,7 @@ describe('POST /groups/:groupId/leave', () => {
|
||||
expect(userWithoutInvitation.invitations.guilds).to.not.be.empty;
|
||||
});
|
||||
|
||||
it('deletes non existant guild from user when user tries to leave', async () => {
|
||||
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);
|
||||
@@ -258,7 +258,7 @@ describe('POST /groups/:groupId/leave', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('deletes non existant party from user when user tries to leave', async () => {
|
||||
it('deletes non existent party from user when user tries to leave', async () => {
|
||||
const nonExistentPartyId = generateUUID();
|
||||
const userWithNonExistentParty = await generateUser({ 'party._id': nonExistentPartyId });
|
||||
expect(userWithNonExistentParty.party._id).to.eql(nonExistentPartyId);
|
||||
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
|
||||
describe('POST /members/send-private-message', () => {
|
||||
let userToSendMessage;
|
||||
const messageToSend = 'Test Private Message';
|
||||
const messageToSend = 'Test *Private* Message';
|
||||
const unformattedMessage = 'Test Private Message';
|
||||
|
||||
beforeEach(async () => {
|
||||
userToSendMessage = await generateUser();
|
||||
@@ -110,7 +111,9 @@ describe('POST /members/send-private-message', () => {
|
||||
|
||||
const sendersMessageInReceiversInbox = _.find(
|
||||
updatedReceiver.inbox.messages,
|
||||
message => message.uuid === userToSendMessage._id && message.text === messageToSend,
|
||||
message => message.uuid === userToSendMessage._id
|
||||
&& message.text === messageToSend
|
||||
&& message.unformattedText === unformattedMessage,
|
||||
);
|
||||
|
||||
const sendersMessageInSendersInbox = _.find(
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('payments - amazon - #checkout', () => {
|
||||
amzLib.checkout.restore();
|
||||
});
|
||||
|
||||
it('makes a purcahse with amazon checkout', async () => {
|
||||
it('makes a purchase with amazon checkout', async () => {
|
||||
user = await generateUser({
|
||||
'profile.name': 'sender',
|
||||
'purchased.plan.customerId': 'customer-id',
|
||||
|
||||
@@ -120,7 +120,7 @@ describe('DELETE /tasks/:id', () => {
|
||||
});
|
||||
|
||||
context('task cannot be deleted', () => {
|
||||
it('cannot delete a non-existant task', async () => {
|
||||
it('cannot delete a non-existent task', async () => {
|
||||
await expect(user.del('/tasks/550e8400-e29b-41d4-a716-446655440000')).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
|
||||
@@ -39,7 +39,7 @@ describe('GET /tasks/:id', () => {
|
||||
});
|
||||
|
||||
context('task cannot be accessed', () => {
|
||||
it('cannot get a non-existant task', async () => {
|
||||
it('cannot get a non-existent task', async () => {
|
||||
const dummyId = generateUUID();
|
||||
|
||||
await expect(user.get(`/tasks/${dummyId}`)).to.eventually.be.rejected.and.eql({
|
||||
|
||||
@@ -27,7 +27,7 @@ describe('DELETE /tasks/:id', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot delete a non-existant task', async () => {
|
||||
it('cannot delete a non-existent task', async () => {
|
||||
await expect(user.del(`/tasks/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
|
||||
@@ -15,7 +15,7 @@ describe('GET /user/toggle-pinned-item', () => {
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('cannotUnpinArmoirPotion'),
|
||||
message: t('cannotUnpinItem'),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -189,6 +189,28 @@ describe('POST /user/auth/reset-password-set-new-one', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the error page if the password is too short', async () => {
|
||||
const user = await generateUser();
|
||||
|
||||
const code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().add({ days: 1 }),
|
||||
}));
|
||||
await user.update({
|
||||
'auth.local.passwordResetCode': code,
|
||||
});
|
||||
|
||||
await expect(api.post(`${endpoint}`, {
|
||||
newPassword: 'short',
|
||||
confirmPassword: 'short',
|
||||
code,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the success page and save the user', async () => {
|
||||
const user = await generateUser();
|
||||
|
||||
|
||||
@@ -326,6 +326,24 @@ describe('POST /user/auth/local/register', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('requires minimum length for the password', async () => {
|
||||
const username = generateRandomUserName();
|
||||
const email = `${username}@example.com`;
|
||||
const password = '1234567';
|
||||
const confirmPassword = '1234567';
|
||||
|
||||
await expect(api.post('/user/auth/local/register', {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
confirmPassword,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('requires a username', async () => {
|
||||
const email = `${generateRandomUserName()}@example.com`;
|
||||
const password = 'password';
|
||||
|
||||
@@ -16,7 +16,7 @@ describe('PUT /user/auth/update-email', () => {
|
||||
const newEmail = 'SOmE-nEw-emAIl_2@example.net';
|
||||
const oldPassword = 'password'; // from habitrpg/test/helpers/api-integration/v3/object-generators.js
|
||||
|
||||
context('Local Authenticaion User', async () => {
|
||||
context('Local Authentication User', async () => {
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
|
||||
@@ -82,6 +82,20 @@ describe('PUT /user/auth/update-password', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error when newPassword is too short', async () => {
|
||||
const body = {
|
||||
password,
|
||||
newPassword: '1234567',
|
||||
confirmPassword: '1234567',
|
||||
};
|
||||
|
||||
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 () => {
|
||||
const body = {
|
||||
password,
|
||||
|
||||
@@ -239,6 +239,47 @@ describe('achievements', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('unearned onboarding achievements', () => {
|
||||
const user = generateUser();
|
||||
const onboardingAchievs = shared
|
||||
.achievements.getAchievementsForProfile(user).onboarding.achievements;
|
||||
|
||||
it('created task achievement exists with no count', () => {
|
||||
const { createdTask } = onboardingAchievs;
|
||||
|
||||
expect(createdTask).to.exist;
|
||||
expect(createdTask.optionalCount).to.be.undefined;
|
||||
});
|
||||
|
||||
it('completed task achievement exists with no count', () => {
|
||||
const { completedTask } = onboardingAchievs;
|
||||
|
||||
expect(completedTask).to.exist;
|
||||
expect(completedTask.optionalCount).to.be.undefined;
|
||||
});
|
||||
|
||||
it('hatched pet achievement exists with no count', () => {
|
||||
const { hatchedPet } = onboardingAchievs;
|
||||
|
||||
expect(hatchedPet).to.exist;
|
||||
expect(hatchedPet.optionalCount).to.be.undefined;
|
||||
});
|
||||
|
||||
it('fed pet achievement exists with no count', () => {
|
||||
const { fedPet } = onboardingAchievs;
|
||||
|
||||
expect(fedPet).to.exist;
|
||||
expect(fedPet.optionalCount).to.be.undefined;
|
||||
});
|
||||
|
||||
it('purchased equipment achievement exists with no count', () => {
|
||||
const { purchasedEquipment } = onboardingAchievs;
|
||||
|
||||
expect(purchasedEquipment).to.exist;
|
||||
expect(purchasedEquipment.optionalCount).to.be.undefined;
|
||||
});
|
||||
});
|
||||
|
||||
describe('earned seasonal achievements', () => {
|
||||
const user = generateUser();
|
||||
const quests = ['dilatory', 'stressbeast', 'burnout', 'bewilder'];
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
|
||||
import getDebuffPotionItems from '../../../website/common/script/libs/getDebuffPotionItems';
|
||||
import { TRANSFORMATION_DEBUFFS_LIST } from '../../../website/common/script/constants';
|
||||
|
||||
describe('getDebuffPotionItems', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
});
|
||||
|
||||
for (const key of Object.keys(TRANSFORMATION_DEBUFFS_LIST)) {
|
||||
const debuff = TRANSFORMATION_DEBUFFS_LIST[key];
|
||||
// Here we iterate the whole object to dynamically create test suites as
|
||||
// described in mocha's docs
|
||||
// https://mochajs.org/#dynamically-generating-tests
|
||||
// That's why we have eslint-disable here
|
||||
// eslint-disable-next-line no-loop-func
|
||||
it(`Should return the ${debuff} on ${key} buff`, () => {
|
||||
user.stats.buffs[key] = true;
|
||||
|
||||
const result = getDebuffPotionItems(user);
|
||||
|
||||
expect(result).to.be.an('array').that.deep
|
||||
.includes({ path: `spells.special.${debuff}`, type: 'debuffPotion' });
|
||||
});
|
||||
}
|
||||
|
||||
it('Should return all debuff potions for all buffs', () => {
|
||||
user.stats.buffs.seafoam = true;
|
||||
user.stats.buffs.spookySparkles = true;
|
||||
user.stats.buffs.snowball = true;
|
||||
user.stats.buffs.shinySeed = true;
|
||||
|
||||
|
||||
const result = getDebuffPotionItems(user);
|
||||
|
||||
expect(result).to.be.an('array').that.deep.include.members([
|
||||
{ path: 'spells.special.sand', type: 'debuffPotion' },
|
||||
{ path: 'spells.special.petalFreePotion', type: 'debuffPotion' },
|
||||
{ path: 'spells.special.salt', type: 'debuffPotion' },
|
||||
{ path: 'spells.special.opaquePotion', type: 'debuffPotion' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
import moment from 'moment';
|
||||
import {
|
||||
hasActiveOnboarding,
|
||||
hasCompletedOnboarding,
|
||||
onOnboardingComplete,
|
||||
checkOnboardingStatus,
|
||||
} from '../../../website/common/script/libs/onboarding';
|
||||
import { generateUser } from '../../helpers/common.helper';
|
||||
|
||||
describe('onboarding', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
user.addNotification = sinon.spy();
|
||||
// Make sure the onboarding is active
|
||||
user.auth.timestamps.created = moment('2019-12-20').toDate();
|
||||
});
|
||||
|
||||
describe('hasActiveOnboarding', () => {
|
||||
// The value of BEGIN DATE is available in common/script/libs/onboarding
|
||||
|
||||
it('returns true if the account is created after BEGIN_DATE', () => {
|
||||
expect(hasActiveOnboarding(user)).to.eql(true);
|
||||
});
|
||||
|
||||
it('returns false if the account is created before BEGIN_DATE', () => {
|
||||
user.auth.timestamps.created = moment('2019-12-01').toDate();
|
||||
expect(hasActiveOnboarding(user)).to.eql(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasCompletedOnboarding', () => {
|
||||
it('returns false if no achievement has been awarded', () => {
|
||||
const result = hasCompletedOnboarding(user);
|
||||
expect(result).to.eql(false);
|
||||
});
|
||||
|
||||
it('returns false if not all achievements have been awarded', () => {
|
||||
user.achievements.completedTask = true;
|
||||
const result = hasCompletedOnboarding(user);
|
||||
expect(result).to.eql(false);
|
||||
});
|
||||
|
||||
it('returns true if all achievements have been awarded', () => {
|
||||
user.achievements.createdTask = true;
|
||||
user.achievements.completedTask = true;
|
||||
user.achievements.hatchedPet = true;
|
||||
user.achievements.fedPet = true;
|
||||
user.achievements.purchasedEquipment = true;
|
||||
|
||||
const result = hasCompletedOnboarding(user);
|
||||
expect(result).to.eql(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onOnboardingComplete', () => {
|
||||
it('awards prizes', () => {
|
||||
const { gp } = user.stats;
|
||||
onOnboardingComplete(user);
|
||||
expect(user.stats.gp).to.eql(gp + 100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkOnboardingStatus', () => {
|
||||
it('does nothing if onboarding is not active', () => {
|
||||
const { gp } = user.stats;
|
||||
user.auth.timestamps.created = moment('2019-12-01').toDate();
|
||||
|
||||
checkOnboardingStatus(user);
|
||||
expect(user.addNotification).to.not.be.called;
|
||||
|
||||
expect(user.stats.gp).to.eql(gp);
|
||||
});
|
||||
|
||||
it('does nothing if onboarding is not complete', () => {
|
||||
const { gp } = user.stats;
|
||||
|
||||
checkOnboardingStatus(user);
|
||||
expect(user.addNotification).to.not.be.called;
|
||||
|
||||
expect(user.stats.gp).to.eql(gp);
|
||||
});
|
||||
|
||||
it('awards prize and add notification when onboarding is complete', () => {
|
||||
user.achievements.createdTask = true;
|
||||
user.achievements.completedTask = true;
|
||||
user.achievements.hatchedPet = true;
|
||||
user.achievements.fedPet = true;
|
||||
user.achievements.purchasedEquipment = true;
|
||||
const { gp } = user.stats;
|
||||
|
||||
checkOnboardingStatus(user);
|
||||
expect(user.addNotification).to.be.calledOnce;
|
||||
expect(user.addNotification).to.be.calledWith('ONBOARDING_COMPLETE');
|
||||
|
||||
expect(user.stats.gp).to.eql(gp + 100);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
|
||||
import setDebuffPotionItems from '../../../website/common/script/libs/setDebuffPotionItems';
|
||||
|
||||
describe('setDebuffPotionItems', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
});
|
||||
|
||||
it('Should push the debuff item to pinned items of user', () => {
|
||||
user.stats.buffs.spookySparkles = true;
|
||||
const previousPinnedItemsLength = user.pinnedItems.length;
|
||||
|
||||
const result = setDebuffPotionItems(user);
|
||||
|
||||
expect(result.pinnedItems.length).to.be.greaterThan(previousPinnedItemsLength);
|
||||
});
|
||||
|
||||
it('Shouldn\'t create duplicate of already added debuff potion', () => {
|
||||
user.stats.buffs.spookySparkles = true;
|
||||
|
||||
const firstSetResult = [...setDebuffPotionItems(user).pinnedItems];
|
||||
const secondSetResult = [...setDebuffPotionItems(user).pinnedItems];
|
||||
|
||||
|
||||
expect(firstSetResult).to.be.deep.equal(secondSetResult);
|
||||
});
|
||||
|
||||
it('Should remove all debuff items from pinnedItems of the user if user have no buffs', () => {
|
||||
user.stats.buffs.seafoam = true;
|
||||
user.stats.buffs.spookySparkles = true;
|
||||
user.stats.buffs.snowball = true;
|
||||
user.stats.buffs.shinySeed = true;
|
||||
|
||||
const firstSetResult = [...setDebuffPotionItems(user).pinnedItems];
|
||||
|
||||
expect(firstSetResult).to.have.lengthOf(4);
|
||||
|
||||
user.stats.buffs.seafoam = false;
|
||||
user.stats.buffs.spookySparkles = false;
|
||||
user.stats.buffs.snowball = false;
|
||||
user.stats.buffs.shinySeed = false;
|
||||
|
||||
const secondSetResult = [...setDebuffPotionItems(user).pinnedItems];
|
||||
|
||||
expect(secondSetResult).to.have.lengthOf(0);
|
||||
});
|
||||
});
|
||||
@@ -41,7 +41,10 @@ describe('shared.ops.buyMarketGear', () => {
|
||||
},
|
||||
});
|
||||
|
||||
user.addAchievement = sinon.spy();
|
||||
|
||||
sinon.stub(shared, 'randomVal');
|
||||
sinon.stub(shared.onboarding, 'checkOnboardingStatus');
|
||||
sinon.stub(shared.fns, 'predictableRandom');
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
@@ -49,6 +52,7 @@ describe('shared.ops.buyMarketGear', () => {
|
||||
afterEach(() => {
|
||||
shared.randomVal.restore();
|
||||
shared.fns.predictableRandom.restore();
|
||||
shared.onboarding.checkOnboardingStatus.restore();
|
||||
analytics.track.restore();
|
||||
});
|
||||
|
||||
@@ -86,6 +90,27 @@ describe('shared.ops.buyMarketGear', () => {
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('adds the onboarding achievement to the user and checks the onboarding status', () => {
|
||||
user.stats.gp = 31;
|
||||
|
||||
buyGear(user, { params: { key: 'armor_warrior_1' } }, analytics);
|
||||
|
||||
expect(user.addAchievement).to.be.calledOnce;
|
||||
expect(user.addAchievement).to.be.calledWith('purchasedEquipment');
|
||||
|
||||
expect(shared.onboarding.checkOnboardingStatus).to.be.calledOnce;
|
||||
expect(shared.onboarding.checkOnboardingStatus).to.be.calledWith(user);
|
||||
});
|
||||
|
||||
it('does not add the onboarding achievement to the user if it\'s already been awarded', () => {
|
||||
user.stats.gp = 31;
|
||||
user.achievements.purchasedEquipment = true;
|
||||
|
||||
buyGear(user, { params: { key: 'armor_warrior_1' } }, analytics);
|
||||
|
||||
expect(user.addAchievement).to.not.be.called;
|
||||
});
|
||||
|
||||
it('deducts gold from user', () => {
|
||||
user.stats.gp = 31;
|
||||
|
||||
|
||||
@@ -10,12 +10,19 @@ import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
import errorMessage from '../../../website/common/script/libs/errorMessage';
|
||||
import shared from '../../../website/common/script';
|
||||
|
||||
describe('shared.ops.feed', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
user.addAchievement = sinon.spy();
|
||||
sinon.stub(shared.onboarding, 'checkOnboardingStatus');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
shared.onboarding.checkOnboardingStatus.restore();
|
||||
});
|
||||
|
||||
context('failure conditions', () => {
|
||||
@@ -223,5 +230,28 @@ describe('shared.ops.feed', () => {
|
||||
expect(user.items.mounts['Wolf-Base']).to.equal(true);
|
||||
expect(user.items.currentPet).to.equal('');
|
||||
});
|
||||
|
||||
it('adds the onboarding achievement to the user and checks the onboarding status', () => {
|
||||
user.items.pets['Wolf-Base'] = 5;
|
||||
user.items.food.Meat = 2;
|
||||
|
||||
feed(user, { params: { pet: 'Wolf-Base', food: 'Meat' } });
|
||||
|
||||
expect(user.addAchievement).to.be.calledOnce;
|
||||
expect(user.addAchievement).to.be.calledWith('fedPet');
|
||||
|
||||
expect(shared.onboarding.checkOnboardingStatus).to.be.calledOnce;
|
||||
expect(shared.onboarding.checkOnboardingStatus).to.be.calledWith(user);
|
||||
});
|
||||
|
||||
it('does not add the onboarding achievement to the user if it\'s already been awarded', () => {
|
||||
user.items.pets['Wolf-Base'] = 5;
|
||||
user.items.food.Meat = 2;
|
||||
user.achievements.fedPet = true;
|
||||
|
||||
feed(user, { params: { pet: 'Wolf-Base', food: 'Meat' } });
|
||||
|
||||
expect(user.addAchievement).to.not.be.called;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,12 +9,19 @@ import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
import errorMessage from '../../../website/common/script/libs/errorMessage';
|
||||
import shared from '../../../website/common/script';
|
||||
|
||||
describe('shared.ops.hatch', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
user.addAchievement = sinon.spy();
|
||||
sinon.stub(shared.onboarding, 'checkOnboardingStatus');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
shared.onboarding.checkOnboardingStatus.restore();
|
||||
});
|
||||
|
||||
context('Pet Hatching', () => {
|
||||
@@ -195,6 +202,30 @@ describe('shared.ops.hatch', () => {
|
||||
hatch(user, { params: { egg: 'Wolf', hatchingPotion: 'Spooky' } });
|
||||
expect(user.achievements.dustDevil).to.eql(true);
|
||||
});
|
||||
|
||||
it('adds the onboarding achievement to the user and checks the onboarding status', () => {
|
||||
user.items.eggs = { Wolf: 1 };
|
||||
user.items.hatchingPotions = { Base: 1 };
|
||||
user.items.pets = {};
|
||||
hatch(user, { params: { egg: 'Wolf', hatchingPotion: 'Base' } });
|
||||
|
||||
expect(user.addAchievement).to.be.calledOnce;
|
||||
expect(user.addAchievement).to.be.calledWith('hatchedPet');
|
||||
|
||||
expect(shared.onboarding.checkOnboardingStatus).to.be.calledOnce;
|
||||
expect(shared.onboarding.checkOnboardingStatus).to.be.calledWith(user);
|
||||
});
|
||||
|
||||
it('does not add the onboarding achievement to the user if it\'s already been awarded', () => {
|
||||
user.items.eggs = { Wolf: 1 };
|
||||
user.items.hatchingPotions = { Base: 1 };
|
||||
user.items.pets = {};
|
||||
user.achievements.hatchedPet = true;
|
||||
|
||||
hatch(user, { params: { egg: 'Wolf', hatchingPotion: 'Base' } });
|
||||
|
||||
expect(user.addAchievement).to.not.be.called;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
NotAuthorized,
|
||||
} from '../../../website/common/script/libs/errors';
|
||||
import crit from '../../../website/common/script/fns/crit';
|
||||
import shared from '../../../website/common/script';
|
||||
|
||||
const EPSILON = 0.0001; // negligible distance between datapoints
|
||||
|
||||
@@ -340,5 +341,49 @@ describe('shared.ops.scoreTask', () => {
|
||||
expectClosePoints(ref.beforeUser, ref.afterUser, freshTodo, todo);
|
||||
});
|
||||
});
|
||||
|
||||
context('onboarding', () => {
|
||||
beforeEach(() => {
|
||||
ref.afterUser.addAchievement = sinon.spy();
|
||||
sinon.stub(shared.onboarding, 'checkOnboardingStatus');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
shared.onboarding.checkOnboardingStatus.restore();
|
||||
});
|
||||
|
||||
it('adds the achievement to the user and checks the onboarding status', () => {
|
||||
scoreTask({ user: ref.afterUser, task: todo, direction: 'up' });
|
||||
expect(ref.afterUser.addAchievement).to.be.calledOnce;
|
||||
expect(ref.afterUser.addAchievement).to.be.calledWith('completedTask');
|
||||
|
||||
expect(shared.onboarding.checkOnboardingStatus).to.be.calledOnce;
|
||||
expect(shared.onboarding.checkOnboardingStatus).to.be.calledWith(ref.afterUser);
|
||||
});
|
||||
|
||||
it('does not add the onboarding achievement to the user if it\'s already been awarded', () => {
|
||||
ref.afterUser.achievements.completedTask = true;
|
||||
scoreTask({ user: ref.afterUser, task: todo, direction: 'up' });
|
||||
|
||||
expect(ref.afterUser.addAchievement).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not add the onboarding achievement to the user if it\'s scored down', () => {
|
||||
scoreTask({ user: ref.afterUser, task: todo, direction: 'down' });
|
||||
|
||||
expect(ref.afterUser.addAchievement).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not add the onboarding achievement to the user if cron is running', () => {
|
||||
scoreTask({
|
||||
user: ref.afterUser,
|
||||
task: todo,
|
||||
direction: 'up',
|
||||
cron: true,
|
||||
});
|
||||
|
||||
expect(ref.afterUser.addAchievement).to.not.be.called;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -54,7 +54,7 @@ describe('shared.ops.unlock', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// disabled untill fully implemente
|
||||
// disabled until fully implemente
|
||||
xit('returns an error when user already owns items in a full set', done => {
|
||||
try {
|
||||
unlock(user, { query: { path: unlockPath } });
|
||||
|
||||
@@ -7,6 +7,7 @@ module.exports = {
|
||||
extends: [
|
||||
'habitrpg/lib/vue',
|
||||
],
|
||||
ignorePatterns: ['dist/', 'node_modules/'],
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# new_client
|
||||
# Habitica Client
|
||||
|
||||
## Project setup
|
||||
```
|
||||
@@ -27,3 +27,31 @@ npm run lint
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
|
||||
## Storybook
|
||||
|
||||
Storybook is mainly used while working on UI-Components to see changes faster instead of using the website.
|
||||
|
||||
### Start Storybook
|
||||
|
||||
```
|
||||
npm run storybook:serve
|
||||
```
|
||||
|
||||
This will start the storybook process, every `*.stories.js`-File is searched and added to the storybook overview.
|
||||
|
||||
### Storybook Worklow
|
||||
|
||||
Usually when you working on `component-name.vue` you also create a `component-name.stories.js` file.
|
||||
|
||||
Example of the stories structure - [Storybook Docs][StorybookDocsExample] - [CountBadge][CountBadgeExample]
|
||||
|
||||
[StorybookDocsExample]: https://storybook.js.org/docs/guides/guide-vue/#step-4-write-your-stories
|
||||
[CountBadgeExample]: src/components/ui/countBadge.stories.js
|
||||
|
||||
Each function or example of this component will be put after `storiesOf('Your Component', module)`,
|
||||
in a separate `.add('function of component', ...`
|
||||
|
||||
### Storybook Build
|
||||
|
||||
After each client build, storybook build is also triggered and will be available in `dist/storybook`
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import '@storybook/addon-actions/register';
|
||||
import '@storybook/addon-knobs/register';
|
||||
import '@storybook/addon-links/register';
|
||||
import '@storybook/addon-notes/register';
|
||||
@@ -0,0 +1,11 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { configure } from '@storybook/vue';
|
||||
import '../../src/assets/scss/index.scss';
|
||||
|
||||
const req = require.context('../../src', true, /.stories.js$/);
|
||||
|
||||
function loadStories () {
|
||||
req.keys().forEach(filename => req(filename));
|
||||
}
|
||||
|
||||
configure(loadStories, module);
|
||||
@@ -5,53 +5,61 @@
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"test:unit": "vue-cli-service test:unit --require ./tests/unit/helpers.js",
|
||||
"lint": "vue-cli-service lint .",
|
||||
"lint-no-fix": "vue-cli-service lint --no-fix .",
|
||||
"postinstall": "node ./scripts/npm-postinstall.js"
|
||||
"postinstall": "node ./scripts/npm-postinstall.js",
|
||||
"storybook:build": "vue-cli-service storybook:build -c config/storybook -o dist/storybook",
|
||||
"storybook:serve": "vue-cli-service storybook:serve -p 6006 -c config/storybook",
|
||||
"test:unit": "vue-cli-service test:unit --require ./tests/unit/helpers.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vue/cli-plugin-babel": "^4.0.5",
|
||||
"@vue/cli-plugin-eslint": "^4.0.5",
|
||||
"@vue/cli-plugin-router": "^4.0.5",
|
||||
"@vue/cli-plugin-unit-mocha": "^4.0.5",
|
||||
"@vue/cli-service": "^4.0.5",
|
||||
"@vue/cli-plugin-babel": "^4.1.2",
|
||||
"@vue/cli-plugin-eslint": "^4.1.2",
|
||||
"@vue/cli-plugin-router": "^4.1.2",
|
||||
"@vue/cli-plugin-unit-mocha": "^4.1.2",
|
||||
"@vue/cli-service": "^4.1.2",
|
||||
"@storybook/addon-actions": "^5.0.0",
|
||||
"@storybook/addon-knobs": "^5.0.0",
|
||||
"@storybook/addon-links": "^5.0.0",
|
||||
"@storybook/addon-notes": "^5.0.0",
|
||||
"@storybook/vue": "^5.2.5",
|
||||
"@vue/test-utils": "1.0.0-beta.29",
|
||||
"amplitude-js": "^5.6.0",
|
||||
"amplitude-js": "^5.8.0",
|
||||
"axios": "^0.19.0",
|
||||
"axios-progress-bar": "^1.2.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"bootstrap": "^4.3.1",
|
||||
"bootstrap": "^4.4.1",
|
||||
"bootstrap-vue": "^2.1.0",
|
||||
"chai": "^4.1.2",
|
||||
"core-js": "^3.4.1",
|
||||
"eslint": "^6.6.0",
|
||||
"core-js": "^3.6.1",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-habitrpg": "^6.2.0",
|
||||
"eslint-plugin-mocha": "^5.3.0",
|
||||
"eslint-plugin-vue": "^6.0.1",
|
||||
"habitica-markdown": "^1.3.0",
|
||||
"eslint-plugin-vue": "^6.1.2",
|
||||
"habitica-markdown": "^1.3.2",
|
||||
"hellojs": "^1.18.1",
|
||||
"inspectpack": "^4.2.2",
|
||||
"inspectpack": "^4.3.0",
|
||||
"intro.js": "^2.9.3",
|
||||
"jquery": "^3.4.1",
|
||||
"lodash": "^4.17.15",
|
||||
"moment": "^2.24.0",
|
||||
"nconf": "^0.10.0",
|
||||
"sass": "^1.23.6",
|
||||
"sass": "^1.24.2",
|
||||
"sass-loader": "^8.0.0",
|
||||
"smartbanner.js": "^1.14.5",
|
||||
"smartbanner.js": "^1.15.0",
|
||||
"svg-inline-loader": "^0.8.0",
|
||||
"svg-url-loader": "^3.0.2",
|
||||
"svg-url-loader": "^3.0.3",
|
||||
"svgo": "^1.3.2",
|
||||
"svgo-loader": "^2.2.1",
|
||||
"uuid": "^3.3.3",
|
||||
"validator": "^11.1.0",
|
||||
"vue": "^2.6.10",
|
||||
"vue": "^2.6.11",
|
||||
"vue-cli-plugin-storybook": "^0.6.1",
|
||||
"vue-mugen-scroll": "^0.2.6",
|
||||
"vue-router": "^3.0.6",
|
||||
"vue-template-compiler": "^2.6.10",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"vuedraggable": "^2.23.1",
|
||||
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#5d237615463a84a23dd6f3f77c6ab577d68593ec",
|
||||
"webpack": "^4.41.2"
|
||||
"webpack": "^4.41.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,4 +6,8 @@ if (process.env.NODE_ENV === 'production') {
|
||||
execSync('npm run build', {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
execSync('npm run storybook:build', {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -135,6 +135,15 @@
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.closepadding {
|
||||
margin: 11px 24px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
right: 0;
|
||||
top: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.container-fluid {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
@@ -165,15 +174,6 @@
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.closepadding {
|
||||
margin: 11px 24px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
right: 0;
|
||||
top: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.content {
|
||||
font-size: 12px;
|
||||
@@ -239,7 +239,11 @@ import subCancelModalConfirm from '@/components/payments/cancelModalConfirm';
|
||||
import subCanceledModal from '@/components/payments/canceledModal';
|
||||
|
||||
import spellsMixin from '@/mixins/spells';
|
||||
import { CONSTANTS, getLocalSetting, removeLocalSetting } from '@/libs/userlocalManager';
|
||||
import {
|
||||
CONSTANTS,
|
||||
getLocalSetting,
|
||||
removeLocalSetting,
|
||||
} from '@/libs/userlocalManager';
|
||||
|
||||
import svgClose from '@/assets/svg/close.svg';
|
||||
import bannedAccountModal from '@/components/bannedAccountModal';
|
||||
|
||||
@@ -1,48 +1,78 @@
|
||||
.promo_armoire_backgrounds_201911 {
|
||||
.promo_armoire_backgrounds_202001 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -451px;
|
||||
background-position: -424px -175px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_costume_achievement {
|
||||
.promo_g1g1_2019 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -747px;
|
||||
width: 144px;
|
||||
height: 156px;
|
||||
background-position: -403px -471px;
|
||||
width: 357px;
|
||||
height: 144px;
|
||||
}
|
||||
.promo_delightful_dinos {
|
||||
.promo_mystery_202001 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -424px -451px;
|
||||
width: 423px;
|
||||
background-position: -241px -619px;
|
||||
width: 279px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_ember_thunderstorm_potions {
|
||||
.promo_snowballs {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -599px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_mystery_201910 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -424px -599px;
|
||||
width: 282px;
|
||||
height: 147px;
|
||||
background-position: 0px 0px;
|
||||
width: 420px;
|
||||
height: 174px;
|
||||
}
|
||||
.promo_take_this {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -451px -365px;
|
||||
background-position: -761px -471px;
|
||||
width: 96px;
|
||||
height: 69px;
|
||||
}
|
||||
.scene_habitica_map {
|
||||
.promo_winter_potions_2020 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px 0px;
|
||||
width: 450px;
|
||||
height: 450px;
|
||||
background-position: 0px -323px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.scene_seaserpent {
|
||||
.promo_winter_quests_bundle {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -451px 0px;
|
||||
width: 476px;
|
||||
height: 364px;
|
||||
background-position: 0px -175px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_winter_wonderland_2019 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -471px;
|
||||
width: 402px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_winter_wonderland_2020 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -421px 0px;
|
||||
width: 468px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_wintery_hair {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -521px -619px;
|
||||
width: 152px;
|
||||
height: 75px;
|
||||
}
|
||||
.promo_wintery_skins {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -424px -323px;
|
||||
width: 420px;
|
||||
height: 147px;
|
||||
}
|
||||
.customize-option.promo_wintery_skins {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -449px -338px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.scene_list {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -619px;
|
||||
width: 240px;
|
||||
height: 195px;
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 481 KiB After Width: | Height: | Size: 476 KiB |
|
Before Width: | Height: | Size: 669 KiB After Width: | Height: | Size: 687 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 369 KiB After Width: | Height: | Size: 313 KiB |
|
Before Width: | Height: | Size: 318 KiB After Width: | Height: | Size: 353 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 182 KiB After Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 178 KiB |