Compare commits

..

6 Commits

Author SHA1 Message Date
Sabe Jones 93437a00dd 3.78.0 2017-03-02 21:05:49 +00:00
Sabe Jones 31afa0345e chore(news): Bailey 2017-03-02 2017-03-02 20:59:03 +00:00
Sabe Jones b2c278b00a fix(sprites): correct shop canvas 2017-03-02 20:58:42 +00:00
Sabe Jones b54037f230 chore(event): disable Cupid Potions 2017-03-02 20:49:02 +00:00
Sabe Jones 488dcb916a chore(sprites): compile 2017-03-02 20:44:25 +00:00
Sabe Jones 10792b65be feat(content): add Armoire and BGs 2017-03 2017-03-02 20:44:00 +00:00
1624 changed files with 58965 additions and 85179 deletions
+5 -2
View File
@@ -8,13 +8,16 @@ dist/
dist-client/
# Not linted
migrations/*
website/client-old/
scripts/*
test/server_side/**/*
test/client-old/spec/**/*
# Temporarilly disabled. These should be removed when the linting errors are fixed TODO
migrations/*
scripts/*
website/common/script/content/index.js
website/common/browserify.js
test/content/**/*
Gruntfile.js
gulpfile.js
gulp
+7 -5
View File
@@ -1,19 +1,21 @@
[//]: # (Before logging this issue, please post to the Report a Bug guild from the Habitica website's Help menu. Most bugs can be handled quickly there. If a GitHub issue is needed, you will be advised of that by a moderator or staff member -- a player with a dark blue or purple name. It is recommended that you don't create a new issue unless advised to.)
[//]: # (Before logging this issue, look through common problems at https://github.com/HabitRPG/habitrpg/issues If you find your issue there, read at least the first post to see if there is a workaround for you)
[//]: # (Bugs in the mobile apps can also be reported there.)
[//]: # (Github is primarily used for reporting bugs. If you have a feature request, use "Help > Request a Feature" so that the feature request can be vetted by the larger Habitica community)
[//]: # (If you have a feature request, use "Help > Request a Feature", not GitHub or the Report a Bug guild.)
[//]: # (To report a bug in one of the mobile apps, please report it in the correct repository. Android: https://github.com/HabitRPG/habitrpg-android, iOS: https://github.com/HabitRPG/habitrpg-ios)
[//]: # (For more guidelines see https://github.com/HabitRPG/habitrpg/issues/2760)
[//]: # (Fill out relevant information - UUID is found in Settings -> API)
### General Info
General Info
* UUID:
* Browser:
* OS:
### Description
[//]: # (Describe bug in detail here. Include screenshots if helpful.)
[//]: # (Describe bug in detail here. Include pictures if helpful.)
#### Console Errors
[//]: # (Include any JavaScript console errors here.)
+8 -10
View File
@@ -12,24 +12,22 @@ before_install:
- $CXX --version
- npm install -g npm@4
- if [ $REQUIRES_SERVER ]; then sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10; echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list; sudo apt-get update; sudo apt-get install mongodb-org-server; fi
install:
- npm install &> npm.install.log || (cat npm.install.log; false)
before_script:
- npm run test:build
- cp config.json.example config.json
- if [ $REQUIRES_SERVER ]; then until nc -z localhost 27017; do echo Waiting for MongoDB; sleep 1; done; export DISPLAY=:99; fi
script:
- npm run $TEST
- if [ $COVERAGE ]; then ./node_modules/.bin/lcov-result-merger 'coverage/**/*.info' | ./node_modules/coveralls/bin/coveralls.js; fi
after_script:
- ./node_modules/.bin/lcov-result-merger 'coverage/**/*.info' | ./node_modules/coveralls/bin/coveralls.js
script: npm run $TEST
env:
global:
- CXX=g++-4.8
- DISABLE_REQUEST_LOGGING=true
matrix:
- TEST="lint"
- TEST="test:api-v3" REQUIRES_SERVER=true COVERAGE=true
- TEST="test:api-v3" REQUIRES_SERVER=true
- TEST="test:sanity"
- TEST="test:content" COVERAGE=true
- TEST="test:common" COVERAGE=true
- TEST="test:karma" COVERAGE=true
- TEST="client:unit" COVERAGE=true
- TEST="test:content"
- TEST="test:common"
- TEST="test:karma"
- TEST="client:unit"
+32 -5
View File
@@ -1,16 +1,43 @@
FROM node:boron
FROM ubuntu:trusty
MAINTAINER Sabe Jones <sabe@habitica.com>
# Avoid ERROR: invoke-rc.d: policy-rc.d denied execution of start.
RUN echo -e '#!/bin/sh\nexit 0' > /usr/sbin/policy-rc.d
# Install prerequisites
RUN apt-get update
RUN apt-get install -y \
build-essential \
curl \
git \
libfontconfig1 \
libfreetype6 \
libkrb5-dev \
python
# Install NodeJS
RUN curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
RUN apt-get install -y nodejs
# Install npm@latest
RUN curl -sL https://www.npmjs.org/install.sh | sh
# Clean up package management
RUN apt-get clean
RUN rm -rf /var/lib/apt/lists/*
# Install global packages
RUN npm install -g gulp grunt-cli bower 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
WORKDIR /habitrpg
RUN git clone https://github.com/HabitRPG/habitica.git /habitrpg
RUN npm install
RUN bower install --allow-root
# Create Build dir
# Create environment config file and build directory
RUN cp config.json.example config.json
RUN mkdir -p ./website/build
# Start Habitica
+3 -2
View File
@@ -5,7 +5,8 @@ Habitica [![Build Status](https://travis-ci.org/HabitRPG/habitica.svg?branch=dev
We need more programmers! Your assistance will be greatly appreciated.
For an introduction to the technologies used and how the software is organized, refer to [Guidance for Blacksmiths](http://habitica.wikia.com/wiki/Guidance_for_Blacksmiths).
For an introduction to the technologies used and how the software is organized, refer to [Contributing to Habitica](http://habitica.wikia.com/wiki/Contributing_to_Habitica#Coders_.28Web_.26_Mobile.29) - "Coders (Web & Mobile)" section.
To set up a local install of Habitica for development and testing on various platforms, see [Setting up Habitica Locally](http://habitica.wikia.com/wiki/Setting_up_Habitica_Locally).
To set up a local install of Habitica for development and testing, see [Setting up Habitica Locally](http://habitica.wikia.com/wiki/Setting_up_Habitica_Locally), which contains instructions for Windows, *nix / Mac OS, and Vagrant.
Then read [Guidance for Blacksmiths](http://habitica.wikia.com/wiki/Guidance_for_Blacksmiths) for additional instructions and useful tips.
+1 -1
View File
@@ -30,7 +30,7 @@
"bootstrap-tour": "0.10.1",
"css-social-buttons": "samcollins/css-social-buttons#v1.1.1 ",
"github-buttons": "mdo/github-buttons#v3.0.0",
"hello": "1.14.1",
"hello": "1.13.4",
"jquery": "2.1.0",
"jquery-colorbox": "1.4.36",
"jquery-ui": "1.10.3",
+2 -16
View File
@@ -73,14 +73,9 @@
"LOGGLY_ACCOUNT": "account",
"PUSH_CONFIGS": {
"GCM_SERVER_API_KEY": "",
"APN_ENABLED": "false",
"APN_ENABLED": "true",
"FCM_SERVER_API_KEY": ""
},
"SITE_HTTP_AUTH": {
"ENABLED": "false",
"USERNAME": "admin",
"PASSWORD": "password"
},
"PUSHER": {
"ENABLED": "false",
"APP_ID": "appId",
@@ -92,14 +87,5 @@
"FLAGGING_FOOTER_LINK": "https://habitrpg.github.io/flag-o-rama/",
"SUBSCRIPTIONS_URL": "https://hooks.slack.com/services/id/id/id"
},
"ITUNES_SHARED_SECRET": "aaaabbbbccccddddeeeeffff00001111",
"EMAILS" : {
"COMMUNITY_MANAGER_EMAIL" : "leslie@habitica.com",
"TECH_ASSISTANCE_EMAIL" : "admin@habitica.com",
"PRESS_ENQUIRY_EMAIL" : "leslie@habitica.com"
},
"LOGGLY" : {
"TOKEN" : "example-token",
"SUBDOMAIN" : "exmaple-subdomain"
}
"ITUNES_SHARED_SECRET": "aaaabbbbccccddddeeeeffff00001111"
}
+2 -2
View File
@@ -25,7 +25,7 @@ gulp.task('build:common', () => {
gulp.task('build:server', ['build:src', 'build:common']);
gulp.task('build:dev', ['browserify', 'prepare:staticNewStuff'], (done) => {
gulp.task('build:dev', ['browserify', 'prepare:staticNewStuff', 'semantic-ui'], (done) => {
gulp.start('grunt-build:dev', done);
});
@@ -33,7 +33,7 @@ gulp.task('build:dev:watch', ['build:dev'], () => {
gulp.watch(['website/client-old/**/*.styl', 'website/common/script/*']);
});
gulp.task('build:prod', ['browserify', 'build:server', 'prepare:staticNewStuff'], (done) => {
gulp.task('build:prod', ['browserify', 'build:server', 'prepare:staticNewStuff', 'semantic-ui'], (done) => {
runSequence(
'grunt-build:prod',
'apidoc',
+43
View File
@@ -0,0 +1,43 @@
import gulp from 'gulp';
import fs from 'fs';
// Make semantic-ui-less work with a theme in a different folder
// Code taken from https://www.artembutusov.com/webpack-semantic-ui/
// Relative to node_modules/semantic-ui-less
const SEMANTIC_THEME_PATH = '../../website/client/assets/less/semantic-ui/theme.config';
// fix well known bug with default distribution
function fixFontPath (filename) {
return new Promise((resolve, reject) => {
fs.readFile(filename, 'utf8', (err, content) => {
if (err) return reject(err);
let newContent = content.replace(
'@fontPath : \'../../themes/',
'@fontPath : \'../../../themes/'
);
fs.writeFile(filename, newContent, 'utf8', (err1) => {
if (err) return reject(err1);
resolve();
});
});
});
}
gulp.task('semantic-ui', (done) => {
// relocate default config
fs.writeFile(
'node_modules/semantic-ui-less/theme.config',
`@import '${SEMANTIC_THEME_PATH}';\n`,
'utf8',
(err) => {
if (err) return done(err);
fixFontPath('node_modules/semantic-ui-less/themes/default/globals/site.variables')
.then(() => done())
.catch(done);
}
);
});
+2 -2
View File
@@ -280,7 +280,7 @@ gulp.task('test:e2e:safe', ['test:prepare', 'test:prepare:server'], (cb) => {
gulp.task('test:api-v3:unit', (done) => {
let runner = exec(
testBin('node_modules/.bin/istanbul cover --dir coverage/api-v3-unit --report lcovonly node_modules/mocha/bin/_mocha -- test/api/v3/unit --recursive --require ./test/helpers/start-server'),
testBin('mocha test/api/v3/unit --recursive --require ./test/helpers/start-server'),
(err, stdout, stderr) => {
if (err) {
process.exit(1);
@@ -298,7 +298,7 @@ gulp.task('test:api-v3:unit:watch', () => {
gulp.task('test:api-v3:integration', (done) => {
let runner = exec(
testBin('node_modules/.bin/istanbul cover --dir coverage/api-v3-integration --report lcovonly node_modules/mocha/bin/_mocha -- test/api/v3/integration --recursive --require ./test/helpers/start-server'),
testBin('mocha test/api/v3/integration --recursive --require ./test/helpers/start-server'),
{maxBuffer: 500 * 1024},
(err, stdout, stderr) => {
if (err) {
+6 -6
View File
@@ -67,7 +67,7 @@ gulp.task('transifex:malformedStrings', () => {
let stringsWithIncorrectNumberOfInterpolations = [];
let count = 0;
_.each(ALL_LANGUAGES, function (lang) {
_(ALL_LANGUAGES).each(function (lang) {
_.each(stringsToLookFor, function (strings, file) {
let translationFile = fs.readFileSync(LOCALES + lang + '/' + file);
@@ -89,7 +89,7 @@ gulp.task('transifex:malformedStrings', () => {
}
});
});
});
}).value();
if (!_.isEmpty(stringsWithMalformedInterpolations)) {
let message = 'The following strings have malformed or missing interpolations';
@@ -114,7 +114,7 @@ function getArrayOfLanguages () {
function eachTranslationFile (languages, cb) {
let jsonFiles = stripOutNonJsonFiles(fs.readdirSync(ENGLISH_LOCALE));
_.each(languages, (lang) => {
_(languages).each((lang) => {
_.each(jsonFiles, (filename) => {
try {
var translationFile = fs.readFileSync(LOCALES + lang + '/' + filename);
@@ -128,7 +128,7 @@ function eachTranslationFile (languages, cb) {
cb(null, lang, filename, parsedEnglishFile, parsedTranslationFile);
});
});
}).value();
}
function eachTranslationString (languages, cb) {
@@ -153,7 +153,7 @@ function formatMessageForPosting (msg, items) {
function getStringsWith (json, interpolationRegex) {
var strings = {};
_.each(json, function (file_name) {
_(json).each(function (file_name) {
var raw_file = fs.readFileSync(ENGLISH_LOCALE + file_name);
var parsed_json = JSON.parse(raw_file);
@@ -162,7 +162,7 @@ function getStringsWith (json, interpolationRegex) {
var match = value.match(interpolationRegex);
if (match) strings[file_name][key] = match;
});
});
}).value();
return strings;
}
+1
View File
@@ -9,6 +9,7 @@
require('babel-register');
if (process.env.NODE_ENV === 'production') {
require('./gulp/gulp-semanticui');
require('./gulp/gulp-apidoc');
require('./gulp/gulp-newstuff');
require('./gulp/gulp-build');
-7
View File
@@ -1,12 +1,5 @@
// %mongo server:27017/dbname underscore.js my_commands.js
// %mongo server:27017/dbname underscore.js --shell
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
var habits = 0,
dailies = 0,
todos = 0,
-6
View File
@@ -3,12 +3,6 @@
*/
// mongo habitrpg ./node_modules/underscore/underscore.js ./migrations/20130326_migrate_pets.js
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
var mapping = {
bearcub: {name:'BearCub', modifier: 'Base'},
cactus: {name:'Cactus', modifier:'Base'},
-6
View File
@@ -4,12 +4,6 @@
// mongo habitrpg ./node_modules/underscore/underscore.js migrations/20130327_apply_tokens.js
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
var mapping = [
{
tier: 1,
@@ -6,11 +6,6 @@
* mongo habitrpg ./node_modules/underscore/underscore.js ./migrations/20130508_fix_duff_party_subscriptions.js
*/
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
// since our primary subscription will first hit parties now, we *definitely* need an index there
db.parties.ensureIndex( { 'members': 1}, {background: true} );
-6
View File
@@ -1,11 +1,5 @@
//mongo habitrpg ./node_modules/lodash/lodash.js migrations/20130602_survey_rewards.js
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
var members = []
members = _.uniq(members);
@@ -3,12 +3,6 @@
// Racer was notorious for adding duplicates, randomly deleting documents, etc. Once we pull the plug on old.habit,
// run this migration to cleanup all the corruption
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
db.users.find().forEach(function(user){
// remove corrupt tasks, which will either be null-value or no id
@@ -5,12 +5,6 @@
// @see http://stackoverflow.com/questions/14867697/mongoose-full-collection-scan
//Also, what do we think of a Mongoose Migration module? something like https://github.com/madhums/mongoose-migrate
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
db.users.find().forEach(function(user){
// Add invites to groups
@@ -8,12 +8,6 @@
var mongo = require('mongoskin');
var _ = require('lodash');
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
var backupUsers = mongo.db('localhost:27017/habitrpg_old?auto_reconnect').collection('users');
var liveUsers = mongo.db('localhost:27017/habitrpg_new?auto_reconnect').collection('users');
-6
View File
@@ -1,11 +1,5 @@
// node .migrations/20131127_restore_dayStart.js
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
var mongo = require('mongoskin');
var _ = require('lodash');
-6
View File
@@ -8,12 +8,6 @@ mongo = require('mongoskin')
_ = require('lodash')
async = require('async')
# IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
# We've now upgraded to lodash v4 but the code used in this migration has not been
# adapted to work with it. Before this migration is used again any lodash method should
# be checked for compatibility against the v4 changelog and changed if necessary.
# https://github.com/lodash/lodash/wiki/Changelog#v400
db = mongo.db('localhost:27017/habitrpg?auto_reconnect')
###
@@ -1,11 +1,5 @@
// node .migrations/20131221_restore_NaN_history.js
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
/**
* After the classes migration, users lost some history entries
*/
-6
View File
@@ -1,11 +1,5 @@
// node .migrations/20131225_restore_streaks.js
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
/**
* After the classes migration, users lost some history entries
*/
@@ -5,12 +5,6 @@ var migrationName = '20140823_remove_undefined_and_false_notifications';
var authorName = 'Alys'; // in case script author needs to know when their ...
var authorUuid = 'd904bd62-da08-416b-a816-ba797c9ee265'; //... own data is done
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
/**
* https://github.com/HabitRPG/habitrpg/pull/3907
*/
@@ -4,12 +4,6 @@ var migrationName = '20140829_change_headAccessory_to_eyewear';
var authorName = 'Alys'; // in case script author needs to know when their ...
var authorUuid = 'd904bd62-da08-416b-a816-ba797c9ee265'; //... own data is done
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
/**
* https://github.com/HabitRPG/habitrpg/issues/3645
*/
@@ -4,11 +4,6 @@
//
// node 20140831_increase_gems_for_previous_contributions.js > 20140831_increase_gems_for_previous_contributions_output.txt
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
var migrationName = '20140831_increase_gems_for_previous_contributions';
@@ -8,12 +8,6 @@ var authorUuid = 'd904bd62-da08-416b-a816-ba797c9ee265'; //... own data is done
* Convert Tier 7 contributors with admin flag to Tier 8 (moderators).
*/
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
var mongo = require('mongoskin');
var _ = require('lodash');
@@ -1,9 +1,3 @@
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
// require moment, lodash
db.users.find(
{'purchased.plan.customerId':{$ne:null}},
-6
View File
@@ -9,12 +9,6 @@ var authorUuid = 'd904bd62-da08-416b-a816-ba797c9ee265'; //... own data is done
var dbserver = 'localhost:27017' // CHANGE THIS FOR PRODUCTION DATABASE
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
var mongo = require('mongoskin');
var _ = require('lodash');
@@ -8,12 +8,6 @@ var authorUuid = 'd904bd62-da08-416b-a816-ba797c9ee265'; //... own data is done
var dbserver = 'localhost:27017' // CHANGE THIS FOR PRODUCTION DATABASE
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
var mongo = require('mongoskin');
var _ = require('lodash');
@@ -19,12 +19,6 @@ var authorUuid = 'd904bd62-da08-416b-a816-ba797c9ee265'; //... own data is done
var dbserver = 'localhost:27017' // CHANGE THIS FOR PRODUCTION DATABASE
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
var mongo = require('mongoskin');
var _ = require('lodash');
var moment = require('moment');
@@ -6,12 +6,6 @@ var authorUuid = 'd904bd62-da08-416b-a816-ba797c9ee265'; //... own data is done
* force all active players to rest in the inn due to massive server fail
*/
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
var dbserver = 'localhost:27017' // CHANGE THIS FOR PRODUCTION DATABASE
var mongo = require('mongoskin');
-6
View File
@@ -19,12 +19,6 @@ var authorUuid = 'd904bd62-da08-416b-a816-ba797c9ee265'; //... own data is done
* means minimal new testing.
*/
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
var dbserver = 'localhost:27017' // FOR TEST DATABASE
// var dbserver = 'username:password@ds031379-a0.mongolab.com:31379' // FOR PRODUCTION DATABASE
var dbname = 'habitrpg';
@@ -7,12 +7,6 @@ var migrationName = '20160111_challenges_condense_same_day_history_entries.js';
var dbserver = '';
var dbname = '';
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
var mongo = require('mongoskin');
var _ = require('lodash');
var moment = require('moment');
-6
View File
@@ -11,12 +11,6 @@ var dbserver = 'localhost:27017'; // FOR TEST DATABASE
// var dbserver = 'username:password@ds031379-a0.mongolab.com:31379'; // FOR PRODUCTION DATABASE
var dbname = 'habitrpg';
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
var mongo = require('mongoskin');
var _ = require('lodash');
-6
View File
@@ -14,12 +14,6 @@ var dbname = 'habitrpg';
var mongo = require('mongoskin');
var _ = require('lodash');
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
var dbUsers = mongo.db(dbserver + '/' + dbname + '?auto_reconnect').collection('users');
// specify a query to limit the affected users (empty for all users):
@@ -2,12 +2,6 @@ var uuid = require('uuid').v4;
var mongo = require('mongodb').MongoClient;
var _ = require('lodash');
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
var taskIds = require('checklists-no-id.json').map(function (obj) {
return obj._id;
});
@@ -1,88 +0,0 @@
var migrationName = '20170418_subscriber_jackalopes.js';
var authorName = 'Sabe'; // in case script author needs to know when their ...
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
/*
* Award Royal Purple Jackalope pet to all current subscribers
*/
var monk = require('monk');
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
var dbUsers = monk(connectionString).get('users', { castIds: false });
var now = new Date();
function processUsers(lastId) {
// specify a query to limit the affected users (empty for all users):
var query = {
'purchased.plan.customerId': {$type: 2},
$or: [
{'purchased.plan.dateTerminated': null},
{'purchased.plan.dateTerminated': {$exists: false}},
{'purchased.plan.dateTerminated': {$gt: now}},
]
};
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(function (err) {
console.log(err);
return exiting(1, 'ERROR! ' + err);
});
}
var progressCount = 1000;
var count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
displayData();
return;
}
var userPromises = users.map(updateUser);
var lastUser = users[users.length - 1];
return Promise.all(userPromises)
.then(function () {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
var set = {'items.pets.Jackalope-RoyalPurple': 5};
dbUsers.update({_id: user._id}, {$set:set});
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
if (user._id == authorUuid) console.warn(authorName + ' processed');
}
function displayData() {
console.warn('\n' + count + ' users processed\n');
return exiting(0);
}
function exiting(code, msg) {
code = code || 0; // 0 = success
if (code && !msg) { msg = 'ERROR!'; }
if (msg) {
if (code) { console.error(msg); }
else { console.log( msg); }
}
process.exit(code);
}
module.exports = processUsers;
-207
View File
@@ -1,207 +0,0 @@
var migrationName = '20170425_missing_incentives';
var authorName = 'Sabe'; // in case script author needs to know when their ...
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
/*
* Award missing Royal Purple Hatching Potion to users with 55+ check-ins
* Reduce users with impossible check-in counts to a reasonable number
*/
import monk from 'monk';
import common from '../website/common';
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
var dbUsers = monk(connectionString).get('users', { castIds: false });
function processUsers(lastId) {
// specify a query to limit the affected users (empty for all users):
var query = {
'loginIncentives': {$gt:99},
'migration': {$ne: migrationName},
};
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(function (err) {
console.log(err);
return exiting(1, 'ERROR! ' + err);
});
}
var progressCount = 1000;
var count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
displayData();
return;
}
var userPromises = users.map(updateUser);
var lastUser = users[users.length - 1];
return Promise.all(userPromises)
.then(function () {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
var language = user.preferences.language || 'en';
var set = {'migration': migrationName};
var inc = {
'items.eggs.BearCub': 0,
'items.eggs.Cactus': 0,
'items.eggs.Dragon': 0,
'items.eggs.FlyingPig': 0,
'items.eggs.Fox': 0,
'items.eggs.LionCub': 0,
'items.eggs.PandaCub': 0,
'items.eggs.TigerCub': 0,
'items.eggs.Wolf': 0,
'items.food.Chocolate': 0,
'items.food.CottonCandyBlue': 0,
'items.food.CottonCandyPink': 0,
'items.food.Fish': 0,
'items.food.Honey': 0,
'items.food.Meat': 0,
'items.food.Milk': 0,
'items.food.Potatoe': 0,
'items.food.RottenMeat': 0,
'items.food.Strawberry': 0,
'items.hatchingPotions.Base': 0,
'items.hatchingPotions.CottonCandyBlue': 0,
'items.hatchingPotions.CottonCandyPink': 0,
'items.hatchingPotions.Desert': 0,
'items.hatchingPotions.Golden': 0,
'items.hatchingPotions.Red': 0,
'items.hatchingPotions.RoyalPurple': 0,
'items.hatchingPotions.Shade': 0,
'items.hatchingPotions.Skeleton': 0,
'items.hatchingPotions.White': 0,
'items.hatchingPotions.Zombie': 0,
};
var nextReward;
if (user.loginIncentives >= 105) {
inc['items.hatchingPotions.RoyalPurple'] += 1;
nextReward = 110;
}
if (user.loginIncentives >= 110) {
inc['items.eggs.BearCub'] += 1;
inc['items.eggs.Cactus'] += 1;
inc['items.eggs.Dragon'] += 1;
inc['items.eggs.FlyingPig'] += 1;
inc['items.eggs.Fox'] += 1;
inc['items.eggs.LionCub'] += 1;
inc['items.eggs.PandaCub'] += 1;
inc['items.eggs.TigerCub'] += 1;
inc['items.eggs.Wolf'] += 1;
nextReward = 115;
}
if (user.loginIncentives >= 115) {
inc['items.hatchingPotions.RoyalPurple'] += 1;
nextReward = 120;
}
if (user.loginIncentives >= 120) {
inc['items.hatchingPotions.Base'] += 1;
inc['items.hatchingPotions.CottonCandyBlue'] += 1;
inc['items.hatchingPotions.CottonCandyPink'] += 1;
inc['items.hatchingPotions.Desert'] += 1;
inc['items.hatchingPotions.Golden'] += 1;
inc['items.hatchingPotions.Red'] += 1;
inc['items.hatchingPotions.Shade'] += 1;
inc['items.hatchingPotions.Skeleton'] += 1;
inc['items.hatchingPotions.White'] += 1;
inc['items.hatchingPotions.Zombie'] += 1;
nextReward = 125;
}
if (user.loginIncentives >= 125) {
inc['items.hatchingPotions.RoyalPurple'] += 1;
nextReward = 130;
}
if (user.loginIncentives >= 130) {
inc['items.food.Chocolate'] += 3;
inc['items.food.CottonCandyBlue'] += 3;
inc['items.food.CottonCandyPink'] += 3;
inc['items.food.Fish'] += 3;
inc['items.food.Honey'] += 3;
inc['items.food.Meat'] += 3;
inc['items.food.Milk'] += 3;
inc['items.food.Potatoe'] += 3;
inc['items.food.RottenMeat'] += 3;
inc['items.food.Strawberry'] += 3;
}
if (user.loginIncentives >= 135) {
inc['items.hatchingPotions.RoyalPurple'] += 1;
nextReward = 140;
}
if (user.loginIncentives >= 140) {
set['items.gear.owned.weapon_special_skeletonKey'] = true;
set['items.gear.owned.shield_special_lootBag'] = true;
nextReward = 145;
}
if (user.loginIncentives >= 145) {
inc['items.hatchingPotions.RoyalPurple'] += 1;
nextReward = 150;
}
if (user.loginIncentives >= 150) {
set['items.gear.owned.head_special_clandestineCowl'] = true;
set['items.gear.owned.armor_special_sneakthiefRobes'] = true;
nextReward = 155;
}
if (user.loginIncentives > 155) {
set.loginIncentives = 155;
nextReward = 160;
}
var push = {
'notifications': {
'type': 'LOGIN_INCENTIVE',
'data': {
'nextRewardAt': nextReward,
'rewardKey': [
'shop_armoire',
],
'rewardText': common.i18n.t('checkInRewards', language),
'reward': [],
'message': common.i18n.t('backloggedCheckInRewards', language),
},
'id': common.uuid(),
}
};
dbUsers.update({_id: user._id}, {$set:set, $push:push, $inc:inc});
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
if (user._id == authorUuid) console.warn(authorName + ' processed');
}
function displayData() {
console.warn('\n' + count + ' users processed\n');
return exiting(0);
}
function exiting(code, msg) {
code = code || 0; // 0 = success
if (code && !msg) { msg = 'ERROR!'; }
if (msg) {
if (code) { console.error(msg); }
else { console.log( msg); }
}
process.exit(code);
}
module.exports = processUsers;
-6
View File
@@ -6,12 +6,6 @@
// Due to some big user profiles it needs more RAM than is allowed by default by v8 (arounf 1.7GB).
// Run the script with --max-old-space-size=4096 to allow up to 4GB of RAM
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
console.log('Starting migrations/api_v3/challenges.js.');
require('babel-register');
-6
View File
@@ -9,12 +9,6 @@
// Run the script with --max-old-space-size=4096 to allow up to 4GB of RAM
console.log('Starting migrations/api_v3/challengesMembers.js.');
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
require('babel-register');
require('babel-polyfill');
-6
View File
@@ -8,12 +8,6 @@
// Run the script with --max-old-space-size=4096 to allow up to 4GB of RAM
console.log('Starting migrations/api_v3/coupons.js.');
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
require('babel-register');
require('babel-polyfill');
@@ -8,12 +8,6 @@
// Run the script with --max-old-space-size=4096 to allow up to 4GB of RAM
console.log('Starting migrations/api_v3/unsubscriptions.js.');
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
require('babel-register');
require('babel-polyfill');
-6
View File
@@ -16,12 +16,6 @@
// Run the script with --max-old-space-size=4096 to allow up to 4GB of RAM
console.log('Starting migrations/api_v3/groups.js.');
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
require('babel-register');
require('babel-polyfill');
-6
View File
@@ -9,12 +9,6 @@
// Run the script with --max-old-space-size=4096 to allow up to 4GB of RAM
console.log('Starting migrations/api_v3/users.js.');
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
require('babel-register');
require('babel-polyfill');
@@ -1,47 +0,0 @@
import Bluebird from 'Bluebird';
import { model as Challenges } from '../../website/server/models/challenge';
import { model as User } from '../../website/server/models/user';
async function syncChallengeToMembers (challenges) {
let challengSyncPromises = challenges.map(async function (challenge) {
let users = await User.find({
// _id: '',
challenges: challenge._id,
}).exec();
let promises = [];
users.forEach(function (user) {
promises.push(challenge.syncToUser(user));
promises.push(challenge.save());
promises.push(user.save());
});
return Bluebird.all(promises);
});
return await Bluebird.all(challengSyncPromises);
}
async function syncChallenges (lastChallengeDate) {
let query = {
// _id: '',
};
if (lastChallengeDate) {
query.createdOn = { $lte: lastChallengeDate };
}
let challengesFound = await Challenges.find(query)
.limit(10)
.sort('-createdAt')
.exec();
let syncedChallenges = await syncChallengeToMembers(challengesFound)
.catch(reason => console.error(reason));
let lastChallenge = challengesFound[challengesFound.length - 1];
if (lastChallenge) syncChallenges(lastChallenge.createdAt);
return syncedChallenges;
};
module.exports = syncChallenges;
+1 -1
View File
@@ -7,6 +7,6 @@
db.users.find().forEach(function(user){
user.tasks = user.habits.concat(user.dailys).concat(user.todos).concat(user.rewards);
var found = _.some(user.tasks, {text: ""})
var found = _.any(user.tasks, {text: ""})
if (found) printjson({id:user._id, auth:user.auth});
})
@@ -1,40 +0,0 @@
var migrationName = 'AddUnlimitedSubscription';
var authorName = 'TheHollidayInn'; // in case script author needs to know when their ...
var authorUuid = ''; //... own data is done
/*
* This migrations will add a free subscription to a specified group
*/
import moment from 'moment';
import { model as Group } from '../../website/server/models/group';
// @TODO: this should probably be a GroupManager library method
async function addUnlimitedSubscription (groupId, dateTerminated) {
let group = await Group.findById(groupId);
group.purchased.plan.customerId = "group-unlimited";
group.purchased.plan.dateCreated = new Date();
group.purchased.plan.dateUpdated = new Date();
group.purchased.plan.paymentMethod = "Group Unlimited";
group.purchased.plan.planId = "group_monthly";
group.purchased.plan.dateTerminated = null;
if (dateTerminated) {
let dateToEnd = moment(dateTerminated).toDate();
group.purchased.plan.dateTerminated = dateToEnd;
}
// group.purchased.plan.owner = ObjectId();
group.purchased.plan.subscriptionId = "";
return group.save();
};
module.exports = async function addUnlimitedSubscriptionCreator () {
let groupId = process.argv[2];
if (!groupId) throw Error('Group ID is required');
let dateTerminated = process.argv[3];
let result = await addUnlimitedSubscription(groupId, dateTerminated);
};
-32
View File
@@ -1,32 +0,0 @@
import Bluebird from 'bluebird';
import { model as Group } from '../../website/server/models/group';
import { model as User } from '../../website/server/models/user';
// @TODO: this should probably be a GroupManager library method
async function createGroup (name, privacy, type, leaderId) {
let user = await User.findById(leaderId);
let group = new Group({
name,
privacy,
type,
});
group.leader = user._id;
user.guilds.push(group._id);
return Bluebird.all([group.save(), user.save()]);
};
module.exports = async function groupCreator () {
let name = process.argv[2];
let privacy = process.argv[3];
let type = process.argv[4];
let leaderId = process.argv[5];
let result = await createGroup(name, privacy, type, leaderId)
};
-46
View File
@@ -1,46 +0,0 @@
var migrationName = 'Jackalopes for Unlimited Subscribers';
/*
* This migration will find users with unlimited subscriptions who are also eligible for Jackalope mounts, and award them
*/
import Bluebird from 'bluebird';
import { model as Group } from '../../website/server/models/group';
import { model as User } from '../../website/server/models/user';
import * as payments from '../../website/server/libs/payments';
async function handOutJackalopes () {
let promises = [];
let cursor = User.find({
'purchased.plan.customerId':'habitrpg',
}).cursor();
cursor.on('data', async function(user) {
console.log('User: ' + user._id);
let groupList = [];
if (user.party._id) groupList.push(user.party._id);
groupList = groupList.concat(user.guilds);
let subscribedGroup =
await Group.findOne({
'_id': {$in: groupList},
'purchased.plan.planId': 'group_monthly',
'purchased.plan.dateTerminated': null,
},
{'_id':1}
);
if (subscribedGroup) {
User.update({'_id':user._id},{$set:{'items.mounts.Jackalope-RoyalPurple':true}}).exec();
promises.push(user.save());
}
});
cursor.on('close', async function() {
console.log('done');
return await Bluebird.all(promises);
});
};
module.exports = handOutJackalopes;
@@ -1,33 +0,0 @@
var migrationName = 'ResyncGroupPlanMembers';
var authorName = 'TheHollidayInn'; // in case script author needs to know when their ...
var authorUuid = ''; //... own data is done
/*
* This migrations will iterate through all groups with a group plan a subscription and resync the free
* subscription to all members
*/
import Bluebird from 'bluebird';
import { model as Group } from '../../website/server/models/group';
import * as payments from '../../website/server/libs/payments';
async function updateGroupsWithGroupPlans () {
let cursor = Group.find({
'purchased.plan.planId': 'group_monthly',
'purchased.plan.dateTerminated': null,
}).cursor();
let promises = [];
cursor.on('data', function(group) {
promises.push(payments.addSubscriptionToGroupUsers(group));
promises.push(group.save())
});
cursor.on('close', async function() {
return await Bluebird.all(promises);
});
};
module.exports = updateGroupsWithGroupPlans;
-3
View File
@@ -1,9 +1,6 @@
// EMAIL="x@y.com" node ./migrations/manual_password_reset.js
// Be sure to have PRODUCTION_DB in your config.json
// IMPORTANT: this script isn't updated to use the new password encryption that uses bcrypt
// using it will break accounts and should not be used until upgraded
var nconf = require('nconf'),
path = require('path');
nconf.argv().env().file('user', path.join(path.resolve(__dirname, '../config.json')));
+21 -24
View File
@@ -1,24 +1,21 @@
require("babel-register");
require("babel-polyfill");
// This file must use ES5, everything required can be in ES6
function setUpServer () {
var nconf = require('nconf');
var mongoose = require('mongoose');
var Bluebird = require('bluebird');
var setupNconf = require('../website/server/libs/setupNconf');
setupNconf();
// We require src/server and npt src/index because
// 1. nconf is already setup
// 2. we don't need clustering
require('../website/server/server'); // eslint-disable-line global-require
}
setUpServer();
// Replace this with your migration
var processUsers = require('./groups/update-groups-with-group-plans');
processUsers()
.catch(function (err) {
console.log(err)
})
require("babel-register");
require("babel-polyfill");
// This file must use ES5, everything required can be in ES6
function setUpServer () {
var nconf = require('nconf');
var mongoose = require('mongoose');
var Bluebird = require('bluebird');
var setupNconf = require('../website/server/libs/setupNconf');
setupNconf();
// We require src/server and npt src/index because
// 1. nconf is already setup
// 2. we don't need clustering
require('../website/server/server'); // eslint-disable-line global-require
}
setUpServer();
// Replace this with your migration
var processUsers = require('./new_stuff');
processUsers();
+1 -1
View File
@@ -2,7 +2,7 @@ var _id = '';
var update = {
$addToSet: {
'purchased.plan.mysteryItems':{
$each:['body_mystery_201705','head_mystery_201705']
$each:['head_mystery_201702','back_mystery_201702']
}
}
};
+2 -2
View File
@@ -1,4 +1,4 @@
var migrationName = '20170502_takeThis.js'; // Update per month
var migrationName = '20170201_takeThis.js'; // Update per month
var authorName = 'Sabe'; // in case script author needs to know when their ...
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
@@ -14,7 +14,7 @@ function processUsers(lastId) {
// specify a query to limit the affected users (empty for all users):
var query = {
'migration':{$ne:migrationName},
'challenges':{$in:['69999331-d4ea-45a0-8c3f-f725d22b56c8']} // Update per month
'challenges':{$in:['b1d436b5-c784-42e3-9b07-7072479a6f8e']} // Update per month
};
if (lastId) {
+2217 -1953
View File
File diff suppressed because it is too large Load Diff
+22 -26
View File
@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "3.95.0",
"version": "3.78.0",
"main": "./website/server/index.js",
"dependencies": {
"@slack/client": "^3.8.1",
@@ -13,7 +13,7 @@
"async": "^1.5.0",
"autoprefixer": "^6.4.0",
"aws-sdk": "^2.0.25",
"axios": "^0.16.0",
"axios": "^0.15.3",
"babel-core": "^6.0.0",
"babel-loader": "^6.0.0",
"babel-plugin-syntax-async-functions": "^6.13.0",
@@ -28,21 +28,18 @@
"bcrypt": "^1.0.2",
"bluebird": "^3.3.5",
"body-parser": "^1.15.0",
"bootstrap": "^4.0.0-alpha.6",
"bootstrap-vue": "^0.15.8",
"bower": "~1.3.12",
"browserify": "~12.0.1",
"compression": "^1.6.1",
"connect-ratelimit": "0.0.7",
"cookie-session": "^1.2.0",
"coupon-code": "^0.4.5",
"css-loader": "^0.28.0",
"css-loader": "^0.26.1",
"csv-stringify": "^1.0.2",
"cwait": "^1.0.0",
"domain-middleware": "~0.1.0",
"estraverse": "^4.1.1",
"express": "~4.14.0",
"express-basic-auth": "^1.0.1",
"express-csv": "~0.6.0",
"express-validator": "^2.18.0",
"extract-text-webpack-plugin": "^2.0.0-rc.3",
@@ -57,7 +54,7 @@
"grunt-contrib-stylus": "~0.20.0",
"grunt-contrib-uglify": "~0.6.0",
"grunt-contrib-watch": "~0.6.1",
"grunt-hashres": "habitrpg/grunt-hashres#v0.4.2",
"grunt-hashres": "~0.4.1",
"gulp": "^3.9.0",
"gulp-babel": "^6.1.2",
"gulp-grunt": "^0.5.2",
@@ -73,18 +70,20 @@
"jade": "~1.11.0",
"jquery": "^3.1.1",
"js2xmlparser": "~1.0.0",
"lodash": "^4.17.4",
"less": "^2.7.1",
"less-loader": "^2.2.3",
"lodash": "^3.10.1",
"lodash.pickby": "^4.2.0",
"lodash.setwith": "^4.2.0",
"merge-stream": "^1.0.0",
"method-override": "^2.3.5",
"moment": "^2.13.0",
"moment-recur": "habitrpg/moment-recur#v1.0.6",
"mongoose": "^4.8.6",
"mongoose": "^4.7.1",
"mongoose-id-autoinc": "~2013.7.14-4",
"morgan": "^1.7.0",
"nconf": "~0.8.2",
"nib": "^1.1.0",
"node-gcm": "^0.14.4",
"node-sass": "^4.5.0",
"nodemailer": "^2.3.2",
"object-path": "^0.9.2",
"ora": "^1.1.0",
@@ -97,14 +96,14 @@
"postcss-easy-import": "^2.0.0",
"pretty-data": "^0.40.0",
"ps-tree": "^1.0.0",
"pug": "^2.0.0-beta.12",
"pug": "^2.0.0-beta11",
"push-notify": "habitrpg/push-notify#v1.2.0",
"pusher": "^1.3.0",
"request": "~2.74.0",
"rimraf": "^2.4.3",
"run-sequence": "^1.1.4",
"s3-upload-stream": "^1.0.6",
"sass-loader": "^6.0.2",
"semantic-ui-less": "~2.2.4",
"serve-favicon": "^2.3.0",
"shelljs": "^0.7.6",
"stripe": "^4.2.0",
@@ -118,14 +117,12 @@
"vinyl-source-stream": "^1.1.0",
"vue": "^2.1.0",
"vue-loader": "^11.0.0",
"vue-mugen-scroll": "^0.2.1",
"vue-router": "^2.0.0-rc.5",
"vue-style-loader": "^3.0.0",
"vue-style-loader": "^2.0.0",
"vue-template-compiler": "^2.1.10",
"webpack": "^2.2.1",
"webpack-merge": "^4.0.0",
"webpack-merge": "^2.6.1",
"winston": "^2.1.0",
"winston-loggly-bulk": "^1.4.2",
"xml2js": "^0.4.4"
},
"private": true,
@@ -141,9 +138,9 @@
"test:api-v3:unit": "gulp test:api-v3:unit",
"test:api-v3:integration": "gulp test:api-v3:integration",
"test:api-v3:integration:separate-server": "NODE_ENV=test gulp test:api-v3:integration:separate-server",
"test:sanity": "istanbul cover --dir coverage/sanity --report lcovonly node_modules/mocha/bin/_mocha -- test/sanity --recursive",
"test:common": "istanbul cover --dir coverage/common --report lcovonly node_modules/mocha/bin/_mocha -- test/common --recursive",
"test:content": "istanbul cover --dir coverage/content --report lcovonly node_modules/mocha/bin/_mocha -- test/content --recursive",
"test:sanity": "mocha test/sanity --recursive",
"test:common": "mocha test/common --recursive",
"test:content": "mocha test/content --recursive",
"test:karma": "karma start test/client-old/spec/karma.conf.js --single-run",
"test:karma:watch": "karma start test/client-old/spec/karma.conf.js",
"test:prepare:webdriver": "webdriver-manager update",
@@ -169,12 +166,12 @@
"chromedriver": "^2.27.2",
"connect-history-api-fallback": "^1.1.0",
"coveralls": "^2.11.2",
"cross-env": "^4.0.0",
"cross-env": "^3.1.4",
"cross-spawn": "^5.0.1",
"csv": "~0.3.6",
"deep-diff": "~0.1.4",
"eslint": "^3.0.0",
"eslint-config-habitrpg": "^3.0.0",
"eslint-config-habitrpg": "^2.0.0",
"eslint-friendly-formatter": "^2.0.5",
"eslint-loader": "^1.3.0",
"eslint-plugin-html": "^2.0.0",
@@ -185,7 +182,7 @@
"grunt-karma": "~0.12.1",
"http-proxy-middleware": "^0.17.0",
"inject-loader": "^3.0.0-beta4",
"istanbul": "^1.1.0-alpha.1",
"istanbul": "^0.3.14",
"karma": "^1.3.0",
"karma-babel-preprocessor": "^6.0.1",
"karma-chai-plugins": "~0.6.0",
@@ -193,14 +190,13 @@
"karma-mocha": "^0.2.0",
"karma-mocha-reporter": "^1.1.1",
"karma-phantomjs-launcher": "^1.0.0",
"karma-sinon-chai": "~1.2.0",
"karma-sinon-stub-promise": "^1.0.0",
"karma-sinon-chai": "^1.2.0",
"karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "0.0.24",
"karma-webpack": "^2.0.2",
"lcov-result-merger": "^1.0.2",
"lolex": "^1.4.0",
"mocha": "^3.2.0",
"mocha": "^2.3.3",
"mongodb": "^2.0.46",
"mongoskin": "~2.1.0",
"monk": "^4.0.0",
-1
View File
@@ -1 +0,0 @@
For information about writing and running tests, see [Using Your Local Install to Modify Habitica's Website and API](http://habitica.wikia.com/wiki/Using_Your_Local_Install_to_Modify_Habitica%27s_Website_and_API).
+123
View File
@@ -0,0 +1,123 @@
# So you want to write API integration tests?
@TODO rewrite
That's great! This README will serve as a quick primer for style conventions and practices for these tests.
## What is this?
These are integration tests for the Habitica API. They are performed by making HTTP requests to the API's endpoints and asserting on the data that is returned.
If the javascript looks weird to you, that's because it's written in [ES2015](http://www.ecma-international.org/ecma-262/6.0/) and transpiled by [Babel](https://babeljs.io/docs/learn-es2015/).
## How to run the tests
First, install gulp.
```bash
$ npm install -g gulp
```
To run the api tests, make sure the mongo db is up and running and then type this on the command line:
```bash
$ gulp test:api-v2
```
It may take a little while to get going, since it requires the web server to start up before the tests can run.
You can also run a watch command for the api tests. This will allow the tests to re-run automatically when you make changes in the `/test/api/v2/`.
```bash
$ gulp test:api-v2:watch
```
One caveat. If you have a severe syntax error in your files, the tests may fail to run, but they won't alert you that they are not running. If you ever find your test hanging for a while, run this to get the stackstrace. Once you've fixed the problem, you can resume running the watch comand.
```bash
$ gulp test:api:safe
```
If you'd like to run the tests individually and inspect the output from the server, in one pane you can run:
```bash
$ gulp test:nodemon
```
And run your tests in another pane:
```bash
$ mocha path/to/file.js
# Mark a test with the `.only` attribute
$ mocha
```
## Structure
Each top level route has it's own directory. So, all the routes that begin with `/groups/` live in `/test/api/groups/`.
Each test should:
* encompase a single route
* begin with the REST parameter for the route
* display the full name of the route, swapping out `/` for `_`
* end with test.js
So, for the `POST` route `/groups/:id/leave`, it would be
```bash
POST-groups_id_leave.test.js
```
## Promises
To mitigate [callback hell](http://callbackhell.com/) :imp:, we've written a helper method to generate a user object that can make http requests that [return promises](https://babeljs.io/docs/learn-es2015/#promises). This makes it very easy to chain together commands. All you need to do to make a subsequent request is return another promise and then call `.then((result) => {})` on the surrounding block, like so:
```js
it('does something', () => {
let user;
return generateUser().then((_user) => { // We return the initial promise so this test can be run asyncronously
user = _user;
return user.post('/groups', {
type: 'party',
});
}).then((party) => { // the result of the promise above is the argument of the function
return user.put(`/groups/${party._id}`, {
name: 'My party',
});
}).then((result) => {
return user.get('/groups/party');
}).then((party) => {
expect(party.name).to.eql('My party');
});
});
```
If the test is simple, you can use the [chai-as-promised](http://chaijs.com/plugins/chai-as-promised) `return expect(somePromise).to.eventually` syntax to make your assertion.
```js
it('makes the party creator the leader automatically', () => {
return expect(user.post('/groups', {
type: 'party',
})).to.eventually.have.deep.property('leader._id', user._id);
});
```
If the test is checking that the request returns an error, use the `.eventually.be.rejected.and.eql` syntax.
```js
it('returns an error', () => {
return expect(user.get('/groups/id-of-a-party-that-user-does-not-belong-to'))
.to.eventually.be.rejected.and.eql({
code: 404,
text: t('messageGroupNotFound'),
});
});
```
## Questions?
Ask in the [Aspiring Coder's Guild](https://habitica.com/#/options/groups/guilds/68d4a01e-db97-4786-8ee3-05d612c5af6f)!
+4
View File
@@ -0,0 +1,4 @@
# How to run tests:
1. `npm test` is equivalent to `gulp test:api-v3` which will run, in order, `gulp lint`, `gulp test:api-v3:unit` and `gulp test:api-v3:integration`. If one of these fails, the whole `npm test` command blocks and fails. Each of these commands can also be run as a standalone command.
2. To run the server and the integrations tests in two different terminals (to better inspect the output in the server) run `npm start` in one and `npm test:api-v3:integration:separate-server` in the other
@@ -117,6 +117,7 @@ describe('POST /challenges/:challengeId/leave', () => {
});
expect(testTask).to.not.be.undefined;
expect(testTask.challenge).to.eql({});
});
});
});
+2 -125
View File
@@ -4,17 +4,11 @@ import {
sleep,
server,
} from '../../../../helpers/api-v3-integration.helper';
import {
SPAM_MESSAGE_LIMIT,
SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
TAVERN_ID,
} from '../../../../../website/server/models/group';
import { v4 as generateUUID } from 'uuid';
describe('POST /chat', () => {
let user, groupWithChat, member, additionalMember;
let user, groupWithChat, userWithChatRevoked, member;
let testMessage = 'Test Message';
let testBannedWordMessage = 'TEST_PLACEHOLDER_SWEAR_WORD_HERE';
before(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
@@ -28,8 +22,8 @@ describe('POST /chat', () => {
user = groupLeader;
groupWithChat = group;
userWithChatRevoked = await members[0].update({'flags.chatRevoked': true});
member = members[0];
additionalMember = members[1];
});
it('Returns an error when no message is provided', async () => {
@@ -68,7 +62,6 @@ describe('POST /chat', () => {
});
it('returns an error when chat privileges are revoked when sending a message to a public guild', async () => {
let userWithChatRevoked = await member.update({'flags.chatRevoked': true});
await expect(userWithChatRevoked.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage})).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
@@ -76,96 +69,6 @@ describe('POST /chat', () => {
});
});
context('banned word', () => {
it('returns an error when chat message contains a banned word in tavern', async () => {
await expect(user.post('/groups/habitrpg/chat', { message: testBannedWordMessage}))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedWordUsed'),
});
});
it('errors when word is part of a phrase', async () => {
let wordInPhrase = `phrase ${testBannedWordMessage} end`;
await expect(user.post('/groups/habitrpg/chat', { message: wordInPhrase}))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedWordUsed'),
});
});
it('errors when word is surrounded by non alphabet characters', async () => {
let wordInPhrase = `_!${testBannedWordMessage}@_`;
await expect(user.post('/groups/habitrpg/chat', { message: wordInPhrase}))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedWordUsed'),
});
});
it('does not error when bad word is suffix of a word', async () => {
let wordAsSuffix = `prefix${testBannedWordMessage}`;
let message = await user.post('/groups/habitrpg/chat', { message: wordAsSuffix});
expect(message.message.id).to.exist;
});
it('does not error when bad word is prefix of a word', async () => {
let wordAsPrefix = `${testBannedWordMessage}suffix`;
let message = await user.post('/groups/habitrpg/chat', { message: wordAsPrefix});
expect(message.message.id).to.exist;
});
it('does not error when sending a chat message containing a banned word to a party', async () => {
let { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'Party',
type: 'party',
privacy: 'private',
},
members: 1,
});
let message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage});
expect(message.message.id).to.exist;
});
it('does not error when sending a chat message containing a banned word to a public guild', async () => {
let { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'public guild',
type: 'guild',
privacy: 'public',
},
members: 1,
});
let message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage});
expect(message.message.id).to.exist;
});
it('does not error when sending a chat message containing a banned word to a private guild', async () => {
let { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'private guild',
type: 'guild',
privacy: 'private',
},
members: 1,
});
let message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage});
expect(message.message.id).to.exist;
});
});
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: {
@@ -270,30 +173,4 @@ describe('POST /chat', () => {
expect(message.message.id).to.exist;
expect(memberWithNotification.newMessages[`${group._id}`]).to.exist;
});
context('Spam prevention', () => {
it('Returns an error when the user has been posting too many messages', async () => {
// Post as many messages are needed to reach the spam limit
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
let result = await additionalMember.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage }); // eslint-disable-line no-await-in-loop
expect(result.message.id).to.exist;
}
await expect(additionalMember.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage })).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageGroupChatSpam'),
});
});
it('contributor should not receive spam alert', async () => {
let userSocialite = await member.update({'contributor.level': SPAM_MIN_EXEMPT_CONTRIB_LEVEL, 'flags.chatRevoked': false});
// 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++) {
let result = await userSocialite.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage }); // eslint-disable-line no-await-in-loop
expect(result.message.id).to.exist;
}
});
});
});
@@ -7,7 +7,6 @@ import {
import {
TAVERN_ID,
} from '../../../../../website/server/models/group';
import apiMessages from '../../../../../website/server/libs/apiMessages';
describe('GET /groups', () => {
let user;
@@ -15,7 +14,6 @@ describe('GET /groups', () => {
const NUMBER_OF_PUBLIC_GUILDS_USER_IS_MEMBER = 1;
const NUMBER_OF_USERS_PRIVATE_GUILDS = 1;
const NUMBER_OF_GROUPS_USER_CAN_VIEW = 5;
const GUILD_PER_PAGE = 30;
before(async () => {
await resetHabiticaDB();
@@ -100,60 +98,6 @@ describe('GET /groups', () => {
.to.eventually.have.a.lengthOf(NUMBER_OF_PUBLIC_GUILDS);
});
describe('public guilds pagination', () => {
it('req.query.paginate must be a boolean string', async () => {
await expect(user.get('/groups?paginate=aString&type=publicGuilds'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Invalid request parameters.',
});
});
it('req.query.paginate can only be true when req.query.type includes publicGuilds', async () => {
await expect(user.get('/groups?paginate=true&type=notPublicGuilds'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: apiMessages('guildsOnlyPaginate'),
});
});
it('req.query.page can\'t be negative', async () => {
await expect(user.get('/groups?paginate=true&page=-1&type=publicGuilds'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Invalid request parameters.',
});
});
it('returns 30 guilds per page ordered by number of members', async () => {
await user.update({balance: 9000});
let groups = await Promise.all(_.times(60, (i) => {
return generateGroup(user, {
name: `public guild ${i} - is member`,
type: 'guild',
privacy: 'public',
});
}));
// update group number 32 and not the first to make sure sorting works
await groups[32].update({name: 'guild with most members', memberCount: 199});
await groups[33].update({name: 'guild with less members', memberCount: -100});
let page0 = await expect(user.get('/groups?type=publicGuilds&paginate=true'))
.to.eventually.have.a.lengthOf(GUILD_PER_PAGE);
expect(page0[0].name).to.equal('guild with most members');
await expect(user.get('/groups?type=publicGuilds&paginate=true&page=1'))
.to.eventually.have.a.lengthOf(GUILD_PER_PAGE);
let page2 = await expect(user.get('/groups?type=publicGuilds&paginate=true&page=2'))
.to.eventually.have.a.lengthOf(1 + 2); // 1 created now, 2 by other tests
expect(page2[2].name).to.equal('guild with less members');
});
});
it('returns all the user\'s guilds when guilds passed in as query', async () => {
await expect(user.get('/groups?type=guilds'))
.to.eventually.have.a.lengthOf(NUMBER_OF_PUBLIC_GUILDS_USER_IS_MEMBER + NUMBER_OF_USERS_PRIVATE_GUILDS);
@@ -63,17 +63,15 @@ describe('GET /groups/:groupId/invites', () => {
});
it('returns only first 30 invites', async () => {
let leader = await generateUser({balance: 4});
let group = await generateGroup(leader, {type: 'guild', privacy: 'public', name: generateUUID()});
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
let invitesToGenerate = [];
for (let i = 0; i < 31; i++) {
invitesToGenerate.push(generateUser());
}
let generatedInvites = await Promise.all(invitesToGenerate);
await leader.post(`/groups/${group._id}/invite`, {uuids: generatedInvites.map(invite => invite._id)});
await user.post(`/groups/${group._id}/invite`, {uuids: generatedInvites.map(invite => invite._id)});
let res = await leader.get(`/groups/${group._id}/invites`);
let res = await user.get('/groups/party/invites');
expect(res.length).to.equal(30);
res.forEach(member => {
expect(member).to.have.all.keys(['_id', 'id', 'profile']);
@@ -84,7 +84,7 @@ describe('GET /groups/:groupId/members', () => {
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
expect(Object.keys(memberRes.preferences).sort()).to.eql([
'size', 'hair', 'skin', 'shirt',
'chair', 'costume', 'sleep', 'background', 'tasks',
'chair', 'costume', 'sleep', 'background',
].sort());
expect(memberRes.stats.maxMP).to.exist;
@@ -1,93 +0,0 @@
import {
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
import { find } from 'lodash';
describe('POST /group/:groupId/remove-manager', () => {
let leader, nonLeader, groupToUpdate;
let groupName = 'Test Public Guild';
let groupType = 'guild';
let nonManager;
function findAssignedTask (memberTask) {
return memberTask.group.id === groupToUpdate._id;
}
beforeEach(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: groupName,
type: groupType,
privacy: 'public',
},
members: 1,
});
groupToUpdate = group;
leader = groupLeader;
nonLeader = members[0];
nonManager = members[0];
});
it('returns an error when a non group leader tries to add member', async () => {
await expect(nonLeader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
managerId: nonLeader._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageGroupOnlyLeaderCanUpdate'),
});
});
it('returns an error when manager does not exist', async () => {
await expect(leader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
managerId: nonManager._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('userIsNotManager'),
});
});
it('allows a leader to remove managers', async () => {
await leader.post(`/groups/${groupToUpdate._id}/add-manager`, {
managerId: nonLeader._id,
});
let updatedGroup = await leader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
managerId: nonLeader._id,
});
expect(updatedGroup.managers[nonLeader._id]).to.not.exist;
});
it('removes group approval notifications from a manager that is removed', async () => {
await leader.post(`/groups/${groupToUpdate._id}/add-manager`, {
managerId: nonLeader._id,
});
let task = await leader.post(`/tasks/group/${groupToUpdate._id}`, {
text: 'test todo',
type: 'todo',
requiresApproval: true,
});
await nonLeader.post(`/tasks/${task._id}/assign/${leader._id}`);
let memberTasks = await leader.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(leader.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
let updatedGroup = await leader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
managerId: nonLeader._id,
});
await nonLeader.sync();
expect(nonLeader.notifications.length).to.equal(0);
expect(updatedGroup.managers[nonLeader._id]).to.not.exist;
});
});
@@ -74,18 +74,6 @@ describe('POST /group', () => {
expect(updatedUser.guilds).to.include(guild._id);
});
it('awards the Joined Guild achievement', async () => {
await user.post('/groups', {
name: 'some guild',
type: 'guild',
privacy: 'public',
});
let updatedUser = await user.get('/user');
expect(updatedUser.achievements.joinedGuild).to.eql(true);
});
context('public guild', () => {
it('creates a group', async () => {
let groupName = 'Test Public Guild';
@@ -68,12 +68,6 @@ describe('POST /group/:groupId/join', () => {
await expect(joiningUser.get(`/groups/${publicGuild._id}`)).to.eventually.have.property('memberCount', oldMemberCount + 1);
});
it('awards Joined Guild achievement', async () => {
await joiningUser.post(`/groups/${publicGuild._id}/join`);
await expect(joiningUser.get('/user')).to.eventually.have.deep.property('achievements.joinedGuild', true);
});
});
context('Joining a private guild', () => {
@@ -153,14 +147,8 @@ describe('POST /group/:groupId/join', () => {
}),
};
expect(inviter.notifications[1].type).to.eql('GROUP_INVITE_ACCEPTED');
expect(inviter.notifications[1].data).to.eql(expectedData);
});
it('awards Joined Guild achievement', async () => {
await invitedUser.post(`/groups/${guild._id}/join`);
await expect(invitedUser.get('/user')).to.eventually.have.deep.property('achievements.joinedGuild', true);
expect(inviter.notifications[0].type).to.eql('GROUP_INVITE_ACCEPTED');
expect(inviter.notifications[0].data).to.eql(expectedData);
});
});
});
@@ -78,7 +78,7 @@ describe('POST /groups/:groupId/leave', () => {
expect(leader.newMessages[groupToLeave._id]).to.be.empty;
});
context('with challenges', () => {
context('With challenges', () => {
let challenge;
beforeEach(async () => {
@@ -106,25 +106,10 @@ describe('POST /groups/:groupId/leave', () => {
let userWithChallengeTasks = await leader.get('/user');
expect(userWithChallengeTasks.challenges).to.not.include(challenge._id);
// @TODO find elegant way to assert against the task existing
expect(userWithChallengeTasks.tasksOrder.habits).to.not.be.empty;
});
it('keeps the user in the challenge when the keepChallenges parameter is set to remain-in-challenges', async () => {
await leader.post(`/groups/${groupToLeave._id}/leave`, {keepChallenges: 'remain-in-challenges'});
let userWithChallengeTasks = await leader.get('/user');
expect(userWithChallengeTasks.challenges).to.include(challenge._id);
});
it('drops the user in the challenge when the keepChallenges parameter isn\'t set', async () => {
await leader.post(`/groups/${groupToLeave._id}/leave`);
let userWithChallengeTasks = await leader.get('/user');
expect(userWithChallengeTasks.challenges).to.not.include(challenge._id);
});
});
it('prevents quest leader from leaving a groupToLeave');
@@ -1,14 +1,10 @@
import {
generateUser,
generateGroup,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
import nconf from 'nconf';
const INVITES_LIMIT = 100;
const PARTY_LIMIT_MEMBERS = 30;
const MAX_EMAIL_INVITES_BY_USER = 200;
describe('Post /groups/:groupId/invite', () => {
let inviter;
@@ -16,7 +12,7 @@ describe('Post /groups/:groupId/invite', () => {
let groupName = 'Test Public Guild';
beforeEach(async () => {
inviter = await generateUser({balance: 4});
inviter = await generateUser({balance: 1});
group = await inviter.post('/groups', {
name: groupName,
type: 'guild',
@@ -207,37 +203,13 @@ describe('Post /groups/:groupId/invite', () => {
});
});
it('returns an error when a user has sent the max number of email invites', async () => {
let inviterWithMax = await generateUser({
invitesSent: MAX_EMAIL_INVITES_BY_USER,
balance: 4,
});
let tmpGroup = await inviterWithMax.post('/groups', {
name: groupName,
type: 'guild',
});
await expect(inviterWithMax.post(`/groups/${tmpGroup._id}/invite`, {
emails: [testInvite],
inviter: 'inviter name',
}))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('inviteLimitReached', {techAssistanceEmail: nconf.get('EMAILS:TECH_ASSISTANCE_EMAIL')}),
});
});
it('invites a user to a group by email', async () => {
let res = await inviter.post(`/groups/${group._id}/invite`, {
emails: [testInvite],
inviter: 'inviter name',
});
let updatedUser = await inviter.sync();
expect(res).to.exist;
expect(updatedUser.invitesSent).to.eql(1);
});
it('invites multiple users to a group by email', async () => {
@@ -245,10 +217,7 @@ describe('Post /groups/:groupId/invite', () => {
emails: [testInvite, {name: 'test2', email: 'test2@habitica.com'}],
});
let updatedUser = await inviter.sync();
expect(res).to.exist;
expect(updatedUser.invitesSent).to.eql(2);
});
});
@@ -296,25 +265,6 @@ describe('Post /groups/:groupId/invite', () => {
expect(invitedUser.invitations.guilds[0].id).to.equal(group._id);
expect(invite).to.exist;
});
it('invites marks invite with cancelled plan', async () => {
let cancelledPlanGroup = await generateGroup(inviter, {
type: 'guild',
name: generateUUID(),
});
await cancelledPlanGroup.createCancelledSubscription();
let newUser = await generateUser();
let invite = await inviter.post(`/groups/${cancelledPlanGroup._id}/invite`, {
uuids: [newUser._id],
emails: [{name: 'test', email: 'test@habitica.com'}],
});
let invitedUser = await newUser.get('/user');
expect(invitedUser.invitations.guilds[0].id).to.equal(cancelledPlanGroup._id);
expect(invitedUser.invitations.guilds[0].cancelledPlan).to.be.true;
expect(invite).to.exist;
});
});
describe('guild invites', () => {
@@ -351,19 +301,6 @@ describe('Post /groups/:groupId/invite', () => {
});
});
it('allows 30+ members in a guild', async () => {
let invitesToGenerate = [];
// Generate 30 users to invite (30 + leader = 31 members)
for (let i = 0; i < PARTY_LIMIT_MEMBERS; i++) {
invitesToGenerate.push(generateUser());
}
let generatedInvites = await Promise.all(invitesToGenerate);
// Invite users
expect(await inviter.post(`/groups/${group._id}/invite`, {
uuids: generatedInvites.map(invite => invite._id),
})).to.be.an('array');
});
// @TODO: Add this after we are able to mock the group plan route
xit('returns an error when a non-leader invites to a group plan', async () => {
let userToInvite = await generateUser();
@@ -453,36 +390,5 @@ describe('Post /groups/:groupId/invite', () => {
});
expect((await userToInvite.get('/user')).invitations.party.id).to.equal(party._id);
});
it('allows 30 members in a party', async () => {
let invitesToGenerate = [];
// Generate 29 users to invite (29 + leader = 30 members)
for (let i = 0; i < PARTY_LIMIT_MEMBERS - 1; i++) {
invitesToGenerate.push(generateUser());
}
let generatedInvites = await Promise.all(invitesToGenerate);
// Invite users
expect(await inviter.post(`/groups/${party._id}/invite`, {
uuids: generatedInvites.map(invite => invite._id),
})).to.be.an('array');
});
it('does not allow 30+ members in a party', async () => {
let invitesToGenerate = [];
// Generate 30 users to invite (30 + leader = 31 members)
for (let i = 0; i < PARTY_LIMIT_MEMBERS; i++) {
invitesToGenerate.push(generateUser());
}
let generatedInvites = await Promise.all(invitesToGenerate);
// Invite users
await expect(inviter.post(`/groups/${party._id}/invite`, {
uuids: generatedInvites.map(invite => invite._id),
}))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('partyExceedsMembersLimit', {maxMembersParty: PARTY_LIMIT_MEMBERS}),
});
});
});
});
@@ -1,85 +0,0 @@
import {
generateUser,
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
describe('POST /group/:groupId/add-manager', () => {
let leader, nonLeader, groupToUpdate;
let groupName = 'Test Public Guild';
let groupType = 'guild';
let nonMember;
context('Guilds', () => {
beforeEach(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: groupName,
type: groupType,
privacy: 'public',
},
members: 1,
});
groupToUpdate = group;
leader = groupLeader;
nonLeader = members[0];
nonMember = await generateUser();
});
it('returns an error when a non group leader tries to add member', async () => {
await expect(nonLeader.post(`/groups/${groupToUpdate._id}/add-manager`, {
managerId: nonLeader._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageGroupOnlyLeaderCanUpdate'),
});
});
it('returns an error when trying to promote a non member', async () => {
await expect(leader.post(`/groups/${groupToUpdate._id}/add-manager`, {
managerId: nonMember._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('userMustBeMember'),
});
});
it('allows a leader to add managers', async () => {
let updatedGroup = await leader.post(`/groups/${groupToUpdate._id}/add-manager`, {
managerId: nonLeader._id,
});
expect(updatedGroup.managers[nonLeader._id]).to.be.true;
});
});
context('Party', () => {
let party, partyLeader, partyNonLeader;
beforeEach(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: groupName,
type: 'party',
privacy: 'private',
},
members: 1,
});
party = group;
partyLeader = groupLeader;
partyNonLeader = members[0];
});
it('allows leader of party to add managers', async () => {
let updatedGroup = await partyLeader.post(`/groups/${party._id}/add-manager`, {
managerId: partyNonLeader._id,
});
expect(updatedGroup.managers[partyNonLeader._id]).to.be.true;
});
});
});
@@ -43,15 +43,4 @@ describe('PUT /group', () => {
expect(updatedGroup.leader.profile.name).to.eql(leader.profile.name);
expect(updatedGroup.name).to.equal(groupUpdatedName);
});
it('allows a leader to change leaders', async () => {
let updatedGroup = await leader.put(`/groups/${groupToUpdate._id}`, {
name: groupUpdatedName,
leader: nonLeader._id,
});
expect(updatedGroup.leader._id).to.eql(nonLeader._id);
expect(updatedGroup.leader.profile.name).to.eql(nonLeader.profile.name);
expect(updatedGroup.name).to.equal(groupUpdatedName);
});
});
@@ -37,7 +37,7 @@ describe('GET /members/:memberId', () => {
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
expect(Object.keys(memberRes.preferences).sort()).to.eql([
'size', 'hair', 'skin', 'shirt',
'chair', 'costume', 'sleep', 'background', 'tasks',
'chair', 'costume', 'sleep', 'background',
].sort());
expect(memberRes.stats.maxMP).to.exist;
@@ -208,20 +208,6 @@ describe('POST /tasks/:id/score/:direction', () => {
expect(task.completed).to.equal(false);
});
it('computes isDue', async () => {
await user.post(`/tasks/${daily._id}/score/up`);
let task = await user.get(`/tasks/${daily._id}`);
expect(task.isDue).to.equal(true);
});
it('computes nextDue', async () => {
await user.post(`/tasks/${daily._id}/score/up`);
let task = await user.get(`/tasks/${daily._id}`);
expect(task.nextDue.length).to.eql(6);
});
it('scores up daily even if it is already completed'); // Yes?
it('scores down daily even if it is already uncompleted'); // Yes?
@@ -329,30 +315,6 @@ describe('POST /tasks/:id/score/:direction', () => {
expect(updatedUser.stats.gp).to.be.greaterThan(user.stats.gp);
});
it('adds score notes to task', async () => {
let scoreNotesString = 'test-notes';
await user.post(`/tasks/${habit._id}/score/up`, {
scoreNotes: scoreNotesString,
});
let updatedTask = await user.get(`/tasks/${habit._id}`);
expect(updatedTask.history[0].scoreNotes).to.eql(scoreNotesString);
});
it('errors when score notes are too large', async () => {
let scoreNotesString = new Array(258).join('a');
await expect(user.post(`/tasks/${habit._id}/score/up`, {
scoreNotes: scoreNotesString,
}))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskScoreNotesTooLong'),
});
});
});
context('reward', () => {
@@ -496,8 +496,6 @@ describe('POST /tasks/user', () => {
frequency: 'daily',
everyX: 5,
startDate: now,
daysOfMonth: [15],
weeksOfMonth: [3],
});
expect(task.userId).to.equal(user._id);
@@ -506,11 +504,7 @@ describe('POST /tasks/user', () => {
expect(task.type).to.eql('daily');
expect(task.frequency).to.eql('daily');
expect(task.everyX).to.eql(5);
expect(task.daysOfMonth).to.eql([15]);
expect(task.weeksOfMonth).to.eql([3]);
expect(new Date(task.startDate)).to.eql(now);
expect(task.isDue).to.be.true;
expect(task.nextDue.length).to.eql(6);
});
it('creates multiple dailys', async () => {
@@ -1,4 +1,3 @@
import moment from 'moment';
import {
generateUser,
generateGroup,
@@ -396,15 +395,12 @@ describe('PUT /tasks/:id', () => {
notes: 'some new notes',
frequency: 'daily',
everyX: 5,
startDate: moment().add(1, 'days').toDate(),
});
expect(savedDaily.text).to.eql('some new text');
expect(savedDaily.notes).to.eql('some new notes');
expect(savedDaily.frequency).to.eql('daily');
expect(savedDaily.everyX).to.eql(5);
expect(savedDaily.isDue).to.be.false;
expect(savedDaily.nextDue.length).to.eql(6);
});
it('can update checklists (replace it)', async () => {
@@ -4,7 +4,7 @@ import {
} from '../../../../../helpers/api-integration/v3';
import { find } from 'lodash';
describe('Groups DELETE /tasks/:id', () => {
describe('DELETE /tasks/:id', () => {
let user, guild, member, member2, task;
function findAssignedTask (memberTask) {
@@ -48,21 +48,6 @@ describe('Groups DELETE /tasks/:id', () => {
});
});
it('allows a manager to delete a group task', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.del(`/tasks/${task._id}`);
await expect(user.get(`/tasks/${task._id}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('taskNotFound'),
});
});
it('unlinks assigned user', async () => {
await user.del(`/tasks/${task._id}`);
@@ -55,13 +55,4 @@ describe('GET /approvals/group/:groupId', () => {
let approvals = await user.get(`/approvals/group/${guild._id}`);
expect(approvals[0]._id).to.equal(syncedTask._id);
});
it('allows managers to get a list of task that need approval', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member._id,
});
let approvals = await member.get(`/approvals/group/${guild._id}`);
expect(approvals[0]._id).to.equal(syncedTask._id);
});
});
@@ -5,7 +5,7 @@ import {
import { find } from 'lodash';
describe('POST /tasks/:id/approve/:userId', () => {
let user, guild, member, member2, task;
let user, guild, member, task;
function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
@@ -17,13 +17,12 @@ describe('POST /tasks/:id/approve/:userId', () => {
name: 'Test Guild',
type: 'guild',
},
members: 2,
members: 1,
});
guild = group;
user = groupLeader;
member = members[0];
member2 = members[1];
task = await user.post(`/tasks/group/${guild._id}`, {
text: 'test todo',
@@ -70,74 +69,4 @@ describe('POST /tasks/:id/approve/:userId', () => {
expect(syncedTask.group.approval.approvingUser).to.equal(user._id);
expect(syncedTask.group.approval.dateApproved).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
});
it('allows a manager to approve an assigned user', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._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 syncedTask = find(memberTasks, findAssignedTask);
await member.sync();
expect(member.notifications.length).to.equal(2);
expect(member.notifications[0].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(syncedTask.group.approval.approved).to.be.true;
expect(syncedTask.group.approval.approvingUser).to.equal(member2._id);
expect(syncedTask.group.approval.dateApproved).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
});
it('removes approval pending notifications from managers', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._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 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(1);
expect(member2.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(1);
expect(member2.notifications.length).to.equal(0);
});
it('prevents double approval on a task', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
await member2.post(`/tasks/${task._id}/approve/${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('canOnlyApproveTaskOnce'),
});
});
});
@@ -5,7 +5,7 @@ import {
import { find } from 'lodash';
describe('POST /tasks/:id/score/:direction', () => {
let user, guild, member, member2, task;
let user, guild, member, task;
function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
@@ -17,13 +17,12 @@ describe('POST /tasks/:id/score/:direction', () => {
name: 'Test Guild',
type: 'guild',
},
members: 2,
members: 1,
});
guild = group;
user = groupLeader;
member = members[0];
member2 = members[1];
task = await user.post(`/tasks/group/${guild._id}`, {
text: 'test todo',
@@ -52,55 +51,18 @@ describe('POST /tasks/:id/score/:direction', () => {
await user.sync();
expect(user.notifications.length).to.equal(2);
expect(user.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
expect(user.notifications[1].data.message).to.equal(t('userHasRequestedTaskApproval', {
expect(user.notifications.length).to.equal(1);
expect(user.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
expect(user.notifications[0].data.message).to.equal(t('userHasRequestedTaskApproval', {
user: member.auth.local.username,
taskName: updatedTask.text,
taskId: updatedTask._id,
}, 'cs')); // This test only works if we have the notification translated
expect(user.notifications[1].data.groupId).to.equal(guild._id);
expect(user.notifications[0].data.groupId).to.equal(guild._id);
expect(updatedTask.group.approval.requested).to.equal(true);
expect(updatedTask.group.approval.requestedDate).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
});
it('sends notifications to all managers', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: 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 updatedTask = await member.get(`/tasks/${syncedTask._id}`);
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(2);
expect(user.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
expect(user.notifications[1].data.message).to.equal(t('userHasRequestedTaskApproval', {
user: member.auth.local.username,
taskName: updatedTask.text,
taskId: updatedTask._id,
}));
expect(user.notifications[1].data.groupId).to.equal(guild._id);
expect(member2.notifications.length).to.equal(1);
expect(member2.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
expect(member2.notifications[0].data.message).to.equal(t('userHasRequestedTaskApproval', {
user: member.auth.local.username,
taskName: updatedTask.text,
taskId: updatedTask._id,
}));
expect(member2.notifications[0].data.groupId).to.equal(guild._id);
});
it('errors when approval has already been requested', async () => {
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
@@ -1,29 +1,16 @@
import {
generateUser,
createAndPopulateGroup,
generateGroup,
translate as t,
} from '../../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
describe('POST /tasks/group/:groupid', () => {
let user, guild, manager;
let groupName = 'Test Public Guild';
let groupType = 'guild';
let user, guild;
beforeEach(async () => {
user = await generateUser({balance: 1});
let { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: groupName,
type: groupType,
privacy: 'private',
},
members: 1,
});
guild = group;
user = groupLeader;
manager = members[0];
guild = await generateGroup(user, {type: 'guild'});
});
it('returns error when group is not found', async () => {
@@ -129,27 +116,4 @@ describe('POST /tasks/group/:groupid', () => {
expect(task.everyX).to.eql(5);
expect(new Date(task.startDate)).to.eql(now);
});
it('allows a manager to add a group task', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: manager._id,
});
let task = await manager.post(`/tasks/group/${guild._id}`, {
text: 'test habit',
type: 'habit',
up: false,
down: true,
notes: 1976,
});
let groupTask = await manager.get(`/tasks/group/${guild._id}`);
expect(groupTask[0].group.id).to.equal(guild._id);
expect(task.text).to.eql('test habit');
expect(task.notes).to.eql('1976');
expect(task.type).to.eql('habit');
expect(task.up).to.eql(false);
expect(task.down).to.eql(true);
});
});
@@ -6,7 +6,7 @@ import {
import { v4 as generateUUID } from 'uuid';
import { find } from 'lodash';
describe('POST /tasks/:taskId/assign/:memberId', () => {
describe('POST /tasks/:taskId', () => {
let user, guild, member, member2, task;
function findAssignedTask (memberTask) {
@@ -130,19 +130,4 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
expect(member1SyncedTask).to.exist;
expect(member2SyncedTask).to.exist;
});
it('allows a manager to assign tasks', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
let groupTask = await member2.get(`/tasks/group/${guild._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
expect(groupTask[0].group.assignedUsers).to.contain(member._id);
expect(syncedTask).to.exist;
});
});
@@ -114,19 +114,4 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
expect(groupTask[0].group.assignedUsers).to.contain(member2._id);
expect(member2SyncedTask).to.exist;
});
it('allows a manager to unassign a user from a task', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/unassign/${member._id}`);
let groupTask = await member2.get(`/tasks/group/${guild._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
expect(groupTask[0].group.assignedUsers).to.not.contain(member._id);
expect(syncedTask).to.not.exist;
});
});
@@ -89,25 +89,4 @@ describe('PUT /tasks/:id', () => {
expect(member2SyncedTask.up).to.eql(false);
expect(member2SyncedTask.down).to.eql(false);
});
it('updates the linked tasks', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.put(`/tasks/${task._id}`, {
text: 'some new text',
up: false,
down: false,
notes: 'some new notes',
});
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
expect(syncedTask.text).to.eql('some new text');
expect(syncedTask.up).to.eql(false);
expect(syncedTask.down).to.eql(false);
});
});
@@ -3,7 +3,6 @@ import {
createAndPopulateGroup,
generateGroup,
generateUser,
generateChallenge,
translate as t,
} from '../../../../helpers/api-integration/v3';
import {
@@ -16,7 +15,6 @@ import {
sha1MakeSalt,
sha1Encrypt as sha1EncryptPassword,
} from '../../../../../website/server/libs/password';
import * as email from '../../../../../website/server/libs/email';
describe('DELETE /user', () => {
let user;
@@ -26,7 +24,7 @@ describe('DELETE /user', () => {
user = await generateUser({balance: 10});
});
it('returns an error if password is wrong', async () => {
it('returns an errors if password is wrong', async () => {
await expect(user.del('/user', {
password: 'wrong-password',
})).to.eventually.be.rejected.and.eql({
@@ -36,33 +34,6 @@ describe('DELETE /user', () => {
});
});
it('returns an error if password is not supplied', async () => {
await expect(user.del('/user', {
password: '',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingPassword'),
});
});
it('returns an error if excessive feedback is supplied', async () => {
let feedbackText = 'spam feedback ';
let feedback = feedbackText;
while (feedback.length < 10000) {
feedback = feedback + feedbackText;
}
await expect(user.del('/user', {
password,
feedback,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Account deletion feedback is limited to 10,000 characters. For lengthy feedback, email admin@habitica.com.',
});
});
it('returns an error if user has active subscription', async () => {
let userWithSubscription = await generateUser({'purchased.plan.customerId': 'fake-customer-id'});
@@ -93,30 +64,6 @@ describe('DELETE /user', () => {
}));
});
it('reduces memberCount in challenges user is linked to', async () => {
let populatedGroup = await createAndPopulateGroup({
members: 2,
});
let group = populatedGroup.group;
let authorizedUser = populatedGroup.members[1];
let challenge = await generateChallenge(populatedGroup.groupLeader, group);
await authorizedUser.post(`/challenges/${challenge._id}/join`);
await challenge.sync();
expect(challenge.memberCount).to.eql(2);
await authorizedUser.del('/user', {
password,
});
await challenge.sync();
expect(challenge.memberCount).to.eql(1);
});
it('deletes the user', async () => {
await user.del('/user', {
password,
@@ -124,32 +71,6 @@ describe('DELETE /user', () => {
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
});
it('sends feedback to the admin email', async () => {
sandbox.spy(email, 'sendTxn');
let feedback = 'Reasons for Deletion';
await user.del('/user', {
password,
feedback,
});
expect(email.sendTxn).to.be.calledOnce;
sandbox.restore();
});
it('does not send email if no feedback is supplied', async () => {
sandbox.spy(email, 'sendTxn');
await user.del('/user', {
password,
});
expect(email.sendTxn).to.not.be.called;
sandbox.restore();
});
it('deletes the user with a legacy sha1 password', async () => {
let textPassword = 'mySecretPassword';
let salt = sha1MakeSalt();
@@ -9,8 +9,6 @@ import {
sha1Encrypt as sha1EncryptPassword,
} from '../../../../../../website/server/libs/password';
import nconf from 'nconf';
describe('POST /user/auth/local/login', () => {
let api;
let user;
@@ -45,7 +43,7 @@ describe('POST /user/auth/local/login', () => {
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('accountSuspended', { communityManagerEmail: nconf.get('EMAILS:COMMUNITY_MANAGER_EMAIL'), userId: user._id }),
message: t('accountSuspended', { userId: user._id }),
});
});
@@ -5,7 +5,6 @@ import {
createAndPopulateGroup,
getProperty,
} from '../../../../../helpers/api-integration/v3';
import { ApiUser } from '../../../../../helpers/api-integration/api-classes';
import { v4 as generateRandomUserName } from 'uuid';
import { each } from 'lodash';
import { encrypt } from '../../../../../../website/server/libs/encryption';
@@ -34,209 +33,25 @@ describe('POST /user/auth/local/register', () => {
expect(user.apiToken).to.exist;
expect(user.auth.local.username).to.eql(username);
expect(user.profile.name).to.eql(username);
expect(user.newUser).to.eql(true);
});
context('provides default tags and tasks', async () => {
it('for a generic API consumer', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
it('provides default tags and tasks', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
let requests = new ApiUser(user);
let habits = await requests.get('/tasks/user?type=habits');
let dailys = await requests.get('/tasks/user?type=dailys');
let todos = await requests.get('/tasks/user?type=todos');
let rewards = await requests.get('/tasks/user?type=rewards');
let tags = await requests.get('/tags');
expect(habits).to.have.a.lengthOf(0);
expect(dailys).to.have.a.lengthOf(0);
expect(todos).to.have.a.lengthOf(1);
expect(todos[0].text).to.eql(t('defaultTodo1Text'));
expect(todos[0].notes).to.eql(t('defaultTodoNotes'));
expect(rewards).to.have.a.lengthOf(0);
expect(tags).to.have.a.lengthOf(7);
expect(tags[0].name).to.eql(t('defaultTag1'));
expect(tags[1].name).to.eql(t('defaultTag2'));
expect(tags[2].name).to.eql(t('defaultTag3'));
expect(tags[3].name).to.eql(t('defaultTag4'));
expect(tags[4].name).to.eql(t('defaultTag5'));
expect(tags[5].name).to.eql(t('defaultTag6'));
expect(tags[6].name).to.eql(t('defaultTag7'));
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
it('for Web', async () => {
api = requester(
null,
{'x-client': 'habitica-web'},
);
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
let requests = new ApiUser(user);
let habits = await requests.get('/tasks/user?type=habits');
let dailys = await requests.get('/tasks/user?type=dailys');
let todos = await requests.get('/tasks/user?type=todos');
let rewards = await requests.get('/tasks/user?type=rewards');
let tags = await requests.get('/tags');
expect(habits).to.have.a.lengthOf(3);
expect(habits[0].text).to.eql(t('defaultHabit1Text'));
expect(habits[0].notes).to.eql('');
expect(habits[1].text).to.eql(t('defaultHabit2Text'));
expect(habits[1].notes).to.eql('');
expect(habits[2].text).to.eql(t('defaultHabit3Text'));
expect(habits[2].notes).to.eql('');
expect(dailys).to.have.a.lengthOf(0);
expect(todos).to.have.a.lengthOf(1);
expect(todos[0].text).to.eql(t('defaultTodo1Text'));
expect(todos[0].notes).to.eql(t('defaultTodoNotes'));
expect(rewards).to.have.a.lengthOf(1);
expect(rewards[0].text).to.eql(t('defaultReward1Text'));
expect(rewards[0].notes).to.eql('');
expect(tags).to.have.a.lengthOf(7);
expect(tags[0].name).to.eql(t('defaultTag1'));
expect(tags[1].name).to.eql(t('defaultTag2'));
expect(tags[2].name).to.eql(t('defaultTag3'));
expect(tags[3].name).to.eql(t('defaultTag4'));
expect(tags[4].name).to.eql(t('defaultTag5'));
expect(tags[5].name).to.eql(t('defaultTag6'));
expect(tags[6].name).to.eql(t('defaultTag7'));
});
it('for Android', async () => {
api = requester(
null,
{'x-client': 'habitica-android'},
);
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
let requests = new ApiUser(user);
let habits = await requests.get('/tasks/user?type=habits');
let dailys = await requests.get('/tasks/user?type=dailys');
let todos = await requests.get('/tasks/user?type=todos');
let rewards = await requests.get('/tasks/user?type=rewards');
let tags = await requests.get('/tags');
expect(habits).to.have.a.lengthOf(2);
expect(habits[0].text).to.eql(t('defaultHabit4Text'));
expect(habits[0].notes).to.eql(t('defaultHabit4Notes'));
expect(habits[1].text).to.eql(t('defaultHabit5Text'));
expect(habits[1].notes).to.eql(t('defaultHabit5Notes'));
expect(dailys).to.have.a.lengthOf(1);
expect(dailys[0].text).to.eql(t('defaultDaily1Text'));
expect(dailys[0].notes).to.eql('');
expect(todos).to.have.a.lengthOf(2);
expect(todos[0].text).to.eql(t('defaultTodo1Text'));
expect(todos[0].notes).to.eql(t('defaultTodoNotes'));
expect(todos[1].text).to.eql(t('defaultTodo2Text'));
expect(todos[1].notes).to.eql(t('defaultTodo2Notes'));
expect(rewards).to.have.a.lengthOf(1);
expect(rewards[0].text).to.eql(t('defaultReward2Text'));
expect(rewards[0].notes).to.eql(t('defaultReward2Notes'));
expect(tags).to.have.a.lengthOf(7);
expect(tags[0].name).to.eql(t('defaultTag1'));
expect(tags[1].name).to.eql(t('defaultTag2'));
expect(tags[2].name).to.eql(t('defaultTag3'));
expect(tags[3].name).to.eql(t('defaultTag4'));
expect(tags[4].name).to.eql(t('defaultTag5'));
expect(tags[5].name).to.eql(t('defaultTag6'));
expect(tags[6].name).to.eql(t('defaultTag7'));
});
it('for iOS', async () => {
api = requester(
null,
{'x-client': 'habitica-ios'},
);
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
let requests = new ApiUser(user);
let habits = await requests.get('/tasks/user?type=habits');
let dailys = await requests.get('/tasks/user?type=dailys');
let todos = await requests.get('/tasks/user?type=todos');
let rewards = await requests.get('/tasks/user?type=rewards');
let tags = await requests.get('/tags');
expect(habits).to.have.a.lengthOf(2);
expect(habits[0].text).to.eql(t('defaultHabit4Text'));
expect(habits[0].notes).to.eql(t('defaultHabit4Notes'));
expect(habits[1].text).to.eql(t('defaultHabit5Text'));
expect(habits[1].notes).to.eql(t('defaultHabit5Notes'));
expect(dailys).to.have.a.lengthOf(1);
expect(dailys[0].text).to.eql(t('defaultDaily1Text'));
expect(dailys[0].notes).to.eql('');
expect(todos).to.have.a.lengthOf(2);
expect(todos[0].text).to.eql(t('defaultTodo1Text'));
expect(todos[0].notes).to.eql(t('defaultTodoNotes'));
expect(todos[1].text).to.eql(t('defaultTodo2Text'));
expect(todos[1].notes).to.eql(t('defaultTodo2Notes'));
expect(rewards).to.have.a.lengthOf(1);
expect(rewards[0].text).to.eql(t('defaultReward2Text'));
expect(rewards[0].notes).to.eql(t('defaultReward2Notes'));
expect(tags).to.have.a.lengthOf(7);
expect(tags[0].name).to.eql(t('defaultTag1'));
expect(tags[1].name).to.eql(t('defaultTag2'));
expect(tags[2].name).to.eql(t('defaultTag3'));
expect(tags[3].name).to.eql(t('defaultTag4'));
expect(tags[4].name).to.eql(t('defaultTag5'));
expect(tags[5].name).to.eql(t('defaultTag6'));
expect(tags[6].name).to.eql(t('defaultTag7'));
});
expect(user.tags).to.have.a.lengthOf(7);
expect(user.tasksOrder.todos).to.have.a.lengthOf(1);
expect(user.tasksOrder.dailys).to.have.a.lengthOf(0);
expect(user.tasksOrder.rewards).to.have.a.lengthOf(0);
expect(user.tasksOrder.habits).to.have.a.lengthOf(0);
});
it('enrolls new users in an A/B test', async () => {
@@ -251,26 +66,10 @@ describe('POST /user/auth/local/register', () => {
confirmPassword: password,
});
await expect(getProperty('users', user._id, '_ABtest')).to.eventually.be.a('string');
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
});
it('includes items awarded by default when creating a new user', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.items.quests.dustbunnies).to.equal(1);
expect(user.purchased.background.violet).to.be.ok;
expect(user.preferences.background).to.equal('violet');
});
it('requires password and confirmPassword to match', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
@@ -617,37 +416,5 @@ describe('POST /user/auth/local/register', () => {
expect(user.tags).to.not.be.empty;
});
it('adds the correct tags to the correct tasks', async () => {
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
let requests = new ApiUser(user);
let habits = await requests.get('/tasks/user?type=habits');
let todos = await requests.get('/tasks/user?type=todos');
function findTag (tagName) {
let tag = user.tags.find((userTag) => {
return userTag.name === t(tagName);
});
return tag.id;
}
expect(habits[0].tags).to.have.a.lengthOf(3);
expect(habits[0].tags).to.include.members(['defaultTag1', 'defaultTag4', 'defaultTag6'].map(findTag));
expect(habits[1].tags).to.have.a.lengthOf(1);
expect(habits[1].tags).to.include.members(['defaultTag3'].map(findTag));
expect(habits[2].tags).to.have.a.lengthOf(2);
expect(habits[2].tags).to.include.members(['defaultTag2', 'defaultTag3'].map(findTag));
expect(todos[0].tags).to.have.a.lengthOf(0);
});
});
});
@@ -83,7 +83,7 @@ describe('POST /user/auth/social', () => {
network,
});
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
await expect(getProperty('users', user._id, '_ABtest')).to.eventually.be.a('string');
});
});
@@ -139,7 +139,7 @@ describe('POST /user/auth/social', () => {
network,
});
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
await expect(getProperty('users', user._id, '_ABtest')).to.eventually.be.a('string');
});
});
});
@@ -8,8 +8,6 @@ import {
sha1Encrypt as sha1EncryptPassword,
} from '../../../../../../website/server/libs/password';
import nconf from 'nconf';
const ENDPOINT = '/user/auth/update-email';
describe('PUT /user/auth/update-email', () => {
@@ -70,7 +68,7 @@ describe('PUT /user/auth/update-email', () => {
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('cannotFulfillReq', { techAssistanceEmail: nconf.get('EMAILS:TECH_ASSISTANCE_EMAIL') }),
message: t('cannotFulfillReq'),
});
});
+9 -151
View File
@@ -1,12 +1,10 @@
import moment from 'moment';
import cc from 'coupon-code';
import uuid from 'uuid';
import {
generateGroup,
} from '../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../website/server/models/user';
import { model as Group } from '../../../../../website/server/models/group';
import { model as Coupon } from '../../../../../website/server/models/coupon';
import amzLib from '../../../../../website/server/libs/amazonPayments';
import payments from '../../../../../website/server/libs/payments';
@@ -107,7 +105,7 @@ describe('Amazon Payments', () => {
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,
paymentMethod: amzLib.constants.PAYMENT_METHOD,
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
headers,
});
expectAmazonStubs();
@@ -130,7 +128,7 @@ describe('Amazon Payments', () => {
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,
paymentMethod: amzLib.constants.PAYMENT_METHOD_GIFT,
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON_GIFT,
headers,
gift,
});
@@ -155,7 +153,7 @@ describe('Amazon Payments', () => {
expect(paymentCreateSubscritionStub).to.be.calledOnce;
expect(paymentCreateSubscritionStub).to.be.calledWith({
user,
paymentMethod: amzLib.constants.PAYMENT_METHOD_GIFT,
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON_GIFT,
headers,
gift,
});
@@ -318,7 +316,7 @@ describe('Amazon Payments', () => {
expect(createSubSpy).to.be.calledWith({
user,
customerId: billingAgreementId,
paymentMethod: amzLib.constants.PAYMENT_METHOD,
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
sub,
headers,
groupId,
@@ -377,73 +375,7 @@ describe('Amazon Payments', () => {
expect(createSubSpy).to.be.calledWith({
user,
customerId: billingAgreementId,
paymentMethod: amzLib.constants.PAYMENT_METHOD,
sub,
headers,
groupId,
});
});
it('subscribes with amazon with price to existing users', async () => {
user = new User();
user.guilds.push(groupId);
await user.save();
group.memberCount = 2;
await group.save();
sub.key = 'group_monthly';
sub.price = 9;
amount = 12;
await amzLib.subscribe({
billingAgreementId,
sub,
coupon,
user,
groupId,
headers,
});
expect(amazonSetBillingAgreementDetailsSpy).to.be.calledOnce;
expect(amazonSetBillingAgreementDetailsSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
BillingAgreementAttributes: {
SellerNote: amzLib.constants.SELLER_NOTE_SUBSCRIPTION,
SellerBillingAgreementAttributes: {
SellerBillingAgreementId: common.uuid(),
StoreName: amzLib.constants.STORE_NAME,
CustomInformation: amzLib.constants.SELLER_NOTE_SUBSCRIPTION,
},
},
});
expect(amazonConfirmBillingAgreementSpy).to.be.calledOnce;
expect(amazonConfirmBillingAgreementSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
});
expect(amazongAuthorizeOnBillingAgreementSpy).to.be.calledOnce;
expect(amazongAuthorizeOnBillingAgreementSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
AuthorizationReferenceId: common.uuid().substring(0, 32),
AuthorizationAmount: {
CurrencyCode: amzLib.constants.CURRENCY_CODE,
Amount: amount,
},
SellerAuthorizationNote: amzLib.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION,
TransactionTimeout: 0,
CaptureNow: true,
SellerNote: amzLib.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION,
SellerOrderAttributes: {
SellerOrderId: common.uuid(),
StoreName: amzLib.constants.STORE_NAME,
},
});
expect(createSubSpy).to.be.calledOnce;
expect(createSubSpy).to.be.calledWith({
user,
customerId: billingAgreementId,
paymentMethod: amzLib.constants.PAYMENT_METHOD,
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
sub,
headers,
groupId,
@@ -523,7 +455,7 @@ describe('Amazon Payments', () => {
user,
groupId: undefined,
nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: amzLib.constants.PAYMENT_METHOD,
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
headers,
});
expectAmazonStubs();
@@ -553,7 +485,7 @@ describe('Amazon Payments', () => {
user,
groupId: undefined,
nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: amzLib.constants.PAYMENT_METHOD,
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
headers,
});
amzLib.closeBillingAgreement.restore();
@@ -591,7 +523,7 @@ describe('Amazon Payments', () => {
user,
groupId: group._id,
nextBill: moment(group.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: amzLib.constants.PAYMENT_METHOD,
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
headers,
});
expectAmazonStubs();
@@ -621,84 +553,10 @@ describe('Amazon Payments', () => {
user,
groupId: group._id,
nextBill: moment(group.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: amzLib.constants.PAYMENT_METHOD,
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
headers,
});
amzLib.closeBillingAgreement.restore();
});
});
describe('#upgradeGroupPlan', () => {
let spy, data, user, group, uuidString;
beforeEach(async function () {
user = new User();
user.profile.name = 'sender';
data = {
user,
sub: {
key: 'basic_3mo', // @TODO: Validate that this is group
},
customerId: 'customer-id',
paymentMethod: 'Payment Method',
headers: {
'x-client': 'habitica-web',
'user-agent': '',
},
};
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
await group.save();
spy = sinon.stub(amzLib, 'authorizeOnBillingAgreement');
spy.returnsPromise().resolves([]);
uuidString = 'uuid-v4';
sinon.stub(uuid, 'v4').returns(uuidString);
data.groupId = group._id;
data.sub.quantity = 3;
});
afterEach(function () {
sinon.restore(amzLib.authorizeOnBillingAgreement);
uuid.v4.restore();
});
it('charges for a new member', async () => {
data.paymentMethod = amzLib.constants.PAYMENT_METHOD;
await payments.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
updatedGroup.memberCount += 1;
await updatedGroup.save();
await amzLib.chargeForAdditionalGroupMember(updatedGroup);
expect(spy.calledOnce).to.be.true;
expect(spy).to.be.calledWith({
AmazonBillingAgreementId: updatedGroup.purchased.plan.customerId,
AuthorizationReferenceId: uuidString.substring(0, 32),
AuthorizationAmount: {
CurrencyCode: amzLib.constants.CURRENCY_CODE,
Amount: 3,
},
SellerAuthorizationNote: amzLib.constants.SELLER_NOTE_GROUP_NEW_MEMBER,
TransactionTimeout: 0,
CaptureNow: true,
SellerNote: amzLib.constants.SELLER_NOTE_GROUP_NEW_MEMBER,
SellerOrderAttributes: {
SellerOrderId: uuidString,
StoreName: amzLib.constants.STORE_NAME,
},
});
});
});
});
-31
View File
@@ -1,31 +0,0 @@
import apiMessages from '../../../../../website/server/libs/apiMessages';
describe('API Messages', () => {
const message = 'Only public guilds support pagination.';
it('returns an API message', () => {
expect(apiMessages('guildsOnlyPaginate')).to.equal(message);
});
it('throws if the API message does not exist', () => {
expect(() => apiMessages('iDoNotExist')).to.throw;
});
it('clones the passed variables', () => {
let vars = {a: 1};
sandbox.stub(_, 'clone').returns({});
apiMessages('guildsOnlyPaginate', vars);
expect(_.clone).to.have.been.called.once;
expect(_.clone).to.have.been.calledWith(vars);
});
it('pass the message through _.template', () => {
let vars = {a: 1};
let stub = sinon.stub().returns('string');
sandbox.stub(_, 'template').returns(stub);
apiMessages('guildsOnlyPaginate', vars);
expect(_.template).to.have.been.called.once;
expect(_.template).to.have.been.calledWith(message);
expect(stub).to.have.been.called.once;
expect(stub).to.have.been.calledWith(vars);
});
});
+3 -100
View File
@@ -6,6 +6,7 @@ import requireAgain from 'require-again';
import { recoverCron, cron } from '../../../../../website/server/libs/cron';
import { model as User } from '../../../../../website/server/models/user';
import * as Tasks from '../../../../../website/server/models/task';
import { clone } from 'lodash';
import common from '../../../../../website/common';
import analytics from '../../../../../website/server/libs/analyticsService';
@@ -134,10 +135,7 @@ describe('cron', () => {
user.purchased.plan.dateUpdated = moment().subtract(6, 'months').toDate();
user.purchased.plan.dateTerminated = moment().subtract(3, 'months').toDate();
user.purchased.plan.consecutive.count = 5;
user.purchased.plan.consecutive.trinkets = 1;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.trinkets).to.equal(1);
});
@@ -313,24 +311,6 @@ describe('cron', () => {
expect(tasksByType.dailys[0].completed).to.be.false;
expect(user.stats.hp).to.equal(healthBefore);
});
it('sets isDue for daily', () => {
let daily = {
text: 'test daily',
type: 'daily',
frequency: 'daily',
everyX: 5,
startDate: new Date(),
};
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line new-cap
tasksByType.dailys.push(task);
tasksByType.dailys[0].completed = true;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].isDue).to.be.exist;
});
});
describe('todos', () => {
@@ -376,22 +356,6 @@ describe('cron', () => {
};
});
it('computes isDue', () => {
tasksByType.dailys[0].frequency = 'daily';
tasksByType.dailys[0].everyX = 5;
tasksByType.dailys[0].startDate = moment().add(1, 'days').toDate();
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].isDue).to.be.false;
});
it('computes nextDue', () => {
tasksByType.dailys[0].frequency = 'daily';
tasksByType.dailys[0].everyX = 5;
tasksByType.dailys[0].startDate = moment().add(1, 'days').toDate();
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].nextDue.length).to.eql(6);
});
it('should add history', () => {
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].history).to.be.lengthOf(1);
@@ -517,67 +481,6 @@ describe('cron', () => {
expect(tasksByType.habits[0].value).to.equal(1);
});
describe('counters', () => {
let notStartOfWeekOrMonth = new Date(2016, 9, 28).getTime(); // a Friday
let clock;
beforeEach(() => {
// Replace system clocks so we can get predictable results
clock = sinon.useFakeTimers(notStartOfWeekOrMonth);
});
afterEach(() => {
return clock.restore();
});
it('should reset a daily habit counter each day', () => {
tasksByType.habits[0].counterUp = 1;
tasksByType.habits[0].counterDown = 1;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.habits[0].counterUp).to.equal(0);
expect(tasksByType.habits[0].counterDown).to.equal(0);
});
it('should reset a weekly habit counter each Monday', () => {
tasksByType.habits[0].frequency = 'weekly';
tasksByType.habits[0].counterUp = 1;
tasksByType.habits[0].counterDown = 1;
// should not reset
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.habits[0].counterUp).to.equal(1);
expect(tasksByType.habits[0].counterDown).to.equal(1);
// should reset
daysMissed = 8;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.habits[0].counterUp).to.equal(0);
expect(tasksByType.habits[0].counterDown).to.equal(0);
});
it('should reset a monthly habit counter the first day of each month', () => {
tasksByType.habits[0].frequency = 'monthly';
tasksByType.habits[0].counterUp = 1;
tasksByType.habits[0].counterDown = 1;
// should not reset
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.habits[0].counterUp).to.equal(1);
expect(tasksByType.habits[0].counterDown).to.equal(1);
// should reset
daysMissed = 32;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.habits[0].counterUp).to.equal(0);
expect(tasksByType.habits[0].counterDown).to.equal(0);
});
});
});
describe('perfect day', () => {
@@ -630,7 +533,7 @@ describe('cron', () => {
tasksByType.dailys[0].completed = true;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
let previousBuffs = user.stats.buffs.toObject();
let previousBuffs = clone(user.stats.buffs);
cron({user, tasksByType, daysMissed, analytics});
@@ -695,7 +598,7 @@ describe('cron', () => {
tasksByType.dailys[0].completed = false;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
let previousBuffs = user.stats.buffs.toObject();
let previousBuffs = clone(user.stats.buffs);
cronOverride({user, tasksByType, daysMissed, analytics});
+198 -35
View File
@@ -1,18 +1,22 @@
import moment from 'moment';
import * as sender from '../../../../../website/server/libs/email';
import * as api from '../../../../../website/server/libs/payments';
import analytics from '../../../../../website/server/libs/analyticsService';
import notifications from '../../../../../website/server/libs/pushNotifications';
import { model as User } from '../../../../../website/server/models/user';
import { model as Group } from '../../../../../website/server/models/group';
import stripeModule from 'stripe';
import moment from 'moment';
import { translate as t } from '../../../../helpers/api-v3-integration.helper';
import {
generateGroup,
} from '../../../../helpers/api-unit.helper.js';
import i18n from '../../../../../website/common/script/i18n';
describe('payments/index', () => {
let user, group, data, plan;
let stripe = stripeModule('test');
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
@@ -83,12 +87,6 @@ describe('payments/index', () => {
};
});
it('awards the Royal Purple Jackalope pet', async () => {
await api.createSubscription(data);
expect(recipient.items.pets['Jackalope-RoyalPurple']).to.eql(5);
});
it('adds extra months to an existing subscription', async () => {
recipient.purchased.plan = plan;
@@ -247,12 +245,6 @@ describe('payments/index', () => {
expect(user.purchased.plan.dateCreated).to.exist;
});
it('awards the Royal Purple Jackalope pet', async () => {
await api.createSubscription(data);
expect(user.items.pets['Jackalope-RoyalPurple']).to.eql(5);
});
it('sets extraMonths if plan has dateTerminated date', async () => {
user.purchased.plan = plan;
user.purchased.plan.dateTerminated = moment(new Date()).add(2, 'months');
@@ -327,6 +319,53 @@ describe('payments/index', () => {
});
});
context('Purchasing a subscription for group', () => {
it('creates a subscription', async () => {
expect(group.purchased.plan.planId).to.not.exist;
data.groupId = group._id;
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.planId).to.eql('basic_3mo');
expect(updatedGroup.purchased.plan.customerId).to.eql('customer-id');
expect(updatedGroup.purchased.plan.dateUpdated).to.exist;
expect(updatedGroup.purchased.plan.gemsBought).to.eql(0);
expect(updatedGroup.purchased.plan.paymentMethod).to.eql('Payment Method');
expect(updatedGroup.purchased.plan.extraMonths).to.eql(0);
expect(updatedGroup.purchased.plan.dateTerminated).to.eql(null);
expect(updatedGroup.purchased.plan.lastBillingDate).to.not.exist;
expect(updatedGroup.purchased.plan.dateCreated).to.exist;
});
it('sets extraMonths if plan has dateTerminated date', async () => {
group.purchased.plan = plan;
group.purchased.plan.dateTerminated = moment(new Date()).add(2, 'months');
await group.save();
expect(group.purchased.plan.extraMonths).to.eql(0);
data.groupId = group._id;
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.extraMonths).to.within(1.9, 2);
});
it('does not set negative extraMonths if plan has past dateTerminated date', async () => {
group.purchased.plan = plan;
group.purchased.plan.dateTerminated = moment(new Date()).subtract(2, 'months');
await group.save();
expect(group.purchased.plan.extraMonths).to.eql(0);
data.groupId = group._id;
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.extraMonths).to.eql(0);
});
});
context('Block subscription perks', () => {
it('adds block months to plan.consecutive.offset', async () => {
await api.createSubscription(data);
@@ -446,6 +485,7 @@ describe('payments/index', () => {
sandbox.spy(user.purchased.plan.mysteryItems, 'push');
data = { paymentMethod: 'PaymentMethod', user, sub: { key: 'basic_3mo' } };
await api.createSubscription(data);
expect(user.purchased.plan.mysteryItems.push).to.be.calledOnce;
@@ -519,6 +559,112 @@ describe('payments/index', () => {
expect(sender.sendTxn).to.be.calledWith(user, 'cancel-subscription');
});
});
context('Canceling a subscription for group', () => {
it('adds a month termination date by default', async () => {
data.groupId = group._id;
await api.cancelSubscription(data);
let now = new Date();
let updatedGroup = await Group.findById(group._id).exec();
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
expect(daysTillTermination).to.be.within(29, 30); // 1 month +/- 1 days
});
it('adds extraMonths to dateTerminated value', async () => {
group.purchased.plan.extraMonths = 2;
await group.save();
data.groupId = group._id;
await api.cancelSubscription(data);
let now = new Date();
let updatedGroup = await Group.findById(group._id).exec();
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
expect(daysTillTermination).to.be.within(89, 90); // 3 months +/- 1 days
});
it('handles extra month fractions', async () => {
group.purchased.plan.extraMonths = 0.3;
await group.save();
data.groupId = group._id;
await api.cancelSubscription(data);
let now = new Date();
let updatedGroup = await Group.findById(group._id).exec();
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
expect(daysTillTermination).to.be.within(38, 39); // should be about 1 month + 1/3 month
});
it('terminates at next billing date if it exists', async () => {
data.nextBill = moment().add({ days: 15 });
data.groupId = group._id;
await api.cancelSubscription(data);
let now = new Date();
let updatedGroup = await Group.findById(group._id).exec();
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
expect(daysTillTermination).to.be.within(13, 15);
});
it('resets plan.extraMonths', async () => {
group.purchased.plan.extraMonths = 5;
await group.save();
data.groupId = group._id;
await api.cancelSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.extraMonths).to.eql(0);
});
it('sends an email', async () => {
data.groupId = group._id;
await api.cancelSubscription(data);
expect(sender.sendTxn).to.be.calledOnce;
expect(sender.sendTxn).to.be.calledWith(user, 'group-cancel-subscription');
});
it('prevents non group leader from manging subscription', async () => {
let groupMember = new User();
data.user = groupMember;
data.groupId = group._id;
await expect(api.cancelSubscription(data))
.eventually.be.rejected.and.to.eql({
httpCode: 401,
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
name: 'NotAuthorized',
});
});
it('allows old group leader to cancel if they created the subscription', async () => {
data.groupId = group._id;
data.sub = {
key: 'group_monthly',
};
data.paymentMethod = 'Payment Method';
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
let newLeader = new User();
updatedGroup.leader = newLeader._id;
await updatedGroup.save();
await api.cancelSubscription(data);
updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.dateTerminated).to.exist;
});
});
});
describe('#buyGems', () => {
@@ -626,32 +772,49 @@ describe('payments/index', () => {
});
});
describe('addSubToGroupUser', () => {
it('adds a group subscription to a new user', async () => {
expect(group.purchased.plan.planId).to.not.exist;
describe('#upgradeGroupPlan', () => {
let spy;
beforeEach(function () {
spy = sinon.stub(stripe.subscriptions, 'update');
spy.returnsPromise().resolves([]);
data.groupId = group._id;
await api.addSubToGroupUser(user, group);
let updatedUser = await User.findById(user._id).exec();
expect(updatedUser.purchased.plan.planId).to.eql('group_plan_auto');
expect(updatedUser.purchased.plan.customerId).to.eql('group-plan');
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan');
expect(updatedUser.purchased.plan.extraMonths).to.eql(0);
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
expect(updatedUser.purchased.plan.dateCreated).to.exist;
data.sub.quantity = 3;
});
it('awards the Royal Purple Jackalope pet', async () => {
await api.addSubToGroupUser(user, group);
afterEach(function () {
sinon.restore(stripe.subscriptions.update);
});
let updatedUser = await User.findById(user._id).exec();
it('updates a group plan quantity', async () => {
data.paymentMethod = 'Stripe';
await api.createSubscription(data);
expect(updatedUser.items.pets['Jackalope-RoyalPurple']).to.eql(5);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.quantity).to.eql(3);
updatedGroup.memberCount += 1;
await updatedGroup.save();
await api.updateStripeGroupPlan(updatedGroup, stripe);
expect(spy.calledOnce).to.be.true;
expect(updatedGroup.purchased.plan.quantity).to.eql(4);
});
it('does not update a group plan quantity that has a payment method other than stripe', async () => {
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.quantity).to.eql(3);
updatedGroup.memberCount += 1;
await updatedGroup.save();
await api.updateStripeGroupPlan(updatedGroup, stripe);
expect(spy.calledOnce).to.be.false;
expect(updatedGroup.purchased.plan.quantity).to.eql(3);
});
});
});
@@ -1,326 +0,0 @@
import moment from 'moment';
import * as sender from '../../../../../../../website/server/libs/email';
import * as api from '../../../../../../../website/server/libs/payments';
import { model as User } from '../../../../../../../website/server/models/user';
import { model as Group } from '../../../../../../../website/server/models/group';
import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
import i18n from '../../../../../../../website/common/script/i18n';
describe('Canceling a subscription for group', () => {
let plan, group, user, data;
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
await user.save();
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
await group.save();
data = {
user,
sub: {
key: 'basic_3mo',
},
customerId: 'customer-id',
paymentMethod: 'Payment Method',
headers: {
'x-client': 'habitica-web',
'user-agent': '',
},
};
plan = {
planId: 'basic_3mo',
customerId: 'customer-id',
dateUpdated: new Date(),
gemsBought: 0,
paymentMethod: 'paymentMethod',
extraMonths: 0,
dateTerminated: null,
lastBillingDate: new Date(),
dateCreated: new Date(),
mysteryItems: [],
consecutive: {
trinkets: 0,
offset: 0,
gemCapExtra: 0,
},
};
sandbox.stub(sender, 'sendTxn');
});
afterEach(() => {
sender.sendTxn.restore();
});
it('adds a month termination date by default', async () => {
data.groupId = group._id;
await api.cancelSubscription(data);
let now = new Date();
let updatedGroup = await Group.findById(group._id).exec();
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
expect(daysTillTermination).to.be.within(29, 30); // 1 month +/- 1 days
});
it('adds extraMonths to dateTerminated value', async () => {
group.purchased.plan.extraMonths = 2;
await group.save();
data.groupId = group._id;
await api.cancelSubscription(data);
let now = new Date();
let updatedGroup = await Group.findById(group._id).exec();
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
expect(daysTillTermination).to.be.within(89, 90); // 3 months +/- 1 days
});
it('handles extra month fractions', async () => {
group.purchased.plan.extraMonths = 0.3;
await group.save();
data.groupId = group._id;
await api.cancelSubscription(data);
let now = new Date();
let updatedGroup = await Group.findById(group._id).exec();
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
expect(daysTillTermination).to.be.within(38, 39); // should be about 1 month + 1/3 month
});
it('terminates at next billing date if it exists', async () => {
data.nextBill = moment().add({ days: 15 });
data.groupId = group._id;
await api.cancelSubscription(data);
let now = new Date();
let updatedGroup = await Group.findById(group._id).exec();
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
expect(daysTillTermination).to.be.within(13, 15);
});
it('resets plan.extraMonths', async () => {
group.purchased.plan.extraMonths = 5;
await group.save();
data.groupId = group._id;
await api.cancelSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.extraMonths).to.eql(0);
});
it('sends an email', async () => {
data.groupId = group._id;
await api.cancelSubscription(data);
expect(sender.sendTxn).to.be.calledOnce;
expect(sender.sendTxn.firstCall.args[0]._id).to.equal(user._id);
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-cancel-subscription');
expect(sender.sendTxn.firstCall.args[2]).to.eql([
{name: 'GROUP_NAME', content: group.name},
]);
});
it('prevents non group leader from manging subscription', async () => {
let groupMember = new User();
data.user = groupMember;
data.groupId = group._id;
await expect(api.cancelSubscription(data))
.eventually.be.rejected.and.to.eql({
httpCode: 401,
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
name: 'NotAuthorized',
});
});
it('allows old group leader to cancel if they created the subscription', async () => {
data.groupId = group._id;
data.sub = {
key: 'group_monthly',
};
data.paymentMethod = 'Payment Method';
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
let newLeader = new User();
updatedGroup.leader = newLeader._id;
await updatedGroup.save();
await api.cancelSubscription(data);
updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.dateTerminated).to.exist;
});
it('cancels member subscriptions', async () => {
data = {
user,
sub: {
key: 'basic_3mo',
},
customerId: 'customer-id',
paymentMethod: 'Payment Method',
headers: {
'x-client': 'habitica-web',
'user-agent': '',
},
};
user.guilds.push(group._id);
await user.save();
expect(group.purchased.plan.planId).to.not.exist;
data.groupId = group._id;
await api.createSubscription(data);
await api.cancelSubscription(data);
let now = new Date();
now.setHours(0, 0, 0, 0);
let updatedLeader = await User.findById(user._id).exec();
let daysTillTermination = moment(updatedLeader.purchased.plan.dateTerminated).diff(now, 'days');
expect(daysTillTermination).to.be.within(2, 3); // only a few days
});
it('sends an email to members of group', async () => {
let recipient = new User();
recipient.profile.name = 'recipient';
recipient.guilds.push(group._id);
await recipient.save();
data.groupId = group._id;
await api.createSubscription(data);
await api.cancelSubscription(data);
expect(sender.sendTxn).to.be.have.callCount(4);
expect(sender.sendTxn.thirdCall.args[0]._id).to.equal(recipient._id);
expect(sender.sendTxn.thirdCall.args[1]).to.equal('group-member-cancel');
expect(sender.sendTxn.thirdCall.args[2]).to.eql([
{name: 'LEADER', content: user.profile.name},
{name: 'GROUP_NAME', content: group.name},
]);
});
it('does not cancel member subscriptions when member does not have a group plan sub (i.e. UNLIMITED_CUSTOMER_ID)', async () => {
plan.key = 'basic_earned';
plan.customerId = api.constants.UNLIMITED_CUSTOMER_ID;
let recipient = new User();
recipient.profile.name = 'recipient';
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
data.groupId = group._id;
await api.cancelSubscription(data);
let updatedLeader = await User.findById(user._id).exec();
expect(updatedLeader.purchased.plan.dateTerminated).to.not.exist;
});
it('does not cancel a user subscription if they are still in another active group plan', async () => {
let recipient = new User();
recipient.profile.name = 'recipient';
plan.key = 'basic_earned';
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
user.guilds.push(group._id);
await user.save();
data.groupId = group._id;
await api.createSubscription(data);
let updatedUser = await User.findById(recipient._id).exec();
let firstDateCreated = updatedUser.purchased.plan.dateCreated;
let extraMonthsBeforeSecond = updatedUser.purchased.plan.extraMonths;
let group2 = generateGroup({
name: 'test group2',
type: 'guild',
privacy: 'public',
leader: user._id,
});
data.groupId = group2._id;
await group2.save();
recipient.guilds.push(group2._id);
await recipient.save();
await api.createSubscription(data);
await api.cancelSubscription(data);
updatedUser = await User.findById(recipient._id).exec();
expect(updatedUser.purchased.plan.planId).to.eql('group_plan_auto');
expect(updatedUser.purchased.plan.customerId).to.eql('group-plan');
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan');
expect(updatedUser.purchased.plan.extraMonths).to.eql(extraMonthsBeforeSecond);
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
expect(updatedUser.purchased.plan.dateCreated).to.eql(firstDateCreated);
});
it('does cancel a leader subscription with two cancelled group plans', async () => {
user.guilds.push(group._id);
await user.save();
data.groupId = group._id;
await api.createSubscription(data);
let updatedUser = await User.findById(user._id).exec();
let firstDateCreated = updatedUser.purchased.plan.dateCreated;
let extraMonthsBeforeSecond = updatedUser.purchased.plan.extraMonths;
let group2 = generateGroup({
name: 'test group2',
type: 'guild',
privacy: 'public',
leader: user._id,
});
user.guilds.push(group2._id);
await user.save();
data.groupId = group2._id;
await group2.save();
await api.createSubscription(data);
await api.cancelSubscription(data);
data.groupId = group._id;
await api.cancelSubscription(data);
updatedUser = await User.findById(user._id).exec();
expect(updatedUser.purchased.plan.planId).to.eql('group_plan_auto');
expect(updatedUser.purchased.plan.customerId).to.eql('group-plan');
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan');
expect(updatedUser.purchased.plan.extraMonths).to.eql(extraMonthsBeforeSecond);
expect(updatedUser.purchased.plan.dateTerminated).to.exist;
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
expect(updatedUser.purchased.plan.dateCreated).to.eql(firstDateCreated);
});
});
@@ -1,693 +0,0 @@
import moment from 'moment';
import stripeModule from 'stripe';
import * as sender from '../../../../../../../website/server/libs/email';
import * as api from '../../../../../../../website/server/libs/payments';
import amzLib from '../../../../../../../website/server/libs/amazonPayments';
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
import { model as User } from '../../../../../../../website/server/models/user';
import { model as Group } from '../../../../../../../website/server/models/group';
import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
describe('Purchasing a subscription for group', () => {
let plan, group, user, data;
let stripe = stripeModule('test');
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
await user.save();
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
await group.save();
data = {
user,
sub: {
key: 'basic_3mo',
},
customerId: 'customer-id',
paymentMethod: 'Payment Method',
headers: {
'x-client': 'habitica-web',
'user-agent': '',
},
};
plan = {
planId: 'basic_3mo',
customerId: 'customer-id',
dateUpdated: new Date(),
gemsBought: 0,
paymentMethod: 'paymentMethod',
extraMonths: 0,
dateTerminated: null,
lastBillingDate: new Date(),
dateCreated: new Date(),
mysteryItems: [],
consecutive: {
trinkets: 0,
offset: 0,
gemCapExtra: 0,
},
};
let subscriptionId = 'subId';
sinon.stub(stripe.customers, 'del').returnsPromise().resolves({});
let currentPeriodEndTimeStamp = moment().add(3, 'months').unix();
sinon.stub(stripe.customers, 'retrieve')
.returnsPromise().resolves({
subscriptions: {
data: [{id: subscriptionId, current_period_end: currentPeriodEndTimeStamp}], // eslint-disable-line camelcase
},
});
stripePayments.setStripeApi(stripe);
sandbox.stub(sender, 'sendTxn');
});
afterEach(() => {
stripe.customers.del.restore();
stripe.customers.retrieve.restore();
sender.sendTxn.restore();
});
it('creates a subscription', async () => {
expect(group.purchased.plan.planId).to.not.exist;
data.groupId = group._id;
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.planId).to.eql('basic_3mo');
expect(updatedGroup.purchased.plan.customerId).to.eql('customer-id');
expect(updatedGroup.purchased.plan.dateUpdated).to.exist;
expect(updatedGroup.purchased.plan.gemsBought).to.eql(0);
expect(updatedGroup.purchased.plan.paymentMethod).to.eql('Payment Method');
expect(updatedGroup.purchased.plan.extraMonths).to.eql(0);
expect(updatedGroup.purchased.plan.dateTerminated).to.eql(null);
expect(updatedGroup.purchased.plan.lastBillingDate).to.not.exist;
expect(updatedGroup.purchased.plan.dateCreated).to.exist;
});
it('sends an email', async () => {
expect(group.purchased.plan.planId).to.not.exist;
data.groupId = group._id;
await api.createSubscription(data);
expect(sender.sendTxn).to.be.calledWith(user, 'group-subscription-begins');
});
it('sets extraMonths if plan has dateTerminated date', async () => {
group.purchased.plan = plan;
group.purchased.plan.dateTerminated = moment(new Date()).add(2, 'months');
await group.save();
expect(group.purchased.plan.extraMonths).to.eql(0);
data.groupId = group._id;
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.extraMonths).to.within(1.9, 2);
});
it('does not set negative extraMonths if plan has past dateTerminated date', async () => {
group.purchased.plan = plan;
group.purchased.plan.dateTerminated = moment(new Date()).subtract(2, 'months');
await group.save();
expect(group.purchased.plan.extraMonths).to.eql(0);
data.groupId = group._id;
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.extraMonths).to.eql(0);
});
it('grants all members of a group a subscription', async () => {
user.guilds.push(group._id);
await user.save();
expect(group.purchased.plan.planId).to.not.exist;
data.groupId = group._id;
await api.createSubscription(data);
let updatedLeader = await User.findById(user._id).exec();
expect(updatedLeader.purchased.plan.planId).to.eql('group_plan_auto');
expect(updatedLeader.purchased.plan.customerId).to.eql('group-plan');
expect(updatedLeader.purchased.plan.dateUpdated).to.exist;
expect(updatedLeader.purchased.plan.gemsBought).to.eql(0);
expect(updatedLeader.purchased.plan.paymentMethod).to.eql('Group Plan');
expect(updatedLeader.purchased.plan.extraMonths).to.eql(0);
expect(updatedLeader.purchased.plan.dateTerminated).to.eql(null);
expect(updatedLeader.purchased.plan.lastBillingDate).to.not.exist;
expect(updatedLeader.purchased.plan.dateCreated).to.exist;
expect(updatedLeader.items.mounts['Jackalope-RoyalPurple']).to.be.true;
});
it('sends an email to members of group', async () => {
let recipient = new User();
recipient.profile.name = 'recipient';
recipient.guilds.push(group._id);
await recipient.save();
data.groupId = group._id;
await api.createSubscription(data);
expect(sender.sendTxn).to.be.calledTwice;
expect(sender.sendTxn.firstCall.args[0]._id).to.equal(recipient._id);
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-joining');
expect(sender.sendTxn.firstCall.args[2]).to.eql([
{name: 'LEADER', content: user.profile.name},
{name: 'GROUP_NAME', content: group.name},
]);
});
it('adds months to members with existing gift subscription', async () => {
let recipient = new User();
recipient.profile.name = 'recipient';
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
plan.planId = 'basic_earned';
plan.paymentMethod = 'paymentMethod';
data.gift = {
member: recipient,
subscription: {
key: 'basic_earned',
months: 1,
},
};
await api.createSubscription(data);
await recipient.save();
data.gift = undefined;
user.guilds.push(group._id);
await user.save();
data.groupId = group._id;
await api.createSubscription(data);
let updatedUser = await User.findById(recipient._id).exec();
expect(updatedUser.purchased.plan.planId).to.eql('group_plan_auto');
expect(updatedUser.purchased.plan.customerId).to.eql('group-plan');
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan');
expect(updatedUser.purchased.plan.extraMonths).to.within(1, 3);
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
expect(updatedUser.purchased.plan.dateCreated).to.exist;
});
it('adds months to members with existing multi-month gift subscription', async () => {
let recipient = new User();
recipient.profile.name = 'recipient';
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
data.gift = {
member: recipient,
subscription: {
key: 'basic_3mo',
months: 3,
},
};
await api.createSubscription(data);
await recipient.save();
data.gift = undefined;
user.guilds.push(group._id);
await user.save();
data.groupId = group._id;
await api.createSubscription(data);
let updatedUser = await User.findById(recipient._id).exec();
expect(updatedUser.purchased.plan.planId).to.eql('group_plan_auto');
expect(updatedUser.purchased.plan.customerId).to.eql('group-plan');
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan');
expect(updatedUser.purchased.plan.extraMonths).to.within(3, 5);
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
expect(updatedUser.purchased.plan.dateCreated).to.exist;
});
it('adds months to members with existing recurring subscription (Stripe)', async () => {
let recipient = new User();
recipient.profile.name = 'recipient';
plan.key = 'basic_earned';
plan.paymentMethod = stripePayments.constants.PAYMENT_METHOD;
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
user.guilds.push(group._id);
await user.save();
data.groupId = group._id;
await api.createSubscription(data);
let updatedUser = await User.findById(recipient._id).exec();
expect(updatedUser.purchased.plan.extraMonths).to.within(2, 3);
});
it('adds months to members with existing recurring subscription (Amazon)', async () => {
sinon.stub(amzLib, 'getBillingAgreementDetails')
.returnsPromise()
.resolves({
BillingAgreementDetails: {
BillingAgreementStatus: {State: 'Closed'},
},
});
let recipient = new User();
recipient.profile.name = 'recipient';
plan.planId = 'basic_earned';
plan.paymentMethod = amzLib.constants.PAYMENT_METHOD;
plan.lastBillingDate = moment().add(3, 'months');
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
user.guilds.push(group._id);
await user.save();
data.groupId = group._id;
await api.createSubscription(data);
let updatedUser = await User.findById(recipient._id).exec();
expect(updatedUser.purchased.plan.extraMonths).to.within(3, 4);
});
it('adds months to members with existing recurring subscription (Paypal)', async () => {
sinon.stub(paypalPayments, 'paypalBillingAgreementCancel').returnsPromise().resolves({});
sinon.stub(paypalPayments, 'paypalBillingAgreementGet')
.returnsPromise().resolves({
agreement_details: { // eslint-disable-line camelcase
next_billing_date: moment().add(3, 'months').toDate(), // eslint-disable-line camelcase
cycles_completed: 1, // eslint-disable-line camelcase
},
});
let recipient = new User();
recipient.profile.name = 'recipient';
plan.planId = 'basic_earned';
plan.paymentMethod = paypalPayments.constants.PAYMENT_METHOD;
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
user.guilds.push(group._id);
await user.save();
data.groupId = group._id;
await api.createSubscription(data);
let updatedUser = await User.findById(recipient._id).exec();
expect(updatedUser.purchased.plan.extraMonths).to.within(2, 3);
paypalPayments.paypalBillingAgreementGet.restore();
paypalPayments.paypalBillingAgreementCancel.restore();
});
it('adds months to members with existing recurring subscription (Android)');
it('adds months to members with existing recurring subscription (iOs)');
it('adds months to members who already cancelled but not yet terminated recurring subscription', async () => {
let recipient = new User();
recipient.profile.name = 'recipient';
plan.key = 'basic_earned';
plan.paymentMethod = stripePayments.constants.PAYMENT_METHOD;
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
user.guilds.push(group._id);
await user.save();
data.groupId = group._id;
await recipient.cancelSubscription();
await api.createSubscription(data);
let updatedUser = await User.findById(recipient._id).exec();
expect(updatedUser.purchased.plan.extraMonths).to.within(2, 3);
});
it('adds months to members who already cancelled but not yet terminated group plan subscription', async () => {
let recipient = new User();
recipient.profile.name = 'recipient';
plan.key = 'basic_earned';
plan.paymentMethod = api.constants.GROUP_PLAN_PAYMENT_METHOD;
plan.extraMonths = 2.94;
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
user.guilds.push(group._id);
await user.save();
data.groupId = group._id;
await recipient.cancelSubscription();
await api.createSubscription(data);
let updatedUser = await User.findById(recipient._id).exec();
expect(updatedUser.purchased.plan.extraMonths).to.within(3, 4);
});
it('resets date terminated if user has old subscription', async () => {
let recipient = new User();
recipient.profile.name = 'recipient';
plan.key = 'basic_earned';
plan.paymentMethod = stripePayments.constants.PAYMENT_METHOD;
plan.dateTerminated = moment().subtract(1, 'days').toDate();
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
user.guilds.push(group._id);
await user.save();
data.groupId = group._id;
await api.createSubscription(data);
let updatedUser = await User.findById(recipient._id).exec();
expect(updatedUser.purchased.plan.dateTerminated).to.not.exist;
});
it('adds months to members with existing recurring subscription and includes existing extraMonths', async () => {
let recipient = new User();
recipient.profile.name = 'recipient';
plan.key = 'basic_earned';
plan.paymentMethod = stripePayments.constants.PAYMENT_METHOD;
plan.extraMonths = 5;
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
user.guilds.push(group._id);
await user.save();
data.groupId = group._id;
await api.createSubscription(data);
let updatedUser = await User.findById(recipient._id).exec();
expect(updatedUser.purchased.plan.extraMonths).to.within(7, 8);
});
it('adds months to members with existing recurring subscription and ignores existing negative extraMonths', async () => {
let recipient = new User();
recipient.profile.name = 'recipient';
plan.key = 'basic_earned';
plan.paymentMethod = stripePayments.constants.PAYMENT_METHOD;
plan.extraMonths = -5;
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
user.guilds.push(group._id);
await user.save();
data.groupId = group._id;
await api.createSubscription(data);
let updatedUser = await User.findById(recipient._id).exec();
expect(updatedUser.purchased.plan.extraMonths).to.within(2, 3);
});
it('does not override gemsBought, mysteryItems, dateCreated, and consective fields', async () => {
let planCreatedDate = moment().toDate();
let mysteryItem = {title: 'item'};
let mysteryItems = [mysteryItem];
let consecutive = {
trinkets: 3,
gemCapExtra: 20,
offset: 1,
count: 13,
};
let recipient = new User();
recipient.profile.name = 'recipient';
plan.key = 'basic_earned';
plan.gemsBought = 3;
plan.dateCreated = planCreatedDate;
plan.mysteryItems = mysteryItems;
plan.consecutive = consecutive;
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
user.guilds.push(group._id);
await user.save();
data.groupId = group._id;
await api.createSubscription(data);
let updatedUser = await User.findById(recipient._id).exec();
expect(updatedUser.purchased.plan.gemsBought).to.equal(3);
expect(updatedUser.purchased.plan.mysteryItems[0]).to.eql(mysteryItem);
expect(updatedUser.purchased.plan.consecutive.count).to.equal(consecutive.count);
expect(updatedUser.purchased.plan.consecutive.offset).to.equal(consecutive.offset);
expect(updatedUser.purchased.plan.consecutive.gemCapExtra).to.equal(consecutive.gemCapExtra);
expect(updatedUser.purchased.plan.consecutive.trinkets).to.equal(consecutive.trinkets);
expect(updatedUser.purchased.plan.dateCreated).to.eql(planCreatedDate);
});
it('does not modify a user with a group subscription when they join another group', async () => {
let recipient = new User();
recipient.profile.name = 'recipient';
plan.key = 'basic_earned';
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
user.guilds.push(group._id);
await user.save();
data.groupId = group._id;
await api.createSubscription(data);
let updatedUser = await User.findById(recipient._id).exec();
let firstDateCreated = updatedUser.purchased.plan.dateCreated;
let extraMonthsBeforeSecond = updatedUser.purchased.plan.extraMonths;
let group2 = generateGroup({
name: 'test group2',
type: 'guild',
privacy: 'public',
leader: user._id,
});
data.groupId = group2._id;
await group2.save();
recipient.guilds.push(group2._id);
await recipient.save();
await api.createSubscription(data);
updatedUser = await User.findById(recipient._id).exec();
expect(updatedUser.purchased.plan.planId).to.eql('group_plan_auto');
expect(updatedUser.purchased.plan.customerId).to.eql('group-plan');
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan');
expect(updatedUser.purchased.plan.extraMonths).to.eql(extraMonthsBeforeSecond);
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
expect(updatedUser.purchased.plan.dateCreated).to.eql(firstDateCreated);
});
it('does not remove a user who is in two groups plans and leaves one', async () => {
let recipient = new User();
recipient.profile.name = 'recipient';
plan.key = 'basic_earned';
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
user.guilds.push(group._id);
await user.save();
data.groupId = group._id;
await api.createSubscription(data);
let updatedUser = await User.findById(recipient._id).exec();
let firstDateCreated = updatedUser.purchased.plan.dateCreated;
let extraMonthsBeforeSecond = updatedUser.purchased.plan.extraMonths;
let group2 = generateGroup({
name: 'test group2',
type: 'guild',
privacy: 'public',
leader: user._id,
});
data.groupId = group2._id;
await group2.save();
recipient.guilds.push(group2._id);
await recipient.save();
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
await updatedGroup.leave(recipient);
updatedUser = await User.findById(recipient._id).exec();
expect(updatedUser.purchased.plan.planId).to.eql('group_plan_auto');
expect(updatedUser.purchased.plan.customerId).to.eql('group-plan');
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan');
expect(updatedUser.purchased.plan.extraMonths).to.eql(extraMonthsBeforeSecond);
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
expect(updatedUser.purchased.plan.dateCreated).to.eql(firstDateCreated);
});
it('does not modify a user with an unlimited subscription', async () => {
plan.key = 'basic_earned';
plan.customerId = api.constants.UNLIMITED_CUSTOMER_ID;
let recipient = new User();
recipient.profile.name = 'recipient';
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
user.guilds.push(group._id);
await user.save();
data.groupId = group._id;
await api.createSubscription(data);
let updatedUser = await User.findById(recipient._id).exec();
expect(updatedUser.purchased.plan.planId).to.eql('basic_3mo');
expect(updatedUser.purchased.plan.customerId).to.eql(api.constants.UNLIMITED_CUSTOMER_ID);
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
expect(updatedUser.purchased.plan.paymentMethod).to.eql('paymentMethod');
expect(updatedUser.purchased.plan.extraMonths).to.eql(0);
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
expect(updatedUser.purchased.plan.lastBillingDate).to.exist;
expect(updatedUser.purchased.plan.dateCreated).to.exist;
});
it('does not modify a user with a Google subscription', async () => {
plan.customerId = 'random';
plan.paymentMethod = api.constants.GOOGLE_PAYMENT_METHOD;
let recipient = new User();
recipient.profile.name = 'recipient';
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
user.guilds.push(group._id);
await user.save();
data.groupId = group._id;
await api.createSubscription(data);
let updatedUser = await User.findById(recipient._id).exec();
expect(updatedUser.purchased.plan.planId).to.eql('basic_3mo');
expect(updatedUser.purchased.plan.customerId).to.eql('random');
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
expect(updatedUser.purchased.plan.paymentMethod).to.eql(api.constants.GOOGLE_PAYMENT_METHOD);
expect(updatedUser.purchased.plan.extraMonths).to.eql(0);
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
expect(updatedUser.purchased.plan.lastBillingDate).to.exist;
expect(updatedUser.purchased.plan.dateCreated).to.exist;
});
it('does not modify a user with an iOS subscription', async () => {
plan.customerId = 'random';
plan.paymentMethod = api.constants.IOS_PAYMENT_METHOD;
let recipient = new User();
recipient.profile.name = 'recipient';
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
user.guilds.push(group._id);
await user.save();
data.groupId = group._id;
await api.createSubscription(data);
let updatedUser = await User.findById(recipient._id).exec();
expect(updatedUser.purchased.plan.planId).to.eql('basic_3mo');
expect(updatedUser.purchased.plan.customerId).to.eql('random');
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
expect(updatedUser.purchased.plan.paymentMethod).to.eql(api.constants.IOS_PAYMENT_METHOD);
expect(updatedUser.purchased.plan.extraMonths).to.eql(0);
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
expect(updatedUser.purchased.plan.lastBillingDate).to.exist;
expect(updatedUser.purchased.plan.dateCreated).to.exist;
});
it('updates a user with a cancelled but active group subscription', async () => {
plan.key = 'basic_earned';
plan.customerId = api.constants.GROUP_PLAN_CUSTOMER_ID;
plan.dateTerminated = moment().add(1, 'months');
let recipient = new User();
recipient.profile.name = 'recipient';
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
user.guilds.push(group._id);
await user.save();
data.groupId = group._id;
await api.createSubscription(data);
let updatedUser = await User.findById(recipient._id).exec();
expect(updatedUser.purchased.plan.planId).to.eql('group_plan_auto');
expect(updatedUser.purchased.plan.customerId).to.eql(api.constants.GROUP_PLAN_CUSTOMER_ID);
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan');
expect(updatedUser.purchased.plan.extraMonths).to.within(0, 2);
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
expect(updatedUser.purchased.plan.dateCreated).to.exist;
});
});
@@ -5,14 +5,10 @@ import {
generateGroup,
} from '../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../website/server/models/user';
import { model as Group } from '../../../../../website/server/models/group';
import { model as Coupon } from '../../../../../website/server/models/coupon';
import stripePayments from '../../../../../website/server/libs/stripePayments';
import payments from '../../../../../website/server/libs/payments';
import common from '../../../../../website/common';
import logger from '../../../../../website/server/libs/logger';
import { v4 as uuid } from 'uuid';
import moment from 'moment';
const i18n = common.i18n;
@@ -398,50 +394,6 @@ describe('Stripe Payments', () => {
subscriptionId,
});
});
it('subscribes a group with the correct number of group members', async () => {
token = 'test-token';
sub = data.sub;
groupId = group._id;
email = 'test@test.com';
headers = {};
user = new User();
user.guilds.push(groupId);
await user.save();
group.memberCount = 2;
await group.save();
await stripePayments.checkout({
token,
user,
gift,
sub,
groupId,
email,
headers,
coupon,
}, stripe);
expect(stripeCreateCustomerSpy).to.be.calledOnce;
expect(stripeCreateCustomerSpy).to.be.calledWith({
email,
metadata: { uuid: user._id },
card: token,
plan: sub.key,
quantity: 4,
});
expect(stripePaymentsCreateSubSpy).to.be.calledOnce;
expect(stripePaymentsCreateSubSpy).to.be.calledWith({
user,
customerId: customerIdResponse,
paymentMethod: 'Stripe',
sub,
headers,
groupId,
subscriptionId,
});
});
});
describe('edit subscription', () => {
@@ -706,301 +658,4 @@ describe('Stripe Payments', () => {
});
});
});
describe('#upgradeGroupPlan', () => {
let spy, data, user, group;
beforeEach(async function () {
user = new User();
user.profile.name = 'sender';
data = {
user,
sub: {
key: 'basic_3mo', // @TODO: Validate that this is group
},
customerId: 'customer-id',
paymentMethod: 'Payment Method',
headers: {
'x-client': 'habitica-web',
'user-agent': '',
},
};
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
await group.save();
spy = sinon.stub(stripe.subscriptions, 'update');
spy.returnsPromise().resolves([]);
data.groupId = group._id;
data.sub.quantity = 3;
stripePayments.setStripeApi(stripe);
});
afterEach(function () {
sinon.restore(stripe.subscriptions.update);
});
it('updates a group plan quantity', async () => {
data.paymentMethod = 'Stripe';
await payments.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.quantity).to.eql(3);
updatedGroup.memberCount += 1;
await updatedGroup.save();
await stripePayments.chargeForAdditionalGroupMember(updatedGroup);
expect(spy.calledOnce).to.be.true;
expect(updatedGroup.purchased.plan.quantity).to.eql(4);
});
});
describe('handleWebhooks', () => {
describe('all events', () => {
const eventType = 'account.updated';
const event = {id: 123};
const eventRetrieved = {type: eventType};
beforeEach(() => {
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves(eventRetrieved);
sinon.stub(logger, 'error');
});
afterEach(() => {
stripe.events.retrieve.restore();
logger.error.restore();
});
it('logs an error if an unsupported webhook event is passed', async () => {
const error = new Error(`Missing handler for Stripe webhook ${eventType}`);
await stripePayments.handleWebhooks({requestBody: event}, stripe);
expect(logger.error).to.have.been.called.once;
expect(logger.error).to.have.been.calledWith(error, {event: eventRetrieved});
});
it('retrieves and validates the event from Stripe', async () => {
await stripePayments.handleWebhooks({requestBody: event}, stripe);
expect(stripe.events.retrieve).to.have.been.called.once;
expect(stripe.events.retrieve).to.have.been.calledWith(event.id);
});
});
describe('customer.subscription.deleted', () => {
const eventType = 'customer.subscription.deleted';
beforeEach(() => {
sinon.stub(stripe.customers, 'del').returnsPromise().resolves({});
sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
});
afterEach(() => {
stripe.customers.del.restore();
payments.cancelSubscription.restore();
});
it('does not do anything if event.request is null (subscription cancelled manually)', async () => {
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
id: 123,
type: eventType,
request: 123,
});
await stripePayments.handleWebhooks({requestBody: {}}, stripe);
expect(stripe.events.retrieve).to.have.been.called.once;
expect(stripe.customers.del).to.not.have.been.called;
expect(payments.cancelSubscription).to.not.have.been.called;
stripe.events.retrieve.restore();
});
describe('user subscription', () => {
it('throws an error if the user is not found', async () => {
const customerId = 456;
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
id: 123,
type: eventType,
data: {
object: {
plan: {
id: 'basic_earned',
},
customer: customerId,
},
},
request: null,
});
await expect(stripePayments.handleWebhooks({requestBody: {}}, stripe)).to.eventually.be.rejectedWith({
message: i18n.t('userNotFound'),
httpCode: 404,
name: 'NotFound',
});
expect(stripe.customers.del).to.not.have.been.called;
expect(payments.cancelSubscription).to.not.have.been.called;
stripe.events.retrieve.restore();
});
it('deletes the customer on Stripe and calls payments.cancelSubscription', async () => {
const customerId = '456';
let subscriber = new User();
subscriber.purchased.plan.customerId = customerId;
subscriber.purchased.plan.paymentMethod = 'Stripe';
await subscriber.save();
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
id: 123,
type: eventType,
data: {
object: {
plan: {
id: 'basic_earned',
},
customer: customerId,
},
},
request: null,
});
await stripePayments.handleWebhooks({requestBody: {}}, stripe);
expect(stripe.customers.del).to.have.been.calledOnce;
expect(stripe.customers.del).to.have.been.calledWith(customerId);
expect(payments.cancelSubscription).to.have.been.calledOnce;
let cancelSubscriptionOpts = payments.cancelSubscription.lastCall.args[0];
expect(cancelSubscriptionOpts.user._id).to.equal(subscriber._id);
expect(cancelSubscriptionOpts.paymentMethod).to.equal('Stripe');
expect(Math.round(moment(cancelSubscriptionOpts.nextBill).diff(new Date(), 'days', true))).to.equal(3);
expect(cancelSubscriptionOpts.groupId).to.be.undefined;
stripe.events.retrieve.restore();
});
});
describe('group plan subscription', () => {
it('throws an error if the group is not found', async () => {
const customerId = 456;
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
id: 123,
type: eventType,
data: {
object: {
plan: {
id: 'group_monthly',
},
customer: customerId,
},
},
request: null,
});
await expect(stripePayments.handleWebhooks({requestBody: {}}, stripe)).to.eventually.be.rejectedWith({
message: i18n.t('groupNotFound'),
httpCode: 404,
name: 'NotFound',
});
expect(stripe.customers.del).to.not.have.been.called;
expect(payments.cancelSubscription).to.not.have.been.called;
stripe.events.retrieve.restore();
});
it('throws an error if the group leader is not found', async () => {
const customerId = 456;
let subscriber = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: uuid(),
});
subscriber.purchased.plan.customerId = customerId;
subscriber.purchased.plan.paymentMethod = 'Stripe';
await subscriber.save();
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
id: 123,
type: eventType,
data: {
object: {
plan: {
id: 'group_monthly',
},
customer: customerId,
},
},
request: null,
});
await expect(stripePayments.handleWebhooks({requestBody: {}}, stripe)).to.eventually.be.rejectedWith({
message: i18n.t('userNotFound'),
httpCode: 404,
name: 'NotFound',
});
expect(stripe.customers.del).to.not.have.been.called;
expect(payments.cancelSubscription).to.not.have.been.called;
stripe.events.retrieve.restore();
});
it('deletes the customer on Stripe and calls payments.cancelSubscription', async () => {
const customerId = '456';
let leader = new User();
await leader.save();
let subscriber = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: leader._id,
});
subscriber.purchased.plan.customerId = customerId;
subscriber.purchased.plan.paymentMethod = 'Stripe';
await subscriber.save();
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
id: 123,
type: eventType,
data: {
object: {
plan: {
id: 'group_monthly',
},
customer: customerId,
},
},
request: null,
});
await stripePayments.handleWebhooks({requestBody: {}}, stripe);
expect(stripe.customers.del).to.have.been.calledOnce;
expect(stripe.customers.del).to.have.been.calledWith(customerId);
expect(payments.cancelSubscription).to.have.been.calledOnce;
let cancelSubscriptionOpts = payments.cancelSubscription.lastCall.args[0];
expect(cancelSubscriptionOpts.user._id).to.equal(leader._id);
expect(cancelSubscriptionOpts.paymentMethod).to.equal('Stripe');
expect(Math.round(moment(cancelSubscriptionOpts.nextBill).diff(new Date(), 'days', true))).to.equal(3);
expect(cancelSubscriptionOpts.groupId).to.equal(subscriber._id);
stripe.events.retrieve.restore();
});
});
});
});
});
+47 -77
View File
@@ -4,6 +4,7 @@ import {
generateTodo,
generateDaily,
} from '../../../../helpers/api-unit.helper';
import { cloneDeep } from 'lodash';
import cronMiddleware from '../../../../../website/server/middlewares/cron';
import moment from 'moment';
import { model as User } from '../../../../../website/server/models/user';
@@ -59,7 +60,7 @@ describe('cron middleware', () => {
cronMiddleware(req, res, done);
});
it('should clear todos older than 30 days for free users', async () => {
it('should clear todos older than 30 days for free users', async (done) => {
user.lastCron = moment(new Date()).subtract({days: 2});
let task = generateTodo(user);
task.dateCompleted = moment(new Date()).subtract({days: 31});
@@ -67,21 +68,16 @@ describe('cron middleware', () => {
await task.save();
await user.save();
await new Promise((resolve, reject) => {
cronMiddleware(req, res, (err) => {
if (err) return reject(err);
Tasks.Task.findOne({_id: task}, function (secondErr, taskFound) {
if (secondErr) return reject(err);
expect(secondErr).to.not.exist;
expect(taskFound).to.not.exist;
resolve();
});
cronMiddleware(req, res, (err) => {
Tasks.Task.findOne({_id: task}, function (secondErr, taskFound) {
expect(secondErr).to.not.exist;
expect(taskFound).to.not.exist;
done(err);
});
});
});
it('should not clear todos older than 30 days for subscribed users', async () => {
it('should not clear todos older than 30 days for subscribed users', async (done) => {
user.purchased.plan.customerId = 'subscribedId';
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
user.lastCron = moment(new Date()).subtract({days: 2});
@@ -91,20 +87,16 @@ describe('cron middleware', () => {
await task.save();
await user.save();
await new Promise((resolve, reject) => {
cronMiddleware(req, res, (err) => {
if (err) return reject(err);
Tasks.Task.findOne({_id: task}, function (secondErr, taskFound) {
if (secondErr) return reject(secondErr);
expect(secondErr).to.not.exist;
expect(taskFound).to.exist;
resolve();
});
cronMiddleware(req, res, (err) => {
Tasks.Task.findOne({_id: task}, function (secondErr, taskFound) {
expect(secondErr).to.not.exist;
expect(taskFound).to.exist;
done(err);
});
});
});
it('should clear todos older than 90 days for subscribed users', async () => {
it('should clear todos older than 90 days for subscribed users', async (done) => {
user.purchased.plan.customerId = 'subscribedId';
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
user.lastCron = moment(new Date()).subtract({days: 2});
@@ -115,49 +107,39 @@ describe('cron middleware', () => {
await task.save();
await user.save();
await new Promise((resolve, reject) => {
cronMiddleware(req, res, (err) => {
if (err) return reject(err);
Tasks.Task.findOne({_id: task}, function (secondErr, taskFound) {
if (secondErr) return reject(secondErr);
expect(secondErr).to.not.exist;
expect(taskFound).to.not.exist;
resolve();
});
cronMiddleware(req, res, (err) => {
Tasks.Task.findOne({_id: task}, function (secondErr, taskFound) {
expect(secondErr).to.not.exist;
expect(taskFound).to.not.exist;
done(err);
});
});
});
it('should call next if user was not modified after cron', async () => {
it('should call next if user was not modified after cron', async (done) => {
let hpBefore = user.stats.hp;
user.lastCron = moment(new Date()).subtract({days: 2});
await user.save();
await new Promise((resolve, reject) => {
cronMiddleware(req, res, (err) => {
if (err) return reject(err);
expect(hpBefore).to.equal(user.stats.hp);
resolve();
});
cronMiddleware(req, res, (err) => {
expect(hpBefore).to.equal(user.stats.hp);
done(err);
});
});
it('updates user.auth.timestamps.loggedin and lastCron', async () => {
it('updates user.auth.timestamps.loggedin and lastCron', async (done) => {
user.lastCron = moment(new Date()).subtract({days: 2});
let now = new Date();
await user.save();
await new Promise((resolve, reject) => {
cronMiddleware(req, res, (err) => {
if (err) return reject(err);
expect(moment(now).isSame(user.lastCron, 'day'));
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
resolve();
});
cronMiddleware(req, res, (err) => {
expect(moment(now).isSame(user.lastCron, 'day'));
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
done(err);
});
});
it('does damage for missing dailies', async () => {
it('does damage for missing dailies', async (done) => {
let hpBefore = user.stats.hp;
user.lastCron = moment(new Date()).subtract({days: 2});
let daily = generateDaily(user);
@@ -165,34 +147,28 @@ describe('cron middleware', () => {
await daily.save();
await user.save();
await new Promise((resolve, reject) => {
cronMiddleware(req, res, (err) => {
if (err) return reject(err);
expect(user.stats.hp).to.be.lessThan(hpBefore);
resolve();
});
cronMiddleware(req, res, (err) => {
expect(user.stats.hp).to.be.lessThan(hpBefore);
done(err);
});
});
it('updates tasks', async () => {
it('updates tasks', async (done) => {
user.lastCron = moment(new Date()).subtract({days: 2});
let todo = generateTodo(user);
let todoValueBefore = todo.value;
await user.save();
await new Promise((resolve, reject) => {
cronMiddleware(req, res, (err) => {
if (err) return reject(err);
Tasks.Task.findOne({_id: todo._id}, function (secondErr, todoFound) {
if (secondErr) return reject(secondErr);
expect(todoFound.value).to.be.lessThan(todoValueBefore);
resolve();
});
cronMiddleware(req, res, () => {
Tasks.Task.findOne({_id: todo._id}, function (err, todoFound) {
expect(err).to.not.exist;
expect(todoFound.value).to.be.lessThan(todoValueBefore);
done();
});
});
});
it('applies quest progress', async () => {
it('applies quest progress', async (done) => {
let hpBefore = user.stats.hp;
user.lastCron = moment(new Date()).subtract({days: 2});
let daily = generateDaily(user);
@@ -216,20 +192,17 @@ describe('cron middleware', () => {
party.startQuest(user);
await new Promise((resolve, reject) => {
cronMiddleware(req, res, (err) => {
if (err) return reject(err);
expect(user.stats.hp).to.be.lessThan(hpBefore);
resolve();
});
cronMiddleware(req, res, () => {
expect(user.stats.hp).to.be.lessThan(hpBefore);
done();
});
});
it('recovers from failed cron and does not error when user is already cronning', async () => {
it('recovers from failed cron and does not error when user is already cronning', async (done) => {
user.lastCron = moment(new Date()).subtract({days: 2});
await user.save();
let updatedUser = user.toObject();
let updatedUser = cloneDeep(user);
updatedUser.nMatched = 0;
sandbox.spy(cronLib, 'recoverCron');
@@ -242,13 +215,10 @@ describe('cron middleware', () => {
},
});
await new Promise((resolve, reject) => {
cronMiddleware(req, res, (err) => {
if (err) return reject(err);
expect(cronLib.recoverCron).to.be.calledOnce;
cronMiddleware(req, res, () => {
expect(cronLib.recoverCron).to.be.calledOnce;
resolve();
});
done();
});
});
});
-182
View File
@@ -1,182 +0,0 @@
import {
generateRes,
generateReq,
generateNext,
} from '../../../../helpers/api-unit.helper';
import nconf from 'nconf';
import requireAgain from 'require-again';
describe('redirects middleware', () => {
let res, req, next;
let pathToRedirectsMiddleware = '../../../../../website/server/middlewares/redirects';
beforeEach(() => {
res = generateRes();
req = generateReq();
next = generateNext();
});
context('forceSSL', () => {
it('sends http requests to https', () => {
let nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
nconfStub.withArgs('IS_PROD').returns(true);
req.header = sandbox.stub().withArgs('x-forwarded-proto').returns('http');
req.originalUrl = '/static/front';
let attachRedirects = requireAgain(pathToRedirectsMiddleware);
attachRedirects.forceSSL(req, res, next);
expect(res.redirect).to.be.calledOnce;
expect(res.redirect).to.be.calledWith('https://habitica.com/static/front');
});
it('does not redirect https forwarded requests', () => {
let nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
nconfStub.withArgs('IS_PROD').returns(true);
req.header = sandbox.stub().withArgs('x-forwarded-proto').returns('https');
req.originalUrl = '/static/front';
let attachRedirects = requireAgain(pathToRedirectsMiddleware);
attachRedirects.forceSSL(req, res, next);
expect(res.redirect).to.be.notCalled;
});
it('does not redirect outside of production environments', () => {
let nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
nconfStub.withArgs('IS_PROD').returns(false);
req.header = sandbox.stub().withArgs('x-forwarded-proto').returns('http');
req.originalUrl = '/static/front';
let attachRedirects = requireAgain(pathToRedirectsMiddleware);
attachRedirects.forceSSL(req, res, next);
expect(res.redirect).to.be.notCalled;
});
it('does not redirect if base URL is not https', () => {
let nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('BASE_URL').returns('http://habitica.com');
nconfStub.withArgs('IS_PROD').returns(true);
req.header = sandbox.stub().withArgs('x-forwarded-proto').returns('http');
req.originalUrl = '/static/front';
let attachRedirects = requireAgain(pathToRedirectsMiddleware);
attachRedirects.forceSSL(req, res, next);
expect(res.redirect).to.be.notCalled;
});
});
context('forceHabitica', () => {
it('sends requests with differing hostname to base URL host', () => {
let nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
nconfStub.withArgs('IGNORE_REDIRECT').returns('false');
nconfStub.withArgs('IS_PROD').returns(true);
req.hostname = 'www.habitica.com';
req.method = 'GET';
req.originalUrl = '/static/front';
req.url = '/static/front';
let attachRedirects = requireAgain(pathToRedirectsMiddleware);
attachRedirects.forceHabitica(req, res, next);
expect(res.redirect).to.be.calledOnce;
expect(res.redirect).to.be.calledWith(301, 'https://habitica.com/static/front');
});
it('does not redirect outside of production environments', () => {
let nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
nconfStub.withArgs('IGNORE_REDIRECT').returns('false');
nconfStub.withArgs('IS_PROD').returns(false);
req.hostname = 'www.habitica.com';
req.method = 'GET';
req.originalUrl = '/static/front';
req.url = '/static/front';
let attachRedirects = requireAgain(pathToRedirectsMiddleware);
attachRedirects.forceHabitica(req, res, next);
expect(res.redirect).to.be.notCalled;
});
it('does not redirect if env is set to ignore redirection', () => {
let nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
nconfStub.withArgs('IGNORE_REDIRECT').returns('true');
nconfStub.withArgs('IS_PROD').returns(true);
req.hostname = 'www.habitica.com';
req.method = 'GET';
req.originalUrl = '/static/front';
req.url = '/static/front';
let attachRedirects = requireAgain(pathToRedirectsMiddleware);
attachRedirects.forceHabitica(req, res, next);
expect(res.redirect).to.be.notCalled;
});
it('does not redirect if request hostname matches base URL host', () => {
let nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
nconfStub.withArgs('IGNORE_REDIRECT').returns('false');
nconfStub.withArgs('IS_PROD').returns(true);
req.hostname = 'habitica.com';
req.method = 'GET';
req.originalUrl = '/static/front';
req.url = '/static/front';
let attachRedirects = requireAgain(pathToRedirectsMiddleware);
attachRedirects.forceHabitica(req, res, next);
expect(res.redirect).to.be.notCalled;
});
it('does not redirect if request is an API URL', () => {
let nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
nconfStub.withArgs('IGNORE_REDIRECT').returns('false');
nconfStub.withArgs('IS_PROD').returns(true);
req.hostname = 'www.habitica.com';
req.method = 'GET';
req.originalUrl = '/api/v3/challenges';
req.url = '/api/v3/challenges';
let attachRedirects = requireAgain(pathToRedirectsMiddleware);
attachRedirects.forceHabitica(req, res, next);
expect(res.redirect).to.be.notCalled;
});
it('does not redirect if request method is not GET', () => {
let nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
nconfStub.withArgs('IGNORE_REDIRECT').returns('false');
nconfStub.withArgs('IS_PROD').returns(true);
req.hostname = 'www.habitica.com';
req.method = 'POST';
req.originalUrl = '/static/front';
req.url = '/static/front';
let attachRedirects = requireAgain(pathToRedirectsMiddleware);
attachRedirects.forceHabitica(req, res, next);
expect(res.redirect).to.be.notCalled;
});
});
});
-34
View File
@@ -104,40 +104,6 @@ describe('Challenge Model', () => {
expect(updatedNewMember.tags[7].id).to.equal(challenge._id);
expect(updatedNewMember.tags[7].name).to.equal(challenge.shortName);
expect(syncedTask).to.exist;
expect(syncedTask.attribute).to.eql('str');
});
it('syncs a challenge to a user with the existing task', async () => {
await challenge.addTasks([task]);
let updatedLeader = await User.findOne({_id: leader._id});
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
let syncedTask = find(updatedLeadersTasks, function findNewTask (updatedLeadersTask) {
return updatedLeadersTask.challenge.taskId === task._id;
});
let createdAtBefore = syncedTask.createdAt;
let attributeBefore = syncedTask.attribute;
let newTitle = 'newName';
task.text = newTitle;
task.attribute = 'int';
await task.save();
await challenge.syncToUser(leader);
updatedLeader = await User.findOne({_id: leader._id});
updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
syncedTask = find(updatedLeadersTasks, function findNewTask (updatedLeadersTask) {
return updatedLeadersTask.challenge.taskId === task._id;
});
let createdAtAfter = syncedTask.createdAt;
let attributeAfter = syncedTask.attribute;
expect(createdAtBefore).to.eql(createdAtAfter);
expect(attributeBefore).to.eql(attributeAfter);
expect(syncedTask.text).to.eql(newTitle);
});
it('updates tasks to challenge and challenge members', async () => {

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