mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-16 14:17:22 +01:00
Compare commits
251 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91b6d3db02 | ||
|
|
d16ce1ce48 | ||
|
|
564c366bfb | ||
|
|
c8c65a4f4f | ||
|
|
a4feae4dbb | ||
|
|
d8620e1636 | ||
|
|
46d96b444b | ||
|
|
60c9434b14 | ||
|
|
c3901e8615 | ||
|
|
d166de8ad0 | ||
|
|
82e9afe9ce | ||
|
|
999202a8a5 | ||
|
|
8b53adfcb1 | ||
|
|
c23062f87e | ||
|
|
ec98541df6 | ||
|
|
4fed13afdd | ||
|
|
866b28ec15 | ||
|
|
bdf4a69eaf | ||
|
|
4c121fba19 | ||
|
|
0ecb95a294 | ||
|
|
ea7c07e21d | ||
|
|
c4463f991b | ||
|
|
96ce948e1a | ||
|
|
6d06685dfa | ||
|
|
32b6566e37 | ||
|
|
ca3b4cd8ae | ||
|
|
c90b4b488e | ||
|
|
8c203637d7 | ||
|
|
58f72b7eaa | ||
|
|
f0bbe84bd1 | ||
|
|
038e3f3235 | ||
|
|
1a7c8c1f87 | ||
|
|
c73d6154a8 | ||
|
|
e8b77ad2b2 | ||
|
|
ea18489991 | ||
|
|
5eb1b6684e | ||
|
|
a2f77eeba2 | ||
|
|
d4198f8913 | ||
|
|
565d50dd99 | ||
|
|
be1754ab07 | ||
|
|
831b122ce2 | ||
|
|
03088f1d9f | ||
|
|
1d7b733759 | ||
|
|
d170f0b1bd | ||
|
|
4846bc5769 | ||
|
|
30f514e46f | ||
|
|
6aad018eb2 | ||
|
|
daf9421f4f | ||
|
|
b2225f05e5 | ||
|
|
842fbe42a8 | ||
|
|
5eadf9e486 | ||
|
|
68ad3e2d4a | ||
|
|
de947f8069 | ||
|
|
d541e3aa31 | ||
|
|
b0eda344f1 | ||
|
|
02708a7b10 | ||
|
|
fd9f3a32c4 | ||
|
|
6e0341a4ff | ||
|
|
625077fc1a | ||
|
|
9d456e934c | ||
|
|
771d8f492a | ||
|
|
f3fab88f0b | ||
|
|
207e3476e6 | ||
|
|
0ec293bd15 | ||
|
|
cb00ecc0be | ||
|
|
94ef4f80cc | ||
|
|
814b163e1a | ||
|
|
421bdce38b | ||
|
|
624566ecec | ||
|
|
77ff91868e | ||
|
|
59f490d178 | ||
|
|
ae64ef94ae | ||
|
|
2335e22a0c | ||
|
|
910154b3ed | ||
|
|
31e36339c4 | ||
|
|
07fe1df024 | ||
|
|
258742f6b7 | ||
|
|
d9d7c69432 | ||
|
|
03d6c459bf | ||
|
|
12cefe4e9f | ||
|
|
21ad808cc1 | ||
|
|
db9befde17 | ||
|
|
01af658c2d | ||
|
|
52738575fa | ||
|
|
cc9bca5f63 | ||
|
|
05d75a4d5c | ||
|
|
164177f010 | ||
|
|
fc69a3a960 | ||
|
|
797adbb1dc | ||
|
|
3dc20a9832 | ||
|
|
cfa433aa75 | ||
|
|
9bc2d22d30 | ||
|
|
4909f67ded | ||
|
|
8bc6534ff5 | ||
|
|
a8ebd04ac8 | ||
|
|
e0d499abab | ||
|
|
f67e065de2 | ||
|
|
283403d6c8 | ||
|
|
55c7d6a191 | ||
|
|
a5011f000e | ||
|
|
c5633e2074 | ||
|
|
939712ad1f | ||
|
|
09490551d4 | ||
|
|
767763fbf6 | ||
|
|
237e0df611 | ||
|
|
ca903f0dc3 | ||
|
|
bde41699ee | ||
|
|
d0d4b47c47 | ||
|
|
c616346233 | ||
|
|
311d3a256a | ||
|
|
af6f3f9656 | ||
|
|
15681fedcc | ||
|
|
bf12f8aa71 | ||
|
|
41ee72d407 | ||
|
|
9a5f6d4ad6 | ||
|
|
d19237cdbe | ||
|
|
18bd3f8c54 | ||
|
|
8b65ce3053 | ||
|
|
38b894db56 | ||
|
|
61db283473 | ||
|
|
d70d39cc49 | ||
|
|
c26b884bc7 | ||
|
|
11c8f2a775 | ||
|
|
1082359f2c | ||
|
|
6486862242 | ||
|
|
f68cc569d6 | ||
|
|
b10751e874 | ||
|
|
b75c57f130 | ||
|
|
28c93ea869 | ||
|
|
7f630f2b86 | ||
|
|
1a8f591251 | ||
|
|
be60fb0635 | ||
|
|
03a1d61c08 | ||
|
|
0767dc97b7 | ||
|
|
4978a62829 | ||
|
|
0a35e63897 | ||
|
|
03b3e79ea0 | ||
|
|
dc8598ae81 | ||
|
|
3629f7f8a5 | ||
|
|
8805f81b96 | ||
|
|
207dbf35d6 | ||
|
|
448a953147 | ||
|
|
4fb1ff2baa | ||
|
|
be64274be4 | ||
|
|
390970a73a | ||
|
|
0cb254d5fc | ||
|
|
98c019a0b6 | ||
|
|
ef02e59590 | ||
|
|
93befcebcc | ||
|
|
68a042cdb9 | ||
|
|
30954fe7c5 | ||
|
|
44f23a7675 | ||
|
|
705a78e835 | ||
|
|
6d0df78441 | ||
|
|
6f0d0b1fb3 | ||
|
|
dfcd32d54a | ||
|
|
c24cbdc987 | ||
|
|
68d8c0de51 | ||
|
|
a7b0fa195f | ||
|
|
ab65aa692f | ||
|
|
198d4df02a | ||
|
|
6fa2f643fd | ||
|
|
4d39861b51 | ||
|
|
d4c99b6db6 | ||
|
|
a79dc90a45 | ||
|
|
daa6bd0315 | ||
|
|
99f2373214 | ||
|
|
374d528647 | ||
|
|
8550ca4d29 | ||
|
|
79f6b59f6e | ||
|
|
e37ad420ce | ||
|
|
2f9ff92cbd | ||
|
|
56479e7fbd | ||
|
|
66e4849553 | ||
|
|
9e29b44ad9 | ||
|
|
605488dd47 | ||
|
|
6c16b4b77e | ||
|
|
80205dcc67 | ||
|
|
65d5bf69f6 | ||
|
|
20792f5455 | ||
|
|
6fd509df13 | ||
|
|
d7df18a97a | ||
|
|
ff5c44c5a8 | ||
|
|
c929fa1351 | ||
|
|
d30e7b9251 | ||
|
|
c6c6632405 | ||
|
|
e0d9ecca52 | ||
|
|
78f187f87a | ||
|
|
af047d3c82 | ||
|
|
20e8f47def | ||
|
|
c496f94c79 | ||
|
|
ad6073220d | ||
|
|
d669db0f9a | ||
|
|
c244fe488d | ||
|
|
aef71db0f5 | ||
|
|
ac566882fe | ||
|
|
1ad662a090 | ||
|
|
1db52de45d | ||
|
|
f8cfdfa37d | ||
|
|
ffd36465c9 | ||
|
|
dbb1e3aa18 | ||
|
|
07ce68c596 | ||
|
|
4c5d72c96f | ||
|
|
7121653515 | ||
|
|
ce2b66a2be | ||
|
|
43e381cfc1 | ||
|
|
ca5161b48a | ||
|
|
44dec6dc4b | ||
|
|
fb8f0bed43 | ||
|
|
c391de9e86 | ||
|
|
7a534ab81d | ||
|
|
32c7f40c4b | ||
|
|
569fbff244 | ||
|
|
3fe847f329 | ||
|
|
d52d759733 | ||
|
|
5e5a755022 | ||
|
|
8d148b4d69 | ||
|
|
248b64a43f | ||
|
|
57ed0f0a10 | ||
|
|
3455adaef5 | ||
|
|
4d0295a60d | ||
|
|
a002bc5e20 | ||
|
|
b9d1086e24 | ||
|
|
412a0ecc8c | ||
|
|
d44c9ea853 | ||
|
|
c44c581265 | ||
|
|
f666f3cd04 | ||
|
|
236bd6cec4 | ||
|
|
e124c36274 | ||
|
|
d5511a0047 | ||
|
|
87f003f392 | ||
|
|
930a869365 | ||
|
|
809da8add0 | ||
|
|
1dad176320 | ||
|
|
1596c6218f | ||
|
|
0d3aba950a | ||
|
|
f1110e0f89 | ||
|
|
7273f8f6d9 | ||
|
|
a0ae200a54 | ||
|
|
ca448f081d | ||
|
|
cd27afa9f0 | ||
|
|
8bc8183895 | ||
|
|
c26c52f1fe | ||
|
|
8d5becc9ce | ||
|
|
cf7f6e2a67 | ||
|
|
acad3b8873 | ||
|
|
04f4eb8490 | ||
|
|
8c8af83dfc | ||
|
|
86d65956d9 | ||
|
|
42c5e6c22b | ||
|
|
79b51a40ce |
@@ -8,16 +8,13 @@ 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
|
||||
website/common/script/content/index.js
|
||||
migrations/*
|
||||
scripts/*
|
||||
website/common/browserify.js
|
||||
test/content/**/*
|
||||
Gruntfile.js
|
||||
gulpfile.js
|
||||
gulp
|
||||
27
.travis.yml
27
.travis.yml
@@ -1,22 +1,33 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- '6'
|
||||
sudo: required
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- g++-4.8
|
||||
before_install:
|
||||
- $CXX --version
|
||||
- npm install -g npm@4
|
||||
- if [ $REQUIRES_SERVER ]; then sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10; echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list; sudo apt-get update; sudo apt-get install mongodb-org-server; fi
|
||||
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
|
||||
after_script:
|
||||
- ./node_modules/.bin/lcov-result-merger 'coverage/**/*.info' | ./node_modules/coveralls/bin/coveralls.js
|
||||
script: npm run $TEST
|
||||
script:
|
||||
- npm run $TEST
|
||||
- if [ $COVERAGE ]; then ./node_modules/.bin/lcov-result-merger 'coverage/**/*.info' | ./node_modules/coveralls/bin/coveralls.js; fi
|
||||
env:
|
||||
global:
|
||||
- CXX=g++-4.8
|
||||
- DISABLE_REQUEST_LOGGING=true
|
||||
matrix:
|
||||
- TEST="lint"
|
||||
- TEST="test:api-v3" REQUIRES_SERVER=true
|
||||
- TEST="test:api-v3" REQUIRES_SERVER=true COVERAGE=true
|
||||
- TEST="test:sanity"
|
||||
- TEST="test:content"
|
||||
- TEST="test:common"
|
||||
- TEST="test:karma"
|
||||
- TEST="client:unit"
|
||||
- TEST="test:content" COVERAGE=true
|
||||
- TEST="test:common" COVERAGE=true
|
||||
- TEST="test:karma" COVERAGE=true
|
||||
- TEST="client:unit" COVERAGE=true
|
||||
|
||||
@@ -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.13.4",
|
||||
"hello": "1.14.1",
|
||||
"jquery": "2.1.0",
|
||||
"jquery-colorbox": "1.4.36",
|
||||
"jquery-ui": "1.10.3",
|
||||
|
||||
@@ -2,13 +2,18 @@
|
||||
"PORT":3000,
|
||||
"ENABLE_CONSOLE_LOGS_IN_PROD":"false",
|
||||
"IP":"0.0.0.0",
|
||||
"CORES":1,
|
||||
"WEB_CONCURRENCY":1,
|
||||
"BASE_URL":"http://localhost:3000",
|
||||
"FACEBOOK_ANALYTICS":"1234567890123456",
|
||||
"FACEBOOK_KEY":"123456789012345",
|
||||
"FACEBOOK_SECRET":"aaaabbbbccccddddeeeeffff00001111",
|
||||
"GOOGLE_CLIENT_ID":"123456789012345",
|
||||
"GOOGLE_CLIENT_SECRET":"aaaabbbbccccddddeeeeffff00001111",
|
||||
"PLAY_API": {
|
||||
"CLIENT_ID": "aaaabbbbccccddddeeeeffff00001111",
|
||||
"CLIENT_SECRET": "aaaabbbbccccddddeeeeffff00001111",
|
||||
"ACCESS_TOKEN":"aaaabbbbccccddddeeeeffff00001111",
|
||||
"REFRESH_TOKEN":"aaaabbbbccccddddeeeeffff00001111"
|
||||
},
|
||||
"NODE_DB_URI":"mongodb://localhost/habitrpg",
|
||||
"TEST_DB_URI":"mongodb://localhost/habitrpg_test",
|
||||
"NODE_ENV":"development",
|
||||
@@ -68,9 +73,14 @@
|
||||
"LOGGLY_ACCOUNT": "account",
|
||||
"PUSH_CONFIGS": {
|
||||
"GCM_SERVER_API_KEY": "",
|
||||
"APN_ENABLED": "true",
|
||||
"APN_ENABLED": "false",
|
||||
"FCM_SERVER_API_KEY": ""
|
||||
},
|
||||
"SITE_HTTP_AUTH": {
|
||||
"ENABLED": "false",
|
||||
"USERNAME": "admin",
|
||||
"PASSWORD": "password"
|
||||
},
|
||||
"PUSHER": {
|
||||
"ENABLED": "false",
|
||||
"APP_ID": "appId",
|
||||
@@ -81,5 +91,11 @@
|
||||
"FLAGGING_URL": "https://hooks.slack.com/services/id/id/id",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ gulp.task('build:common', () => {
|
||||
|
||||
gulp.task('build:server', ['build:src', 'build:common']);
|
||||
|
||||
gulp.task('build:dev', ['browserify', 'prepare:staticNewStuff', 'semantic-ui'], (done) => {
|
||||
gulp.task('build:dev', ['browserify', 'prepare:staticNewStuff'], (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', 'semantic-ui'], (done) => {
|
||||
gulp.task('build:prod', ['browserify', 'build:server', 'prepare:staticNewStuff'], (done) => {
|
||||
runSequence(
|
||||
'grunt-build:prod',
|
||||
'apidoc',
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
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);
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -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('mocha test/api/v3/unit --recursive --require ./test/helpers/start-server'),
|
||||
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'),
|
||||
(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('mocha test/api/v3/integration --recursive --require ./test/helpers/start-server'),
|
||||
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'),
|
||||
{maxBuffer: 500 * 1024},
|
||||
(err, stdout, stderr) => {
|
||||
if (err) {
|
||||
|
||||
@@ -67,7 +67,7 @@ gulp.task('transifex:malformedStrings', () => {
|
||||
let stringsWithIncorrectNumberOfInterpolations = [];
|
||||
|
||||
let count = 0;
|
||||
_(ALL_LANGUAGES).each(function (lang) {
|
||||
_.each(ALL_LANGUAGES, 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));
|
||||
|
||||
_(languages).each((lang) => {
|
||||
_.each(languages, (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 = {};
|
||||
|
||||
_(json).each(function (file_name) {
|
||||
_.each(json, 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;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
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');
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
// %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,
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
*/
|
||||
// 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'},
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
|
||||
// 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,6 +6,11 @@
|
||||
* 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} );
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
//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,6 +3,12 @@
|
||||
// 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,6 +5,12 @@
|
||||
// @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,6 +8,12 @@
|
||||
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');
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
// 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');
|
||||
|
||||
|
||||
@@ -8,6 +8,12 @@ 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,5 +1,11 @@
|
||||
// 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
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
// 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,6 +5,12 @@ 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,6 +4,12 @@ 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,6 +4,11 @@
|
||||
//
|
||||
// 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,6 +8,12 @@ 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,3 +1,9 @@
|
||||
// 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}},
|
||||
|
||||
@@ -9,6 +9,12 @@ 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,6 +8,12 @@ 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,6 +19,12 @@ 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,6 +6,12 @@ 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');
|
||||
|
||||
@@ -19,6 +19,12 @@ 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,6 +7,12 @@ 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');
|
||||
|
||||
@@ -11,6 +11,12 @@ 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');
|
||||
|
||||
|
||||
@@ -14,6 +14,12 @@ 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,6 +2,12 @@ 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;
|
||||
});
|
||||
|
||||
109
migrations/20170131_habit_birthday.js
Normal file
109
migrations/20170131_habit_birthday.js
Normal file
@@ -0,0 +1,109 @@
|
||||
var migrationName = '20170131_habit_birthday.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 2017 party robes if user has 2016 robes, 2016 robes if they have the 2015 robes,
|
||||
* 2015 robes if they have the 2014 robes, and 2014 robes otherwise. Also cake!
|
||||
*/
|
||||
|
||||
var monk = require('monk');
|
||||
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 = {
|
||||
'migration':{$ne:migrationName},
|
||||
'auth.timestamps.loggedin':{$gt:new Date('2017-01-24')}, // remove after first run to cover remaining users
|
||||
};
|
||||
|
||||
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)
|
||||
'items.gear.owned'
|
||||
],
|
||||
})
|
||||
.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 = {'migration':migrationName};
|
||||
if (user.items && user.items.gear && user.items.gear.owned && user.items.gear.owned.hasOwnProperty('armor_special_birthday2016')) {
|
||||
set['items.gear.owned.armor_special_birthday2017'] = false;
|
||||
} else if (user.items && user.items.gear && user.items.gear.owned && user.items.gear.owned.hasOwnProperty('armor_special_birthday2015')) {
|
||||
set['items.gear.owned.armor_special_birthday2016'] = false;
|
||||
} else if (user.items && user.items.gear && user.items.gear.owned && user.items.gear.owned.hasOwnProperty('armor_special_birthday')) {
|
||||
set['items.gear.owned.armor_special_birthday2015'] = false;
|
||||
} else {
|
||||
set['items.gear.owned.armor_special_birthday'] = false;
|
||||
}
|
||||
|
||||
var inc = {
|
||||
'items.food.Cake_Skeleton':1,
|
||||
'items.food.Cake_Base':1,
|
||||
'items.food.Cake_CottonCandyBlue':1,
|
||||
'items.food.Cake_CottonCandyPink':1,
|
||||
'items.food.Cake_Shade':1,
|
||||
'items.food.Cake_White':1,
|
||||
'items.food.Cake_Golden':1,
|
||||
'items.food.Cake_Zombie':1,
|
||||
'items.food.Cake_Desert':1,
|
||||
'items.food.Cake_Red':1,
|
||||
'achievements.habitBirthdays':1
|
||||
};
|
||||
|
||||
dbUsers.update({_id: user._id}, {$set:set, $inc:inc});
|
||||
|
||||
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
|
||||
if (user._id == authorUuid) console.warn(authorName + ' processed');
|
||||
}
|
||||
|
||||
function displayData() {
|
||||
console.warn('\n' + count + ' users processed\n');
|
||||
return exiting(0);
|
||||
}
|
||||
|
||||
function exiting(code, msg) {
|
||||
code = code || 0; // 0 = success
|
||||
if (code && !msg) { msg = 'ERROR!'; }
|
||||
if (msg) {
|
||||
if (code) { console.error(msg); }
|
||||
else { console.log( msg); }
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
module.exports = processUsers;
|
||||
@@ -6,6 +6,12 @@
|
||||
|
||||
// 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');
|
||||
|
||||
@@ -9,6 +9,12 @@
|
||||
// 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');
|
||||
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
// 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,6 +8,12 @@
|
||||
// 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');
|
||||
|
||||
|
||||
@@ -16,6 +16,12 @@
|
||||
// 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');
|
||||
|
||||
|
||||
@@ -9,6 +9,12 @@
|
||||
// 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');
|
||||
|
||||
|
||||
47
migrations/challenges/sync-all-challenges.js
Normal file
47
migrations/challenges/sync-all-challenges.js
Normal file
@@ -0,0 +1,47 @@
|
||||
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;
|
||||
@@ -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 = _.any(user.tasks, {text: ""})
|
||||
var found = _.some(user.tasks, {text: ""})
|
||||
if (found) printjson({id:user._id, auth:user.auth});
|
||||
})
|
||||
40
migrations/groups/add-unlimited-subscription.js
Normal file
40
migrations/groups/add-unlimited-subscription.js
Normal file
@@ -0,0 +1,40 @@
|
||||
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
migrations/groups/create-group.js
Normal file
32
migrations/groups/create-group.js
Normal file
@@ -0,0 +1,32 @@
|
||||
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
migrations/groups/habitrpg-jackalopes.js
Normal file
46
migrations/groups/habitrpg-jackalopes.js
Normal file
@@ -0,0 +1,46 @@
|
||||
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;
|
||||
33
migrations/groups/update-groups-with-group-plans.js
Normal file
33
migrations/groups/update-groups-with-group-plans.js
Normal file
@@ -0,0 +1,33 @@
|
||||
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;
|
||||
@@ -1,6 +1,9 @@
|
||||
// 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')));
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
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();
|
||||
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)
|
||||
})
|
||||
@@ -2,7 +2,7 @@ var _id = '';
|
||||
var update = {
|
||||
$addToSet: {
|
||||
'purchased.plan.mysteryItems':{
|
||||
$each:['head_mystery_201612','armor_mystery_201612']
|
||||
$each:['head_mystery_201703','armor_mystery_201703']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -10,7 +10,6 @@ var monk = require('monk');
|
||||
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 = {
|
||||
@@ -45,10 +44,10 @@ function updateUsers (users) {
|
||||
return;
|
||||
}
|
||||
|
||||
var userPaymentPromises = users.map(updateUser);
|
||||
var userPromises = users.map(updateUser);
|
||||
var lastUser = users[users.length - 1];
|
||||
|
||||
return Promise.all(userPaymentPromises)
|
||||
return Promise.all(userPromises)
|
||||
.then(function () {
|
||||
processUsers(lastUser._id);
|
||||
});
|
||||
|
||||
@@ -6,49 +6,69 @@ var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
|
||||
* Remove flag stating that the Enchanted Armoire is empty, for when new equipment is added
|
||||
*/
|
||||
|
||||
var mongo = require('mongoskin');
|
||||
|
||||
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 dbUsers = mongo.db(connectionString).collection('users');
|
||||
function processUsers(lastId) {
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
var query = {
|
||||
'flags.armoireEmpty': true,
|
||||
};
|
||||
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
var query = {
|
||||
'flags.armoireEmpty':true
|
||||
};
|
||||
if (lastId) {
|
||||
query._id = {
|
||||
$gt: lastId
|
||||
}
|
||||
}
|
||||
|
||||
// specify fields we are interested in to limit retrieved data (empty if we're not reading data):
|
||||
var fields = {
|
||||
};
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
console.warn('Updating users...');
|
||||
var progressCount = 1000;
|
||||
var count = 0;
|
||||
dbUsers.findEach(query, fields, {batchSize:250}, function(err, user) {
|
||||
if (err) { return exiting(1, 'ERROR! ' + err); }
|
||||
if (!user) {
|
||||
|
||||
function updateUsers (users) {
|
||||
if (!users || users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
setTimeout(displayData, 300000);
|
||||
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++;
|
||||
|
||||
// specify user data to change:
|
||||
var set = {'migration':migrationName, 'flags.armoireEmpty':false};
|
||||
var set = {'migration': migrationName, 'flags.armoireEmpty': false};
|
||||
|
||||
dbUsers.update({_id:user._id}, {$set:set});
|
||||
dbUsers.update({_id: user._id}, {$set:set});
|
||||
|
||||
if (count%progressCount == 0) console.warn(count + ' ' + user._id);
|
||||
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
|
||||
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!'; }
|
||||
@@ -58,3 +78,5 @@ function exiting(code, msg) {
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
module.exports = processUsers;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
var migrationName = '20170103_takeThis.js'; // Update per month
|
||||
var migrationName = '20170404_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
|
||||
|
||||
@@ -6,41 +6,64 @@ var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
|
||||
* Award Take This ladder items to participants in this month's challenge
|
||||
*/
|
||||
|
||||
var mongo = require('mongoskin');
|
||||
|
||||
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 dbUsers = mongo.db(connectionString).collection('users');
|
||||
function processUsers(lastId) {
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
var query = {
|
||||
'migration':{$ne:migrationName},
|
||||
'challenges':{$in:['630018a7-49ab-4e95-ac26-12417b746e1c']} // Update per month
|
||||
};
|
||||
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
var query = {
|
||||
'migration':{$ne:migrationName},
|
||||
'challenges':{$in:['ff674aba-a114-4a6f-8ebc-1de27ffb646e']}
|
||||
};
|
||||
if (lastId) {
|
||||
query._id = {
|
||||
$gt: lastId
|
||||
}
|
||||
}
|
||||
|
||||
// specify fields we are interested in to limit retrieved data (empty if we're not reading data):
|
||||
var fields = {
|
||||
'items.gear.owned': 1
|
||||
};
|
||||
dbUsers.find(query, {
|
||||
sort: {_id: 1},
|
||||
limit: 250,
|
||||
fields: [
|
||||
'items.gear.owned',
|
||||
] // 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);
|
||||
});
|
||||
}
|
||||
|
||||
console.warn('Updating users...');
|
||||
var progressCount = 1000;
|
||||
var count = 0;
|
||||
dbUsers.findEach(query, fields, {batchSize:250}, function(err, user) {
|
||||
if (err) { return exiting(1, 'ERROR! ' + err); }
|
||||
if (!user) {
|
||||
|
||||
function updateUsers (users) {
|
||||
if (!users || users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
setTimeout(displayData, 300000);
|
||||
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++;
|
||||
|
||||
// specify user data to change:
|
||||
var set = {};
|
||||
|
||||
if (typeof user.items.gear.owned.back_special_takeThis !== 'undefined') {
|
||||
set = {'migration':migrationName};
|
||||
{ else if (typeof user.items.gear.owned.body_special_takeThis !== 'undefined') {
|
||||
} else if (typeof user.items.gear.owned.body_special_takeThis !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.back_special_takeThis':false};
|
||||
} else if (typeof user.items.gear.owned.head_special_takeThis !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.body_special_takeThis':false};
|
||||
@@ -54,19 +77,17 @@ dbUsers.findEach(query, fields, {batchSize:250}, function(err, user) {
|
||||
set = {'migration':migrationName, 'items.gear.owned.shield_special_takeThis':false};
|
||||
}
|
||||
|
||||
dbUsers.update({_id:user._id}, {$set:set});
|
||||
dbUsers.update({_id: user._id}, {$set:set});
|
||||
|
||||
if (count%progressCount == 0) console.warn(count + ' ' + user._id);
|
||||
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
|
||||
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!'; }
|
||||
@@ -77,4 +98,4 @@ function exiting(code, msg) {
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
|
||||
module.exports = processUsers;
|
||||
|
||||
5624
npm-shrinkwrap.json
generated
5624
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
105
package.json
105
package.json
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "3.71.0",
|
||||
"version": "3.84.1",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@slack/client": "3.6.0",
|
||||
"@slack/client": "^3.8.1",
|
||||
"accepts": "^1.3.2",
|
||||
"amazon-payments": "0.0.4",
|
||||
"amplitude": "^2.0.3",
|
||||
"apidoc": "^0.16.0",
|
||||
"apidoc": "^0.17.5",
|
||||
"apn": "^1.7.6",
|
||||
"async": "^1.5.0",
|
||||
"autoprefixer": "^6.4.0",
|
||||
@@ -25,24 +25,27 @@
|
||||
"babel-register": "^6.6.0",
|
||||
"babel-runtime": "^6.11.6",
|
||||
"babelify": "^7.2.0",
|
||||
"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",
|
||||
"connect-ratelimit": "0.0.7",
|
||||
"cookie-session": "^1.2.0",
|
||||
"coupon-code": "^0.4.5",
|
||||
"css-loader": "^0.23.1",
|
||||
"css-loader": "^0.26.1",
|
||||
"csv-stringify": "^1.0.2",
|
||||
"cwait": "^1.0.0",
|
||||
"domain-middleware": "~0.1.0",
|
||||
"estraverse": "^4.1.1",
|
||||
"express": "~4.14.0",
|
||||
"express-basic-auth": "^1.0.1",
|
||||
"express-csv": "~0.6.0",
|
||||
"express-validator": "^2.18.0",
|
||||
"extract-text-webpack-plugin": "^1.0.1",
|
||||
"file-loader": "^0.8.4",
|
||||
"extract-text-webpack-plugin": "^2.0.0-rc.3",
|
||||
"file-loader": "^0.10.0",
|
||||
"glob": "^4.3.5",
|
||||
"got": "^6.1.1",
|
||||
"grunt": "~0.4.1",
|
||||
@@ -53,7 +56,7 @@
|
||||
"grunt-contrib-stylus": "~0.20.0",
|
||||
"grunt-contrib-uglify": "~0.6.0",
|
||||
"grunt-contrib-watch": "~0.6.1",
|
||||
"grunt-hashres": "~0.4.1",
|
||||
"grunt-hashres": "habitrpg/grunt-hashres#v0.4.2",
|
||||
"gulp": "^3.9.0",
|
||||
"gulp-babel": "^6.1.2",
|
||||
"gulp-grunt": "^0.5.2",
|
||||
@@ -67,62 +70,59 @@
|
||||
"image-size": "~0.3.2",
|
||||
"in-app-purchase": "^1.1.6",
|
||||
"jade": "~1.11.0",
|
||||
"jquery": "https://registry.npmjs.org/jquery/-/jquery-3.1.1.tgz",
|
||||
"jquery": "^3.1.1",
|
||||
"js2xmlparser": "~1.0.0",
|
||||
"json-loader": "^0.5.4",
|
||||
"less": "^2.7.1",
|
||||
"less-loader": "^2.2.3",
|
||||
"lodash": "^3.10.1",
|
||||
"lodash.pickby": "^4.2.0",
|
||||
"lodash.setwith": "^4.2.0",
|
||||
"lodash": "^4.17.4",
|
||||
"merge-stream": "^1.0.0",
|
||||
"method-override": "^2.3.5",
|
||||
"moment": "^2.13.0",
|
||||
"mongoose": "^4.7.1",
|
||||
"moment-recur": "habitrpg/moment-recur#v1.0.6",
|
||||
"mongoose": "^4.8.6",
|
||||
"mongoose-id-autoinc": "~2013.7.14-4",
|
||||
"morgan": "^1.7.0",
|
||||
"nconf": "~0.8.2",
|
||||
"newrelic": "^1.27.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": "^0.2.0",
|
||||
"ora": "^1.1.0",
|
||||
"pageres": "^4.1.1",
|
||||
"passport": "~0.2.1",
|
||||
"passport-facebook": "2.0.0",
|
||||
"passport": "^0.3.2",
|
||||
"passport-facebook": "^2.0.0",
|
||||
"passport-google-oauth20": "1.0.0",
|
||||
"paypal-ipn": "3.0.0",
|
||||
"paypal-rest-sdk": "^1.2.1",
|
||||
"postcss-easy-import": "^1.0.1",
|
||||
"postcss-easy-import": "^2.0.0",
|
||||
"pretty-data": "^0.40.0",
|
||||
"ps-tree": "^1.0.0",
|
||||
"pug": "^2.0.0-beta6",
|
||||
"pug": "^2.0.0-beta11",
|
||||
"push-notify": "habitrpg/push-notify#v1.2.0",
|
||||
"pusher": "^1.3.0",
|
||||
"request": "~2.74.0",
|
||||
"rimraf": "^2.4.3",
|
||||
"run-sequence": "^1.1.4",
|
||||
"s3-upload-stream": "^1.0.6",
|
||||
"semantic-ui-less": "~2.2.4",
|
||||
"sass-loader": "^6.0.2",
|
||||
"serve-favicon": "^2.3.0",
|
||||
"shelljs": "^0.6.0",
|
||||
"shelljs": "^0.7.6",
|
||||
"stripe": "^4.2.0",
|
||||
"superagent": "^1.8.3",
|
||||
"superagent": "^3.4.3",
|
||||
"universal-analytics": "~0.3.2",
|
||||
"url-loader": "^0.5.7",
|
||||
"useragent": "2.1.9",
|
||||
"uuid": "^2.0.1",
|
||||
"useragent": "^2.1.9",
|
||||
"uuid": "^3.0.1",
|
||||
"validator": "^4.9.0",
|
||||
"vinyl-buffer": "^1.0.0",
|
||||
"vinyl-source-stream": "^1.1.0",
|
||||
"vue": "^2.1.0",
|
||||
"vue-hot-reload-api": "^1.2.0",
|
||||
"vue-loader": "^10.0.0",
|
||||
"vue-loader": "^11.0.0",
|
||||
"vue-mugen-scroll": "^0.2.1",
|
||||
"vue-router": "^2.0.0-rc.5",
|
||||
"vue-template-compiler": "^2.1.0",
|
||||
"webpack": "^1.12.2",
|
||||
"webpack-merge": "^0.8.3",
|
||||
"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",
|
||||
"xml2js": "^0.4.4"
|
||||
},
|
||||
@@ -139,9 +139,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": "mocha test/sanity --recursive",
|
||||
"test:common": "mocha test/common --recursive",
|
||||
"test:content": "mocha test/content --recursive",
|
||||
"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: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",
|
||||
@@ -152,37 +152,38 @@
|
||||
"sprites": "gulp sprites:compile",
|
||||
"client:dev": "node webpack/dev-server.js",
|
||||
"client:build": "node webpack/build.js",
|
||||
"client:unit": "karma start test/client/unit/karma.conf.js --single-run",
|
||||
"client:unit:watch": "karma start test/client/unit/karma.conf.js",
|
||||
"client:unit": "cross-env NODE_ENV=test karma start test/client/unit/karma.conf.js --single-run",
|
||||
"client:unit:watch": "cross-env NODE_ENV=test karma start test/client/unit/karma.conf.js",
|
||||
"client:e2e": "node test/client/e2e/runner.js",
|
||||
"client:test": "npm run client:unit && npm run client:e2e",
|
||||
"start": "gulp run:dev",
|
||||
"postinstall": "bower --config.interactive=false install -f; gulp build; npm run client:build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-plugin-istanbul": "^4.0.0",
|
||||
"chai": "^3.4.0",
|
||||
"chai-as-promised": "^5.1.0",
|
||||
"chalk": "^1.1.3",
|
||||
"chromedriver": "^2.21.2",
|
||||
"chromedriver": "^2.27.2",
|
||||
"connect-history-api-fallback": "^1.1.0",
|
||||
"coveralls": "^2.11.2",
|
||||
"cross-spawn": "^2.1.5",
|
||||
"cross-env": "^3.1.4",
|
||||
"cross-spawn": "^5.0.1",
|
||||
"csv": "~0.3.6",
|
||||
"deep-diff": "~0.1.4",
|
||||
"eslint": "^3.0.0",
|
||||
"eslint-config-habitrpg": "^2.0.0",
|
||||
"eslint-config-habitrpg": "^3.0.0",
|
||||
"eslint-friendly-formatter": "^2.0.5",
|
||||
"eslint-loader": "^1.3.0",
|
||||
"eslint-plugin-html": "^1.3.0",
|
||||
"eslint-plugin-html": "^2.0.0",
|
||||
"eslint-plugin-mocha": "^4.7.0",
|
||||
"event-stream": "^3.2.2",
|
||||
"eventsource-polyfill": "^0.9.6",
|
||||
"expect.js": "~0.2.0",
|
||||
"grunt-karma": "~0.12.1",
|
||||
"http-proxy-middleware": "^0.12.0",
|
||||
"inject-loader": "^2.0.1",
|
||||
"isparta-loader": "^2.0.0",
|
||||
"istanbul": "^0.3.14",
|
||||
"http-proxy-middleware": "^0.17.0",
|
||||
"inject-loader": "^3.0.0-beta4",
|
||||
"istanbul": "^1.1.0-alpha.1",
|
||||
"karma": "^1.3.0",
|
||||
"karma-babel-preprocessor": "^6.0.1",
|
||||
"karma-chai-plugins": "~0.6.0",
|
||||
@@ -191,27 +192,29 @@
|
||||
"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": "^1.7.0",
|
||||
"karma-webpack": "^2.0.2",
|
||||
"lcov-result-merger": "^1.0.2",
|
||||
"lolex": "^1.4.0",
|
||||
"mocha": "^2.3.3",
|
||||
"mocha": "^3.2.0",
|
||||
"mongodb": "^2.0.46",
|
||||
"mongoskin": "~2.1.0",
|
||||
"monk": "^3.1.3",
|
||||
"nightwatch": "^0.8.18",
|
||||
"monk": "^4.0.0",
|
||||
"nightwatch": "^0.9.12",
|
||||
"phantomjs-prebuilt": "^2.1.12",
|
||||
"protractor": "^3.1.1",
|
||||
"require-again": "^2.0.0",
|
||||
"rewire": "^2.3.3",
|
||||
"selenium-server": "2.53.0",
|
||||
"selenium-server": "^3.0.1",
|
||||
"sinon": "^1.17.2",
|
||||
"sinon-chai": "^2.8.0",
|
||||
"sinon-stub-promise": "^4.0.0",
|
||||
"superagent-defaults": "^0.1.13",
|
||||
"vinyl-transform": "^1.0.0",
|
||||
"webpack-dev-middleware": "^1.4.0",
|
||||
"webpack-hot-middleware": "^2.6.0"
|
||||
"webpack-bundle-analyzer": "^2.2.1",
|
||||
"webpack-dev-middleware": "^1.10.0",
|
||||
"webpack-hot-middleware": "^2.6.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +117,6 @@ describe('POST /challenges/:challengeId/leave', () => {
|
||||
});
|
||||
|
||||
expect(testTask).to.not.be.undefined;
|
||||
expect(testTask.challenge).to.eql({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import {
|
||||
TAVERN_ID,
|
||||
} from '../../../../../website/server/models/group';
|
||||
import apiMessages from '../../../../../website/server/libs/apiMessages';
|
||||
|
||||
describe('GET /groups', () => {
|
||||
let user;
|
||||
@@ -14,6 +15,7 @@ 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();
|
||||
@@ -98,6 +100,60 @@ 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,15 +63,17 @@ describe('GET /groups/:groupId/invites', () => {
|
||||
});
|
||||
|
||||
it('returns only first 30 invites', async () => {
|
||||
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
|
||||
let leader = await generateUser({balance: 4});
|
||||
let group = await generateGroup(leader, {type: 'guild', privacy: 'public', name: generateUUID()});
|
||||
|
||||
let invitesToGenerate = [];
|
||||
for (let i = 0; i < 31; i++) {
|
||||
invitesToGenerate.push(generateUser());
|
||||
}
|
||||
let generatedInvites = await Promise.all(invitesToGenerate);
|
||||
await user.post(`/groups/${group._id}/invite`, {uuids: generatedInvites.map(invite => invite._id)});
|
||||
await leader.post(`/groups/${group._id}/invite`, {uuids: generatedInvites.map(invite => invite._id)});
|
||||
|
||||
let res = await user.get('/groups/party/invites');
|
||||
let res = await leader.get(`/groups/${group._id}/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',
|
||||
'chair', 'costume', 'sleep', 'background', 'tasks',
|
||||
].sort());
|
||||
|
||||
expect(memberRes.stats.maxMP).to.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,10 +106,25 @@ 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,10 +1,12 @@
|
||||
import {
|
||||
generateUser,
|
||||
generateGroup,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
const INVITES_LIMIT = 100;
|
||||
const PARTY_LIMIT_MEMBERS = 30;
|
||||
|
||||
describe('Post /groups/:groupId/invite', () => {
|
||||
let inviter;
|
||||
@@ -12,7 +14,7 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
let groupName = 'Test Public Guild';
|
||||
|
||||
beforeEach(async () => {
|
||||
inviter = await generateUser({balance: 1});
|
||||
inviter = await generateUser({balance: 4});
|
||||
group = await inviter.post('/groups', {
|
||||
name: groupName,
|
||||
type: 'guild',
|
||||
@@ -265,6 +267,25 @@ 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', () => {
|
||||
@@ -301,6 +322,19 @@ 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();
|
||||
@@ -390,5 +424,36 @@ 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');
|
||||
});
|
||||
|
||||
xit('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}),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,4 +43,15 @@ 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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,13 +40,14 @@ describe('PUT /heroes/:heroId', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('updates contributor level, balance, ads, blocked', async () => {
|
||||
it('change contributor level, balance, ads', async () => {
|
||||
let hero = await generateUser();
|
||||
let prevBlockState = hero.auth.blocked;
|
||||
let prevSleepState = hero.preferences.sleep;
|
||||
let heroRes = await user.put(`/hall/heroes/${hero._id}`, {
|
||||
balance: 3,
|
||||
contributor: {level: 1},
|
||||
purchased: {ads: true},
|
||||
auth: {blocked: true},
|
||||
});
|
||||
|
||||
// test response
|
||||
@@ -61,18 +62,47 @@ describe('PUT /heroes/:heroId', () => {
|
||||
expect(heroRes.balance).to.equal(3 + 0.75); // 3+0.75 for first contrib level
|
||||
expect(heroRes.contributor.level).to.equal(1);
|
||||
expect(heroRes.purchased.ads).to.equal(true);
|
||||
expect(heroRes.auth.blocked).to.equal(true);
|
||||
// test hero values
|
||||
await hero.sync();
|
||||
expect(hero.balance).to.equal(3 + 0.75); // 3+0.75 for first contrib level
|
||||
expect(hero.contributor.level).to.equal(1);
|
||||
expect(hero.purchased.ads).to.equal(true);
|
||||
expect(hero.auth.blocked).to.equal(true);
|
||||
expect(hero.preferences.sleep).to.equal(true);
|
||||
expect(hero.auth.blocked).to.equal(prevBlockState);
|
||||
expect(hero.preferences.sleep).to.equal(prevSleepState);
|
||||
expect(hero.notifications.length).to.equal(1);
|
||||
expect(hero.notifications[0].type).to.equal('NEW_CONTRIBUTOR_LEVEL');
|
||||
});
|
||||
|
||||
it('block a user', async () => {
|
||||
let hero = await generateUser();
|
||||
let heroRes = await user.put(`/hall/heroes/${hero._id}`, {
|
||||
auth: {blocked: true},
|
||||
preferences: {sleep: true},
|
||||
});
|
||||
|
||||
// test response values
|
||||
expect(heroRes.auth.blocked).to.equal(true);
|
||||
// test hero values
|
||||
await hero.sync();
|
||||
expect(hero.auth.blocked).to.equal(true);
|
||||
expect(hero.preferences.sleep).to.equal(true);
|
||||
});
|
||||
|
||||
it('unblock a user', async () => {
|
||||
let hero = await generateUser();
|
||||
let prevSleepState = hero.preferences.sleep;
|
||||
let heroRes = await user.put(`/hall/heroes/${hero._id}`, {
|
||||
auth: {blocked: false},
|
||||
});
|
||||
|
||||
// test response values
|
||||
expect(heroRes.auth.blocked).to.equal(false);
|
||||
// test hero values
|
||||
await hero.sync();
|
||||
expect(hero.auth.blocked).to.equal(false);
|
||||
expect(hero.preferences.sleep).to.equal(prevSleepState);
|
||||
});
|
||||
|
||||
it('updates chatRevoked flag', async () => {
|
||||
let hero = await generateUser();
|
||||
|
||||
|
||||
@@ -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',
|
||||
'chair', 'costume', 'sleep', 'background', 'tasks',
|
||||
].sort());
|
||||
|
||||
expect(memberRes.stats.maxMP).to.exist;
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
import amzLib from '../../../../../../website/server/libs/amazonPayments';
|
||||
|
||||
describe('payments : amazon #subscribeCancel', () => {
|
||||
let endpoint = '/amazon/subscribe/cancel';
|
||||
let endpoint = '/amazon/subscribe/cancel?noRedirect=true';
|
||||
let user, group, amazonSubscribeCancelStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -22,7 +22,7 @@ describe('payments : amazon #subscribeCancel', () => {
|
||||
});
|
||||
|
||||
describe('success', () => {
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
amazonSubscribeCancelStub = sinon.stub(amzLib, 'cancelSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
@@ -66,7 +66,7 @@ describe('payments : amazon #subscribeCancel', () => {
|
||||
'purchased.plan.lastBillingDate': new Date(),
|
||||
});
|
||||
|
||||
await user.get(`${endpoint}?groupId=${group._id}`);
|
||||
await user.get(`${endpoint}&groupId=${group._id}`);
|
||||
|
||||
expect(amazonSubscribeCancelStub).to.be.calledOnce;
|
||||
expect(amazonSubscribeCancelStub.args[0][0].user._id).to.eql(user._id);
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('payments - amazon - #subscribe', () => {
|
||||
let subscription = 'basic_3mo';
|
||||
let coupon;
|
||||
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
subscribeWithAmazonStub = sinon.stub(amzLib, 'subscribe').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import {generateUser} from '../../../../../helpers/api-integration/v3';
|
||||
import applePayments from '../../../../../../website/server/libs/applePayments';
|
||||
|
||||
describe('payments : apple #cancelSubscribe', () => {
|
||||
let endpoint = '/iap/ios/subscribe/cancel?noRedirect=true';
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
describe('success', () => {
|
||||
let cancelStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
cancelStub = sinon.stub(applePayments, 'cancelSubscribe').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
applePayments.cancelSubscribe.restore();
|
||||
});
|
||||
|
||||
it('cancels the subscription', async () => {
|
||||
user = await generateUser({
|
||||
'profile.name': 'sender',
|
||||
'purchased.plan.paymentMethod': 'Apple',
|
||||
'purchased.plan.customerId': 'customer-id',
|
||||
'purchased.plan.planId': 'basic_3mo',
|
||||
'purchased.plan.lastBillingDate': new Date(),
|
||||
balance: 2,
|
||||
});
|
||||
|
||||
await user.get(endpoint);
|
||||
|
||||
expect(cancelStub).to.be.calledOnce;
|
||||
expect(cancelStub.args[0][0]._id).to.eql(user._id);
|
||||
expect(cancelStub.args[0][1]['x-api-key']).to.eql(user.apiToken);
|
||||
expect(cancelStub.args[0][1]['x-api-user']).to.eql(user._id);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import {generateUser} from '../../../../../helpers/api-integration/v3';
|
||||
import applePayments from '../../../../../../website/server/libs/applePayments';
|
||||
|
||||
describe('payments : apple #verify', () => {
|
||||
let endpoint = '/iap/ios/verify';
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
describe('success', () => {
|
||||
let verifyStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
verifyStub = sinon.stub(applePayments, 'verifyGemPurchase').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
applePayments.verifyGemPurchase.restore();
|
||||
});
|
||||
|
||||
it('makes a purchase', async () => {
|
||||
user = await generateUser({
|
||||
balance: 2,
|
||||
});
|
||||
|
||||
await user.post(endpoint, {
|
||||
transaction: {
|
||||
receipt: 'receipt',
|
||||
}});
|
||||
|
||||
expect(verifyStub).to.be.calledOnce;
|
||||
expect(verifyStub.args[0][0]._id).to.eql(user._id);
|
||||
expect(verifyStub.args[0][1]).to.eql('receipt');
|
||||
expect(verifyStub.args[0][2]['x-api-key']).to.eql(user.apiToken);
|
||||
expect(verifyStub.args[0][2]['x-api-user']).to.eql(user._id);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import {generateUser, translate as t} from '../../../../../helpers/api-integration/v3';
|
||||
import applePayments from '../../../../../../website/server/libs/applePayments';
|
||||
|
||||
describe('payments : apple #subscribe', () => {
|
||||
let endpoint = '/iap/ios/subscribe';
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('verifies sub key', async () => {
|
||||
await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('missingSubscriptionCode'),
|
||||
});
|
||||
});
|
||||
|
||||
describe('success', () => {
|
||||
let subscribeStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
subscribeStub = sinon.stub(applePayments, 'subscribe').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
applePayments.subscribe.restore();
|
||||
});
|
||||
|
||||
it('makes a purchase', async () => {
|
||||
user = await generateUser({
|
||||
'profile.name': 'sender',
|
||||
'purchased.plan.customerId': 'customer-id',
|
||||
'purchased.plan.planId': 'basic_3mo',
|
||||
'purchased.plan.lastBillingDate': new Date(),
|
||||
balance: 2,
|
||||
});
|
||||
|
||||
let sku = 'com.habitrpg.ios.habitica.subscription.3month';
|
||||
|
||||
await user.post(endpoint, {
|
||||
sku,
|
||||
receipt: 'receipt',
|
||||
});
|
||||
|
||||
expect(subscribeStub).to.be.calledOnce;
|
||||
expect(subscribeStub.args[0][0]).to.eql(sku);
|
||||
expect(subscribeStub.args[0][1]._id).to.eql(user._id);
|
||||
expect(subscribeStub.args[0][2]).to.eql('receipt');
|
||||
expect(subscribeStub.args[0][3]['x-api-key']).to.eql(user.apiToken);
|
||||
expect(subscribeStub.args[0][3]['x-api-user']).to.eql(user._id);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import {generateUser} from '../../../../../helpers/api-integration/v3';
|
||||
import googlePayments from '../../../../../../website/server/libs/googlePayments';
|
||||
|
||||
describe('payments : google #cancelSubscribe', () => {
|
||||
let endpoint = '/iap/android/subscribe/cancel?noRedirect=true';
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
describe('success', () => {
|
||||
let cancelStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
cancelStub = sinon.stub(googlePayments, 'cancelSubscribe').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
googlePayments.cancelSubscribe.restore();
|
||||
});
|
||||
|
||||
it('makes a purchase', async () => {
|
||||
user = await generateUser({
|
||||
'profile.name': 'sender',
|
||||
'purchased.plan.paymentMethod': 'Google',
|
||||
'purchased.plan.customerId': 'customer-id',
|
||||
'purchased.plan.planId': 'basic_3mo',
|
||||
'purchased.plan.lastBillingDate': new Date(),
|
||||
balance: 2,
|
||||
});
|
||||
|
||||
await user.get(endpoint);
|
||||
|
||||
expect(cancelStub).to.be.calledOnce;
|
||||
expect(cancelStub.args[0][0]._id).to.eql(user._id);
|
||||
expect(cancelStub.args[0][1]['x-api-key']).to.eql(user.apiToken);
|
||||
expect(cancelStub.args[0][1]['x-api-user']).to.eql(user._id);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import {generateUser, translate as t} from '../../../../../helpers/api-integration/v3';
|
||||
import googlePayments from '../../../../../../website/server/libs/googlePayments';
|
||||
|
||||
describe('payments : google #subscribe', () => {
|
||||
let endpoint = '/iap/android/subscribe';
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('verifies sub key', async () => {
|
||||
await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('missingSubscriptionCode'),
|
||||
});
|
||||
});
|
||||
|
||||
describe('success', () => {
|
||||
let subscribeStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
subscribeStub = sinon.stub(googlePayments, 'subscribe').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
googlePayments.subscribe.restore();
|
||||
});
|
||||
|
||||
it('makes a purchase', async () => {
|
||||
user = await generateUser({
|
||||
'profile.name': 'sender',
|
||||
'purchased.plan.customerId': 'customer-id',
|
||||
'purchased.plan.planId': 'basic_3mo',
|
||||
'purchased.plan.lastBillingDate': new Date(),
|
||||
balance: 2,
|
||||
});
|
||||
|
||||
let sku = 'com.habitrpg.android.habitica.subscription.3month';
|
||||
|
||||
await user.post(endpoint, {
|
||||
sku,
|
||||
transaction: {receipt: 'receipt', signature: 'signature'},
|
||||
});
|
||||
|
||||
expect(subscribeStub).to.be.calledOnce;
|
||||
expect(subscribeStub.args[0][0]).to.eql(sku);
|
||||
expect(subscribeStub.args[0][1]._id).to.eql(user._id);
|
||||
expect(subscribeStub.args[0][2]).to.eql('receipt');
|
||||
expect(subscribeStub.args[0][3]).to.eql('signature');
|
||||
expect(subscribeStub.args[0][4]['x-api-key']).to.eql(user.apiToken);
|
||||
expect(subscribeStub.args[0][4]['x-api-user']).to.eql(user._id);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import {generateUser} from '../../../../../helpers/api-integration/v3';
|
||||
import googlePayments from '../../../../../../website/server/libs/googlePayments';
|
||||
|
||||
describe('payments : google #verify', () => {
|
||||
let endpoint = '/iap/android/verify';
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
describe('success', () => {
|
||||
let verifyStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
verifyStub = sinon.stub(googlePayments, 'verifyGemPurchase').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
googlePayments.verifyGemPurchase.restore();
|
||||
});
|
||||
|
||||
it('makes a purchase', async () => {
|
||||
user = await generateUser({
|
||||
balance: 2,
|
||||
});
|
||||
|
||||
await user.post(endpoint, {
|
||||
transaction: {receipt: 'receipt', signature: 'signature'},
|
||||
});
|
||||
|
||||
expect(verifyStub).to.be.calledOnce;
|
||||
expect(verifyStub.args[0][0]._id).to.eql(user._id);
|
||||
expect(verifyStub.args[0][1]).to.eql('receipt');
|
||||
expect(verifyStub.args[0][2]).to.eql('signature');
|
||||
expect(verifyStub.args[0][3]['x-api-key']).to.eql(user.apiToken);
|
||||
expect(verifyStub.args[0][3]['x-api-user']).to.eql(user._id);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -31,7 +31,7 @@ describe('payments : paypal #checkout', () => {
|
||||
balance: 2,
|
||||
});
|
||||
|
||||
await user.get(endpoint);
|
||||
await user.get(`${endpoint}?noRedirect=true`);
|
||||
|
||||
expect(checkoutStub).to.be.calledOnce;
|
||||
expect(checkoutStub.args[0][0].gift).to.eql(undefined);
|
||||
|
||||
@@ -53,7 +53,7 @@ describe('payments : paypal #checkoutSuccess', () => {
|
||||
balance: 2,
|
||||
});
|
||||
|
||||
await user.get(`${endpoint}?PayerID=${customerId}&paymentId=${paymentId}`);
|
||||
await user.get(`${endpoint}?PayerID=${customerId}&paymentId=${paymentId}&noRedirect=true`);
|
||||
|
||||
expect(checkoutSuccessStub).to.be.calledOnce;
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ describe('payments : paypal #subscribe', () => {
|
||||
balance: 2,
|
||||
});
|
||||
|
||||
await user.get(`${endpoint}?sub=${subKey}`);
|
||||
await user.get(`${endpoint}?sub=${subKey}&noRedirect=true`);
|
||||
|
||||
expect(subscribeStub).to.be.calledOnce;
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ describe('payments : paypal #subscribeCancel', () => {
|
||||
balance: 2,
|
||||
});
|
||||
|
||||
await user.get(endpoint);
|
||||
await user.get(`${endpoint}?noRedirect=true`);
|
||||
|
||||
expect(subscribeCancelStub).to.be.calledOnce;
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ describe('payments : paypal #subscribeSuccess', () => {
|
||||
balance: 2,
|
||||
});
|
||||
|
||||
await user.get(`${endpoint}?token=${token}`);
|
||||
await user.get(`${endpoint}?token=${token}&noRedirect=true`);
|
||||
|
||||
expect(subscribeSuccessStub).to.be.calledOnce;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
import stripePayments from '../../../../../../website/server/libs/stripePayments';
|
||||
|
||||
describe('payments - stripe - #subscribeCancel', () => {
|
||||
let endpoint = '/stripe/subscribe/cancel';
|
||||
let endpoint = '/stripe/subscribe/cancel?redirect=none';
|
||||
let user, group, stripeCancelSubscriptionStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -39,7 +39,7 @@ describe('payments - stripe - #subscribeCancel', () => {
|
||||
balance: 2,
|
||||
});
|
||||
|
||||
await user.get(`${endpoint}?redirect=none`);
|
||||
await user.get(`${endpoint}`);
|
||||
|
||||
expect(stripeCancelSubscriptionStub).to.be.calledOnce;
|
||||
expect(stripeCancelSubscriptionStub.args[0][0].user._id).to.eql(user._id);
|
||||
@@ -64,7 +64,7 @@ describe('payments - stripe - #subscribeCancel', () => {
|
||||
'purchased.plan.lastBillingDate': new Date(),
|
||||
});
|
||||
|
||||
await user.get(`${endpoint}?groupId=${group._id}&redirect=none`);
|
||||
await user.get(`${endpoint}&groupId=${group._id}`);
|
||||
|
||||
expect(stripeCancelSubscriptionStub).to.be.calledOnce;
|
||||
expect(stripeCancelSubscriptionStub.args[0][0].user._id).to.eql(user._id);
|
||||
|
||||
@@ -43,7 +43,7 @@ describe('POST /tasks/clearCompletedTodos', () => {
|
||||
let completedTodos = await user.get('/tasks/user?type=completedTodos');
|
||||
let todos = await user.get('/tasks/user?type=todos');
|
||||
let allTodos = todos.concat(completedTodos);
|
||||
expect(allTodos.length).to.equal(initialTodoCount + 4); // + 6 - 3 completed (but one is from challenge)
|
||||
expect(allTodos[allTodos.length - 1].text).to.equal('todo 7');
|
||||
expect(allTodos.length).to.equal(initialTodoCount + 5); // + 7 - 3 completed (but one is from challenge)
|
||||
expect(allTodos[allTodos.length - 1].text).to.equal('todo 6'); // last completed todo
|
||||
});
|
||||
});
|
||||
|
||||
@@ -315,6 +315,30 @@ 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,6 +496,8 @@ describe('POST /tasks/user', () => {
|
||||
frequency: 'daily',
|
||||
everyX: 5,
|
||||
startDate: now,
|
||||
daysOfMonth: [15],
|
||||
weeksOfMonth: [3],
|
||||
});
|
||||
|
||||
expect(task.userId).to.equal(user._id);
|
||||
@@ -504,6 +506,8 @@ 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);
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
createAndPopulateGroup,
|
||||
generateGroup,
|
||||
generateUser,
|
||||
generateChallenge,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import {
|
||||
@@ -11,6 +12,10 @@ import {
|
||||
map,
|
||||
} from 'lodash';
|
||||
import Bluebird from 'bluebird';
|
||||
import {
|
||||
sha1MakeSalt,
|
||||
sha1Encrypt as sha1EncryptPassword,
|
||||
} from '../../../../../website/server/libs/password';
|
||||
|
||||
describe('DELETE /user', () => {
|
||||
let user;
|
||||
@@ -60,6 +65,30 @@ 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,
|
||||
@@ -67,6 +96,30 @@ describe('DELETE /user', () => {
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
||||
});
|
||||
|
||||
it('deletes the user with a legacy sha1 password', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let salt = sha1MakeSalt();
|
||||
let sha1HashedPassword = sha1EncryptPassword(textPassword, salt);
|
||||
|
||||
await user.update({
|
||||
'auth.local.hashed_password': sha1HashedPassword,
|
||||
'auth.local.passwordHashMethod': 'sha1',
|
||||
'auth.local.salt': salt,
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
|
||||
expect(user.auth.local.passwordHashMethod).to.equal('sha1');
|
||||
expect(user.auth.local.salt).to.equal(salt);
|
||||
expect(user.auth.local.hashed_password).to.equal(sha1HashedPassword);
|
||||
|
||||
// delete the user
|
||||
await user.del('/user', {
|
||||
password: textPassword,
|
||||
});
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
||||
});
|
||||
|
||||
context('last member of a party', () => {
|
||||
let party;
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ describe('GET /user', () => {
|
||||
let returnedUser = await user.get('/user');
|
||||
|
||||
expect(returnedUser.auth.local.hashed_password).to.not.exist;
|
||||
expect(returnedUser.auth.local.passwordHashMethod).to.not.exist;
|
||||
expect(returnedUser.auth.local.salt).to.not.exist;
|
||||
expect(returnedUser.apiToken).to.not.exist;
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
createAndPopulateGroup,
|
||||
generateGroup,
|
||||
generateChallenge,
|
||||
sleep,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
@@ -186,6 +187,40 @@ describe('POST /user/class/cast/:spellId', () => {
|
||||
expect(group.chat[0].uuid).to.equal('system');
|
||||
});
|
||||
|
||||
it('searing brightness does not affect challenge or group tasks', async () => {
|
||||
let guild = await generateGroup(user);
|
||||
let challenge = await generateChallenge(user, guild);
|
||||
await user.post(`/tasks/challenge/${challenge._id}`, {
|
||||
text: 'test challenge habit',
|
||||
type: 'habit',
|
||||
});
|
||||
|
||||
let groupTask = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'todo group',
|
||||
type: 'todo',
|
||||
});
|
||||
await user.update({'stats.class': 'healer', 'stats.mp': 200, 'stats.lvl': 15});
|
||||
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
|
||||
|
||||
await user.post('/user/class/cast/brightness');
|
||||
await user.sync();
|
||||
|
||||
let memberTasks = await user.get('/tasks/user');
|
||||
|
||||
let syncedGroupTask = find(memberTasks, function findAssignedTask (memberTask) {
|
||||
return memberTask.group.id === guild._id;
|
||||
});
|
||||
|
||||
let userChallengeTask = find(memberTasks, function findAssignedTask (memberTask) {
|
||||
return memberTask.challenge.id === challenge._id;
|
||||
});
|
||||
|
||||
expect(userChallengeTask).to.exist;
|
||||
expect(syncedGroupTask).to.exist;
|
||||
expect(userChallengeTask.value).to.equal(0);
|
||||
expect(syncedGroupTask.value).to.equal(0);
|
||||
});
|
||||
|
||||
// TODO find a way to have sinon working in integration tests
|
||||
// it doesn't work when tests are running separately from server
|
||||
it('passes correct target to spell when targetType === \'task\'');
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import {
|
||||
encrypt,
|
||||
} from '../../../../../../website/server/libs/encryption';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import superagent from 'superagent';
|
||||
import nconf from 'nconf';
|
||||
|
||||
const API_TEST_SERVER_PORT = nconf.get('PORT');
|
||||
|
||||
describe('GET /user/auth/local/reset-password-set-new-one', () => {
|
||||
let endpoint = `http://localhost:${API_TEST_SERVER_PORT}/static/user/auth/local/reset-password-set-new-one`;
|
||||
|
||||
// Tests to validate the validatePasswordResetCodeAndFindUser function
|
||||
|
||||
it('renders an error page if the code is missing', async () => {
|
||||
try {
|
||||
await superagent.get(endpoint);
|
||||
throw new Error('Request should fail.');
|
||||
} catch (err) {
|
||||
expect(err.status).to.equal(401);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders an error page if the code is invalid json', async () => {
|
||||
try {
|
||||
await superagent.get(`${endpoint}?code=invalid`);
|
||||
throw new Error('Request should fail.');
|
||||
} catch (err) {
|
||||
expect(err.status).to.equal(401);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders an error page if the code cannot be decrypted', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
try {
|
||||
let code = JSON.stringify({ // not encrypted
|
||||
userId: user._id,
|
||||
expiresAt: new Date(),
|
||||
});
|
||||
await superagent.get(`${endpoint}?code=${code}`);
|
||||
|
||||
throw new Error('Request should fail.');
|
||||
} catch (err) {
|
||||
expect(err.status).to.equal(401);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders an error page if the code is expired', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().subtract({minutes: 1}),
|
||||
}));
|
||||
await user.update({
|
||||
'auth.local.passwordResetCode': code,
|
||||
});
|
||||
|
||||
try {
|
||||
await superagent.get(`${endpoint}?code=${code}`);
|
||||
throw new Error('Request should fail.');
|
||||
} catch (err) {
|
||||
expect(err.status).to.equal(401);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders an error page if the user does not exist', async () => {
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: Date.now().toString(),
|
||||
expiresAt: moment().add({days: 1}),
|
||||
}));
|
||||
|
||||
try {
|
||||
await superagent.get(`${endpoint}?code=${code}`);
|
||||
throw new Error('Request should fail.');
|
||||
} catch (err) {
|
||||
expect(err.status).to.equal(401);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders an error page if the user has no local auth', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().add({days: 1}),
|
||||
}));
|
||||
await user.update({
|
||||
auth: 'not an object with valid fields',
|
||||
});
|
||||
|
||||
try {
|
||||
await superagent.get(`${endpoint}?code=${code}`);
|
||||
throw new Error('Request should fail.');
|
||||
} catch (err) {
|
||||
expect(err.status).to.equal(401);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders an error page if the code doesn\'t match the one saved at user.auth.passwordResetCode', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().add({days: 1}),
|
||||
}));
|
||||
await user.update({
|
||||
'auth.local.passwordResetCode': 'invalid',
|
||||
});
|
||||
|
||||
try {
|
||||
await superagent.get(`${endpoint}?code=${code}`);
|
||||
throw new Error('Request should fail.');
|
||||
} catch (err) {
|
||||
expect(err.status).to.equal(401);
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
|
||||
it('returns the password reset page if the password reset code is valid', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().add({days: 1}),
|
||||
}));
|
||||
await user.update({
|
||||
'auth.local.passwordResetCode': code,
|
||||
});
|
||||
|
||||
let res = await superagent.get(`${endpoint}?code=${code}`);
|
||||
expect(res.status).to.equal(200);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
import {
|
||||
encrypt,
|
||||
} from '../../../../../../website/server/libs/encryption';
|
||||
import {
|
||||
compare,
|
||||
bcryptCompare,
|
||||
sha1MakeSalt,
|
||||
sha1Encrypt as sha1EncryptPassword,
|
||||
} from '../../../../../../website/server/libs/password';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import superagent from 'superagent';
|
||||
import nconf from 'nconf';
|
||||
|
||||
const API_TEST_SERVER_PORT = nconf.get('PORT');
|
||||
|
||||
describe('POST /user/auth/local/reset-password-set-new-one', () => {
|
||||
let endpoint = `http://localhost:${API_TEST_SERVER_PORT}/static/user/auth/local/reset-password-set-new-one`;
|
||||
|
||||
// Tests to validate the validatePasswordResetCodeAndFindUser function
|
||||
|
||||
it('renders an error page if the code is missing', async () => {
|
||||
try {
|
||||
await superagent.post(endpoint);
|
||||
throw new Error('Request should fail.');
|
||||
} catch (err) {
|
||||
expect(err.status).to.equal(401);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders an error page if the code is invalid json', async () => {
|
||||
try {
|
||||
await superagent.post(`${endpoint}?code=invalid`);
|
||||
throw new Error('Request should fail.');
|
||||
} catch (err) {
|
||||
expect(err.status).to.equal(401);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders an error page if the code cannot be decrypted', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
try {
|
||||
let code = JSON.stringify({ // not encrypted
|
||||
userId: user._id,
|
||||
expiresAt: new Date(),
|
||||
});
|
||||
await superagent.post(`${endpoint}?code=${code}`);
|
||||
|
||||
throw new Error('Request should fail.');
|
||||
} catch (err) {
|
||||
expect(err.status).to.equal(401);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders an error page if the code is expired', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().subtract({minutes: 1}),
|
||||
}));
|
||||
await user.update({
|
||||
'auth.local.passwordResetCode': code,
|
||||
});
|
||||
|
||||
try {
|
||||
await superagent.post(`${endpoint}?code=${code}`);
|
||||
throw new Error('Request should fail.');
|
||||
} catch (err) {
|
||||
expect(err.status).to.equal(401);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders an error page if the user does not exist', async () => {
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: Date.now().toString(),
|
||||
expiresAt: moment().add({days: 1}),
|
||||
}));
|
||||
|
||||
try {
|
||||
await superagent.post(`${endpoint}?code=${code}`);
|
||||
throw new Error('Request should fail.');
|
||||
} catch (err) {
|
||||
expect(err.status).to.equal(401);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders an error page if the user has no local auth', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().add({days: 1}),
|
||||
}));
|
||||
await user.update({
|
||||
auth: 'not an object with valid fields',
|
||||
});
|
||||
|
||||
try {
|
||||
await superagent.post(`${endpoint}?code=${code}`);
|
||||
throw new Error('Request should fail.');
|
||||
} catch (err) {
|
||||
expect(err.status).to.equal(401);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders an error page if the code doesn\'t match the one saved at user.auth.passwordResetCode', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().add({days: 1}),
|
||||
}));
|
||||
await user.update({
|
||||
'auth.local.passwordResetCode': 'invalid',
|
||||
});
|
||||
|
||||
try {
|
||||
await superagent.post(`${endpoint}?code=${code}`);
|
||||
throw new Error('Request should fail.');
|
||||
} catch (err) {
|
||||
expect(err.status).to.equal(401);
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
|
||||
it('renders the error page if the new password is missing', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().add({days: 1}),
|
||||
}));
|
||||
await user.update({
|
||||
'auth.local.passwordResetCode': code,
|
||||
});
|
||||
|
||||
try {
|
||||
await superagent.post(`${endpoint}?code=${code}`);
|
||||
throw new Error('Request should fail.');
|
||||
} catch (err) {
|
||||
expect(err.status).to.equal(401);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders the error page if the password confirmation is missing', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().add({days: 1}),
|
||||
}));
|
||||
await user.update({
|
||||
'auth.local.passwordResetCode': code,
|
||||
});
|
||||
|
||||
try {
|
||||
await superagent
|
||||
.post(`${endpoint}?code=${code}`)
|
||||
.send({newPassword: 'my new password'});
|
||||
throw new Error('Request should fail.');
|
||||
} catch (err) {
|
||||
expect(err.status).to.equal(401);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders the error page if the password confirmation does not match', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().add({days: 1}),
|
||||
}));
|
||||
await user.update({
|
||||
'auth.local.passwordResetCode': code,
|
||||
});
|
||||
|
||||
try {
|
||||
await superagent
|
||||
.post(`${endpoint}?code=${code}`)
|
||||
.send({
|
||||
newPassword: 'my new password',
|
||||
confirmPassword: 'not matching',
|
||||
});
|
||||
throw new Error('Request should fail.');
|
||||
} catch (err) {
|
||||
expect(err.status).to.equal(401);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders the success page and save the user', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().add({days: 1}),
|
||||
}));
|
||||
await user.update({
|
||||
'auth.local.passwordResetCode': code,
|
||||
});
|
||||
|
||||
let res = await superagent
|
||||
.post(`${endpoint}?code=${code}`)
|
||||
.send({
|
||||
newPassword: 'my new password',
|
||||
confirmPassword: 'my new password',
|
||||
});
|
||||
|
||||
expect(res.status).to.equal(200);
|
||||
|
||||
await user.sync();
|
||||
expect(user.auth.local.passwordResetCode).to.equal(undefined);
|
||||
expect(user.auth.local.passwordHashMethod).to.equal('bcrypt');
|
||||
expect(user.auth.local.salt).to.be.undefined;
|
||||
let isPassValid = await compare(user, 'my new password');
|
||||
expect(isPassValid).to.equal(true);
|
||||
});
|
||||
|
||||
it('renders the success page and convert the password from sha1 to bcrypt', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
let textPassword = 'mySecretPassword';
|
||||
let salt = sha1MakeSalt();
|
||||
let sha1HashedPassword = sha1EncryptPassword(textPassword, salt);
|
||||
|
||||
await user.update({
|
||||
'auth.local.hashed_password': sha1HashedPassword,
|
||||
'auth.local.passwordHashMethod': 'sha1',
|
||||
'auth.local.salt': salt,
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
expect(user.auth.local.passwordHashMethod).to.equal('sha1');
|
||||
expect(user.auth.local.salt).to.equal(salt);
|
||||
expect(user.auth.local.hashed_password).to.equal(sha1HashedPassword);
|
||||
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().add({days: 1}),
|
||||
}));
|
||||
await user.update({
|
||||
'auth.local.passwordResetCode': code,
|
||||
});
|
||||
|
||||
let res = await superagent
|
||||
.post(`${endpoint}?code=${code}`)
|
||||
.send({
|
||||
newPassword: 'my new password',
|
||||
confirmPassword: 'my new password',
|
||||
});
|
||||
|
||||
expect(res.status).to.equal(200);
|
||||
|
||||
await user.sync();
|
||||
expect(user.auth.local.passwordResetCode).to.equal(undefined);
|
||||
expect(user.auth.local.passwordHashMethod).to.equal('bcrypt');
|
||||
expect(user.auth.local.salt).to.be.undefined;
|
||||
expect(user.auth.local.hashed_password).not.to.equal(sha1HashedPassword);
|
||||
|
||||
let isValidPassword = await bcryptCompare('my new password', user.auth.local.hashed_password);
|
||||
expect(isValidPassword).to.equal(true);
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,13 @@ import {
|
||||
requester,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import {
|
||||
bcryptCompare,
|
||||
sha1MakeSalt,
|
||||
sha1Encrypt as sha1EncryptPassword,
|
||||
} from '../../../../../../website/server/libs/password';
|
||||
|
||||
import nconf from 'nconf';
|
||||
|
||||
describe('POST /user/auth/local/login', () => {
|
||||
let api;
|
||||
@@ -38,7 +45,7 @@ describe('POST /user/auth/local/login', () => {
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('accountSuspended', { userId: user._id }),
|
||||
message: t('accountSuspended', { communityManagerEmail: nconf.get('EMAILS:COMMUNITY_MANAGER_EMAIL'), userId: user._id }),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -72,4 +79,35 @@ describe('POST /user/auth/local/login', () => {
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('converts user with SHA1 encrypted password to bcrypt encryption', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let salt = sha1MakeSalt();
|
||||
let sha1HashedPassword = sha1EncryptPassword(textPassword, salt);
|
||||
|
||||
await user.update({
|
||||
'auth.local.hashed_password': sha1HashedPassword,
|
||||
'auth.local.passwordHashMethod': 'sha1',
|
||||
'auth.local.salt': salt,
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
expect(user.auth.local.passwordHashMethod).to.equal('sha1');
|
||||
expect(user.auth.local.salt).to.equal(salt);
|
||||
expect(user.auth.local.hashed_password).to.equal(sha1HashedPassword);
|
||||
|
||||
// login
|
||||
await api.post(endpoint, {
|
||||
username: user.auth.local.email,
|
||||
password: textPassword,
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
expect(user.auth.local.passwordHashMethod).to.equal('bcrypt');
|
||||
expect(user.auth.local.salt).to.be.undefined;
|
||||
expect(user.auth.local.hashed_password).not.to.equal(sha1HashedPassword);
|
||||
|
||||
let isValidPassword = await bcryptCompare(textPassword, user.auth.local.hashed_password);
|
||||
expect(isValidPassword).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ 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';
|
||||
@@ -70,6 +71,23 @@ 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`;
|
||||
@@ -416,5 +434,37 @@ 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,10 @@ import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
decrypt,
|
||||
} from '../../../../../../website/server/libs/encryption';
|
||||
|
||||
describe('POST /user/reset-password', async () => {
|
||||
let endpoint = '/user/reset-password';
|
||||
@@ -35,4 +39,19 @@ describe('POST /user/reset-password', async () => {
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('sets a new password reset code on user.auth.local that expires in 1 day', async () => {
|
||||
expect(user.auth.local.passwordResetCode).to.be.undefined;
|
||||
|
||||
await user.post(endpoint, {
|
||||
email: user.auth.local.email,
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
|
||||
expect(user.auth.local.passwordResetCode).to.be.a.string;
|
||||
let decryptedCode = JSON.parse(decrypt(user.auth.local.passwordResetCode));
|
||||
expect(decryptedCode.userId).to.equal(user._id);
|
||||
expect(moment(decryptedCode.expiresAt).isAfter(moment().add({hours: 23}))).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,13 @@ import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-v3-integration.helper';
|
||||
import {
|
||||
bcryptCompare,
|
||||
sha1MakeSalt,
|
||||
sha1Encrypt as sha1EncryptPassword,
|
||||
} from '../../../../../../website/server/libs/password';
|
||||
|
||||
import nconf from 'nconf';
|
||||
|
||||
const ENDPOINT = '/user/auth/update-email';
|
||||
|
||||
@@ -63,9 +70,44 @@ describe('PUT /user/auth/update-email', () => {
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('cannotFulfillReq'),
|
||||
message: t('cannotFulfillReq', { techAssistanceEmail: nconf.get('EMAILS:TECH_ASSISTANCE_EMAIL') }),
|
||||
});
|
||||
});
|
||||
|
||||
it('converts user with SHA1 encrypted password to bcrypt encryption', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let salt = sha1MakeSalt();
|
||||
let sha1HashedPassword = sha1EncryptPassword(textPassword, salt);
|
||||
let myNewEmail = 'my-new-random-email@example.net';
|
||||
|
||||
await user.update({
|
||||
'auth.local.hashed_password': sha1HashedPassword,
|
||||
'auth.local.passwordHashMethod': 'sha1',
|
||||
'auth.local.salt': salt,
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
expect(user.auth.local.passwordHashMethod).to.equal('sha1');
|
||||
expect(user.auth.local.salt).to.equal(salt);
|
||||
expect(user.auth.local.hashed_password).to.equal(sha1HashedPassword);
|
||||
|
||||
// update email
|
||||
let response = await user.put(ENDPOINT, {
|
||||
newEmail: myNewEmail,
|
||||
password: textPassword,
|
||||
});
|
||||
expect(response).to.eql({ email: myNewEmail });
|
||||
|
||||
await user.sync();
|
||||
|
||||
expect(user.auth.local.email).to.equal(myNewEmail);
|
||||
expect(user.auth.local.passwordHashMethod).to.equal('bcrypt');
|
||||
expect(user.auth.local.salt).to.be.undefined;
|
||||
expect(user.auth.local.hashed_password).not.to.equal(sha1HashedPassword);
|
||||
|
||||
let isValidPassword = await bcryptCompare(textPassword, user.auth.local.hashed_password);
|
||||
expect(isValidPassword).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
context('Social Login User', async () => {
|
||||
|
||||
@@ -2,6 +2,11 @@ import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-v3-integration.helper';
|
||||
import {
|
||||
bcryptCompare,
|
||||
sha1MakeSalt,
|
||||
sha1Encrypt as sha1EncryptPassword,
|
||||
} from '../../../../../../website/server/libs/password';
|
||||
|
||||
const ENDPOINT = '/user/auth/update-password';
|
||||
|
||||
@@ -89,4 +94,36 @@ describe('PUT /user/auth/update-password', async () => {
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('converts user with SHA1 encrypted password to bcrypt encryption', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let salt = sha1MakeSalt();
|
||||
let sha1HashedPassword = sha1EncryptPassword(textPassword, salt);
|
||||
|
||||
await user.update({
|
||||
'auth.local.hashed_password': sha1HashedPassword,
|
||||
'auth.local.passwordHashMethod': 'sha1',
|
||||
'auth.local.salt': salt,
|
||||
});
|
||||
|
||||
user.sync();
|
||||
expect(user.auth.local.passwordHashMethod).to.equal('sha1');
|
||||
expect(user.auth.local.salt).to.equal(salt);
|
||||
expect(user.auth.local.hashed_password).to.equal(sha1HashedPassword);
|
||||
|
||||
// update email
|
||||
await user.put(ENDPOINT, {
|
||||
password: textPassword,
|
||||
newPassword,
|
||||
confirmPassword: newPassword,
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
expect(user.auth.local.passwordHashMethod).to.equal('bcrypt');
|
||||
expect(user.auth.local.salt).to.be.undefined;
|
||||
expect(user.auth.local.hashed_password).not.to.equal(sha1HashedPassword);
|
||||
|
||||
let isValidPassword = await bcryptCompare(newPassword, user.auth.local.hashed_password);
|
||||
expect(isValidPassword).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,11 @@ import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-v3-integration.helper';
|
||||
import {
|
||||
bcryptCompare,
|
||||
sha1MakeSalt,
|
||||
sha1Encrypt as sha1EncryptPassword,
|
||||
} from '../../../../../../website/server/libs/password';
|
||||
|
||||
const ENDPOINT = '/user/auth/update-username';
|
||||
|
||||
@@ -24,6 +29,41 @@ describe('PUT /user/auth/update-username', async () => {
|
||||
expect(user.auth.local.username).to.eql(newUsername);
|
||||
});
|
||||
|
||||
it('converts user with SHA1 encrypted password to bcrypt encryption', async () => {
|
||||
let myNewUsername = 'my-new-username';
|
||||
let textPassword = 'mySecretPassword';
|
||||
let salt = sha1MakeSalt();
|
||||
let sha1HashedPassword = sha1EncryptPassword(textPassword, salt);
|
||||
|
||||
await user.update({
|
||||
'auth.local.hashed_password': sha1HashedPassword,
|
||||
'auth.local.passwordHashMethod': 'sha1',
|
||||
'auth.local.salt': salt,
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
expect(user.auth.local.passwordHashMethod).to.equal('sha1');
|
||||
expect(user.auth.local.salt).to.equal(salt);
|
||||
expect(user.auth.local.hashed_password).to.equal(sha1HashedPassword);
|
||||
|
||||
// update email
|
||||
let response = await user.put(ENDPOINT, {
|
||||
username: myNewUsername,
|
||||
password: textPassword,
|
||||
});
|
||||
expect(response).to.eql({ username: myNewUsername });
|
||||
|
||||
await user.sync();
|
||||
|
||||
expect(user.auth.local.username).to.eql(myNewUsername);
|
||||
expect(user.auth.local.passwordHashMethod).to.equal('bcrypt');
|
||||
expect(user.auth.local.salt).to.be.undefined;
|
||||
expect(user.auth.local.hashed_password).not.to.equal(sha1HashedPassword);
|
||||
|
||||
let isValidPassword = await bcryptCompare(textPassword, user.auth.local.hashed_password);
|
||||
expect(isValidPassword).to.equal(true);
|
||||
});
|
||||
|
||||
context('errors', async () => {
|
||||
it('prevents username update if new username is already taken', async () => {
|
||||
let existingUsername = 'existing-username';
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
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';
|
||||
@@ -105,7 +107,7 @@ describe('Amazon Payments', () => {
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
headers,
|
||||
});
|
||||
expectAmazonStubs();
|
||||
@@ -128,7 +130,7 @@ describe('Amazon Payments', () => {
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON_GIFT,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD_GIFT,
|
||||
headers,
|
||||
gift,
|
||||
});
|
||||
@@ -153,7 +155,7 @@ describe('Amazon Payments', () => {
|
||||
expect(paymentCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON_GIFT,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD_GIFT,
|
||||
headers,
|
||||
gift,
|
||||
});
|
||||
@@ -316,7 +318,7 @@ describe('Amazon Payments', () => {
|
||||
expect(createSubSpy).to.be.calledWith({
|
||||
user,
|
||||
customerId: billingAgreementId,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
sub,
|
||||
headers,
|
||||
groupId,
|
||||
@@ -375,7 +377,73 @@ describe('Amazon Payments', () => {
|
||||
expect(createSubSpy).to.be.calledWith({
|
||||
user,
|
||||
customerId: billingAgreementId,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
|
||||
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,
|
||||
sub,
|
||||
headers,
|
||||
groupId,
|
||||
@@ -455,7 +523,7 @@ describe('Amazon Payments', () => {
|
||||
user,
|
||||
groupId: undefined,
|
||||
nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
headers,
|
||||
});
|
||||
expectAmazonStubs();
|
||||
@@ -485,7 +553,7 @@ describe('Amazon Payments', () => {
|
||||
user,
|
||||
groupId: undefined,
|
||||
nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
headers,
|
||||
});
|
||||
amzLib.closeBillingAgreement.restore();
|
||||
@@ -523,7 +591,7 @@ describe('Amazon Payments', () => {
|
||||
user,
|
||||
groupId: group._id,
|
||||
nextBill: moment(group.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
headers,
|
||||
});
|
||||
expectAmazonStubs();
|
||||
@@ -553,10 +621,84 @@ describe('Amazon Payments', () => {
|
||||
user,
|
||||
groupId: group._id,
|
||||
nextBill: moment(group.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
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
test/api/v3/unit/libs/apiMessages.js
Normal file
31
test/api/v3/unit/libs/apiMessages.js
Normal file
@@ -0,0 +1,31 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
258
test/api/v3/unit/libs/applePayments.test.js
Normal file
258
test/api/v3/unit/libs/applePayments.test.js
Normal file
@@ -0,0 +1,258 @@
|
||||
/* eslint-disable camelcase */
|
||||
import iapModule from '../../../../../website/server/libs/inAppPurchases';
|
||||
import payments from '../../../../../website/server/libs/payments';
|
||||
import applePayments from '../../../../../website/server/libs/applePayments';
|
||||
import iap from '../../../../../website/server/libs/inAppPurchases';
|
||||
import {model as User} from '../../../../../website/server/models/user';
|
||||
import common from '../../../../../website/common';
|
||||
import moment from 'moment';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('Apple Payments', () => {
|
||||
let subKey = 'basic_3mo';
|
||||
|
||||
describe('verifyGemPurchase', () => {
|
||||
let sku, user, token, receipt, headers;
|
||||
let iapSetupStub, iapValidateStub, iapIsValidatedStub, paymentBuyGemsStub, iapGetPurchaseDataStub;
|
||||
|
||||
beforeEach(() => {
|
||||
token = 'testToken';
|
||||
sku = 'com.habitrpg.ios.habitica.iap.21gems';
|
||||
user = new User();
|
||||
receipt = `{"token": "${token}", "productId": "${sku}"}`;
|
||||
headers = {};
|
||||
|
||||
iapSetupStub = sinon.stub(iapModule, 'setup')
|
||||
.returnsPromise().resolves();
|
||||
iapValidateStub = sinon.stub(iapModule, 'validate')
|
||||
.returnsPromise().resolves({});
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(true);
|
||||
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
|
||||
.returns([{productId: 'com.habitrpg.ios.Habitica.21gems',
|
||||
transactionId: token,
|
||||
}]);
|
||||
paymentBuyGemsStub = sinon.stub(payments, 'buyGems').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iapModule.setup.restore();
|
||||
iapModule.validate.restore();
|
||||
iapModule.isValidated.restore();
|
||||
iapModule.getPurchaseData.restore();
|
||||
payments.buyGems.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if receipt is invalid', async () => {
|
||||
iapModule.isValidated.restore();
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(false);
|
||||
|
||||
await expect(applePayments.verifyGemPurchase(user, receipt, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: applePayments.constants.RESPONSE_INVALID_RECEIPT,
|
||||
});
|
||||
});
|
||||
|
||||
it('purchases gems', async () => {
|
||||
await applePayments.verifyGemPurchase(user, receipt, headers);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt);
|
||||
expect(iapIsValidatedStub).to.be.calledOnce;
|
||||
expect(iapIsValidatedStub).to.be.calledWith({});
|
||||
expect(iapGetPurchaseDataStub).to.be.calledOnce;
|
||||
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
|
||||
amount: 5.25,
|
||||
headers,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscribe', () => {
|
||||
let sub, sku, user, token, receipt, headers, nextPaymentProcessing;
|
||||
let iapSetupStub, iapValidateStub, iapIsValidatedStub, paymentsCreateSubscritionStub, iapGetPurchaseDataStub;
|
||||
|
||||
beforeEach(() => {
|
||||
sub = common.content.subscriptionBlocks[subKey];
|
||||
sku = 'com.habitrpg.ios.habitica.subscription.3month';
|
||||
|
||||
token = 'test-token';
|
||||
headers = {};
|
||||
receipt = `{"token": "${token}"}`;
|
||||
nextPaymentProcessing = moment.utc().add({days: 2});
|
||||
|
||||
iapSetupStub = sinon.stub(iapModule, 'setup')
|
||||
.returnsPromise().resolves();
|
||||
iapValidateStub = sinon.stub(iapModule, 'validate')
|
||||
.returnsPromise().resolves({});
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(true);
|
||||
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
|
||||
.returns([{
|
||||
expirationDate: moment.utc().subtract({day: 1}).toDate(),
|
||||
productId: sku,
|
||||
transactionId: token,
|
||||
}, {
|
||||
expirationDate: moment.utc().add({day: 1}).toDate(),
|
||||
productId: 'wrongsku',
|
||||
transactionId: token,
|
||||
}, {
|
||||
expirationDate: moment.utc().add({day: 1}).toDate(),
|
||||
productId: sku,
|
||||
transactionId: token,
|
||||
}]);
|
||||
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iapModule.setup.restore();
|
||||
iapModule.validate.restore();
|
||||
iapModule.isValidated.restore();
|
||||
iapModule.getPurchaseData.restore();
|
||||
payments.createSubscription.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if receipt is invalid', async () => {
|
||||
iapModule.isValidated.restore();
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(false);
|
||||
|
||||
await expect(applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: applePayments.constants.RESPONSE_INVALID_RECEIPT,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a user subscription', async () => {
|
||||
await applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt);
|
||||
expect(iapIsValidatedStub).to.be.calledOnce;
|
||||
expect(iapIsValidatedStub).to.be.calledWith({});
|
||||
expect(iapGetPurchaseDataStub).to.be.calledOnce;
|
||||
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
customerId: token,
|
||||
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
|
||||
sub,
|
||||
headers,
|
||||
additionalData: receipt,
|
||||
nextPaymentProcessing,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancelSubscribe ', () => {
|
||||
let user, token, receipt, headers, customerId, expirationDate;
|
||||
let iapSetupStub, iapValidateStub, iapIsValidatedStub, iapGetPurchaseDataStub, paymentCancelSubscriptionSpy;
|
||||
|
||||
beforeEach(async () => {
|
||||
token = 'test-token';
|
||||
headers = {};
|
||||
receipt = `{"token": "${token}"}`;
|
||||
customerId = 'test-customerId';
|
||||
expirationDate = moment.utc();
|
||||
|
||||
iapSetupStub = sinon.stub(iapModule, 'setup')
|
||||
.returnsPromise().resolves();
|
||||
iapValidateStub = sinon.stub(iapModule, 'validate')
|
||||
.returnsPromise().resolves({
|
||||
expirationDate,
|
||||
});
|
||||
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
|
||||
.returns([{expirationDate: expirationDate.toDate()}]);
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(true);
|
||||
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE;
|
||||
user.purchased.plan.customerId = customerId;
|
||||
user.purchased.plan.planId = subKey;
|
||||
user.purchased.plan.additionalData = receipt;
|
||||
|
||||
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
iapModule.setup.restore();
|
||||
iapModule.validate.restore();
|
||||
iapModule.isValidated.restore();
|
||||
iapModule.getPurchaseData.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if we are missing a subscription', async () => {
|
||||
user.purchased.plan.paymentMethod = undefined;
|
||||
|
||||
await expect(applePayments.cancelSubscribe(user, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('missingSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if subscription is still valid', async () => {
|
||||
iapModule.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
|
||||
.returns([{expirationDate: expirationDate.add({day: 1}).toDate()}]);
|
||||
|
||||
await expect(applePayments.cancelSubscribe(user, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: applePayments.constants.RESPONSE_STILL_VALID,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if receipt is invalid', async () => {
|
||||
iapModule.isValidated.restore();
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(false);
|
||||
|
||||
await expect(applePayments.cancelSubscribe(user, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: applePayments.constants.RESPONSE_INVALID_RECEIPT,
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel a user subscription', async () => {
|
||||
await applePayments.cancelSubscribe(user, headers);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt);
|
||||
expect(iapIsValidatedStub).to.be.calledOnce;
|
||||
expect(iapIsValidatedStub).to.be.calledWith({
|
||||
expirationDate,
|
||||
});
|
||||
expect(iapGetPurchaseDataStub).to.be.calledOnce;
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
|
||||
nextBill: expirationDate.toDate(),
|
||||
headers,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,6 @@ 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';
|
||||
|
||||
@@ -135,7 +134,10 @@ 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);
|
||||
});
|
||||
|
||||
@@ -481,6 +483,67 @@ 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', () => {
|
||||
@@ -533,7 +596,7 @@ describe('cron', () => {
|
||||
tasksByType.dailys[0].completed = true;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
|
||||
|
||||
let previousBuffs = clone(user.stats.buffs);
|
||||
let previousBuffs = user.stats.buffs.toObject();
|
||||
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
|
||||
@@ -598,7 +661,7 @@ describe('cron', () => {
|
||||
tasksByType.dailys[0].completed = false;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
|
||||
|
||||
let previousBuffs = clone(user.stats.buffs);
|
||||
let previousBuffs = user.stats.buffs.toObject();
|
||||
|
||||
cronOverride({user, tasksByType, daysMissed, analytics});
|
||||
|
||||
@@ -975,19 +1038,18 @@ describe('recoverCron', () => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('throws an error if user cannot be found', async (done) => {
|
||||
it('throws an error if user cannot be found', async () => {
|
||||
execStub.returns(Bluebird.resolve(null));
|
||||
|
||||
try {
|
||||
await recoverCron(status, locals);
|
||||
throw new Error('no exception when user cannot be found');
|
||||
} catch (err) {
|
||||
expect(err.message).to.eql(`User ${locals.user._id} not found while recovering.`);
|
||||
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('increases status.times count and reruns up to 4 times', async (done) => {
|
||||
it('increases status.times count and reruns up to 4 times', async () => {
|
||||
execStub.returns(Bluebird.resolve({_cronSignature: 'RUNNING_CRON'}));
|
||||
execStub.onCall(4).returns(Bluebird.resolve({_cronSignature: 'NOT_RUNNING'}));
|
||||
|
||||
@@ -995,20 +1057,17 @@ describe('recoverCron', () => {
|
||||
|
||||
expect(status.times).to.eql(4);
|
||||
expect(locals.user).to.eql({_cronSignature: 'NOT_RUNNING'});
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('throws an error if recoverCron runs 5 times', async (done) => {
|
||||
it('throws an error if recoverCron runs 5 times', async () => {
|
||||
execStub.returns(Bluebird.resolve({_cronSignature: 'RUNNING_CRON'}));
|
||||
|
||||
try {
|
||||
await recoverCron(status, locals);
|
||||
throw new Error('no exception when recoverCron runs 5 times');
|
||||
} catch (err) {
|
||||
expect(status.times).to.eql(5);
|
||||
expect(err.message).to.eql(`Impossible to recover from cron for user ${locals.user._id}.`);
|
||||
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
269
test/api/v3/unit/libs/googlePayments.test.js
Normal file
269
test/api/v3/unit/libs/googlePayments.test.js
Normal file
@@ -0,0 +1,269 @@
|
||||
/* eslint-disable camelcase */
|
||||
import iapModule from '../../../../../website/server/libs/inAppPurchases';
|
||||
import payments from '../../../../../website/server/libs/payments';
|
||||
import googlePayments from '../../../../../website/server/libs/googlePayments';
|
||||
import iap from '../../../../../website/server/libs/inAppPurchases';
|
||||
import {model as User} from '../../../../../website/server/models/user';
|
||||
import common from '../../../../../website/common';
|
||||
import moment from 'moment';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('Google Payments', () => {
|
||||
let subKey = 'basic_3mo';
|
||||
|
||||
describe('verifyGemPurchase', () => {
|
||||
let sku, user, token, receipt, signature, headers;
|
||||
let iapSetupStub, iapValidateStub, iapIsValidatedStub, paymentBuyGemsStub;
|
||||
|
||||
beforeEach(() => {
|
||||
sku = 'com.habitrpg.android.habitica.iap.21gems';
|
||||
user = new User();
|
||||
receipt = `{"token": "${token}", "productId": "${sku}"}`;
|
||||
signature = '';
|
||||
headers = {};
|
||||
|
||||
iapSetupStub = sinon.stub(iapModule, 'setup')
|
||||
.returnsPromise().resolves();
|
||||
iapValidateStub = sinon.stub(iapModule, 'validate')
|
||||
.returnsPromise().resolves({});
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(true);
|
||||
paymentBuyGemsStub = sinon.stub(payments, 'buyGems').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iapModule.setup.restore();
|
||||
iapModule.validate.restore();
|
||||
iapModule.isValidated.restore();
|
||||
payments.buyGems.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if receipt is invalid', async () => {
|
||||
iapModule.isValidated.restore();
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(false);
|
||||
|
||||
await expect(googlePayments.verifyGemPurchase(user, receipt, signature, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: googlePayments.constants.RESPONSE_INVALID_RECEIPT,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if productId is invalid', async () => {
|
||||
receipt = `{"token": "${token}", "productId": "invalid"}`;
|
||||
|
||||
await expect(googlePayments.verifyGemPurchase(user, receipt, signature, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: googlePayments.constants.RESPONSE_INVALID_ITEM,
|
||||
});
|
||||
});
|
||||
|
||||
it('purchases gems', async () => {
|
||||
await googlePayments.verifyGemPurchase(user, receipt, signature, headers);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledWith(iap.GOOGLE, {
|
||||
data: receipt,
|
||||
signature,
|
||||
});
|
||||
expect(iapIsValidatedStub).to.be.calledOnce;
|
||||
expect(iapIsValidatedStub).to.be.calledWith({});
|
||||
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: googlePayments.constants.PAYMENT_METHOD_GOOGLE,
|
||||
amount: 5.25,
|
||||
headers,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscribe', () => {
|
||||
let sub, sku, user, token, receipt, signature, headers, nextPaymentProcessing;
|
||||
let iapSetupStub, iapValidateStub, iapIsValidatedStub, paymentsCreateSubscritionStub;
|
||||
|
||||
beforeEach(() => {
|
||||
sub = common.content.subscriptionBlocks[subKey];
|
||||
sku = 'com.habitrpg.android.habitica.subscription.3month';
|
||||
|
||||
token = 'test-token';
|
||||
headers = {};
|
||||
receipt = `{"token": "${token}"}`;
|
||||
signature = '';
|
||||
nextPaymentProcessing = moment.utc().add({days: 2});
|
||||
|
||||
iapSetupStub = sinon.stub(iapModule, 'setup')
|
||||
.returnsPromise().resolves();
|
||||
iapValidateStub = sinon.stub(iapModule, 'validate')
|
||||
.returnsPromise().resolves({});
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(true);
|
||||
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iapModule.setup.restore();
|
||||
iapModule.validate.restore();
|
||||
iapModule.isValidated.restore();
|
||||
payments.createSubscription.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if receipt is invalid', async () => {
|
||||
iapModule.isValidated.restore();
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(false);
|
||||
|
||||
await expect(googlePayments.subscribe(sku, user, receipt, signature, headers, nextPaymentProcessing))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: googlePayments.constants.RESPONSE_INVALID_RECEIPT,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if sku is invalid', async () => {
|
||||
sku = 'invalid';
|
||||
|
||||
await expect(googlePayments.subscribe(sku, user, receipt, signature, headers, nextPaymentProcessing))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: googlePayments.constants.RESPONSE_INVALID_ITEM,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a user subscription', async () => {
|
||||
await googlePayments.subscribe(sku, user, receipt, signature, headers, nextPaymentProcessing);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledWith(iap.GOOGLE, {
|
||||
data: receipt,
|
||||
signature,
|
||||
});
|
||||
expect(iapIsValidatedStub).to.be.calledOnce;
|
||||
expect(iapIsValidatedStub).to.be.calledWith({});
|
||||
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
customerId: token,
|
||||
paymentMethod: googlePayments.constants.PAYMENT_METHOD_GOOGLE,
|
||||
sub,
|
||||
headers,
|
||||
additionalData: {data: receipt, signature},
|
||||
nextPaymentProcessing,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancelSubscribe ', () => {
|
||||
let user, token, receipt, signature, headers, customerId, expirationDate;
|
||||
let iapSetupStub, iapValidateStub, iapIsValidatedStub, iapGetPurchaseDataStub, paymentCancelSubscriptionSpy;
|
||||
|
||||
beforeEach(async () => {
|
||||
token = 'test-token';
|
||||
headers = {};
|
||||
receipt = `{"token": "${token}"}`;
|
||||
signature = '';
|
||||
customerId = 'test-customerId';
|
||||
expirationDate = moment.utc();
|
||||
|
||||
iapSetupStub = sinon.stub(iapModule, 'setup')
|
||||
.returnsPromise().resolves();
|
||||
iapValidateStub = sinon.stub(iapModule, 'validate')
|
||||
.returnsPromise().resolves({
|
||||
expirationDate,
|
||||
});
|
||||
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
|
||||
.returns([{expirationDate: expirationDate.toDate()}]);
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(true);
|
||||
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.customerId = customerId;
|
||||
user.purchased.plan.paymentMethod = googlePayments.constants.PAYMENT_METHOD_GOOGLE;
|
||||
user.purchased.plan.planId = subKey;
|
||||
user.purchased.plan.additionalData = {data: receipt, signature};
|
||||
|
||||
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
iapModule.setup.restore();
|
||||
iapModule.validate.restore();
|
||||
iapModule.isValidated.restore();
|
||||
iapModule.getPurchaseData.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if we are missing a subscription', async () => {
|
||||
user.purchased.plan.paymentMethod = undefined;
|
||||
|
||||
await expect(googlePayments.cancelSubscribe(user, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('missingSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if subscription is still valid', async () => {
|
||||
iapModule.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
|
||||
.returns([{expirationDate: expirationDate.add({day: 1}).toDate()}]);
|
||||
|
||||
await expect(googlePayments.cancelSubscribe(user, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: googlePayments.constants.RESPONSE_STILL_VALID,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if receipt is invalid', async () => {
|
||||
iapModule.isValidated.restore();
|
||||
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
|
||||
.returns(false);
|
||||
|
||||
await expect(googlePayments.cancelSubscribe(user, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: googlePayments.constants.RESPONSE_INVALID_RECEIPT,
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel a user subscription', async () => {
|
||||
await googlePayments.cancelSubscribe(user, headers);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledWith(iap.GOOGLE, {
|
||||
data: receipt,
|
||||
signature,
|
||||
});
|
||||
expect(iapIsValidatedStub).to.be.calledOnce;
|
||||
expect(iapIsValidatedStub).to.be.calledWith({
|
||||
expirationDate,
|
||||
});
|
||||
expect(iapGetPurchaseDataStub).to.be.calledOnce;
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: googlePayments.constants.PAYMENT_METHOD_GOOGLE,
|
||||
nextBill: expirationDate.toDate(),
|
||||
headers,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,41 +1,337 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import {
|
||||
encrypt as encryptPassword,
|
||||
makeSalt,
|
||||
encrypt,
|
||||
} from '../../../../../website/server/libs/encryption';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import {
|
||||
sha1Encrypt as sha1EncryptPassword,
|
||||
sha1MakeSalt,
|
||||
bcryptHash,
|
||||
bcryptCompare,
|
||||
compare,
|
||||
convertToBcrypt,
|
||||
validatePasswordResetCodeAndFindUser,
|
||||
} from '../../../../../website/server/libs/password';
|
||||
|
||||
describe('Password Utilities', () => {
|
||||
describe('Encrypt', () => {
|
||||
it('always encrypt the same password to the same value when using the same salt', () => {
|
||||
describe('compare', () => {
|
||||
it('can compare a correct password hashed with SHA1', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let salt = makeSalt();
|
||||
let encryptedPassword = encryptPassword(textPassword, salt);
|
||||
let salt = sha1MakeSalt();
|
||||
let hashedPassword = sha1EncryptPassword(textPassword, salt);
|
||||
|
||||
expect(encryptPassword(textPassword, salt)).to.eql(encryptedPassword);
|
||||
let user = {
|
||||
auth: {
|
||||
local: {
|
||||
hashed_password: hashedPassword,
|
||||
salt,
|
||||
passwordHashMethod: 'sha1',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let isValidPassword = await compare(user, textPassword);
|
||||
expect(isValidPassword).to.eql(true);
|
||||
});
|
||||
|
||||
it('never encrypt the same password to the same value when using a different salt', () => {
|
||||
it('can compare an invalid password hashed with SHA1', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let aSalt = makeSalt();
|
||||
let anotherSalt = makeSalt();
|
||||
let anEncryptedPassword = encryptPassword(textPassword, aSalt);
|
||||
let anotherEncryptedPassword = encryptPassword(textPassword, anotherSalt);
|
||||
let salt = sha1MakeSalt();
|
||||
let hashedPassword = sha1EncryptPassword(textPassword, salt);
|
||||
|
||||
expect(anEncryptedPassword).not.to.eql(anotherEncryptedPassword);
|
||||
let user = {
|
||||
auth: {
|
||||
local: {
|
||||
hashed_password: hashedPassword,
|
||||
salt,
|
||||
passwordHashMethod: 'sha1',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let isValidPassword = await compare(user, 'wrongPassword');
|
||||
expect(isValidPassword).to.eql(false);
|
||||
});
|
||||
|
||||
it('can compare a correct password hashed with bcrypt', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let hashedPassword = await bcryptHash(textPassword);
|
||||
|
||||
let user = {
|
||||
auth: {
|
||||
local: {
|
||||
hashed_password: hashedPassword,
|
||||
passwordHashMethod: 'bcrypt',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let isValidPassword = await compare(user, textPassword);
|
||||
expect(isValidPassword).to.eql(true);
|
||||
});
|
||||
|
||||
it('can compare an invalid password hashed with bcrypt', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let hashedPassword = await bcryptHash(textPassword);
|
||||
|
||||
let user = {
|
||||
auth: {
|
||||
local: {
|
||||
hashed_password: hashedPassword,
|
||||
passwordHashMethod: 'bcrypt',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let isValidPassword = await compare(user, 'wrongPassword');
|
||||
expect(isValidPassword).to.eql(false);
|
||||
});
|
||||
|
||||
it('throws an error if user is missing', async () => {
|
||||
try {
|
||||
await compare(null, 'some password');
|
||||
} catch (e) {
|
||||
expect(e.toString()).to.equal('Error: user and passwordToCheck are required parameters.');
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if passwordToCheck is missing', async () => {
|
||||
try {
|
||||
await compare({a: true});
|
||||
} catch (e) {
|
||||
expect(e.toString()).to.equal('Error: user and passwordToCheck are required parameters.');
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if an invalid hashing method is used', async () => {
|
||||
try {
|
||||
await compare({
|
||||
auth: {
|
||||
local: {
|
||||
passwordHashMethod: 'invalid',
|
||||
},
|
||||
},
|
||||
}, 'pass');
|
||||
} catch (e) {
|
||||
expect(e.toString()).to.equal('Error: Invalid password hash method.');
|
||||
}
|
||||
});
|
||||
|
||||
it('returns true if comparing the same password', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let hashedPassword = await bcryptHash(textPassword);
|
||||
|
||||
let isValidPassword = await bcryptCompare(textPassword, hashedPassword);
|
||||
expect(isValidPassword).to.eql(true);
|
||||
});
|
||||
|
||||
it('returns true if comparing a different password', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let hashedPassword = await bcryptHash(textPassword);
|
||||
|
||||
let isValidPassword = await bcryptCompare('anotherPassword', hashedPassword);
|
||||
expect(isValidPassword).to.eql(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Make Salt', () => {
|
||||
it('creates a salt with length 10 by default', () => {
|
||||
let salt = makeSalt();
|
||||
describe('convertToBcrypt', () => {
|
||||
it('converts an user password hashed with sha1 to bcrypt', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let salt = sha1MakeSalt();
|
||||
let hashedPassword = sha1EncryptPassword(textPassword, salt);
|
||||
|
||||
expect(salt.length).to.eql(10);
|
||||
let user = {
|
||||
auth: {
|
||||
local: {
|
||||
hashed_password: hashedPassword,
|
||||
salt,
|
||||
passwordHashMethod: 'sha1',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await convertToBcrypt(user, textPassword);
|
||||
expect(user.auth.local.salt).to.be.undefined;
|
||||
expect(user.auth.local.passwordHashMethod).to.equal('bcrypt');
|
||||
expect(user.auth.local.hashed_password).to.be.a.string;
|
||||
|
||||
let isValidPassword = await compare(user, textPassword);
|
||||
expect(isValidPassword).to.eql(true);
|
||||
});
|
||||
|
||||
it('can create a salt of any length', () => {
|
||||
let length = 24;
|
||||
let salt = makeSalt(length);
|
||||
it('throws an error if user is missing', async () => {
|
||||
try {
|
||||
await convertToBcrypt(null, 'string');
|
||||
} catch (e) {
|
||||
expect(e.toString()).to.equal('Error: user and plainTextPassword are required parameters.');
|
||||
}
|
||||
});
|
||||
|
||||
expect(salt.length).to.eql(length);
|
||||
it('throws an error if plainTextPassword is missing', async () => {
|
||||
try {
|
||||
await convertToBcrypt({a: true});
|
||||
} catch (e) {
|
||||
expect(e.toString()).to.equal('Error: user and plainTextPassword are required parameters.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('validatePasswordResetCodeAndFindUser', () => {
|
||||
it('returns false if the code is missing', async () => {
|
||||
let res = await validatePasswordResetCodeAndFindUser();
|
||||
expect(res).to.equal(false);
|
||||
});
|
||||
|
||||
it('returns false if the code is invalid json', async () => {
|
||||
let res = await validatePasswordResetCodeAndFindUser('invalid json');
|
||||
expect(res).to.equal(false);
|
||||
});
|
||||
|
||||
it('returns false if the code cannot be decrypted', async () => {
|
||||
let user = await generateUser();
|
||||
let res = await validatePasswordResetCodeAndFindUser(JSON.stringify({ // not encrypted
|
||||
userId: user._id,
|
||||
expiresAt: new Date(),
|
||||
}));
|
||||
expect(res).to.equal(false);
|
||||
});
|
||||
|
||||
it('returns false if the code is expired', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().subtract({minutes: 1}),
|
||||
}));
|
||||
|
||||
await user.update({
|
||||
'auth.local.passwordResetCode': code,
|
||||
});
|
||||
|
||||
let res = await validatePasswordResetCodeAndFindUser(code);
|
||||
expect(res).to.equal(false);
|
||||
});
|
||||
|
||||
it('returns false if the user does not exist', async () => {
|
||||
let res = await validatePasswordResetCodeAndFindUser(encrypt(JSON.stringify({
|
||||
userId: Date.now().toString(),
|
||||
expiresAt: moment().add({days: 1}),
|
||||
})));
|
||||
expect(res).to.equal(false);
|
||||
});
|
||||
|
||||
it('returns false if the user has no local auth', async () => {
|
||||
let user = await generateUser({
|
||||
auth: 'not an object with valid fields',
|
||||
});
|
||||
let res = await validatePasswordResetCodeAndFindUser(encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().add({days: 1}),
|
||||
})));
|
||||
expect(res).to.equal(false);
|
||||
});
|
||||
|
||||
it('returns false if the code doesn\'t match the one saved at user.auth.passwordResetCode', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().add({days: 1}),
|
||||
}));
|
||||
|
||||
await user.update({
|
||||
'auth.local.passwordResetCode': 'invalid',
|
||||
});
|
||||
|
||||
let res = await validatePasswordResetCodeAndFindUser(code);
|
||||
expect(res).to.equal(false);
|
||||
});
|
||||
|
||||
it('returns the user if the password reset code is valid', async () => {
|
||||
let user = await generateUser();
|
||||
|
||||
let code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().add({days: 1}),
|
||||
}));
|
||||
|
||||
await user.update({
|
||||
'auth.local.passwordResetCode': code,
|
||||
});
|
||||
|
||||
let res = await validatePasswordResetCodeAndFindUser(code);
|
||||
expect(res).not.to.equal(false);
|
||||
expect(res._id).to.equal(user._id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bcrypt', () => {
|
||||
describe('Hash', () => {
|
||||
it('returns a hashed string', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let hashedPassword = await bcryptHash(textPassword);
|
||||
|
||||
expect(hashedPassword).to.be.a.string;
|
||||
});
|
||||
});
|
||||
|
||||
describe('Compare', () => {
|
||||
it('returns true if comparing the same password', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let hashedPassword = await bcryptHash(textPassword);
|
||||
|
||||
let isValidPassword = await bcryptCompare(textPassword, hashedPassword);
|
||||
expect(isValidPassword).to.eql(true);
|
||||
});
|
||||
|
||||
it('returns true if comparing a different password', async () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let hashedPassword = await bcryptHash(textPassword);
|
||||
|
||||
let isValidPassword = await bcryptCompare('anotherPassword', hashedPassword);
|
||||
expect(isValidPassword).to.eql(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('SHA1', () => {
|
||||
describe('Encrypt', () => {
|
||||
it('always encrypt the same password to the same value when using the same salt', () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let salt = sha1MakeSalt();
|
||||
let encryptedPassword = sha1EncryptPassword(textPassword, salt);
|
||||
|
||||
expect(sha1EncryptPassword(textPassword, salt)).to.eql(encryptedPassword);
|
||||
});
|
||||
|
||||
it('never encrypt the same password to the same value when using a different salt', () => {
|
||||
let textPassword = 'mySecretPassword';
|
||||
let aSalt = sha1MakeSalt();
|
||||
let anotherSalt = sha1MakeSalt();
|
||||
let anEncryptedPassword = sha1EncryptPassword(textPassword, aSalt);
|
||||
let anotherEncryptedPassword = sha1EncryptPassword(textPassword, anotherSalt);
|
||||
|
||||
expect(anEncryptedPassword).not.to.eql(anotherEncryptedPassword);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Make Salt', () => {
|
||||
it('creates a salt with length 10 by default', () => {
|
||||
let salt = sha1MakeSalt();
|
||||
|
||||
expect(salt.length).to.eql(10);
|
||||
});
|
||||
|
||||
it('can create a salt of any length', () => {
|
||||
let length = 24;
|
||||
let salt = sha1MakeSalt(length);
|
||||
|
||||
expect(salt.length).to.eql(length);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
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';
|
||||
@@ -319,53 +315,6 @@ 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);
|
||||
@@ -485,7 +434,6 @@ 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;
|
||||
@@ -559,112 +507,6 @@ 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', () => {
|
||||
@@ -772,49 +614,24 @@ describe('payments/index', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#upgradeGroupPlan', () => {
|
||||
let spy;
|
||||
|
||||
beforeEach(function () {
|
||||
spy = sinon.stub(stripe.subscriptions, 'update');
|
||||
spy.returnsPromise().resolves([]);
|
||||
describe('addSubToGroupUser', () => {
|
||||
it('adds a group subscription to a new user', async () => {
|
||||
expect(group.purchased.plan.planId).to.not.exist;
|
||||
data.groupId = group._id;
|
||||
data.sub.quantity = 3;
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore(stripe.subscriptions.update);
|
||||
});
|
||||
await api.addSubToGroupUser(user, group);
|
||||
|
||||
it('updates a group plan quantity', async () => {
|
||||
data.paymentMethod = 'Stripe';
|
||||
await api.createSubscription(data);
|
||||
let updatedUser = await User.findById(user._id).exec();
|
||||
|
||||
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);
|
||||
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;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user