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
1388 changed files with 56828 additions and 76419 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 -8
View File
@@ -16,18 +16,18 @@ 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"
+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:['back_mystery_201704','armor_mystery_201704']
$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) {
+1921 -1441
View File
File diff suppressed because it is too large Load Diff
+15 -18
View File
@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "3.90.2",
"version": "3.78.0",
"main": "./website/server/index.js",
"dependencies": {
"@slack/client": "^3.8.1",
@@ -28,7 +28,6 @@
"bcrypt": "^1.0.2",
"bluebird": "^3.3.5",
"body-parser": "^1.15.0",
"bootstrap": "^4.0.0-alpha.6",
"bower": "~1.3.12",
"browserify": "~12.0.1",
"compression": "^1.6.1",
@@ -41,7 +40,6 @@
"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",
@@ -56,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",
@@ -72,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",
@@ -103,7 +103,7 @@
"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",
@@ -117,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": "^2.0.0",
"vue-template-compiler": "^2.1.10",
"webpack": "^2.2.1",
"webpack-merge": "^2.6.1",
"winston": "^2.1.0",
"winston-loggly-bulk": "^1.4.2",
"xml2js": "^0.4.4"
},
"private": true,
@@ -140,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",
@@ -173,7 +171,7 @@
"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",
@@ -184,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,13 +191,12 @@
"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-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",
@@ -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;
});
});
@@ -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;
@@ -315,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,8 +504,6 @@ 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);
});
@@ -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(1);
expect(user.notifications[0].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(0);
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',
@@ -57,7 +56,6 @@ describe('POST /tasks/:id/score/:direction', () => {
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[0].data.groupId).to.equal(guild._id);
@@ -65,42 +63,6 @@ describe('POST /tasks/:id/score/:direction', () => {
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(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,
}));
expect(user.notifications[0].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 {
@@ -65,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,
@@ -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 () => {
@@ -255,23 +70,6 @@ describe('POST /user/auth/local/register', () => {
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`;
@@ -618,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);
});
});
});
@@ -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 -66
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);
});
@@ -483,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', () => {
@@ -596,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});
@@ -661,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 () => {
+101 -270
View File
@@ -1,19 +1,15 @@
import moment from 'moment';
import { v4 as generateUUID } from 'uuid';
import validator from 'validator';
import { sleep } from '../../../../helpers/api-unit.helper';
import {
SPAM_MESSAGE_LIMIT,
SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
SPAM_WINDOW_LENGTH,
INVITES_LIMIT,
model as Group,
} from '../../../../../website/server/models/group';
import { model as Group, INVITES_LIMIT } from '../../../../../website/server/models/group';
import { model as User } from '../../../../../website/server/models/user';
import {
BadRequest,
} from '../../../../../website/server/libs/errors';
import { quests as questScrolls } from '../../../../../website/common/script/content';
import { groupChatReceivedWebhook } from '../../../../../website/server/libs/webhook';
import * as email from '../../../../../website/server/libs/email';
import validator from 'validator';
import { TAVERN_ID } from '../../../../../website/common/script/';
import { v4 as generateUUID } from 'uuid';
import shared from '../../../../../website/common';
describe('Group Model', () => {
@@ -463,67 +459,73 @@ describe('Group Model', () => {
};
});
it('throws an error if no uuids or emails are passed in', async () => {
await expect(Group.validateInvitations(null, null, res)).to.eventually.be.rejected.and.eql({
httpCode: 400,
message: 'Bad request.',
name: 'BadRequest',
});
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('canOnlyInviteEmailUuid');
it('throws an error if no uuids or emails are passed in', (done) => {
try {
Group.validateInvitations(null, null, res);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('canOnlyInviteEmailUuid');
done();
}
});
it('throws an error if only uuids are passed in, but they are not an array', async () => {
await expect(Group.validateInvitations({ uuid: 'user-id'}, null, res)).to.eventually.be.rejected.and.eql({
httpCode: 400,
message: 'Bad request.',
name: 'BadRequest',
});
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('uuidsMustBeAnArray');
it('throws an error if only uuids are passed in, but they are not an array', (done) => {
try {
Group.validateInvitations({ uuid: 'user-id'}, null, res);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('uuidsMustBeAnArray');
done();
}
});
it('throws an error if only emails are passed in, but they are not an array', async () => {
await expect(Group.validateInvitations(null, { emails: 'user@example.com'}, res)).to.eventually.be.rejected.and.eql({
httpCode: 400,
message: 'Bad request.',
name: 'BadRequest',
});
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('emailsMustBeAnArray');
it('throws an error if only emails are passed in, but they are not an array', (done) => {
try {
Group.validateInvitations(null, { emails: 'user@example.com'}, res);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('emailsMustBeAnArray');
done();
}
});
it('throws an error if emails are not passed in, and uuid array is empty', async () => {
await expect(Group.validateInvitations([], null, res)).to.eventually.be.rejected.and.eql({
httpCode: 400,
message: 'Bad request.',
name: 'BadRequest',
});
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('inviteMissingUuid');
it('throws an error if emails are not passed in, and uuid array is empty', (done) => {
try {
Group.validateInvitations([], null, res);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('inviteMissingUuid');
done();
}
});
it('throws an error if uuids are not passed in, and email array is empty', async () => {
await expect(Group.validateInvitations(null, [], res)).to.eventually.be.rejected.and.eql({
httpCode: 400,
message: 'Bad request.',
name: 'BadRequest',
});
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('inviteMissingEmail');
it('throws an error if uuids are not passed in, and email array is empty', (done) => {
try {
Group.validateInvitations(null, [], res);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('inviteMissingEmail');
done();
}
});
it('throws an error if uuids and emails are passed in as empty arrays', async () => {
await expect(Group.validateInvitations([], [], res)).to.eventually.be.rejected.and.eql({
httpCode: 400,
message: 'Bad request.',
name: 'BadRequest',
});
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('inviteMustNotBeEmpty');
it('throws an error if uuids and emails are passed in as empty arrays', (done) => {
try {
Group.validateInvitations([], [], res);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('inviteMustNotBeEmpty');
done();
}
});
it('throws an error if total invites exceed max invite constant', async () => {
it('throws an error if total invites exceed max invite constant', (done) => {
let uuids = [];
let emails = [];
@@ -534,16 +536,17 @@ describe('Group Model', () => {
uuids.push('one-more-uuid'); // to put it over the limit
await expect(Group.validateInvitations(uuids, emails, res)).to.eventually.be.rejected.and.eql({
httpCode: 400,
message: 'Bad request.',
name: 'BadRequest',
});
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('canOnlyInviteMaxInvites', {maxInvites: INVITES_LIMIT });
try {
Group.validateInvitations(uuids, emails, res);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('canOnlyInviteMaxInvites', {maxInvites: INVITES_LIMIT });
done();
}
});
it('does not throw error if number of invites matches max invite limit', async () => {
it('does not throw error if number of invites matches max invite limit', () => {
let uuids = [];
let emails = [];
@@ -552,33 +555,49 @@ describe('Group Model', () => {
emails.push(`user-${i}@example.com`);
}
await Group.validateInvitations(uuids, emails, res);
expect(res.t).to.not.be.called;
expect(function () {
Group.validateInvitations(uuids, emails, res);
}).to.not.throw();
});
it('does not throw an error if only user ids are passed in', async () => {
await Group.validateInvitations(['user-id', 'user-id2'], null, res);
it('does not throw an error if only user ids are passed in', () => {
expect(function () {
Group.validateInvitations(['user-id', 'user-id2'], null, res);
}).to.not.throw();
expect(res.t).to.not.be.called;
});
it('does not throw an error if only emails are passed in', async () => {
await Group.validateInvitations(null, ['user1@example.com', 'user2@example.com'], res);
it('does not throw an error if only emails are passed in', () => {
expect(function () {
Group.validateInvitations(null, ['user1@example.com', 'user2@example.com'], res);
}).to.not.throw();
expect(res.t).to.not.be.called;
});
it('does not throw an error if both uuids and emails are passed in', async () => {
await Group.validateInvitations(['user-id', 'user-id2'], ['user1@example.com', 'user2@example.com'], res);
it('does not throw an error if both uuids and emails are passed in', () => {
expect(function () {
Group.validateInvitations(['user-id', 'user-id2'], ['user1@example.com', 'user2@example.com'], res);
}).to.not.throw();
expect(res.t).to.not.be.called;
});
it('does not throw an error if uuids are passed in and emails are an empty array', async () => {
await Group.validateInvitations(['user-id', 'user-id2'], [], res);
it('does not throw an error if uuids are passed in and emails are an empty array', () => {
expect(function () {
Group.validateInvitations(['user-id', 'user-id2'], [], res);
}).to.not.throw();
expect(res.t).to.not.be.called;
});
it('does not throw an error if emails are passed in and uuids are an empty array', async () => {
await Group.validateInvitations([], ['user1@example.com', 'user2@example.com'], res);
it('does not throw an error if emails are passed in and uuids are an empty array', () => {
expect(function () {
Group.validateInvitations([], ['user1@example.com', 'user2@example.com'], res);
}).to.not.throw();
expect(res.t).to.not.be.called;
});
});
@@ -601,99 +620,6 @@ describe('Group Model', () => {
});
});
describe('#checkChatSpam', () => {
let testUser, testTime, tavern;
let testUserID = '1';
beforeEach(async () => {
testTime = Date.now();
tavern = new Group({
name: 'test tavern',
type: 'guild',
privacy: 'public',
});
tavern._id = TAVERN_ID;
testUser = {
_id: testUserID,
};
});
function generateTestMessage (overrides = {}) {
return Object.assign({}, {
text: 'test message',
uuid: testUserID,
timestamp: testTime,
}, overrides);
}
it('group that is not the tavern returns false, while tavern returns true', async () => {
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
party.chat.push(generateTestMessage());
}
expect(party.checkChatSpam(testUser)).to.eql(false);
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
tavern.chat.push(generateTestMessage());
}
expect(tavern.checkChatSpam(testUser)).to.eql(true);
});
it('high enough contributor returns false', async () => {
let highContributor = testUser;
highContributor.contributor = {
level: SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
};
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
tavern.chat.push(generateTestMessage());
}
expect(tavern.checkChatSpam(highContributor)).to.eql(false);
});
it('chat with no messages returns false', async () => {
expect(tavern.chat.length).to.eql(0);
expect(tavern.checkChatSpam(testUser)).to.eql(false);
});
it('user has not reached limit but another one has returns false', async () => {
let otherUserID = '2';
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
tavern.chat.push(generateTestMessage({uuid: otherUserID}));
}
expect(tavern.checkChatSpam(testUser)).to.eql(false);
});
it('user messages is less than the limit returns false', async () => {
for (let i = 0; i < SPAM_MESSAGE_LIMIT - 1; i++) {
tavern.chat.push(generateTestMessage());
}
expect(tavern.checkChatSpam(testUser)).to.eql(false);
});
it('user has reached the message limit outside of window returns false', async () => {
for (let i = 0; i < SPAM_MESSAGE_LIMIT - 1; i++) {
tavern.chat.push(generateTestMessage());
}
let earlierTimestamp = testTime - SPAM_WINDOW_LENGTH - 1;
tavern.chat.push(generateTestMessage({timestamp: earlierTimestamp}));
expect(tavern.checkChatSpam(testUser)).to.eql(false);
});
it('user has posted too many messages in limit returns true', async () => {
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
tavern.chat.push(generateTestMessage());
}
expect(tavern.checkChatSpam(testUser)).to.eql(true);
});
});
describe('#leaveGroup', () => {
it('removes user from group quest', async () => {
party.quest.members = {
@@ -741,49 +667,6 @@ describe('Group Model', () => {
expect(party.memberCount).to.eql(1);
});
it('does not allow a leader to leave a group with an active subscription', async () => {
party.memberCount = 2;
party.purchased.plan.customerId = '110002222333';
await expect(party.leave(questLeader))
.to.eventually.be.rejected.and.to.eql({
name: 'NotAuthorized',
httpCode: 401,
message: shared.i18n.t('leaderCannotLeaveGroupWithActiveGroup'),
});
party = await Group.findOne({_id: party._id});
expect(party).to.exist;
expect(party.memberCount).to.eql(1);
});
it('deletes a private group when the last member leaves and a subscription is cancelled', async () => {
let guild = new Group({
name: 'test guild',
type: 'guild',
memberCount: 1,
});
let leader = new User({
guilds: [guild._id],
});
guild.leader = leader._id;
await Promise.all([
guild.save(),
leader.save(),
]);
guild.purchased.plan.customerId = '110002222333';
guild.purchased.plan.dateTerminated = new Date();
await guild.leave(leader);
party = await Group.findOne({_id: guild._id});
expect(party).to.not.exist;
});
it('does not delete a public group when the last member leaves', async () => {
party.privacy = 'public';
@@ -1162,7 +1045,7 @@ describe('Group Model', () => {
expect(email.sendTxn).to.be.calledOnce;
let memberIds = _.map(email.sendTxn.args[0][0], '_id');
let memberIds = _.pluck(email.sendTxn.args[0][0], '_id');
let typeOfEmail = email.sendTxn.args[0][1];
expect(memberIds).to.have.a.lengthOf(2);
@@ -1185,7 +1068,7 @@ describe('Group Model', () => {
expect(email.sendTxn).to.be.calledOnce;
let memberIds = _.map(email.sendTxn.args[0][0], '_id');
let memberIds = _.pluck(email.sendTxn.args[0][0], '_id');
expect(memberIds).to.have.a.lengthOf(1);
expect(memberIds).to.not.include(participatingMember._id);
@@ -1206,7 +1089,7 @@ describe('Group Model', () => {
expect(email.sendTxn).to.be.calledOnce;
let memberIds = _.map(email.sendTxn.args[0][0], '_id');
let memberIds = _.pluck(email.sendTxn.args[0][0], '_id');
expect(memberIds).to.have.a.lengthOf(1);
expect(memberIds).to.not.include(participatingMember._id);
@@ -1662,57 +1545,5 @@ describe('Group Model', () => {
expect(args.find(arg => arg[0][0].id === memberWithWebhook3.webhooks[0].id)).to.be.exist;
});
});
context('isSubscribed', () => {
it('returns false if group does not have customer id', () => {
expect(party.isSubscribed()).to.be.undefined;
});
it('returns true if group does not have plan.dateTerminated', () => {
party.purchased.plan.customerId = 'test-id';
expect(party.isSubscribed()).to.be.true;
});
it('returns true if group if plan.dateTerminated is after today', () => {
party.purchased.plan.customerId = 'test-id';
party.purchased.plan.dateTerminated = moment().add(1, 'days').toDate();
expect(party.isSubscribed()).to.be.true;
});
it('returns false if group if plan.dateTerminated is before today', () => {
party.purchased.plan.customerId = 'test-id';
party.purchased.plan.dateTerminated = moment().subtract(1, 'days').toDate();
expect(party.isSubscribed()).to.be.false;
});
});
context('hasNotCancelled', () => {
it('returns false if group does not have customer id', () => {
expect(party.hasNotCancelled()).to.be.undefined;
});
it('returns true if party does not have plan.dateTerminated', () => {
party.purchased.plan.customerId = 'test-id';
expect(party.hasNotCancelled()).to.be.true;
});
it('returns false if party if plan.dateTerminated is after today', () => {
party.purchased.plan.customerId = 'test-id';
party.purchased.plan.dateTerminated = moment().add(1, 'days').toDate();
expect(party.hasNotCancelled()).to.be.false;
});
it('returns false if party if plan.dateTerminated is before today', () => {
party.purchased.plan.customerId = 'test-id';
party.purchased.plan.dateTerminated = moment().subtract(1, 'days').toDate();
expect(party.hasNotCancelled()).to.be.false;
});
});
});
});
+1 -109
View File
@@ -1,7 +1,6 @@
import Bluebird from 'bluebird';
import moment from 'moment';
import { model as User } from '../../../../../website/server/models/user';
import common from '../../../../../website/common';
import Bluebird from 'bluebird';
describe('User Model', () => {
it('keeps user._tmp when calling .toJSON', () => {
@@ -146,111 +145,4 @@ describe('User Model', () => {
});
});
});
context('isSubscribed', () => {
let user;
beforeEach(() => {
user = new User();
});
it('returns false if user does not have customer id', () => {
expect(user.isSubscribed()).to.be.undefined;
});
it('returns true if user does not have plan.dateTerminated', () => {
user.purchased.plan.customerId = 'test-id';
expect(user.isSubscribed()).to.be.true;
});
it('returns true if user if plan.dateTerminated is after today', () => {
user.purchased.plan.customerId = 'test-id';
user.purchased.plan.dateTerminated = moment().add(1, 'days').toDate();
expect(user.isSubscribed()).to.be.true;
});
it('returns false if user if plan.dateTerminated is before today', () => {
user.purchased.plan.customerId = 'test-id';
user.purchased.plan.dateTerminated = moment().subtract(1, 'days').toDate();
expect(user.isSubscribed()).to.be.false;
});
});
context('hasNotCancelled', () => {
let user;
beforeEach(() => {
user = new User();
});
it('returns false if user does not have customer id', () => {
expect(user.hasNotCancelled()).to.be.undefined;
});
it('returns true if user does not have plan.dateTerminated', () => {
user.purchased.plan.customerId = 'test-id';
expect(user.hasNotCancelled()).to.be.true;
});
it('returns false if user if plan.dateTerminated is after today', () => {
user.purchased.plan.customerId = 'test-id';
user.purchased.plan.dateTerminated = moment().add(1, 'days').toDate();
expect(user.hasNotCancelled()).to.be.false;
});
it('returns false if user if plan.dateTerminated is before today', () => {
user.purchased.plan.customerId = 'test-id';
user.purchased.plan.dateTerminated = moment().subtract(1, 'days').toDate();
expect(user.hasNotCancelled()).to.be.false;
});
});
context('pre-save hook', () => {
it('does not try to award achievements when achievements or items not selected in query', async () => {
let user = new User();
user = await user.save(); // necessary for user.isSelected to work correctly
// Create conditions for the Beast Master achievement to be awarded
user.achievements.beastMasterCount = 3;
expect(user.achievements.beastMaster).to.not.equal(true); // verify that it was not awarded initially
user = await user.save();
// verify that it's been awarded
expect(user.achievements.beastMaster).to.equal(true);
// reset the user
user.achievements.beastMasterCount = 0;
user.achievements.beastMaster = false;
user = await user.save();
// verify it's been removed
expect(user.achievements.beastMaster).to.equal(false);
// fetch the user without selecting the 'items' field
user = await User.findById(user._id).select('-items').exec();
expect(user.isSelected('items')).to.equal(false);
// create the conditions for the beast master achievement but this time it should not be awarded
user.achievements.beastMasterCount = 3;
user = await user.save();
expect(user.achievements.beastMaster).to.equal(false);
// reset
user.achievements.beastMasterCount = 0;
user = await user.save();
// this time with achievements not selected
user = await User.findById(user._id).select('-achievements').exec();
expect(user.isSelected('achievements')).to.equal(false);
user.achievements.beastMasterCount = 3;
user = await user.save();
expect(user.achievements.beastMaster).to.not.equal(true);
});
});
});
@@ -1,14 +1,14 @@
'use strict';
describe("Chat Controller", function() {
var scope, ctrl, user, $rootScope, $controller, $httpBackend, html;
var scope, ctrl, user, $rootScope, $controller;
beforeEach(function() {
module(function($provide) {
$provide.value('User', {});
});
inject(function(_$rootScope_, _$controller_, _$compile_, _$httpBackend_){
inject(function(_$rootScope_, _$controller_){
user = specHelper.newUser();
user._id = "unique-user-id";
$rootScope = _$rootScope_;
@@ -16,20 +16,13 @@ describe("Chat Controller", function() {
scope = _$rootScope_.$new();
$controller = _$controller_;
$httpBackend = _$httpBackend_;
// Load RootCtrl to ensure shared behaviors are loaded
$controller('RootCtrl', {$scope: scope, User: {user: user}});
html = _$compile_('<div><form ng-submit="postChat(group, message.content)"><textarea submit-on-meta-enter ng-model="message.content" ng-model-options="{debounce: 250}"></textarea></form></div>')(scope);
document.body.appendChild(html[0]);
ctrl = $controller('ChatCtrl', {$scope: scope, $element: html});
ctrl = $controller('ChatCtrl', {$scope: scope});
});
});
afterEach(function() {
html.remove();
});
describe('copyToDo', function() {
it('when copying a user message it opens modal with information from message', function() {
@@ -75,47 +68,5 @@ describe("Chat Controller", function() {
}));
});
});
it('updates model on enter key press', function() {
// Set initial state of the page with some dummy data.
scope.group = { name: 'group' };
// The main controller is going to try to fetch the template right off.
// No big deal, just return an empty string.
$httpBackend.when('GET', 'partials/main.html').respond('');
// Let the page settle, and the controllers set their initial state.
$rootScope.$digest();
// Watch for calls to postChat & make sure it doesn't do anything.
let postChatSpy = sandbox.stub(scope, 'postChat');
// Pretend we typed 'aaa' into the textarea.
var textarea = html.find('textarea');
textarea[0].value = 'aaa';
let inputEvent = new Event('input');
textarea[0].dispatchEvent(inputEvent);
// Give a change for the ng-model watchers to notice that the value in the
// textarea has changed.
$rootScope.$digest();
// Since no time has elapsed and we debounce the model change, we should
// see no model update just yet.
expect(scope.message.content).to.equal('');
// Now, press the enter key in the textarea. We use jquery here to paper
// over browser differences with initializing the keyboard event.
var keyboardEvent = jQuery.Event('keydown', {keyCode: 13, key: 'Enter', metaKey: true});
jQuery(textarea).trigger(keyboardEvent);
// Now, allow the model to update given the changes to the page still
// without letting any time elapse...
$rootScope.$digest();
// ... and nevertheless seeing the desired call to postChat with the right
// data. Yay!
postChatSpy.should.have.been.calledWith(scope.group, 'aaa');
});
});
@@ -30,9 +30,7 @@ describe('Inventory Controller', function() {
suppressModals: {}
},
purchased: {
plan: {
mysteryItems: [],
},
plan: {}
},
});
@@ -334,34 +334,4 @@ describe('Settings Controller', function () {
});
});
});
context('Fixing character values', function () {
describe('#restore', function () {
var blankRestoreValues = {
stats: {
hp: 0,
exp: 0,
gp: 0,
lvl: 0,
mp: 0,
},
achievements: {
streak: 0,
},
};
it('doesn\'t update character values when level is less than 1', function () {
scope.restoreValues = blankRestoreValues;
scope.restore();
expect(User.set).to.not.be.called;
});
it('updates character values when level is at least 1', function () {
scope.restoreValues = blankRestoreValues;
scope.restoreValues.stats.lvl = 1;
scope.restore();
expect(User.set).to.be.called;
});
});
});
});
@@ -1,69 +0,0 @@
'use strict';
describe('User Controller', function() {
var $rootScope, $window, User, shared, scope, ctrl, content;
beforeEach(function() {
module(function ($provide) {
var user = specHelper.newUser();
User = {user: user}
$provide.value('Guide', sandbox.stub());
$provide.value('User', User);
$provide.value('Achievement', sandbox.stub());
$provide.value('Social', sandbox.stub());
$provide.value('Shared', {
achievements: {
getAchievementsForProfile: sandbox.stub()
},
shops: {
getBackgroundShopSets: sandbox.stub()
}
});
$provide.value('Content', {
loginIncentives: sandbox.stub()
})
});
inject(function($rootScope, $controller, User, Content) {
scope = $rootScope.$new();
content = Content;
$controller('RootCtrl', { $scope: scope, User: User});
ctrl = $controller('UserCtrl', { $scope: scope, User: User, $window: $window});
});
});
describe('getProgressDisplay', function() {
beforeEach(() => {
sandbox.stub(window.env, 't');
window.env.t.onFirstCall().returns('Progress until next');
});
it('should return initial progress', function() {
scope.profile.loginIncentives = 0;
content.loginIncentives = [{
nextRewardAt: 1,
reward: true
}];
var actual = scope.getProgressDisplay();
expect(actual.trim()).to.eql('Progress until next 0/1');
});
it('should return progress between next reward and current reward', function() {
scope.profile.loginIncentives = 1;
content.loginIncentives = [{
nextRewardAt: 1,
reward: true
}, {
prevRewardAt: 0,
nextRewardAt: 2,
reward: true
}, {
prevRewardAt: 1,
nextRewardAt: 3
}];
var actual = scope.getProgressDisplay();
expect(actual.trim()).to.eql('Progress until next 0/1');
});
});
});
@@ -1,35 +0,0 @@
describe('task Directive', () => {
var compile, scope, directiveElem, $modal;
beforeEach(function(){
module(function($provide) {
$modal = {
open: sandbox.spy(),
};
$provide.value('$modal', $modal);
});
inject(function($compile, $rootScope, $templateCache) {
compile = $compile;
scope = $rootScope.$new();
$templateCache.put('templates/task.html', '<div>Task</div>');
});
directiveElem = getCompiledElement();
});
function getCompiledElement(){
var element = angular.element('<task></task>');
var compiledElement = compile(element)(scope);
scope.$digest();
return compiledElement;
}
xit('opens task note modal', () => {
scope.showNoteDetails();
expect($modal.open).to.be.calledOnce;
});
})
+2 -5
View File
@@ -81,11 +81,8 @@ module.exports = function karmaConfig (config) {
},
coverageReporter: {
reporters: [
{ type: 'lcov', subdir: '.' },
{ type: 'text-summary' },
],
dir: '../../../coverage/karma',
type: 'lcov',
dir: 'coverage/karma',
},
// Enable mocha-style reporting, for better test visibility
+2 -11
View File
@@ -2,17 +2,8 @@
// and babel-runtime doesn't affect external libraries
require('babel-polyfill');
// Automatically setup SinonJS' sandbox for each test
beforeEach(() => {
global.sandbox = sinon.sandbox.create();
});
afterEach(() => {
global.sandbox.restore();
});
// require all test files
let testsContext = require.context('./specs', true, /\.js$/);
// require all test files (files that ends with .spec.js)
let testsContext = require.context('./specs', true, /\.spec$/);
testsContext.keys().forEach(testsContext);
// require all .vue and .js files except main.js for coverage.

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