Merge branch 'develop' into issue-11615

This commit is contained in:
alexthomson1
2020-01-12 00:20:13 -07:00
762 changed files with 47411 additions and 37824 deletions
-1
View File
@@ -1,3 +1,2 @@
node_modules
.git
website
+11 -5
View File
@@ -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
}
};
+118
View File
@@ -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
}
};
-110
View File
@@ -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;
+1 -1
View File
@@ -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());
});
+1 -1
View File
@@ -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);
}
});
+1 -1
View File
@@ -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
-72
View File
@@ -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
}
}
+581 -670
View File
File diff suppressed because it is too large Load Diff
+17 -16
View File
@@ -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": {}
+37 -24
View File
@@ -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;
+2 -2
View File
@@ -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)
+22
View File
@@ -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);
+1 -26
View File
@@ -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);
+46
View File
@@ -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;
+34 -5
View File
@@ -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`] } });
+5 -2
View File
@@ -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,
+97
View File
@@ -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,
+41
View File
@@ -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' },
]);
});
});
+100
View File
@@ -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);
});
});
+25
View File
@@ -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;
+30
View File
@@ -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;
});
});
});
+31
View File
@@ -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;
});
});
});
});
+45
View File
@@ -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;
});
});
});
});
+1 -1
View File
@@ -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 } });
+1
View File
@@ -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',
+29 -1
View File
@@ -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';
+11
View File
@@ -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);
+5175 -991
View File
File diff suppressed because it is too large Load Diff
+28 -20
View File
@@ -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',
});
}
+14 -10
View File
@@ -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;
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 481 KiB

After

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 669 KiB

After

Width:  |  Height:  |  Size: 687 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 369 KiB

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 318 KiB

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 178 KiB

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