Compare commits

...

111 Commits

Author SHA1 Message Date
Sabe Jones 93437a00dd 3.78.0 2017-03-02 21:05:49 +00:00
Sabe Jones 31afa0345e chore(news): Bailey 2017-03-02 2017-03-02 20:59:03 +00:00
Sabe Jones b2c278b00a fix(sprites): correct shop canvas 2017-03-02 20:58:42 +00:00
Sabe Jones b54037f230 chore(event): disable Cupid Potions 2017-03-02 20:49:02 +00:00
Sabe Jones 488dcb916a chore(sprites): compile 2017-03-02 20:44:25 +00:00
Sabe Jones 10792b65be feat(content): add Armoire and BGs 2017-03 2017-03-02 20:44:00 +00:00
Sabe Jones 390970a73a chore(news): Bailey 2017-03-01 (#8524) 2017-03-01 18:54:14 -06:00
Megan Tiu 6f0d0b1fb3 Use backticks in 'quest started' system message (#8503)
Fixes #8445
2017-02-26 16:03:58 +10:00
Sabe Jones dfcd32d54a chore(i18n): update locales 2017-02-23 18:04:06 +00:00
Sabe Jones c24cbdc987 feat(content): strings for Armoire and BGs 2017-03 2017-02-23 17:55:50 +00:00
Sabe Jones 68d8c0de51 3.77.0 2017-02-23 05:22:59 +00:00
Sabe Jones a7b0fa195f chore(news): Bailey 2017-02-23 02:57:09 +00:00
Sabe Jones ab65aa692f fix(sprites): NYE 2x pixelfixes
and move a promo to subfolder
2017-02-23 02:56:33 +00:00
Sabe Jones 198d4df02a fix(analytics): consistent partyID label 2017-02-22 20:47:52 +00:00
Sabe Jones 6fa2f643fd fix(event): stop giving out party stuff 2017-02-21 22:21:13 +00:00
Sabe Jones 4d39861b51 fix(event): disable buying Santa quests 2017-02-21 22:16:32 +00:00
Sabe Jones d4c99b6db6 fix(avatar): disable wintery customizations 2017-02-21 21:59:43 +00:00
Sabe Jones a79dc90a45 chore(sprites): compile 2017-02-21 21:21:53 +00:00
Sabe Jones daa6bd0315 chore(event): end Valentine's 2017-02-21 21:21:25 +00:00
Sabe Jones 99f2373214 feat(content): Mystery items 2017/02 2017-02-21 21:20:24 +00:00
Phillip Thelen 374d528647 Implement iOS subscriptions (#8493)
* implement iOS subscriptions

* add additional tests to request body

* Improve subscription cancelling

* change string to constant
2017-02-21 19:22:13 +01:00
Alys 8550ca4d29 change heading on forgot-password form to "Email a Password Reset Link"
Previously it was "Email New Password"
2017-02-19 09:45:58 +10:00
Keith Holliday 79f6b59f6e Options view cleanup (#8484)
* Split the settings into their own files

* Refactor profile jade items to have separate files
2017-02-17 03:55:59 -07:00
Keith Holliday e37ad420ce Updated tests to have no redirect to homes (#8494) 2017-02-16 16:33:49 -07:00
Sabe Jones 2f9ff92cbd 3.76.0 2017-02-16 23:12:55 +00:00
Sabe Jones 56479e7fbd chore(i18n): update locales 2017-02-16 21:07:51 +00:00
Sabe Jones 66e4849553 feat(content): subscriber item strings 2017-02-16 20:56:16 +00:00
Sabe Jones 9e29b44ad9 chore(sprites): compile 2017-02-16 20:51:35 +00:00
Sabe Jones 605488dd47 feat(content): Guinea Pig quest 2017-02-16 20:51:12 +00:00
MathWhiz 6c16b4b77e ApiDoc - Development (#8480)
* Add api info

* Remove extra spaces
2017-02-16 08:53:21 -07:00
Matteo Pagliazzi 80205dcc67 recompile shrinkwrap 2017-02-15 17:25:49 +01:00
Matteo Pagliazzi 65d5bf69f6 Upgrade webpack to v2 and other deps (#8491)
* update to webpack2 and other deps

* fix linting
2017-02-15 17:24:37 +01:00
Matteo Pagliazzi 20792f5455 New Client: stable (#8426)
* try to freeze content object

* deep freeze the content object, start to implement stable

* freeze at the /common level

* go back to freezing content only on the client

* use deep-frezze-strict to support phantomjs

* use own version of deepFreeze

* update comment about deepFreeze
2017-02-15 12:49:57 +01:00
Sabe Jones 6fd509df13 fix(news): missing blog info 2017-02-14 21:00:38 +00:00
Sabe Jones d7df18a97a 3.75.3 2017-02-14 18:29:11 +00:00
Sabe Jones ff5c44c5a8 feat(event): Valentine's 2017-02-14 17:32:23 +00:00
Matteo Pagliazzi c929fa1351 chore(i18n): update locales 2017-02-14 18:19:14 +01:00
Matteo Pagliazzi d30e7b9251 Don't send plaintext reset passwords via email (#8457)
* start work to avoid sending reset password in plaintext via email

* start checking parameters

* fix new password reset email

* render error if password reset code is missing or invalid

* implement POST route, conversion to bcrypt and messages

* add auth.local.passwordResetCode field

* add failing tests, move reset code validation func to lib, fixes, remove old tests

* fix unit tests

* fix page rendering and add integration tests

* fix password reset page

* add integration test

* fix string

* fix tests url
2017-02-14 18:08:31 +01:00
Matteo Pagliazzi c6c6632405 fix(tests): correctly append query string 2017-02-13 12:54:45 +01:00
Matteo Pagliazzi e0d9ecca52 fix(tests): do not redirect to home page when cancelling a subscription 2017-02-13 12:21:34 +01:00
Marcel O'Neil 78f187f87a Task edit screen changes [fixes #8474]
move Dailies repeats out of Advanced Options; enlarge Notes; reduce spacings
2017-02-13 20:02:27 +10:00
Alys af047d3c82 revert commits accidentally added from "Added support for grouping tasks by challenge"
So sorry, but I accidentally pushed some commits I'd been testing. :( This reverts them.

Reverts c496f94c79 ad6073220d d669db0f9a c244fe488d
2017-02-12 12:17:23 +10:00
Alys 20e8f47def adjust links to wiki's page for Extensions, Add-Ons, and Customizations
- remove redundant link from footer's Company column (still exists in Community)
- change link to point directly to wiki page instead of to a wiki redirect for:
   - static/features page ("3rd Party Tools" section)
   - static/extensions redirection
2017-02-11 23:14:47 +10:00
Keith Holliday c496f94c79 Updated setting string 2017-02-11 17:39:53 +10:00
Keith Holliday ad6073220d Fixed broken member test 2017-02-11 17:39:53 +10:00
Keith Holliday d669db0f9a Fixed tests and updated default challenge model name 2017-02-11 17:39:53 +10:00
Keith Holliday c244fe488d Added support for grouping tasks by chllenge 2017-02-11 17:39:53 +10:00
Monica Williams aef71db0f5 removes changes to all local files except for en file (#8463) 2017-02-11 17:06:57 +10:00
Sabe Jones ac566882fe 3.75.2 2017-02-08 22:02:59 +00:00
Sabe Jones 1ad662a090 chore(news): blog Bailey 2017-02-08 20:38:40 +00:00
Rachel Williams 1db52de45d Reordered Magic Hatching Potions, fixes issue 8465 (#8467)
* Moved Royal Purple to top of list, ordered other mounts from Spring to Winter

* Issue 8465, moved Royal Purple to top and arranged other pets/mounts from Spring-Winter

* Fixed whitespacing mistake from previous commit

* Magic Hatching Potions reordered by season
2017-02-08 13:14:30 -06:00
Travis f8cfdfa37d Updating attribute calculation to look at all equipment #8188 (#8202)
* Taking all equipped items into account when calculating attributes instead of just head, weapon, sheild, and armor. closes #8185

* Refactored the stat calculations a bit so that all stat calculations are handled in the same location in the same way to try and reduce duplicate logic.
This commit also adds a number of tests to test this new behavior.

* spelling fixes
2017-02-07 12:04:12 -07:00
Sabe Jones ffd36465c9 chore(packages): update passport 2017-02-07 00:25:41 +00:00
Myles Louis Dakan dbb1e3aa18 Mods blocking (#8364)
* updated logic for blocking/unblocking

* updated 1 test and added 2
2017-02-05 22:32:10 -07:00
Matteo Pagliazzi 07ce68c596 remove newrelic 2017-02-05 18:43:00 +01:00
Keith Holliday 4c5d72c96f Fixed group plan checkout with stripe (#8475) 2017-02-04 09:01:53 -07:00
Matteo Pagliazzi 7121653515 3.75.1 2017-02-03 11:38:55 +01:00
Matteo Pagliazzi ce2b66a2be update deps 2017-02-03 11:15:30 +01:00
Matteo Pagliazzi 43e381cfc1 Revert "Fix Gem purchase validation" (#8471) 2017-02-03 11:08:06 +01:00
Phillip Thelen ca5161b48a fix gem purchase validation (#8470) 2017-02-03 10:58:48 +01:00
Sabe Jones 44dec6dc4b fix(news): Android URL 2017-02-02 22:13:31 +00:00
Sabe Jones fb8f0bed43 3.75.0 2017-02-02 22:08:17 +00:00
Sabe Jones c391de9e86 chore(news): Bailey
and tweak today's migrations for new format
2017-02-02 21:04:36 +00:00
Sabe Jones 7a534ab81d fix(payments): customerId query 2017-02-02 19:51:52 +00:00
Sabe Jones 32c7f40c4b chore(sprites): compile 2017-02-02 17:31:43 +00:00
Sabe Jones 569fbff244 feat(content): backgrounds and Armoire Feb 2017 2017-02-02 17:30:46 +00:00
Sabe Jones 3fe847f329 feat(content): Cupid Hatching Potions 2017-02-02 17:29:33 +00:00
Sabe Jones d52d759733 3.74.1 2017-02-02 15:45:23 +00:00
Sabe Jones 5e5a755022 chore(sprites): compile 2017-02-02 15:18:12 +00:00
Sabe Jones 8d148b4d69 End Winter Wonderland (#8466)
* chore(event): end Winter Wonderland

* fix(test): update constant
2017-02-02 09:08:51 -06:00
Sabe Jones 248b64a43f 3.74.0 2017-02-02 00:59:44 +00:00
Sabe Jones 57ed0f0a10 chore(i18n): update locales 2017-02-02 00:55:58 +00:00
Sabe Jones 3455adaef5 chore(news): Bailey 2017-02-02 00:48:24 +00:00
Phillip Thelen 4d0295a60d Support subscription payment through Google Play Store (#8437)
* Support subscription payment through Google Play Store

* minor fixes to iap subscriptions

* Support subscription payment through Google Play Store

* minor fixes to iap subscriptions

* revert change to test

* add unit tests for google payments

* add integration tests for google payments

* change config formatting for play api

* fix typo in file name

* fix typo in example config

* Improve google payment tests

* fix linter errors
2017-02-01 18:39:37 -06:00
Sabe Jones a002bc5e20 fix(migration): curly typo 2017-02-01 21:00:48 +00:00
Sabe Jones b9d1086e24 chore(migration): update Take This format 2017-02-01 20:52:26 +00:00
madpink 412a0ecc8c Updating Tasks and Tags API Doc (#8447)
* Updating Tasks and Tags API Doc

* Update Tasks and Tag API Doc

added back  * @apiUse ChallengeNotFound

* Update Tasks and Tag API Doc #8447

Corrected NotAuthorized errors to 401
2017-02-01 19:37:27 +01:00
Sabe Jones d44c9ea853 3.73.1 2017-01-31 21:11:21 +00:00
Sabe Jones c44c581265 fix(shops): correct event boolean 2017-01-31 21:10:51 +00:00
Sabe Jones f666f3cd04 3.73.0 2017-01-31 19:22:22 +00:00
Sabe Jones 236bd6cec4 fix(test): linting and constant 2017-01-31 19:21:13 +00:00
Keith Holliday e124c36274 Added double columns to edit modal (#8456)
* Added double columns to edit modal

* Fixed task notes z index and border styles

* Moved tags to right column and updated min width
2017-01-31 12:05:49 -07:00
Sabe Jones d5511a0047 chore(i18n): update locales 2017-01-31 18:38:23 +00:00
Sabe Jones 87f003f392 chore(sprites): compile 2017-01-31 18:32:03 +00:00
Sabe Jones 930a869365 feat(event): Habit Birthday 2017 2017-01-31 18:30:39 +00:00
Alys 809da8add0 update landing page's number of user accounts to 2 million
YES! TWO MILLION!
2017-01-31 20:54:33 +10:00
Sabe Jones 1dad176320 3.72.1 2017-01-26 18:52:39 +00:00
Sabe Jones 1596c6218f chore(news): Bailey 2017-01-26 2017-01-26 18:10:13 +00:00
Sabe Jones 0d3aba950a fix(config): remove deprecated env var 2017-01-25 23:27:34 +00:00
Sabe Jones f1110e0f89 chore(i18n): update locales 2017-01-25 22:31:54 +00:00
Sabe Jones 7273f8f6d9 feat(content): strings for upcoming items 2017-01-25 22:25:32 +00:00
Keith Holliday a0ae200a54 Removed extra async (#8455) 2017-01-25 09:47:21 -07:00
Sabe Jones ca448f081d 3.72.0 2017-01-25 00:35:36 +00:00
Keith Holliday cd27afa9f0 Used profile name in inviter message (#8453) 2017-01-24 17:44:45 -06:00
Sabe Jones 8bc8183895 chore(i18n): update locales 2017-01-24 21:44:48 +00:00
Sabe Jones c26c52f1fe chore(sprites): compile 2017-01-24 21:37:57 +00:00
Sabe Jones 8d5becc9ce feat(content): subscriber items 2017-01 2017-01-24 21:30:16 +00:00
Matteo Pagliazzi cf7f6e2a67 chore(i18n): update locales 2017-01-24 20:23:20 +01:00
Matteo Pagliazzi acad3b8873 Migrate to bcrypt (#8446)
* start migrating to bcrypt

* added method to convert the password to bcrypt when logging in, added method to compare password without knowing the hashing algorhytm, remove default

* travis: try to upgrade to container based infrastructure

* travis: add deps to build bcrypt.js

* travis: add deps to build bcrypt.js

* travis: add deps to build bcrypt.js

* travis: add deps to build bcrypt.js

* use bcryptjs until bcrypt can be installed on travis, see https://github.com/kelektiv/node.bcrypt.js/issues/476

* correct sha1 unit tests

* try different mongodb repo

* try without mognodb services

* try again with bcrypt

* disable request logging in travis

* migrate missing routes

* simplify code

* remove bcryptjs

* fix typo

* fix typo

* fix typo in comment

* add unit tests for new passwords utility emthods

* travis: back to old infrastructure, containers often have timeouts

* add integration test for passwordHashMethod

* update shrinkwrap

* clarify code and add comments

* add integration tests

* fix linting

* fix integration tests
2017-01-24 12:28:42 +01:00
Jaka Kranjc 04f4eb8490 added test case for #8423 (#8434) 2017-01-24 11:15:08 +01:00
Jaka Kranjc 8c8af83dfc spells: searing brightness should not affect challenge tasks (#8427)
* spells: searing brightness should not affect challenge tasks

* fixed other incorrect group vs challenge task exclusions

* fixed /tasks/clearCompletedTodos test

didn't account for the new group task

* fixed comment omission in tasks/clearCompletedTodos
2017-01-24 11:14:25 +01:00
Valeriy 86d65956d9 fixed recoverCron tests failure when using mocha v3+ (#8407)
* fixed recoverCron tests

* type'o fix

* grammar nazi fix

* lint fix

* reverted to async/await (but done call removed)

* fixed Tasks tests failure on Mocha 3+
2017-01-24 10:14:27 +01:00
MathWhiz 42c5e6c22b Facebook pixel Revert (3rd time's a charm?) (#8450)
* attempt revert

* Add back unrelated stuff
2017-01-23 14:42:57 -06:00
Cai Lu 79b51a40ce Validate coupon code for subscriptions fixes #8398 (#8440)
* (fix) If coupon is not valid, display an error

* (test) Add valid and invalid coupon tests for applyCoupon

* Add test descriptions
2017-01-23 12:01:30 -07:00
Matteo Pagliazzi 79c3efaf9c 3.71.0 2017-01-23 19:05:39 +01:00
Matteo Pagliazzi 74c6a891fc Revert "Revert Facebook Pixel" (#8449) 2017-01-23 16:38:56 +01:00
MathWhiz 9a5d17f538 attempt revert (#8406) 2017-01-23 16:03:23 +01:00
Matteo Pagliazzi 070c4a8fbd add auth.local.passwordHashMethod field 2017-01-23 10:38:41 +01:00
Keith Holliday 2bbc4f4f4d Paypal refactor lib (#8420)
* Abstracted paypal logic from controller. Added intial tests

* Added checkout tests

* Added checkout success test

* Added subscribe test

* Added subscribeSuccess tests

* Added subscribeCancel tests

* Added ipn tests

* Fixed broken test

* Added integration tests and fixed linting issues

* Added errors for paypal checkout success integration test

* Removed extra test

* Removed pending status from subscribe cancel test

* Added more error checking and tests

* Fixed lint issues
2017-01-22 09:59:47 -07:00
Alys 39c00ea433 change HTML <title> on static/old-news from "Old News" to "News"
That page now contains the most recent news as well as the old news.
2017-01-22 13:32:54 +10:00
Alys dd6c1c764a change footer's "Add-ons & Extensions" link
The new link is a better wiki page and is already in use in the Settings > API screen.
2017-01-22 13:15:12 +10:00
769 changed files with 51657 additions and 44854 deletions
+11
View File
@@ -1,7 +1,15 @@
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:
@@ -12,6 +20,9 @@ after_script:
- ./node_modules/.bin/lcov-result-merger 'coverage/**/*.info' | ./node_modules/coveralls/bin/coveralls.js
script: npm run $TEST
env:
global:
- CXX=g++-4.8
- DISABLE_REQUEST_LOGGING=true
matrix:
- TEST="lint"
- TEST="test:api-v3" REQUIRES_SERVER=true
+9 -3
View File
@@ -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",
@@ -81,5 +86,6 @@
"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"
}
+109
View 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;
+1 -1
View File
@@ -2,7 +2,7 @@ var _id = '';
var update = {
$addToSet: {
'purchased.plan.mysteryItems':{
$each:['head_mystery_201612','armor_mystery_201612']
$each:['head_mystery_201702','back_mystery_201702']
}
}
};
+2 -3
View File
@@ -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);
});
+44 -22
View File
@@ -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;
+47 -26
View File
@@ -1,4 +1,4 @@
var migrationName = '20170103_takeThis.js'; // Update per month
var migrationName = '20170201_takeThis.js'; // Update per month
var authorName = 'Sabe'; // in case script author needs to know when their ...
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
@@ -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:['b1d436b5-c784-42e3-9b07-7072479a6f8e']} // 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;
+1646 -1892
View File
File diff suppressed because it is too large Load Diff
+38 -37
View File
@@ -1,14 +1,14 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "3.70.0",
"version": "3.78.0",
"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,6 +25,7 @@
"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",
"bower": "~1.3.12",
@@ -33,7 +34,7 @@
"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",
@@ -41,8 +42,8 @@
"express": "~4.14.0",
"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",
@@ -67,9 +68,8 @@
"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",
@@ -82,22 +82,21 @@
"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",
"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",
@@ -106,23 +105,23 @@
"s3-upload-stream": "^1.0.6",
"semantic-ui-less": "~2.2.4",
"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-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"
},
@@ -152,36 +151,37 @@
"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-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",
"http-proxy-middleware": "^0.17.0",
"inject-loader": "^3.0.0-beta4",
"istanbul": "^0.3.14",
"karma": "^1.3.0",
"karma-babel-preprocessor": "^6.0.1",
@@ -193,25 +193,26 @@
"karma-sinon-chai": "^1.2.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",
"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"
}
}
@@ -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();
@@ -1,21 +0,0 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
xdescribe('payments : paypal #checkout', () => {
let endpoint = '/paypal/checkout';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies subscription', async () => {
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('missingSubscription'),
});
});
});
@@ -1,21 +0,0 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
xdescribe('payments : paypal #checkoutSuccess', () => {
let endpoint = '/paypal/checkout/success';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies subscription', async () => {
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('missingSubscription'),
});
});
});
@@ -1,21 +0,0 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
xdescribe('payments : paypal #subscribe', () => {
let endpoint = '/paypal/subscribe';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies credentials', async () => {
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('missingSubscription'),
});
});
});
@@ -1,21 +0,0 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('payments : paypal #subscribeCancel', () => {
let endpoint = '/paypal/subscribe/cancel';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies credentials', async () => {
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('missingSubscription'),
});
});
});
@@ -1,21 +0,0 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
xdescribe('payments : paypal #subscribeSuccess', () => {
let endpoint = '/paypal/subscribe/success';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies credentials', async () => {
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('missingSubscription'),
});
});
});
@@ -1,17 +0,0 @@
import {
generateUser,
} from '../../../../helpers/api-integration/v3';
describe('payments - paypal - #ipn', () => {
let endpoint = '/paypal/ipn';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies credentials', async () => {
let result = await user.post(endpoint);
expect(result).to.eql('OK');
});
});
@@ -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);
});
});
});
@@ -0,0 +1,40 @@
import {
generateUser,
} from '../../../../../helpers/api-integration/v3';
import paypalPayments from '../../../../../../website/server/libs/paypalPayments';
describe('payments : paypal #checkout', () => {
let endpoint = '/paypal/checkout';
let user;
beforeEach(async () => {
user = await generateUser();
});
describe('success', () => {
let checkoutStub;
beforeEach(async () => {
checkoutStub = sinon.stub(paypalPayments, 'checkout').returnsPromise().resolves('/');
});
afterEach(() => {
paypalPayments.checkout.restore();
});
it('creates a purchase link', async () => {
user = await generateUser({
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
balance: 2,
});
await user.get(`${endpoint}?noRedirect=true`);
expect(checkoutStub).to.be.calledOnce;
expect(checkoutStub.args[0][0].gift).to.eql(undefined);
});
});
});
@@ -0,0 +1,66 @@
import {
generateUser,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import paypalPayments from '../../../../../../website/server/libs/paypalPayments';
describe('payments : paypal #checkoutSuccess', () => {
let endpoint = '/paypal/checkout/success';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies paymentId', async () => {
await expect(user.get(endpoint))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingPaymentId'),
});
});
it('verifies customerId', async () => {
await expect(user.get(`${endpoint}?paymentId=test-paymentid`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingCustomerId'),
});
});
describe('success', () => {
let checkoutSuccessStub;
beforeEach(async () => {
checkoutSuccessStub = sinon.stub(paypalPayments, 'checkoutSuccess').returnsPromise().resolves({});
});
afterEach(() => {
paypalPayments.checkoutSuccess.restore();
});
it('makes a purchase', async () => {
let paymentId = 'test-paymentid';
let customerId = 'test-customerId';
user = await generateUser({
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
balance: 2,
});
await user.get(`${endpoint}?PayerID=${customerId}&paymentId=${paymentId}&noRedirect=true`);
expect(checkoutSuccessStub).to.be.calledOnce;
expect(checkoutSuccessStub.args[0][0].user._id).to.eql(user._id);
expect(checkoutSuccessStub.args[0][0].gift).to.eql(undefined);
expect(checkoutSuccessStub.args[0][0].paymentId).to.eql(paymentId);
expect(checkoutSuccessStub.args[0][0].customerId).to.eql(customerId);
});
});
});
@@ -0,0 +1,55 @@
import {
generateUser,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import paypalPayments from '../../../../../../website/server/libs/paypalPayments';
import shared from '../../../../../../website/common';
describe('payments : paypal #subscribe', () => {
let endpoint = '/paypal/subscribe';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies sub key', async () => {
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingSubKey'),
});
});
describe('success', () => {
let subscribeStub;
beforeEach(async () => {
subscribeStub = sinon.stub(paypalPayments, 'subscribe').returnsPromise().resolves('/');
});
afterEach(() => {
paypalPayments.subscribe.restore();
});
it('makes a purchase', async () => {
let subKey = 'basic_3mo';
let sub = shared.content.subscriptionBlocks[subKey];
user = await generateUser({
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
balance: 2,
});
await user.get(`${endpoint}?sub=${subKey}&noRedirect=true`);
expect(subscribeStub).to.be.calledOnce;
expect(subscribeStub.args[0][0].sub).to.eql(sub);
expect(subscribeStub.args[0][0].coupon).to.eql(undefined);
});
});
});
@@ -0,0 +1,52 @@
import {
generateUser,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import paypalPayments from '../../../../../../website/server/libs/paypalPayments';
describe('payments : paypal #subscribeCancel', () => {
let endpoint = '/paypal/subscribe/cancel';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies credentials', async () => {
await expect(user.get(endpoint))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('missingSubscription'),
});
});
describe('success', () => {
let subscribeCancelStub;
beforeEach(async () => {
subscribeCancelStub = sinon.stub(paypalPayments, 'subscribeCancel').returnsPromise().resolves('/');
});
afterEach(() => {
paypalPayments.subscribeCancel.restore();
});
it('cancels a subscription', async () => {
user = await generateUser({
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
balance: 2,
});
await user.get(`${endpoint}?noRedirect=true`);
expect(subscribeCancelStub).to.be.calledOnce;
expect(subscribeCancelStub.args[0][0].user._id).to.eql(user._id);
expect(subscribeCancelStub.args[0][0].groupId).to.eql(undefined);
});
});
});
@@ -0,0 +1,56 @@
import {
generateUser,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import paypalPayments from '../../../../../../website/server/libs/paypalPayments';
describe('payments : paypal #subscribeSuccess', () => {
let endpoint = '/paypal/subscribe/success';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies Paypal Block', async () => {
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingPaypalBlock'),
});
});
xdescribe('success', () => {
let subscribeSuccessStub;
beforeEach(async () => {
subscribeSuccessStub = sinon.stub(paypalPayments, 'subscribeSuccess').returnsPromise().resolves({});
});
afterEach(() => {
paypalPayments.subscribeSuccess.restore();
});
it('creates a subscription', async () => {
let token = 'test-token';
user = await generateUser({
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
balance: 2,
});
await user.get(`${endpoint}?token=${token}&noRedirect=true`);
expect(subscribeSuccessStub).to.be.calledOnce;
expect(subscribeSuccessStub.args[0][0].user._id).to.eql(user._id);
expect(subscribeSuccessStub.args[0][0].block).to.eql(undefined);
expect(subscribeSuccessStub.args[0][0].groupId).to.eql(undefined);
expect(subscribeSuccessStub.args[0][0].token).to.eql(token);
expect(subscribeSuccessStub.args[0][0].headers).to.exist;
});
});
});
@@ -0,0 +1,44 @@
import {
generateUser,
} from '../../../../../helpers/api-integration/v3';
import paypalPayments from '../../../../../../website/server/libs/paypalPayments';
describe('payments - paypal - #ipn', () => {
let endpoint = '/paypal/ipn';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies credentials', async () => {
let result = await user.post(endpoint);
expect(result).to.eql('OK');
});
describe('success', () => {
let ipnStub;
beforeEach(async () => {
ipnStub = sinon.stub(paypalPayments, 'ipn').returnsPromise().resolves({});
});
afterEach(() => {
paypalPayments.ipn.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,
});
await user.post(endpoint);
expect(ipnStub).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
});
});
@@ -11,6 +11,10 @@ import {
map,
} from 'lodash';
import Bluebird from 'bluebird';
import {
sha1MakeSalt,
sha1Encrypt as sha1EncryptPassword,
} from '../../../../../website/server/libs/password';
describe('DELETE /user', () => {
let user;
@@ -67,6 +71,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,11 @@ import {
requester,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import {
bcryptCompare,
sha1MakeSalt,
sha1Encrypt as sha1EncryptPassword,
} from '../../../../../../website/server/libs/password';
describe('POST /user/auth/local/login', () => {
let api;
@@ -72,4 +77,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);
});
});
@@ -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,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-email';
@@ -66,6 +71,41 @@ describe('PUT /user/auth/update-email', () => {
message: t('cannotFulfillReq'),
});
});
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';
+258
View 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,
});
});
});
});
+5 -9
View File
@@ -975,19 +975,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 +994,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();
}
});
});
@@ -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,
});
});
});
});
+317 -21
View File
@@ -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);
});
});
});
});
@@ -0,0 +1,528 @@
/* eslint-disable camelcase */
import nconf from 'nconf';
import moment from 'moment';
import cc from 'coupon-code';
import payments from '../../../../../website/server/libs/payments';
import paypalPayments from '../../../../../website/server/libs/paypalPayments';
import {
generateGroup,
} from '../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../website/server/models/user';
import { model as Coupon } from '../../../../../website/server/models/coupon';
import common from '../../../../../website/common';
const BASE_URL = nconf.get('BASE_URL');
const i18n = common.i18n;
describe('Paypal Payments', () => {
let subKey = 'basic_3mo';
describe('checkout', () => {
let paypalPaymentCreateStub;
let approvalHerf;
function getPaypalCreateOptions (description, amount) {
return {
intent: 'sale',
payer: { payment_method: 'Paypal' },
redirect_urls: {
return_url: `${BASE_URL}/paypal/checkout/success`,
cancel_url: `${BASE_URL}`,
},
transactions: [{
item_list: {
items: [{
name: description,
price: amount,
currency: 'USD',
quantity: 1,
}],
},
amount: {
currency: 'USD',
total: amount,
},
description,
}],
};
}
beforeEach(() => {
approvalHerf = 'approval_href';
paypalPaymentCreateStub = sinon.stub(paypalPayments, 'paypalPaymentCreate')
.returnsPromise().resolves({
links: [{ rel: 'approval_url', href: approvalHerf }],
});
});
afterEach(() => {
paypalPayments.paypalPaymentCreate.restore();
});
it('creates a link for gem purchases', async () => {
let link = await paypalPayments.checkout();
expect(paypalPaymentCreateStub).to.be.calledOnce;
expect(paypalPaymentCreateStub).to.be.calledWith(getPaypalCreateOptions('Habitica Gems', 5.00));
expect(link).to.eql(approvalHerf);
});
it('creates a link for gifting gems', async () => {
let receivingUser = new User();
let gift = {
type: 'gems',
gems: {
amount: 16,
uuid: receivingUser._id,
},
};
let link = await paypalPayments.checkout({gift});
expect(paypalPaymentCreateStub).to.be.calledOnce;
expect(paypalPaymentCreateStub).to.be.calledWith(getPaypalCreateOptions('Habitica Gems (Gift)', '4.00'));
expect(link).to.eql(approvalHerf);
});
it('creates a link for gifting a subscription', async () => {
let receivingUser = new User();
receivingUser.save();
let gift = {
type: 'subscription',
subscription: {
key: subKey,
uuid: receivingUser._id,
},
};
let link = await paypalPayments.checkout({gift});
expect(paypalPaymentCreateStub).to.be.calledOnce;
expect(paypalPaymentCreateStub).to.be.calledWith(getPaypalCreateOptions('mo. Habitica Subscription (Gift)', '15.00'));
expect(link).to.eql(approvalHerf);
});
});
describe('checkout success', () => {
let user, gift, customerId, paymentId;
let paypalPaymentExecuteStub, paymentBuyGemsStub, paymentsCreateSubscritionStub;
beforeEach(() => {
user = new User();
customerId = 'customerId-test';
paymentId = 'paymentId-test';
paypalPaymentExecuteStub = sinon.stub(paypalPayments, 'paypalPaymentExecute').returnsPromise().resolves({});
paymentBuyGemsStub = sinon.stub(payments, 'buyGems').returnsPromise().resolves({});
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({});
});
afterEach(() => {
paypalPayments.paypalPaymentExecute.restore();
payments.buyGems.restore();
payments.createSubscription.restore();
});
it('purchases gems', async () => {
await paypalPayments.checkoutSuccess({user, gift, paymentId, customerId});
expect(paypalPaymentExecuteStub).to.be.calledOnce;
expect(paypalPaymentExecuteStub).to.be.calledWith(paymentId, { payer_id: customerId });
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,
customerId,
paymentMethod: 'Paypal',
});
});
it('gifts gems', async () => {
let receivingUser = new User();
await receivingUser.save();
gift = {
type: 'gems',
gems: {
amount: 16,
uuid: receivingUser._id,
},
};
await paypalPayments.checkoutSuccess({user, gift, paymentId, customerId});
expect(paypalPaymentExecuteStub).to.be.calledOnce;
expect(paypalPaymentExecuteStub).to.be.calledWith(paymentId, { payer_id: customerId });
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,
customerId,
paymentMethod: 'PayPal (Gift)',
gift,
});
});
it('gifts subscription', async () => {
let receivingUser = new User();
await receivingUser.save();
gift = {
type: 'subscription',
subscription: {
key: subKey,
uuid: receivingUser._id,
},
};
await paypalPayments.checkoutSuccess({user, gift, paymentId, customerId});
expect(paypalPaymentExecuteStub).to.be.calledOnce;
expect(paypalPaymentExecuteStub).to.be.calledWith(paymentId, { payer_id: customerId });
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
expect(paymentsCreateSubscritionStub).to.be.calledWith({
user,
customerId,
paymentMethod: 'PayPal (Gift)',
gift,
});
});
});
describe('subscribe', () => {
let coupon, sub, approvalHerf;
let paypalBillingAgreementCreateStub;
beforeEach(() => {
approvalHerf = 'approvalHerf-test';
sub = common.content.subscriptionBlocks[subKey];
paypalBillingAgreementCreateStub = sinon.stub(paypalPayments, 'paypalBillingAgreementCreate')
.returnsPromise().resolves({
links: [{ rel: 'approval_url', href: approvalHerf }],
});
});
afterEach(() => {
paypalPayments.paypalBillingAgreementCreate.restore();
});
it('should throw an error when coupon code is missing', async () => {
sub.discount = 40;
await expect(paypalPayments.subscribe({sub, coupon}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
message: i18n.t('couponCodeRequired'),
});
});
it('should throw an error when coupon code is invalid', async () => {
sub.discount = 40;
sub.key = 'google_6mo';
coupon = 'example-coupon';
let couponModel = new Coupon();
couponModel.event = 'google_6mo';
await couponModel.save();
sinon.stub(cc, 'validate').returns('invalid');
await expect(paypalPayments.subscribe({sub, coupon}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('invalidCoupon'),
});
cc.validate.restore();
});
it('subscribes with amazon with a coupon', async () => {
sub.discount = 40;
sub.key = 'google_6mo';
coupon = 'example-coupon';
let couponModel = new Coupon();
couponModel.event = 'google_6mo';
let updatedCouponModel = await couponModel.save();
sinon.stub(cc, 'validate').returns(updatedCouponModel._id);
let link = await paypalPayments.subscribe({sub, coupon});
expect(link).to.eql(approvalHerf);
expect(paypalBillingAgreementCreateStub).to.be.calledOnce;
let billingPlanTitle = `Habitica Subscription ($${sub.price} every ${sub.months} months, recurring)`;
expect(paypalBillingAgreementCreateStub).to.be.calledWith({
name: billingPlanTitle,
description: billingPlanTitle,
start_date: moment().add({ minutes: 5 }).format(),
plan: {
id: sub.paypalKey,
},
payer: {
payment_method: 'Paypal',
},
});
cc.validate.restore();
});
it('creates a link for a subscription', async () => {
delete sub.discount;
let link = await paypalPayments.subscribe({sub, coupon});
expect(link).to.eql(approvalHerf);
expect(paypalBillingAgreementCreateStub).to.be.calledOnce;
let billingPlanTitle = `Habitica Subscription ($${sub.price} every ${sub.months} months, recurring)`;
expect(paypalBillingAgreementCreateStub).to.be.calledWith({
name: billingPlanTitle,
description: billingPlanTitle,
start_date: moment().add({ minutes: 5 }).format(),
plan: {
id: sub.paypalKey,
},
payer: {
payment_method: 'Paypal',
},
});
});
});
describe('subscribeSuccess', () => {
let user, group, block, groupId, token, headers, customerId;
let paypalBillingAgreementExecuteStub, paymentsCreateSubscritionStub;
beforeEach(async () => {
user = new User();
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
token = 'test-token';
headers = {};
block = common.content.subscriptionBlocks[subKey];
customerId = 'test-customerId';
paypalBillingAgreementExecuteStub = sinon.stub(paypalPayments, 'paypalBillingAgreementExecute')
.returnsPromise({}).resolves({
id: customerId,
});
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({});
});
afterEach(() => {
paypalPayments.paypalBillingAgreementExecute.restore();
payments.createSubscription.restore();
});
it('creates a user subscription', async () => {
await paypalPayments.subscribeSuccess({user, block, groupId, token, headers});
expect(paypalBillingAgreementExecuteStub).to.be.calledOnce;
expect(paypalBillingAgreementExecuteStub).to.be.calledWith(token, {});
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
expect(paymentsCreateSubscritionStub).to.be.calledWith({
user,
groupId,
customerId,
paymentMethod: 'Paypal',
sub: block,
headers,
});
});
it('create a group subscription', async () => {
groupId = group._id;
await paypalPayments.subscribeSuccess({user, block, groupId, token, headers});
expect(paypalBillingAgreementExecuteStub).to.be.calledOnce;
expect(paypalBillingAgreementExecuteStub).to.be.calledWith(token, {});
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
expect(paymentsCreateSubscritionStub).to.be.calledWith({
user,
groupId,
customerId,
paymentMethod: 'Paypal',
sub: block,
headers,
});
});
});
describe('subscribeCancel', () => {
let user, group, groupId, customerId, groupCustomerId, nextBillingDate;
let paymentCancelSubscriptionSpy, paypalBillingAgreementCancelStub, paypalBillingAgreementGetStub;
beforeEach(async () => {
customerId = 'customer-id';
groupCustomerId = 'groupCustomerId-test';
user = new User();
user.profile.name = 'sender';
user.purchased.plan.customerId = customerId;
user.purchased.plan.planId = subKey;
user.purchased.plan.lastBillingDate = new Date();
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
group.purchased.plan.customerId = groupCustomerId;
group.purchased.plan.planId = subKey;
group.purchased.plan.lastBillingDate = new Date();
await group.save();
nextBillingDate = new Date();
paypalBillingAgreementCancelStub = sinon.stub(paypalPayments, 'paypalBillingAgreementCancel').returnsPromise().resolves({});
paypalBillingAgreementGetStub = sinon.stub(paypalPayments, 'paypalBillingAgreementGet')
.returnsPromise().resolves({
agreement_details: {
next_billing_date: nextBillingDate,
cycles_completed: 1,
},
});
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
});
afterEach(function () {
paypalPayments.paypalBillingAgreementGet.restore();
paypalPayments.paypalBillingAgreementCancel.restore();
payments.cancelSubscription.restore();
});
it('should throw an error if we are missing a subscription', async () => {
user.purchased.plan.customerId = undefined;
await expect(paypalPayments.subscribeCancel({user}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('missingSubscription'),
});
});
it('should throw an error if group is not found', async () => {
await expect(paypalPayments.subscribeCancel({user, groupId: 'fake-id'}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 404,
name: 'NotFound',
message: i18n.t('groupNotFound'),
});
});
it('should throw an error if user is not group leader', async () => {
let nonLeader = new User();
nonLeader.guilds.push(group._id);
await nonLeader.save();
await expect(paypalPayments.subscribeCancel({user: nonLeader, groupId: group._id}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
});
});
it('should cancel a user subscription', async () => {
await paypalPayments.subscribeCancel({user});
expect(paypalBillingAgreementGetStub).to.be.calledOnce;
expect(paypalBillingAgreementGetStub).to.be.calledWith(customerId);
expect(paypalBillingAgreementCancelStub).to.be.calledOnce;
expect(paypalBillingAgreementCancelStub).to.be.calledWith(customerId, { note: i18n.t('cancelingSubscription') });
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
user,
groupId,
paymentMethod: 'Paypal',
nextBill: nextBillingDate,
});
});
it('should cancel a group subscription', async () => {
await paypalPayments.subscribeCancel({user, groupId: group._id});
expect(paypalBillingAgreementGetStub).to.be.calledOnce;
expect(paypalBillingAgreementGetStub).to.be.calledWith(groupCustomerId);
expect(paypalBillingAgreementCancelStub).to.be.calledOnce;
expect(paypalBillingAgreementCancelStub).to.be.calledWith(groupCustomerId, { note: i18n.t('cancelingSubscription') });
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
user,
groupId: group._id,
paymentMethod: 'Paypal',
nextBill: nextBillingDate,
});
});
});
describe('ipn', () => {
let user, group, txn_type, userPaymentId, groupPaymentId;
let ipnVerifyAsyncStub, paymentCancelSubscriptionSpy;
beforeEach(async () => {
txn_type = 'recurring_payment_profile_cancel';
userPaymentId = 'userPaymentId-test';
groupPaymentId = 'groupPaymentId-test';
user = new User();
user.profile.name = 'sender';
user.purchased.plan.customerId = userPaymentId;
user.purchased.plan.planId = subKey;
user.purchased.plan.lastBillingDate = new Date();
await user.save();
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
group.purchased.plan.customerId = groupPaymentId;
group.purchased.plan.planId = subKey;
group.purchased.plan.lastBillingDate = new Date();
await group.save();
ipnVerifyAsyncStub = sinon.stub(paypalPayments, 'ipnVerifyAsync').returnsPromise().resolves({});
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
});
afterEach(function () {
paypalPayments.ipnVerifyAsync.restore();
payments.cancelSubscription.restore();
});
it('should cancel a user subscription', async () => {
await paypalPayments.ipn({txn_type, recurring_payment_id: userPaymentId});
expect(ipnVerifyAsyncStub).to.be.calledOnce;
expect(ipnVerifyAsyncStub).to.be.calledWith({txn_type, recurring_payment_id: userPaymentId});
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy.args[0][0].user._id).to.eql(user._id);
expect(paymentCancelSubscriptionSpy.args[0][0].paymentMethod).to.eql('Paypal');
});
it('should cancel a group subscription', async () => {
await paypalPayments.ipn({txn_type, recurring_payment_id: groupPaymentId});
expect(ipnVerifyAsyncStub).to.be.calledOnce;
expect(ipnVerifyAsyncStub).to.be.calledWith({txn_type, recurring_payment_id: groupPaymentId});
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy).to.be.calledWith({ groupId: group._id, paymentMethod: 'Paypal' });
});
});
});
+4 -6
View File
@@ -91,25 +91,23 @@ describe('Task Model', () => {
sandbox.spy(Tasks.Task, 'findOne');
});
it('throws an error if task identifier is not passed in', async (done) => {
it('throws an error if task identifier is not passed in', async () => {
try {
await Tasks.Task.findByIdOrAlias(null, user._id);
throw new Error('No exception when Id is None');
} catch (err) {
expect(err).to.exist;
expect(err).to.eql(new InternalServerError('Task identifier is a required argument'));
done();
}
});
it('throws an error if user identifier is not passed in', async (done) => {
it('throws an error if user identifier is not passed in', async () => {
try {
await Tasks.Task.findByIdOrAlias(taskWithAlias._id);
throw new Error('No exception when user_id is undefined');
} catch (err) {
expect(err).to.exist;
expect(err).to.eql(new InternalServerError('User identifier is a required argument'));
done();
}
});
@@ -1,7 +1,7 @@
'use strict';
describe('Settings Controller', function () {
var rootScope, scope, user, User, ctrl;
var rootScope, scope, $httpBackend, user, User, ctrl, Notification;
const actionClickEvent = {
target: document.createElement('button'),
@@ -29,18 +29,27 @@ describe('Settings Controller', function () {
releaseBoth: sandbox.stub(),
};
Notification = {
error: sandbox.stub(),
text: sandbox.stub()
};
$provide.value('Notification', Notification);
$provide.value('User', User);
$provide.value('Guide', sandbox.stub());
});
inject(function(_$rootScope_, _$controller_) {
inject(function(_$rootScope_, _$controller_, _$httpBackend_) {
scope = _$rootScope_.$new();
rootScope = _$rootScope_;
$httpBackend = _$httpBackend_;
$httpBackend.whenGET(/partials/).respond();
// Load RootCtrl to ensure shared behaviors are loaded
_$controller_('RootCtrl', {$scope: scope, User: User});
_$controller_('RootCtrl', {$scope: scope, User: User, Notification: Notification});
ctrl = _$controller_('SettingsCtrl', {$scope: scope, User: User});
ctrl = _$controller_('SettingsCtrl', {$scope: scope, User: User, Notification: Notification});
});
});
@@ -281,4 +290,48 @@ describe('Settings Controller', function () {
});
});
});
context('Validating coupons', function () {
describe('#applyCoupon', function () {
it('displays an error when an invalid coupon is applied', function () {
$httpBackend
.whenPOST('/api/v3/coupons/validate/INVALID_COUPON?userV=undefined')
.respond(200, {
success: true,
data: {
valid: false
},
notifications: [],
userV: 'undefined'
});
scope.applyCoupon('INVALID_COUPON');
$httpBackend.flush();
expect(Notification.error).to.be.called;
expect(Notification.error).to.be.calledWith(env.t('invalidCoupon'), true);
});
it('displays an confirmation when a valid coupon is applied', function () {
$httpBackend
.whenPOST('/api/v3/coupons/validate/VALID_COUPON?userV=undefined')
.respond(200, {
success: true,
data: {
valid: true
},
notifications: [],
userV: 'undefined'
});
scope.applyCoupon('VALID_COUPON');
$httpBackend.flush();
expect(Notification.error).to.not.be.called;
expect(Notification.text).to.be.calledWith('Coupon applied!');
});
});
});
});
@@ -26,14 +26,22 @@ describe('Analytics Service', function () {
describe('register', function() {
beforeEach(function() {
sandbox.stub(window, 'fbq');
sandbox.stub(amplitude, 'setUserId');
sandbox.stub(window, 'ga');
});
it('records a registration event on Facebook', function() {
it('sets up user with Amplitude', function() {
analytics.register();
clock.tick();
expect(fbq).to.have.been.calledOnce;
expect(fbq).to.have.been.calledWith('track', 'CompleteRegistration');
expect(amplitude.setUserId).to.have.been.calledOnce;
expect(amplitude.setUserId).to.have.been.calledWith(user._id);
});
it('sets up user with Google Analytics', function() {
analytics.register();
clock.tick();
expect(ga).to.have.been.calledOnce;
expect(ga).to.have.been.calledWith('set', {userId: user._id});
});
});
@@ -66,7 +74,6 @@ describe('Analytics Service', function () {
beforeEach(function() {
sandbox.stub(amplitude, 'logEvent');
sandbox.stub(window, 'ga');
sandbox.stub(window, 'fbq');
});
context('successful tracking', function() {
@@ -106,15 +113,6 @@ describe('Analytics Service', function () {
expect(ga).to.have.been.calledOnce;
expect(ga).to.have.been.calledWith('send', properties);
});
it('tracks a page view with Facebook', function() {
var properties = {'hitType':'pageview','eventCategory':'behavior','eventAction':'tasks'};
analytics.track(properties);
clock.tick();
expect(fbq).to.have.been.calledOnce;
expect(fbq).to.have.been.calledWith('track', 'PageView');
});
});
context('unsuccessful tracking', function() {
@@ -67,59 +67,6 @@ describe('Stats Service', function() {
});
});
describe('classBonus', function() {
it('calculates class bonus', function() {
var equippedGear = {
"weapon" : "weapon_warrior_1",
"shield" : "shield_warrior_1",
"head" : "head_warrior_1",
"armor" : "armor_warrior_1"
};
var user = {
fns: {
statsComputed: function () {
return { str: 50 };
},
},
stats: {
lvl: 10,
buffs: { str: 10 },
str: 10
},
items: {
gear: { equipped: equippedGear }
}
};
var stat = 'str';
var classBonus = statCalc.classBonus(user, stat);
expect(classBonus).to.eql(20)
});
it('does not return value if user has not been wrapped (_statComputed)', function() {
var equippedGear = {
"weapon" : "weapon_warrior_1",
"shield" : "shield_warrior_1",
"head" : "head_warrior_1",
"armor" : "armor_warrior_1"
};
var user = {
stats: {
lvl: 10,
buffs: { str: 10 },
str: 10
},
items: {
gear: { equipped: equippedGear }
}
};
var stat = 'str';
var classBonus = statCalc.classBonus(user, stat);
expect(classBonus).to.not.exist;
});
});
describe('expDisplay', function() {
it('displays exp as "exp / toNextLevelExp"', function() {
user.stats.exp = 10;
@@ -138,27 +85,6 @@ describe('Stats Service', function() {
});
});
describe('equipmentStatBonus', function() {
it('tallies up stats from equipment that is equipped', function() {
var equippedGear = {
"weapon" : "weapon_special_1",
"shield" : "shield_special_1",
"head" : "head_special_1",
"armor" : "armor_special_1"
};
var strStat = statCalc.equipmentStatBonus('str', equippedGear);
var conStat = statCalc.equipmentStatBonus('con', equippedGear);
var intStat = statCalc.equipmentStatBonus('int', equippedGear);
var perStat = statCalc.equipmentStatBonus('per', equippedGear);
expect(strStat).to.eql(24);
expect(conStat).to.eql(24);
expect(intStat).to.eql(24);
expect(perStat).to.eql(24);
});
});
describe('goldDisplay', function() {
it('displays gold', function() {
var gold = 30;
@@ -192,32 +118,6 @@ describe('Stats Service', function() {
});
});
describe('levelBonus', function() {
it('calculates bonus as half of level for even numbered level under 100', function() {
var level = 50;
var bonus = statCalc.levelBonus(level);
expect(bonus).to.eql(25);
});
it('calculates bonus as half of level, rounded down, for odd numbered level under 100', function() {
var level = 51;
var bonus = statCalc.levelBonus(level);
expect(bonus).to.eql(25);
});
it('calculates bonus as 50 for levels >= 100', function() {
var level = 150;
var bonus = statCalc.levelBonus(level);
expect(bonus).to.eql(50);
});
it('calculates bonus as 0 for level 1', function() {
var level = 1;
var bonus = statCalc.levelBonus(level);
expect(bonus).to.eql(0);
});
});
describe('mountMasterProgress', function() {
it('counts drop mounts that user has', function() {
user.items.mounts = {
+11 -2
View File
@@ -1,9 +1,18 @@
{
"presets": ["es2015"],
"env": {
"test": {
plugins: [
["istanbul"],
],
},
},
"presets": [
["es2015", { modules: false }],
],
"plugins": [
"transform-object-rest-spread",
"syntax-async-functions",
"transform-regenerator",
],
"comments": false
"comments": false,
}
+2 -1
View File
@@ -3,6 +3,7 @@
require('babel-register');
const config = require('../../../webpack/config');
const chromeDriverPath = require('chromedriver').path;
const seleniumServerPath = require('selenium-server').path;
// http://nightwatchjs.org/guide#settings-file
module.exports = {
@@ -12,7 +13,7 @@ module.exports = {
selenium: {
start_process: true,
server_path: 'node_modules/selenium-server/lib/runner/selenium-server-standalone-2.53.0.jar',
server_path: seleniumServerPath,
host: '127.0.0.1',
port: 4444,
cli_args: {
+5 -46
View File
@@ -3,53 +3,12 @@
// we are also using it with karma-webpack
// https://github.com/webpack/karma-webpack
const path = require('path');
const merge = require('webpack-merge');
const baseConfig = require('../../../webpack/webpack.base.conf');
const utils = require('../../../webpack/utils');
const webpack = require('webpack');
const projectRoot = path.resolve(__dirname, '../../../');
const testEnv = require('../../../webpack/config/test.env');
// Necessary for babel to respect the env version of .babelrc which is necessary
// Because inject-loader does not work with ["es2015", { modules: false }] that we use
// in order to let webpack2 handle the imports
const webpackConfig = merge(baseConfig, {
// use inline sourcemap for karma-sourcemap-loader
module: {
loaders: utils.styleLoaders(),
},
devtool: '#inline-source-map',
vue: {
loaders: {
js: 'isparta',
},
},
plugins: [
new webpack.DefinePlugin({
'process.env': testEnv,
}),
],
});
// no need for app entry during tests
delete webpackConfig.entry;
// make sure isparta loader is applied before eslint
webpackConfig.module.preLoaders = webpackConfig.module.preLoaders || [];
webpackConfig.module.preLoaders.unshift({
test: /\.js$/,
loader: 'isparta',
include: [
path.resolve(projectRoot, 'website/client'),
path.resolve(projectRoot, 'website/common'),
],
});
// only apply babel for test files when using isparta
webpackConfig.module.loaders.some((loader) => {
if (loader.loader === 'babel') {
loader.include = path.resolve(projectRoot, 'test/client/unit');
return true;
}
});
process.env.BABEL_ENV = process.env.NODE_ENV; // eslint-disable-line no-process-env
const webpackConfig = require('../../../webpack/webpack.test.conf');
module.exports = function (config) {
config.set({
@@ -0,0 +1,25 @@
import deepFreeze from 'client/libs/deepFreeze';
describe('deepFreeze', () => {
it('works as expected', () => {
let obj = {
a: 1,
b () {
return this.a;
},
nested: {
c: 2,
nestedTwice: {
d: 1,
},
},
};
let result = deepFreeze(obj);
expect(result).to.equal(obj);
expect(Object.isFrozen(obj)).to.equal(true);
expect(Object.isFrozen(obj.nested)).to.equal(true);
expect(Object.isFrozen(obj.nested.nestedTwice)).to.equal(true);
});
});
+1 -1
View File
@@ -1,5 +1,5 @@
import Vue from 'vue';
import storeInjector from 'inject?-vue!client/store';
import storeInjector from 'inject-loader?-vue!client/store';
import { mapState, mapGetters, mapActions } from 'client/store';
import { flattenAndNamespace } from 'client/store/helpers/internals';
+67
View File
@@ -25,4 +25,71 @@ describe('common.fns.statsComputed', () => {
expect(result.str).to.eql(0);
expect(result.maxMP).to.eql(30);
});
it('calculates stat bonuses for equipment', () => {
user.items.gear.equipped.weapon = 'weapon_rogue_1';
let result = statsComputed(user);
expect(result.str).to.eql(2);
expect(result.gearBonus.str).to.eql(2);
});
it('calculates stat bonuses for class', () => {
user.items.gear.equipped.weapon = 'weapon_warrior_1';
let result = statsComputed(user);
expect(result.str).to.eql(4.5);
expect(result.gearBonus.str).to.eql(3);
expect(result.classBonus.str).to.eql(1.5);
});
it('calculates stat bonuses for level', () => {
user.stats.lvl = 25;
let result = statsComputed(user);
expect(result.str).to.eql(12);
expect(result.levelBonus.str).to.eql(12);
});
it('correctly caps level stat bonuses', () => {
user.stats.lvl = 150;
let result = statsComputed(user);
expect(result.str).to.eql(50);
expect(result.levelBonus.str).to.eql(50);
});
it('sets baseStat field', () => {
user.stats.str = 20;
let result = statsComputed(user);
expect(result.str).to.eql(20);
expect(result.baseStat.str).to.eql(20);
});
it('sets buffs field', () => {
user.stats.buffs.str = 150;
let result = statsComputed(user);
expect(result.str).to.eql(150);
expect(result.buff.str).to.eql(150);
});
it('calculates mp from intelligence', () => {
user.stats.int = 150;
user.stats.buffs.int = 50;
let result = statsComputed(user);
expect(result.maxMP).to.eql(430);
});
it('calculates stat bonuses for back equipment', () => {
user.items.gear.equipped.back = 'back_special_takeThis';
let result = statsComputed(user);
expect(result.int).to.eql(1);
expect(result.per).to.eql(1);
expect(result.con).to.eql(1);
expect(result.str).to.eql(1);
});
});
+1 -1
View File
@@ -55,8 +55,8 @@ export async function resetHabiticaDB () {
username: 'username',
lowerCaseUsername: 'username',
email: 'username@email.com',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
passwordHashMethod: 'bcrypt',
},
},
}, (insertErr) => {
+6
View File
@@ -19,10 +19,16 @@ module.exports = {
// npm install --save-dev compression-webpack-plugin
productionGzip: false,
productionGzipExtensions: ['js', 'css'],
// Run the build command with an extra argument to
// View the bundle analyzer report after build finishes:
// `npm run build --report`
// Set to `true` or `false` to always turn it on or off
bundleAnalyzerReport: process.env.npm_config_report, // eslint-disable-line no-process-env
},
dev: {
env: devEnv,
port: 8080,
autoOpenBrowser: true,
assetsSubDirectory: 'static',
assetsPublicPath: '/',
staticAssetsDirectory,
+1 -1
View File
@@ -2,5 +2,5 @@ const merge = require('webpack-merge');
const devEnv = require('./dev.env');
module.exports = merge(devEnv, {
NODE_ENV: '"testing"',
NODE_ENV: '"test"',
});
+7 -2
View File
@@ -4,8 +4,13 @@ const path = require('path');
const express = require('express');
const webpack = require('webpack');
const config = require('./config');
if (!process.env.NODE_ENV) {
process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV);
}
const proxyMiddleware = require('http-proxy-middleware');
const webpackConfig = process.env.NODE_ENV === 'testing' ?
const webpackConfig = process.env.NODE_ENV === 'test' ?
require('./webpack.prod.conf') :
require('./webpack.dev.conf');
@@ -41,7 +46,7 @@ Object.keys(proxyTable).forEach((context) => {
if (typeof options === 'string') {
options = { target: options };
}
app.use(proxyMiddleware(context, options));
app.use(proxyMiddleware(options.filter || context, options));
});
// handle fallback for HTML5 history API
+9 -4
View File
@@ -11,7 +11,7 @@ exports.assetsPath = (_path) => {
return path.posix.join(assetsSubDirectory, _path);
};
exports.cssLoaders = (options) => {
exports.cssLoaders = function cssLoaders (options) {
options = options || {};
// generate loader string to be used with extract text plugin
function generateLoaders (loaders) {
@@ -24,17 +24,22 @@ exports.cssLoaders = (options) => {
loader = `${loader}-loader`;
extraParamChar = '?';
}
return `${loader}${(options.sourceMap ? extraParamChar + 'sourceMap' : '')}`; // eslint-disable-line prefer-template
return loader + (options.sourceMap ? `${extraParamChar}sourceMap` : '');
}).join('!');
// Extract CSS when that option is specified
// (which is the case during production build)
if (options.extract) {
return ExtractTextPlugin.extract('vue-style-loader', sourceLoader);
return ExtractTextPlugin.extract({
use: sourceLoader,
fallback: 'vue-style-loader',
});
} else {
return ['vue-style-loader', sourceLoader].join('!');
}
}
// http://vuejs.github.io/vue-loader/configurations/extract-css.html
// http://vuejs.github.io/vue-loader/en/configurations/extract-css.html
return {
css: generateLoaders(['css']),
postcss: generateLoaders(['css']),
+36 -43
View File
@@ -19,8 +19,12 @@ const baseConfig = {
filename: '[name].js',
},
resolve: {
extensions: ['', '.js', '.vue'],
fallback: [path.join(__dirname, '../node_modules')],
extensions: ['*', '.js', '.vue', '.json'],
modules: [
path.join(__dirname, '..', 'website'),
path.join(__dirname, '..', 'test/client/unit'),
path.join(__dirname, '..', 'node_modules'),
],
alias: {
jquery: 'jquery/src/jquery',
website: path.resolve(__dirname, '../website'),
@@ -30,9 +34,6 @@ const baseConfig = {
components: path.resolve(__dirname, '../website/client/components'),
},
},
resolveLoader: {
fallback: [path.join(__dirname, '../node_modules')],
},
plugins: [
new webpack.ProvidePlugin({
$: 'jquery',
@@ -40,38 +41,34 @@ const baseConfig = {
}),
],
module: {
preLoaders: !IS_PROD ? [
rules: [
{
test: /\.vue$/,
loader: 'eslint',
include: projectRoot,
exclude: /node_modules/,
loader: 'vue-loader',
options: {
loaders: utils.cssLoaders({
sourceMap: IS_PROD ?
config.build.productionSourceMap :
config.dev.cssSourceMap,
extract: IS_PROD,
}),
postcss: [
autoprefixer({
browsers: ['last 2 versions'],
}),
postcssEasyImport(),
],
},
},
{
test: /\.js$/,
loader: 'eslint',
loader: 'babel-loader',
include: projectRoot,
exclude: /node_modules/,
},
] : [],
loaders: [
{
test: /\.vue$/,
loader: 'vue',
},
{
test: /\.js$/,
loader: 'babel',
include: projectRoot,
exclude: /node_modules/,
},
{
test: /\.json$/,
loader: 'json',
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url',
loader: 'url-loader',
query: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]'),
@@ -79,7 +76,7 @@ const baseConfig = {
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url',
loader: 'url-loader',
query: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]'),
@@ -87,25 +84,21 @@ const baseConfig = {
},
],
},
vue: {
loaders: utils.cssLoaders(),
postcss: [
autoprefixer({
browsers: ['last 2 versions'],
}),
postcssEasyImport({
glob: true,
}),
],
},
};
if (!IS_PROD) {
const eslintFriendlyFormatter = require('eslint-friendly-formatter'); // eslint-disable-line global-require
baseConfig.eslint = {
formatter: eslintFriendlyFormatter,
emitWarning: true,
};
baseConfig.module.rules.unshift({
test: /\.(js|vue)$/,
loader: 'eslint-loader',
enforce: 'pre',
include: projectRoot,
options: {
formatter: eslintFriendlyFormatter,
emitWarning: true,
},
exclude: /node_modules/,
});
}
module.exports = baseConfig;
+3 -4
View File
@@ -12,16 +12,15 @@ Object.keys(baseWebpackConfig.entry).forEach((name) => {
module.exports = merge(baseWebpackConfig, {
module: {
loaders: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }),
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }),
},
// eval-source-map is faster for development
devtool: '#eval-source-map',
// cheap-module-eval-source-map is faster for development
devtool: '#cheap-module-eval-source-map',
plugins: [
new webpack.DefinePlugin({
'process.env': config.dev.env,
}),
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
new webpack.optimize.OccurenceOrderPlugin(),
new webpack.HotModuleReplacementPlugin(),
// https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
+12 -11
View File
@@ -8,13 +8,13 @@ const merge = require('webpack-merge');
const baseWebpackConfig = require('./webpack.base.conf');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const env = process.env.NODE_ENV === 'testing' ?
const env = process.env.NODE_ENV === 'test' ?
require('./config/test.env') :
config.build.env;
const webpackConfig = merge(baseWebpackConfig, {
module: {
loaders: utils.styleLoaders({ sourceMap: config.build.productionSourceMap, extract: true }),
rules: utils.styleLoaders({ sourceMap: config.build.productionSourceMap, extract: true }),
},
devtool: config.build.productionSourceMap ? '#source-map' : false,
output: {
@@ -22,12 +22,6 @@ const webpackConfig = merge(baseWebpackConfig, {
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js'),
},
vue: {
loaders: utils.cssLoaders({
sourceMap: config.build.productionSourceMap,
extract: true,
}),
},
plugins: [
// http://vuejs.github.io/vue-loader/workflow/production.html
new webpack.DefinePlugin({
@@ -37,15 +31,17 @@ const webpackConfig = merge(baseWebpackConfig, {
compress: {
warnings: false,
},
sourceMap: true,
}),
new webpack.optimize.OccurenceOrderPlugin(),
// extract css into its own file
new ExtractTextPlugin(utils.assetsPath('css/[name].[contenthash].css')),
new ExtractTextPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css'),
}),
// generate dist index.html with correct asset hash for caching.
// you can customize output by editing /index.html
// see https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: process.env.NODE_ENV === 'testing' ?
filename: process.env.NODE_ENV === 'test' ?
'index.html' :
config.build.index,
template: './website/client/index.html',
@@ -97,4 +93,9 @@ if (config.build.productionGzip) {
);
}
if (config.build.bundleAnalyzerReport) {
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; // eslint-disable-line global-require
webpackConfig.plugins.push(new BundleAnalyzerPlugin());
}
module.exports = webpackConfig;
+24
View File
@@ -0,0 +1,24 @@
// This is the webpack config used for unit tests.
const merge = require('webpack-merge');
const baseConfig = require('./webpack.base.conf');
const utils = require('./utils');
const webpack = require('webpack');
const testEnv = require('./config/test.env');
const webpackConfig = merge(baseConfig, {
// use inline sourcemap for karma-sourcemap-loader
module: {
rules: utils.styleLoaders(),
},
devtool: '#inline-source-map',
plugins: [
new webpack.DefinePlugin({
'process.env': testEnv,
}),
],
});
// no need for app entry during tests
delete webpackConfig.entry;
module.exports = webpackConfig;
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

+2 -2
View File
@@ -1,9 +1,9 @@
/* Comment out for holiday events */
/* .npc_ian {
.npc_ian {
background: url("/npc_ian.gif") no-repeat;
width: 78px;
height: 135px;
} */
}
.quest_burnout {
background: url("/quest_burnout.gif") no-repeat;
+129 -147
View File
@@ -1,30 +1,30 @@
.promo_android {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1545px -180px;
background-position: -1547px -553px;
width: 175px;
height: 175px;
}
.promo_backgrounds_armoire_201602 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -284px -1193px;
background-position: -845px -1041px;
width: 141px;
height: 294px;
}
.promo_backgrounds_armoire_201603 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -142px -1193px;
background-position: -703px -1041px;
width: 141px;
height: 294px;
}
.promo_backgrounds_armoire_201604 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1264px 0px;
background-position: -282px -1041px;
width: 140px;
height: 441px;
}
.promo_backgrounds_armoire_201605 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -846px -600px;
background-position: -1124px 0px;
width: 140px;
height: 441px;
}
@@ -42,45 +42,51 @@
}
.promo_backgrounds_armoire_201608 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1264px -442px;
background-position: -142px -600px;
width: 140px;
height: 439px;
}
.promo_backgrounds_armoire_201609 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1405px 0px;
background-position: -283px -600px;
width: 139px;
height: 438px;
}
.promo_backgrounds_armoire_201610 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -141px -600px;
background-position: -1265px 0px;
width: 140px;
height: 441px;
}
.promo_backgrounds_armoire_201611 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -282px -600px;
background-position: -141px -1041px;
width: 140px;
height: 441px;
}
.promo_backgrounds_armoire_201612 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -423px -600px;
background-position: -1406px -442px;
width: 140px;
height: 441px;
}
.promo_backgrounds_armoire_201701 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -564px -600px;
background-position: -1124px -442px;
width: 140px;
height: 441px;
}
.promo_backtoschool {
.promo_backgrounds_armoire_201702 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1746px -433px;
width: 150px;
height: 150px;
background-position: -982px 0px;
width: 141px;
height: 441px;
}
.promo_backgrounds_armoire_201703 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -840px 0px;
width: 141px;
height: 441px;
}
.promo_burnout {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
@@ -90,139 +96,151 @@
}
.promo_chairs_glasses {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1651px -532px;
background-position: 0px -1483px;
width: 51px;
height: 210px;
}
.promo_checkin_incentives {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: 0px -1193px;
background-position: -987px -1041px;
width: 141px;
height: 294px;
}
.promo_classes_fall_2014 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: 0px -1488px;
background-position: -1547px -452px;
width: 321px;
height: 100px;
}
.promo_classes_fall_2015 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -831px -1042px;
background-position: -703px -1336px;
width: 377px;
height: 99px;
}
.promo_classes_fall_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1405px -439px;
background-position: -1547px -103px;
width: 103px;
height: 348px;
}
.promo_coffee_mug {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1545px 0px;
background-position: -1651px -103px;
width: 200px;
height: 179px;
}
.promo_contrib_spotlight_Keith {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1947px -1248px;
background-position: -90px -1694px;
width: 87px;
height: 111px;
}
.promo_contrib_spotlight_beffymaroo {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1897px -433px;
background-position: -1794px -729px;
width: 114px;
height: 147px;
}
.promo_contrib_spotlight_blade {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1636px -897px;
background-position: 0px -1694px;
width: 89px;
height: 111px;
}
.promo_contrib_spotlight_cantras {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1545px -1185px;
background-position: -178px -1694px;
width: 87px;
height: 109px;
}
.promo_contrib_spotlight_megan {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1545px -897px;
background-position: -143px -1483px;
width: 90px;
height: 111px;
}
.promo_contrib_spotlight_shanaqui {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -52px -1483px;
width: 90px;
height: 111px;
}
.promo_cow {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1123px 0px;
background-position: -1406px 0px;
width: 140px;
height: 441px;
}
.promo_cupid_potions {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -564px -1041px;
width: 138px;
height: 441px;
}
.promo_dilatoryDistress {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -728px -1678px;
background-position: -1910px -970px;
width: 90px;
height: 90px;
}
.promo_egg_mounts {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -567px -1193px;
background-position: -824px -600px;
width: 280px;
height: 147px;
}
.promo_enchanted_armoire {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -928px -1341px;
background-position: -1081px -1336px;
width: 374px;
height: 76px;
}
.promo_enchanted_armoire_201507 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1746px -974px;
background-position: -853px -751px;
width: 217px;
height: 90px;
}
.promo_enchanted_armoire_201508 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1746px -1157px;
background-position: -1270px -1133px;
width: 180px;
height: 90px;
}
.promo_enchanted_armoire_201509 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -182px -1678px;
background-position: -1910px -1243px;
width: 90px;
height: 90px;
}
.promo_enchanted_armoire_201511 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1746px -1510px;
background-position: -1270px -1224px;
width: 122px;
height: 90px;
}
.promo_enchanted_armoire_201601 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -987px -691px;
background-position: -1910px -1425px;
width: 90px;
height: 90px;
}
.promo_floral_potions {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1545px -532px;
background-position: -1547px -729px;
width: 105px;
height: 273px;
}
.promo_ghost_potions {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: 0px -600px;
background-position: -423px -1041px;
width: 140px;
height: 441px;
}
.promo_habitica {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1545px -356px;
background-position: -1723px -553px;
width: 175px;
height: 175px;
}
@@ -234,211 +252,211 @@
}
.promo_habitoween_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -982px 0px;
background-position: -1265px -442px;
width: 140px;
height: 441px;
}
.promo_haunted_hair {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1846px -1248px;
background-position: -840px -442px;
width: 100px;
height: 137px;
}
.promo_holly_potions {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -840px 0px;
background-position: 0px -600px;
width: 141px;
height: 440px;
}
.promo_item_notif {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1746px -182px;
background-position: -1651px -283px;
width: 249px;
height: 102px;
}
.promo_mystery_201405 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -455px -1678px;
background-position: -1910px -1698px;
width: 90px;
height: 90px;
}
.promo_mystery_201406 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1405px -888px;
background-position: -1910px -418px;
width: 90px;
height: 96px;
}
.promo_mystery_201407 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1703px -669px;
background-position: -306px -536px;
width: 42px;
height: 62px;
}
.promo_mystery_201408 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1966px -1396px;
background-position: -1469px -1041px;
width: 60px;
height: 71px;
}
.promo_mystery_201409 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -637px -1678px;
background-position: -1910px -1607px;
width: 90px;
height: 90px;
}
.promo_mystery_201410 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1666px -806px;
background-position: -1836px -1351px;
width: 72px;
height: 63px;
}
.promo_mystery_201411 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -819px -1678px;
background-position: -1910px -1516px;
width: 90px;
height: 90px;
}
.promo_mystery_201412 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1703px -602px;
background-position: -1071px -751px;
width: 42px;
height: 66px;
}
.promo_mystery_201501 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1660px -1097px;
background-position: -1852px -103px;
width: 48px;
height: 63px;
}
.promo_mystery_201502 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -699px -448px;
background-position: -1910px -515px;
width: 90px;
height: 90px;
}
.promo_mystery_201503 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -987px -600px;
background-position: -1910px -1061px;
width: 90px;
height: 90px;
}
.promo_mystery_201504 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1660px -1009px;
background-position: -1456px -1336px;
width: 60px;
height: 69px;
}
.promo_mystery_201505 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -546px -1678px;
background-position: -1910px -788px;
width: 90px;
height: 90px;
}
.promo_mystery_201506 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1703px -532px;
background-position: -1852px -167px;
width: 42px;
height: 69px;
}
.promo_mystery_201507 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1636px -1295px;
background-position: -1910px -106px;
width: 90px;
height: 105px;
}
.promo_mystery_201508 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -840px -441px;
background-position: -146px -1595px;
width: 93px;
height: 90px;
}
.promo_mystery_201509 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -364px -1678px;
background-position: -1910px -606px;
width: 90px;
height: 90px;
}
.promo_mystery_201510 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -982px -442px;
background-position: -240px -1595px;
width: 93px;
height: 90px;
}
.promo_mystery_201511 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -273px -1678px;
background-position: -428px -1595px;
width: 90px;
height: 90px;
}
.promo_mystery_201512 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1945px -1065px;
background-position: -1848px -1171px;
width: 60px;
height: 81px;
}
.promo_mystery_201601 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1545px -806px;
background-position: -1393px -1224px;
width: 120px;
height: 90px;
}
.promo_mystery_201602 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -91px -1678px;
background-position: -1910px -1334px;
width: 90px;
height: 90px;
}
.promo_mystery_201603 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: 0px -1678px;
background-position: -1910px -879px;
width: 90px;
height: 90px;
}
.promo_mystery_201604 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1264px -882px;
background-position: -52px -1595px;
width: 93px;
height: 90px;
}
.promo_mystery_201605 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -987px -873px;
background-position: -1910px -697px;
width: 90px;
height: 90px;
}
.promo_mystery_201606 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1545px -1295px;
background-position: -1910px 0px;
width: 90px;
height: 105px;
}
.promo_mystery_201607 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -987px -782px;
background-position: -1910px -1152px;
width: 90px;
height: 90px;
}
.promo_mystery_201608 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1927px -1157px;
background-position: -334px -1595px;
width: 93px;
height: 90px;
}
.promo_mystery_201609 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1123px -884px;
background-position: -1451px -1133px;
width: 93px;
height: 90px;
}
.promo_mystery_201610 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1964px -883px;
background-position: -1836px -1260px;
width: 63px;
height: 84px;
}
.promo_mystery_201611 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1405px -788px;
background-position: -1910px -318px;
width: 90px;
height: 99px;
}
@@ -448,207 +466,171 @@
width: 558px;
height: 294px;
}
.promo_mystery_201701 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1910px -212px;
width: 90px;
height: 105px;
}
.promo_mystery_201702 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -703px -854px;
width: 279px;
height: 147px;
}
.promo_mystery_3014 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1746px -883px;
background-position: -1653px -877px;
width: 217px;
height: 90px;
}
.promo_new_hair_fall2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1123px -442px;
background-position: 0px -1041px;
width: 140px;
height: 441px;
}
.promo_orca {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1860px -1396px;
background-position: -983px -854px;
width: 105px;
height: 105px;
}
.promo_partyhats {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1746px -1601px;
background-position: -1651px -386px;
width: 115px;
height: 47px;
}
.promo_pastel_skin {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1209px -1042px;
background-position: -1547px -1087px;
width: 330px;
height: 83px;
}
.customize-option.promo_pastel_skin {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1234px -1057px;
background-position: -1572px -1102px;
width: 60px;
height: 60px;
}
.promo_peppermint_flame {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1746px -735px;
background-position: -1406px -884px;
width: 140px;
height: 147px;
}
.promo_pet_skins {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1887px -735px;
background-position: -1653px -729px;
width: 140px;
height: 147px;
}
.customize-option.promo_pet_skins {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1912px -750px;
background-position: -1678px -744px;
width: 60px;
height: 60px;
}
.promo_pyromancer {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1746px -1396px;
background-position: -699px -448px;
width: 113px;
height: 113px;
}
.promo_rainbow_armor {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1633px -1185px;
background-position: -266px -1694px;
width: 92px;
height: 103px;
}
.promo_seasonal_shop_fall_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -848px -1193px;
background-position: -423px -854px;
width: 279px;
height: 147px;
}
.promo_shimmer_hair {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -322px -1488px;
background-position: -1547px -1003px;
width: 330px;
height: 83px;
}
.promo_splashyskins {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1746px -1065px;
background-position: -1270px -1041px;
width: 198px;
height: 91px;
}
.customize-option.promo_splashyskins {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1771px -1080px;
background-position: -1295px -1056px;
width: 60px;
height: 60px;
}
.promo_spooky_sparkles_fall_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -426px -1193px;
background-position: -1129px -1041px;
width: 140px;
height: 294px;
}
.promo_spring_classes_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1128px -1193px;
background-position: -1547px 0px;
width: 362px;
height: 102px;
}
.promo_springclasses2014 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1746px -91px;
background-position: -1547px -1351px;
width: 288px;
height: 90px;
}
.promo_springclasses2015 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1746px 0px;
background-position: -1547px -1260px;
width: 288px;
height: 90px;
}
.promo_staff_spotlight_Lemoness {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1897px -584px;
background-position: -1124px -884px;
width: 102px;
height: 146px;
}
.promo_staff_spotlight_Viirus {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1903px -285px;
background-position: -1265px -884px;
width: 119px;
height: 147px;
}
.promo_staff_spotlight_paglias {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1746px -1248px;
background-position: -982px -442px;
width: 99px;
height: 147px;
}
.promo_startingover {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1746px -584px;
width: 150px;
height: 150px;
}
.promo_summer_classes_2014 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -401px -1042px;
background-position: -423px -751px;
width: 429px;
height: 102px;
}
.promo_summer_classes_2015 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: 0px -1589px;
background-position: -1547px -1171px;
width: 300px;
height: 88px;
}
.promo_summer_classes_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: 0px -1042px;
background-position: -423px -600px;
width: 400px;
height: 150px;
}
.promo_takeThis_gear {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1545px -1009px;
width: 114px;
height: 87px;
}
.promo_takethis_armor {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1545px -1097px;
background-position: -234px -1483px;
width: 114px;
height: 87px;
}
.promo_turkey_day_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -705px -600px;
width: 140px;
height: 441px;
}
.promo_unconventional_armor {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1651px -743px;
width: 60px;
height: 60px;
}
.promo_unconventional_armor2 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1964px -974px;
width: 70px;
height: 74px;
}
.promo_updos {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1746px -285px;
width: 156px;
height: 147px;
}
.promo_veteran_pets {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1869px -1510px;
width: 146px;
height: 75px;
}
.promo_winter_classes_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -567px -1341px;
width: 360px;
height: 90px;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1016 KiB

+98 -14
View File
@@ -1,48 +1,96 @@
.promo_takeThis_gear {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -643px -732px;
width: 114px;
height: 87px;
}
.promo_task_planning {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -282px -536px;
width: 240px;
height: 195px;
}
.promo_turkey_day_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: 0px -440px;
width: 140px;
height: 441px;
}
.promo_unconventional_armor {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1108px -558px;
width: 60px;
height: 60px;
}
.promo_unconventional_armor2 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1108px -483px;
width: 70px;
height: 74px;
}
.promo_updos {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -885px -655px;
width: 156px;
height: 147px;
}
.promo_veteran_pets {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1038px -803px;
width: 146px;
height: 75px;
}
.promo_winter_classes_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -282px -732px;
width: 360px;
height: 90px;
}
.promo_winter_classes_2017 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: 0px -220px;
background-position: -452px 0px;
width: 432px;
height: 144px;
}
.promo_winter_fireworks {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -754px -373px;
background-position: -1042px -655px;
width: 138px;
height: 147px;
}
.promo_winterclasses2015 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -452px -262px;
background-position: -452px -326px;
width: 325px;
height: 110px;
}
.promo_wintery_skins {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: 0px -365px;
background-position: -141px -440px;
width: 140px;
height: 441px;
}
.customize-option.promo_wintery_skins {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -25px -380px;
background-position: -166px -455px;
width: 60px;
height: 60px;
}
.promo_winteryhair {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -778px -262px;
background-position: -885px -803px;
width: 152px;
height: 75px;
}
.avatar_variety {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -452px 0px;
background-position: -282px -440px;
width: 498px;
height: 95px;
}
.npc_viirus {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -452px -524px;
background-position: -453px -882px;
width: 108px;
height: 90px;
}
@@ -52,33 +100,69 @@
width: 451px;
height: 219px;
}
.scene_coding {
.promo_backtoschool {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -452px -373px;
background-position: 0px -882px;
width: 150px;
height: 150px;
}
.promo_cooking {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: 0px -220px;
width: 396px;
height: 219px;
}
.promo_startingover {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -151px -882px;
width: 150px;
height: 150px;
}
.promo_valentines {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -523px -536px;
width: 309px;
height: 147px;
}
.promo_working_out {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -885px 0px;
width: 300px;
height: 150px;
}
.scene_coding {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -302px -882px;
width: 150px;
height: 150px;
}
.scene_eco_friendly {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -885px -483px;
width: 222px;
height: 171px;
}
.scene_phone_peek {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -603px -373px;
background-position: -723px -145px;
width: 150px;
height: 150px;
}
.welcome_basic_avatars {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -699px -96px;
background-position: -885px -151px;
width: 246px;
height: 165px;
}
.welcome_promo_party {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -141px -365px;
background-position: -452px -145px;
width: 270px;
height: 180px;
}
.welcome_sample_tasks {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -452px -96px;
background-position: -885px -317px;
width: 246px;
height: 165px;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 188 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 458 KiB

After

Width:  |  Height:  |  Size: 478 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 74 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 132 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 156 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 141 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 157 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 156 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

After

Width:  |  Height:  |  Size: 170 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 126 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 64 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 115 KiB

File diff suppressed because it is too large Load Diff

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