Merge branch 'develop' of https://github.com/HabitRPG/habitica into negue/flagpm

# Conflicts:
#	website/client/components/chat/chatCard.vue
#	website/client/components/chat/chatMessages.vue
#	website/common/locales/en/messages.json
#	website/server/libs/slack.js
This commit is contained in:
negue
2018-09-22 19:18:08 +02:00
1128 changed files with 51381 additions and 42173 deletions

1
.gitignore vendored
View File

@@ -39,6 +39,7 @@ dist-client
test/client/unit/coverage
test/client/e2e/reports
test/client-old/spec/mocks/translations.js
yarn.lock
# Elastic Beanstalk Files
.elasticbeanstalk/*

View File

@@ -1,18 +1,29 @@
FROM node:8
ENV ADMIN_EMAIL admin@habitica.com
ENV AMAZON_PAYMENTS_CLIENT_ID amzn1.application-oa2-client.68ed9e6904ef438fbc1bf86bf494056e
ENV AMAZON_PAYMENTS_SELLER_ID AMQ3SB4SG5E91
ENV AMPLITUDE_KEY e8d4c24b3d6ef3ee73eeba715023dd43
ENV BASE_URL https://habitica.com
ENV FACEBOOK_KEY 128307497299777
ENV GA_ID UA-33510635-1
ENV GOOGLE_CLIENT_ID 1035232791481-32vtplgnjnd1aufv3mcu1lthf31795fq.apps.googleusercontent.com
ENV NODE_ENV production
ENV STRIPE_PUB_KEY pk_85fQ0yMECHNfHTSsZoxZXlPSwSNfA
# Install global packages
RUN npm install -g gulp-cli mocha
# Clone Habitica repo and install dependencies
RUN mkdir -p /usr/src/habitrpg
WORKDIR /usr/src/habitrpg
RUN git clone https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
RUN cp config.json.example config.json
RUN git clone --branch release https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
RUN npm install
RUN gulp build:prod --force
# Create Build dir
RUN mkdir -p ./website/build
# Start Habitica
EXPOSE 3000
CMD ["npm", "start"]
CMD ["node", "./website/transpiled-babel/index.js"]

18
Dockerfile-Dev Normal file
View File

@@ -0,0 +1,18 @@
FROM node:8
# Install global packages
RUN npm install -g gulp-cli mocha
# Clone Habitica repo and install dependencies
RUN mkdir -p /usr/src/habitrpg
WORKDIR /usr/src/habitrpg
RUN git clone https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
RUN cp config.json.example config.json
RUN npm install
# Create Build dir
RUN mkdir -p ./website/build
# Start Habitica
EXPOSE 3000
CMD ["npm", "start"]

View File

@@ -1,29 +0,0 @@
FROM node:8
ENV ADMIN_EMAIL admin@habitica.com
ENV AMAZON_PAYMENTS_CLIENT_ID amzn1.application-oa2-client.68ed9e6904ef438fbc1bf86bf494056e
ENV AMAZON_PAYMENTS_SELLER_ID AMQ3SB4SG5E91
ENV AMPLITUDE_KEY e8d4c24b3d6ef3ee73eeba715023dd43
ENV BASE_URL https://habitica.com
ENV FACEBOOK_KEY 128307497299777
ENV GA_ID UA-33510635-1
ENV GOOGLE_CLIENT_ID 1035232791481-32vtplgnjnd1aufv3mcu1lthf31795fq.apps.googleusercontent.com
ENV NODE_ENV production
ENV STRIPE_PUB_KEY pk_85fQ0yMECHNfHTSsZoxZXlPSwSNfA
# Install global packages
RUN npm install -g gulp-cli mocha
# Clone Habitica repo and install dependencies
RUN mkdir -p /usr/src/habitrpg
WORKDIR /usr/src/habitrpg
RUN git clone --branch v4.48.0 https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
RUN npm install
RUN gulp build:prod --force
# Create Build dir
RUN mkdir -p ./website/build
# Start Habitica
EXPOSE 3000
CMD ["node", "./website/transpiled-babel/index.js"]

View File

@@ -78,6 +78,9 @@
"PUSH_CONFIGS": {
"GCM_SERVER_API_KEY": "",
"APN_ENABLED": "false",
"APN_KEY_ID": "xxxxxxxxxx",
"APN_KEY": "xxxxxxxxxx",
"APN_TEAM_ID": "aaabbbcccd",
"FCM_SERVER_API_KEY": ""
},
"SITE_HTTP_AUTH": {

View File

@@ -2,9 +2,13 @@ version: "3"
services:
client:
environment:
- NODE_ENV=development
volumes:
- '.:/usr/src/habitrpg'
server:
environment:
- NODE_ENV=development
volumes:
- '.:/usr/src/habitrpg'

View File

@@ -0,0 +1,123 @@
const migrationName = '20180811_inboxOutsideUser.js';
const authorName = 'paglias'; // in case script author needs to know when their ...
const authorUuid = 'ed4c688c-6652-4a92-9d03-a5a79844174a'; // ... own data is done
/*
* Move inbox messages from the user model to their own collection
*/
const monk = require('monk');
const nconf = require('nconf');
const Inbox = require('../website/server/models/message').inboxModel;
const connectionString = nconf.get('MIGRATION_CONNECT_STRING'); // FOR TEST DATABASE
const dbInboxes = monk(connectionString).get('inboxes', { castIds: false });
const dbUsers = monk(connectionString).get('users', { castIds: false });
function processUsers (lastId) {
let query = {
migration: {$ne: migrationName},
};
if (lastId) {
query._id = {
$gt: lastId,
};
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 1000,
fields: ['_id', 'inbox'],
})
.then(updateUsers)
.catch((err) => {
console.log(err);
return exiting(1, `ERROR! ${ err}`);
});
}
let progressCount = 1000;
let count = 0;
let msgCount = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users and their tasks found and modified.');
displayData();
return;
}
let usersPromises = users.map(updateUser);
let lastUser = users[users.length - 1];
return Promise.all(usersPromises)
.then(() => {
return processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
if (count % progressCount === 0) console.warn(`${count } ${ user._id}`);
if (msgCount % progressCount === 0) console.warn(`${msgCount } messages processed`);
if (user._id === authorUuid) console.warn(`${authorName } being processed`);
const oldInboxMessages = user.inbox.messages || {};
const oldInboxMessagesIds = Object.keys(oldInboxMessages);
msgCount += oldInboxMessagesIds.length;
const newInboxMessages = oldInboxMessagesIds.map(msgId => {
const msg = oldInboxMessages[msgId];
if (!msg || (!msg.id && !msg._id)) { // eslint-disable-line no-extra-parens
console.log('missing message or message _id and id', msg);
throw new Error('error!');
}
if (msg.id && !msg._id) msg._id = msg.id;
if (msg._id && !msg.id) msg.id = msg._id;
const newMsg = new Inbox(msg);
newMsg.ownerId = user._id;
return newMsg.toJSON();
});
return dbInboxes.insert(newInboxMessages)
.then(() => {
return dbUsers.update({_id: user._id}, {
$set: {
migration: migrationName,
'inbox.messages': {},
},
});
})
.catch((err) => {
console.log(err);
return exiting(1, `ERROR! ${ err}`);
});
}
function displayData () {
console.warn(`\n${ count } users processed\n`);
console.warn(`\n${ msgCount } messages 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;

View File

@@ -17,5 +17,5 @@ function setUpServer () {
setUpServer();
// Replace this with your migration
const processUsers = require('./tasks/habits-one-history-entry-per-day-challenges.js');
const processUsers = require('./users/takeThis.js');
processUsers();

View File

@@ -1,4 +1,4 @@
// const migrationName = 'habits-one-history-entry-per-day';
const migrationName = 'habits-one-history-entry-per-day';
const authorName = 'paglias'; // in case script author needs to know when their ...
const authorUuid = 'ed4c688c-6652-4a92-9d03-a5a79844174a'; // ... own data is done
@@ -14,7 +14,9 @@ const dbTasks = monk(connectionString).get('tasks', { castIds: false });
const dbUsers = monk(connectionString).get('users', { castIds: false });
function processUsers (lastId) {
let query = {};
let query = {
migration: {$ne: migrationName},
};
if (lastId) {
query._id = {
@@ -127,6 +129,11 @@ function updateUser (user) {
.then(habits => {
return Promise.all(habits.map(habit => updateHabit(habit, timezoneOffset, dayStart)));
})
.then(() => {
return dbUsers.update({_id: user._id}, {
$set: {migration: migrationName},
});
})
.catch((err) => {
console.log(err);
return exiting(1, `ERROR! ${ err}`);

View File

@@ -1,14 +1,14 @@
import monk from 'monk';
import nconf from 'nconf';
const migrationName = 'mystery-items-201806.js'; // Update per month
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_201806', 'head_mystery_201806'];
const MYSTERY_ITEMS = ['armor_mystery_201808', 'head_mystery_201808'];
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING');
let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false });

View File

@@ -0,0 +1,123 @@
let migrationName = '20180731_naming-day.js'; // Update when running in future years
let authorName = 'Sabe'; // in case script author needs to know when their ...
let authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
/*
* Award Naming Day ladder items to participants in this month's Naming Day festivities
*/
import monk from 'monk';
import nconf from 'nconf';
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING'); // FOR TEST DATABASE
let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false });
function processUsers (lastId) {
// specify a query to limit the affected users (empty for all users):
let query = {
migration: {$ne: migrationName},
};
if (lastId) {
query._id = {
$gt: lastId,
};
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 250,
fields: [
'items.gear.owned',
'items.mounts',
'items.pets',
], // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
})
.then(updateUsers)
.catch((err) => {
console.log(err);
return exiting(1, `ERROR! ${ err}`);
});
}
let progressCount = 1000;
let count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
displayData();
return;
}
let userPromises = users.map(updateUser);
let lastUser = users[users.length - 1];
return Promise.all(userPromises)
.then(() => {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
let set = {};
let push;
const inc = {
'items.food.Cake_Base': 1,
'items.food.Cake_CottonCandyBlue': 1,
'items.food.Cake_CottonCandyPink': 1,
'items.food.Cake_Desert': 1,
'items.food.Cake_Golden': 1,
'items.food.Cake_Red': 1,
'items.food.Cake_Shade': 1,
'items.food.Cake_Skeleton': 1,
'items.food.Cake_White': 1,
'items.food.Cake_Zombie': 1,
'achievements.habiticaDays': 1,
};
if (user && user.items && user.items.gear && user.items.gear.owned && typeof user.items.gear.owned.head_special_namingDay2017 !== 'undefined') {
set = {migration: migrationName, 'items.gear.owned.body_special_namingDay2018': false};
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.body_special_namingDay2018', _id: monk.id()}};
} else if (user && user.items && user.items.pets && typeof user.items.pets['Gryphon-RoyalPurple'] !== 'undefined') {
set = {migration: migrationName, 'items.gear.owned.head_special_namingDay2017': false};
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.head_special_namingDay2017', _id: monk.id()}};
} else if (user && user.items && user.items.mounts && typeof user.items.mounts['Gryphon-RoyalPurple'] !== 'undefined') {
set = {migration: migrationName, 'items.pets.Gryphon-RoyalPurple': 5};
} else {
set = {migration: migrationName, 'items.mounts.Gryphon-RoyalPurple': true};
}
if (push) {
dbUsers.update({_id: user._id}, {$set: set, $push: push, $inc: inc});
} else {
dbUsers.update({_id: user._id}, {$set: set, $inc: inc});
}
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;

View File

@@ -0,0 +1,99 @@
let migrationName = '20180724_summer-splash-orcas.js'; // Update per month
let authorName = 'Sabe'; // in case script author needs to know when their ...
let authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
/*
* Award ladder items to participants in this year's Summer Splash festivities
*/
import monk from 'monk';
import nconf from 'nconf';
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING');
let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false });
function processUsers (lastId) {
// specify a query to limit the affected users (empty for all users):
let query = {
migration: {$ne: migrationName},
'auth.timestamps.loggedin': {$gt: new Date('2018-07-01')}, // rerun without date restriction after initial run
};
if (lastId) {
query._id = {
$gt: lastId,
};
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 250,
fields: [
'items.mounts',
], // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
})
.then(updateUsers)
.catch((err) => {
console.log(err);
return exiting(1, `ERROR! ${ err}`);
});
}
let progressCount = 1000;
let count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
displayData();
return;
}
let userPromises = users.map(updateUser);
let lastUser = users[users.length - 1];
return Promise.all(userPromises)
.then(() => {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
let set = {};
if (user && user.items && user.items.pets && typeof user.items.pets['Orca-Base'] !== 'undefined') {
set = {migration: migrationName};
} else if (user && user.items && user.items.mounts && typeof user.items.mounts['Orca-Base'] !== 'undefined') {
set = {migration: migrationName, 'items.pets.Orca-Base': 5};
} else {
set = {migration: migrationName, 'items.mounts.Orca-Base': true};
}
dbUsers.update({_id: user._id}, {$set: set});
if (count % progressCount === 0) console.warn(`${count } ${ user._id}`);
if (user._id === authorUuid) console.warn(`${authorName } processed`);
}
function displayData () {
console.warn(`\n${ count } users processed\n`);
return exiting(0);
}
function exiting (code, msg) {
code = code || 0; // 0 = success
if (code && !msg) {
msg = 'ERROR!';
}
if (msg) {
if (code) {
console.error(msg);
} else {
console.log(msg);
}
}
process.exit(code);
}
module.exports = processUsers;

View File

@@ -1,4 +1,4 @@
let migrationName = '20180102_takeThis.js'; // Update per month
let migrationName = '20180904_takeThis.js'; // Update per month
let authorName = 'Sabe'; // in case script author needs to know when their ...
let authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
@@ -6,15 +6,16 @@ let authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
* Award Take This ladder items to participants in this month's challenge
*/
let monk = require('monk');
let connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
let dbUsers = monk(connectionString).get('users', { castIds: false });
import monk from 'monk';
import nconf from 'nconf';
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING'); // FOR TEST DATABASE
let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false });
function processUsers (lastId) {
// specify a query to limit the affected users (empty for all users):
let query = {
migration: {$ne: migrationName},
challenges: {$in: ['5f70ce5b-2d82-4114-8e44-ca65615aae62']}, // Update per month
challenges: {$in: ['1044ec0c-4a85-48c5-9f36-d51c0c62c7d3']}, // Update per month
};
if (lastId) {

2219
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "4.48.1",
"version": "4.61.0",
"main": "./website/server/index.js",
"dependencies": {
"@slack/client": "^3.8.1",
@@ -9,6 +9,7 @@
"amazon-payments": "^0.2.7",
"amplitude": "^3.5.0",
"apidoc": "^0.17.5",
"apn": "^2.2.0",
"autoprefixer": "^8.5.0",
"aws-sdk": "^2.239.1",
"axios": "^0.18.0",
@@ -25,7 +26,7 @@
"babel-preset-es2015": "^6.6.0",
"babel-register": "^6.6.0",
"babel-runtime": "^6.11.6",
"bcrypt": "^2.0.0",
"bcrypt": "github:MylesBorins/node.bcrypt.js#update-nan",
"body-parser": "^1.18.3",
"bootstrap": "^4.1.1",
"bootstrap-vue": "^2.0.0-rc.9",
@@ -34,7 +35,7 @@
"coupon-code": "^0.4.5",
"cross-env": "^5.1.5",
"css-loader": "^0.28.11",
"csv-stringify": "^2.1.0",
"csv-stringify": "^3.0.0",
"cwait": "^1.1.1",
"domain-middleware": "~0.1.0",
"express": "^4.16.3",
@@ -42,7 +43,7 @@
"express-validator": "^5.2.0",
"extract-text-webpack-plugin": "^3.0.2",
"glob": "^7.1.2",
"got": "^8.3.1",
"got": "^9.0.0",
"gulp": "^4.0.0",
"gulp-babel": "^7.0.1",
"gulp-imagemin": "^4.1.0",
@@ -78,11 +79,11 @@
"postcss-easy-import": "^3.0.0",
"ps-tree": "^1.0.0",
"pug": "^2.0.3",
"push-notify": "git://github.com/habitrpg/push-notify.git#6bc2b5fdb1bdc9649b9ec1964d79ca50187fc8a9",
"pusher": "^1.3.0",
"rimraf": "^2.4.3",
"sass-loader": "^7.0.0",
"shelljs": "^0.8.2",
"smartbanner.js": "^1.9.1",
"stripe": "^5.9.0",
"superagent": "^3.8.3",
"svg-inline-loader": "^0.8.0",
@@ -95,7 +96,7 @@
"url-loader": "^1.0.0",
"useragent": "^2.1.9",
"uuid": "^3.0.1",
"validator": "^9.4.1",
"validator": "^10.5.0",
"vinyl-buffer": "^1.0.1",
"vue": "^2.5.16",
"vue-loader": "^14.2.2",
@@ -163,19 +164,19 @@
"expect.js": "^0.3.1",
"http-proxy-middleware": "^0.18.0",
"istanbul": "^1.1.0-alpha.1",
"karma": "^2.0.2",
"karma": "^3.0.0",
"karma-babel-preprocessor": "^7.0.0",
"karma-chai-plugins": "^0.9.0",
"karma-chrome-launcher": "^2.2.0",
"karma-coverage": "^1.1.2",
"karma-mocha": "^1.3.0",
"karma-mocha-reporter": "^2.2.5",
"karma-sinon-chai": "^1.3.4",
"karma-sinon-chai": "^2.0.0",
"karma-sinon-stub-promise": "^1.0.0",
"karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "0.0.32",
"karma-webpack": "^3.0.0",
"lcov-result-merger": "^2.0.0",
"lcov-result-merger": "^3.0.0",
"mocha": "^5.1.1",
"monk": "^6.0.6",
"nightwatch": "^0.9.21",

View File

@@ -65,6 +65,12 @@ describe('cron', () => {
expect(analytics.track.callCount).to.equal(1);
});
it('calls analytics when user is sleeping', () => {
user.preferences.sleep = true;
cron({user, tasksByType, daysMissed, analytics});
expect(analytics.track.callCount).to.equal(1);
});
describe('end of the month perks', () => {
beforeEach(() => {
user.purchased.plan.customerId = 'subscribedId';
@@ -655,76 +661,6 @@ describe('cron', () => {
});
});
describe('user is sleeping', () => {
beforeEach(() => {
user.preferences.sleep = true;
});
it('calls analytics', () => {
cron({user, tasksByType, daysMissed, analytics});
expect(analytics.track.callCount).to.equal(1);
});
it('clears user buffs', () => {
user.stats.buffs = {
str: 1,
int: 1,
per: 1,
con: 1,
stealth: 1,
streaks: true,
};
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.buffs.str).to.equal(0);
expect(user.stats.buffs.int).to.equal(0);
expect(user.stats.buffs.per).to.equal(0);
expect(user.stats.buffs.con).to.equal(0);
expect(user.stats.buffs.stealth).to.equal(0);
expect(user.stats.buffs.streaks).to.be.false;
});
it('resets all dailies without damaging user', () => {
let daily = {
text: 'test daily',
type: 'daily',
frequency: 'daily',
everyX: 5,
startDate: new Date(),
};
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line new-cap
tasksByType.dailys.push(task);
tasksByType.dailys[0].completed = true;
let healthBefore = user.stats.hp;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].completed).to.be.false;
expect(user.stats.hp).to.equal(healthBefore);
});
it('sets isDue for daily', () => {
let daily = {
text: 'test daily',
type: 'daily',
frequency: 'daily',
everyX: 5,
startDate: new Date(),
};
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line new-cap
tasksByType.dailys.push(task);
tasksByType.dailys[0].completed = true;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].isDue).to.be.exist;
});
});
describe('todos', () => {
beforeEach(() => {
let todo = {
@@ -846,6 +782,15 @@ describe('cron', () => {
expect(tasksByType.dailys[0].isDue).to.be.false;
});
it('computes isDue when user is sleeping', () => {
user.preferences.sleep = true;
tasksByType.dailys[0].frequency = 'daily';
tasksByType.dailys[0].everyX = 5;
tasksByType.dailys[0].startDate = moment().toDate();
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].isDue).to.exist;
});
it('computes nextDue', () => {
tasksByType.dailys[0].frequency = 'daily';
tasksByType.dailys[0].everyX = 5;
@@ -865,6 +810,13 @@ describe('cron', () => {
expect(tasksByType.dailys[0].completed).to.be.false;
});
it('should set tasks completed to false when user is sleeping', () => {
user.preferences.sleep = true;
tasksByType.dailys[0].completed = true;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].completed).to.be.false;
});
it('should reset task checklist for completed dailys', () => {
tasksByType.dailys[0].checklist.push({title: 'test', completed: false});
tasksByType.dailys[0].completed = true;
@@ -872,6 +824,14 @@ describe('cron', () => {
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
});
it('should reset task checklist for completed dailys when user is sleeping', () => {
user.preferences.sleep = true;
tasksByType.dailys[0].checklist.push({title: 'test', completed: false});
tasksByType.dailys[0].completed = true;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
});
it('should reset task checklist for dailys with scheduled misses', () => {
daysMissed = 10;
tasksByType.dailys[0].checklist.push({title: 'test', completed: false});
@@ -884,12 +844,19 @@ describe('cron', () => {
daysMissed = 1;
let hpBefore = user.stats.hp;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.hp).to.be.lessThan(hpBefore);
});
it('should not do damage for missing a daily when user is sleeping', () => {
user.preferences.sleep = true;
daysMissed = 1;
let hpBefore = user.stats.hp;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.hp).to.equal(hpBefore);
});
it('should not do damage for missing a daily when CRON_SAFE_MODE is set', () => {
sandbox.stub(nconf, 'get').withArgs('CRON_SAFE_MODE').returns('true');
let cronOverride = requireAgain(pathToCronLib).cron;
@@ -930,7 +897,7 @@ describe('cron', () => {
expect(hpDifferenceOfPartiallyIncompleteDaily).to.be.lessThan(hpDifferenceOfFullyIncompleteDaily);
});
it('should decrement quest progress down for missing a daily', () => {
it('should decrement quest.progress.down for missing a daily', () => {
daysMissed = 1;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
@@ -939,6 +906,16 @@ describe('cron', () => {
expect(progress.down).to.equal(-1);
});
it('should not decrement quest.progress.down for missing a daily when user is sleeping', () => {
user.preferences.sleep = true;
daysMissed = 1;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
let progress = cron({user, tasksByType, daysMissed, analytics});
expect(progress.down).to.equal(0);
});
it('should do damage for only yesterday\'s dailies', () => {
daysMissed = 3;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
@@ -1017,7 +994,7 @@ describe('cron', () => {
expect(tasksByType.habits[0].counterDown).to.equal(0);
});
it('should reset habit counters even if user is resting in the Inn', () => {
it('should reset habit counters even if user is sleeping', () => {
user.preferences.sleep = true;
tasksByType.habits[0].counterUp = 1;
tasksByType.habits[0].counterDown = 1;
@@ -1278,7 +1255,23 @@ describe('cron', () => {
expect(user.achievements.perfect).to.equal(0);
});
it('increments user buffs if all (at least 1) due dailies were completed', () => {
it('gives perfect day buff if all (at least 1) due dailies were completed', () => {
daysMissed = 1;
tasksByType.dailys[0].completed = true;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
let previousBuffs = user.stats.buffs.toObject();
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
expect(user.stats.buffs.int).to.be.greaterThan(previousBuffs.int);
expect(user.stats.buffs.per).to.be.greaterThan(previousBuffs.per);
expect(user.stats.buffs.con).to.be.greaterThan(previousBuffs.con);
});
it('gives perfect day buff if all (at least 1) due dailies were completed when user is sleeping', () => {
user.preferences.sleep = true;
daysMissed = 1;
tasksByType.dailys[0].completed = true;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
@@ -1317,6 +1310,31 @@ describe('cron', () => {
expect(user.stats.buffs.streaks).to.be.false;
});
it('clears buffs if user does not have a perfect day (no due dailys) when user is sleeping', () => {
user.preferences.sleep = true;
daysMissed = 1;
tasksByType.dailys[0].completed = true;
tasksByType.dailys[0].startDate = moment(new Date()).add({days: 1});
user.stats.buffs = {
str: 1,
int: 1,
per: 1,
con: 1,
stealth: 0,
streaks: true,
};
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.buffs.str).to.equal(0);
expect(user.stats.buffs.int).to.equal(0);
expect(user.stats.buffs.per).to.equal(0);
expect(user.stats.buffs.con).to.equal(0);
expect(user.stats.buffs.stealth).to.equal(0);
expect(user.stats.buffs.streaks).to.be.false;
});
it('clears buffs if user does not have a perfect day (at least one due daily not completed)', () => {
daysMissed = 1;
tasksByType.dailys[0].completed = false;
@@ -1341,7 +1359,50 @@ describe('cron', () => {
expect(user.stats.buffs.streaks).to.be.false;
});
it('still grants a perfect day when CRON_SAFE_MODE is set', () => {
it('clears buffs if user does not have a perfect day (at least one due daily not completed) when user is sleeping', () => {
user.preferences.sleep = true;
daysMissed = 1;
tasksByType.dailys[0].completed = false;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
user.stats.buffs = {
str: 1,
int: 1,
per: 1,
con: 1,
stealth: 0,
streaks: true,
};
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.buffs.str).to.equal(0);
expect(user.stats.buffs.int).to.equal(0);
expect(user.stats.buffs.per).to.equal(0);
expect(user.stats.buffs.con).to.equal(0);
expect(user.stats.buffs.stealth).to.equal(0);
expect(user.stats.buffs.streaks).to.be.false;
});
it('always grants a perfect day buff when CRON_SAFE_MODE is set', () => {
sandbox.stub(nconf, 'get').withArgs('CRON_SAFE_MODE').returns('true');
let cronOverride = requireAgain(pathToCronLib).cron;
daysMissed = 1;
tasksByType.dailys[0].completed = false;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
let previousBuffs = user.stats.buffs.toObject();
cronOverride({user, tasksByType, daysMissed, analytics});
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
expect(user.stats.buffs.int).to.be.greaterThan(previousBuffs.int);
expect(user.stats.buffs.per).to.be.greaterThan(previousBuffs.per);
expect(user.stats.buffs.con).to.be.greaterThan(previousBuffs.con);
});
it('always grants a perfect day buff when CRON_SAFE_MODE is set when user is sleeping', () => {
user.preferences.sleep = true;
sandbox.stub(nconf, 'get').withArgs('CRON_SAFE_MODE').returns('true');
let cronOverride = requireAgain(pathToCronLib).cron;
daysMissed = 1;
@@ -1373,6 +1434,20 @@ describe('cron', () => {
common.statsComputed.restore();
});
it('should not add mp to user when user is sleeping', () => {
const statsComputedRes = common.statsComputed(user);
const stubbedStatsComputed = sinon.stub(common, 'statsComputed');
user.preferences.sleep = true;
let mpBefore = user.stats.mp;
tasksByType.dailys[0].completed = true;
stubbedStatsComputed.returns(Object.assign(statsComputedRes, {maxMP: 100}));
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.mp).to.equal(mpBefore);
common.statsComputed.restore();
});
it('set user\'s mp to statsComputed.maxMP when user.stats.mp is greater', () => {
const statsComputedRes = common.statsComputed(user);
const stubbedStatsComputed = sinon.stub(common, 'statsComputed');
@@ -1514,27 +1589,6 @@ describe('cron', () => {
flagCount: 0,
};
});
xit('does not clear pms under 200', () => {
cron({user, tasksByType, daysMissed, analytics});
expect(user.inbox.messages[lastMessageId]).to.exist;
});
xit('clears pms over 200', () => {
let messageId = common.uuid();
user.inbox.messages[messageId] = {
id: messageId,
text: `test ${messageId}`,
timestamp: Number(new Date()),
likes: {},
flags: {},
flagCount: 0,
};
cron({user, tasksByType, daysMissed, analytics});
expect(user.inbox.messages[messageId]).to.not.exist;
});
});
describe('login incentives', () => {
@@ -1568,7 +1622,7 @@ describe('cron', () => {
expect(user.loginIncentives).to.eql(1);
});
it('increments loginIncentives by 1 even if user has Dailies paused', () => {
it('increments loginIncentives by 1 even if user is sleeping', () => {
user.preferences.sleep = true;
cron({user, tasksByType, daysMissed, analytics});
expect(user.loginIncentives).to.eql(1);

View File

@@ -107,6 +107,25 @@ describe('Password Utilities', () => {
}
});
it('defaults to SHA1 encryption if salt is provided', async () => {
let textPassword = 'mySecretPassword';
let salt = sha1MakeSalt();
let hashedPassword = sha1EncryptPassword(textPassword, salt);
let user = {
auth: {
local: {
hashed_password: hashedPassword,
salt,
passwordHashMethod: '',
},
},
};
let isValidPassword = await compare(user, textPassword);
expect(isValidPassword).to.eql(true);
});
it('throws an error if an invalid hashing method is used', async () => {
try {
await compare({

View File

@@ -1,6 +1,6 @@
import { model as User } from '../../../../website/server/models/user';
import requireAgain from 'require-again';
import pushNotify from 'push-notify';
import apn from 'apn/mock';
import nconf from 'nconf';
import gcmLib from 'node-gcm'; // works with FCM notifications too
@@ -24,7 +24,7 @@ describe('pushNotifications', () => {
sandbox.stub(gcmLib.Sender.prototype, 'send').callsFake(fcmSendSpy);
sandbox.stub(pushNotify, 'apn').returns({
sandbox.stub(apn.Provider.prototype, 'send').returns({
on: () => null,
send: apnSendSpy,
});
@@ -104,10 +104,7 @@ describe('pushNotifications', () => {
},
};
sendPushNotification(user, details);
expect(apnSendSpy).to.have.been.calledOnce;
expect(apnSendSpy).to.have.been.calledWithMatch({
token: '123',
const expectedNotification = new apn.Notification({
alert: message,
sound: 'default',
category: 'fun',
@@ -117,6 +114,10 @@ describe('pushNotifications', () => {
b: true,
},
});
sendPushNotification(user, details);
expect(apnSendSpy).to.have.been.calledOnce;
expect(apnSendSpy).to.have.been.calledWithMatch(expectedNotification, '123');
expect(fcmSendSpy).to.not.have.been.called;
});
});

View File

@@ -58,7 +58,7 @@ describe('slack', () => {
title: 'Flag in Some group - (private guild)',
title_link: undefined,
text: 'some text',
footer: sandbox.match(/<.*?groupId=group-id&chatId=chat-id\|Flag this message>/),
footer: sandbox.match(/<.*?groupId=group-id&chatId=chat-id\|Flag this message.>/),
mrkdwn_in: [
'text',
],

View File

@@ -20,7 +20,7 @@ import { TAVERN_ID } from '../../../../website/common/script/';
import shared from '../../../../website/common';
describe('Group Model', () => {
let party, questLeader, participatingMember, nonParticipatingMember, undecidedMember;
let party, questLeader, participatingMember, sleepingParticipatingMember, nonParticipatingMember, undecidedMember;
beforeEach(async () => {
sandbox.stub(email, 'sendTxn');
@@ -48,6 +48,11 @@ describe('Group Model', () => {
party: { _id: party._id },
profile: { name: 'Participating Member' },
});
sleepingParticipatingMember = new User({
party: { _id: party._id },
profile: { name: 'Sleeping Participating Member' },
preferences: { sleep: true },
});
nonParticipatingMember = new User({
party: { _id: party._id },
profile: { name: 'Non-Participating Member' },
@@ -61,6 +66,7 @@ describe('Group Model', () => {
party.save(),
questLeader.save(),
participatingMember.save(),
sleepingParticipatingMember.save(),
nonParticipatingMember.save(),
undecidedMember.save(),
]);
@@ -80,6 +86,7 @@ describe('Group Model', () => {
party.quest.members = {
[questLeader._id]: true,
[participatingMember._id]: true,
[sleepingParticipatingMember._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
};
@@ -175,6 +182,34 @@ describe('Group Model', () => {
expect(party._processBossQuest).to.not.be.called;
expect(Group.prototype._processCollectionQuest).to.be.calledOnce;
});
it('does not call _processBossQuest when user is resting in the inn', async () => {
party.quest.key = 'whale';
await party.startQuest(questLeader);
await party.save();
await Group.processQuestProgress(sleepingParticipatingMember, progress);
party = await Group.findOne({_id: party._id});
expect(party._processBossQuest).to.not.be.called;
expect(party._processCollectionQuest).to.not.be.called;
});
it('does not call _processCollectionQuest when user is resting in the inn', async () => {
party.quest.key = 'evilsanta2';
await party.startQuest(questLeader);
await party.save();
await Group.processQuestProgress(sleepingParticipatingMember, progress);
party = await Group.findOne({_id: party._id});
expect(party._processBossQuest).to.not.be.called;
expect(party._processCollectionQuest).to.not.be.called;
});
});
context('Boss Quests', () => {
@@ -216,17 +251,20 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
updatedNonParticipatingMember,
updatedUndecidedMember,
] = await Promise.all([
User.findById(questLeader._id),
User.findById(participatingMember._id),
User.findById(sleepingParticipatingMember._id),
User.findById(nonParticipatingMember._id),
User.findById(undecidedMember._id),
]);
expect(updatedLeader.stats.hp).to.eql(42.5);
expect(updatedParticipatingMember.stats.hp).to.eql(42.5);
expect(updatedSleepingParticipatingMember.stats.hp).to.eql(42.5);
expect(updatedNonParticipatingMember.stats.hp).to.eql(50);
expect(updatedUndecidedMember.stats.hp).to.eql(50);
});
@@ -236,6 +274,7 @@ describe('Group Model', () => {
party.quest.members = {
[questLeader._id]: true,
[participatingMember._id]: true,
[sleepingParticipatingMember._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
};
@@ -248,17 +287,20 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
updatedNonParticipatingMember,
updatedUndecidedMember,
] = await Promise.all([
User.findById(questLeader._id),
User.findById(participatingMember._id),
User.findById(sleepingParticipatingMember._id),
User.findById(nonParticipatingMember._id),
User.findById(undecidedMember._id),
]);
expect(updatedLeader.stats.hp).to.eql(42.5);
expect(updatedParticipatingMember.stats.hp).to.eql(42.5);
expect(updatedSleepingParticipatingMember.stats.hp).to.eql(42.5);
expect(updatedNonParticipatingMember.stats.hp).to.eql(50);
expect(updatedUndecidedMember.stats.hp).to.eql(50);
});
@@ -497,9 +539,11 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
] = await Promise.all([
User.findById(questLeader._id),
User.findById(participatingMember._id),
User.findById(sleepingParticipatingMember._id),
]);
expect(updatedLeader.achievements.quests[party.quest.key]).to.eql(1);
@@ -508,6 +552,9 @@ describe('Group Model', () => {
expect(updatedParticipatingMember.achievements.quests[party.quest.key]).to.eql(1);
expect(updatedParticipatingMember.stats.exp).to.be.greaterThan(0);
expect(updatedParticipatingMember.stats.gp).to.be.greaterThan(0);
expect(updatedSleepingParticipatingMember.achievements.quests[party.quest.key]).to.eql(1);
expect(updatedSleepingParticipatingMember.stats.exp).to.be.greaterThan(0);
expect(updatedSleepingParticipatingMember.stats.gp).to.be.greaterThan(0);
});
});
});
@@ -647,6 +694,7 @@ describe('Group Model', () => {
it('returns an array of members whose quest status set to true', () => {
party.quest.members = {
[participatingMember._id]: true,
[sleepingParticipatingMember._id]: true,
[questLeader._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
@@ -654,6 +702,7 @@ describe('Group Model', () => {
expect(party.getParticipatingQuestMembers()).to.eql([
participatingMember._id,
sleepingParticipatingMember._id,
questLeader._id,
]);
});
@@ -756,11 +805,12 @@ describe('Group Model', () => {
it('removes user from group quest', async () => {
party.quest.members = {
[participatingMember._id]: true,
[sleepingParticipatingMember._id]: true,
[questLeader._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
};
party.memberCount = 4;
party.memberCount = 5;
await party.save();
await party.leave(participatingMember);
@@ -768,6 +818,7 @@ describe('Group Model', () => {
party = await Group.findOne({_id: party._id});
expect(party.quest.members).to.eql({
[questLeader._id]: true,
[sleepingParticipatingMember._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
});
@@ -775,6 +826,7 @@ describe('Group Model', () => {
it('deletes a private party when the last member leaves', async () => {
await party.leave(participatingMember);
await party.leave(sleepingParticipatingMember);
await party.leave(questLeader);
await party.leave(nonParticipatingMember);
await party.leave(undecidedMember);
@@ -846,6 +898,7 @@ describe('Group Model', () => {
party.privacy = 'public';
await party.leave(participatingMember);
await party.leave(sleepingParticipatingMember);
await party.leave(questLeader);
await party.leave(nonParticipatingMember);
await party.leave(undecidedMember);
@@ -967,32 +1020,6 @@ describe('Group Model', () => {
expect(chat.user).to.not.exist;
});
it('cuts down chat to 200 messages', () => {
for (let i = 0; i < 220; i++) {
party.chat.push({ text: 'a message' });
}
expect(party.chat).to.have.a.lengthOf(220);
party.sendChat('message');
expect(party.chat).to.have.a.lengthOf(200);
});
it('cuts down chat to 400 messages when group is subcribed', () => {
party.purchased.plan.customerId = 'test-customer-id';
for (let i = 0; i < 420; i++) {
party.chat.push({ text: 'a message' });
}
expect(party.chat).to.have.a.lengthOf(420);
party.sendChat('message');
expect(party.chat).to.have.a.lengthOf(400);
});
it('updates users about new messages in party', () => {
party.sendChat('message');
@@ -1074,6 +1101,7 @@ describe('Group Model', () => {
party.quest.members = {
[questLeader._id]: true,
[participatingMember._id]: true,
[sleepingParticipatingMember._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
};
@@ -1130,6 +1158,7 @@ describe('Group Model', () => {
let expectedQuestMembers = {};
expectedQuestMembers[questLeader._id] = true;
expectedQuestMembers[participatingMember._id] = true;
expectedQuestMembers[sleepingParticipatingMember._id] = true;
expect(party.quest.members).to.eql(expectedQuestMembers);
});
@@ -1148,12 +1177,18 @@ describe('Group Model', () => {
questLeader = await User.findById(questLeader._id);
participatingMember = await User.findById(participatingMember._id);
sleepingParticipatingMember = await User.findById(sleepingParticipatingMember._id);
expect(participatingMember.party.quest.key).to.eql('whale');
expect(participatingMember.party.quest.progress.down).to.eql(0);
expect(participatingMember.party.quest.progress.collectedItems).to.eql(0);
expect(participatingMember.party.quest.completed).to.eql(null);
expect(sleepingParticipatingMember.party.quest.key).to.eql('whale');
expect(sleepingParticipatingMember.party.quest.progress.down).to.eql(0);
expect(sleepingParticipatingMember.party.quest.progress.collectedItems).to.eql(0);
expect(sleepingParticipatingMember.party.quest.completed).to.eql(null);
expect(questLeader.party.quest.key).to.eql('whale');
expect(questLeader.party.quest.progress.down).to.eql(0);
expect(questLeader.party.quest.progress.collectedItems).to.eql(0);
@@ -1172,9 +1207,11 @@ describe('Group Model', () => {
it('sends email to participating members that quest has started', async () => {
participatingMember.preferences.emailNotifications.questStarted = true;
sleepingParticipatingMember.preferences.emailNotifications.questStarted = true;
questLeader.preferences.emailNotifications.questStarted = true;
await Promise.all([
participatingMember.save(),
sleepingParticipatingMember.save(),
questLeader.save(),
]);
@@ -1187,8 +1224,9 @@ describe('Group Model', () => {
let memberIds = _.map(email.sendTxn.args[0][0], '_id');
let typeOfEmail = email.sendTxn.args[0][1];
expect(memberIds).to.have.a.lengthOf(2);
expect(memberIds).to.have.a.lengthOf(3);
expect(memberIds).to.include(participatingMember._id);
expect(memberIds).to.include(sleepingParticipatingMember._id);
expect(memberIds).to.include(questLeader._id);
expect(typeOfEmail).to.eql('quest-started');
});
@@ -1202,6 +1240,13 @@ describe('Group Model', () => {
questStarted: true,
},
}];
sleepingParticipatingMember.webhooks = [{
type: 'questActivity',
url: 'http://someurl.com',
options: {
questStarted: true,
},
}];
questLeader.webhooks = [{
type: 'questActivity',
url: 'http://someurl.com',
@@ -1210,13 +1255,13 @@ describe('Group Model', () => {
},
}];
await Promise.all([participatingMember.save(), questLeader.save()]);
await Promise.all([participatingMember.save(), sleepingParticipatingMember.save(), questLeader.save()]);
await party.startQuest(nonParticipatingMember);
await sleep(0.5);
expect(questActivityWebhook.send).to.be.calledTwice; // for 2 participating members
expect(questActivityWebhook.send).to.be.calledThrice; // for 3 participating members
let args = questActivityWebhook.send.args[0];
let webhooks = args[0].webhooks;
@@ -1226,6 +1271,8 @@ describe('Group Model', () => {
expect(webhooks).to.have.a.lengthOf(1);
if (webhookOwner === questLeader._id) {
expect(webhooks[0].id).to.eql(questLeader.webhooks[0].id);
} else if (webhookOwner === sleepingParticipatingMember._id) {
expect(webhooks[0].id).to.eql(sleepingParticipatingMember.webhooks[0].id);
} else {
expect(webhooks[0].id).to.eql(participatingMember.webhooks[0].id);
}
@@ -1236,9 +1283,11 @@ describe('Group Model', () => {
it('sends email only to members who have not opted out', async () => {
participatingMember.preferences.emailNotifications.questStarted = false;
sleepingParticipatingMember.preferences.emailNotifications.questStarted = false;
questLeader.preferences.emailNotifications.questStarted = true;
await Promise.all([
participatingMember.save(),
sleepingParticipatingMember.save(),
questLeader.save(),
]);
@@ -1252,14 +1301,17 @@ describe('Group Model', () => {
expect(memberIds).to.have.a.lengthOf(1);
expect(memberIds).to.not.include(participatingMember._id);
expect(memberIds).to.not.include(sleepingParticipatingMember._id);
expect(memberIds).to.include(questLeader._id);
});
it('does not send email to initiating member', async () => {
participatingMember.preferences.emailNotifications.questStarted = true;
sleepingParticipatingMember.preferences.emailNotifications.questStarted = true;
questLeader.preferences.emailNotifications.questStarted = true;
await Promise.all([
participatingMember.save(),
sleepingParticipatingMember.save(),
questLeader.save(),
]);
@@ -1271,8 +1323,9 @@ describe('Group Model', () => {
let memberIds = _.map(email.sendTxn.args[0][0], '_id');
expect(memberIds).to.have.a.lengthOf(1);
expect(memberIds).to.have.a.lengthOf(2);
expect(memberIds).to.not.include(participatingMember._id);
expect(memberIds).to.include(sleepingParticipatingMember._id);
expect(memberIds).to.include(questLeader._id);
});
@@ -1281,7 +1334,7 @@ describe('Group Model', () => {
await party.startQuest(nonParticipatingMember);
let members = [questLeader._id, participatingMember._id];
let members = [questLeader._id, participatingMember._id, sleepingParticipatingMember._id];
expect(User.update).to.be.calledWith(
{ _id: { $in: members } },
@@ -1346,6 +1399,7 @@ describe('Group Model', () => {
party.quest.members = {
[questLeader._id]: true,
[participatingMember._id]: true,
[sleepingParticipatingMember._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
};
@@ -1368,7 +1422,7 @@ describe('Group Model', () => {
await party.finishQuest(quest);
expect(User.update).to.be.calledTwice;
expect(User.update).to.be.calledThrice;
});
it('stops retrying when a successful update has occurred', async () => {
@@ -1378,7 +1432,7 @@ describe('Group Model', () => {
await party.finishQuest(quest);
expect(User.update).to.be.calledThrice;
expect(User.update.callCount).to.equal(4);
});
it('retries failed updates at most five times per user', async () => {
@@ -1386,7 +1440,7 @@ describe('Group Model', () => {
await expect(party.finishQuest(quest)).to.eventually.be.rejected;
expect(User.update.callCount).to.eql(10);
expect(User.update.callCount).to.eql(15); // for 3 users
});
});
@@ -1396,17 +1450,19 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
] = await Promise.all([
User.findById(questLeader._id),
User.findById(participatingMember._id),
User.findById(sleepingParticipatingMember._id),
]);
expect(updatedLeader.achievements.quests[quest.key]).to.eql(1);
expect(updatedParticipatingMember.achievements.quests[quest.key]).to.eql(1);
expect(updatedSleepingParticipatingMember.achievements.quests[quest.key]).to.eql(1);
});
// Disable test, it fails on TravisCI, but only there
xit('gives out super awesome Masterclasser achievement to the deserving', async () => {
it('gives out super awesome Masterclasser achievement to the deserving', async () => {
quest = questScrolls.lostMasterclasser4;
party.quest.key = quest.key;
@@ -1433,17 +1489,19 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
] = await Promise.all([
User.findById(questLeader._id).exec(),
User.findById(participatingMember._id).exec(),
User.findById(sleepingParticipatingMember._id).exec(),
]);
expect(updatedLeader.achievements.lostMasterclasser).to.eql(true);
expect(updatedParticipatingMember.achievements.lostMasterclasser).to.not.eql(true);
expect(updatedSleepingParticipatingMember.achievements.lostMasterclasser).to.not.eql(true);
});
// Disable test, it fails on TravisCI, but only there
xit('gives out super awesome Masterclasser achievement when quests done out of order', async () => {
it('gives out super awesome Masterclasser achievement when quests done out of order', async () => {
quest = questScrolls.lostMasterclasser1;
party.quest.key = quest.key;
@@ -1470,13 +1528,16 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
] = await Promise.all([
User.findById(questLeader._id).exec(),
User.findById(participatingMember._id).exec(),
User.findById(sleepingParticipatingMember._id).exec(),
]);
expect(updatedLeader.achievements.lostMasterclasser).to.eql(true);
expect(updatedParticipatingMember.achievements.lostMasterclasser).to.not.eql(true);
expect(updatedSleepingParticipatingMember.achievements.lostMasterclasser).to.not.eql(true);
});
it('gives xp and gold', async () => {
@@ -1485,15 +1546,19 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
] = await Promise.all([
User.findById(questLeader._id),
User.findById(participatingMember._id),
User.findById(sleepingParticipatingMember._id),
]);
expect(updatedLeader.stats.exp).to.eql(quest.drop.exp);
expect(updatedLeader.stats.gp).to.eql(quest.drop.gp);
expect(updatedParticipatingMember.stats.exp).to.eql(quest.drop.exp);
expect(updatedParticipatingMember.stats.gp).to.eql(quest.drop.gp);
expect(updatedSleepingParticipatingMember.stats.exp).to.eql(quest.drop.exp);
expect(updatedSleepingParticipatingMember.stats.gp).to.eql(quest.drop.gp);
});
context('drops', () => {
@@ -1593,13 +1658,16 @@ describe('Group Model', () => {
sandbox.spy(User, 'update');
await party.finishQuest(quest);
expect(User.update).to.be.calledTwice;
expect(User.update).to.be.calledThrice;
expect(User.update).to.be.calledWithMatch({
_id: questLeader._id,
});
expect(User.update).to.be.calledWithMatch({
_id: participatingMember._id,
});
expect(User.update).to.be.calledWithMatch({
_id: sleepingParticipatingMember._id,
});
});
it('sets user quest object to a clean state', async () => {
@@ -1632,7 +1700,7 @@ describe('Group Model', () => {
},
}];
await Promise.all([participatingMember.save(), questLeader.save()]);
await Promise.all([participatingMember.save(), sleepingParticipatingMember.save(), questLeader.save()]);
await party.finishQuest(quest);

View File

@@ -63,45 +63,48 @@ describe('GET /challenges/:challengeId', () => {
context('private guild', () => {
let groupLeader;
let challengeLeader;
let group;
let challenge;
let members;
let user;
let nonMember;
let otherMember;
beforeEach(async () => {
user = await generateUser();
nonMember = await generateUser();
let populatedGroup = await createAndPopulateGroup({
groupDetails: {type: 'guild', privacy: 'private'},
members: 1,
members: 2,
});
groupLeader = populatedGroup.groupLeader;
group = populatedGroup.group;
members = populatedGroup.members;
challenge = await generateChallenge(groupLeader, group);
await members[0].post(`/challenges/${challenge._id}/join`);
await groupLeader.post(`/challenges/${challenge._id}/join`);
challengeLeader = members[0];
otherMember = members[1];
challenge = await generateChallenge(challengeLeader, group);
});
it('fails if user doesn\'t have access to the challenge', async () => {
await expect(user.get(`/challenges/${challenge._id}`)).to.eventually.be.rejected.and.eql({
it('fails if user isn\'t in the guild and isn\'t challenge leader', async () => {
await expect(nonMember.get(`/challenges/${challenge._id}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('challengeNotFound'),
});
});
it('should return challenge data', async () => {
let chal = await members[0].get(`/challenges/${challenge._id}`);
it('returns challenge data for any user in the guild', async () => {
let chal = await otherMember.get(`/challenges/${challenge._id}`);
expect(chal.name).to.equal(challenge.name);
expect(chal._id).to.equal(challenge._id);
expect(chal.leader).to.eql({
_id: groupLeader._id,
id: groupLeader._id,
profile: {name: groupLeader.profile.name},
_id: challengeLeader._id,
id: challengeLeader._id,
profile: {name: challengeLeader.profile.name},
});
expect(chal.group).to.eql({
_id: group._id,
@@ -114,53 +117,72 @@ describe('GET /challenges/:challengeId', () => {
leader: groupLeader.id,
});
});
it('returns challenge data if challenge leader isn\'t in the guild or challenge', async () => {
await challengeLeader.post(`/groups/${group._id}/leave`);
await challengeLeader.sync();
expect(challengeLeader.guilds).to.be.empty; // check that leaving worked
let chal = await challengeLeader.get(`/challenges/${challenge._id}`);
expect(chal.name).to.equal(challenge.name);
expect(chal._id).to.equal(challenge._id);
expect(chal.leader).to.eql({
_id: challengeLeader._id,
id: challengeLeader._id,
profile: {name: challengeLeader.profile.name},
});
});
});
context('party', () => {
let groupLeader;
let challengeLeader;
let group;
let challenge;
let members;
let user;
let nonMember;
let otherMember;
beforeEach(async () => {
user = await generateUser();
nonMember = await generateUser();
let populatedGroup = await createAndPopulateGroup({
groupDetails: {type: 'party'},
members: 1,
groupDetails: {type: 'party', privacy: 'private'},
members: 2,
});
groupLeader = populatedGroup.groupLeader;
group = populatedGroup.group;
members = populatedGroup.members;
challenge = await generateChallenge(groupLeader, group);
await members[0].post(`/challenges/${challenge._id}/join`);
await groupLeader.post(`/challenges/${challenge._id}/join`);
challengeLeader = members[0];
otherMember = members[1];
challenge = await generateChallenge(challengeLeader, group);
});
it('fails if user doesn\'t have access to the challenge', async () => {
await expect(user.get(`/challenges/${challenge._id}`)).to.eventually.be.rejected.and.eql({
it('fails if user isn\'t in the party and isn\'t challenge leader', async () => {
await expect(nonMember.get(`/challenges/${challenge._id}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('challengeNotFound'),
});
});
it('should return challenge data', async () => {
let chal = await members[0].get(`/challenges/${challenge._id}`);
it('returns challenge data for any user in the party', async () => {
let chal = await otherMember.get(`/challenges/${challenge._id}`);
expect(chal.name).to.equal(challenge.name);
expect(chal._id).to.equal(challenge._id);
expect(chal.leader).to.eql({
_id: groupLeader._id,
id: groupLeader.id,
profile: {name: groupLeader.profile.name},
_id: challengeLeader._id,
id: challengeLeader._id,
profile: {name: challengeLeader.profile.name},
});
expect(chal.group).to.eql({
_id: group._id,
id: group.id,
id: group._id,
categories: [],
name: group.name,
summary: group.name,
@@ -169,5 +191,21 @@ describe('GET /challenges/:challengeId', () => {
leader: groupLeader.id,
});
});
it('returns challenge data if challenge leader isn\'t in the party or challenge', async () => {
await challengeLeader.post('/groups/party/leave');
await challengeLeader.sync();
expect(challengeLeader.party._id).to.be.undefined; // check that leaving worked
let chal = await challengeLeader.get(`/challenges/${challenge._id}`);
expect(chal.name).to.equal(challenge.name);
expect(chal._id).to.equal(challenge._id);
expect(chal.leader).to.eql({
_id: challengeLeader._id,
id: challengeLeader._id,
profile: {name: challengeLeader.profile.name},
});
});
});
});

View File

@@ -1,6 +1,7 @@
import {
generateUser,
generateGroup,
createAndPopulateGroup,
generateChallenge,
translate as t,
} from '../../../../helpers/api-integration/v3';
@@ -10,7 +11,7 @@ describe('GET /challenges/:challengeId/members', () => {
let user;
beforeEach(async () => {
user = await generateUser();
user = await generateUser({ balance: 1 });
});
it('validates optional req.query.lastId to be an UUID', async () => {
@@ -21,7 +22,7 @@ describe('GET /challenges/:challengeId/members', () => {
});
});
it('fails if challenge doesn\'t exists', async () => {
it('fails if challenge doesn\'t exist', async () => {
await expect(user.get(`/challenges/${generateUUID()}/members`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
@@ -29,8 +30,8 @@ describe('GET /challenges/:challengeId/members', () => {
});
});
it('fails if user doesn\'t have access to the challenge', async () => {
let group = await generateGroup(user);
it('fails if user isn\'t in the private group and isn\'t challenge leader', async () => {
let group = await generateGroup(user, {type: 'party', privacy: 'private'});
let challenge = await generateChallenge(user, group);
let anotherUser = await generateUser();
@@ -41,6 +42,27 @@ describe('GET /challenges/:challengeId/members', () => {
});
});
it('works if user isn\'t in the private group but is challenge leader', async () => {
let populatedGroup = await createAndPopulateGroup({
groupDetails: {type: 'party', privacy: 'private'},
members: 1,
});
let groupLeader = populatedGroup.groupLeader;
let challengeLeader = populatedGroup.members[0];
let challenge = await generateChallenge(challengeLeader, populatedGroup.group);
await groupLeader.post(`/challenges/${challenge._id}/join`);
await challengeLeader.post('/groups/party/leave');
await challengeLeader.sync();
expect(challengeLeader.party._id).to.be.undefined; // check that leaving worked
let res = await challengeLeader.get(`/challenges/${challenge._id}/members`);
expect(res[0]).to.eql({
_id: groupLeader._id,
id: groupLeader._id,
profile: {name: groupLeader.profile.name},
});
});
it('works with challenges belonging to public guild', async () => {
let leader = await generateUser({balance: 4});
let group = await generateGroup(leader, {type: 'guild', privacy: 'public', name: generateUUID()});

View File

@@ -94,16 +94,6 @@ describe('POST /challenges', () => {
});
});
it('returns an error when non-leader member creates a challenge in leaderOnly group', async () => {
await expect(groupMember.post('/challenges', {
group: group._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlyGroupLeaderChal'),
});
});
it('allows non-leader member to create a challenge', async () => {
let populatedGroup = await createAndPopulateGroup({
members: 1,
@@ -304,14 +294,14 @@ describe('POST /challenges', () => {
expect(groupLeader.challenges.length).to.equal(0);
});
it('awards achievement if this is creator\'s first challenge', async () => {
it('does not award joinedChallenge achievement for creating a challenge', async () => {
await groupLeader.post('/challenges', {
group: group._id,
name: 'Test Challenge',
shortName: 'TC Label',
});
groupLeader = await groupLeader.sync();
expect(groupLeader.achievements.joinedChallenge).to.be.true;
expect(groupLeader.achievements.joinedChallenge).to.not.be.true;
});
it('sets summary to challenges name when not supplied', async () => {

View File

@@ -46,7 +46,7 @@ describe('POST /challenges/:challengeId/join', () => {
await groupLeader.post(`/challenges/${challenge._id}/join`);
});
it('returns an error when user doesn\'t have permissions to access the challenge', async () => {
it('returns an error when user isn\'t in the private group and isn\'t challenge leader', async () => {
let unauthorizedUser = await generateUser();
await expect(unauthorizedUser.post(`/challenges/${challenge._id}/join`)).to.eventually.be.rejected.and.eql({
@@ -56,6 +56,16 @@ describe('POST /challenges/:challengeId/join', () => {
});
});
it('succeeds when user isn\'t in the private group but is challenge leader', async () => {
await groupLeader.post(`/challenges/${challenge._id}/leave`);
await groupLeader.post(`/groups/${group._id}/leave`);
await groupLeader.sync();
expect(groupLeader.guilds).to.be.empty; // check that leaving worked
let res = await groupLeader.post(`/challenges/${challenge._id}/join`);
expect(res.name).to.equal(challenge.name);
});
it('returns challenge data', async () => {
let res = await authorizedUser.post(`/challenges/${challenge._id}/join`);

View File

@@ -3,15 +3,23 @@ import {
translate as t,
} from '../../../../helpers/api-integration/v3';
import { find } from 'lodash';
import moment from 'moment';
import nconf from 'nconf';
import { IncomingWebhook } from '@slack/client';
const BASE_URL = nconf.get('BASE_URL');
describe('POST /chat/:chatId/flag', () => {
let user, admin, anotherUser, group;
let user, admin, anotherUser, newUser, group;
const TEST_MESSAGE = 'Test Message';
const USER_AGE_FOR_FLAGGING = 3;
beforeEach(async () => {
user = await generateUser({balance: 1});
user = await generateUser({balance: 1, 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate()});
admin = await generateUser({balance: 1, 'contributor.admin': true});
anotherUser = await generateUser();
anotherUser = await generateUser({'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate()});
newUser = await generateUser({'auth.timestamps.created': moment().subtract(1, 'days').toDate()});
sandbox.stub(IncomingWebhook.prototype, 'send');
group = await user.post('/groups', {
name: 'Test Guild',
@@ -20,6 +28,10 @@ describe('POST /chat/:chatId/flag', () => {
});
});
afterEach(() => {
sandbox.restore();
});
it('Returns an error when chat message is not found', async () => {
await expect(user.post(`/groups/${group._id}/chat/incorrectMessage/flag`))
.to.eventually.be.rejected.and.eql({
@@ -34,7 +46,7 @@ describe('POST /chat/:chatId/flag', () => {
await expect(user.post(`/groups/${group._id}/chat/${message.message.id}/flag`)).to.eventually.be.ok;
});
it('Flags a chat', async () => {
it('Flags a chat and sends normal message to moderator Slack when user is not new', async () => {
let { message } = await anotherUser.post(`/groups/${group._id}/chat`, {message: TEST_MESSAGE});
let flagResult = await user.post(`/groups/${group._id}/chat/${message.id}/flag`);
@@ -45,6 +57,62 @@ describe('POST /chat/:chatId/flag', () => {
let messageToCheck = find(groupWithFlags.chat, {id: message.id});
expect(messageToCheck.flags[user._id]).to.equal(true);
// Slack message to mods
const timestamp = `${moment(message.timestamp).utc().format('YYYY-MM-DD HH:mm')} UTC`;
/* eslint-disable camelcase */
expect(IncomingWebhook.prototype.send).to.be.calledWith({
text: `${user.profile.name} (${user.id}; language: en) flagged a message`,
attachments: [{
fallback: 'Flag Message',
color: 'danger',
author_name: `${anotherUser.profile.name} - ${anotherUser.auth.local.email} - ${anotherUser._id}\n${timestamp}`,
title: 'Flag in Test Guild',
title_link: `${BASE_URL}/groups/guild/${group._id}`,
text: TEST_MESSAGE,
footer: `<https://habitrpg.github.io/flag-o-rama/?groupId=${group._id}&chatId=${message.id}|Flag this message.>`,
mrkdwn_in: [
'text',
],
}],
});
/* eslint-ensable camelcase */
});
it('Does not increment message flag count and sends different message to moderator Slack when user is new', async () => {
let automatedComment = `The post's flag count has not been increased because the flagger's account is less than ${USER_AGE_FOR_FLAGGING} days old.`;
let { message } = await newUser.post(`/groups/${group._id}/chat`, {message: TEST_MESSAGE});
let flagResult = await newUser.post(`/groups/${group._id}/chat/${message.id}/flag`);
expect(flagResult.flags[newUser._id]).to.equal(true);
expect(flagResult.flagCount).to.equal(0);
let groupWithFlags = await admin.get(`/groups/${group._id}`);
let messageToCheck = find(groupWithFlags.chat, {id: message.id});
expect(messageToCheck.flags[newUser._id]).to.equal(true);
// Slack message to mods
const timestamp = `${moment(message.timestamp).utc().format('YYYY-MM-DD HH:mm')} UTC`;
/* eslint-disable camelcase */
expect(IncomingWebhook.prototype.send).to.be.calledWith({
text: `${newUser.profile.name} (${newUser.id}; language: en) flagged a message`,
attachments: [{
fallback: 'Flag Message',
color: 'danger',
author_name: `${newUser.profile.name} - ${newUser.auth.local.email} - ${newUser._id}\n${timestamp}`,
title: 'Flag in Test Guild',
title_link: `${BASE_URL}/groups/guild/${group._id}`,
text: TEST_MESSAGE,
footer: `<https://habitrpg.github.io/flag-o-rama/?groupId=${group._id}&chatId=${message.id}|Flag this message.> ${automatedComment}`,
mrkdwn_in: [
'text',
],
}],
});
/* eslint-ensable camelcase */
});
it('Flags a chat when the author\'s account was deleted', async () => {
@@ -117,7 +185,7 @@ describe('POST /chat/:chatId/flag', () => {
});
});
it('Returns an error when user tries to flag a message that is already flagged', async () => {
it('Returns an error when user tries to flag a message that they already flagged', async () => {
let { message } = await anotherUser.post(`/groups/${group._id}/chat`, {message: TEST_MESSAGE});
await user.post(`/groups/${group._id}/chat/${message.id}/flag`);

View File

@@ -1,3 +1,5 @@
import { IncomingWebhook } from '@slack/client';
import nconf from 'nconf';
import {
createAndPopulateGroup,
generateUser,
@@ -15,8 +17,6 @@ import { getMatchesByWordArray } from '../../../../../website/server/libs/string
import bannedWords from '../../../../../website/server/libs/bannedWords';
import guildsAllowingBannedWords from '../../../../../website/server/libs/guildsAllowingBannedWords';
import * as email from '../../../../../website/server/libs/email';
import { IncomingWebhook } from '@slack/client';
import nconf from 'nconf';
const BASE_URL = nconf.get('BASE_URL');
@@ -80,14 +80,16 @@ describe('POST /chat', () => {
});
});
describe('mute user', () => {
it('returns an error when chat privileges are revoked when sending a message to a public guild', async () => {
let userWithChatRevoked = await member.update({'flags.chatRevoked': true});
const userWithChatRevoked = await member.update({'flags.chatRevoked': true});
await expect(userWithChatRevoked.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('chatPrivilegesRevoked'),
});
});
});
context('banned word', () => {
it('returns an error when chat message contains a banned word in tavern', async () => {
@@ -259,7 +261,6 @@ describe('POST /chat', () => {
title: 'Slur in Test Guild',
title_link: `${BASE_URL}/groups/guild/${groupWithChat.id}`,
text: testSlurMessage,
// footer: sandbox.match(/<.*?groupId=group-id&chatId=chat-id\|Flag this message>/),
mrkdwn_in: [
'text',
],
@@ -274,6 +275,7 @@ describe('POST /chat', () => {
message: t('chatPrivilegesRevoked'),
});
// @TODO: The next test should not depend on this. We should reset the user test in a beforeEach
// Restore chat privileges to continue testing
user.flags.chatRevoked = false;
await user.update({'flags.chatRevoked': false});
@@ -312,7 +314,6 @@ describe('POST /chat', () => {
title: 'Slur in Party - (private party)',
title_link: undefined,
text: testSlurMessage,
// footer: sandbox.match(/<.*?groupId=group-id&chatId=chat-id\|Flag this message>/),
mrkdwn_in: [
'text',
],
@@ -388,6 +389,23 @@ describe('POST /chat', () => {
expect(groupMessages[0].id).to.exist;
});
it('creates a chat with a max length of 3000 chars', async () => {
const veryLongMessage = `
123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789.
THIS PART WON'T BE IN THE MESSAGE (over 3000)
`;
const newMessage = await user.post(`/groups/${groupWithChat._id}/chat`, { message: veryLongMessage});
const groupMessages = await user.get(`/groups/${groupWithChat._id}/chat`);
expect(newMessage.message.id).to.exist;
expect(groupMessages[0].id).to.exist;
expect(newMessage.message.text.length).to.eql(3000);
expect(newMessage.message.text).to.not.contain('MESSAGE');
expect(groupMessages[0].text.length).to.eql(3000);
});
it('creates a chat with user styles', async () => {
const mount = 'test-mount';
const pet = 'test-pet';

View File

@@ -4,23 +4,24 @@ import {
translate as t,
} from '../../../../helpers/api-integration/v3';
import config from '../../../../../config.json';
import moment from 'moment';
import { v4 as generateUUID } from 'uuid';
describe('POST /groups/:id/chat/:id/clearflags', () => {
const USER_AGE_FOR_FLAGGING = 3;
let groupWithChat, message, author, nonAdmin, admin;
before(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'public',
},
members: 1,
});
groupWithChat = group;
author = groupLeader;
nonAdmin = members[0];
nonAdmin = await generateUser({'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate()});
admin = await generateUser({'contributor.admin': true});
message = await author.post(`/groups/${groupWithChat._id}/chat`, { message: 'Some message' });
@@ -69,9 +70,14 @@ describe('POST /groups/:id/chat/:id/clearflags', () => {
privateMessage = privateMessage.message;
await admin.post(`/groups/${group._id}/chat/${privateMessage.id}/flag`);
// first test that the flag was actually successful
let messages = await members[0].get(`/groups/${group._id}/chat`);
expect(messages[0].flagCount).to.eql(5);
await admin.post(`/groups/${group._id}/chat/${privateMessage.id}/clearflags`);
let messages = await members[0].get(`/groups/${group._id}/chat`);
messages = await members[0].get(`/groups/${group._id}/chat`);
expect(messages[0].flagCount).to.eql(0);
});

View File

@@ -23,6 +23,17 @@ describe('GET /export/userdata.xml', () => {
]);
// add pinnedItem
await user.get('/user/toggle-pinned-item/marketGear/gear.flat.shield_rogue_5');
// add a private message
let receiver = await generateUser();
user.post('/members/send-private-message', {
message: 'Your first message, hi!',
toUserId: receiver._id,
});
let response = await user.get('/export/userdata.xml');
let {user: res} = await parseStringAsync(response, {explicitArray: false});

View File

@@ -77,7 +77,7 @@ describe('GET /groups/:groupId/members', () => {
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
expect(Object.keys(memberRes.preferences).sort()).to.eql([
'size', 'hair', 'skin', 'shirt',
'chair', 'costume', 'sleep', 'background', 'tasks',
'chair', 'costume', 'sleep', 'background', 'tasks', 'disableClasses',
].sort());
expect(memberRes.stats.maxMP).to.exist;
@@ -98,7 +98,7 @@ describe('GET /groups/:groupId/members', () => {
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
expect(Object.keys(memberRes.preferences).sort()).to.eql([
'size', 'hair', 'skin', 'shirt',
'chair', 'costume', 'sleep', 'background', 'tasks',
'chair', 'costume', 'sleep', 'background', 'tasks', 'disableClasses',
].sort());
expect(memberRes.stats.maxMP).to.exist;

View File

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

View File

@@ -1,6 +1,6 @@
import {
generateUser,
} from '../../../helpers/api-integration/v4';
} from '../../../../helpers/api-integration/v3';
describe('GET /inbox/messages', () => {
let user;
@@ -22,17 +22,27 @@ describe('GET /inbox/messages', () => {
message: 'third',
});
// message to yourself
await user.post('/members/send-private-message', {
toUserId: user.id,
message: 'fourth',
});
await user.sync();
});
it('returns the user inbox messages as an array of ordered messages (from most to least recent)', async () => {
const messages = await user.get('/inbox/messages');
expect(messages.length).to.equal(3);
expect(messages.length).to.equal(Object.keys(user.inbox.messages).length);
expect(messages.length).to.equal(4);
expect(messages[0].text).to.equal('third');
expect(messages[1].text).to.equal('second');
expect(messages[2].text).to.equal('first');
// message to yourself
expect(messages[0].text).to.equal('fourth');
expect(messages[0].sent).to.equal(false);
expect(messages[0].uuid).to.equal(user._id);
expect(messages[1].text).to.equal('third');
expect(messages[2].text).to.equal('second');
expect(messages[3].text).to.equal('first');
});
});

View File

@@ -37,7 +37,7 @@ describe('GET /members/:memberId', () => {
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
expect(Object.keys(memberRes.preferences).sort()).to.eql([
'size', 'hair', 'skin', 'shirt',
'chair', 'costume', 'sleep', 'background', 'tasks',
'chair', 'costume', 'sleep', 'background', 'tasks', 'disableClasses',
].sort());
expect(memberRes.stats.maxMP).to.exist;

View File

@@ -100,7 +100,7 @@ describe('POST /members/send-private-message', () => {
let receiver = await generateUser();
// const initialNotifications = receiver.notifications.length;
await userToSendMessage.post('/members/send-private-message', {
const response = await userToSendMessage.post('/members/send-private-message', {
message: messageToSend,
toUserId: receiver._id,
});
@@ -116,6 +116,9 @@ describe('POST /members/send-private-message', () => {
return message.uuid === receiver._id && message.text === messageToSend;
});
expect(response.message.text).to.deep.equal(sendersMessageInSendersInbox.text);
expect(response.message.uuid).to.deep.equal(sendersMessageInSendersInbox.uuid);
// @TODO waiting for mobile support
// expect(updatedReceiver.notifications.length).to.equal(initialNotifications + 1);
// const notification = updatedReceiver.notifications[updatedReceiver.notifications.length - 1];

View File

@@ -0,0 +1,39 @@
import {
createAndPopulateGroup,
} from '../../../../helpers/api-integration/v3';
describe('Prevent multiple notifications', () => {
let partyLeader, partyMembers, party;
before(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
type: 'party',
privacy: 'private',
},
members: 4,
});
party = group;
partyLeader = groupLeader;
partyMembers = members;
});
it('does not add the same notification twice', async () => {
const multipleChatMessages = [];
for (let i = 0; i < 4; i++) {
for (let memberIndex = 0; memberIndex < partyMembers.length; memberIndex++) {
multipleChatMessages.push(
partyMembers[memberIndex].post(`/groups/${party._id}/chat`, { message: `Message ${i}_${memberIndex}`}),
);
}
}
await Promise.all(multipleChatMessages);
const userWithNotification = await partyLeader.get('/user');
expect(userWithNotification.notifications.length).to.be.eq(1);
});
});

View File

@@ -6,7 +6,7 @@ import {
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
describe('payments - stripe - #subscribeCancel', () => {
let endpoint = '/stripe/subscribe/cancel?redirect=none';
let endpoint = '/stripe/subscribe/cancel?noRedirect=true';
let user, group, stripeCancelSubscriptionStub;
beforeEach(async () => {

View File

@@ -4,7 +4,7 @@ import {
generateUser,
sleep,
} from '../../../../helpers/api-integration/v3';
import { model as Chat } from '../../../../../website/server/models/chat';
import { chatModel as Chat } from '../../../../../website/server/models/message';
describe('POST /groups/:groupId/quests/accept', () => {
const PET_QUEST = 'whale';

View File

@@ -4,7 +4,7 @@ import {
generateUser,
sleep,
} from '../../../../helpers/api-integration/v3';
import { model as Chat } from '../../../../../website/server/models/chat';
import { chatModel as Chat } from '../../../../../website/server/models/message';
describe('POST /groups/:groupId/quests/force-start', () => {
const PET_QUEST = 'whale';

View File

@@ -5,7 +5,7 @@ import {
} from '../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
import { quests as questScrolls } from '../../../../../website/common/script/content';
import { model as Chat } from '../../../../../website/server/models/chat';
import { chatModel as Chat } from '../../../../../website/server/models/message';
import apiError from '../../../../../website/server/libs/apiError';
describe('POST /groups/:groupId/quests/invite/:questKey', () => {

View File

@@ -5,7 +5,7 @@ import {
sleep,
} from '../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
import { model as Chat } from '../../../../../website/server/models/chat';
import { chatModel as Chat } from '../../../../../website/server/models/message';
describe('POST /groups/:groupId/quests/reject', () => {
let questingGroup;

View File

@@ -140,4 +140,89 @@ describe('POST /tasks/:id/approve/:userId', () => {
message: t('canOnlyApproveTaskOnce'),
});
});
it('completes master task when single-completion task is approved', async () => {
let sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: true,
sharedCompletion: 'singleCompletion',
});
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
let groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
let masterTask = find(groupTasks, (groupTask) => {
return groupTask._id === sharedCompletionTask._id;
});
expect(masterTask.completed).to.equal(true);
});
it('deletes other assigned user tasks when single-completion task is approved', async () => {
let sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: true,
sharedCompletion: 'singleCompletion',
});
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
let member2Tasks = await member2.get('/tasks/user');
let syncedTask2 = find(member2Tasks, (memberTask) => {
return memberTask.group.taskId === sharedCompletionTask._id;
});
expect(syncedTask2).to.equal(undefined);
});
it('does not complete master task when not all user tasks are approved if all assigned must complete', async () => {
let sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: true,
sharedCompletion: 'allAssignedCompletion',
});
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
let groupTasks = await user.get(`/tasks/group/${guild._id}`);
let masterTask = find(groupTasks, (groupTask) => {
return groupTask._id === sharedCompletionTask._id;
});
expect(masterTask.completed).to.equal(false);
});
it('completes master task when all user tasks are approved if all assigned must complete', async () => {
let sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: true,
sharedCompletion: 'allAssignedCompletion',
});
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member2._id}`);
let groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
let masterTask = find(groupTasks, (groupTask) => {
return groupTask._id === sharedCompletionTask._id;
});
expect(masterTask.completed).to.equal(true);
});
});

View File

@@ -125,7 +125,7 @@ describe('POST /tasks/:id/score/:direction', () => {
});
});
it('allows a user to score an apporoved task', async () => {
it('allows a user to score an approved task', async () => {
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
@@ -137,4 +137,112 @@ describe('POST /tasks/:id/score/:direction', () => {
expect(updatedTask.completed).to.equal(true);
expect(updatedTask.dateCompleted).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
});
it('completes master task when single-completion task is completed', async () => {
let sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: false,
sharedCompletion: 'singleCompletion',
});
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, (memberTask) => {
return memberTask.group.taskId === sharedCompletionTask._id;
});
await member.post(`/tasks/${syncedTask._id}/score/up`);
let groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
let masterTask = find(groupTasks, (groupTask) => {
return groupTask._id === sharedCompletionTask._id;
});
expect(masterTask.completed).to.equal(true);
});
it('deletes other assigned user tasks when single-completion task is completed', async () => {
let sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: false,
sharedCompletion: 'singleCompletion',
});
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, (memberTask) => {
return memberTask.group.taskId === sharedCompletionTask._id;
});
await member.post(`/tasks/${syncedTask._id}/score/up`);
let member2Tasks = await member2.get('/tasks/user');
let syncedTask2 = find(member2Tasks, (memberTask) => {
return memberTask.group.taskId === sharedCompletionTask._id;
});
expect(syncedTask2).to.equal(undefined);
});
it('does not complete master task when not all user tasks are completed if all assigned must complete', async () => {
let sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: false,
sharedCompletion: 'allAssignedCompletion',
});
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, (memberTask) => {
return memberTask.group.taskId === sharedCompletionTask._id;
});
await member.post(`/tasks/${syncedTask._id}/score/up`);
let groupTasks = await user.get(`/tasks/group/${guild._id}`);
let masterTask = find(groupTasks, (groupTask) => {
return groupTask._id === sharedCompletionTask._id;
});
expect(masterTask.completed).to.equal(false);
});
it('completes master task when all user tasks are completed if all assigned must complete', async () => {
let sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: false,
sharedCompletion: 'allAssignedCompletion',
});
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
let memberTasks = await member.get('/tasks/user');
let member2Tasks = await member2.get('/tasks/user');
let syncedTask = find(memberTasks, (memberTask) => {
return memberTask.group.taskId === sharedCompletionTask._id;
});
let syncedTask2 = find(member2Tasks, (memberTask) => {
return memberTask.group.taskId === sharedCompletionTask._id;
});
await member.post(`/tasks/${syncedTask._id}/score/up`);
await member2.post(`/tasks/${syncedTask2._id}/score/up`);
let groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
let masterTask = find(groupTasks, (groupTask) => {
return groupTask._id === sharedCompletionTask._id;
});
expect(masterTask.completed).to.equal(true);
});
});

View File

@@ -3,25 +3,41 @@ import {
} from '../../../../helpers/api-integration/v3';
describe('DELETE user message', () => {
let user;
let user, messagesId, otherUser;
beforeEach(async () => {
user = await generateUser({ inbox: { messages: { first: 'message', second: 'message' } } });
expect(user.inbox.messages.first).to.eql('message');
expect(user.inbox.messages.second).to.eql('message');
before(async () => {
[user, otherUser] = await Promise.all([generateUser(), generateUser()]);
await user.post('/members/send-private-message', {
toUserId: otherUser.id,
message: 'first',
});
await user.post('/members/send-private-message', {
toUserId: otherUser.id,
message: 'second',
});
let userRes = await user.get('/user');
messagesId = Object.keys(userRes.inbox.messages);
expect(messagesId.length).to.eql(2);
expect(userRes.inbox.messages[messagesId[0]].text).to.eql('first');
expect(userRes.inbox.messages[messagesId[1]].text).to.eql('second');
});
it('one message', async () => {
let result = await user.del('/user/messages/first');
await user.sync();
expect(result).to.eql({ second: 'message' });
expect(user.inbox.messages).to.eql({ second: 'message' });
let result = await user.del(`/user/messages/${messagesId[0]}`);
messagesId = Object.keys(result);
expect(messagesId.length).to.eql(1);
let userRes = await user.get('/user');
expect(Object.keys(userRes.inbox.messages).length).to.eql(1);
expect(userRes.inbox.messages[messagesId[0]].text).to.eql('second');
});
it('clear all', async () => {
let result = await user.del('/user/messages');
await user.sync();
expect(user.inbox.messages).to.eql({});
let userRes = await user.get('/user');
expect(userRes.inbox.messages).to.eql({});
expect(result).to.eql({});
});
});

View File

@@ -58,6 +58,21 @@ describe('POST /user/class/cast/:spellId', () => {
});
});
it('returns an error if use Healing Light spell with full health', async () => {
await user.update({
'stats.class': 'healer',
'stats.lvl': 11,
'stats.hp': 50,
'stats.mp': 200,
});
await expect(user.post('/user/class/cast/heal'))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageHealthAlreadyMax'),
});
});
it('returns an error if spell.lvl > user.level', async () => {
await user.update({'stats.mp': 200, 'stats.class': 'wizard'});
await expect(user.post('/user/class/cast/earth'))

View File

@@ -50,11 +50,24 @@ describe('POST /user/push-devices', () => {
});
it('adds a push device to the user', async () => {
let response = await user.post('/user/push-devices', {type, regId});
const response = await user.post('/user/push-devices', {type, regId});
await user.sync();
expect(response.message).to.equal(t('pushDeviceAdded'));
expect(response.data[0].type).to.equal(type);
expect(response.data[0].regId).to.equal(regId);
expect(user.pushDevices[0].type).to.equal(type);
expect(user.pushDevices[0].regId).to.equal(regId);
});
it('removes a push device to the user', async () => {
await user.post('/user/push-devices', {type, regId});
const response = await user.del(`/user/push-devices/${regId}`);
await user.sync();
expect(response.message).to.equal(t('pushDeviceRemoved'));
expect(response.data[0]).to.not.exist;
expect(user.pushDevices[0]).to.not.exist;
});
});

View File

@@ -8,7 +8,11 @@ describe('POST /user/allocate', () => {
let user;
beforeEach(async () => {
user = await generateUser();
user = await generateUser({
'stats.lvl': 10,
'flags.classSelected': true,
'preferences.disableClasses': false,
});
});
// More tests in common code unit tests
@@ -31,6 +35,16 @@ describe('POST /user/allocate', () => {
});
});
it('returns an error if the user hasn\'t selected class', async () => {
await user.update({'flags.classSelected': false});
await expect(user.post('/user/allocate'))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('classNotSelected'),
});
});
it('allocates attribute points', async () => {
await user.update({'stats.points': 1});
let res = await user.post('/user/allocate?stat=con');

View File

@@ -13,7 +13,11 @@ describe('POST /user/allocate-bulk', () => {
};
beforeEach(async () => {
user = await generateUser();
user = await generateUser({
'stats.lvl': 10,
'flags.classSelected': true,
'preferences.disableClasses': false,
});
});
// More tests in common code unit tests
@@ -27,6 +31,16 @@ describe('POST /user/allocate-bulk', () => {
});
});
it('returns an error if user has not selected class', async () => {
await user.update({'flags.classSelected': false});
await expect(user.post('/user/allocate-bulk', statsUpdate))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('classNotSelected'),
});
});
it('allocates attribute points', async () => {
await user.update({'stats.points': 3});

View File

@@ -0,0 +1,62 @@
import {
generateUser,
translate as t,
resetHabiticaDB,
} from '../../../helpers/api-integration/v4';
describe('POST /coupons/enter/:code', () => {
let user;
let sudoUser;
before(async () => {
await resetHabiticaDB();
});
beforeEach(async () => {
user = await generateUser();
sudoUser = await generateUser({
'contributor.sudo': true,
});
});
it('returns an error if code is missing', async () => {
await expect(user.post('/coupons/enter')).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: 'Not found.',
});
});
it('returns an error if code is invalid', async () => {
await expect(user.post('/coupons/enter/notValid')).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidCoupon'),
});
});
it('returns an error if coupon has been used', async () => {
let [coupon] = await sudoUser.post('/coupons/generate/wondercon?count=1');
await user.post(`/coupons/enter/${coupon._id}`); // use coupon
await expect(user.post(`/coupons/enter/${coupon._id}`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('couponUsed'),
});
});
it('should apply the coupon to the user', async () => {
let [coupon] = await sudoUser.post('/coupons/generate/wondercon?count=1');
let userRes = await user.post(`/coupons/enter/${coupon._id}`);
expect(userRes._id).to.equal(user._id);
expect(userRes.items.gear.owned.eyewear_special_wondercon_red).to.be.true;
expect(userRes.items.gear.owned.eyewear_special_wondercon_black).to.be.true;
expect(userRes.items.gear.owned.back_special_wondercon_black).to.be.true;
expect(userRes.items.gear.owned.back_special_wondercon_red).to.be.true;
expect(userRes.items.gear.owned.body_special_wondercon_red).to.be.true;
expect(userRes.items.gear.owned.body_special_wondercon_black).to.be.true;
expect(userRes.items.gear.owned.body_special_wondercon_gold).to.be.true;
expect(userRes.extra).to.eql({signupEvent: 'wondercon'});
});
});

View File

@@ -0,0 +1,30 @@
import {
generateUser,
} from '../../../helpers/api-integration/v4';
describe('DELETE /inbox/clear', () => {
it('removes all inbox messages for the user', async () => {
const [user, otherUser] = await Promise.all([generateUser(), generateUser()]);
await otherUser.post('/members/send-private-message', {
toUserId: user.id,
message: 'first',
});
await user.post('/members/send-private-message', {
toUserId: otherUser.id,
message: 'second',
});
await otherUser.post('/members/send-private-message', {
toUserId: user.id,
message: 'third',
});
let messages = await user.get('/inbox/messages');
expect(messages.length).to.equal(3);
await user.del('/inbox/clear/');
messages = await user.get('/inbox/messages');
expect(messages.length).to.equal(0);
});
});

View File

@@ -0,0 +1,62 @@
import {
generateUser,
translate as t,
} from '../../../helpers/api-integration/v4';
import { v4 as generateUUID } from 'uuid';
describe('DELETE /inbox/messages/:messageId', () => {
let user;
let otherUser;
before(async () => {
[user, otherUser] = await Promise.all([generateUser(), generateUser()]);
await otherUser.post('/members/send-private-message', {
toUserId: user.id,
message: 'first',
});
await user.post('/members/send-private-message', {
toUserId: otherUser.id,
message: 'second',
});
await otherUser.post('/members/send-private-message', {
toUserId: user.id,
message: 'third',
});
});
it('returns an error if the messageId parameter is not an UUID', async () => {
await expect(user.del('/inbox/messages/123'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Invalid request parameters.',
});
});
it('returns an error if the message does not exist', async () => {
await expect(user.del(`/inbox/messages/${generateUUID()}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('messageGroupChatNotFound'),
});
});
it('deletes one message', async () => {
const messages = await user.get('/inbox/messages');
expect(messages.length).to.equal(3);
expect(messages[0].text).to.equal('third');
expect(messages[1].text).to.equal('second');
expect(messages[2].text).to.equal('first');
await user.del(`/inbox/messages/${messages[1]._id}`);
const updatedMessages = await user.get('/inbox/messages');
expect(updatedMessages.length).to.equal(2);
expect(updatedMessages[0].text).to.equal('third');
expect(updatedMessages[1].text).to.equal('first');
});
});

View File

@@ -0,0 +1,58 @@
import {
generateUser,
} from '../../../helpers/api-integration/v4';
import common from '../../../../website/common';
describe('GET /user', () => {
let user;
before(async () => {
user = await generateUser();
});
it('returns the authenticated user with computed stats', async () => {
let returnedUser = await user.get('/user');
expect(returnedUser._id).to.equal(user._id);
expect(returnedUser.stats.maxMP).to.exist;
expect(returnedUser.stats.maxHealth).to.equal(common.maxHealth);
expect(returnedUser.stats.toNextLevel).to.equal(common.tnl(returnedUser.stats.lvl));
});
it('does not return private paths (and apiToken)', async () => {
let returnedUser = await user.get('/user');
expect(returnedUser.auth.local.hashed_password).to.not.exist;
expect(returnedUser.auth.local.passwordHashMethod).to.not.exist;
expect(returnedUser.auth.local.salt).to.not.exist;
expect(returnedUser.apiToken).to.not.exist;
});
it('returns only user properties requested', async () => {
let returnedUser = await user.get('/user?userFields=achievements,items.mounts');
expect(returnedUser._id).to.equal(user._id);
expect(returnedUser.achievements).to.exist;
expect(returnedUser.items.mounts).to.exist;
// Notifications are always returned
expect(returnedUser.notifications).to.exist;
expect(returnedUser.stats).to.not.exist;
});
it('does not return new inbox messages', async () => {
const otherUser = await generateUser();
await otherUser.post('/members/send-private-message', {
toUserId: user.id,
message: 'first',
});
await otherUser.post('/members/send-private-message', {
toUserId: user.id,
message: 'second',
});
let returnedUser = await user.get('/user');
expect(returnedUser._id).to.equal(user._id);
expect(returnedUser.inbox.messages).to.be.empty;
});
});

View File

@@ -0,0 +1,324 @@
import {
generateUser,
translate as t,
createAndPopulateGroup,
generateGroup,
generateChallenge,
sleep,
} from '../../../helpers/api-integration/v4';
import { v4 as generateUUID } from 'uuid';
import { find } from 'lodash';
import apiError from '../../../../website/server/libs/apiError';
describe('POST /user/class/cast/:spellId', () => {
let user;
beforeEach(async () => {
user = await generateUser();
});
it('returns an error if spell does not exist', async () => {
await user.update({'stats.class': 'rogue'});
let spellId = 'invalidSpell';
await expect(user.post(`/user/class/cast/${spellId}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: apiError('spellNotFound', {spellId}),
});
});
it('returns an error if spell does not exist in user\'s class', async () => {
let spellId = 'pickPocket';
await expect(user.post(`/user/class/cast/${spellId}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: apiError('spellNotFound', {spellId}),
});
});
it('returns an error if spell.mana > user.mana', async () => {
await user.update({'stats.class': 'rogue'});
await expect(user.post('/user/class/cast/backStab'))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('notEnoughMana'),
});
});
it('returns an error if spell.value > user.gold', async () => {
await expect(user.post('/user/class/cast/birthday'))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageNotEnoughGold'),
});
});
it('returns an error if spell.lvl > user.level', async () => {
await user.update({'stats.mp': 200, 'stats.class': 'wizard'});
await expect(user.post('/user/class/cast/earth'))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('spellLevelTooHigh', {level: 13}),
});
});
it('returns an error if user doesn\'t own the spell', async () => {
await expect(user.post('/user/class/cast/snowball'))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('spellNotOwned'),
});
});
it('returns an error if targetId is not an UUID', async () => {
await expect(user.post('/user/class/cast/spellId?targetId=notAnUUID'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('returns an error if targetId is required but missing', async () => {
await user.update({'stats.class': 'rogue', 'stats.lvl': 11});
await expect(user.post('/user/class/cast/pickPocket'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('targetIdUUID'),
});
});
it('returns an error if targeted task doesn\'t exist', async () => {
await user.update({'stats.class': 'rogue', 'stats.lvl': 11});
await expect(user.post(`/user/class/cast/pickPocket?targetId=${generateUUID()}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('taskNotFound'),
});
});
it('returns an error if a challenge task was targeted', async () => {
let {group, groupLeader} = await createAndPopulateGroup();
let challenge = await generateChallenge(groupLeader, group);
await groupLeader.post(`/challenges/${challenge._id}/join`);
await groupLeader.post(`/tasks/challenge/${challenge._id}`, [
{type: 'habit', text: 'task text'},
]);
await groupLeader.update({'stats.class': 'rogue', 'stats.lvl': 11});
await sleep(0.5);
await groupLeader.sync();
await expect(groupLeader.post(`/user/class/cast/pickPocket?targetId=${groupLeader.tasksOrder.habits[0]}`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('challengeTasksNoCast'),
});
});
it('returns an error if a group task was targeted', async () => {
let {group, groupLeader} = await createAndPopulateGroup();
let groupTask = await groupLeader.post(`/tasks/group/${group._id}`, {
text: 'todo group',
type: 'todo',
});
await groupLeader.post(`/tasks/${groupTask._id}/assign/${groupLeader._id}`);
let memberTasks = await groupLeader.get('/tasks/user');
let syncedGroupTask = find(memberTasks, function findAssignedTask (memberTask) {
return memberTask.group.id === group._id;
});
await groupLeader.update({'stats.class': 'rogue', 'stats.lvl': 11});
await sleep(0.5);
await groupLeader.sync();
await expect(groupLeader.post(`/user/class/cast/pickPocket?targetId=${syncedGroupTask._id}`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('groupTasksNoCast'),
});
});
it('returns an error if targeted party member doesn\'t exist', async () => {
let {groupLeader} = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
members: 1,
});
await groupLeader.update({'items.special.snowball': 3});
let target = generateUUID();
await expect(groupLeader.post(`/user/class/cast/snowball?targetId=${target}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('userWithIDNotFound', {userId: target}),
});
});
it('returns an error if party does not exists', async () => {
await user.update({'items.special.snowball': 3});
await expect(user.post(`/user/class/cast/snowball?targetId=${generateUUID()}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('partyNotFound'),
});
});
it('send message in party chat if party && !spell.silent', async () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
members: 1,
});
await groupLeader.update({'stats.mp': 200, 'stats.class': 'wizard', 'stats.lvl': 13});
await groupLeader.post('/user/class/cast/earth');
await sleep(1);
const groupMessages = await groupLeader.get(`/groups/${group._id}/chat`);
expect(groupMessages[0]).to.exist;
expect(groupMessages[0].uuid).to.equal('system');
});
it('Ethereal Surge does not recover mp of other mages', async () => {
let group = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
members: 4,
});
let promises = [];
promises.push(group.groupLeader.update({'stats.mp': 200, 'stats.class': 'wizard', 'stats.lvl': 20}));
promises.push(group.members[0].update({'stats.mp': 0, 'stats.class': 'warrior', 'stats.lvl': 20}));
promises.push(group.members[1].update({'stats.mp': 0, 'stats.class': 'wizard', 'stats.lvl': 20}));
promises.push(group.members[2].update({'stats.mp': 0, 'stats.class': 'rogue', 'stats.lvl': 20}));
promises.push(group.members[3].update({'stats.mp': 0, 'stats.class': 'healer', 'stats.lvl': 20}));
await Promise.all(promises);
await group.groupLeader.post('/user/class/cast/mpheal');
promises = [];
promises.push(group.members[0].sync());
promises.push(group.members[1].sync());
promises.push(group.members[2].sync());
promises.push(group.members[3].sync());
await Promise.all(promises);
expect(group.members[0].stats.mp).to.be.greaterThan(0); // warrior
expect(group.members[1].stats.mp).to.equal(0); // wizard
expect(group.members[2].stats.mp).to.be.greaterThan(0); // rogue
expect(group.members[3].stats.mp).to.be.greaterThan(0); // healer
});
it('cast bulk', async () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
members: 1,
});
await groupLeader.update({'stats.mp': 200, 'stats.class': 'wizard', 'stats.lvl': 13});
await groupLeader.post('/user/class/cast/earth', {quantity: 2});
await sleep(1);
group = await groupLeader.get(`/groups/${group._id}`);
expect(group.chat[0]).to.exist;
expect(group.chat[0].uuid).to.equal('system');
});
it('searing brightness does not affect challenge or group tasks', async () => {
let guild = await generateGroup(user);
let challenge = await generateChallenge(user, guild);
await user.post(`/challenges/${challenge._id}/join`);
await user.post(`/tasks/challenge/${challenge._id}`, {
text: 'test challenge habit',
type: 'habit',
});
let groupTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'todo group',
type: 'todo',
});
await user.update({'stats.class': 'healer', 'stats.mp': 200, 'stats.lvl': 15});
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
await user.post('/user/class/cast/brightness');
await user.sync();
let memberTasks = await user.get('/tasks/user');
let syncedGroupTask = find(memberTasks, function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
});
let userChallengeTask = find(memberTasks, function findAssignedTask (memberTask) {
return memberTask.challenge.id === challenge._id;
});
expect(userChallengeTask).to.exist;
expect(syncedGroupTask).to.exist;
expect(userChallengeTask.value).to.equal(0);
expect(syncedGroupTask.value).to.equal(0);
});
it('increases both user\'s achievement values', async () => {
let party = await createAndPopulateGroup({
members: 1,
});
let leader = party.groupLeader;
let recipient = party.members[0];
await leader.update({'stats.gp': 10});
await leader.post(`/user/class/cast/birthday?targetId=${recipient._id}`);
await leader.sync();
await recipient.sync();
expect(leader.achievements.birthday).to.equal(1);
expect(recipient.achievements.birthday).to.equal(1);
});
it('only increases user\'s achievement one if target == caster', async () => {
await user.update({'stats.gp': 10});
await user.post(`/user/class/cast/birthday?targetId=${user._id}`);
await user.sync();
expect(user.achievements.birthday).to.equal(1);
});
it('passes correct target to spell when targetType === \'task\'', async () => {
await user.update({'stats.class': 'wizard', 'stats.lvl': 11});
let task = await user.post('/tasks/user', {
text: 'test habit',
type: 'habit',
});
let result = await user.post(`/user/class/cast/fireball?targetId=${task._id}`);
expect(result.task._id).to.equal(task._id);
});
it('passes correct target to spell when targetType === \'self\'', async () => {
await user.update({'stats.class': 'wizard', 'stats.lvl': 14, 'stats.mp': 50});
let result = await user.post('/user/class/cast/frost');
expect(result.user.stats.mp).to.equal(10);
});
// TODO find a way to have sinon working in integration tests
// it doesn't work when tests are running separately from server
it('passes correct target to spell when targetType === \'tasks\'');
it('passes correct target to spell when targetType === \'party\'');
it('passes correct target to spell when targetType === \'user\'');
it('passes correct target to spell when targetType === \'party\' and user is not in a party');
it('passes correct target to spell when targetType === \'user\' and user is not in a party');
});

View File

@@ -0,0 +1,60 @@
import {
generateUser,
generateDaily,
generateReward,
translate as t,
} from '../../../helpers/api-integration/v4';
describe('POST /user/rebirth', () => {
let user;
beforeEach(async () => {
user = await generateUser();
});
it('returns an error when user balance is too low', async () => {
await expect(user.post('/user/rebirth'))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('notEnoughGems'),
});
});
// More tests in common code unit tests
it('resets user\'s tasks', async () => {
await user.update({
balance: 1.5,
});
let daily = await generateDaily({
text: 'test habit',
type: 'daily',
value: 1,
streak: 1,
userId: user._id,
});
let reward = await generateReward({
text: 'test reward',
type: 'reward',
value: 1,
userId: user._id,
});
let response = await user.post('/user/rebirth');
await user.sync();
expect(user.notifications.length).to.equal(1);
expect(user.notifications[0].type).to.equal('REBIRTH_ACHIEVEMENT');
let updatedDaily = await user.get(`/tasks/${daily._id}`);
let updatedReward = await user.get(`/tasks/${reward._id}`);
expect(response.message).to.equal(t('rebirthComplete'));
expect(updatedDaily.streak).to.equal(0);
expect(updatedDaily.value).to.equal(0);
expect(updatedReward.value).to.equal(1);
});
});

View File

@@ -0,0 +1,54 @@
import {
generateUser,
generateDaily,
generateReward,
translate as t,
} from '../../../helpers/api-integration/v4';
describe('POST /user/reroll', () => {
let user;
beforeEach(async () => {
user = await generateUser();
});
it('returns an error when user balance is too low', async () => {
await expect(user.post('/user/reroll'))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('notEnoughGems'),
});
});
// More tests in common code unit tests
it('resets user\'s tasks', async () => {
await user.update({
balance: 2,
});
let daily = await generateDaily({
text: 'test habit',
type: 'daily',
userId: user._id,
});
let reward = await generateReward({
text: 'test reward',
type: 'reward',
value: 1,
userId: user._id,
});
let response = await user.post('/user/reroll');
await user.sync();
let updatedDaily = await user.get(`/tasks/${daily._id}`);
let updatedReward = await user.get(`/tasks/${reward._id}`);
expect(response.message).to.equal(t('fortifyComplete'));
expect(updatedDaily.value).to.equal(0);
expect(updatedReward.value).to.equal(1);
});
});

View File

@@ -0,0 +1,121 @@
import {
generateUser,
generateGroup,
generateChallenge,
translate as t,
} from '../../../helpers/api-integration/v4';
import { find } from 'lodash';
describe('POST /user/reset', () => {
let user;
beforeEach(async () => {
user = await generateUser();
});
// More tests in common code unit tests
it('resets user\'s habits', async () => {
let task = await user.post('/tasks/user', {
text: 'test habit',
type: 'habit',
});
await user.post('/user/reset');
await user.sync();
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('taskNotFound'),
});
expect(user.tasksOrder.habits).to.be.empty;
});
it('resets user\'s dailys', async () => {
let task = await user.post('/tasks/user', {
text: 'test daily',
type: 'daily',
});
await user.post('/user/reset');
await user.sync();
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('taskNotFound'),
});
expect(user.tasksOrder.dailys).to.be.empty;
});
it('resets user\'s todos', async () => {
let task = await user.post('/tasks/user', {
text: 'test todo',
type: 'todo',
});
await user.post('/user/reset');
await user.sync();
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('taskNotFound'),
});
expect(user.tasksOrder.todos).to.be.empty;
});
it('resets user\'s rewards', async () => {
let task = await user.post('/tasks/user', {
text: 'test reward',
type: 'reward',
});
await user.post('/user/reset');
await user.sync();
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('taskNotFound'),
});
expect(user.tasksOrder.rewards).to.be.empty;
});
it('does not delete challenge or group tasks', async () => {
let guild = await generateGroup(user);
let challenge = await generateChallenge(user, guild);
await user.post(`/challenges/${challenge._id}/join`);
await user.post(`/tasks/challenge/${challenge._id}`, {
text: 'test challenge habit',
type: 'habit',
});
let groupTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'todo group',
type: 'todo',
});
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
await user.post('/user/reset');
await user.sync();
let memberTasks = await user.get('/tasks/user');
let syncedGroupTask = find(memberTasks, function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
});
let userChallengeTask = find(memberTasks, function findAssignedTask (memberTask) {
return memberTask.challenge.id === challenge._id;
});
expect(userChallengeTask).to.exist;
expect(syncedGroupTask).to.exist;
});
});

View File

@@ -0,0 +1,256 @@
import {
generateUser,
translate as t,
} from '../../../helpers/api-integration/v4';
import { each, get } from 'lodash';
describe('PUT /user', () => {
let user;
beforeEach(async () => {
user = await generateUser();
});
context('Allowed Operations', () => {
it('updates the user', async () => {
await user.put('/user', {
'profile.name': 'Frodo',
'preferences.costume': true,
'stats.hp': 14,
});
await user.sync();
expect(user.profile.name).to.eql('Frodo');
expect(user.preferences.costume).to.eql(true);
expect(user.stats.hp).to.eql(14);
});
it('tags must be an array', async () => {
await expect(user.put('/user', {
tags: {
tag: true,
},
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'mustBeArray',
});
});
it('update tags', async () => {
let userTags = user.tags;
await user.put('/user', {
tags: [...user.tags, {
name: 'new tag',
}],
});
await user.sync();
expect(user.tags.length).to.be.eql(userTags.length + 1);
});
it('profile.name cannot be an empty string or null', async () => {
await expect(user.put('/user', {
'profile.name': ' ', // string should be trimmed
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'User validation failed',
});
await expect(user.put('/user', {
'profile.name': '',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'User validation failed',
});
await expect(user.put('/user', {
'profile.name': null,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'User validation failed',
});
});
});
context('Top Level Protected Operations', () => {
let protectedOperations = {
'gem balance': {balance: 100},
auth: {'auth.blocked': true, 'auth.timestamps.created': new Date()},
contributor: {'contributor.level': 9, 'contributor.admin': true, 'contributor.text': 'some text'},
backer: {'backer.tier': 10, 'backer.npc': 'Bilbo'},
subscriptions: {'purchased.plan.extraMonths': 500, 'purchased.plan.consecutive.trinkets': 1000},
'customization gem purchases': {'purchased.background.tavern': true, 'purchased.skin.bear': true},
notifications: [{type: 123}],
webhooks: {webhooks: [{url: 'https://foobar.com'}]},
};
each(protectedOperations, (data, testName) => {
it(`does not allow updating ${testName}`, async () => {
let errorText = t('messageUserOperationProtected', { operation: Object.keys(data)[0] });
await expect(user.put('/user', data)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: errorText,
});
});
});
});
context('Sub-Level Protected Operations', () => {
let protectedOperations = {
'class stat': {'stats.class': 'wizard'},
'flags unless whitelisted': {'flags.dropsEnabled': true},
webhooks: {'preferences.webhooks': [1, 2, 3]},
sleep: {'preferences.sleep': true},
'disable classes': {'preferences.disableClasses': true},
};
each(protectedOperations, (data, testName) => {
it(`does not allow updating ${testName}`, async () => {
let errorText = t('messageUserOperationProtected', { operation: Object.keys(data)[0] });
await expect(user.put('/user', data)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: errorText,
});
});
});
});
context('Default Appearance Preferences', () => {
let testCases = {
shirt: 'yellow',
skin: 'ddc994',
'hair.color': 'blond',
'hair.bangs': 2,
'hair.base': 1,
'hair.flower': 4,
size: 'broad',
};
each(testCases, (item, type) => {
const update = {};
update[`preferences.${type}`] = item;
it(`updates user with ${type} that is a default`, async () => {
let dbUpdate = {};
dbUpdate[`purchased.${type}.${item}`] = true;
await user.update(dbUpdate);
// Sanity checks to make sure user is not already equipped with item
expect(get(user.preferences, type)).to.not.eql(item);
let updatedUser = await user.put('/user', update);
expect(get(updatedUser.preferences, type)).to.eql(item);
});
});
it('returns an error if user tries to update body size with invalid type', async () => {
await expect(user.put('/user', {
'preferences.size': 'round',
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('mustPurchaseToSet', { val: 'round', key: 'preferences.size' }),
});
});
it('can set beard to default', async () => {
await user.update({
'purchased.hair.beard': 3,
'preferences.hair.beard': 3,
});
let updatedUser = await user.put('/user', {
'preferences.hair.beard': 0,
});
expect(updatedUser.preferences.hair.beard).to.eql(0);
});
it('can set mustache to default', async () => {
await user.update({
'purchased.hair.mustache': 2,
'preferences.hair.mustache': 2,
});
let updatedUser = await user.put('/user', {
'preferences.hair.mustache': 0,
});
expect(updatedUser.preferences.hair.mustache).to.eql(0);
});
});
context('Purchasable Appearance Preferences', () => {
let testCases = {
background: 'volcano',
shirt: 'convict',
skin: 'cactus',
'hair.base': 7,
'hair.beard': 2,
'hair.color': 'rainbow',
'hair.mustache': 2,
};
each(testCases, (item, type) => {
const update = {};
update[`preferences.${type}`] = item;
it(`returns an error if user tries to update ${type} with ${type} the user does not own`, async () => {
await expect(user.put('/user', update)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('mustPurchaseToSet', {val: item, key: `preferences.${type}`}),
});
});
it(`updates user with ${type} user does own`, async () => {
let dbUpdate = {};
dbUpdate[`purchased.${type}.${item}`] = true;
await user.update(dbUpdate);
// Sanity check to make sure user is not already equipped with item
expect(get(user.preferences, type)).to.not.eql(item);
let updatedUser = await user.put('/user', update);
expect(get(updatedUser.preferences, type)).to.eql(item);
});
});
});
context('Improvement Categories', () => {
it('sets valid categories', async () => {
await user.put('/user', {
'preferences.improvementCategories': ['work', 'school'],
});
await user.sync();
expect(user.preferences.improvementCategories).to.eql(['work', 'school']);
});
it('discards invalid categories', async () => {
await expect(user.put('/user', {
'preferences.improvementCategories': ['work', 'procrastination', 'school'],
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'User validation failed',
});
});
});
});

View File

@@ -0,0 +1,739 @@
import {
generateUser,
requester,
translate as t,
createAndPopulateGroup,
getProperty,
} from '../../../../helpers/api-integration/v4';
import { ApiUser } from '../../../../helpers/api-integration/api-classes';
import { v4 as uuid } from 'uuid';
import { each } from 'lodash';
import { encrypt } from '../../../../../website/server/libs/encryption';
function generateRandomUserName () {
return (Date.now() + uuid()).substring(0, 20);
}
describe('POST /user/auth/local/register', () => {
context('username and email are free', () => {
let api;
beforeEach(async () => {
api = requester();
});
it('registers a new user', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user._id).to.exist;
expect(user.apiToken).to.exist;
expect(user.auth.local.username).to.eql(username);
expect(user.profile.name).to.eql(username);
expect(user.newUser).to.eql(true);
});
xit('remove spaces from username', async () => {
// TODO can probably delete this test now
let username = ' usernamewithspaces ';
let email = 'test@example.com';
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.auth.local.username).to.eql(username.trim());
expect(user.profile.name).to.eql(username.trim());
});
context('validates username', () => {
const email = 'test@example.com';
const password = 'password';
it('requires to username to be less than 20', async () => {
const username = (Date.now() + uuid()).substring(0, 21);
await expect(api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Invalid request parameters.',
});
});
it('rejects chracters not in [-_a-zA-Z0-9]', async () => {
const username = 'a-zA_Z09*';
await expect(api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Invalid request parameters.',
});
});
it('allows only [-_a-zA-Z0-9] characters', async () => {
const username = 'a-zA_Z09';
const user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.auth.local.username).to.eql(username);
});
});
context('provides default tags and tasks', async () => {
it('for a generic API consumer', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
let requests = new ApiUser(user);
let habits = await requests.get('/tasks/user?type=habits');
let dailys = await requests.get('/tasks/user?type=dailys');
let todos = await requests.get('/tasks/user?type=todos');
let rewards = await requests.get('/tasks/user?type=rewards');
let tags = await requests.get('/tags');
expect(habits).to.have.a.lengthOf(0);
expect(dailys).to.have.a.lengthOf(0);
expect(todos).to.have.a.lengthOf(1);
expect(rewards).to.have.a.lengthOf(0);
expect(tags).to.have.a.lengthOf(7);
expect(tags[0].name).to.eql(t('defaultTag1'));
expect(tags[1].name).to.eql(t('defaultTag2'));
expect(tags[2].name).to.eql(t('defaultTag3'));
expect(tags[3].name).to.eql(t('defaultTag4'));
expect(tags[4].name).to.eql(t('defaultTag5'));
expect(tags[5].name).to.eql(t('defaultTag6'));
expect(tags[6].name).to.eql(t('defaultTag7'));
});
xit('for Web', async () => {
api = requester(
null,
{'x-client': 'habitica-web'},
);
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
let requests = new ApiUser(user);
let habits = await requests.get('/tasks/user?type=habits');
let dailys = await requests.get('/tasks/user?type=dailys');
let todos = await requests.get('/tasks/user?type=todos');
let rewards = await requests.get('/tasks/user?type=rewards');
let tags = await requests.get('/tags');
expect(habits).to.have.a.lengthOf(3);
expect(habits[0].text).to.eql(t('defaultHabit1Text'));
expect(habits[0].notes).to.eql('');
expect(habits[1].text).to.eql(t('defaultHabit2Text'));
expect(habits[1].notes).to.eql('');
expect(habits[2].text).to.eql(t('defaultHabit3Text'));
expect(habits[2].notes).to.eql('');
expect(dailys).to.have.a.lengthOf(0);
expect(todos).to.have.a.lengthOf(1);
expect(todos[0].text).to.eql(t('defaultTodo1Text'));
expect(todos[0].notes).to.eql(t('defaultTodoNotes'));
expect(rewards).to.have.a.lengthOf(1);
expect(rewards[0].text).to.eql(t('defaultReward1Text'));
expect(rewards[0].notes).to.eql('');
expect(tags).to.have.a.lengthOf(7);
expect(tags[0].name).to.eql(t('defaultTag1'));
expect(tags[1].name).to.eql(t('defaultTag2'));
expect(tags[2].name).to.eql(t('defaultTag3'));
expect(tags[3].name).to.eql(t('defaultTag4'));
expect(tags[4].name).to.eql(t('defaultTag5'));
expect(tags[5].name).to.eql(t('defaultTag6'));
expect(tags[6].name).to.eql(t('defaultTag7'));
});
});
context('does not provide default tags and tasks', async () => {
it('for Android', async () => {
api = requester(
null,
{'x-client': 'habitica-android'},
);
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
let requests = new ApiUser(user);
let habits = await requests.get('/tasks/user?type=habits');
let dailys = await requests.get('/tasks/user?type=dailys');
let todos = await requests.get('/tasks/user?type=todos');
let rewards = await requests.get('/tasks/user?type=rewards');
let tags = await requests.get('/tags');
expect(habits).to.have.a.lengthOf(0);
expect(dailys).to.have.a.lengthOf(0);
expect(todos).to.have.a.lengthOf(0);
expect(rewards).to.have.a.lengthOf(0);
expect(tags).to.have.a.lengthOf(0);
});
it('for iOS', async () => {
api = requester(
null,
{'x-client': 'habitica-ios'},
);
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
let requests = new ApiUser(user);
let habits = await requests.get('/tasks/user?type=habits');
let dailys = await requests.get('/tasks/user?type=dailys');
let todos = await requests.get('/tasks/user?type=todos');
let rewards = await requests.get('/tasks/user?type=rewards');
let tags = await requests.get('/tags');
expect(habits).to.have.a.lengthOf(0);
expect(dailys).to.have.a.lengthOf(0);
expect(todos).to.have.a.lengthOf(0);
expect(rewards).to.have.a.lengthOf(0);
expect(tags).to.have.a.lengthOf(0);
});
});
it('enrolls new users in an A/B test', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
});
it('includes items awarded by default when creating a new user', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.items.quests.dustbunnies).to.equal(1);
expect(user.purchased.background.violet).to.be.ok;
expect(user.preferences.background).to.equal('violet');
});
it('requires password and confirmPassword to match', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let confirmPassword = 'not password';
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 () => {
let email = `${generateRandomUserName()}@example.com`;
let password = 'password';
let confirmPassword = 'password';
await expect(api.post('/user/auth/local/register', {
email,
password,
confirmPassword,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('requires an email', async () => {
let username = generateRandomUserName();
let password = 'password';
await expect(api.post('/user/auth/local/register', {
username,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('requires a valid email', async () => {
let username = generateRandomUserName();
let email = 'notanemail@sdf';
let password = 'password';
await expect(api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('sanitizes email params to a lowercase string before creating the user', async () => {
let username = generateRandomUserName();
let email = 'ISANEmAiL@ExAmPle.coM';
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.auth.local.email).to.equal(email.toLowerCase());
});
it('fails on a habitica.com email', async () => {
let username = generateRandomUserName();
let email = `${username}@habitica.com`;
let password = 'password';
await expect(api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'User validation failed',
});
});
it('fails on a habitrpg.com email', async () => {
let username = generateRandomUserName();
let email = `${username}@habitrpg.com`;
let password = 'password';
await expect(api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'User validation failed',
});
});
it('requires a password', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
let confirmPassword = 'password';
await expect(api.post('/user/auth/local/register', {
username,
email,
confirmPassword,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
});
context('attach to facebook user', () => {
let user;
let email = 'some@email.net';
let username = 'some-username';
let password = 'some-password';
beforeEach(async () => {
user = await generateUser();
});
it('checks onlySocialAttachLocal', async () => {
await expect(user.post('/user/auth/local/register', {
email,
username,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlySocialAttachLocal'),
});
});
it('succeeds', async () => {
await user.update({ 'auth.facebook.id': 'some-fb-id', 'auth.local': { ok: true } });
await user.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
await user.sync();
expect(user.auth.local.username).to.eql(username);
expect(user.auth.local.email).to.eql(email);
});
});
context('login is already taken', () => {
let username, email, api;
beforeEach(async () => {
api = requester();
username = generateRandomUserName();
email = `${username}@example.com`;
return generateUser({
'auth.local.username': username,
'auth.local.lowerCaseUsername': username,
'auth.local.email': email,
});
});
it('rejects if username is already taken', async () => {
let uniqueEmail = `${generateRandomUserName()}@exampe.com`;
let password = 'password';
await expect(api.post('/user/auth/local/register', {
username,
email: uniqueEmail,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('usernameTaken'),
});
});
it('rejects if email is already taken', async () => {
let uniqueUsername = generateRandomUserName();
let password = 'password';
await expect(api.post('/user/auth/local/register', {
username: uniqueUsername,
email,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('emailTaken'),
});
});
});
context('req.query.groupInvite', () => {
let api, username, email, password;
beforeEach(() => {
api = requester();
username = generateRandomUserName();
email = `${username}@example.com`;
password = 'password';
});
it('does not crash the signup process when it\'s invalid', async () => {
let user = await api.post('/user/auth/local/register?groupInvite=aaaaInvalid', {
username,
email,
password,
confirmPassword: password,
});
expect(user._id).to.be.a('string');
});
it('supports invite using req.query.groupInvite', async () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
});
let invite = encrypt(JSON.stringify({
id: group._id,
inviter: groupLeader._id,
sentAt: Date.now(), // so we can let it expire
}));
let user = await api.post(`/user/auth/local/register?groupInvite=${invite}`, {
username,
email,
password,
confirmPassword: password,
});
expect(user.invitations.parties[0].id).to.eql(group._id);
expect(user.invitations.parties[0].name).to.eql(group.name);
expect(user.invitations.parties[0].inviter).to.eql(groupLeader._id);
});
it('awards achievement to inviter', async () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
});
let invite = encrypt(JSON.stringify({
id: group._id,
inviter: groupLeader._id,
sentAt: Date.now(),
}));
await api.post(`/user/auth/local/register?groupInvite=${invite}`, {
username,
email,
password,
confirmPassword: password,
});
await groupLeader.sync();
expect(groupLeader.achievements.invitedFriend).to.be.true;
});
it('user not added to a party on expired invite', async () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
});
let invite = encrypt(JSON.stringify({
id: group._id,
inviter: groupLeader._id,
sentAt: Date.now() - 6.912e8, // 8 days old
}));
let user = await api.post(`/user/auth/local/register?groupInvite=${invite}`, {
username,
email,
password,
confirmPassword: password,
});
expect(user.invitations.party).to.eql({});
});
it('adds a user to a guild on an invite of type other than party', async () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
});
let invite = encrypt(JSON.stringify({
id: group._id,
inviter: groupLeader._id,
sentAt: Date.now(),
}));
let user = await api.post(`/user/auth/local/register?groupInvite=${invite}`, {
username,
email,
password,
confirmPassword: password,
});
expect(user.invitations.guilds[0]).to.eql({
id: group._id,
name: group.name,
inviter: groupLeader._id,
});
});
});
context('successful login via api', () => {
let api, username, email, password;
beforeEach(() => {
api = requester();
username = generateRandomUserName();
email = `${username}@example.com`;
password = 'password';
});
it('sets all site tour values to -2 (already seen)', async () => {
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.flags.tour).to.not.be.empty;
each(user.flags.tour, (value) => {
expect(value).to.eql(-2);
});
});
it('populates user with default todos, not no other task types', async () => {
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.tasksOrder.todos).to.not.be.empty;
expect(user.tasksOrder.dailys).to.be.empty;
expect(user.tasksOrder.habits).to.be.empty;
expect(user.tasksOrder.rewards).to.be.empty;
});
it('populates user with default tags', async () => {
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.tags).to.not.be.empty;
});
});
context('successful login with habitica-web header', () => {
let api, username, email, password;
beforeEach(() => {
api = requester({}, {'x-client': 'habitica-web'});
username = generateRandomUserName();
email = `${username}@example.com`;
password = 'password';
});
it('sets all common tutorial flags to true', async () => {
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.flags.tour).to.not.be.empty;
each(user.flags.tutorial.common, (value) => {
expect(value).to.eql(true);
});
});
it('populates user with default todos, habits, and rewards', async () => {
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.tasksOrder.todos).to.be.empty;
expect(user.tasksOrder.dailys).to.be.empty;
expect(user.tasksOrder.habits).to.be.empty;
expect(user.tasksOrder.rewards).to.be.empty;
});
it('populates user with default tags', async () => {
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.tags).to.not.be.empty;
});
it('adds the correct tags to the correct tasks', async () => {
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
let requests = new ApiUser(user);
let habits = await requests.get('/tasks/user?type=habits');
let todos = await requests.get('/tasks/user?type=todos');
expect(habits).to.have.a.lengthOf(0);
expect(todos).to.have.a.lengthOf(0);
});
});
});

View File

@@ -0,0 +1,23 @@
import Vue from 'vue';
import MemberDetailsComponent from 'client/components/memberDetails.vue';
describe('Members Details Component', () => {
let CTor;
let vm;
beforeEach(() => {
CTor = Vue.extend(MemberDetailsComponent);
vm = new CTor().$mount();
});
afterEach(() => {
vm.$destroy();
});
xit('prevents flickering by setting a 1px margin-right on elements of class member-stats', () => {
const memberstats = vm.$el.querySelector('.member-stats');
const style = window.getComputedStyle(memberstats, null);
const marginRightProp = style.getPropertyValue('margin-right');
expect(marginRightProp).to.equal('1');
});
});

View File

@@ -0,0 +1,50 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import NotificationsComponent from 'client/components/notifications.vue';
import Store from 'client/libs/store';
import { hasClass } from 'client/store/getters/members';
const localVue = createLocalVue();
localVue.use(Store);
describe('Notifications', () => {
let store;
beforeEach(() => {
store = new Store({
state: {
user: {
data: {
stats: {
lvl: 0,
},
flags: {},
preferences: {},
party: {
quest: {
},
},
},
},
},
actions: {
'user:fetch': () => {},
'tasks:fetchUserTasks': () => {},
},
getters: {
'members:hasClass': hasClass,
},
});
});
it('set user has class computed prop', () => {
const wrapper = shallowMount(NotificationsComponent, { store, localVue });
expect(wrapper.vm.userHasClass).to.be.false;
store.state.user.data.stats.lvl = 10;
store.state.user.data.flags.classSelected = true;
store.state.user.data.preferences.disableClasses = false;
expect(wrapper.vm.userHasClass).to.be.true;
});
});

View File

@@ -0,0 +1,52 @@
import hasClass from '../../../website/common/script/libs/hasClass';
import { generateUser } from '../../helpers/common.helper';
describe('hasClass', () => {
it('returns false for user with level below 10', () => {
let userLvl9 = generateUser({
'stats.lvl': 9,
'flags.classSelected': true,
'preferences.disableClasses': false,
});
let result = hasClass(userLvl9);
expect(result).to.eql(false);
});
it('returns false for user with class not selected', () => {
let userClassNotSelected = generateUser({
'stats.lvl': 10,
'flags.classSelected': false,
'preferences.disableClasses': false,
});
let result = hasClass(userClassNotSelected);
expect(result).to.eql(false);
});
it('returns false for user with classes disabled', () => {
let userClassesDisabled = generateUser({
'stats.lvl': 10,
'flags.classSelected': true,
'preferences.disableClasses': true,
});
let result = hasClass(userClassesDisabled);
expect(result).to.eql(false);
});
it('returns true for user with class', () => {
let userClassSelected = generateUser({
'stats.lvl': 10,
'flags.classSelected': true,
'preferences.disableClasses': false,
});
let result = hasClass(userClassSelected);
expect(result).to.eql(true);
});
});

View File

@@ -0,0 +1,94 @@
import pinnedGearUtils from '../../../../website/common/script/ops/pinnedGearUtils';
import {
NotAuthorized,
} from '../../../../website/common/script/libs/errors';
import i18n from '../../../../website/common/script/i18n';
import {
generateUser,
} from '../../../helpers/common.helper';
import {BuyQuestWithGemOperation} from '../../../../website/common/script/ops/buy/buyQuestGem';
describe('shared.ops.buyQuestGems', () => {
let user;
let goldPoints = 40;
let analytics = {track () {}};
function buyQuest (_user, _req, _analytics) {
const buyOp = new BuyQuestWithGemOperation(_user, _req, _analytics);
return buyOp.purchase();
}
before(() => {
user = generateUser({'stats.class': 'rogue'});
});
beforeEach(() => {
sinon.stub(analytics, 'track');
sinon.spy(pinnedGearUtils, 'removeItemByPath');
});
afterEach(() => {
analytics.track.restore();
pinnedGearUtils.removeItemByPath.restore();
});
context('successful purchase', () => {
let userGemAmount = 10;
before(() => {
user.balance = userGemAmount;
user.stats.gp = goldPoints;
user.purchased.plan.gemsBought = 0;
user.purchased.plan.customerId = 'customer-id';
user.pinnedItems.push({type: 'quests', key: 'gryphon'});
});
it('purchases quests', () => {
let key = 'gryphon';
buyQuest(user, {params: {key}});
expect(user.items.quests[key]).to.equal(1);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
});
});
context('bulk purchase', () => {
let userGemAmount = 10;
beforeEach(() => {
user.balance = userGemAmount;
user.stats.gp = goldPoints;
user.purchased.plan.gemsBought = 0;
user.purchased.plan.customerId = 'customer-id';
});
it('errors when user does not have enough gems', (done) => {
user.balance = 1;
let key = 'gryphon';
try {
buyQuest(user, {
params: {key},
quantity: 2,
});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('notEnoughGems'));
done();
}
});
it('makes bulk purchases of quests', () => {
let key = 'gryphon';
buyQuest(user, {
params: {key},
quantity: 3,
});
expect(user.items.quests[key]).to.equal(4);
});
});
});

View File

@@ -121,7 +121,6 @@ describe('shared.ops.purchase', () => {
user.pinnedItems.push({type: 'eggs', key: 'Wolf'});
user.pinnedItems.push({type: 'hatchingPotions', key: 'Base'});
user.pinnedItems.push({type: 'food', key: SEASONAL_FOOD});
user.pinnedItems.push({type: 'quests', key: 'gryphon'});
user.pinnedItems.push({type: 'gear', key: 'headAccessory_special_tigerEars'});
user.pinnedItems.push({type: 'bundles', key: 'featheredFriends'});
});
@@ -157,16 +156,6 @@ describe('shared.ops.purchase', () => {
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
});
it('purchases quests', () => {
let type = 'quests';
let key = 'gryphon';
purchase(user, {params: {type, key}});
expect(user.items[type][key]).to.equal(1);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
});
it('purchases gear', () => {
let type = 'gear';
let key = 'headAccessory_special_tigerEars';

View File

@@ -1,20 +0,0 @@
import clearPMs from '../../../website/common/script/ops/clearPMs';
import {
generateUser,
} from '../../helpers/common.helper';
describe('shared.ops.clearPMs', () => {
let user;
beforeEach(() => {
user = generateUser();
user.inbox.messages = { first: 'message', second: 'message' };
});
it('clears messages', () => {
expect(user.inbox.messages).to.not.eql({});
let [result] = clearPMs(user);
expect(user.inbox.messages).to.eql({});
expect(result).to.eql({});
});
});

View File

@@ -1,20 +0,0 @@
import deletePM from '../../../website/common/script/ops/deletePM';
import {
generateUser,
} from '../../helpers/common.helper';
describe('shared.ops.deletePM', () => {
let user;
beforeEach(() => {
user = generateUser();
user.inbox.messages = { first: 'message', second: 'message' };
});
it('delete message', () => {
expect(user.inbox.messages).to.not.eql({ second: 'message' });
let [response] = deletePM(user, { params: { id: 'first' } });
expect(user.inbox.messages).to.eql({ second: 'message' });
expect(response).to.eql({ second: 'message' });
});
});

38
test/common/ops/spells.js Normal file
View File

@@ -0,0 +1,38 @@
import {
generateUser,
} from '../../helpers/common.helper';
import spells from '../../../website/common/script/content/spells';
import {
NotAuthorized,
} from '../../../website/common/script/libs/errors';
import i18n from '../../../website/common/script/i18n';
// TODO complete the test suite...
describe('shared.ops.spells', () => {
let user;
beforeEach(() => {
user = generateUser();
});
it('returns an error when healer tries to cast Healing Light with full health', (done) => {
user.stats.class = 'healer';
user.stats.lvl = 11;
user.stats.hp = 50;
user.stats.mp = 200;
let spell = spells.healer.heal;
try {
spell.cast(user);
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('messageHealthAlreadyMax'));
expect(user.stats.hp).to.eql(50);
expect(user.stats.mp).to.eql(200);
done();
}
});
});

View File

@@ -13,7 +13,11 @@ describe('shared.ops.allocate', () => {
let user;
beforeEach(() => {
user = generateUser();
user = generateUser({
'stats.lvl': 10,
'flags.classSelected': true,
'preferences.disableClasses': false,
});
});
it('throws an error if an invalid attribute is supplied', (done) => {
@@ -28,6 +32,39 @@ describe('shared.ops.allocate', () => {
}
});
it('throws an error if the user is below lvl 10', (done) => {
user.stats.lvl = 9;
try {
allocate(user);
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('classNotSelected'));
done();
}
});
it('throws an error if the user hasn\'t selected class', (done) => {
user.flags.classSelected = false;
try {
allocate(user);
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('classNotSelected'));
done();
}
});
it('throws an error if the user has disabled classes', (done) => {
user.preferences.disableClasses = true;
try {
allocate(user);
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('classNotSelected'));
done();
}
});
it('throws an error if the user doesn\'t have attribute points', (done) => {
try {
allocate(user);

View File

@@ -13,7 +13,11 @@ describe('shared.ops.allocateBulk', () => {
let user;
beforeEach(() => {
user = generateUser();
user = generateUser({
'stats.lvl': 10,
'flags.classSelected': true,
'preferences.disableClasses': false,
});
});
it('throws an error if an invalid attribute is supplied', (done) => {
@@ -43,6 +47,60 @@ describe('shared.ops.allocateBulk', () => {
}
});
it('throws an error if the user is below lvl 10', (done) => {
user.stats.lvl = 9;
try {
allocateBulk(user, {
body: {
stats: {
int: 1,
str: 2,
},
},
});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('classNotSelected'));
done();
}
});
it('throws an error if the user hasn\'t selected class', (done) => {
user.flags.classSelected = false;
try {
allocateBulk(user, {
body: {
stats: {
int: 1,
str: 2,
},
},
});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('classNotSelected'));
done();
}
});
it('throws an error if the user has disabled classes', (done) => {
user.preferences.disableClasses = true;
try {
allocateBulk(user, {
body: {
stats: {
int: 1,
str: 2,
},
},
});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('classNotSelected'));
done();
}
});
it('throws an error if the user doesn\'t have attribute points', (done) => {
try {
allocateBulk(user, {

View File

@@ -62,7 +62,11 @@ module.exports = {
target: DEV_BASE_URL,
changeOrigin: true,
},
'/logout': {
'/logout-server': {
target: DEV_BASE_URL,
changeOrigin: true,
},
'/export': {
target: DEV_BASE_URL,
changeOrigin: true,
},

View File

@@ -1,21 +1,21 @@
# Running
For information about installing Habitica locally, see [Setting up Habitica Locally](http://habitica.wikia.com/wiki/Setting_up_Habitica_Locally) and for information about running the local client, refer to the ["Run Habitica" section](http://habitica.wikia.com/wiki/Setting_up_Habitica_Locally#Run_Habitica) in that page.
For information about installing and running Habitica locally, see [Setting up Habitica Locally](http://habitica.wikia.com/wiki/Setting_up_Habitica_Locally).
# Preparation Reading
- Vue 2 (https://vuejs.org)
- Webpack (https://webpack.github.io/) is the build system and it includes plugins for code transformation, right now we have: BabelJS for ES6 transpilation, eslint for code style, less and postcss for css compilation. The code comes from https://github.com/vuejs-templates/webpack which is a Webpack template for Vue, with some small modifications to adapt it to our use case. Docs http://vuejs-templates.github.io/webpack/
- Were using `.vue` files that make it possible to have HTML, JS and CSS for each component together in a single location. Theyre implemented as a webpack plugin and the docs can be found here http://vue-loader.vuejs.org/en/
- We're using `.vue` files that make it possible to have HTML, JS and CSS for each component together in a single location. They're implemented as a webpack plugin and the docs can be found here http://vue-loader.vuejs.org/en/
- SemanticUI is the UI framework http://semantic-ui.com/. So far Ive only used the CSS part, it also has JS plugins but Ive yet to use them. It supports theming so if its not too difficult well want to customize the base theme with our own styles instead of writing CSS rules to override the original styling.
- SemanticUI is the UI framework http://semantic-ui.com/. So far I've only used the CSS part, it also has JS plugins but I've yet to use them. It supports theming so if it's not too difficult we'll want to customize the base theme with our own styles instead of writing CSS rules to override the original styling.
The code is in `/website/client`. Were using something very similar to Vuex (equivalent of Reacts Redux) for state management http://vuex.vuejs.org/en/index.html
The code is in `/website/client`. We're using something very similar to Vuex (equivalent of React's Redux) for state management http://vuex.vuejs.org/en/index.html
The API is almost the same except that we dont use mutations but only actions because it would make it difficult to work with common code
The API is almost the same except that we don't use mutations but only actions because it would make it difficult to work with common code
The project is developed directly in the `develop` branch as long as well be able to avoid splitting it into a different branch.
The project is developed directly in the `develop` branch as long as we'll be able to avoid splitting it into a different branch.
So far most of the work has been on the template, so theres no complex logic to understand. The only thing I would suggest you to read about is Vuex for data management: its basically a Flux implementation: theres a central store that hold the data for the entire app, and every change to the data must happen through an action, the data cannot be mutated directly.
So far most of the work has been on the template, so there's no complex logic to understand. The only thing I would suggest you to read about is Vuex for data management: it's basically a Flux implementation: there's a central store that hold the data for the entire app, and every change to the data must happen through an action, the data cannot be mutated directly.
For further resources, see [Guidance for Blacksmiths](http://habitica.wikia.com/wiki/Guidance_for_Blacksmiths), and in particular the ["Website Technology Stack" section](http://habitica.wikia.com/wiki/Guidance_for_Blacksmiths#Website_Technology_Stack).

View File

@@ -29,7 +29,6 @@ div
buyModal(
:item="selectedItemToBuy || {}",
:withPin="true",
@change="resetItemToBuy($event)",
@buyPressed="customPurchase($event)",
:genericPurchase="genericPurchase(selectedItemToBuy)",
@@ -105,8 +104,8 @@ div
@import '~client/assets/scss/colors.scss';
/* @TODO: The modal-open class is not being removed. Let's try this for now */
.modal, .modal-open {
overflow-y: scroll;
.modal {
overflow-y: scroll !important;
}
.modal-backdrop.show {
@@ -116,7 +115,7 @@ div
/* Push progress bar above modals */
#nprogress .bar {
z-index: 1090 !important; /* Must stay above nav bar */
z-index: 1600 !important; /* Must stay above nav bar */
}
.restingInn {
@@ -136,7 +135,7 @@ div
background-color: $blue-10;
position: fixed;
top: 0;
z-index: 1030;
z-index: 1300;
display: flex;
.content {
@@ -331,9 +330,33 @@ export default {
];
if (notificationNotFoundMessage.indexOf(errorMessage) !== -1) snackbarTimeout = true;
let errorsToShow = [];
let usernameCheck = false;
let emailCheck = false;
let passwordCheck = false;
// show only the first error for each param
if (errorData.errors) {
for (let e of errorData.errors) {
if (!usernameCheck && e.param === 'username') {
errorsToShow.push(e.message);
usernameCheck = true;
}
if (!emailCheck && e.param === 'email') {
errorsToShow.push(e.message);
emailCheck = true;
}
if (!passwordCheck && e.param === 'password') {
errorsToShow.push(e.message);
passwordCheck = true;
}
}
} else {
errorsToShow.push(errorMessage);
}
// dispatch as one snackbar notification
this.$store.dispatch('snackbars:add', {
title: 'Habitica',
text: errorMessage,
text: errorsToShow.join(' '),
type: 'error',
timeout: snackbarTimeout,
});
@@ -475,8 +498,16 @@ export default {
});
this.$root.$on('bv::modal::hidden', (bvEvent) => {
const modalId = bvEvent.target && bvEvent.target.id;
if (!modalId) return;
let modalId = bvEvent.target && bvEvent.target.id;
// sometimes the target isn't passed to the hidden event, fallback is the vueTarget
if (!modalId) {
modalId = bvEvent.vueTarget && bvEvent.vueTarget.id;
}
if (!modalId) {
return;
}
const modalStack = this.$store.state.modalStack;
@@ -493,6 +524,7 @@ export default {
// Get previous modal
const modalBefore = modalOnTop ? modalOnTop.prev : undefined;
if (modalBefore) this.$root.$emit('bv::show::modal', modalBefore, {fromRoot: true});
});
},
@@ -534,13 +566,6 @@ export default {
});
}
},
resetItemToBuy ($event) {
// @TODO: Do we need this? I think selecting a new item
// overwrites. @negue might know
if (!$event && this.selectedItemToBuy.purchaseType !== 'card') {
this.selectedItemToBuy = null;
}
},
itemSelected (item) {
this.selectedItemToBuy = item;
},
@@ -626,3 +651,4 @@ export default {
<style src="assets/css/sprites/spritesmith-main-21.css"></style>
<style src="assets/css/sprites/spritesmith-main-22.css"></style>
<style src="assets/css/sprites.css"></style>
<style src="smartbanner.js/dist/smartbanner.min.css"></style>

View File

@@ -1,54 +1,78 @@
.promo_armoire_backgrounds_201806 {
.promo_animal_tails {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -627px 0px;
background-position: -284px -699px;
width: 141px;
height: 441px;
}
.promo_bundle_aquaticAmigos {
.promo_armoire_backgrounds_201809 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -142px -533px;
background-position: 0px -699px;
width: 141px;
height: 441px;
}
.promo_ember_potions {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -142px -699px;
width: 141px;
height: 441px;
}
.promo_fall_festival_2017 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -788px 0px;
width: 414px;
height: 210px;
}
.promo_fall_festival_2018 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -788px -211px;
width: 393px;
height: 213px;
}
.promo_forest_friends_bundle {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -426px -699px;
width: 423px;
height: 147px;
}
.promo_ios {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -337px;
width: 375px;
height: 361px;
}
.promo_kangaroo {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px 0px;
width: 325px;
width: 420px;
height: 336px;
}
.promo_mystery_201806 {
.promo_mystery_201808 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -627px -442px;
width: 121px;
height: 114px;
background-position: -421px -286px;
width: 78px;
height: 81px;
}
.promo_seasonal_shop_summer {
.promo_seasonal_shop {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -142px -681px;
background-position: -969px -425px;
width: 162px;
height: 138px;
}
.promo_summer_splash_2018 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -337px;
width: 141px;
height: 588px;
}
.promo_take_this {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -627px -557px;
background-position: -788px -606px;
width: 96px;
height: 69px;
}
.scene_families {
.promo_unconventional_armor {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -142px -337px;
width: 345px;
height: 195px;
background-position: -788px -425px;
width: 180px;
height: 180px;
}
.scene_moderators {
.scene_tools {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -326px 0px;
width: 300px;
height: 300px;
background-position: -421px 0px;
width: 366px;
height: 285px;
}

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.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

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