Merge branch 'develop' of https://github.com/HabitRPG/habitica into autocomplete-username

# Conflicts:
#	package.json
#	website/client/components/chat/autoComplete.vue
#	website/client/components/chat/chatCard.vue
#	website/client/components/groups/chat.vue
#	website/server/controllers/api-v3/chat.js
#	website/server/controllers/api-v3/members.js
#	website/server/controllers/api-v4/members.js
This commit is contained in:
Phillip Thelen
2019-09-19 16:08:13 +02:00
1570 changed files with 75816 additions and 62017 deletions

View File

@@ -13,3 +13,8 @@ Habitica uses [Trello](https://trello.com/b/EpoYEYod/habitica) to track feature
# Contributing Code # Contributing Code
See [Contributing to Habitica](http://habitica.fandom.com/wiki/Contributing_to_Habitica#Coders_.28Web_.26_Mobile.29) See [Contributing to Habitica](http://habitica.fandom.com/wiki/Contributing_to_Habitica#Coders_.28Web_.26_Mobile.29)
## Issue Triage [![Open Source Helpers](https://www.codetriage.com/habitrpg/habitica/badges/users.svg)](https://www.codetriage.com/habitrpg/habitica)
You can triage issues which may include reproducing bug reports or asking for vital information, such as version numbers or reproduction instructions. If you would like to start triaging issues, one easy way to get started is to [subscribe to habitrpg on CodeTriage](https://www.codetriage.com/habitrpg/habitica).

1
.gitignore vendored
View File

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

2
.nvmrc
View File

@@ -1 +1 @@
10 12

View File

@@ -1,6 +1,6 @@
language: node_js language: node_js
node_js: node_js:
- '10' - '12'
services: services:
- mongodb - mongodb
cache: cache:
@@ -14,7 +14,6 @@ before_script:
- sleep 5 - sleep 5
script: script:
- npm run $TEST - npm run $TEST
- if [ $COVERAGE ]; then ./node_modules/.bin/lcov-result-merger 'coverage/**/*.info' | ./node_modules/coveralls/bin/coveralls.js; fi
env: env:
global: global:
- DISABLE_REQUEST_LOGGING=true - DISABLE_REQUEST_LOGGING=true

View File

@@ -1,4 +1,4 @@
FROM node:10 FROM node:12
ENV ADMIN_EMAIL admin@habitica.com ENV ADMIN_EMAIL admin@habitica.com
ENV AMAZON_PAYMENTS_CLIENT_ID amzn1.application-oa2-client.68ed9e6904ef438fbc1bf86bf494056e ENV AMAZON_PAYMENTS_CLIENT_ID amzn1.application-oa2-client.68ed9e6904ef438fbc1bf86bf494056e
@@ -18,7 +18,7 @@ RUN npm install -g gulp-cli mocha
# Clone Habitica repo and install dependencies # Clone Habitica repo and install dependencies
RUN mkdir -p /usr/src/habitrpg RUN mkdir -p /usr/src/habitrpg
WORKDIR /usr/src/habitrpg WORKDIR /usr/src/habitrpg
RUN git clone --branch release https://github.com/HabitRPG/habitica.git /usr/src/habitrpg RUN git clone --branch release --depth 1 https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
RUN npm install RUN npm install
RUN gulp build:prod --force RUN gulp build:prod --force

View File

@@ -1,18 +1,5 @@
FROM node:10 FROM node:12
WORKDIR /code
# Install global packages COPY package*.json /code/
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 RUN npm install
RUN npm install -g gulp-cli mocha
# Create Build dir
RUN mkdir -p ./website/build
# Start Habitica
EXPOSE 3000
CMD ["npm", "start"]

View File

@@ -1,4 +1,4 @@
Habitica [![Build Status](https://travis-ci.org/HabitRPG/habitica.svg?branch=develop)](https://travis-ci.org/HabitRPG/habitica) [![Code Climate](https://codeclimate.com/github/HabitRPG/habitrpg.svg)](https://codeclimate.com/github/HabitRPG/habitrpg) [![Coverage Status](https://coveralls.io/repos/github/HabitRPG/habitica/badge.svg?branch=develop)](https://coveralls.io/github/HabitRPG/habitica?branch=develop) [![Bountysource](https://api.bountysource.com/badge/tracker?tracker_id=68393)](https://www.bountysource.com/trackers/68393-habitrpg?utm_source=68393&utm_medium=shield&utm_campaign=TRACKER_BADGE) [![Open Source Helpers](https://www.codetriage.com/habitrpg/habitica/badges/users.svg)](https://www.codetriage.com/habitrpg/habitica) Habitica [![Build Status](https://travis-ci.org/HabitRPG/habitica.svg?branch=develop)](https://travis-ci.org/HabitRPG/habitica) [![Code Climate](https://codeclimate.com/github/HabitRPG/habitrpg.svg)](https://codeclimate.com/github/HabitRPG/habitrpg) [![Bountysource](https://api.bountysource.com/badge/tracker?tracker_id=68393)](https://www.bountysource.com/trackers/68393-habitrpg?utm_source=68393&utm_medium=shield&utm_campaign=TRACKER_BADGE) [![Open Source Helpers](https://www.codetriage.com/habitrpg/habitica/badges/users.svg)](https://www.codetriage.com/habitrpg/habitica)
=============== ===============
[![Greenkeeper badge](https://badges.greenkeeper.io/HabitRPG/habitica.svg)](https://greenkeeper.io/) [![Greenkeeper badge](https://badges.greenkeeper.io/HabitRPG/habitica.svg)](https://greenkeeper.io/)

View File

@@ -61,22 +61,17 @@
"SESSION_SECRET_IV": "12345678912345678912345678912345", "SESSION_SECRET_IV": "12345678912345678912345678912345",
"SESSION_SECRET_KEY": "1234567891234567891234567891234567891234567891234567891234567891", "SESSION_SECRET_KEY": "1234567891234567891234567891234567891234567891234567891234567891",
"SITE_HTTP_AUTH_ENABLED": "false", "SITE_HTTP_AUTH_ENABLED": "false",
"SITE_HTTP_AUTH_PASSWORD": "password", "SITE_HTTP_AUTH_PASSWORDS": "password,wordpass,passkey",
"SITE_HTTP_AUTH_USERNAME": "admin", "SITE_HTTP_AUTH_USERNAMES": "admin,tester,contributor",
"SLACK_FLAGGING_FOOTER_LINK": "https://habitrpg.github.io/flag-o-rama/", "SLACK_FLAGGING_FOOTER_LINK": "https://habitrpg.github.io/flag-o-rama/",
"SLACK_FLAGGING_URL": "https://hooks.slack.com/services/id/id/id", "SLACK_FLAGGING_URL": "https://hooks.slack.com/services/id/id/id",
"SLACK_SUBSCRIPTIONS_URL": "https://hooks.slack.com/services/id/id/id", "SLACK_SUBSCRIPTIONS_URL": "https://hooks.slack.com/services/id/id/id",
"SLACK_URL": "https://hooks.slack.com/services/some-url", "SLACK_URL": "https://hooks.slack.com/services/some-url",
"SMTP_HOST": "example.com",
"SMTP_PASS": "password",
"SMTP_PORT": 587,
"SMTP_SERVICE": "Gmail",
"SMTP_TLS": "true",
"SMTP_USER": "user@example.com",
"STRIPE_API_KEY": "aaaabbbbccccddddeeeeffff00001111", "STRIPE_API_KEY": "aaaabbbbccccddddeeeeffff00001111",
"STRIPE_PUB_KEY": "22223333444455556666777788889999", "STRIPE_PUB_KEY": "22223333444455556666777788889999",
"TEST_DB_URI": "mongodb://localhost/habitrpg_test", "TEST_DB_URI": "mongodb://localhost/habitrpg_test",
"TRANSIFEX_SLACK_CHANNEL": "transifex", "TRANSIFEX_SLACK_CHANNEL": "transifex",
"WEB_CONCURRENCY": 1, "WEB_CONCURRENCY": 1,
"SKIP_SSL_CHECK_KEY": "key" "SKIP_SSL_CHECK_KEY": "key",
"ENABLE_STACKDRIVER_TRACING": "false"
} }

View File

@@ -1,14 +1,45 @@
version: "3" version: "3"
services: services:
client: client:
build:
context: .
dockerfile: ./Dockerfile-Dev
command: ["npm", "run", "client:dev"]
depends_on:
- server
environment: environment:
- NODE_ENV=development - BASE_URL=http://server:3000
image: habitica
networks:
- habitica
ports:
- "8080:8080"
volumes: volumes:
- '.:/usr/src/habitrpg' - .:/code
- /code/node_modules
server: server:
build:
context: .
dockerfile: ./Dockerfile-Dev
command: ["npm", "start"]
depends_on:
- mongo
environment: environment:
- NODE_ENV=development - NODE_DB_URI=mongodb://mongo/habitrpg
image: habitica
networks:
- habitica
ports:
- "3000:3000"
volumes: volumes:
- '.:/usr/src/habitrpg' - .:/code
- /code/node_modules
mongo:
image: mongo:3.4
networks:
- habitica
ports:
- "27017:27017"
networks:
habitica:
driver: bridge

View File

@@ -16,7 +16,7 @@ const IMG_DIST_PATH = 'website/client/assets/images/sprites/';
const CSS_DIST_PATH = 'website/client/assets/css/sprites/'; const CSS_DIST_PATH = 'website/client/assets/css/sprites/';
function checkForSpecialTreatment (name) { function checkForSpecialTreatment (name) {
let regex = /^hair|skin|beard|mustach|shirt|flower|^headAccessory_special_\w+Ears|^eyewear_special_\w+TopFrame/; let regex = /^hair|skin|beard|mustach|shirt|flower|^headAccessory_special_\w+Ears|^eyewear_special_\w+TopFrame|^eyewear_special_\w+HalfMoon/;
return name.match(regex) || name === 'head_0'; return name.match(regex) || name === 'head_0';
} }

View File

@@ -0,0 +1,62 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20190530_halfmoon_glasses';
import { v4 as uuid } from 'uuid';
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
const set = {
'items.gear.owned.eyewear_special_blackHalfMoon': true,
'items.gear.owned.eyewear_special_blueHalfMoon': true,
'items.gear.owned.eyewear_special_greenHalfMoon': true,
'items.gear.owned.eyewear_special_pinkHalfMoon': true,
'items.gear.owned.eyewear_special_redHalfMoon': true,
'items.gear.owned.eyewear_special_whiteHalfMoon': true,
'items.gear.owned.eyewear_special_yellowHalfMoon': true,
};
set.migration = MIGRATION_NAME;
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.update({_id: user._id}, {$set: set}).exec();
}
module.exports = async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
'auth.timestamps.loggedin': {$gt: new Date('2019-05-01')},
};
const fields = {
_id: 1,
items: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.lean()
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1],
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -0,0 +1,59 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20190618_summer_splash_orcas';
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
let set;
if (user && user.items && user.items.pets && typeof user.items.pets['Orca-Base'] !== 'undefined') {
set = { migration: MIGRATION_NAME };
} else if (user && user.items && user.items.mounts && typeof user.items.mounts['Orca-Base'] !== 'undefined') {
set = { migration: MIGRATION_NAME, 'items.pets.Orca-Base': 5 };
} else {
set = { migration: MIGRATION_NAME, 'items.mounts.Orca-Base': true };
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.update({ _id: user._id }, { $set: set }).exec();
}
module.exports = async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
'auth.timestamps.loggedin': {$gt: new Date('2019-05-18')},
};
const fields = {
_id: 1,
items: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.lean()
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1],
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -0,0 +1,84 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20190716_groups_fix';
import monk from 'monk';
import nconf from 'nconf';
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING');
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
let backupUsers;
async function updateUser (user) {
count++;
let set = { migration: MIGRATION_NAME };
let addToSet;
const monkPromise = new Promise((resolve, reject) => {
backupUsers.findOne(
{ _id: user._id },
{ fields: { _id: 1, party: 1, guilds: 1 }},
).then(foundUserInBackup => {
resolve(foundUserInBackup);
}).catch(e => {
reject(e);
})
});
let backupUser = await monkPromise;
if (!backupUser) return;
if (!user.party._id) {
set.party = backupUser.party;
}
addToSet = { guilds: { $each: backupUser.guilds }};
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return User.update({ _id: user._id }, { $set: set, $addToSet: addToSet }).exec();
}
module.exports = async function processUsers () {
const query = {
'auth.timestamps.loggedin': {$gt: new Date('2019-07-15')},
};
let backupDb = monk(CONNECTION_STRING);
const backupDbPromise = new Promise((resolve, reject) => {
backupDb.then(() => resolve()).catch((e) => reject(e));
});
await backupDbPromise;
console.log('Connected to backup db');
backupUsers = backupDb.get('users', { castIds: false });
const fields = {
_id: 1,
party: 1,
guilds: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.lean()
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1],
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -0,0 +1,88 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20190717_groups_fix_2';
import monk from 'monk';
import nconf from 'nconf';
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING');
import { model as User } from '../../../website/server/models/user';
import { sendTxn as sendTxnEmail } from '../../../website/server/libs/email';
import shared from '../../../website/common';
const questScrolls = shared.content.quests;
const progressCount = 1000;
let count = 0;
async function updateGroup (group) {
count++;
if (group && group.quest && group.quest.key && group.quest.leader) {
const quest = questScrolls[group.quest.key];
const leader = await User.findOne({_id: group.quest.leader}).exec();
if (leader && quest) {
await User.update({
_id: leader._id,
migration: {$ne: MIGRATION_NAME},
}, {
$set: {migration: MIGRATION_NAME},
$inc: {
balance: 1,
[`items.quests.${group.quest.key}`]: 1,
},
}).exec();
// unsubscribe from all is already checked by sendTxnEmail
if (leader.preferences && leader.preferences.emailNotifications && leader.preferences.emailNotifications.majorUpdates !== false) {
sendTxnEmail(leader, 'groups-outage');
}
}
}
if (count % progressCount === 0) console.warn(`${count} ${group._id}`);
}
module.exports = async function processUsers () {
const query = {
type: 'party'
};
let backupDb = monk(CONNECTION_STRING);
const backupDbPromise = new Promise((resolve, reject) => {
backupDb.then(() => resolve()).catch((e) => reject(e));
});
await backupDbPromise;
console.log('Connected to backup db');
const backupGroups = backupDb.get('groups', { castIds: false });
while (true) { // eslint-disable-line no-constant-condition
const groupsPromise = new Promise((resolve, reject) => {
backupGroups
.find(query, {
limit: 250,
sort: {_id: 1}
})
.then(foundGroupInBackup => {
resolve(foundGroupInBackup);
}).catch(e => {
reject(e);
});
});
const groups = await groupsPromise;
if (groups.length === 0) {
console.warn('All appropriate groups found and modified.');
console.warn(`\n${count} groups processed\n`);
break;
} else {
query._id = {
$gt: groups[groups.length - 1]._id,
};
}
await Promise.all(groups.map(updateGroup)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -0,0 +1,84 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20190731_naming_day';
import { v4 as uuid } from 'uuid';
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
let set;
let push;
const inc = {
'items.food.Cake_Base': 1,
'items.food.Cake_CottonCandyBlue': 1,
'items.food.Cake_CottonCandyPink': 1,
'items.food.Cake_Desert': 1,
'items.food.Cake_Golden': 1,
'items.food.Cake_Red': 1,
'items.food.Cake_Shade': 1,
'items.food.Cake_Skeleton': 1,
'items.food.Cake_White': 1,
'items.food.Cake_Zombie': 1,
'achievements.habiticaDays': 1,
};
if (user && user.items && user.items.gear && user.items.gear.owned && typeof user.items.gear.owned.body_special_namingDay2018 !== 'undefined') {
set = { migration: MIGRATION_NAME };
} else if (user && user.items && user.items.gear && user.items.gear.owned && typeof user.items.gear.owned.head_special_namingDay2017 !== 'undefined') {
set = { migration: MIGRATION_NAME, 'items.gear.owned.body_special_namingDay2018': false };
push = { pinnedItems: { type: 'marketGear', path: 'gear.flat.body_special_namingDay2018', _id: uuid() }};
} else if (user && user.items && user.items.pets && typeof user.items.pets['Gryphon-RoyalPurple'] !== 'undefined') {
set = { migration: MIGRATION_NAME, 'items.gear.owned.head_special_namingDay2017': false };
push = { pinnedItems: { type: 'marketGear', path: 'gear.flat.head_special_namingDay2017', _id: uuid() }};
} else if (user && user.items && user.items.mounts && typeof user.items.mounts['Gryphon-RoyalPurple'] !== 'undefined') {
set = { migration: MIGRATION_NAME, 'items.pets.Gryphon-RoyalPurple': 5 };
} else {
set = { migration: MIGRATION_NAME, 'items.mounts.Gryphon-RoyalPurple': true };
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
if (push) {
return await User.update({ _id: user._id }, { $set: set, $inc: inc, $push: push }).exec();
} else {
return await User.update({ _id: user._id }, { $set: set, $inc: inc }).exec();
}
}
module.exports = async function processUsers () {
let query = {
migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2019-07-01') },
};
const fields = {
_id: 1,
items: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.lean()
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1]._id,
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -0,0 +1,104 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20190917_pet_color_achievements';
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
let set = {
migration: MIGRATION_NAME,
};
if (user && user.items && user.items.pets) {
const pets = user.items.pets;
if (pets['Wolf-Base'] > 0
&& pets['TigerCub-Base'] > 0
&& pets['PandaCub-Base'] > 0
&& pets['LionCub-Base'] > 0
&& pets['Fox-Base'] > 0
&& pets['FlyingPig-Base'] > 0
&& pets['Dragon-Base'] > 0
&& pets['Cactus-Base'] > 0
&& pets['BearCub-Base'] > 0) {
set['achievements.backToBasics'] = true;
}
if (pets['Wolf-Desert'] > 0
&& pets['TigerCub-Desert'] > 0
&& pets['PandaCub-Desert'] > 0
&& pets['LionCub-Desert'] > 0
&& pets['Fox-Desert'] > 0
&& pets['FlyingPig-Desert'] > 0
&& pets['Dragon-Desert'] > 0
&& pets['Cactus-Desert'] > 0
&& pets['BearCub-Desert'] > 0) {
set['achievements.dustDevil'] = true;
}
}
if (user && user.items && user.items.mounts) {
const mounts = user.items.mounts;
if (mounts['Wolf-Base']
&& mounts['TigerCub-Base']
&& mounts['PandaCub-Base']
&& mounts['LionCub-Base']
&& mounts['Fox-Base']
&& mounts['FlyingPig-Base']
&& mounts['Dragon-Base']
&& mounts['Cactus-Base']
&& mounts['BearCub-Base'] ) {
set['achievements.allYourBase'] = true;
}
if (mounts['Wolf-Desert']
&& mounts['TigerCub-Desert']
&& mounts['PandaCub-Desert']
&& mounts['LionCub-Desert']
&& mounts['Fox-Desert']
&& mounts['FlyingPig-Desert']
&& mounts['Dragon-Desert']
&& mounts['Cactus-Desert']
&& mounts['BearCub-Desert'] ) {
set['achievements.aridAuthority'] = true;
}
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.update({ _id: user._id }, { $set: set }).exec();
}
module.exports = async function processUsers () {
let query = {
migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2019-09-01') },
};
const fields = {
_id: 1,
items: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.lean()
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1]._id,
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -17,7 +17,7 @@ function setUpServer () {
setUpServer(); setUpServer();
// Replace this with your migration // Replace this with your migration
const processUsers = require('./users/mystery-items.js'); const processUsers = require('');
processUsers() processUsers()
.then(function success () { .then(function success () {
process.exit(0); process.exit(0);

View File

@@ -1,5 +1,5 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
import { sendTxn } from '../../../website/server/libs/email'; import { sendTxn } from '../../website/server/libs/email';
import { model as User } from '../../website/server/models/user'; import { model as User } from '../../website/server/models/user';
import moment from 'moment'; import moment from 'moment';
import nconf from 'nconf'; import nconf from 'nconf';

View File

@@ -1,67 +1,24 @@
/* eslint-disable no-console */
const MIGRATION_NAME = 'full-stable';
import each from 'lodash/each'; import each from 'lodash/each';
import keys from 'lodash/keys'; import keys from 'lodash/keys';
import content from '../../website/common/script/content/index'; import content from '../../website/common/script/content/index';
const migrationName = 'full-stable.js';
const authorName = 'Sabe'; // in case script author needs to know when their ... import { model as User } from '../../website/server/models/user';
const authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
const progressCount = 1000;
let count = 0;
/* /*
* Award users every extant pet and mount * Award users every extant pet and mount
*/ */
const connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
let monk = require('monk'); async function updateUser (user) {
let dbUsers = monk(connectionString).get('users', { castIds: false });
function processUsers (lastId) {
// specify a query to limit the affected users (empty for all users):
let query = {
'profile.name': 'SabreCat',
};
if (lastId) {
query._id = {
$gt: lastId,
};
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 250,
fields: [
], // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
})
.then(updateUsers)
.catch((err) => {
console.log(err);
return exiting(1, `ERROR! ${ err}`);
});
}
let progressCount = 1000;
let count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
displayData();
return;
}
let userPromises = users.map(updateUser);
let lastUser = users[users.length - 1];
return Promise.all(userPromises)
.then(() => {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++; count++;
let set = {
migration: migrationName, const set = {};
};
set.migration = MIGRATION_NAME;
each(keys(content.pets), (pet) => { each(keys(content.pets), (pet) => {
set[`items.pets.${pet}`] = 5; set[`items.pets.${pet}`] = 5;
@@ -88,30 +45,40 @@ function updateUser (user) {
set[`items.mounts.${mount}`] = true; set[`items.mounts.${mount}`] = true;
}); });
dbUsers.update({_id: user._id}, {$set: set}); if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
if (count % progressCount === 0) console.warn(`${count } ${ user._id}`); return await User.update({_id: user._id}, {$set: set}).exec();
if (user._id === authorUuid) console.warn(`${authorName } processed`);
} }
function displayData () { module.exports = async function processUsers () {
console.warn(`\n${ count } users processed\n`); let query = {
return exiting(0); migration: {$ne: MIGRATION_NAME},
} 'auth.local.username': 'olson22',
};
function exiting (code, msg) { const fields = {
code = code || 0; // 0 = success _id: 1,
if (code && !msg) { };
msg = 'ERROR!';
} while (true) { // eslint-disable-line no-constant-condition
if (msg) { const users = await User // eslint-disable-line no-await-in-loop
if (code) { .find(query)
console.error(msg); .limit(250)
.sort({_id: 1})
.select(fields)
.lean()
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else { } else {
console.log(msg); query._id = {
$gt: users[users.length - 1],
};
} }
}
process.exit(code);
}
module.exports = processUsers; await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
const MIGRATION_NAME = 'mystery_items_201901'; const MIGRATION_NAME = 'mystery_items_201908';
const MYSTERY_ITEMS = ['head_mystery_201901', 'body_mystery_201901']; const MYSTERY_ITEMS = ['armor_mystery_201908', 'headAccessory_mystery_201908'];
import { model as User } from '../../website/server/models/user'; import { model as User } from '../../website/server/models/user';
import { model as UserNotification } from '../../website/server/models/userNotification'; import { model as UserNotification } from '../../website/server/models/userNotification';

View File

@@ -0,0 +1,73 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20190314_pi_day';
import { v4 as uuid } from 'uuid';
import { model as User } from '../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
const inc = {
'items.food.Pie_Skeleton': 1,
'items.food.Pie_Base': 1,
'items.food.Pie_CottonCandyBlue': 1,
'items.food.Pie_CottonCandyPink': 1,
'items.food.Pie_Shade': 1,
'items.food.Pie_White': 1,
'items.food.Pie_Golden': 1,
'items.food.Pie_Zombie': 1,
'items.food.Pie_Desert': 1,
'items.food.Pie_Red': 1,
};
const set = {};
set.migration = MIGRATION_NAME;
set['items.gear.owned.head_special_piDay'] = false;
set['items.gear.owned.shield_special_piDay'] = false;
const push = [
{type: 'marketGear', path: 'gear.flat.head_special_piDay', _id: uuid()},
{type: 'marketGear', path: 'gear.flat.shield_special_piDay', _id: uuid()},
];
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.update({_id: user._id}, {$inc: inc, $set: set, $push: {pinnedItems: {$each: push}}}).exec();
}
module.exports = async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
'auth.timestamps.loggedin': {$gt: new Date('2019-02-15')},
};
const fields = {
_id: 1,
items: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.lean()
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1],
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

17534
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,20 @@
{ {
"name": "habitica", "name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.", "description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "4.84.0", "version": "4.112.0",
"main": "./website/server/index.js", "main": "./website/server/index.js",
"dependencies": { "dependencies": {
"@google-cloud/trace-agent": "^4.0.0",
"@slack/client": "^3.8.1", "@slack/client": "^3.8.1",
"accepts": "^1.3.5", "accepts": "^1.3.5",
"amazon-payments": "^0.2.7", "amazon-payments": "^0.2.7",
"amplitude": "^3.5.0", "amplitude": "^3.5.0",
"amplitude-js": "^4.6.0-beta.2", "amplitude-js": "^5.2.2",
"apidoc": "^0.17.5", "apidoc": "^0.17.5",
"apn": "^2.2.0", "apn": "^2.2.0",
"autoprefixer": "^8.5.0", "autoprefixer": "^9.4.0",
"aws-sdk": "^2.400.0", "aws-sdk": "^2.432.0",
"axios": "^0.18.0", "axios": "^0.19.0",
"axios-progress-bar": "^1.2.0", "axios-progress-bar": "^1.2.0",
"babel-core": "^6.26.3", "babel-core": "^6.26.3",
"babel-eslint": "^8.2.3", "babel-eslint": "^8.2.3",
@@ -27,16 +28,16 @@
"babel-preset-es2015": "^6.6.0", "babel-preset-es2015": "^6.6.0",
"babel-register": "^6.6.0", "babel-register": "^6.6.0",
"babel-runtime": "^6.11.6", "babel-runtime": "^6.11.6",
"bcrypt": "^3.0.1", "bcrypt": "^3.0.6",
"body-parser": "^1.18.3", "body-parser": "^1.18.3",
"bootstrap": "^4.1.1", "bootstrap": "^4.1.1",
"bootstrap-vue": "^2.0.0-rc.9", "bootstrap-vue": "^2.0.0-rc.18",
"compression": "^1.7.2", "compression": "^1.7.4",
"cookie-session": "^1.2.0", "cookie-session": "^1.3.3",
"coupon-code": "^0.4.5", "coupon-code": "^0.4.5",
"cross-env": "^5.2.0", "cross-env": "^5.2.0",
"css-loader": "^0.28.11", "css-loader": "^0.28.11",
"csv-stringify": "^4.3.1", "csv-stringify": "^5.1.0",
"cwait": "^1.1.1", "cwait": "^1.1.1",
"domain-middleware": "~0.1.0", "domain-middleware": "~0.1.0",
"express": "^4.16.3", "express": "^4.16.3",
@@ -47,30 +48,29 @@
"got": "^9.0.0", "got": "^9.0.0",
"gulp": "^4.0.0", "gulp": "^4.0.0",
"gulp-babel": "^7.0.1", "gulp-babel": "^7.0.1",
"gulp-imagemin": "^5.0.3", "gulp-imagemin": "^6.0.0",
"gulp-nodemon": "^2.4.1", "gulp-nodemon": "^2.4.1",
"gulp.spritesmith": "^6.9.0", "gulp.spritesmith": "^6.9.0",
"habitica-markdown": "^1.3.0", "habitica-markdown": "^1.3.0",
"hellojs": "^1.15.1", "hellojs": "^1.18.1",
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",
"image-size": "^0.6.2", "image-size": "^0.7.0",
"in-app-purchase": "^1.10.2", "in-app-purchase": "^1.11.3",
"intro.js": "^2.9.3", "intro.js": "^2.9.3",
"jquery": ">=3.0.0", "jquery": ">=3.0.0",
"js2xmlparser": "^3.0.0", "js2xmlparser": "^4.0.0",
"lodash": "^4.17.10", "lodash": "^4.17.10",
"merge-stream": "^1.0.0", "merge-stream": "^2.0.0",
"method-override": "^3.0.0", "method-override": "^3.0.0",
"moment": "^2.22.1", "moment": "^2.22.1",
"moment-recur": "^1.0.7", "moment-recur": "^1.0.7",
"mongoose": "^5.4.11", "mongoose": "^5.6.9",
"morgan": "^1.7.0", "morgan": "^1.7.0",
"nconf": "^0.10.0", "nconf": "^0.10.0",
"node-gcm": "^1.0.2", "node-gcm": "^1.0.2",
"node-sass": "^4.9.0", "node-sass": "^4.12.0",
"nodemailer": "^5.0.0", "ora": "^3.2.0",
"ora": "^3.0.0", "pageres": "^5.1.0",
"pageres": "^4.1.1",
"passport": "^0.4.0", "passport": "^0.4.0",
"passport-facebook": "^2.0.0", "passport-facebook": "^2.0.0",
"passport-google-oauth20": "1.0.0", "passport-google-oauth20": "1.0.0",
@@ -80,16 +80,17 @@
"postcss-easy-import": "^3.0.0", "postcss-easy-import": "^3.0.0",
"ps-tree": "^1.0.0", "ps-tree": "^1.0.0",
"pug": "^2.0.3", "pug": "^2.0.3",
"regenerator-runtime": "^0.13.3",
"rimraf": "^2.4.3", "rimraf": "^2.4.3",
"sass-loader": "^7.0.3", "sass-loader": "^7.0.3",
"shelljs": "^0.8.2", "shelljs": "^0.8.2",
"short-uuid": "^3.0.0", "short-uuid": "^3.0.0",
"smartbanner.js": "^1.9.1", "smartbanner.js": "^1.11.0",
"stripe": "^5.9.0", "stripe": "^5.9.0",
"superagent": "^4.0.0", "superagent": "^5.0.2",
"svg-inline-loader": "^0.8.0", "svg-inline-loader": "^0.8.0",
"svg-url-loader": "^2.3.2", "svg-url-loader": "^3.0.0",
"svgo": "^1.0.5", "svgo": "^1.2.0",
"svgo-loader": "^2.1.0", "svgo-loader": "^2.1.0",
"tributejs": "^3.4.0", "tributejs": "^3.4.0",
"universal-analytics": "^0.4.17", "universal-analytics": "^0.4.17",
@@ -98,16 +99,16 @@
"url-loader": "^1.0.0", "url-loader": "^1.0.0",
"useragent": "^2.1.9", "useragent": "^2.1.9",
"uuid": "^3.0.1", "uuid": "^3.0.1",
"validator": "^10.5.0", "validator": "^11.0.0",
"vinyl-buffer": "^1.0.1", "vinyl-buffer": "^1.0.1",
"vue": "^2.6.4", "vue": "^2.6.10",
"vue-loader": "^14.2.2", "vue-loader": "^14.2.2",
"vue-mugen-scroll": "^0.2.1", "vue-mugen-scroll": "^0.2.1",
"vue-router": "^3.0.0", "vue-router": "^3.0.0",
"vue-style-loader": "^4.1.0", "vue-style-loader": "^4.1.0",
"vue-tribute": "^1.0.1", "vue-tribute": "^1.0.1",
"vue-template-compiler": "^2.6.4", "vue-template-compiler": "^2.6.10",
"vuedraggable": "^2.15.0", "vuedraggable": "^2.20.0",
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#5d237615463a84a23dd6f3f77c6ab577d68593ec", "vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#5d237615463a84a23dd6f3f77c6ab577d68593ec",
"webpack": "^3.12.0", "webpack": "^3.12.0",
"webpack-merge": "^4.1.3", "webpack-merge": "^4.1.3",
@@ -117,7 +118,7 @@
}, },
"private": true, "private": true,
"engines": { "engines": {
"node": "^10", "node": "^12",
"npm": "^6" "npm": "^6"
}, },
"scripts": { "scripts": {
@@ -153,9 +154,8 @@
"chai": "^4.1.2", "chai": "^4.1.2",
"chai-as-promised": "^7.1.1", "chai-as-promised": "^7.1.1",
"chalk": "^2.4.1", "chalk": "^2.4.1",
"chromedriver": "^2.40.0", "chromedriver": "^76.0.0",
"connect-history-api-fallback": "^1.1.0", "connect-history-api-fallback": "^1.1.0",
"coveralls": "^3.0.1",
"cross-spawn": "^6.0.5", "cross-spawn": "^6.0.5",
"eslint": "^4.19.1", "eslint": "^4.19.1",
"eslint-config-habitrpg": "^4.0.0", "eslint-config-habitrpg": "^4.0.0",
@@ -167,7 +167,7 @@
"expect.js": "^0.3.1", "expect.js": "^0.3.1",
"http-proxy-middleware": "^0.19.0", "http-proxy-middleware": "^0.19.0",
"istanbul": "^1.1.0-alpha.1", "istanbul": "^1.1.0-alpha.1",
"karma": "^3.1.3", "karma": "^4.0.1",
"karma-babel-preprocessor": "^7.0.0", "karma-babel-preprocessor": "^7.0.0",
"karma-chai-plugins": "^0.9.0", "karma-chai-plugins": "^0.9.0",
"karma-chrome-launcher": "^2.2.0", "karma-chrome-launcher": "^2.2.0",
@@ -179,14 +179,13 @@
"karma-sourcemap-loader": "^0.3.7", "karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "0.0.32", "karma-spec-reporter": "0.0.32",
"karma-webpack": "^3.0.0", "karma-webpack": "^3.0.0",
"lcov-result-merger": "^3.0.0",
"mocha": "^5.1.1", "mocha": "^5.1.1",
"monk": "^6.0.6", "monk": "^6.0.6",
"nightwatch": "^0.9.21", "nightwatch": "^1.0.16",
"puppeteer": "^1.5.0", "puppeteer": "^1.14.0",
"require-again": "^2.0.0", "require-again": "^2.0.0",
"selenium-server": "^3.12.0", "selenium-server": "^3.12.0",
"sinon": "^6.3.5", "sinon": "^7.2.4",
"sinon-chai": "^3.0.0", "sinon-chai": "^3.0.0",
"sinon-stub-promise": "^4.0.0", "sinon-stub-promise": "^4.0.0",
"webpack-bundle-analyzer": "^2.12.0", "webpack-bundle-analyzer": "^2.12.0",

View File

@@ -53,12 +53,12 @@ async function _deleteHabiticaData (user, email) {
if (response) { if (response) {
console.log(`${response.status} ${response.statusText}`); console.log(`${response.status} ${response.statusText}`);
if (response.status === 200) console.log(`${user._id} removed. Last login: ${user.auth.timestamps.loggedin}`); if (response.status === 200) console.log(`${user._id} (${email}) removed. Last login: ${user.auth.timestamps.loggedin}`);
} }
} }
async function _processEmailAddress (email) { async function _processEmailAddress (email) {
const emailRegex = new RegExp(`^${email}`, 'i'); const emailRegex = new RegExp(`^${email}$`, 'i');
const users = await User.find({ const users = await User.find({
$or: [ $or: [
{'auth.local.email': emailRegex}, {'auth.local.email': emailRegex},

View File

@@ -335,14 +335,13 @@ describe('analyticsService', () => {
let data, itemSpy; let data, itemSpy;
beforeEach(() => { beforeEach(() => {
Visitor.prototype.event.yields();
itemSpy = sandbox.stub().returnsThis(); itemSpy = sandbox.stub().returnsThis();
Visitor.prototype.event.returns({
send: sandbox.stub(),
});
Visitor.prototype.transaction.returns({ Visitor.prototype.transaction.returns({
item: itemSpy, item: itemSpy,
send: sandbox.stub().returnsThis(), send: sandbox.stub().yields(),
}); });
data = { data = {

View File

@@ -1232,7 +1232,7 @@ describe('cron', () => {
cron({user, tasksByType, daysMissed, analytics}); cron({user, tasksByType, daysMissed, analytics});
expect(user.history.exp).to.have.lengthOf(1); expect(user.history.exp).to.have.lengthOf(1);
expect(user.history.exp[0].value).to.equal(150); expect(user.history.exp[0].value).to.equal(25);
}); });
it('increments perfect day achievement if all (at least 1) due dailies were completed', () => { it('increments perfect day achievement if all (at least 1) due dailies were completed', () => {

View File

@@ -1,9 +1,7 @@
/* eslint-disable global-require */ /* eslint-disable global-require */
import got from 'got'; import got from 'got';
import nconf from 'nconf'; import nconf from 'nconf';
import nodemailer from 'nodemailer';
import requireAgain from 'require-again'; import requireAgain from 'require-again';
import logger from '../../../../website/server/libs/logger';
import { TAVERN_ID } from '../../../../website/server/models/group'; import { TAVERN_ID } from '../../../../website/server/models/group';
import { defer } from '../../../helpers/api-unit.helper'; import { defer } from '../../../helpers/api-unit.helper';
@@ -35,42 +33,6 @@ function getUser () {
describe('emails', () => { describe('emails', () => {
let pathToEmailLib = '../../../../website/server/libs/email'; let pathToEmailLib = '../../../../website/server/libs/email';
describe('sendEmail', () => {
let sendMailSpy;
beforeEach(() => {
sendMailSpy = sandbox.stub().returns(defer().promise);
sandbox.stub(nodemailer, 'createTransport').returns({
sendMail: sendMailSpy,
});
});
afterEach(() => {
sandbox.restore();
});
it('can send an email using the default transport', () => {
let attachEmail = requireAgain(pathToEmailLib);
attachEmail.send();
expect(sendMailSpy).to.be.calledOnce;
});
it('logs errors', (done) => {
sandbox.stub(logger, 'error');
let attachEmail = requireAgain(pathToEmailLib);
attachEmail.send();
expect(sendMailSpy).to.be.calledOnce;
defer().reject();
// wait for unhandledRejection event to fire
setTimeout(() => {
expect(logger.error).to.be.calledOnce;
done();
}, 20);
});
});
describe('getUserInfo', () => { describe('getUserInfo', () => {
it('returns an empty object if no field request', () => { it('returns an empty object if no field request', () => {
let attachEmail = requireAgain(pathToEmailLib); let attachEmail = requireAgain(pathToEmailLib);
@@ -84,7 +46,7 @@ describe('emails', () => {
let user = getUser(); let user = getUser();
let data = getUserInfo(user, ['name', 'email', '_id', 'canSend']); let data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
expect(data).to.have.property('name', user.profile.name); expect(data).to.have.property('name', user.auth.local.username);
expect(data).to.have.property('email', user.auth.local.email); expect(data).to.have.property('email', user.auth.local.email);
expect(data).to.have.property('_id', user._id); expect(data).to.have.property('_id', user._id);
expect(data).to.have.property('canSend', true); expect(data).to.have.property('canSend', true);
@@ -95,11 +57,11 @@ describe('emails', () => {
let getUserInfo = attachEmail.getUserInfo; let getUserInfo = attachEmail.getUserInfo;
let user = getUser(); let user = getUser();
delete user.profile.name; delete user.profile.name;
delete user.auth.local; delete user.auth.local.email;
let data = getUserInfo(user, ['name', 'email', '_id', 'canSend']); let data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
expect(data).to.have.property('name', user.profile.name); expect(data).to.have.property('name', user.auth.local.username);
expect(data).to.have.property('email', user.auth.facebook.emails[0].value); expect(data).to.have.property('email', user.auth.facebook.emails[0].value);
expect(data).to.have.property('_id', user._id); expect(data).to.have.property('_id', user._id);
expect(data).to.have.property('canSend', true); expect(data).to.have.property('canSend', true);
@@ -114,7 +76,7 @@ describe('emails', () => {
let data = getUserInfo(user, ['name', 'email', '_id', 'canSend']); let data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
expect(data).to.have.property('name', user.profile.name); expect(data).to.have.property('name', user.auth.local.username);
expect(data).not.to.have.property('email'); expect(data).not.to.have.property('email');
expect(data).to.have.property('_id', user._id); expect(data).to.have.property('_id', user._id);
expect(data).to.have.property('canSend', true); expect(data).to.have.property('canSend', true);

View File

@@ -0,0 +1,113 @@
/* eslint-disable camelcase */
import {
validateItemPath,
getDefaultOwnedGear,
castItemVal,
} from '../../../../../website/server/libs/items/utils';
describe('Items Utils', () => {
describe('getDefaultOwnedGear', () => {
it('clones the result object', () => {
const res1 = getDefaultOwnedGear();
res1.extraProperty = true;
const res2 = getDefaultOwnedGear();
expect(res2).not.to.have.property('extraProperty');
});
});
describe('validateItemPath', () => {
it('returns false if not an item path', () => {
expect(validateItemPath('notitems.gear.owned.item')).to.equal(false);
});
it('returns true if a valid schema path', () => {
expect(validateItemPath('items.gear.equipped.weapon')).to.equal(true);
expect(validateItemPath('items.currentPet')).to.equal(true);
expect(validateItemPath('items.special.snowball')).to.equal(true);
});
it('works with owned gear paths', () => {
expect(validateItemPath('items.gear.owned.head_armoire_crownOfHearts')).to.equal(true);
expect(validateItemPath('items.gear.owned.head_invalid')).to.equal(false);
});
it('works with pets paths', () => {
expect(validateItemPath('items.pets.Wolf-CottonCandyPink')).to.equal(true);
expect(validateItemPath('items.pets.Wolf-Invalid')).to.equal(false);
});
it('works with eggs paths', () => {
expect(validateItemPath('items.eggs.LionCub')).to.equal(true);
expect(validateItemPath('items.eggs.Armadillo')).to.equal(true);
expect(validateItemPath('items.eggs.NotAnArmadillo')).to.equal(false);
});
it('works with hatching potions paths', () => {
expect(validateItemPath('items.hatchingPotions.Base')).to.equal(true);
expect(validateItemPath('items.hatchingPotions.StarryNight')).to.equal(true);
expect(validateItemPath('items.hatchingPotions.Invalid')).to.equal(false);
});
it('works with food paths', () => {
expect(validateItemPath('items.food.Cake_Base')).to.equal(true);
expect(validateItemPath('items.food.Cake_Invalid')).to.equal(false);
});
it('works with mounts paths', () => {
expect(validateItemPath('items.mounts.Cactus-Base')).to.equal(true);
expect(validateItemPath('items.mounts.Aether-Invisible')).to.equal(true);
expect(validateItemPath('items.mounts.Aether-Invalid')).to.equal(false);
});
it('works with quests paths', () => {
expect(validateItemPath('items.quests.atom3')).to.equal(true);
expect(validateItemPath('items.quests.invalid')).to.equal(false);
});
});
describe('castItemVal', () => {
it('returns the item val untouched if not an item path', () => {
expect(castItemVal('notitems.gear.owned.item', 'a string')).to.equal('a string');
});
it('returns the item val untouched if an unsupported path', () => {
expect(castItemVal('items.gear.equipped.weapon', 'a string')).to.equal('a string');
expect(castItemVal('items.currentPet', 'a string')).to.equal('a string');
expect(castItemVal('items.special.snowball', 'a string')).to.equal('a string');
});
it('converts values for pets paths to numbers', () => {
expect(castItemVal('items.pets.Wolf-CottonCandyPink', '5')).to.equal(5);
expect(castItemVal('items.pets.Wolf-Invalid', '5')).to.equal(5);
});
it('converts values for eggs paths to numbers', () => {
expect(castItemVal('items.eggs.LionCub', '5')).to.equal(5);
expect(castItemVal('items.eggs.Armadillo', '5')).to.equal(5);
expect(castItemVal('items.eggs.NotAnArmadillo', '5')).to.equal(5);
});
it('converts values for hatching potions paths to numbers', () => {
expect(castItemVal('items.hatchingPotions.Base', '5')).to.equal(5);
expect(castItemVal('items.hatchingPotions.StarryNight', '5')).to.equal(5);
expect(castItemVal('items.hatchingPotions.Invalid', '5')).to.equal(5);
});
it('converts values for food paths to numbers', () => {
expect(castItemVal('items.food.Cake_Base', '5')).to.equal(5);
expect(castItemVal('items.food.Cake_Invalid', '5')).to.equal(5);
});
it('converts values for mounts paths to numbers', () => {
expect(castItemVal('items.mounts.Cactus-Base', '5')).to.equal(5);
expect(castItemVal('items.mounts.Aether-Invisible', '5')).to.equal(5);
expect(castItemVal('items.mounts.Aether-Invalid', '5')).to.equal(5);
});
it('converts values for quests paths to numbers', () => {
expect(castItemVal('items.quests.atom3', '5')).to.equal(5);
expect(castItemVal('items.quests.invalid', '5')).to.equal(5);
});
});
});

View File

@@ -16,6 +16,7 @@ describe('payments/index', () => {
beforeEach(async () => { beforeEach(async () => {
user = new User(); user = new User();
user.profile.name = 'sender'; user.profile.name = 'sender';
user.auth.local.username = 'sender';
await user.save(); await user.save();
group = generateGroup({ group = generateGroup({

View File

@@ -32,6 +32,7 @@ describe('slack', () => {
}, },
message: { message: {
id: 'chat-id', id: 'chat-id',
username: 'author',
user: 'Author', user: 'Author',
uuid: 'author-id', uuid: 'author-id',
text: 'some text', text: 'some text',
@@ -50,11 +51,11 @@ describe('slack', () => {
expect(IncomingWebhook.prototype.send).to.be.calledOnce; expect(IncomingWebhook.prototype.send).to.be.calledOnce;
expect(IncomingWebhook.prototype.send).to.be.calledWith({ expect(IncomingWebhook.prototype.send).to.be.calledWith({
text: 'flagger (flagger-id; language: flagger-lang) flagged a message', text: 'flagger (flagger-id; language: flagger-lang) flagged a group message',
attachments: [{ attachments: [{
fallback: 'Flag Message', fallback: 'Flag Message',
color: 'danger', color: 'danger',
author_name: `Author - author@example.com - author-id\n${timestamp}`, author_name: `@author Author (author@example.com; author-id)\n${timestamp}`,
title: 'Flag in Some group - (private guild)', title: 'Flag in Some group - (private guild)',
title_link: undefined, title_link: undefined,
text: 'some text', text: 'some text',

View File

@@ -16,7 +16,7 @@ describe('auth middleware', () => {
describe('auth with headers', () => { describe('auth with headers', () => {
it('allows to specify a list of user field that we do not want to load', (done) => { it('allows to specify a list of user field that we do not want to load', (done) => {
const authWithHeaders = authWithHeadersFactory({ const authWithHeaders = authWithHeadersFactory({
userFieldsToExclude: ['items', 'flags', 'auth.timestamps'], userFieldsToExclude: ['items'],
}); });
req.headers['x-api-user'] = user._id; req.headers['x-api-user'] = user._id;
@@ -27,11 +27,34 @@ describe('auth middleware', () => {
const userToJSON = res.locals.user.toJSON(); const userToJSON = res.locals.user.toJSON();
expect(userToJSON.items).to.not.exist; expect(userToJSON.items).to.not.exist;
expect(userToJSON.flags).to.not.exist; expect(userToJSON.auth).to.exist;
expect(userToJSON.auth.timestamps).to.not.exist;
done();
});
});
it('makes sure some fields are always included', (done) => {
const authWithHeaders = authWithHeadersFactory({
userFieldsToExclude: [
'items', 'auth.timestamps',
'preferences', 'notifications', '_id', 'flags', 'auth', // these are always loaded
],
});
req.headers['x-api-user'] = user._id;
req.headers['x-api-key'] = user.apiToken;
authWithHeaders(req, res, (err) => {
if (err) return done(err);
const userToJSON = res.locals.user.toJSON();
expect(userToJSON.items).to.not.exist;
expect(userToJSON.auth.timestamps).to.exist;
expect(userToJSON.auth).to.exist; expect(userToJSON.auth).to.exist;
expect(userToJSON.notifications).to.exist; expect(userToJSON.notifications).to.exist;
expect(userToJSON.preferences).to.exist; expect(userToJSON.preferences).to.exist;
expect(userToJSON._id).to.exist;
expect(userToJSON.flags).to.exist;
done(); done();
}); });

View File

@@ -1,7 +1,7 @@
import moment from 'moment'; import moment from 'moment';
import { v4 as generateUUID } from 'uuid'; import { v4 as generateUUID } from 'uuid';
import validator from 'validator'; import validator from 'validator';
import { sleep } from '../../../helpers/api-unit.helper'; import { sleep, translationCheck } from '../../../helpers/api-unit.helper';
import { import {
SPAM_MESSAGE_LIMIT, SPAM_MESSAGE_LIMIT,
SPAM_MIN_EXEMPT_CONTRIB_LEVEL, SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
@@ -271,7 +271,16 @@ describe('Group Model', () => {
party = await Group.findOne({_id: party._id}); party = await Group.findOne({_id: party._id});
expect(Group.prototype.sendChat).to.be.calledOnce; expect(Group.prototype.sendChat).to.be.calledOnce;
expect(Group.prototype.sendChat).to.be.calledWith('`Participating Member attacks Wailing Whale for 5.0 damage.` `Wailing Whale attacks party for 7.5 damage.`'); expect(Group.prototype.sendChat).to.be.calledWith({
message: '`Participating Member attacks Wailing Whale for 5.0 damage. Wailing Whale attacks party for 7.5 damage.`',
info: {
bossDamage: '7.5',
quest: 'whale',
type: 'boss_damage',
user: 'Participating Member',
userDamage: '5.0',
},
});
}); });
it('applies damage only to participating members of party', async () => { it('applies damage only to participating members of party', async () => {
@@ -344,7 +353,10 @@ describe('Group Model', () => {
party = await Group.findOne({_id: party._id}); party = await Group.findOne({_id: party._id});
expect(Group.prototype.sendChat).to.be.calledTwice; expect(Group.prototype.sendChat).to.be.calledTwice;
expect(Group.prototype.sendChat).to.be.calledWith('`You defeated Wailing Whale! Questing party members receive the rewards of victory.`'); expect(Group.prototype.sendChat).to.be.calledWith({
message: '`You defeated Wailing Whale! Questing party members receive the rewards of victory.`',
info: { quest: 'whale', type: 'boss_defeated' },
});
}); });
it('calls finishQuest when boss has <= 0 hp', async () => { it('calls finishQuest when boss has <= 0 hp', async () => {
@@ -387,7 +399,10 @@ describe('Group Model', () => {
party = await Group.findOne({_id: party._id}); party = await Group.findOne({_id: party._id});
expect(Group.prototype.sendChat).to.be.calledWith(quest.boss.rage.effect('en')); expect(Group.prototype.sendChat).to.be.calledWith({
message: quest.boss.rage.effect('en'),
info: { quest: 'trex_undead', type: 'boss_rage' },
});
expect(party.quest.progress.hp).to.eql(383.5); expect(party.quest.progress.hp).to.eql(383.5);
expect(party.quest.progress.rage).to.eql(0); expect(party.quest.progress.rage).to.eql(0);
}); });
@@ -437,7 +452,10 @@ describe('Group Model', () => {
party = await Group.findOne({_id: party._id}); party = await Group.findOne({_id: party._id});
expect(Group.prototype.sendChat).to.be.calledWith(quest.boss.rage.effect('en')); expect(Group.prototype.sendChat).to.be.calledWith({
message: quest.boss.rage.effect('en'),
info: { quest: 'lostMasterclasser4', type: 'boss_rage' },
});
expect(party.quest.progress.rage).to.eql(0); expect(party.quest.progress.rage).to.eql(0);
let drainedUser = await User.findById(participatingMember._id); let drainedUser = await User.findById(participatingMember._id);
@@ -488,7 +506,15 @@ describe('Group Model', () => {
party = await Group.findOne({_id: party._id}); party = await Group.findOne({_id: party._id});
expect(Group.prototype.sendChat).to.be.calledOnce; expect(Group.prototype.sendChat).to.be.calledOnce;
expect(Group.prototype.sendChat).to.be.calledWith('`Participating Member found 5 Bars of Soap.`'); expect(Group.prototype.sendChat).to.be.calledWith({
message: '`Participating Member found 5 Bars of Soap.`',
info: {
items: { soapBars: 5 },
quest: 'atom1',
type: 'user_found_items',
user: 'Participating Member',
},
});
}); });
it('sends a chat message if no progress is made', async () => { it('sends a chat message if no progress is made', async () => {
@@ -499,7 +525,15 @@ describe('Group Model', () => {
party = await Group.findOne({_id: party._id}); party = await Group.findOne({_id: party._id});
expect(Group.prototype.sendChat).to.be.calledOnce; expect(Group.prototype.sendChat).to.be.calledOnce;
expect(Group.prototype.sendChat).to.be.calledWith('`Participating Member found 0 Bars of Soap.`'); expect(Group.prototype.sendChat).to.be.calledWith({
message: '`Participating Member found 0 Bars of Soap.`',
info: {
items: { soapBars: 0 },
quest: 'atom1',
type: 'user_found_items',
user: 'Participating Member',
},
});
}); });
it('sends a chat message if no progress is made on quest with multiple items', async () => { it('sends a chat message if no progress is made on quest with multiple items', async () => {
@@ -516,9 +550,15 @@ describe('Group Model', () => {
party = await Group.findOne({_id: party._id}); party = await Group.findOne({_id: party._id});
expect(Group.prototype.sendChat).to.be.calledOnce; expect(Group.prototype.sendChat).to.be.calledOnce;
expect(Group.prototype.sendChat).to.be.calledWithMatch(/`Participating Member found/); expect(Group.prototype.sendChat).to.be.calledWith({
expect(Group.prototype.sendChat).to.be.calledWithMatch(/0 Blue Fins/); message: '`Participating Member found 0 Fire Coral, 0 Blue Fins.`',
expect(Group.prototype.sendChat).to.be.calledWithMatch(/0 Fire Coral/); info: {
items: { blueFins: 0, fireCoral: 0 },
quest: 'dilatoryDistress1',
type: 'user_found_items',
user: 'Participating Member',
},
});
}); });
it('handles collection quests with multiple items', async () => { it('handles collection quests with multiple items', async () => {
@@ -535,8 +575,14 @@ describe('Group Model', () => {
party = await Group.findOne({_id: party._id}); party = await Group.findOne({_id: party._id});
expect(Group.prototype.sendChat).to.be.calledOnce; expect(Group.prototype.sendChat).to.be.calledOnce;
expect(Group.prototype.sendChat).to.be.calledWithMatch(/`Participating Member found/); expect(Group.prototype.sendChat).to.be.calledWithMatch({
expect(Group.prototype.sendChat).to.be.calledWithMatch(/\d* (Tracks|Broken Twigs)/); message: sinon.match(/`Participating Member found/).and(sinon.match(/\d* (Tracks|Broken Twigs)/)),
info: {
quest: 'evilsanta2',
type: 'user_found_items',
user: 'Participating Member',
},
});
}); });
it('sends message about victory', async () => { it('sends message about victory', async () => {
@@ -547,7 +593,10 @@ describe('Group Model', () => {
party = await Group.findOne({_id: party._id}); party = await Group.findOne({_id: party._id});
expect(Group.prototype.sendChat).to.be.calledTwice; expect(Group.prototype.sendChat).to.be.calledTwice;
expect(Group.prototype.sendChat).to.be.calledWith('`All items found! Party has received their rewards.`'); expect(Group.prototype.sendChat).to.be.calledWith({
message: '`All items found! Party has received their rewards.`',
info: { type: 'all_items_found' },
});
}); });
it('calls finishQuest when all items are found', async () => { it('calls finishQuest when all items are found', async () => {
@@ -718,6 +767,258 @@ describe('Group Model', () => {
expect(res.t).to.not.be.called; expect(res.t).to.not.be.called;
}); });
}); });
describe('translateSystemMessages', () => {
it('translate quest_start', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'quest_start',
quest: 'basilist',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate boss_damage', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'boss_damage',
user: questLeader.profile.name,
quest: 'basilist',
userDamage: 15.3,
bossDamage: 3.7,
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate boss_dont_attack', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'boss_dont_attack',
user: questLeader.profile.name,
quest: 'basilist',
userDamage: 15.3,
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate boss_rage', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'boss_rage',
quest: 'lostMasterclasser3',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate boss_defeated', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'boss_defeated',
quest: 'lostMasterclasser3',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate user_found_items', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'user_found_items',
user: questLeader.profile.name,
quest: 'lostMasterclasser1',
items: {
ancientTome: 3,
forbiddenTome: 2,
hiddenTome: 1,
},
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate all_items_found', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'all_items_found',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate spell_cast_party', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'spell_cast_party',
user: questLeader.profile.name,
class: 'wizard',
spell: 'earth',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate spell_cast_user', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'spell_cast_user',
user: questLeader.profile.name,
class: 'special',
spell: 'snowball',
target: participatingMember.profile.name,
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate quest_cancel', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'quest_cancel',
user: questLeader.profile.name,
quest: 'basilist',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate quest_abort', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'quest_abort',
user: questLeader.profile.name,
quest: 'basilist',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate tavern_quest_completed', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'tavern_quest_completed',
quest: 'stressbeast',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate tavern_boss_rage_tired', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'tavern_boss_rage_tired',
quest: 'stressbeast',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate tavern_boss_rage', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'tavern_boss_rage',
quest: 'dysheartener',
scene: 'market',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate tavern_boss_desperation', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'tavern_boss_desperation',
quest: 'stressbeast',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate claim_task', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'claim_task',
user: questLeader.profile.name,
task: 'Feed the pet',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
});
describe('toJSONCleanChat', () => {
it('shows messages with 1 flag to non-admins', async () => {
party.chat = [{
flagCount: 1,
info: {
type: 'quest_start',
quest: 'basilist',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
expect(toJSON.chat.length).to.equal(1);
});
it('shows messages with >= 2 flag to admins', async () => {
party.chat = [{
flagCount: 3,
info: {
type: 'quest_start',
quest: 'basilist',
},
}];
const admin = new User({'contributor.admin': true});
let toJSON = await Group.toJSONCleanChat(party, admin);
expect(toJSON.chat.length).to.equal(1);
});
it('doesn\'t show flagged messages to non-admins', async () => {
party.chat = [{
flagCount: 3,
info: {
type: 'quest_start',
quest: 'basilist',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
expect(toJSON.chat.length).to.equal(0);
});
});
}); });
context('Instance Methods', () => { context('Instance Methods', () => {
@@ -1007,7 +1308,8 @@ describe('Group Model', () => {
}); });
it('formats message', () => { it('formats message', () => {
const chatMessage = party.sendChat('a new message', { const chatMessage = party.sendChat({
message: 'a new message', user: {
_id: 'user-id', _id: 'user-id',
profile: { name: 'user name' }, profile: { name: 'user name' },
contributor: { contributor: {
@@ -1020,7 +1322,8 @@ describe('Group Model', () => {
return 'backer object'; return 'backer object';
}, },
}, },
}); }}
);
const chat = chatMessage; const chat = chatMessage;
@@ -1037,7 +1340,7 @@ describe('Group Model', () => {
}); });
it('formats message as system if no user is passed in', () => { it('formats message as system if no user is passed in', () => {
const chat = party.sendChat('a system message'); const chat = party.sendChat({message: 'a system message'});
expect(chat.text).to.eql('a system message'); expect(chat.text).to.eql('a system message');
expect(validator.isUUID(chat.id)).to.eql(true); expect(validator.isUUID(chat.id)).to.eql(true);
@@ -1052,7 +1355,7 @@ describe('Group Model', () => {
}); });
it('updates users about new messages in party', () => { it('updates users about new messages in party', () => {
party.sendChat('message'); party.sendChat({message: 'message'});
expect(User.update).to.be.calledOnce; expect(User.update).to.be.calledOnce;
expect(User.update).to.be.calledWithMatch({ expect(User.update).to.be.calledWithMatch({
@@ -1066,7 +1369,7 @@ describe('Group Model', () => {
type: 'guild', type: 'guild',
}); });
group.sendChat('message'); group.sendChat({message: 'message'});
expect(User.update).to.be.calledOnce; expect(User.update).to.be.calledOnce;
expect(User.update).to.be.calledWithMatch({ expect(User.update).to.be.calledWithMatch({
@@ -1076,7 +1379,7 @@ describe('Group Model', () => {
}); });
it('does not send update to user that sent the message', () => { it('does not send update to user that sent the message', () => {
party.sendChat('message', {_id: 'user-id', profile: { name: 'user' }}); party.sendChat({message: 'message', user: {_id: 'user-id', profile: { name: 'user' }}});
expect(User.update).to.be.calledOnce; expect(User.update).to.be.calledOnce;
expect(User.update).to.be.calledWithMatch({ expect(User.update).to.be.calledWithMatch({
@@ -1088,7 +1391,7 @@ describe('Group Model', () => {
it('skips sending new message notification for guilds with > 5000 members', () => { it('skips sending new message notification for guilds with > 5000 members', () => {
party.memberCount = 5001; party.memberCount = 5001;
party.sendChat('message'); party.sendChat({message: 'message'});
expect(User.update).to.not.be.called; expect(User.update).to.not.be.called;
}); });
@@ -1096,7 +1399,7 @@ describe('Group Model', () => {
it('skips sending messages to the tavern', () => { it('skips sending messages to the tavern', () => {
party._id = TAVERN_ID; party._id = TAVERN_ID;
party.sendChat('message'); party.sendChat({message: 'message'});
expect(User.update).to.not.be.called; expect(User.update).to.not.be.called;
}); });
@@ -1431,7 +1734,7 @@ describe('Group Model', () => {
let quest; let quest;
beforeEach(() => { beforeEach(() => {
quest = questScrolls.whale; quest = questScrolls.armadillo;
party.quest.key = quest.key; party.quest.key = quest.key;
party.quest.active = false; party.quest.active = false;
party.quest.leader = questLeader._id; party.quest.leader = questLeader._id;
@@ -1579,6 +1882,36 @@ describe('Group Model', () => {
expect(updatedSleepingParticipatingMember.achievements.lostMasterclasser).to.not.eql(true); expect(updatedSleepingParticipatingMember.achievements.lostMasterclasser).to.not.eql(true);
}); });
it('gives out other pet-related quest achievements', async () => {
quest = questScrolls.rock;
party.quest.key = quest.key;
questLeader.achievements.quests = {
mayhemMistiflying1: 1,
yarn: 1,
mayhemMistiflying2: 1,
egg: 1,
mayhemMistiflying3: 1,
slime: 2,
};
await questLeader.save();
await party.finishQuest(quest);
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.mindOverMatter).to.eql(true);
expect(updatedParticipatingMember.achievements.mindOverMatter).to.not.eql(true);
expect(updatedSleepingParticipatingMember.achievements.mindOverMatter).to.not.eql(true);
});
it('gives xp and gold', async () => { it('gives xp and gold', async () => {
await party.finishQuest(quest); await party.finishQuest(quest);
@@ -1715,13 +2048,13 @@ describe('Group Model', () => {
questLeader = await User.findById(questLeader._id); questLeader = await User.findById(questLeader._id);
participatingMember = await User.findById(participatingMember._id); participatingMember = await User.findById(participatingMember._id);
expect(questLeader.party.quest.completed).to.eql('whale'); expect(questLeader.party.quest.completed).to.eql('armadillo');
expect(questLeader.party.quest.progress.up).to.eql(10); expect(questLeader.party.quest.progress.up).to.eql(10);
expect(questLeader.party.quest.progress.down).to.eql(8); expect(questLeader.party.quest.progress.down).to.eql(8);
expect(questLeader.party.quest.progress.collectedItems).to.eql(5); expect(questLeader.party.quest.progress.collectedItems).to.eql(5);
expect(questLeader.party.quest.RSVPNeeded).to.eql(false); expect(questLeader.party.quest.RSVPNeeded).to.eql(false);
expect(participatingMember.party.quest.completed).to.eql('whale'); expect(participatingMember.party.quest.completed).to.eql('armadillo');
expect(participatingMember.party.quest.progress.up).to.eql(10); expect(participatingMember.party.quest.progress.up).to.eql(10);
expect(participatingMember.party.quest.progress.down).to.eql(8); expect(participatingMember.party.quest.progress.down).to.eql(8);
expect(participatingMember.party.quest.progress.collectedItems).to.eql(5); expect(participatingMember.party.quest.progress.collectedItems).to.eql(5);
@@ -1928,7 +2261,7 @@ describe('Group Model', () => {
await guild.save(); await guild.save();
const groupMessage = guild.sendChat('Test message.'); const groupMessage = guild.sendChat({message: 'Test message.'});
await groupMessage.save(); await groupMessage.save();
await sleep(); await sleep();

View File

@@ -171,7 +171,7 @@ describe('GET challenges/user', () => {
}); });
}); });
it('should return not return challenges in user groups if we send member true param', async () => { it('should not return challenges in user groups if we send member true param', async () => {
let challenges = await member.get(`/challenges/user?member=${true}`); let challenges = await member.get(`/challenges/user?member=${true}`);
let foundChallenge1 = _.find(challenges, { _id: challenge._id }); let foundChallenge1 = _.find(challenges, { _id: challenge._id });
@@ -214,6 +214,28 @@ describe('GET challenges/user', () => {
let foundChallenge = _.find(challenges, { _id: privateChallenge._id }); let foundChallenge = _.find(challenges, { _id: privateChallenge._id });
expect(foundChallenge).to.not.exist; expect(foundChallenge).to.not.exist;
}); });
it('should not return challenges user doesn\'t have access to, even with query parameters', async () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'TestPrivateGuild',
summary: 'summary for TestPrivateGuild',
type: 'guild',
privacy: 'private',
},
});
let privateChallenge = await generateChallenge(groupLeader, group, {categories: [{
name: 'academics',
slug: 'academics',
}]});
await groupLeader.post(`/challenges/${privateChallenge._id}/join`);
let challenges = await nonMember.get('/challenges/user?categories=academics&owned=not_owned');
let foundChallenge = _.find(challenges, { _id: privateChallenge._id });
expect(foundChallenge).to.not.exist;
});
}); });
context('official challenge is present', () => { context('official challenge is present', () => {

View File

@@ -56,11 +56,11 @@ describe('PUT /challenges/:challengeId', () => {
tasksOrder: 'new order', tasksOrder: 'new order',
official: true, official: true,
shortName: 'new short name', shortName: 'new short name',
leader: member._id,
// applied // applied
name: 'New Challenge Name', name: 'New Challenge Name',
description: 'New challenge description.', description: 'New challenge description.',
leader: member._id,
}); });
expect(res.prize).to.equal(0); expect(res.prize).to.equal(0);
@@ -76,12 +76,12 @@ describe('PUT /challenges/:challengeId', () => {
expect(res.shortName).not.to.equal('new short name'); expect(res.shortName).not.to.equal('new short name');
expect(res.leader).to.eql({ expect(res.leader).to.eql({
_id: member._id, _id: user._id,
id: member._id, id: user._id,
profile: {name: member.profile.name}, profile: {name: user.profile.name},
auth: { auth: {
local: { local: {
username: member.auth.local.username, username: user.auth.local.username,
}, },
}, },
flags: { flags: {

View File

@@ -63,11 +63,11 @@ describe('POST /chat/:chatId/flag', () => {
/* eslint-disable camelcase */ /* eslint-disable camelcase */
expect(IncomingWebhook.prototype.send).to.be.calledWith({ expect(IncomingWebhook.prototype.send).to.be.calledWith({
text: `${user.profile.name} (${user.id}; language: en) flagged a message`, text: `${user.profile.name} (${user.id}; language: en) flagged a group message`,
attachments: [{ attachments: [{
fallback: 'Flag Message', fallback: 'Flag Message',
color: 'danger', color: 'danger',
author_name: `${anotherUser.profile.name} - ${anotherUser.auth.local.email} - ${anotherUser._id}\n${timestamp}`, author_name: `@${anotherUser.auth.local.username} ${anotherUser.profile.name} (${anotherUser.auth.local.email}; ${anotherUser._id})\n${timestamp}`,
title: 'Flag in Test Guild', title: 'Flag in Test Guild',
title_link: `${BASE_URL}/groups/guild/${group._id}`, title_link: `${BASE_URL}/groups/guild/${group._id}`,
text: TEST_MESSAGE, text: TEST_MESSAGE,
@@ -98,11 +98,11 @@ describe('POST /chat/:chatId/flag', () => {
/* eslint-disable camelcase */ /* eslint-disable camelcase */
expect(IncomingWebhook.prototype.send).to.be.calledWith({ expect(IncomingWebhook.prototype.send).to.be.calledWith({
text: `${newUser.profile.name} (${newUser.id}; language: en) flagged a message`, text: `${newUser.profile.name} (${newUser.id}; language: en) flagged a group message`,
attachments: [{ attachments: [{
fallback: 'Flag Message', fallback: 'Flag Message',
color: 'danger', color: 'danger',
author_name: `${newUser.profile.name} - ${newUser.auth.local.email} - ${newUser._id}\n${timestamp}`, author_name: `@${newUser.auth.local.username} ${newUser.profile.name} (${newUser.auth.local.email}; ${newUser._id})\n${timestamp}`,
title: 'Flag in Test Guild', title: 'Flag in Test Guild',
title_link: `${BASE_URL}/groups/guild/${group._id}`, title_link: `${BASE_URL}/groups/guild/${group._id}`,
text: TEST_MESSAGE, text: TEST_MESSAGE,

View File

@@ -12,6 +12,7 @@ import {
SPAM_MIN_EXEMPT_CONTRIB_LEVEL, SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
TAVERN_ID, TAVERN_ID,
} from '../../../../../website/server/models/group'; } from '../../../../../website/server/models/group';
import { CHAT_FLAG_FROM_SHADOW_MUTE } from '../../../../../website/common/script/constants';
import { v4 as generateUUID } from 'uuid'; import { v4 as generateUUID } from 'uuid';
import { getMatchesByWordArray } from '../../../../../website/server/libs/stringUtils'; import { getMatchesByWordArray } from '../../../../../website/server/libs/stringUtils';
import bannedWords from '../../../../../website/server/libs/bannedWords'; import bannedWords from '../../../../../website/server/libs/bannedWords';
@@ -81,6 +82,10 @@ describe('POST /chat', () => {
}); });
describe('mute user', () => { describe('mute user', () => {
afterEach(() => {
member.update({'flags.chatRevoked': false});
});
it('returns an error when chat privileges are revoked when sending a message to a public guild', async () => { it('returns an error when chat privileges are revoked when sending a message to a public guild', async () => {
const userWithChatRevoked = await member.update({'flags.chatRevoked': true}); const userWithChatRevoked = await member.update({'flags.chatRevoked': true});
await expect(userWithChatRevoked.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage})).to.eventually.be.rejected.and.eql({ await expect(userWithChatRevoked.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage})).to.eventually.be.rejected.and.eql({
@@ -89,6 +94,129 @@ describe('POST /chat', () => {
message: t('chatPrivilegesRevoked'), message: t('chatPrivilegesRevoked'),
}); });
}); });
it('does not error when chat privileges are revoked when sending a message to a private guild', async () => {
const { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'Private Guild',
type: 'guild',
privacy: 'private',
},
members: 1,
});
const privateGuildMemberWithChatsRevoked = members[0];
await privateGuildMemberWithChatsRevoked.update({'flags.chatRevoked': true});
const message = await privateGuildMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage});
expect(message.message.id).to.exist;
});
it('does not error when chat privileges are revoked when sending a message to a party', async () => {
const { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'Party',
type: 'party',
privacy: 'private',
},
members: 1,
});
const privatePartyMemberWithChatsRevoked = members[0];
await privatePartyMemberWithChatsRevoked.update({'flags.chatRevoked': true});
const message = await privatePartyMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage});
expect(message.message.id).to.exist;
});
});
describe('shadow-mute user', () => {
beforeEach(() => {
sandbox.spy(email, 'sendTxn');
sandbox.stub(IncomingWebhook.prototype, 'send');
});
afterEach(() => {
sandbox.restore();
member.update({'flags.chatShadowMuted': false});
});
it('creates a chat with flagCount already set and notifies mods when sending a message to a public guild', async () => {
const userWithChatShadowMuted = await member.update({'flags.chatShadowMuted': true});
const message = await userWithChatShadowMuted.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage});
expect(message.message.id).to.exist;
expect(message.message.flagCount).to.eql(CHAT_FLAG_FROM_SHADOW_MUTE);
// Email sent to mods
await sleep(0.5);
expect(email.sendTxn).to.be.calledOnce;
expect(email.sendTxn.args[0][1]).to.eql('shadow-muted-post-report-to-mods');
// Slack message to mods
expect(IncomingWebhook.prototype.send).to.be.calledOnce;
/* eslint-disable camelcase */
expect(IncomingWebhook.prototype.send).to.be.calledWith({
text: `@${member.auth.local.username} / ${member.profile.name} posted while shadow-muted`,
attachments: [{
fallback: 'Shadow-Muted Message',
color: 'danger',
author_name: `@${member.auth.local.username} ${member.profile.name} (${member.auth.local.email}; ${member._id})`,
title: 'Shadow-Muted Post in Test Guild',
title_link: `${BASE_URL}/groups/guild/${groupWithChat.id}`,
text: testMessage,
mrkdwn_in: [
'text',
],
}],
});
/* eslint-enable camelcase */
});
it('creates a chat with zero flagCount when sending a message to a private guild', async () => {
const { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'Private Guild',
type: 'guild',
privacy: 'private',
},
members: 1,
});
const userWithChatShadowMuted = members[0];
await userWithChatShadowMuted.update({'flags.chatShadowMuted': true});
const message = await userWithChatShadowMuted.post(`/groups/${group._id}/chat`, { message: testMessage});
expect(message.message.id).to.exist;
expect(message.message.flagCount).to.eql(0);
});
it('creates a chat with zero flagCount when sending a message to a party', async () => {
const { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'Party',
type: 'party',
privacy: 'private',
},
members: 1,
});
const userWithChatShadowMuted = members[0];
await userWithChatShadowMuted.update({'flags.chatShadowMuted': true});
const message = await userWithChatShadowMuted.post(`/groups/${group._id}/chat`, { message: testMessage});
expect(message.message.id).to.exist;
expect(message.message.flagCount).to.eql(0);
});
it('creates a chat with zero flagCount when non-shadow-muted user sends a message to a public guild', async () => {
const message = await member.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage});
expect(message.message.id).to.exist;
expect(message.message.flagCount).to.eql(0);
});
}); });
context('banned word', () => { context('banned word', () => {
@@ -235,6 +363,7 @@ describe('POST /chat', () => {
afterEach(() => { afterEach(() => {
sandbox.restore(); sandbox.restore();
user.update({'flags.chatRevoked': false});
}); });
it('errors and revokes privileges when chat message contains a banned slur', async () => { it('errors and revokes privileges when chat message contains a banned slur', async () => {
@@ -257,7 +386,7 @@ describe('POST /chat', () => {
attachments: [{ attachments: [{
fallback: 'Slur Message', fallback: 'Slur Message',
color: 'danger', color: 'danger',
author_name: `${user.profile.name} - ${user.auth.local.email} - ${user._id}`, author_name: `@${user.auth.local.username} ${user.profile.name} (${user.auth.local.email}; ${user._id})`,
title: 'Slur in Test Guild', title: 'Slur in Test Guild',
title_link: `${BASE_URL}/groups/guild/${groupWithChat.id}`, title_link: `${BASE_URL}/groups/guild/${groupWithChat.id}`,
text: testSlurMessage, text: testSlurMessage,
@@ -274,11 +403,6 @@ describe('POST /chat', () => {
error: 'NotAuthorized', error: 'NotAuthorized',
message: t('chatPrivilegesRevoked'), 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});
}); });
it('does not allow slurs in private groups', async () => { it('does not allow slurs in private groups', async () => {
@@ -310,7 +434,7 @@ describe('POST /chat', () => {
attachments: [{ attachments: [{
fallback: 'Slur Message', fallback: 'Slur Message',
color: 'danger', color: 'danger',
author_name: `${members[0].profile.name} - ${members[0].auth.local.email} - ${members[0]._id}`, author_name: `@${members[0].auth.local.username} ${members[0].profile.name} (${members[0].auth.local.email}; ${members[0]._id})`,
title: 'Slur in Party - (private party)', title: 'Slur in Party - (private party)',
title_link: undefined, title_link: undefined,
text: testSlurMessage, text: testSlurMessage,
@@ -327,10 +451,6 @@ describe('POST /chat', () => {
error: 'NotAuthorized', error: 'NotAuthorized',
message: t('chatPrivilegesRevoked'), message: t('chatPrivilegesRevoked'),
}); });
// Restore chat privileges to continue testing
members[0].flags.chatRevoked = false;
await members[0].update({'flags.chatRevoked': false});
}); });
it('errors when slur is typed in mixed case', async () => { it('errors when slur is typed in mixed case', async () => {
@@ -345,42 +465,6 @@ describe('POST /chat', () => {
}); });
}); });
it('does not error when sending a message to a private guild with a user with revoked chat', async () => {
let { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'Private Guild',
type: 'guild',
privacy: 'private',
},
members: 1,
});
let privateGuildMemberWithChatsRevoked = members[0];
await privateGuildMemberWithChatsRevoked.update({'flags.chatRevoked': true});
let message = await privateGuildMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage});
expect(message.message.id).to.exist;
});
it('does not error when sending a message to a party with a user with revoked chat', async () => {
let { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'Party',
type: 'party',
privacy: 'private',
},
members: 1,
});
let privatePartyMemberWithChatsRevoked = members[0];
await privatePartyMemberWithChatsRevoked.update({'flags.chatRevoked': true});
let message = await privatePartyMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage});
expect(message.message.id).to.exist;
});
it('creates a chat', async () => { it('creates a chat', async () => {
const newMessage = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage}); const newMessage = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage});
const groupMessages = await user.get(`/groups/${groupWithChat._id}/chat`); const groupMessages = await user.get(`/groups/${groupWithChat._id}/chat`);
@@ -486,8 +570,13 @@ describe('POST /chat', () => {
}); });
}); });
context('chat notifications', () => {
beforeEach(() => {
member.update({newMessages: {}, notifications: []});
});
it('notifies other users of new messages for a guild', async () => { it('notifies other users of new messages for a guild', async () => {
let message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage}); let message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
let memberWithNotification = await member.get('/user'); let memberWithNotification = await member.get('/user');
expect(message.message.id).to.exist; expect(message.message.id).to.exist;
@@ -507,7 +596,7 @@ describe('POST /chat', () => {
members: 1, members: 1,
}); });
let message = await groupLeader.post(`/groups/${group._id}/chat`, { message: testMessage}); let message = await groupLeader.post(`/groups/${group._id}/chat`, { message: testMessage });
let memberWithNotification = await members[0].get('/user'); let memberWithNotification = await members[0].get('/user');
expect(message.message.id).to.exist; expect(message.message.id).to.exist;
@@ -517,6 +606,21 @@ describe('POST /chat', () => {
})).to.exist; })).to.exist;
}); });
it('does not notify other users of a new message that is already hidden from shadow-muting', async () => {
await user.update({'flags.chatShadowMuted': true});
let message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
let memberWithNotification = await member.get('/user');
await user.update({'flags.chatShadowMuted': false});
expect(message.message.id).to.exist;
expect(memberWithNotification.newMessages[`${groupWithChat._id}`]).to.not.exist;
expect(memberWithNotification.notifications.find(n => {
return n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupWithChat._id;
})).to.not.exist;
});
});
context('Spam prevention', () => { context('Spam prevention', () => {
it('Returns an error when the user has been posting too many messages', async () => { it('Returns an error when the user has been posting too many messages', async () => {
// Post as many messages are needed to reach the spam limit // Post as many messages are needed to reach the spam limit
@@ -533,7 +637,7 @@ describe('POST /chat', () => {
}); });
it('contributor should not receive spam alert', async () => { it('contributor should not receive spam alert', async () => {
let userSocialite = await member.update({'contributor.level': SPAM_MIN_EXEMPT_CONTRIB_LEVEL, 'flags.chatRevoked': false}); let userSocialite = await member.update({'contributor.level': SPAM_MIN_EXEMPT_CONTRIB_LEVEL});
// Post 1 more message than the spam limit to ensure they do not reach the limit // Post 1 more message than the spam limit to ensure they do not reach the limit
for (let i = 0; i < SPAM_MESSAGE_LIMIT + 1; i++) { for (let i = 0; i < SPAM_MESSAGE_LIMIT + 1; i++) {

View File

@@ -149,7 +149,7 @@ describe('POST /group', () => {
).to.eventually.be.rejected.and.eql({ ).to.eventually.be.rejected.and.eql({
code: 401, code: 401,
error: 'NotAuthorized', error: 'NotAuthorized',
message: t('cannotCreatePublicGuildWhenMuted'), message: t('chatPrivilegesRevoked'),
}); });
}); });
}); });

View File

@@ -276,25 +276,26 @@ describe('POST /groups/:groupId/leave', () => {
}); });
}); });
context('Leaving a group plan', () => { each(typesOfGroups, (groupDetails, groupType) => {
it('cancels the free subscription', async () => { context(`Leaving a group plan when the group is a ${groupType}`, () => {
// Create group let groupWithPlan;
let leader;
let member;
beforeEach(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({ let { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: { groupDetails,
name: 'Test Private Guild',
type: 'guild',
},
members: 1, members: 1,
}); });
leader = groupLeader;
let leader = groupLeader; member = members[0];
let member = members[0]; groupWithPlan = group;
let userWithFreePlan = await User.findById(leader._id).exec(); let userWithFreePlan = await User.findById(leader._id).exec();
// Create subscription // Create subscription
let paymentData = { let paymentData = {
user: userWithFreePlan, user: userWithFreePlan,
groupId: group._id, groupId: groupWithPlan._id,
sub: { sub: {
key: 'basic_3mo', key: 'basic_3mo',
}, },
@@ -307,13 +308,38 @@ describe('POST /groups/:groupId/leave', () => {
}; };
await payments.createSubscription(paymentData); await payments.createSubscription(paymentData);
await member.sync(); await member.sync();
});
it('cancels the free subscription', async () => {
expect(member.purchased.plan.planId).to.equal('group_plan_auto'); expect(member.purchased.plan.planId).to.equal('group_plan_auto');
expect(member.purchased.plan.dateTerminated).to.not.exist; expect(member.purchased.plan.dateTerminated).to.not.exist;
// Leave // Leave
await member.post(`/groups/${group._id}/leave`); await member.post(`/groups/${groupWithPlan._id}/leave`);
await member.sync(); await member.sync();
expect(member.purchased.plan.dateTerminated).to.exist; expect(member.purchased.plan.dateTerminated).to.exist;
}); });
it('preserves the free subscription when leaving a any other group without a plan', async () => {
// Joining a guild without a group plan
let { group: groupWithNoPlan } = await createAndPopulateGroup({
groupDetails: {
name: 'Group Without Plan',
type: 'guild',
privacy: 'public',
},
});
await member.post(`/groups/${groupWithNoPlan._id}/join`);
await member.sync();
expect(member.purchased.plan.planId).to.equal('group_plan_auto');
expect(member.purchased.plan.dateTerminated).to.not.exist;
// Leaving the guild without a group plan
await member.post(`/groups/${groupWithNoPlan._id}/leave`);
await member.sync();
expect(member.purchased.plan.dateTerminated).to.not.exist;
});
});
}); });
}); });

View File

@@ -100,7 +100,7 @@ describe('Post /groups/:groupId/invite', () => {
.to.eventually.be.rejected.and.eql({ .to.eventually.be.rejected.and.eql({
code: 401, code: 401,
error: 'NotAuthorized', error: 'NotAuthorized',
message: t('cannotInviteWhenMuted'), message: t('chatPrivilegesRevoked'),
}); });
}); });
@@ -262,7 +262,7 @@ describe('Post /groups/:groupId/invite', () => {
.to.eventually.be.rejected.and.eql({ .to.eventually.be.rejected.and.eql({
code: 401, code: 401,
error: 'NotAuthorized', error: 'NotAuthorized',
message: t('cannotInviteWhenMuted'), message: t('chatPrivilegesRevoked'),
}); });
}); });
@@ -436,7 +436,7 @@ describe('Post /groups/:groupId/invite', () => {
.to.eventually.be.rejected.and.eql({ .to.eventually.be.rejected.and.eql({
code: 401, code: 401,
error: 'NotAuthorized', error: 'NotAuthorized',
message: t('cannotInviteWhenMuted'), message: t('chatPrivilegesRevoked'),
}); });
}); });
@@ -526,7 +526,7 @@ describe('Post /groups/:groupId/invite', () => {
.to.eventually.be.rejected.and.eql({ .to.eventually.be.rejected.and.eql({
code: 401, code: 401,
error: 'NotAuthorized', error: 'NotAuthorized',
message: t('cannotInviteWhenMuted'), message: t('chatPrivilegesRevoked'),
}); });
}); });

View File

@@ -25,9 +25,9 @@ describe('GET /heroes/:heroId', () => {
it('validates req.params.heroId', async () => { it('validates req.params.heroId', async () => {
await expect(user.get('/hall/heroes/invalidUUID')).to.eventually.be.rejected.and.eql({ await expect(user.get('/hall/heroes/invalidUUID')).to.eventually.be.rejected.and.eql({
code: 400, code: 404,
error: 'BadRequest', error: 'NotFound',
message: t('invalidReqParams'), message: t('userWithIDNotFound', {userId: 'invalidUUID'}),
}); });
}); });
@@ -40,7 +40,7 @@ describe('GET /heroes/:heroId', () => {
}); });
}); });
it('returns only necessary hero data', async () => { it('returns only necessary hero data given user id', async () => {
let hero = await generateUser({ let hero = await generateUser({
contributor: {tier: 23}, contributor: {tier: 23},
}); });
@@ -53,4 +53,24 @@ describe('GET /heroes/:heroId', () => {
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']); expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
expect(heroRes.profile).to.have.all.keys(['name']); expect(heroRes.profile).to.have.all.keys(['name']);
}); });
it('returns only necessary hero data given username', async () => {
let hero = await generateUser({
contributor: {tier: 23},
});
let heroRes = await user.get(`/hall/heroes/${hero.auth.local.username}`);
expect(heroRes).to.have.all.keys([ // works as: object has all and only these keys
'_id', 'id', 'balance', 'profile', 'purchased',
'contributor', 'auth', 'items',
]);
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
expect(heroRes.profile).to.have.all.keys(['name']);
});
it('returns correct hero using search with difference case', async () => {
await generateUser({}, { username: 'TestUpperCaseName123' });
let heroRes = await user.get('/hall/heroes/TestuPPerCasEName123');
expect(heroRes.auth.local.username).to.equal('TestUpperCaseName123');
});
}); });

View File

@@ -105,16 +105,22 @@ describe('PUT /heroes/:heroId', () => {
it('updates chatRevoked flag', async () => { it('updates chatRevoked flag', async () => {
let hero = await generateUser(); let hero = await generateUser();
await user.put(`/hall/heroes/${hero._id}`, { await user.put(`/hall/heroes/${hero._id}`, {
flags: {chatRevoked: true}, flags: {chatRevoked: true},
}); });
await hero.sync(); await hero.sync();
expect(hero.flags.chatRevoked).to.eql(true); expect(hero.flags.chatRevoked).to.eql(true);
}); });
it('updates chatShadowMuted flag', async () => {
let hero = await generateUser();
await user.put(`/hall/heroes/${hero._id}`, {
flags: {chatShadowMuted: true},
});
await hero.sync();
expect(hero.flags.chatShadowMuted).to.eql(true);
});
it('updates contributor level', async () => { it('updates contributor level', async () => {
let hero = await generateUser({ let hero = await generateUser({
contributor: {level: 5}, contributor: {level: 5},

View File

@@ -6,7 +6,7 @@ describe('GET /inbox/messages', () => {
let user; let user;
let otherUser; let otherUser;
before(async () => { beforeEach(async () => {
[user, otherUser] = await Promise.all([generateUser(), generateUser()]); [user, otherUser] = await Promise.all([generateUser(), generateUser()]);
await otherUser.post('/members/send-private-message', { await otherUser.post('/members/send-private-message', {
@@ -27,8 +27,6 @@ describe('GET /inbox/messages', () => {
toUserId: user.id, toUserId: user.id,
message: 'fourth', message: 'fourth',
}); });
await user.sync();
}); });
it('returns the user inbox messages as an array of ordered messages (from most to least recent)', async () => { it('returns the user inbox messages as an array of ordered messages (from most to least recent)', async () => {
@@ -45,4 +43,27 @@ describe('GET /inbox/messages', () => {
expect(messages[2].text).to.equal('second'); expect(messages[2].text).to.equal('second');
expect(messages[3].text).to.equal('first'); expect(messages[3].text).to.equal('first');
}); });
it('returns four messages when using page-query ', async () => {
const promises = [];
for (let i = 0; i < 10; i++) {
promises.push(user.post('/members/send-private-message', {
toUserId: user.id,
message: 'fourth',
}));
}
await Promise.all(promises);
const messages = await user.get('/inbox/messages?page=1');
expect(messages.length).to.equal(4);
});
it('returns only the messages of one conversation', async () => {
const messages = await user.get(`/inbox/messages?conversation=${otherUser.id}`);
expect(messages.length).to.equal(3);
});
}); });

View File

@@ -13,10 +13,10 @@ describe('payments - stripe - #checkout', () => {
}); });
it('verifies credentials', async () => { it('verifies credentials', async () => {
await expect(user.post(endpoint, {id: 123})).to.eventually.be.rejected.and.eql({ await expect(user.post(endpoint, {id: 123})).to.eventually.be.rejected.and.include({
code: 401, code: 401,
error: 'Error', error: 'Error',
message: 'Invalid API Key provided: ****************************1111', message: 'Invalid API Key provided: aaaabbbb********************1111',
}); });
}); });

View File

@@ -127,7 +127,13 @@ describe('POST /groups/:groupId/quests/abort', () => {
members: {}, members: {},
}); });
expect(Group.prototype.sendChat).to.be.calledOnce; expect(Group.prototype.sendChat).to.be.calledOnce;
expect(Group.prototype.sendChat).to.be.calledWithMatch(/aborted the party quest Wail of the Whale.`/); expect(Group.prototype.sendChat).to.be.calledWithMatch({
message: sinon.match(/aborted the party quest Wail of the Whale.`/),
info: {
quest: 'whale',
type: 'quest_abort',
},
});
stub.restore(); stub.restore();
}); });

View File

@@ -4,6 +4,7 @@ import {
generateUser, generateUser,
} from '../../../../helpers/api-integration/v3'; } from '../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid'; import { v4 as generateUUID } from 'uuid';
import { model as Group } from '../../../../../website/server/models/group';
describe('POST /groups/:groupId/quests/cancel', () => { describe('POST /groups/:groupId/quests/cancel', () => {
let questingGroup; let questingGroup;
@@ -99,6 +100,10 @@ describe('POST /groups/:groupId/quests/cancel', () => {
it('cancels a quest', async () => { it('cancels a quest', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`); await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`); await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
// partyMembers[1] hasn't accepted the invitation, because if he accepts, invitation phase ends.
// The cancel command can be done only in the invitation phase.
let stub = sandbox.spy(Group.prototype, 'sendChat');
let res = await leader.post(`/groups/${questingGroup._id}/quests/cancel`); let res = await leader.post(`/groups/${questingGroup._id}/quests/cancel`);
@@ -135,5 +140,16 @@ describe('POST /groups/:groupId/quests/cancel', () => {
}, },
members: {}, members: {},
}); });
expect(Group.prototype.sendChat).to.be.calledOnce;
expect(Group.prototype.sendChat).to.be.calledWithMatch({
message: sinon.match(/cancelled the party quest Wail of the Whale.`/),
info: {
quest: 'whale',
type: 'quest_cancel',
user: sinon.match.any,
},
});
stub.restore();
}); });
}); });

View File

@@ -54,6 +54,21 @@ describe('POST /tasks/challenge/:challengeId', () => {
expect(tasksOrder.habits).to.include(task.id); expect(tasksOrder.habits).to.include(task.id);
}); });
it('allows non-leader admin to add tasks to a challenge when not a member', async () => {
const admin = await generateUser({'contributor.admin': true});
let task = await admin.post(`/tasks/challenge/${challenge._id}`, {
text: 'test habit from admin',
type: 'habit',
up: false,
down: true,
notes: 1976,
});
let {tasksOrder} = await user.get(`/challenges/${challenge._id}`);
expect(tasksOrder.habits).to.include(task.id);
});
it('returns error when user tries to create task with a alias', async () => { it('returns error when user tries to create task with a alias', async () => {
await expect(user.post(`/tasks/challenge/${challenge._id}`, { await expect(user.post(`/tasks/challenge/${challenge._id}`, {
text: 'test habit', text: 'test habit',

View File

@@ -63,6 +63,38 @@ describe('Groups DELETE /tasks/:id', () => {
}); });
}); });
it('removes deleted taskʾs approval pending notifications from managers', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await user.put(`/tasks/${task._id}/`, {
requiresApproval: true,
});
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(2);
expect(user.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
expect(member2.notifications.length).to.equal(2);
expect(member2.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
await member2.del(`/tasks/${task._id}`);
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(1);
expect(member2.notifications.length).to.equal(1);
});
it('unlinks assigned user', async () => { it('unlinks assigned user', async () => {
await user.del(`/tasks/${task._id}`); await user.del(`/tasks/${task._id}`);

View File

@@ -53,18 +53,29 @@ describe('POST /tasks/:id/approve/:userId', () => {
it('approves an assigned user', async () => { it('approves an assigned user', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`); await user.post(`/tasks/${task._id}/assign/${member._id}`);
await user.post(`/tasks/${task._id}/approve/${member._id}`);
let memberTasks = await member.get('/tasks/user'); let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask); let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await user.post(`/tasks/${task._id}/approve/${member._id}`);
await member.sync(); await member.sync();
expect(member.notifications.length).to.equal(2); expect(member.notifications.length).to.equal(3);
expect(member.notifications[0].type).to.equal('GROUP_TASK_APPROVED'); expect(member.notifications[1].type).to.equal('GROUP_TASK_APPROVED');
expect(member.notifications[0].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
expect(member.notifications[1].type).to.equal('SCORED_TASK');
expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text})); expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
expect(member.notifications[2].type).to.equal('SCORED_TASK');
expect(member.notifications[2].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
memberTasks = await member.get('/tasks/user');
syncedTask = find(memberTasks, findAssignedTask);
expect(syncedTask.group.approval.approved).to.be.true; expect(syncedTask.group.approval.approved).to.be.true;
expect(syncedTask.group.approval.approvingUser).to.equal(user._id); expect(syncedTask.group.approval.approvingUser).to.equal(user._id);
@@ -77,18 +88,28 @@ describe('POST /tasks/:id/approve/:userId', () => {
}); });
await member2.post(`/tasks/${task._id}/assign/${member._id}`); await member2.post(`/tasks/${task._id}/assign/${member._id}`);
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
let memberTasks = await member.get('/tasks/user'); let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask); let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await member.sync(); await member.sync();
expect(member.notifications.length).to.equal(2); expect(member.notifications.length).to.equal(3);
expect(member.notifications[0].type).to.equal('GROUP_TASK_APPROVED'); expect(member.notifications[1].type).to.equal('GROUP_TASK_APPROVED');
expect(member.notifications[0].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
expect(member.notifications[1].type).to.equal('SCORED_TASK');
expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text})); expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
expect(member.notifications[2].type).to.equal('SCORED_TASK');
expect(member.notifications[2].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
memberTasks = await member.get('/tasks/user');
syncedTask = find(memberTasks, findAssignedTask);
expect(syncedTask.group.approval.approved).to.be.true; expect(syncedTask.group.approval.approved).to.be.true;
expect(syncedTask.group.approval.approvingUser).to.equal(member2._id); expect(syncedTask.group.approval.approvingUser).to.equal(member2._id);
@@ -132,6 +153,16 @@ describe('POST /tasks/:id/approve/:userId', () => {
}); });
await member2.post(`/tasks/${task._id}/assign/${member._id}`); await member2.post(`/tasks/${task._id}/assign/${member._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await member2.post(`/tasks/${task._id}/approve/${member._id}`); await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await expect(user.post(`/tasks/${task._id}/approve/${member._id}`)) await expect(user.post(`/tasks/${task._id}/approve/${member._id}`))
.to.eventually.be.rejected.and.to.eql({ .to.eventually.be.rejected.and.to.eql({
@@ -141,6 +172,17 @@ describe('POST /tasks/:id/approve/:userId', () => {
}); });
}); });
it('prevents approving a task if it is not waiting for approval', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await expect(user.post(`/tasks/${task._id}/approve/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalWasNotRequested'),
});
});
it('completes master task when single-completion task is approved', async () => { it('completes master task when single-completion task is approved', async () => {
let sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, { let sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo', text: 'shared completion todo',
@@ -151,6 +193,16 @@ describe('POST /tasks/:id/approve/:userId', () => {
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`); await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`); await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`); await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
let groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`); let groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
@@ -172,6 +224,16 @@ describe('POST /tasks/:id/approve/:userId', () => {
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`); await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`); await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`); await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
let member2Tasks = await member2.get('/tasks/user'); let member2Tasks = await member2.get('/tasks/user');
@@ -193,6 +255,16 @@ describe('POST /tasks/:id/approve/:userId', () => {
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`); await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`); await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`); await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
let groupTasks = await user.get(`/tasks/group/${guild._id}`); let groupTasks = await user.get(`/tasks/group/${guild._id}`);
@@ -214,6 +286,25 @@ describe('POST /tasks/:id/approve/:userId', () => {
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`); await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`); await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
let member2Tasks = await member2.get('/tasks/user');
let member2SyncedTask = find(member2Tasks, findAssignedTask);
await expect(member2.post(`/tasks/${member2SyncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`); await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member2._id}`); await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member2._id}`);

View File

@@ -51,7 +51,7 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
}); });
}); });
it('marks as task as needing more work', async () => { it('marks a task as needing more work', async () => {
const initialNotifications = member.notifications.length; const initialNotifications = member.notifications.length;
await user.post(`/tasks/${task._id}/assign/${member._id}`); await user.post(`/tasks/${task._id}/assign/${member._id}`);
@@ -77,7 +77,7 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
expect(syncedTask.group.approval.requestedDate).to.equal(undefined); expect(syncedTask.group.approval.requestedDate).to.equal(undefined);
// Check that the notification is correct // Check that the notification is correct
expect(member.notifications.length).to.equal(initialNotifications + 1); expect(member.notifications.length).to.equal(initialNotifications + 2);
const notification = member.notifications[member.notifications.length - 1]; const notification = member.notifications[member.notifications.length - 1];
expect(notification.type).to.equal('GROUP_TASK_NEEDS_WORK'); expect(notification.type).to.equal('GROUP_TASK_NEEDS_WORK');
@@ -131,7 +131,7 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
expect(syncedTask.group.approval.requested).to.equal(false); expect(syncedTask.group.approval.requested).to.equal(false);
expect(syncedTask.group.approval.requestedDate).to.equal(undefined); expect(syncedTask.group.approval.requestedDate).to.equal(undefined);
expect(member.notifications.length).to.equal(initialNotifications + 1); expect(member.notifications.length).to.equal(initialNotifications + 2);
const notification = member.notifications[member.notifications.length - 1]; const notification = member.notifications[member.notifications.length - 1];
expect(notification.type).to.equal('GROUP_TASK_NEEDS_WORK'); expect(notification.type).to.equal('GROUP_TASK_NEEDS_WORK');
@@ -167,6 +167,17 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
}); });
await member2.post(`/tasks/${task._id}/assign/${member._id}`); await member2.post(`/tasks/${task._id}/assign/${member._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await member2.post(`/tasks/${task._id}/approve/${member._id}`); await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`)) await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`))
.to.eventually.be.rejected.and.to.eql({ .to.eventually.be.rejected.and.to.eql({

View File

@@ -129,6 +129,13 @@ describe('POST /tasks/:id/score/:direction', () => {
let memberTasks = await member.get('/tasks/user'); let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask); let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await user.post(`/tasks/${task._id}/approve/${member._id}`); await user.post(`/tasks/${task._id}/approve/${member._id}`);
await member.post(`/tasks/${syncedTask._id}/score/up`); await member.post(`/tasks/${syncedTask._id}/score/up`);

View File

@@ -93,15 +93,6 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
expect(syncedTask).to.exist; expect(syncedTask).to.exist;
}); });
it('sends a message to the group when a user claims a task', async () => {
await member.post(`/tasks/${task._id}/assign/${member._id}`);
let updateGroup = await user.get(`/groups/${guild._id}`);
expect(updateGroup.chat[0].text).to.equal(t('userIsClamingTask', {username: member.profile.name, task: task.text}));
expect(updateGroup.chat[0].uuid).to.equal('system');
});
it('assigns a task to a user', async () => { it('assigns a task to a user', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`); await user.post(`/tasks/${task._id}/assign/${member._id}`);
@@ -113,6 +104,17 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
expect(syncedTask).to.exist; expect(syncedTask).to.exist;
}); });
it('sends a notification to assigned user', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await member.sync();
let groupTask = await user.get(`/tasks/group/${guild._id}`);
expect(member.notifications.length).to.equal(1);
expect(member.notifications[0].type).to.equal('GROUP_TASK_ASSIGNED');
expect(member.notifications[0].taskId).to.equal(groupTask._id);
});
it('assigns a task to multiple users', async () => { it('assigns a task to multiple users', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`); await user.post(`/tasks/${task._id}/assign/${member._id}`);
await user.post(`/tasks/${task._id}/assign/${member2._id}`); await user.post(`/tasks/${task._id}/assign/${member2._id}`);

View File

@@ -86,6 +86,13 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
expect(syncedTask).to.not.exist; expect(syncedTask).to.not.exist;
}); });
it('removes task assignment notification from unassigned user', async () => {
await user.post(`/tasks/${task._id}/unassign/${member._id}`);
await member.sync();
expect(member.notifications.length).to.equal(0);
});
it('unassigns a user and only that user from a task', async () => { it('unassigns a user and only that user from a task', async () => {
await user.post(`/tasks/${task._id}/assign/${member2._id}`); await user.post(`/tasks/${task._id}/assign/${member2._id}`);

View File

@@ -9,6 +9,7 @@ describe('POST /user/open-mystery-item', () => {
let mysteryItemKey = 'eyewear_special_summerRogue'; let mysteryItemKey = 'eyewear_special_summerRogue';
let mysteryItemIndex = content.gear.flat[mysteryItemKey].index; let mysteryItemIndex = content.gear.flat[mysteryItemKey].index;
let mysteryItemType = content.gear.flat[mysteryItemKey].type; let mysteryItemType = content.gear.flat[mysteryItemKey].type;
let mysteryItemText = content.gear.flat[mysteryItemKey].text();
beforeEach(async () => { beforeEach(async () => {
user = await generateUser({ user = await generateUser({
@@ -32,5 +33,6 @@ describe('POST /user/open-mystery-item', () => {
expect(response.data.key).to.eql(mysteryItemKey); expect(response.data.key).to.eql(mysteryItemKey);
expect(response.data.index).to.eql(mysteryItemIndex); expect(response.data.index).to.eql(mysteryItemIndex);
expect(response.data.type).to.eql(mysteryItemType); expect(response.data.type).to.eql(mysteryItemType);
expect(response.data.text).to.eql(mysteryItemText);
}); });
}); });

View File

@@ -14,7 +14,7 @@ describe('POST /user/purchase-hourglass/:type/:key', () => {
// More tests in common code unit tests // More tests in common code unit tests
it('buys a hourglass pet', async () => { it('buys an hourglass pet', async () => {
let response = await user.post('/user/purchase-hourglass/pets/MantisShrimp-Base'); let response = await user.post('/user/purchase-hourglass/pets/MantisShrimp-Base');
await user.sync(); await user.sync();
@@ -22,4 +22,22 @@ describe('POST /user/purchase-hourglass/:type/:key', () => {
expect(user.purchased.plan.consecutive.trinkets).to.eql(1); expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
expect(user.items.pets['MantisShrimp-Base']).to.eql(5); expect(user.items.pets['MantisShrimp-Base']).to.eql(5);
}); });
it('buys an hourglass quest', async () => {
let response = await user.post('/user/purchase-hourglass/quests/robot');
await user.sync();
expect(response.message).to.eql(t('hourglassPurchase'));
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
expect(user.items.quests.robot).to.eql(1);
});
it('buys multiple hourglass quests', async () => {
let response = await user.post('/user/purchase-hourglass/quests/robot', {quantity: 2});
await user.sync();
expect(response.message).to.eql(t('hourglassPurchase'));
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
expect(user.items.quests.robot).to.eql(2);
});
}); });

View File

@@ -110,4 +110,22 @@ describe('POST /user/auth/local/login', () => {
let isValidPassword = await bcryptCompare(textPassword, user.auth.local.hashed_password); let isValidPassword = await bcryptCompare(textPassword, user.auth.local.hashed_password);
expect(isValidPassword).to.equal(true); expect(isValidPassword).to.equal(true);
}); });
it('user uses social authentication and has no password', async () => {
await user.unset({
'auth.local.hashed_password': 1,
});
await user.sync();
expect(user.auth.local.hashed_password).to.be.undefined;
await expect(api.post(endpoint, {
username: user.auth.local.username,
password: 'any-password',
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('invalidLoginCredentialsLong'),
});
});
}); });

View File

@@ -44,7 +44,7 @@ describe('DELETE /inbox/messages/:messageId', () => {
}); });
it('deletes one message', async () => { it('deletes one message', async () => {
const messages = await user.get('/inbox/messages'); const messages = await user.get('/inbox/paged-messages');
expect(messages.length).to.equal(3); expect(messages.length).to.equal(3);
@@ -53,7 +53,7 @@ describe('DELETE /inbox/messages/:messageId', () => {
expect(messages[2].text).to.equal('first'); expect(messages[2].text).to.equal('first');
await user.del(`/inbox/messages/${messages[1]._id}`); await user.del(`/inbox/messages/${messages[1]._id}`);
const updatedMessages = await user.get('/inbox/messages'); const updatedMessages = await user.get('/inbox/paged-messages');
expect(updatedMessages.length).to.equal(2); expect(updatedMessages.length).to.equal(2);
expect(updatedMessages[0].text).to.equal('third'); expect(updatedMessages[0].text).to.equal('third');

View File

@@ -0,0 +1,92 @@
import {
generateUser,
} from '../../../helpers/api-integration/v4';
describe('GET /inbox/conversations', () => {
let user;
let otherUser;
let thirdUser;
beforeEach(async () => {
[user, otherUser, thirdUser] = await Promise.all([generateUser(), 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 user.post('/members/send-private-message', {
toUserId: thirdUser.id,
message: 'third',
});
await otherUser.post('/members/send-private-message', {
toUserId: user.id,
message: 'fourth',
});
// message to yourself
await user.post('/members/send-private-message', {
toUserId: user.id,
message: 'fifth',
});
});
it('returns the conversations', async () => {
const result = await user.get('/inbox/conversations');
expect(result.length).to.be.equal(3);
expect(result[0].user).to.be.equal(user.profile.name);
expect(result[0].username).to.be.equal(user.auth.local.username);
expect(result[0].text).to.be.not.empty;
});
it('returns the user inbox messages as an array of ordered messages (from most to least recent)', async () => {
const messages = await user.get('/inbox/paged-messages');
expect(messages.length).to.equal(5);
// message to yourself
expect(messages[0].text).to.equal('fifth');
expect(messages[0].sent).to.equal(false);
expect(messages[0].uuid).to.equal(user._id);
expect(messages[1].text).to.equal('fourth');
expect(messages[2].text).to.equal('third');
expect(messages[3].text).to.equal('second');
expect(messages[4].text).to.equal('first');
});
it('returns four messages when using page-query ', async () => {
const promises = [];
for (let i = 0; i < 10; i++) {
promises.push(user.post('/members/send-private-message', {
toUserId: user.id,
message: 'fourth',
}));
}
await Promise.all(promises);
const messages = await user.get('/inbox/paged-messages?page=1');
expect(messages.length).to.equal(5);
});
it('returns only the messages of one conversation', async () => {
const messages = await user.get(`/inbox/paged-messages?conversation=${otherUser.id}`);
expect(messages.length).to.equal(3);
});
it('returns the correct message format', async () => {
const messages = await otherUser.get(`/inbox/paged-messages?conversation=${user.id}`);
expect(messages[0].toUUID).to.equal(user.id); // from user
expect(messages[1].toUUID).to.not.exist; // only filled if its from the chat partner
expect(messages[2].toUUID).to.equal(user.id); // from user
});
});

View File

@@ -0,0 +1,74 @@
import {
generateUser,
translate as t,
} from '../../../helpers/api-integration/v4';
describe('POST /members/flag-private-message/:messageId', () => {
let userToSendMessage;
let messageToSend = 'Test Private Message';
beforeEach(async () => {
userToSendMessage = await generateUser();
});
it('Allows players to flag their own private message', async () => {
let receiver = await generateUser();
await userToSendMessage.post('/members/send-private-message', {
message: messageToSend,
toUserId: receiver._id,
});
let senderMessages = await userToSendMessage.get('/inbox/paged-messages');
let sendersMessageInSendersInbox = _.find(senderMessages, (message) => {
return message.toUUID === receiver._id && message.text === messageToSend;
});
expect(sendersMessageInSendersInbox).to.exist;
await expect(userToSendMessage.post(`/members/flag-private-message/${sendersMessageInSendersInbox.id}`)).to.eventually.be.ok;
});
it('Flags a private message', async () => {
let receiver = await generateUser();
await userToSendMessage.post('/members/send-private-message', {
message: messageToSend,
toUserId: receiver._id,
});
let receiversMessages = await receiver.get('/inbox/paged-messages');
let sendersMessageInReceiversInbox = _.find(receiversMessages, (message) => {
return message.uuid === userToSendMessage._id && message.text === messageToSend;
});
expect(sendersMessageInReceiversInbox).to.exist;
await expect(receiver.post(`/members/flag-private-message/${sendersMessageInReceiversInbox.id}`)).to.eventually.be.ok;
});
it('Returns an error when user tries to flag a private message that is already flagged', async () => {
let receiver = await generateUser();
await userToSendMessage.post('/members/send-private-message', {
message: messageToSend,
toUserId: receiver._id,
});
let receiversMessages = await receiver.get('/inbox/paged-messages');
let sendersMessageInReceiversInbox = _.find(receiversMessages, (message) => {
return message.uuid === userToSendMessage._id && message.text === messageToSend;
});
expect(sendersMessageInReceiversInbox).to.exist;
await expect(receiver.post(`/members/flag-private-message/${sendersMessageInReceiversInbox.id}`)).to.eventually.be.ok;
await expect(receiver.post(`/members/flag-private-message/${sendersMessageInReceiversInbox.id}`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('messageGroupChatFlagAlreadyReported'),
});
});
});

View File

@@ -11,8 +11,6 @@
], ],
"plugins": [ "plugins": [
"transform-object-rest-spread", "transform-object-rest-spread",
"syntax-async-functions",
"transform-regenerator",
], ],
"comments": false, "comments": false,
} }

View File

@@ -83,12 +83,12 @@ context('avatar.vue', () => {
expect(vm.paddingTop).to.equal('28px'); expect(vm.paddingTop).to.equal('28px');
}); });
it('is 24.5px if user has a pet', () => { it('is 24px if user has a pet', () => {
vm.member.items = { vm.member.items = {
currentPet: { name: 'Foo' }, currentPet: { name: 'Foo' },
}; };
expect(vm.paddingTop).to.equal('24.5px'); expect(vm.paddingTop).to.equal('24px');
}); });
it('is 0px if user has a mount', () => { it('is 0px if user has a mount', () => {

View File

@@ -0,0 +1,61 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import ChallengeDetailComponent from 'client/components/challenges/challengeDetail.vue';
import Store from 'client/libs/store';
const localVue = createLocalVue();
localVue.use(Store);
describe('Challenge Detail', () => {
let store;
let wrapper;
beforeEach(() => {
store = new Store({
state: {
user: {
data: {
contributor: {
admin: false,
},
challenges: [],
stats: {
},
flags: {},
preferences: {},
party: {
quest: {
},
},
},
},
},
actions: {
'members:getChallengeMembers': () => {},
'challenges:getChallenge': () => [
{_id: '1', group: { name: '', type: ''}, memberCount: 1, name: '', summary: '', description: '', leader: '', price: 1},
],
'tasks:getChallengeTasks': () => [
{_id: '1', type: 'habit'},
{_id: '2', type: 'daily'},
{_id: '3', type: 'reward'},
{_id: '4', type: 'todo'},
],
},
getters: {
},
});
wrapper = shallowMount(ChallengeDetailComponent, {
store,
localVue,
mocks: {
$t: (string) => string,
},
});
});
it('removes a destroyed task from task list', () => {
let taskToRemove = {_id: '1', type: 'habit'};
wrapper.vm.taskDestroyed(taskToRemove);
expect(wrapper.vm.tasksByType[taskToRemove.type].length).to.eq(0);
});
});

View File

@@ -7,14 +7,14 @@ describe('highlightUserAndEmail', () => {
const result = highlightUsers(text, 'user', 'displayedUser'); const result = highlightUsers(text, 'user', 'displayedUser');
expect(result).to.contain('<span class="at-highlight at-text">@displayedUser</span>'); expect(result).to.contain('<span class="at-text at-highlight">@displayedUser</span>');
}); });
it('highlights username', () => { it('highlights username', () => {
const text = 'hello @user'; const text = 'hello @user';
const result = highlightUsers(text, 'user', 'displayedUser'); const result = highlightUsers(text, 'user', 'displayedUser');
expect(result).to.contain('<span class="at-highlight at-text">@user</span>'); expect(result).to.contain('<span class="at-text at-highlight">@user</span>');
}); });
it('not highlights any email', () => { it('not highlights any email', () => {
@@ -32,8 +32,8 @@ describe('highlightUserAndEmail', () => {
const result = highlightUsers(text, 'use', 'mentions'); const result = highlightUsers(text, 'use', 'mentions');
expect(result).to.contain('<span class="at-highlight at-text">@mentions</span>'); expect(result).to.contain('<span class="at-text at-highlight">@mentions</span>');
expect(result).to.contain('<span class="at-highlight at-text">@use</span>'); expect(result).to.contain('<span class="at-text at-highlight">@use</span>');
expect(result).to.not.contain('<span class="at-highlight at-text">@mentions</span>.com'); expect(result).to.not.contain('<span class="at-text at-highlight">@mentions</span>.com');
}); });
}); });

View File

@@ -131,10 +131,12 @@ describe('getTaskClasses getter', () => {
up: { up: {
bg: 'task-good-control-bg', bg: 'task-good-control-bg',
inner: 'task-good-control-inner-habit', inner: 'task-good-control-inner-habit',
icon: 'task-good-control-icon',
}, },
down: { down: {
bg: 'task-disabled-habit-control-bg', bg: 'task-disabled-habit-control-bg',
inner: 'task-disabled-habit-control-inner', inner: 'task-disabled-habit-control-inner',
icon: 'task-good-control-icon',
}, },
}); });
}); });

View File

@@ -62,6 +62,18 @@ describe('inAppRewards', () => {
expect(result[9].path).to.eql('potion'); expect(result[9].path).to.eql('potion');
}); });
it('ignores null/undefined entries', () => {
user.pinnedItems = testPinnedItems;
user.pinnedItems.push(null);
user.pinnedItems.push(undefined);
user.pinnedItemsOrder = testPinnedItemsOrder;
let result = inAppRewards(user);
expect(result[2].path).to.eql('armoire');
expect(result[9].path).to.eql('potion');
});
it('does not return seasonal items which have been unpinned', () => { it('does not return seasonal items which have been unpinned', () => {
if (officialPinnedItems.length === 0) { if (officialPinnedItems.length === 0) {
return; // if no seasonal items, this test is not applicable return; // if no seasonal items, this test is not applicable

View File

@@ -9,6 +9,7 @@ import {
import i18n from '../../../../website/common/script/i18n'; import i18n from '../../../../website/common/script/i18n';
import content from '../../../../website/common/script/content/index'; import content from '../../../../website/common/script/content/index';
import errorMessage from '../../../../website/common/script/libs/errorMessage'; import errorMessage from '../../../../website/common/script/libs/errorMessage';
import { defaultsDeep } from 'lodash';
describe('shared.ops.buy', () => { describe('shared.ops.buy', () => {
let user; let user;
@@ -16,6 +17,10 @@ describe('shared.ops.buy', () => {
beforeEach(() => { beforeEach(() => {
user = generateUser({ user = generateUser({
stats: { gp: 200 },
});
defaultsDeep(user, {
items: { items: {
gear: { gear: {
owned: { owned: {
@@ -26,7 +31,6 @@ describe('shared.ops.buy', () => {
}, },
}, },
}, },
stats: { gp: 200 },
}); });
sinon.stub(analytics, 'track'); sinon.stub(analytics, 'track');
@@ -76,6 +80,13 @@ describe('shared.ops.buy', () => {
headAccessory_special_redHeadband: true, headAccessory_special_redHeadband: true,
headAccessory_special_whiteHeadband: true, headAccessory_special_whiteHeadband: true,
headAccessory_special_yellowHeadband: true, headAccessory_special_yellowHeadband: true,
eyewear_special_blackHalfMoon: true,
eyewear_special_blueHalfMoon: true,
eyewear_special_greenHalfMoon: true,
eyewear_special_pinkHalfMoon: true,
eyewear_special_redHalfMoon: true,
eyewear_special_whiteHalfMoon: true,
eyewear_special_yellowHalfMoon: true,
}); });
}); });
@@ -138,4 +149,52 @@ describe('shared.ops.buy', () => {
buy(user, {params: {key: 'potion'}, quantity: 2}); buy(user, {params: {key: 'potion'}, quantity: 2});
expect(user.stats.hp).to.eql(50); expect(user.stats.hp).to.eql(50);
}); });
it('errors if user supplies a non-numeric quantity', (done) => {
try {
buy(user, {
params: {
key: 'dilatoryDistress1',
},
type: 'quest',
quantity: 'bogle',
});
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(errorMessage('invalidQuantity'));
done();
}
});
it('errors if user supplies a negative quantity', (done) => {
try {
buy(user, {
params: {
key: 'dilatoryDistress1',
},
type: 'quest',
quantity: -3,
});
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(errorMessage('invalidQuantity'));
done();
}
});
it('errors if user supplies a decimal quantity', (done) => {
try {
buy(user, {
params: {
key: 'dilatoryDistress1',
},
type: 'quest',
quantity: 1.83,
});
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(errorMessage('invalidQuantity'));
done();
}
});
}); });

View File

@@ -11,6 +11,7 @@ import {
} from '../../../../website/common/script/libs/errors'; } from '../../../../website/common/script/libs/errors';
import i18n from '../../../../website/common/script/i18n'; import i18n from '../../../../website/common/script/i18n';
import errorMessage from '../../../../website/common/script/libs/errorMessage'; import errorMessage from '../../../../website/common/script/libs/errorMessage';
import { defaultsDeep } from 'lodash';
function buyGear (user, req, analytics) { function buyGear (user, req, analytics) {
let buyOp = new BuyMarketGearOperation(user, req, analytics); let buyOp = new BuyMarketGearOperation(user, req, analytics);
@@ -24,6 +25,10 @@ describe('shared.ops.buyMarketGear', () => {
beforeEach(() => { beforeEach(() => {
user = generateUser({ user = generateUser({
stats: { gp: 200 },
});
defaultsDeep(user, {
items: { items: {
gear: { gear: {
owned: { owned: {
@@ -34,7 +39,6 @@ describe('shared.ops.buyMarketGear', () => {
}, },
}, },
}, },
stats: { gp: 200 },
}); });
sinon.stub(shared, 'randomVal'); sinon.stub(shared, 'randomVal');
@@ -71,6 +75,13 @@ describe('shared.ops.buyMarketGear', () => {
headAccessory_special_redHeadband: true, headAccessory_special_redHeadband: true,
headAccessory_special_whiteHeadband: true, headAccessory_special_whiteHeadband: true,
headAccessory_special_yellowHeadband: true, headAccessory_special_yellowHeadband: true,
eyewear_special_blackHalfMoon: true,
eyewear_special_blueHalfMoon: true,
eyewear_special_greenHalfMoon: true,
eyewear_special_pinkHalfMoon: true,
eyewear_special_redHalfMoon: true,
eyewear_special_whiteHalfMoon: true,
eyewear_special_yellowHalfMoon: true,
}); });
expect(analytics.track).to.be.calledOnce; expect(analytics.track).to.be.calledOnce;
}); });

View File

@@ -108,6 +108,47 @@ describe('shared.ops.purchase', () => {
done(); done();
} }
}); });
it('returns error when user supplies a non-numeric quantity', (done) => {
let type = 'eggs';
let key = 'Wolf';
try {
purchase(user, {params: {type, key}, quantity: 'jamboree'}, analytics);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity'));
done();
}
});
it('returns error when user supplies a negative quantity', (done) => {
let type = 'eggs';
let key = 'Wolf';
user.balance = 10;
try {
purchase(user, {params: {type, key}, quantity: -2}, analytics);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity'));
done();
}
});
it('returns error when user supplies a decimal quantity', (done) => {
let type = 'eggs';
let key = 'Wolf';
user.balance = 10;
try {
purchase(user, {params: {type, key}, quantity: 2.9}, analytics);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity'));
done();
}
});
}); });
context('successful purchase', () => { context('successful purchase', () => {
@@ -168,7 +209,7 @@ describe('shared.ops.purchase', () => {
it('purchases quest bundles', () => { it('purchases quest bundles', () => {
let startingBalance = user.balance; let startingBalance = user.balance;
let clock = sandbox.useFakeTimers(moment('2017-05-20').valueOf()); let clock = sandbox.useFakeTimers(moment('2019-05-20').valueOf());
let type = 'bundles'; let type = 'bundles';
let key = 'featheredFriends'; let key = 'featheredFriends';
let price = 1.75; let price = 1.75;

View File

@@ -169,6 +169,42 @@ describe('shared.ops.feed', () => {
expect(user.items.pets['Wolf-Base']).to.equal(7); expect(user.items.pets['Wolf-Base']).to.equal(7);
}); });
it('awards All Your Base achievement', () => {
user.items.pets['Wolf-Spooky'] = 5;
user.items.food.Milk = 2;
user.items.mounts = {
'Wolf-Base': true,
'TigerCub-Base': true,
'PandaCub-Base': true,
'LionCub-Base': true,
'Fox-Base': true,
'FlyingPig-Base': true,
'Dragon-Base': true,
'Cactus-Base': true,
'BearCub-Base': true,
};
feed(user, {params: {pet: 'Wolf-Spooky', food: 'Milk'}});
expect(user.achievements.allYourBase).to.eql(true);
});
it('awards Arid Authority achievement', () => {
user.items.pets['Wolf-Spooky'] = 5;
user.items.food.Milk = 2;
user.items.mounts = {
'Wolf-Desert': true,
'TigerCub-Desert': true,
'PandaCub-Desert': true,
'LionCub-Desert': true,
'Fox-Desert': true,
'FlyingPig-Desert': true,
'Dragon-Desert': true,
'Cactus-Desert': true,
'BearCub-Desert': true,
};
feed(user, {params: {pet: 'Wolf-Spooky', food: 'Milk'}});
expect(user.achievements.aridAuthority).to.eql(true);
});
it('evolves the pet into a mount when feeding user.items.pets[pet] >= 50', () => { it('evolves the pet into a mount when feeding user.items.pets[pet] >= 50', () => {
user.items.pets['Wolf-Base'] = 49; user.items.pets['Wolf-Base'] = 49;
user.items.food.Milk = 2; user.items.food.Milk = 2;

View File

@@ -93,6 +93,22 @@ describe('shared.ops.hatch', () => {
done(); done();
} }
}); });
it('does not allow hatching quest pet egg using wacky potion', (done) => {
user.items.eggs = {Bunny: 1};
user.items.hatchingPotions = {Veggie: 1};
user.items.pets = {};
try {
hatch(user, {params: {egg: 'Bunny', hatchingPotion: 'Veggie'}});
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('messageInvalidEggPotionCombo'));
expect(user.items.pets).to.be.empty;
expect(user.items.eggs).to.eql({Bunny: 1});
expect(user.items.hatchingPotions).to.eql({Veggie: 1});
done();
}
});
}); });
context('successful hatching', () => { context('successful hatching', () => {
@@ -143,6 +159,42 @@ describe('shared.ops.hatch', () => {
expect(user.items.eggs).to.eql({Wolf: 0}); expect(user.items.eggs).to.eql({Wolf: 0});
expect(user.items.hatchingPotions).to.eql({Base: 0}); expect(user.items.hatchingPotions).to.eql({Base: 0});
}); });
it('awards Back to Basics achievement', () => {
user.items.pets = {
'Wolf-Base': 5,
'TigerCub-Base': 5,
'PandaCub-Base': 10,
'LionCub-Base': 5,
'Fox-Base': 5,
'FlyingPig-Base': 5,
'Dragon-Base': 5,
'Cactus-Base': 15,
'BearCub-Base': 5,
};
user.items.eggs = {Wolf: 1};
user.items.hatchingPotions = {Spooky: 1};
hatch(user, {params: {egg: 'Wolf', hatchingPotion: 'Spooky'}});
expect(user.achievements.backToBasics).to.eql(true);
});
it('awards Dust Devil achievement', () => {
user.items.pets = {
'Wolf-Desert': 5,
'TigerCub-Desert': 5,
'PandaCub-Desert': 10,
'LionCub-Desert': 5,
'Fox-Desert': 5,
'FlyingPig-Desert': 5,
'Dragon-Desert': 5,
'Cactus-Desert': 15,
'BearCub-Desert': 5,
};
user.items.eggs = {Wolf: 1};
user.items.hatchingPotions = {Spooky: 1};
hatch(user, {params: {egg: 'Wolf', hatchingPotion: 'Spooky'}});
expect(user.achievements.dustDevil).to.eql(true);
});
}); });
}); });
}); });

View File

@@ -36,7 +36,9 @@ describe('shared.ops.openMysteryItem', () => {
expect(user.items.gear.owned[mysteryItemKey]).to.be.true; expect(user.items.gear.owned[mysteryItemKey]).to.be.true;
expect(message).to.equal(i18n.t('mysteryItemOpened')); expect(message).to.equal(i18n.t('mysteryItemOpened'));
expect(data).to.eql(content.gear.flat[mysteryItemKey]); let item = _.cloneDeep(content.gear.flat[mysteryItemKey]);
item.text = content.gear.flat[mysteryItemKey].text();
expect(data).to.eql(item);
expect(user.notifications.length).to.equal(0); expect(user.notifications.length).to.equal(0);
}); });
}); });

View File

@@ -0,0 +1,18 @@
import {
generateUser,
} from '../../helpers/common.helper';
import {addPinnedGear} from '../../../website/common/script/ops/pinnedGearUtils';
describe('shared.ops.pinnedGearUtils.addPinnedGear', () => {
let user;
beforeEach(() => {
user = generateUser();
});
it('not adds an item with empty properties to pinnedItems', () => {
addPinnedGear(user, undefined, undefined);
expect(user.pinnedItems.length).to.be.eql(0);
});
});

View File

@@ -49,6 +49,7 @@ describe('shared.ops.rebirth', () => {
let [, message] = rebirth(user); let [, message] = rebirth(user);
expect(message).to.equal(i18n.t('rebirthComplete')); expect(message).to.equal(i18n.t('rebirthComplete'));
expect(user.flags.lastFreeRebirth).to.exist;
}); });
it('rebirths a user with not enough gems but more than max level', () => { it('rebirths a user with not enough gems but more than max level', () => {
@@ -60,6 +61,16 @@ describe('shared.ops.rebirth', () => {
expect(message).to.equal(i18n.t('rebirthComplete')); expect(message).to.equal(i18n.t('rebirthComplete'));
}); });
it('rebirths a user using gems if over max level but rebirthed recently', () => {
user.stats.lvl = MAX_LEVEL + 1;
user.flags.lastFreeRebirth = new Date();
let [, message] = rebirth(user);
expect(message).to.equal(i18n.t('rebirthComplete'));
expect(user.balance).to.equal(0);
});
it('resets user\'s tasks values except for rewards to 0', () => { it('resets user\'s tasks values except for rewards to 0', () => {
tasks[0].value = 1; tasks[0].value = 1;
tasks[1].value = 1; tasks[1].value = 1;
@@ -75,10 +86,14 @@ describe('shared.ops.rebirth', () => {
}); });
it('resets user\'s daily streaks to 0', () => { it('resets user\'s daily streaks to 0', () => {
tasks[0].counterDown = 1; // Habit
tasks[0].counterUp = 1; // Habit
tasks[1].streak = 1; // Daily tasks[1].streak = 1; // Daily
rebirth(user, tasks); rebirth(user, tasks);
expect(tasks[0].counterDown).to.equal(0);
expect(tasks[0].counterUp).to.equal(0);
expect(tasks[1].streak).to.equal(0); expect(tasks[1].streak).to.equal(0);
}); });

View File

@@ -153,7 +153,7 @@ describe('shared.ops.scoreTask', () => {
it('does not give a streak achievement for a streak of zero', () => { it('does not give a streak achievement for a streak of zero', () => {
let task = generateDaily({ userId: ref.afterUser._id, text: 'some daily', streak: -1 }); let task = generateDaily({ userId: ref.afterUser._id, text: 'some daily', streak: -1 });
scoreTask({ user: ref.afterUser, task, direction: 'up' }); scoreTask({ user: ref.afterUser, task, direction: 'up' });
expect(ref.afterUser.achievements.streak).to.be.undefined; expect(ref.afterUser.achievements.streak).to.equal(0);
}); });
it('does not remove a streak achievement when unticking a Daily gives a streak of zero', () => { it('does not remove a streak achievement when unticking a Daily gives a streak of zero', () => {

View File

@@ -85,6 +85,19 @@ describe('shared.ops.sell', () => {
} }
}); });
it('returns error when trying to sell Saddle', (done) => {
const foodType = 'food';
const saddleKey = 'Saddle';
user.items[foodType][saddleKey] = 1;
try {
sell(user, {params: {type: foodType, key: saddleKey}});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('foodSaddleSellWarningNote'));
done();
}
});
it('reduces item count from user', () => { it('reduces item count from user', () => {
sell(user, {params: { type, key } }); sell(user, {params: { type, key } });

View File

@@ -9,13 +9,14 @@ import hatchingPotions from '../../website/common/script/content/hatching-potion
describe('hatchingPotions', () => { describe('hatchingPotions', () => {
describe('all', () => { describe('all', () => {
it('is a combination of drop and premium potions', () => { it('is a combination of drop, premium, and wacky potions', () => {
let dropNumber = Object.keys(hatchingPotions.drops).length; let dropNumber = Object.keys(hatchingPotions.drops).length;
let premiumNumber = Object.keys(hatchingPotions.premium).length; let premiumNumber = Object.keys(hatchingPotions.premium).length;
let wackyNumber = Object.keys(hatchingPotions.wacky).length;
let allNumber = Object.keys(hatchingPotions.all).length; let allNumber = Object.keys(hatchingPotions.all).length;
expect(allNumber).to.be.greaterThan(0); expect(allNumber).to.be.greaterThan(0);
expect(allNumber).to.equal(dropNumber + premiumNumber); expect(allNumber).to.equal(dropNumber + premiumNumber + wackyNumber);
}); });
it('contains basic information about each potion', () => { it('contains basic information about each potion', () => {

View File

@@ -47,6 +47,18 @@ describe('stable', () => {
}); });
}); });
describe('wackyPets', () => {
it('contains a pet for each wacky potion * each drop egg', () => {
let numberOfWackyPotions = Object.keys(potions.wacky).length;
let numberOfDropEggs = Object.keys(eggs.drops).length;
let numberOfWackyPets = Object.keys(stable.wackyPets).length;
let expectedTotal = numberOfWackyPotions * numberOfDropEggs;
expect(numberOfWackyPets).to.be.greaterThan(0);
expect(numberOfWackyPets).to.equal(expectedTotal);
});
});
describe('specialPets', () => { describe('specialPets', () => {
it('each value is a valid translation string', () => { it('each value is a valid translation string', () => {
each(stable.specialPets, (pet) => { each(stable.specialPets, (pet) => {
@@ -107,10 +119,11 @@ describe('stable', () => {
let questNumber = Object.keys(stable.questPets).length; let questNumber = Object.keys(stable.questPets).length;
let specialNumber = Object.keys(stable.specialPets).length; let specialNumber = Object.keys(stable.specialPets).length;
let premiumNumber = Object.keys(stable.premiumPets).length; let premiumNumber = Object.keys(stable.premiumPets).length;
let wackyNumber = Object.keys(stable.wackyPets).length;
let allNumber = Object.keys(stable.petInfo).length; let allNumber = Object.keys(stable.petInfo).length;
expect(allNumber).to.be.greaterThan(0); expect(allNumber).to.be.greaterThan(0);
expect(allNumber).to.equal(dropNumber + questNumber + specialNumber + premiumNumber); expect(allNumber).to.equal(dropNumber + questNumber + specialNumber + premiumNumber + wackyNumber);
}); });
it('contains basic information about each pet', () => { it('contains basic information about each pet', () => {

View File

@@ -4,6 +4,7 @@ import { requester } from './requester';
import { import {
getDocument as getDocumentFromMongo, getDocument as getDocumentFromMongo,
updateDocument as updateDocumentInMongo, updateDocument as updateDocumentInMongo,
unsetDocument as unsetDocumentInMongo,
} from '../mongo'; } from '../mongo';
import { import {
assign, assign,
@@ -29,6 +30,18 @@ class ApiObject {
return this; return this;
} }
async unset (options) {
if (isEmpty(options)) {
return;
}
await unsetDocumentInMongo(this._docType, this, options);
_updateLocalParameters((this, options));
return this;
}
async sync () { async sync () {
let updatedDoc = await getDocumentFromMongo(this._docType, this); let updatedDoc = await getDocumentFromMongo(this._docType, this);

View File

@@ -13,10 +13,16 @@ import * as Tasks from '../../../../website/server/models/task';
// parameter, such as the number of wolf eggs the user has, // parameter, such as the number of wolf eggs the user has,
// , you can do so by passing in the full path as a string: // , you can do so by passing in the full path as a string:
// { 'items.eggs.Wolf': 10 } // { 'items.eggs.Wolf': 10 }
export async function generateUser (update = {}) { //
let username = (Date.now() + generateUUID()).substring(0, 20); // To manually set a username, email or password pass it in as
let password = 'password'; // an object for the second parameter. Only overrides need to be
let email = `${username}@example.com`; // added. Items that don't exist will be autogenerated.
// Example: generateUser({}, { username: 'TestName' }) adds user
// with the 'TestName' username.
export async function generateUser (update = {}, overrides = {}) {
let username = overrides.username || (Date.now() + generateUUID()).substring(0, 20);
let password = overrides.password || 'password';
let email = overrides.email || `${username}@example.com`;
let user = await requester().post('/user/auth/local/register', { let user = await requester().post('/user/auth/local/register', {
username, username,
@@ -80,7 +86,7 @@ export async function generateGroup (leader, details = {}, update = {}) {
// This is generate group + the ability to create // This is generate group + the ability to create
// real users to populate it. The settings object // real users to populate it. The settings object
// takes in: // takes in:
// members: Number - the number of group members to create. Defaults to 0. // members: Number - the number of group members to create. Defaults to 0. Does not include group leader.
// inivtes: Number - the number of users to create and invite to the group. Defaults to 0. // inivtes: Number - the number of users to create and invite to the group. Defaults to 0.
// groupDetails: Object - how to initialize the group // groupDetails: Object - how to initialize the group
// leaderDetails: Object - defaults for the leader, defaults with a gem balance so the user // leaderDetails: Object - defaults for the leader, defaults with a gem balance so the user

View File

@@ -80,7 +80,7 @@ export async function generateGroup (leader, details = {}, update = {}) {
// This is generate group + the ability to create // This is generate group + the ability to create
// real users to populate it. The settings object // real users to populate it. The settings object
// takes in: // takes in:
// members: Number - the number of group members to create. Defaults to 0. // members: Number - the number of group members to create. Defaults to 0. Does not include group leader.
// inivtes: Number - the number of users to create and invite to the group. Defaults to 0. // inivtes: Number - the number of users to create and invite to the group. Defaults to 0.
// groupDetails: Object - how to initialize the group // groupDetails: Object - how to initialize the group
// leaderDetails: Object - defaults for the leader, defaults with a gem balance so the user // leaderDetails: Object - defaults for the leader, defaults with a gem balance so the user

View File

@@ -8,6 +8,7 @@ import mongo from './mongo'; // eslint-disable-line
import moment from 'moment'; import moment from 'moment';
import i18n from '../../website/common/script/i18n'; import i18n from '../../website/common/script/i18n';
import * as Tasks from '../../website/server/models/task'; import * as Tasks from '../../website/server/models/task';
export { translationCheck } from './translate';
afterEach((done) => { afterEach((done) => {
sandbox.restore(); sandbox.restore();

View File

@@ -9,6 +9,7 @@ import {
} from '../../website/server/models/task'; } from '../../website/server/models/task';
export {translate} from './translate'; export {translate} from './translate';
export function generateUser (options = {}) { export function generateUser (options = {}) {
let user = new User(options).toObject(); let user = new User(options).toObject();

View File

@@ -98,6 +98,19 @@ export async function updateDocument (collectionName, doc, update) {
}); });
} }
// Unset a property in the database.
// Useful for testing.
export async function unsetDocument (collectionName, doc, update) {
let collection = mongoose.connection.db.collection(collectionName);
return new Promise((resolve) => {
collection.updateOne({ _id: doc._id }, { $unset: update }, (updateErr) => {
if (updateErr) throw new Error(`Error updating ${collectionName}: ${updateErr}`);
resolve();
});
});
}
export async function getDocument (collectionName, doc) { export async function getDocument (collectionName, doc) {
let collection = mongoose.connection.db.collection(collectionName); let collection = mongoose.connection.db.collection(collectionName);

View File

@@ -16,3 +16,9 @@ export function translate (key, variables, language) {
return translatedString; return translatedString;
} }
export function translationCheck (translatedString) {
expect(translatedString).to.not.be.empty;
expect(translatedString).to.not.eql(STRING_ERROR_MSG);
expect(translatedString).to.not.match(STRING_DOES_NOT_EXIST_MSG);
}

View File

@@ -60,7 +60,7 @@ const baseConfig = {
}), }),
postcss: [ postcss: [
autoprefixer({ autoprefixer({
browsers: ['last 2 versions'], overrideBrowserslist: ['last 2 versions'],
}), }),
postcssEasyImport(), postcssEasyImport(),
], ],
@@ -103,6 +103,7 @@ const baseConfig = {
options: { options: {
plugins: [ plugins: [
{removeViewBox: false}, {removeViewBox: false},
{convertPathData: {noSpaceAfterFlags: false}},
], ],
}, },
}, },
@@ -124,6 +125,7 @@ const baseConfig = {
options: { options: {
plugins: [ plugins: [
{removeViewBox: false}, {removeViewBox: false},
{convertPathData: {noSpaceAfterFlags: false}},
], ],
}, },
}, },

View File

@@ -12,6 +12,8 @@ div
banned-account-modal banned-account-modal
amazon-payments-modal(v-if='!isStaticPage') amazon-payments-modal(v-if='!isStaticPage')
payments-success-modal payments-success-modal
sub-cancel-modal-confirm(v-if='isUserLoaded')
sub-canceled-modal(v-if='isUserLoaded')
snackbars snackbars
router-view(v-if="!isUserLoggedIn || isStaticPage") router-view(v-if="!isUserLoggedIn || isStaticPage")
template(v-else) template(v-else)
@@ -55,6 +57,7 @@ div
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 100vh; min-height: 100vh;
overflow-x: hidden;
} }
#loading-screen-inapp { #loading-screen-inapp {
@@ -87,7 +90,6 @@ div
} }
.container-fluid { .container-fluid {
overflow-x: hidden;
flex: 1 0 auto; flex: 1 0 auto;
} }
@@ -187,6 +189,8 @@ import notifications from 'client/mixins/notifications';
import { setup as setupPayments } from 'client/libs/payments'; import { setup as setupPayments } from 'client/libs/payments';
import amazonPaymentsModal from 'client/components/payments/amazonModal'; import amazonPaymentsModal from 'client/components/payments/amazonModal';
import paymentsSuccessModal from 'client/components/payments/successModal'; import paymentsSuccessModal from 'client/components/payments/successModal';
import subCancelModalConfirm from 'client/components/payments/cancelModalConfirm';
import subCanceledModal from 'client/components/payments/canceledModal';
import spellsMixin from 'client/mixins/spells'; import spellsMixin from 'client/mixins/spells';
import { CONSTANTS, getLocalSetting, removeLocalSetting } from 'client/libs/userlocalManager'; import { CONSTANTS, getLocalSetting, removeLocalSetting } from 'client/libs/userlocalManager';
@@ -210,6 +214,8 @@ export default {
amazonPaymentsModal, amazonPaymentsModal,
bannedAccountModal, bannedAccountModal,
paymentsSuccessModal, paymentsSuccessModal,
subCancelModalConfirm,
subCanceledModal,
}, },
data () { data () {
return { return {
@@ -650,5 +656,6 @@ export default {
<style src="assets/css/sprites/spritesmith-main-22.css"></style> <style src="assets/css/sprites/spritesmith-main-22.css"></style>
<style src="assets/css/sprites/spritesmith-main-23.css"></style> <style src="assets/css/sprites/spritesmith-main-23.css"></style>
<style src="assets/css/sprites/spritesmith-main-24.css"></style> <style src="assets/css/sprites/spritesmith-main-24.css"></style>
<style src="assets/css/sprites/spritesmith-main-25.css"></style>
<style src="assets/css/sprites.css"></style> <style src="assets/css/sprites.css"></style>
<style src="smartbanner.js/dist/smartbanner.min.css"></style> <style src="smartbanner.js/dist/smartbanner.min.css"></style>

View File

@@ -10,6 +10,12 @@
height: 219px; height: 219px;
} }
.Pet_HatchingPotion_Veggie {
background: url("~assets/images/Pet_HatchingPotion_Veggie.gif") no-repeat;
width: 68px;
height: 68px;
}
.Gems { .Gems {
display:inline-block; display:inline-block;
margin-right:5px; margin-right:5px;

View File

@@ -1,54 +1,30 @@
.promo_armoire_backgrounds_201902 { .promo_armoire_backgrounds_201909 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -277px; background-position: 0px 0px;
width: 423px; width: 423px;
height: 147px; height: 147px;
} }
.promo_bird_buddies_bundle { .promo_desert_pet_achievements {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -425px; background-position: 0px -296px;
width: 420px; width: 204px;
height: 147px; height: 102px;
} }
.promo_mystery_201901 { .promo_rocking_reptiles_bundle {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -662px -296px; background-position: 0px -148px;
width: 282px; width: 420px;
height: 147px; height: 147px;
} }
.promo_take_this { .promo_take_this {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -972px -148px; background-position: -424px -211px;
width: 96px; width: 96px;
height: 69px; height: 69px;
} }
.promo_valentines { .scene_medal {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -662px -148px; background-position: -424px 0px;
width: 309px; width: 210px;
height: 147px; height: 210px;
}
.promo_valentines_potions {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -662px 0px;
width: 420px;
height: 147px;
}
.scene_apollo {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -573px;
width: 279px;
height: 147px;
}
.scene_eating_healthy {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -328px 0px;
width: 333px;
height: 252px;
}
.scene_yesterdailies {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px 0px;
width: 327px;
height: 276px;
} }

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

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