Compare commits

...

174 Commits

Author SHA1 Message Date
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
Sabe Jones
9b456d1760 3.70.0 2017-01-21 03:13:36 +00:00
Sabe Jones
acf1031317 chore(i18n): update locales 2017-01-21 02:50:49 +00:00
Sabe Jones
5d45204d8b chore(sprites): compile 2017-01-21 02:41:31 +00:00
Sabe Jones
37a71924fe Incentives, Batch 2 (#8435)
* feat(content): Incentives batch 2 gear

* feat(incentives): incentives 55-100

* chore(migration): hand out missed incentive

* refactor(constant): export MAX_INCENTIVES

* fix(incentives): correct const import
and say "Royal Purple Potion" not "Royal Purple"
2017-01-20 20:36:38 -06:00
Alys
9cf2408988 change name of Newbies Guild to Habitica Help: Ask a Question guild (#8424)
* change name of Newbies Guild to Habitica Help: Ask a Question guild

* remove ": Ask a Question" from "Habitica Help: Ask a Question" in locations where we are linking to the guild or its wiki page
2017-01-21 09:20:39 +10:00
Keith Holliday
638525f8d8 Stripe refactor to lib (#8417)
* Moved stripe tests to folder

* Abstracted stripe payments logic to lib

* Added initial unit test for stripe payment

* Added subscription tests

* Added tests for regulare purchases

* Added tests for edit subscription

* Added cancel tests

* Added integration tests

* Fixed lint issues

* Fixed lint issue
2017-01-20 16:08:52 -07:00
Keith Holliday
2c37ba3cee Added migration runner that allows for migrations to use server code (#8436)
* Added migration runner that allows for migrations to use server code

* Replaced example script in migration runner
2017-01-20 16:08:01 -06:00
Sabe Jones
ad5b2fe540 3.69.1 2017-01-19 23:02:41 +00:00
Sabe Jones
bfb6daad51 fix(docs): separate def blocks 2017-01-19 23:02:22 +00:00
Sabe Jones
281b8e2b7c 3.69.0 2017-01-19 22:09:56 +00:00
Sabe Jones
3f88ea2378 fix(typo): extra brackets 2017-01-19 22:04:36 +00:00
Sabe Jones
9c6275d4ab chore(i18n): update locales 2017-01-19 20:31:23 +00:00
Sabe Jones
fd3c8ddc8b chore(sprites): compile 2017-01-19 20:22:32 +00:00
Sabe Jones
72f47ad4e6 chore(news): misc Bailey 2017-01-19 20:19:15 +00:00
ean
74c9a1b02d Add missing text to buttons on the party / guild invite modal - Fixes #8391 (#8402)
* Fix missing text in party / guild invite modal

* add tests and abstract logic

* use translation lib for test
2017-01-19 12:17:38 -07:00
Keith Holliday
ffa561473c [WIP] Amazon refactor to lib (#8403)
* Moved amazon tests to folder

* Abstracted amazon payment code and added initial test

* Abstracted cancel and subscribe logic to amazon payment lib

* Added arg checks to checkout

* Added constants. Added more subscription test

* Added with arg checks to cancel

* Fixed linting issues

* Added integration tests for amazon subscribe cancel

* Added integration test for amazon checkout

* Added integration test for amazon subscribe

* Added coupon unit test

* Fixed lint

* Fixed minor test issue and changed header expectations

* Fixed line endings
2017-01-19 08:30:30 -07:00
MathWhiz
080ffae4e1 apiDoc /chat Documentation (#8277)
* apiDoc /chat Documentation

* Add {NotFound} to FlagOwnMessage error

* Quick changes

* Update chat.js
2017-01-18 22:28:00 -07:00
Gerardo Saca
e395182c95 Fix User > Profile showing {getProgressDisplay()} - fixes #8412 (#8421)
* Fix User > Profile showing {getProgressDisplay()}

* Remove bad nextRewardAt check
2017-01-18 22:25:37 -07:00
Matteo Pagliazzi
68f4275c44 fix tests failures (#8422)
* fix tests failures

* make sure _meta is not public

* fix typo

* fix typo
2017-01-18 20:17:56 +01:00
Alys
4bf4c3a6c2 remove Windows line breaks 2017-01-19 05:00:54 +10:00
Sabe Jones
f00bb29192 fix(quest): correct pt 2 label 2017-01-18 16:25:52 +00:00
Alyssa Batula
016447ec77 Critical Hits now affect boss damage fixes #5429 (#8092)
* Moved critical hit calculation from _addPoints() to _calculateDelta(). Added user as an input argument to _calculateDelta() so for critical hit calculation

* Changed test to expect task value of 1.5 after critical hit

* Revert "Moved critical hit calculation from _addPoints() to _calculateDelta(). Added user as an input argument to _calculateDelta() so for critical hit calculation"

This reverts commit 51b8ab6498.

* Moved critical hit calculation to _changeTaskValue(). Use value stored in user._tmp.crit in _addPoints()

* Test is no longer affected by critical hits

* Removed unneeded comment

* Added WIP test of critical hits

* Want the crit function to return 2 to test critical hits

* Changed crit function to export as a function within an object so that it can be stubbed for testing. References to the crit() function were updated to call crit.crit() instead

* Added test for increased experience on critical hits
2017-01-18 08:11:39 -07:00
Dumindu Buddhika
fa024e071b added message saying quest is started (#8107)
* added chat message saying quest has started

* added info to message

* added information as meta data

* fixed failing test case

* added new tests
2017-01-18 08:08:47 -07:00
Keith Holliday
28fec237fe Group plans misc fixes (#8388)
* Added notification for approval request in the group leaders language

* Added test for group task meta actions. Added sync when user claims

* Added tests for group task actions. Ensured assigned members are synce when added or removed

* Fixed approval required toggle

* Added support for users with comma in their name

* Fixed sync issue when user is approved and reloads the website

* Added advance options for group rewards

* Added back ticks to group claim message

* Fixed disappearing tasks that need approval

* Up chat limit to 400 for subbed groups

* Fixed line endings

* Updated activie subscription check

* Added group isSubscribed function

* Changed to isAfter
2017-01-18 07:54:49 -07:00
Sabe Jones
e4bb82768c fix(sprites): add large-1 to repo 2017-01-18 01:52:14 +00:00
Sabe Jones
65eca22407 3.68.0 2017-01-18 01:26:58 +00:00
Sabe Jones
cea1597ee1 chore(i18n): update locales 2017-01-18 00:27:01 +00:00
Sabe Jones
3906952154 chore(sprites): compile 2017-01-18 00:16:01 +00:00
Sabe Jones
6169b9d0ae feat(content): new Gold quests (#8418)
and Wintery Skins, and fixes #8412
2017-01-17 16:03:10 -08:00
Declan Ramsay
69cac7e504 Add markdown formatting to task-edit title text (#8416) 2017-01-17 14:49:16 -07:00
mleah
febf3f0024 Adding "How to gift subscription" copy to Settings > Subscription Page: Fixes Issue 8341 (#8386)
* Issue 8341 updating subscriber json and subscription settings page with gifting a subscription directions

* Issue-8341 updating the layout of the settings - subscription page

* Issue-8341 removing extra comma from gift subscription copy

* Issue-8341 removing extra spans in subscription jade file
2017-01-17 14:48:21 -07:00
Matteo Pagliazzi
563f40e4b7 client: reorganize files, router and add inventory skeleton 2017-01-17 19:45:27 +01:00
Matteo Pagliazzi
e2b06161e1 client: add task component 2017-01-17 18:07:02 +01:00
Alys
e7de8b8e2f clarify two Notifications descriptions (onboarding, inactive account)
New words were supplied by Lemoness.
2017-01-17 07:32:35 +10:00
Matteo Pagliazzi
a0624d9507 fix build by not requiring optional package in prod 2017-01-16 18:31:29 +01:00
Matteo Pagliazzi
cddd0df4f2 chore(i18n) add overview and login incentives 2017-01-16 17:59:04 +01:00
Matteo Pagliazzi
220bfb3517 chore(i18n): update locales 2017-01-16 17:58:30 +01:00
Matteo Pagliazzi
2106a5ebd3 correct deps loading and shrinkwrap 2017-01-15 23:45:22 +01:00
Matteo Pagliazzi
bbffa9830b client: replace deprecated vue-resource with axios, lint more file 2017-01-15 20:49:15 +01:00
Matteo Pagliazzi
caa546eb62 client: add missing test files and use v-once 2017-01-15 20:08:36 +01:00
Alys
4e83059652 remove outdated information about changing your email address
This is being done to remove a redundant string that
contains an email address, in preparation for actioning
https://github.com/HabitRPG/habitica/issues/8385 ("Email addresses
should not probably be translatable")
2017-01-15 22:50:32 +10:00
Matteo Pagliazzi
903cdb36ef client: remove old code 2017-01-14 21:46:28 +01:00
Matteo Pagliazzi
d8128cc3db New Client: english translations and misc fixes (#8410)
* misc fixes and add english translations

* add tests
2017-01-14 21:12:11 +01:00
MathWhiz
f888e80b01 apiDoc: meta (#8167)
* apiDoc: meta

* Update modelsPaths.js

* Update error

* fix test

* Update GET-model_paths.test.js

* Fixed test
2017-01-13 14:06:46 -07:00
Matteo Pagliazzi
fd7aedbff2 chore(locales): add merch file 2017-01-13 19:09:47 +01:00
Matteo Pagliazzi
6c5234313d chore(i18n): update locales 2017-01-13 19:01:26 +01:00
Sabe Jones
08aa5758b4 3.67.1 2017-01-13 00:05:39 +00:00
Sabe Jones
1415e344c0 chore(i18n): update locales 2017-01-12 23:24:04 +00:00
Sabe Jones
2ce7915f06 fix(incentives): return 0 for upcoming at end of list
Also adds some new strings for upcoming content, adds a news announcement, and compiles a recent sprites fix.
2017-01-12 23:15:37 +00:00
MathWhiz
838c8b4e08 Update Royal Purple Flying PIg (#8401) 2017-01-12 15:06:25 -08:00
Keith Holliday
1590d955cd Group plans reorder tasks (#8358)
* Added move route for group tasks

* Added group task reorder to front end

* Added syncing with group task order

* Fixed linting issues

* Added missing exec and abstracted move code

* Added unit test for moveTask
2017-01-11 19:16:20 +01:00
MathWhiz
2690caed35 News translation link (#8393)
* Add translation link to news

* Add newsArchive string

* Translate news archive link

* Fix link?

* Add link to wiki
2017-01-11 09:16:10 -07:00
Sabe Jones
dc2d4fa10b fix(coveralls): update badge for /habitica 2017-01-10 22:25:40 -06:00
Sabe Jones
1540ec89ee 3.67.0 2017-01-10 20:49:54 +00:00
Sabe Jones
f304d4fe52 chore(migration): update Bailey to use monk 2017-01-10 20:47:58 +00:00
Sabe Jones
023e433a5c feat(content): Triceratops pet quest 2017-01-10 20:32:21 +00:00
Declan Ramsay
ef4aeb29ab Fixes task left in odd setting when cancelling editing with ESC / Escape Key; add click outside to close modal #8308 #8321 (#8382)
* Remove backdrop property from open modal call to allow click-outside to close, call cancelTaskEdit function on dismissal of edit modal via ESC or click-outside

* Remove commented out code, change catch function to normal (non-arrow)

* Modify task services test mock openModal function, to handle new use of subsequent promise resolution functionality

* Tidy old edit controls away
2017-01-10 12:04:34 -07:00
Jan Jorgensen
2b80931202 Fix home column style on medium screen widths fixes #8372 (#8387)
* handle medium screen width on home columns

* rename new-row helper used on home screen for todo column flow
2017-01-10 12:03:21 -07:00
myoshuGO
2950713712 Fixes #8227 (#8380)
* Fixes #8227

* Fixes #8227

* Fixes #8227
2017-01-10 12:00:53 -07:00
Jaka Kranjc
118f3bd1bb gulp/gulp-transifex-test.js: fixed variable name typo (#8389) 2017-01-10 10:07:35 +00:00
Alys
69f1343ea8 correct apidocs: /api/v3/user/... not /user/... 2017-01-09 19:57:26 +00:00
Keith Holliday
918ee02d64 Added hp back to party member query (#8378) 2017-01-07 20:59:17 -06:00
Sabe Jones
0cac34dd26 3.66.3 2017-01-07 22:42:50 +00:00
Sabe Jones
1c859fc91f chore(promo): end gift subs promo 2017-01-07 15:42:03 +00:00
Keith Holliday
857aa5827b Ensured group tasks are removed from places that challegnes tasks are (#8359)
* Ensured group tasks are removed from places that challegnes tasks are

* Added tests for user reset and class cast
2017-01-07 12:01:12 +01:00
Matteo Pagliazzi
28e8ec2d2c add BOSS_DAMAGE to valid notifications 2017-01-07 11:47:19 +01:00
Matteo Pagliazzi
856f13d213 chore(i18n): update locales 2017-01-07 11:43:53 +01:00
Sabe Jones
121fd38bd1 3.66.2 2017-01-06 21:28:35 +00:00
Keith Holliday
36d72f5f7a Claim task messages are now system messages (#8375) 2017-01-06 13:00:23 -08:00
Keith Holliday
f1b8bd80e7 Added fix to correctly check if object is sub group for all users (#8377) 2017-01-06 14:09:57 -06:00
Brian David
84d2ce6a3f Fixes issue #8315 (https://github.com/HabitRPG/habitica/issues/8315) (#8367) 2017-01-06 11:45:13 -06:00
Keith Holliday
76010e6c8f Added migration for profile restoration (#8355)
* Added migration for restoring profile data

* Updated migrations to use monk

* Fixed line endings

* Moved monk to dev dependencies
2017-01-06 11:07:21 -06:00
Keith Holliday
c707b6c99b Add check to ensure obj is not user (#8373) 2017-01-06 10:23:39 -06:00
1119 changed files with 62335 additions and 49362 deletions

View File

@@ -20,8 +20,4 @@ website/common/browserify.js
test/content/**/*
Gruntfile.js
gulpfile.js
gulp
webpack
test/client/e2e
test/client/unit/index.js
test/client/unit/karma.conf.js
gulp

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

View File

@@ -1,4 +1,4 @@
Habitica [![Build Status](https://travis-ci.org/HabitRPG/habitica.svg?branch=develop)](https://travis-ci.org/HabitRPG/habitica) [![Code Climate](https://codeclimate.com/github/HabitRPG/habitrpg.svg)](https://codeclimate.com/github/HabitRPG/habitrpg) [![Coverage Status](https://coveralls.io/repos/HabitRPG/habitrpg/badge.svg?branch=develop)](https://coveralls.io/r/HabitRPG/habitrpg?branch=develop) [![Bountysource](https://api.bountysource.com/badge/tracker?tracker_id=68393)](https://www.bountysource.com/trackers/68393-habitrpg?utm_source=68393&utm_medium=shield&utm_campaign=TRACKER_BADGE)
Habitica [![Build Status](https://travis-ci.org/HabitRPG/habitica.svg?branch=develop)](https://travis-ci.org/HabitRPG/habitica) [![Code Climate](https://codeclimate.com/github/HabitRPG/habitrpg.svg)](https://codeclimate.com/github/HabitRPG/habitrpg) [![Coverage Status](https://coveralls.io/repos/github/HabitRPG/habitica/badge.svg?branch=develop)](https://coveralls.io/github/HabitRPG/habitica?branch=develop) [![Bountysource](https://api.bountysource.com/badge/tracker?tracker_id=68393)](https://www.bountysource.com/trackers/68393-habitrpg?utm_source=68393&utm_medium=shield&utm_campaign=TRACKER_BADGE)
===============
[Habitica](https://habitica.com) is an open source habit building program which treats your life like a Role Playing Game. Level up as you succeed, lose HP as you fail, earn money to buy weapons and armor.

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"
}

View File

@@ -84,8 +84,8 @@ gulp.task('transifex:malformedStrings', () => {
let malformedString = `${lang} - ${file} - ${key} - ${translationString}`;
stringsWithMalformedInterpolations.push(malformedString);
} else if (englishOccurences.length !== translationOccurences.length && !malformedStringExceptions[key]) {
let missingInterploationString = `${lang} - ${file} - ${key} - ${translationString}`;
stringsWithIncorrectNumberOfInterpolations.push(missingInterploationString);
let missingInterpolationString = `${lang} - ${file} - ${key} - ${translationString}`;
stringsWithIncorrectNumberOfInterpolations.push(missingInterpolationString);
}
});
});

View File

@@ -0,0 +1,113 @@
var migrationName = '20170120_missing_incentive.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 missing Royal Purple Hatching Potion to users with 55+ check-ins
* Reduce users with impossible check-in counts to a reasonable number
*/
import monk from 'monk';
import common from '../website/common';
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
var dbUsers = monk(connectionString).get('users', { castIds: false });
function processUsers(lastId) {
// specify a query to limit the affected users (empty for all users):
var query = {
'loginIncentives': {$gt:54},
'migration': {$ne: migrationName},
};
if (lastId) {
query._id = {
$gt: lastId
}
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 250,
fields: [] // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
})
.then(updateUsers)
.catch(function (err) {
console.log(err);
return exiting(1, 'ERROR! ' + err);
});
}
var progressCount = 1000;
var count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
displayData();
return;
}
var userPromises = users.map(updateUser);
var lastUser = users[users.length - 1];
return Promise.all(userPromises)
.then(function () {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
var language = user.preferences.language || 'en';
var set = {'migration': migrationName};
var inc = {'items.hatchingPotions.RoyalPurple': 1};
if (user.loginIncentives > 58) {
set = {'migration': migrationName, 'loginIncentives': 58};
}
var push = {
'notifications': {
'type': 'LOGIN_INCENTIVE',
'data': {
'nextRewardAt': 60,
'rewardKey': [
'Pet_HatchingPotion_Purple',
],
'rewardText': common.i18n.t('potion', {potionType: common.i18n.t('hatchingPotionRoyalPurple', language)}, language),
'reward': [
{
'premium': true,
'key': 'RoyalPurple',
'limited': true,
'value': 2,
}
],
'message': common.i18n.t('unlockedCheckInReward', language),
},
'id': common.uuid(),
}
};
dbUsers.update({_id: user._id}, {$set:set, $push:push, $inc:inc});
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
if (user._id == authorUuid) console.warn(authorName + ' processed');
}
function displayData() {
console.warn('\n' + count + ' users processed\n');
return exiting(0);
}
function exiting(code, msg) {
code = code || 0; // 0 = success
if (code && !msg) { msg = 'ERROR!'; }
if (msg) {
if (code) { console.error(msg); }
else { console.log( msg); }
}
process.exit(code);
}
module.exports = processUsers;

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;

View File

@@ -0,0 +1,21 @@
require("babel-register");
require("babel-polyfill");
// This file must use ES5, everything required can be in ES6
function setUpServer () {
var nconf = require('nconf');
var mongoose = require('mongoose');
var Bluebird = require('bluebird');
var setupNconf = require('../website/server/libs/setupNconf');
setupNconf();
// We require src/server and npt src/index because
// 1. nconf is already setup
// 2. we don't need clustering
require('../website/server/server'); // eslint-disable-line global-require
}
setUpServer();
// Replace this with your migration
var processUsers = require('./new_stuff');
processUsers();

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']
}
}
};

View File

@@ -6,49 +6,69 @@ var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
* set the newStuff flag in all user accounts so they see a Bailey message
*/
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.newStuff': {$ne:true},
};
// specify a query to limit the affected users (empty for all users):
var query = {
'flags.newStuff':{$ne: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 = {'flags.newStuff':true};
var set = {'flags.newStuff': true};
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;

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;

View File

@@ -0,0 +1,115 @@
var migrationName = 'restore_profile_data.js';
var authorName = 'ThehollidayInn'; // in case script author needs to know when their ...
var authorUuid = ''; //... own data is done
/*
* Check if users have empty profile data in new database and update it with old database info
*/
var monk = require('monk');
var connectionString = ''; // FOR TEST DATABASE
var dbUsers = monk(connectionString).get('users', { castIds: false });
var monk2 = require('monk');
var oldDbConnectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
var olDbUsers = monk2(oldDbConnectionString).get('users', { castIds: false });
function processUsers(lastId)
{
// specify a query to limit the affected users (empty for all users):
var query = {
// 'profile.name': 'profile name not found',
'profile.blurb': null,
// 'auth.timestamps.loggedin': {$gt: new Date('11/30/2016')},
};
if (lastId) {
query._id = {
$gt: lastId
}
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 250,
fields: ['_id', 'profile', 'auth.timestamps.loggedin'] // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
})
.then(updateUsers)
.catch(function (err) {
console.log(err);
return exiting(1, 'ERROR! ' + err);
});
}
var progressCount = 1000;
var count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
setTimeout(displayData, 300000);
return;
}
var userPaymentPromises = users.map(updateUser);
var lastUser = users[users.length - 1];
return Promise.all(userPaymentPromises)
.then(function () {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
if (!user.profile.name || user.profile.name === 'profile name not found' || !user.profile.imageUrl || !user.profile.blurb) {
return olDbUsers.findOne({_id: user._id}, '_id profile')
.then((oldUserData) => {
if (!oldUserData) return;
// specify user data to change:
var set = {};
if (oldUserData.profile.name === 'profile name not found') return;
var userNeedsProfileName = !user.profile.name || user.profile.name === 'profile name not found';
if (userNeedsProfileName && oldUserData.profile.name) {
set['profile.name'] = oldUserData.profile.name;
}
if (!user.profile.imageUrl && oldUserData.profile.imageUrl) {
set['profile.imageUrl'] = oldUserData.profile.imageUrl;
}
if (!user.profile.blurb && oldUserData.profile.blurb) {
set['profile.blurb'] = oldUserData.profile.blurb;
}
if (Object.keys(set).length !== 0 && set.constructor === Object) {
console.log(set)
return dbUsers.update({_id: user._id}, {$set:set});
}
});
}
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
if (user._id == authorUuid) console.warn(authorName + ' processed');
}
function displayData() {
console.warn('\n' + count + ' users processed\n');
return exiting(0);
}
function exiting(code, msg) {
code = code || 0; // 0 = success
if (code && !msg) { msg = 'ERROR!'; }
if (msg) {
if (code) { console.error(msg); }
else { console.log( msg); }
}
process.exit(code);
}
processUsers()

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;

3781
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,19 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "3.66.1",
"version": "3.77.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",
"aws-sdk": "^2.0.25",
"axios": "^0.15.3",
"babel-core": "^6.0.0",
"babel-loader": "^6.0.0",
"babel-plugin-syntax-async-functions": "^6.13.0",
@@ -24,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",
@@ -32,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",
@@ -40,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",
@@ -66,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",
@@ -81,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",
@@ -105,24 +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-resource": "^1.0.2",
"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,24 +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",
"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"
}
}

View File

@@ -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();

View File

@@ -10,7 +10,7 @@ describe('GET /models/:model/paths', () => {
user = await generateUser();
});
it('returns an error when model is not accessible or doesn\'t exists', async () => {
it('returns an error when model is not accessible or doesn\'t exist', async () => {
await expect(user.get('/models/1234/paths')).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',

View File

@@ -1,21 +0,0 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('payments : amazon #subscribeCancel', () => {
let endpoint = '/amazon/subscribe/cancel';
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'),
});
});
});

View File

@@ -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'),
});
});
});

View File

@@ -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'),
});
});
});

View File

@@ -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'),
});
});
});

View File

@@ -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'),
});
});
});

View File

@@ -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'),
});
});
});

View File

@@ -1,21 +0,0 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('payments - stripe - #subscribeCancel', () => {
let endpoint = '/stripe/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'),
});
});
});

View File

@@ -1,20 +0,0 @@
import {
generateUser,
} from '../../../../helpers/api-integration/v3';
describe('payments - amazon - #checkout', () => {
let endpoint = '/amazon/checkout';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies credentials', async () => {
await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Missing req.body.orderReferenceId',
});
});
});

View File

@@ -1,21 +0,0 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('payments - amazon - #subscribe', () => {
let endpoint = '/amazon/subscribe';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies subscription code', async () => {
await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingSubscriptionCode'),
});
});
});

View File

@@ -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');
});
});

View File

@@ -1,20 +0,0 @@
import {
generateUser,
} from '../../../../helpers/api-integration/v3';
describe('payments - stripe - #checkout', () => {
let endpoint = '/stripe/checkout';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies credentials', async () => {
await expect(user.post(endpoint, {id: 123})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'Error',
message: 'Invalid API Key provided: ****************************1111',
});
});
});

View File

@@ -1,21 +0,0 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('payments - stripe - #subscribeEdit', () => {
let endpoint = '/stripe/subscribe/edit';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies credentials', async () => {
await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('missingSubscription'),
});
});
});

View File

@@ -0,0 +1,78 @@
import {
generateUser,
generateGroup,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import amzLib from '../../../../../../website/server/libs/amazonPayments';
describe('payments : amazon #subscribeCancel', () => {
let endpoint = '/amazon/subscribe/cancel?noRedirect=true';
let user, group, amazonSubscribeCancelStub;
beforeEach(async () => {
user = await generateUser();
});
it('throws error when there users has no subscription', async () => {
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('missingSubscription'),
});
});
describe('success', () => {
beforeEach(() => {
amazonSubscribeCancelStub = sinon.stub(amzLib, 'cancelSubscription').returnsPromise().resolves({});
});
afterEach(() => {
amzLib.cancelSubscription.restore();
});
it('cancels a user 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);
expect(amazonSubscribeCancelStub).to.be.calledOnce;
expect(amazonSubscribeCancelStub.args[0][0].user._id).to.eql(user._id);
expect(amazonSubscribeCancelStub.args[0][0].groupId).to.eql(undefined);
expect(amazonSubscribeCancelStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken);
expect(amazonSubscribeCancelStub.args[0][0].headers['x-api-user']).to.eql(user._id);
});
it('cancels a group 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,
});
group = await generateGroup(user, {
name: 'test group',
type: 'guild',
privacy: 'public',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
});
await user.get(`${endpoint}&groupId=${group._id}`);
expect(amazonSubscribeCancelStub).to.be.calledOnce;
expect(amazonSubscribeCancelStub.args[0][0].user._id).to.eql(user._id);
expect(amazonSubscribeCancelStub.args[0][0].groupId).to.eql(group._id);
expect(amazonSubscribeCancelStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken);
expect(amazonSubscribeCancelStub.args[0][0].headers['x-api-user']).to.eql(user._id);
});
});
});

View File

@@ -0,0 +1,63 @@
import {
generateUser,
} from '../../../../../helpers/api-integration/v3';
import amzLib from '../../../../../../website/server/libs/amazonPayments';
describe('payments - amazon - #checkout', () => {
let endpoint = '/amazon/checkout';
let user, amazonCheckoutStub;
beforeEach(async () => {
user = await generateUser();
});
it('verifies credentials', async () => {
await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Missing req.body.orderReferenceId',
});
});
describe('success', () => {
beforeEach(async () => {
amazonCheckoutStub = sinon.stub(amzLib, 'checkout').returnsPromise().resolves({});
});
afterEach(() => {
amzLib.checkout.restore();
});
it('makes a purcahse with amazon checkout', 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 gift = {
type: 'gems',
gems: {
amount: 16,
uuid: user._id,
},
};
let orderReferenceId = 'orderReferenceId-example';
await user.post(endpoint, {
gift,
orderReferenceId,
});
expect(amazonCheckoutStub).to.be.calledOnce;
expect(amazonCheckoutStub.args[0][0].user._id).to.eql(user._id);
expect(amazonCheckoutStub.args[0][0].gift).to.eql(gift);
expect(amazonCheckoutStub.args[0][0].orderReferenceId).to.eql(orderReferenceId);
expect(amazonCheckoutStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken);
expect(amazonCheckoutStub.args[0][0].headers['x-api-user']).to.eql(user._id);
});
});
});

View File

@@ -1,6 +1,6 @@
import {
generateUser,
} from '../../../../helpers/api-integration/v3';
} from '../../../../../helpers/api-integration/v3';
describe('payments - amazon - #createOrderReferenceId', () => {
let endpoint = '/amazon/createOrderReferenceId';

View File

@@ -0,0 +1,96 @@
import {
generateUser,
generateGroup,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import amzLib from '../../../../../../website/server/libs/amazonPayments';
describe('payments - amazon - #subscribe', () => {
let endpoint = '/amazon/subscribe';
let user, group, subscribeWithAmazonStub;
beforeEach(async () => {
user = await generateUser();
});
it('verifies subscription code', async () => {
await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingSubscriptionCode'),
});
});
describe('success', () => {
let billingAgreementId = 'billingAgreementId-example';
let subscription = 'basic_3mo';
let coupon;
beforeEach(() => {
subscribeWithAmazonStub = sinon.stub(amzLib, 'subscribe').returnsPromise().resolves({});
});
afterEach(() => {
amzLib.subscribe.restore();
});
it('creates a user 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.post(endpoint, {
billingAgreementId,
subscription,
coupon,
});
expect(subscribeWithAmazonStub).to.be.calledOnce;
expect(subscribeWithAmazonStub.args[0][0].billingAgreementId).to.eql(billingAgreementId);
expect(subscribeWithAmazonStub.args[0][0].sub).to.exist;
expect(subscribeWithAmazonStub.args[0][0].coupon).to.eql(coupon);
expect(subscribeWithAmazonStub.args[0][0].groupId).not.exist;
expect(subscribeWithAmazonStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken);
expect(subscribeWithAmazonStub.args[0][0].headers['x-api-user']).to.eql(user._id);
});
it('creates a group 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,
});
group = await generateGroup(user, {
name: 'test group',
type: 'guild',
privacy: 'public',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
});
await user.post(endpoint, {
billingAgreementId,
subscription,
coupon,
groupId: group._id,
});
expect(subscribeWithAmazonStub).to.be.calledOnce;
expect(subscribeWithAmazonStub.args[0][0].billingAgreementId).to.eql(billingAgreementId);
expect(subscribeWithAmazonStub.args[0][0].sub).to.exist;
expect(subscribeWithAmazonStub.args[0][0].coupon).to.eql(coupon);
expect(subscribeWithAmazonStub.args[0][0].user._id).to.eql(user._id);
expect(subscribeWithAmazonStub.args[0][0].groupId).to.eql(group._id);
expect(subscribeWithAmazonStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken);
expect(subscribeWithAmazonStub.args[0][0].headers['x-api-user']).to.eql(user._id);
});
});
});

View File

@@ -1,6 +1,6 @@
import {
generateUser,
} from '../../../../helpers/api-integration/v3';
} from '../../../../../helpers/api-integration/v3';
describe('payments : amazon', () => {
let endpoint = '/amazon/verifyAccessToken';

View File

@@ -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);
});
});
});

View File

@@ -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);
});
});
});

View File

@@ -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);
});
});
});

View File

@@ -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);
});
});
});

View File

@@ -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);
});
});
});

View File

@@ -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);
});
});
});

View File

@@ -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);
});
});
});

View File

@@ -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);
});
});
});

View File

@@ -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);
});
});
});

View File

@@ -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);
});
});
});

View File

@@ -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;
});
});
});

View File

@@ -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;
});
});
});

View File

@@ -0,0 +1,74 @@
import {
generateUser,
generateGroup,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import stripePayments from '../../../../../../website/server/libs/stripePayments';
describe('payments - stripe - #subscribeCancel', () => {
let endpoint = '/stripe/subscribe/cancel?redirect=none';
let user, group, stripeCancelSubscriptionStub;
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', () => {
beforeEach(async () => {
stripeCancelSubscriptionStub = sinon.stub(stripePayments, 'cancelSubscription').returnsPromise().resolves({});
});
afterEach(() => {
stripePayments.cancelSubscription.restore();
});
it('cancels a user 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}`);
expect(stripeCancelSubscriptionStub).to.be.calledOnce;
expect(stripeCancelSubscriptionStub.args[0][0].user._id).to.eql(user._id);
expect(stripeCancelSubscriptionStub.args[0][0].groupId).to.eql(undefined);
});
it('cancels a group 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,
});
group = await generateGroup(user, {
name: 'test group',
type: 'guild',
privacy: 'public',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
});
await user.get(`${endpoint}&groupId=${group._id}`);
expect(stripeCancelSubscriptionStub).to.be.calledOnce;
expect(stripeCancelSubscriptionStub.args[0][0].user._id).to.eql(user._id);
expect(stripeCancelSubscriptionStub.args[0][0].groupId).to.eql(group._id);
});
});
});

View File

@@ -0,0 +1,75 @@
import {
generateUser,
generateGroup,
} from '../../../../../helpers/api-integration/v3';
import stripePayments from '../../../../../../website/server/libs/stripePayments';
describe('payments - stripe - #checkout', () => {
let endpoint = '/stripe/checkout';
let user, group;
beforeEach(async () => {
user = await generateUser();
});
it('verifies credentials', async () => {
await expect(user.post(endpoint, {id: 123})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'Error',
message: 'Invalid API Key provided: ****************************1111',
});
});
describe('success', () => {
let stripeCheckoutSubscriptionStub;
beforeEach(async () => {
stripeCheckoutSubscriptionStub = sinon.stub(stripePayments, 'checkout').returnsPromise().resolves({});
});
afterEach(() => {
stripePayments.checkout.restore();
});
it('cancels a user 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.post(endpoint);
expect(stripeCheckoutSubscriptionStub).to.be.calledOnce;
expect(stripeCheckoutSubscriptionStub.args[0][0].user._id).to.eql(user._id);
expect(stripeCheckoutSubscriptionStub.args[0][0].groupId).to.eql(undefined);
});
it('cancels a group 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,
});
group = await generateGroup(user, {
name: 'test group',
type: 'guild',
privacy: 'public',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
});
await user.post(`${endpoint}?groupId=${group._id}`);
expect(stripeCheckoutSubscriptionStub).to.be.calledOnce;
expect(stripeCheckoutSubscriptionStub.args[0][0].user._id).to.eql(user._id);
expect(stripeCheckoutSubscriptionStub.args[0][0].groupId).to.eql(group._id);
});
});
});

View File

@@ -0,0 +1,78 @@
import {
generateUser,
generateGroup,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import stripePayments from '../../../../../../website/server/libs/stripePayments';
describe('payments - stripe - #subscribeEdit', () => {
let endpoint = '/stripe/subscribe/edit';
let user, group;
beforeEach(async () => {
user = await generateUser();
});
it('verifies credentials', async () => {
await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('missingSubscription'),
});
});
describe('success', () => {
let stripeEditSubscriptionStub;
beforeEach(async () => {
stripeEditSubscriptionStub = sinon.stub(stripePayments, 'editSubscription').returnsPromise().resolves({});
});
afterEach(() => {
stripePayments.editSubscription.restore();
});
it('cancels a user 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.post(endpoint);
expect(stripeEditSubscriptionStub).to.be.calledOnce;
expect(stripeEditSubscriptionStub.args[0][0].user._id).to.eql(user._id);
expect(stripeEditSubscriptionStub.args[0][0].groupId).to.eql(undefined);
});
it('cancels a group 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,
});
group = await generateGroup(user, {
name: 'test group',
type: 'guild',
privacy: 'public',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
});
await user.post(endpoint, {
groupId: group._id,
});
expect(stripeEditSubscriptionStub).to.be.calledOnce;
expect(stripeEditSubscriptionStub.args[0][0].user._id).to.eql(user._id);
expect(stripeEditSubscriptionStub.args[0][0].groupId).to.eql(group._id);
});
});
});

View File

@@ -148,5 +148,20 @@ describe('POST /groups/:groupId/quests/accept', () => {
expect(rejectingMember.party.quest.key).to.not.exist;
expect(rejectingMember.party.quest.completed).to.not.exist;
});
it('begins the quest if accepting the last pending invite and verifies chat', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
// quest will start after everyone has accepted
await partyMembers[1].post(`/groups/${questingGroup._id}/quests/accept`);
await questingGroup.sync();
expect(questingGroup.chat[0].text).to.exist;
expect(questingGroup.chat[0]._meta).to.exist;
expect(questingGroup.chat[0]._meta).to.have.all.keys(['participatingMembers']);
let returnedGroup = await leader.get(`/groups/${questingGroup._id}`);
expect(returnedGroup.chat[0]._meta).to.be.undefined;
});
});
});

View File

@@ -231,5 +231,22 @@ describe('POST /groups/:groupId/quests/force-start', () => {
expect(questingGroup.quest.members[partyMembers[0]._id]).to.exist;
expect(questingGroup.quest.members[leader._id]).to.exist;
});
it('allows group leader to force start quest and verifies chat', async () => {
let questLeader = partyMembers[0];
await questLeader.update({[`items.quests.${PET_QUEST}`]: 1});
await questLeader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await leader.post(`/groups/${questingGroup._id}/quests/force-start`);
await questingGroup.sync();
expect(questingGroup.chat[0].text).to.exist;
expect(questingGroup.chat[0]._meta).to.exist;
expect(questingGroup.chat[0]._meta).to.have.all.keys(['participatingMembers']);
let returnedGroup = await leader.get(`/groups/${questingGroup._id}`);
expect(returnedGroup.chat[0]._meta).to.be.undefined;
});
});
});

View File

@@ -188,5 +188,25 @@ describe('POST /groups/:groupId/quests/invite/:questKey', () => {
expect(group.quest.active).to.eql(true);
});
it('starts quest automatically if user is in a solo party and verifies chat', async () => {
let leaderDetails = { balance: 10 };
leaderDetails[`items.quests.${PET_QUEST}`] = 1;
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
leaderDetails,
});
await groupLeader.post(`/groups/${group._id}/quests/invite/${PET_QUEST}`);
await group.sync();
expect(group.chat[0].text).to.exist;
expect(group.chat[0]._meta).to.exist;
expect(group.chat[0]._meta).to.have.all.keys(['participatingMembers']);
let returnedGroup = await groupLeader.get(`/groups/${group._id}`);
expect(returnedGroup.chat[0]._meta).to.be.undefined;
});
});
});

View File

@@ -86,11 +86,12 @@ describe('POST /groups/:groupId/quests/abort', () => {
});
it('aborts a quest', async () => {
sandbox.stub(Group.prototype, 'sendChat');
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
await partyMembers[1].post(`/groups/${questingGroup._id}/quests/accept`);
let stub = sandbox.stub(Group.prototype, 'sendChat');
let res = await leader.post(`/groups/${questingGroup._id}/quests/abort`);
await Promise.all([
leader.sync(),
@@ -127,6 +128,7 @@ describe('POST /groups/:groupId/quests/abort', () => {
});
expect(Group.prototype.sendChat).to.be.calledOnce;
expect(Group.prototype.sendChat).to.be.calledWithMatch(/aborted the party quest Wail of the Whale.`/);
Group.prototype.sendChat.restore();
stub.restore();
});
});

View File

@@ -180,5 +180,19 @@ describe('POST /groups/:groupId/quests/reject', () => {
expect(rejectingMember.party.quest.key).to.not.exist;
expect(rejectingMember.party.quest.completed).to.not.exist;
});
it('starts the quest when the last user reject and verifies chat', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
await partyMembers[1].post(`/groups/${questingGroup._id}/quests/reject`);
await questingGroup.sync();
expect(questingGroup.chat[0].text).to.exist;
expect(questingGroup.chat[0]._meta).to.exist;
expect(questingGroup.chat[0]._meta).to.have.all.keys(['participatingMembers']);
let returnedGroup = await leader.get(`/groups/${questingGroup._id}`);
expect(returnedGroup.chat[0]._meta).to.be.undefined;
});
});
});

View File

@@ -5,7 +5,7 @@ import {
} from '../../../../helpers/api-integration/v3';
describe('POST /tasks/clearCompletedTodos', () => {
it('deletes all completed todos except the ones from a challenge', async () => {
it('deletes all completed todos except the ones from a challenge and group', async () => {
let user = await generateUser({balance: 1});
let guild = await generateGroup(user);
let challenge = await generateChallenge(user, guild);
@@ -24,8 +24,14 @@ describe('POST /tasks/clearCompletedTodos', () => {
type: 'todo',
});
let groupTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'todo 7',
type: 'todo',
});
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
let tasks = await user.get('/tasks/user?type=todos');
expect(tasks.length).to.equal(initialTodoCount + 6);
expect(tasks.length).to.equal(initialTodoCount + 7);
for (let task of tasks) {
if (['todo 2', 'todo 3', 'todo 6'].indexOf(task.text) !== -1) {
@@ -37,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 6');
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
});
});

View File

@@ -34,6 +34,10 @@ describe('POST /tasks/:id/score/:direction', () => {
});
it('prevents user from scoring a task that needs to be approved', async () => {
await user.update({
'preferences.language': 'cs',
});
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
@@ -52,7 +56,7 @@ describe('POST /tasks/:id/score/:direction', () => {
expect(user.notifications[0].data.message).to.equal(t('userHasRequestedTaskApproval', {
user: member.auth.local.username,
taskName: updatedTask.text,
}));
}, 'cs')); // This test only works if we have the notification translated
expect(user.notifications[0].data.groupId).to.equal(guild._id);
expect(updatedTask.group.approval.requested).to.equal(true);

View File

@@ -99,6 +99,7 @@ describe('POST /tasks/:taskId', () => {
let updateGroup = await user.get(`/groups/${guild._id}`);
expect(updateGroup.chat[0].text).to.equal(t('userIsClamingTask', {username: member.profile.name, task: task.text}));
expect(updateGroup.chat[0].uuid).to.equal('system');
});
it('assigns a task to a user', async () => {

View File

@@ -0,0 +1,49 @@
import {
generateUser,
generateGroup,
} from '../../../../../helpers/api-v3-integration.helper';
describe('POST group-tasks/:taskId/move/to/:position', () => {
let user, guild;
beforeEach(async () => {
user = await generateUser({balance: 1});
guild = await generateGroup(user, {type: 'guild'});
});
it('can move task to new position', async () => {
let tasks = await user.post(`/tasks/group/${guild._id}`, [
{type: 'habit', text: 'habit 1'},
{type: 'habit', text: 'habit 2'},
{type: 'daily', text: 'daily 1'},
{type: 'habit', text: 'habit 3'},
{type: 'habit', text: 'habit 4'},
{type: 'todo', text: 'todo 1'},
{type: 'habit', text: 'habit 5'},
]);
let taskToMove = tasks[1];
expect(taskToMove.text).to.equal('habit 2');
let newOrder = await user.post(`/group-tasks/${tasks[1]._id}/move/to/3`);
expect(newOrder[3]).to.equal(taskToMove._id);
expect(newOrder.length).to.equal(5);
});
it('can push to bottom', async () => {
let tasks = await user.post(`/tasks/group/${guild._id}`, [
{type: 'habit', text: 'habit 1'},
{type: 'habit', text: 'habit 2'},
{type: 'daily', text: 'daily 1'},
{type: 'habit', text: 'habit 3'},
{type: 'habit', text: 'habit 4'},
{type: 'todo', text: 'todo 1'},
{type: 'habit', text: 'habit 5'},
]);
let taskToMove = tasks[1];
expect(taskToMove.text).to.equal('habit 2');
let newOrder = await user.post(`/group-tasks/${tasks[1]._id}/move/to/-1`);
expect(newOrder[4]).to.equal(taskToMove._id);
expect(newOrder.length).to.equal(5);
});
});

View File

@@ -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;

View File

@@ -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;
});

View File

@@ -2,11 +2,13 @@ import {
generateUser,
translate as t,
createAndPopulateGroup,
generateGroup,
generateChallenge,
sleep,
} from '../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
import { find } from 'lodash';
describe('POST /user/class/cast/:spellId', () => {
let user;
@@ -120,6 +122,31 @@ describe('POST /user/class/cast/:spellId', () => {
});
});
it('returns an error if a group task was targeted', async () => {
let {group, groupLeader} = await createAndPopulateGroup();
let groupTask = await groupLeader.post(`/tasks/group/${group._id}`, {
text: 'todo group',
type: 'todo',
});
await groupLeader.post(`/tasks/${groupTask._id}/assign/${groupLeader._id}`);
let memberTasks = await groupLeader.get('/tasks/user');
let syncedGroupTask = find(memberTasks, function findAssignedTask (memberTask) {
return memberTask.group.id === group._id;
});
await groupLeader.update({'stats.class': 'rogue', 'stats.lvl': 11});
await sleep(0.5);
await groupLeader.sync();
await expect(groupLeader.post(`/user/class/cast/pickPocket?targetId=${syncedGroupTask._id}`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('groupTasksNoCast'),
});
});
it('returns an error if targeted party member doesn\'t exist', async () => {
let {groupLeader} = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
@@ -160,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\'');

View File

@@ -4,6 +4,7 @@ import {
generateChallenge,
translate as t,
} from '../../../../helpers/api-integration/v3';
import { find } from 'lodash';
describe('POST /user/reset', () => {
let user;
@@ -86,19 +87,34 @@ describe('POST /user/reset', () => {
expect(user.tasksOrder.rewards).to.be.empty;
});
it('does not delete challenge tasks', async () => {
it('does not delete challenge or group tasks', async () => {
let guild = await generateGroup(user);
let challenge = await generateChallenge(user, guild);
let task = await user.post(`/tasks/challenge/${challenge._id}`, {
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.post(`/tasks/${groupTask._id}/assign/${user._id}`);
await user.post('/user/reset');
await user.sync();
let userChallengeTask = await user.get(`/tasks/${task._id}`);
let memberTasks = await user.get('/tasks/user');
expect(userChallengeTask).to.eql(task);
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;
});
});

View File

@@ -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);
});
});

View File

@@ -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);
});
});

View File

@@ -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);
});
});

View File

@@ -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);
});
});

View File

@@ -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 () => {

View File

@@ -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);
});
});

View File

@@ -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';

View File

@@ -0,0 +1,562 @@
import moment from 'moment';
import cc from 'coupon-code';
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 amzLib from '../../../../../website/server/libs/amazonPayments';
import payments from '../../../../../website/server/libs/payments';
import common from '../../../../../website/common';
const i18n = common.i18n;
describe('Amazon Payments', () => {
let subKey = 'basic_3mo';
describe('checkout', () => {
let user, orderReferenceId, headers;
let setOrderReferenceDetailsSpy;
let confirmOrderReferenceSpy;
let authorizeSpy;
let closeOrderReferenceSpy;
let paymentBuyGemsStub;
let paymentCreateSubscritionStub;
let amount = 5;
function expectAmazonStubs () {
expect(setOrderReferenceDetailsSpy).to.be.calledOnce;
expect(setOrderReferenceDetailsSpy).to.be.calledWith({
AmazonOrderReferenceId: orderReferenceId,
OrderReferenceAttributes: {
OrderTotal: {
CurrencyCode: amzLib.constants.CURRENCY_CODE,
Amount: amount,
},
SellerNote: amzLib.constants.SELLER_NOTE,
SellerOrderAttributes: {
SellerOrderId: common.uuid(),
StoreName: amzLib.constants.STORE_NAME,
},
},
});
expect(confirmOrderReferenceSpy).to.be.calledOnce;
expect(confirmOrderReferenceSpy).to.be.calledWith({ AmazonOrderReferenceId: orderReferenceId });
expect(authorizeSpy).to.be.calledOnce;
expect(authorizeSpy).to.be.calledWith({
AmazonOrderReferenceId: orderReferenceId,
AuthorizationReferenceId: common.uuid().substring(0, 32),
AuthorizationAmount: {
CurrencyCode: amzLib.constants.CURRENCY_CODE,
Amount: amount,
},
SellerAuthorizationNote: amzLib.constants.SELLER_NOTE,
TransactionTimeout: 0,
CaptureNow: true,
});
expect(closeOrderReferenceSpy).to.be.calledOnce;
expect(closeOrderReferenceSpy).to.be.calledWith({ AmazonOrderReferenceId: orderReferenceId });
}
beforeEach(function () {
user = new User();
headers = {};
orderReferenceId = 'orderReferenceId';
setOrderReferenceDetailsSpy = sinon.stub(amzLib, 'setOrderReferenceDetails');
setOrderReferenceDetailsSpy.returnsPromise().resolves({});
confirmOrderReferenceSpy = sinon.stub(amzLib, 'confirmOrderReference');
confirmOrderReferenceSpy.returnsPromise().resolves({});
authorizeSpy = sinon.stub(amzLib, 'authorize');
authorizeSpy.returnsPromise().resolves({});
closeOrderReferenceSpy = sinon.stub(amzLib, 'closeOrderReference');
closeOrderReferenceSpy.returnsPromise().resolves({});
paymentBuyGemsStub = sinon.stub(payments, 'buyGems');
paymentBuyGemsStub.returnsPromise().resolves({});
paymentCreateSubscritionStub = sinon.stub(payments, 'createSubscription');
paymentCreateSubscritionStub.returnsPromise().resolves({});
sinon.stub(common, 'uuid').returns('uuid-generated');
});
afterEach(function () {
amzLib.setOrderReferenceDetails.restore();
amzLib.confirmOrderReference.restore();
amzLib.authorize.restore();
amzLib.closeOrderReference.restore();
payments.buyGems.restore();
payments.createSubscription.restore();
common.uuid.restore();
});
it('should purchase gems', async () => {
await amzLib.checkout({user, orderReferenceId, headers});
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
headers,
});
expectAmazonStubs();
});
it('should gift gems', async () => {
let receivingUser = new User();
receivingUser.save();
let gift = {
type: 'gems',
gems: {
amount: 16,
uuid: receivingUser._id,
},
};
amount = 16 / 4;
await amzLib.checkout({gift, user, orderReferenceId, headers});
gift.member = receivingUser;
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON_GIFT,
headers,
gift,
});
expectAmazonStubs();
});
it('should gift a subscription', async () => {
let receivingUser = new User();
receivingUser.save();
let gift = {
type: 'subscription',
subscription: {
key: subKey,
uuid: receivingUser._id,
},
};
amount = common.content.subscriptionBlocks[subKey].price;
await amzLib.checkout({user, orderReferenceId, headers, gift});
gift.member = receivingUser;
expect(paymentCreateSubscritionStub).to.be.calledOnce;
expect(paymentCreateSubscritionStub).to.be.calledWith({
user,
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON_GIFT,
headers,
gift,
});
expectAmazonStubs();
});
});
describe('subscribe', () => {
let user, group, amount, billingAgreementId, sub, coupon, groupId, headers;
let amazonSetBillingAgreementDetailsSpy;
let amazonConfirmBillingAgreementSpy;
let amazongAuthorizeOnBillingAgreementSpy;
let createSubSpy;
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
user.purchased.plan.customerId = 'customer-id';
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 = 'customer-id';
group.purchased.plan.planId = subKey;
await group.save();
amount = common.content.subscriptionBlocks[subKey].price;
billingAgreementId = 'billingAgreementId';
sub = {
key: subKey,
price: amount,
};
groupId = group._id;
headers = {};
amazonSetBillingAgreementDetailsSpy = sinon.stub(amzLib, 'setBillingAgreementDetails');
amazonSetBillingAgreementDetailsSpy.returnsPromise().resolves({});
amazonConfirmBillingAgreementSpy = sinon.stub(amzLib, 'confirmBillingAgreement');
amazonConfirmBillingAgreementSpy.returnsPromise().resolves({});
amazongAuthorizeOnBillingAgreementSpy = sinon.stub(amzLib, 'authorizeOnBillingAgreement');
amazongAuthorizeOnBillingAgreementSpy.returnsPromise().resolves({});
createSubSpy = sinon.stub(payments, 'createSubscription');
createSubSpy.returnsPromise().resolves({});
sinon.stub(common, 'uuid').returns('uuid-generated');
});
afterEach(function () {
amzLib.setBillingAgreementDetails.restore();
amzLib.confirmBillingAgreement.restore();
amzLib.authorizeOnBillingAgreement.restore();
payments.createSubscription.restore();
common.uuid.restore();
});
it('should throw an error if we are missing a subscription', async () => {
await expect(amzLib.subscribe({
billingAgreementId,
coupon,
user,
groupId,
headers,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
message: i18n.t('missingSubscriptionCode'),
});
});
it('should throw an error if we are missing a billingAgreementId', async () => {
await expect(amzLib.subscribe({
sub,
coupon,
user,
groupId,
headers,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
message: 'Missing req.body.billingAgreementId',
});
});
it('should throw an error when coupon code is missing', async () => {
sub.discount = 40;
await expect(amzLib.subscribe({
billingAgreementId,
sub,
coupon,
user,
groupId,
headers,
}))
.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(amzLib.subscribe({
billingAgreementId,
sub,
coupon,
user,
groupId,
headers,
}))
.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);
await amzLib.subscribe({
billingAgreementId,
sub,
coupon,
user,
groupId,
headers,
});
expect(createSubSpy).to.be.calledOnce;
expect(createSubSpy).to.be.calledWith({
user,
customerId: billingAgreementId,
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
sub,
headers,
groupId,
});
cc.validate.restore();
});
it('subscribes with amazon', async () => {
await amzLib.subscribe({
billingAgreementId,
sub,
coupon,
user,
groupId,
headers,
});
expect(amazonSetBillingAgreementDetailsSpy).to.be.calledOnce;
expect(amazonSetBillingAgreementDetailsSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
BillingAgreementAttributes: {
SellerNote: amzLib.constants.SELLER_NOTE_SUBSCRIPTION,
SellerBillingAgreementAttributes: {
SellerBillingAgreementId: common.uuid(),
StoreName: amzLib.constants.STORE_NAME,
CustomInformation: amzLib.constants.SELLER_NOTE_SUBSCRIPTION,
},
},
});
expect(amazonConfirmBillingAgreementSpy).to.be.calledOnce;
expect(amazonConfirmBillingAgreementSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
});
expect(amazongAuthorizeOnBillingAgreementSpy).to.be.calledOnce;
expect(amazongAuthorizeOnBillingAgreementSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
AuthorizationReferenceId: common.uuid().substring(0, 32),
AuthorizationAmount: {
CurrencyCode: amzLib.constants.CURRENCY_CODE,
Amount: amount,
},
SellerAuthorizationNote: amzLib.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION,
TransactionTimeout: 0,
CaptureNow: true,
SellerNote: amzLib.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION,
SellerOrderAttributes: {
SellerOrderId: common.uuid(),
StoreName: amzLib.constants.STORE_NAME,
},
});
expect(createSubSpy).to.be.calledOnce;
expect(createSubSpy).to.be.calledWith({
user,
customerId: billingAgreementId,
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
sub,
headers,
groupId,
});
});
});
describe('cancelSubscription', () => {
let user, group, headers, billingAgreementId, subscriptionBlock, subscriptionLength;
let getBillingAgreementDetailsSpy;
let paymentCancelSubscriptionSpy;
function expectAmazonStubs () {
expect(getBillingAgreementDetailsSpy).to.be.calledOnce;
expect(getBillingAgreementDetailsSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
});
}
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
user.purchased.plan.customerId = 'customer-id';
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 = 'customer-id';
group.purchased.plan.planId = subKey;
group.purchased.plan.lastBillingDate = new Date();
await group.save();
subscriptionBlock = common.content.subscriptionBlocks[subKey];
subscriptionLength = subscriptionBlock.months * 30;
headers = {};
getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails');
getBillingAgreementDetailsSpy.returnsPromise().resolves({
BillingAgreementDetails: {
BillingAgreementStatus: {State: 'Closed'},
},
});
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription');
paymentCancelSubscriptionSpy.returnsPromise().resolves({});
});
afterEach(function () {
amzLib.getBillingAgreementDetails.restore();
payments.cancelSubscription.restore();
});
it('should throw an error if we are missing a subscription', async () => {
user.purchased.plan.customerId = undefined;
await expect(amzLib.cancelSubscription({user}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('missingSubscription'),
});
});
it('should cancel a user subscription', async () => {
billingAgreementId = user.purchased.plan.customerId;
await amzLib.cancelSubscription({user, headers});
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
user,
groupId: undefined,
nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
headers,
});
expectAmazonStubs();
});
it('should close a user subscription if amazon not closed', async () => {
amzLib.getBillingAgreementDetails.restore();
getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails')
.returnsPromise()
.resolves({
BillingAgreementDetails: {
BillingAgreementStatus: {State: 'Open'},
},
});
let closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').returnsPromise().resolves({});
billingAgreementId = user.purchased.plan.customerId;
await amzLib.cancelSubscription({user, headers});
expectAmazonStubs();
expect(closeBillingAgreementSpy).to.be.calledOnce;
expect(closeBillingAgreementSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
});
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
user,
groupId: undefined,
nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
headers,
});
amzLib.closeBillingAgreement.restore();
});
it('should throw an error if group is not found', async () => {
await expect(amzLib.cancelSubscription({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(amzLib.cancelSubscription({user: nonLeader, groupId: group._id}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
});
});
it('should cancel a group subscription', async () => {
billingAgreementId = group.purchased.plan.customerId;
await amzLib.cancelSubscription({user, groupId: group._id, headers});
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
user,
groupId: group._id,
nextBill: moment(group.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
headers,
});
expectAmazonStubs();
});
it('should close a group subscription if amazon not closed', async () => {
amzLib.getBillingAgreementDetails.restore();
getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails')
.returnsPromise()
.resolves({
BillingAgreementDetails: {
BillingAgreementStatus: {State: 'Open'},
},
});
let closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').returnsPromise().resolves({});
billingAgreementId = group.purchased.plan.customerId;
await amzLib.cancelSubscription({user, groupId: group._id, headers});
expectAmazonStubs();
expect(closeBillingAgreementSpy).to.be.calledOnce;
expect(closeBillingAgreementSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
});
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
user,
groupId: group._id,
nextBill: moment(group.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
headers,
});
amzLib.closeBillingAgreement.restore();
});
});
});

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,
});
});
});
});

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();
}
});
});

View File

@@ -0,0 +1,269 @@
/* eslint-disable camelcase */
import iapModule from '../../../../../website/server/libs/inAppPurchases';
import payments from '../../../../../website/server/libs/payments';
import googlePayments from '../../../../../website/server/libs/googlePayments';
import iap from '../../../../../website/server/libs/inAppPurchases';
import {model as User} from '../../../../../website/server/models/user';
import common from '../../../../../website/common';
import moment from 'moment';
const i18n = common.i18n;
describe('Google Payments', () => {
let subKey = 'basic_3mo';
describe('verifyGemPurchase', () => {
let sku, user, token, receipt, signature, headers;
let iapSetupStub, iapValidateStub, iapIsValidatedStub, paymentBuyGemsStub;
beforeEach(() => {
sku = 'com.habitrpg.android.habitica.iap.21gems';
user = new User();
receipt = `{"token": "${token}", "productId": "${sku}"}`;
signature = '';
headers = {};
iapSetupStub = sinon.stub(iapModule, 'setup')
.returnsPromise().resolves();
iapValidateStub = sinon.stub(iapModule, 'validate')
.returnsPromise().resolves({});
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
.returns(true);
paymentBuyGemsStub = sinon.stub(payments, 'buyGems').returnsPromise().resolves({});
});
afterEach(() => {
iapModule.setup.restore();
iapModule.validate.restore();
iapModule.isValidated.restore();
payments.buyGems.restore();
});
it('should throw an error if receipt is invalid', async () => {
iapModule.isValidated.restore();
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
.returns(false);
await expect(googlePayments.verifyGemPurchase(user, receipt, signature, headers))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: googlePayments.constants.RESPONSE_INVALID_RECEIPT,
});
});
it('should throw an error if productId is invalid', async () => {
receipt = `{"token": "${token}", "productId": "invalid"}`;
await expect(googlePayments.verifyGemPurchase(user, receipt, signature, headers))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: googlePayments.constants.RESPONSE_INVALID_ITEM,
});
});
it('purchases gems', async () => {
await googlePayments.verifyGemPurchase(user, receipt, signature, headers);
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledWith(iap.GOOGLE, {
data: receipt,
signature,
});
expect(iapIsValidatedStub).to.be.calledOnce;
expect(iapIsValidatedStub).to.be.calledWith({});
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,
paymentMethod: googlePayments.constants.PAYMENT_METHOD_GOOGLE,
amount: 5.25,
headers,
});
});
});
describe('subscribe', () => {
let sub, sku, user, token, receipt, signature, headers, nextPaymentProcessing;
let iapSetupStub, iapValidateStub, iapIsValidatedStub, paymentsCreateSubscritionStub;
beforeEach(() => {
sub = common.content.subscriptionBlocks[subKey];
sku = 'com.habitrpg.android.habitica.subscription.3month';
token = 'test-token';
headers = {};
receipt = `{"token": "${token}"}`;
signature = '';
nextPaymentProcessing = moment.utc().add({days: 2});
iapSetupStub = sinon.stub(iapModule, 'setup')
.returnsPromise().resolves();
iapValidateStub = sinon.stub(iapModule, 'validate')
.returnsPromise().resolves({});
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
.returns(true);
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({});
});
afterEach(() => {
iapModule.setup.restore();
iapModule.validate.restore();
iapModule.isValidated.restore();
payments.createSubscription.restore();
});
it('should throw an error if receipt is invalid', async () => {
iapModule.isValidated.restore();
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
.returns(false);
await expect(googlePayments.subscribe(sku, user, receipt, signature, headers, nextPaymentProcessing))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: googlePayments.constants.RESPONSE_INVALID_RECEIPT,
});
});
it('should throw an error if sku is invalid', async () => {
sku = 'invalid';
await expect(googlePayments.subscribe(sku, user, receipt, signature, headers, nextPaymentProcessing))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: googlePayments.constants.RESPONSE_INVALID_ITEM,
});
});
it('creates a user subscription', async () => {
await googlePayments.subscribe(sku, user, receipt, signature, headers, nextPaymentProcessing);
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledWith(iap.GOOGLE, {
data: receipt,
signature,
});
expect(iapIsValidatedStub).to.be.calledOnce;
expect(iapIsValidatedStub).to.be.calledWith({});
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
expect(paymentsCreateSubscritionStub).to.be.calledWith({
user,
customerId: token,
paymentMethod: googlePayments.constants.PAYMENT_METHOD_GOOGLE,
sub,
headers,
additionalData: {data: receipt, signature},
nextPaymentProcessing,
});
});
});
describe('cancelSubscribe ', () => {
let user, token, receipt, signature, headers, customerId, expirationDate;
let iapSetupStub, iapValidateStub, iapIsValidatedStub, iapGetPurchaseDataStub, paymentCancelSubscriptionSpy;
beforeEach(async () => {
token = 'test-token';
headers = {};
receipt = `{"token": "${token}"}`;
signature = '';
customerId = 'test-customerId';
expirationDate = moment.utc();
iapSetupStub = sinon.stub(iapModule, 'setup')
.returnsPromise().resolves();
iapValidateStub = sinon.stub(iapModule, 'validate')
.returnsPromise().resolves({
expirationDate,
});
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
.returns([{expirationDate: expirationDate.toDate()}]);
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
.returns(true);
user = new User();
user.profile.name = 'sender';
user.purchased.plan.customerId = customerId;
user.purchased.plan.paymentMethod = googlePayments.constants.PAYMENT_METHOD_GOOGLE;
user.purchased.plan.planId = subKey;
user.purchased.plan.additionalData = {data: receipt, signature};
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
});
afterEach(function () {
iapModule.setup.restore();
iapModule.validate.restore();
iapModule.isValidated.restore();
iapModule.getPurchaseData.restore();
payments.cancelSubscription.restore();
});
it('should throw an error if we are missing a subscription', async () => {
user.purchased.plan.paymentMethod = undefined;
await expect(googlePayments.cancelSubscribe(user, headers))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('missingSubscription'),
});
});
it('should throw an error if subscription is still valid', async () => {
iapModule.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
.returns([{expirationDate: expirationDate.add({day: 1}).toDate()}]);
await expect(googlePayments.cancelSubscribe(user, headers))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: googlePayments.constants.RESPONSE_STILL_VALID,
});
});
it('should throw an error if receipt is invalid', async () => {
iapModule.isValidated.restore();
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
.returns(false);
await expect(googlePayments.cancelSubscribe(user, headers))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: googlePayments.constants.RESPONSE_INVALID_RECEIPT,
});
});
it('should cancel a user subscription', async () => {
await googlePayments.cancelSubscribe(user, headers);
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledWith(iap.GOOGLE, {
data: receipt,
signature,
});
expect(iapIsValidatedStub).to.be.calledOnce;
expect(iapIsValidatedStub).to.be.calledWith({
expirationDate,
});
expect(iapGetPurchaseDataStub).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
user,
paymentMethod: googlePayments.constants.PAYMENT_METHOD_GOOGLE,
nextBill: expirationDate.toDate(),
headers,
});
});
});
});

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);
});
});
});
});

View File

@@ -11,7 +11,6 @@ import {
generateGroup,
} from '../../../../helpers/api-unit.helper.js';
import i18n from '../../../../../website/common/script/i18n';
import amzLib from '../../../../../website/server/libs/amazonPayments';
describe('payments/index', () => {
let user, group, data, plan;
@@ -818,109 +817,4 @@ describe('payments/index', () => {
expect(updatedGroup.purchased.plan.quantity).to.eql(3);
});
});
describe('payWithStripe', () => {
let spy;
let stripeCreateCustomerSpy;
let createSubSpy;
beforeEach(function () {
spy = sinon.stub(stripe.subscriptions, 'update');
spy.returnsPromise().resolves;
stripeCreateCustomerSpy = sinon.stub(stripe.customers, 'create');
let stripCustomerResponse = {
subscriptions: {
data: [{id: 'test-id'}],
},
};
stripeCreateCustomerSpy.returnsPromise().resolves(stripCustomerResponse);
createSubSpy = sinon.stub(api, 'createSubscription');
createSubSpy.returnsPromise().resolves({});
data.groupId = group._id;
data.sub.quantity = 3;
});
afterEach(function () {
sinon.restore(stripe.subscriptions.update);
stripe.customers.create.restore();
api.createSubscription.restore();
});
it('subscribes with stripe', async () => {
let token = 'test-token';
let gift;
let sub = data.sub;
let groupId = group._id;
let email = 'test@test.com';
let headers = {};
let coupon;
await api.payWithStripe({
token,
user,
gift,
sub,
groupId,
email,
headers,
coupon,
}, stripe);
expect(stripeCreateCustomerSpy.calledOnce).to.be.true;
expect(createSubSpy.calledOnce).to.be.true;
});
});
describe('subscribeWithAmazon', () => {
let amazonSetBillingAgreementDetailsSpy;
let amazonConfirmBillingAgreementSpy;
let amazongAuthorizeOnBillingAgreementSpy;
let createSubSpy;
beforeEach(function () {
amazonSetBillingAgreementDetailsSpy = sinon.stub(amzLib, 'setBillingAgreementDetails');
amazonSetBillingAgreementDetailsSpy.returnsPromise().resolves({});
amazonConfirmBillingAgreementSpy = sinon.stub(amzLib, 'confirmBillingAgreement');
amazonConfirmBillingAgreementSpy.returnsPromise().resolves({});
amazongAuthorizeOnBillingAgreementSpy = sinon.stub(amzLib, 'authorizeOnBillingAgreement');
amazongAuthorizeOnBillingAgreementSpy.returnsPromise().resolves({});
createSubSpy = sinon.stub(api, 'createSubscription');
createSubSpy.returnsPromise().resolves({});
});
afterEach(function () {
amzLib.setBillingAgreementDetails.restore();
amzLib.confirmBillingAgreement.restore();
amzLib.authorizeOnBillingAgreement.restore();
api.createSubscription.restore();
});
it('subscribes with amazon', async () => {
let billingAgreementId = 'billingAgreementId';
let sub = data.sub;
let coupon;
let groupId = group._id;
let headers = {};
await api.subscribeWithAmazon({
billingAgreementId,
sub,
coupon,
user,
groupId,
headers,
});
expect(amazonSetBillingAgreementDetailsSpy.calledOnce).to.be.true;
expect(amazonConfirmBillingAgreementSpy.calledOnce).to.be.true;
expect(amazongAuthorizeOnBillingAgreementSpy.calledOnce).to.be.true;
expect(createSubSpy.calledOnce).to.be.true;
});
});
});

View File

@@ -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' });
});
});
});

View File

@@ -0,0 +1,661 @@
import stripeModule from 'stripe';
import cc from 'coupon-code';
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 stripePayments from '../../../../../website/server/libs/stripePayments';
import payments from '../../../../../website/server/libs/payments';
import common from '../../../../../website/common';
const i18n = common.i18n;
describe('Stripe Payments', () => {
let subKey = 'basic_3mo';
let stripe = stripeModule('test');
describe('checkout', () => {
let stripeChargeStub, paymentBuyGemsStub, paymentCreateSubscritionStub;
let user, gift, groupId, email, headers, coupon, customerIdResponse, token;
beforeEach(() => {
user = new User();
user.profile.name = 'sender';
user.purchased.plan.customerId = 'customer-id';
user.purchased.plan.planId = subKey;
user.purchased.plan.lastBillingDate = new Date();
token = 'test-token';
customerIdResponse = 'example-customerIdResponse';
let stripCustomerResponse = {
id: customerIdResponse,
};
stripeChargeStub = sinon.stub(stripe.charges, 'create').returnsPromise().resolves(stripCustomerResponse);
paymentBuyGemsStub = sinon.stub(payments, 'buyGems').returnsPromise().resolves({});
paymentCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({});
});
afterEach(() => {
stripe.charges.create.restore();
payments.buyGems.restore();
payments.createSubscription.restore();
});
it('should purchase gems', async () => {
await stripePayments.checkout({
token,
user,
gift,
groupId,
email,
headers,
coupon,
}, stripe);
expect(stripeChargeStub).to.be.calledOnce;
expect(stripeChargeStub).to.be.calledWith({
amount: 500,
currency: 'usd',
card: token,
});
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,
customerId: customerIdResponse,
paymentMethod: 'Stripe',
gift,
});
});
it('should gift gems', async () => {
let receivingUser = new User();
receivingUser.save();
gift = {
type: 'gems',
gems: {
amount: 16,
uuid: receivingUser._id,
},
};
await stripePayments.checkout({
token,
user,
gift,
groupId,
email,
headers,
coupon,
}, stripe);
gift.member = receivingUser;
expect(stripeChargeStub).to.be.calledOnce;
expect(stripeChargeStub).to.be.calledWith({
amount: '400',
currency: 'usd',
card: token,
});
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,
customerId: customerIdResponse,
paymentMethod: 'Gift',
gift,
});
});
it('should gift a subscription', async () => {
let receivingUser = new User();
receivingUser.save();
gift = {
type: 'subscription',
subscription: {
key: subKey,
uuid: receivingUser._id,
},
};
await stripePayments.checkout({
token,
user,
gift,
groupId,
email,
headers,
coupon,
}, stripe);
gift.member = receivingUser;
expect(stripeChargeStub).to.be.calledOnce;
expect(stripeChargeStub).to.be.calledWith({
amount: '1500',
currency: 'usd',
card: token,
});
expect(paymentCreateSubscritionStub).to.be.calledOnce;
expect(paymentCreateSubscritionStub).to.be.calledWith({
user,
customerId: customerIdResponse,
paymentMethod: 'Gift',
gift,
});
});
});
describe('checkout with subscription', () => {
let user, group, data, gift, sub, groupId, email, headers, coupon, customerIdResponse, subscriptionId, token;
let spy;
let stripeCreateCustomerSpy;
let stripePaymentsCreateSubSpy;
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
user.purchased.plan.customerId = 'customer-id';
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 = 'customer-id';
group.purchased.plan.planId = subKey;
await group.save();
sub = {
key: 'basic_3mo',
};
data = {
user,
sub,
customerId: 'customer-id',
paymentMethod: 'Payment Method',
};
email = 'example@example.com';
customerIdResponse = 'test-id';
subscriptionId = 'test-sub-id';
token = 'test-token';
spy = sinon.stub(stripe.subscriptions, 'update');
spy.returnsPromise().resolves;
stripeCreateCustomerSpy = sinon.stub(stripe.customers, 'create');
let stripCustomerResponse = {
id: customerIdResponse,
subscriptions: {
data: [{id: subscriptionId}],
},
};
stripeCreateCustomerSpy.returnsPromise().resolves(stripCustomerResponse);
stripePaymentsCreateSubSpy = sinon.stub(payments, 'createSubscription');
stripePaymentsCreateSubSpy.returnsPromise().resolves({});
data.groupId = group._id;
data.sub.quantity = 3;
});
afterEach(function () {
sinon.restore(stripe.subscriptions.update);
stripe.customers.create.restore();
payments.createSubscription.restore();
});
it('should throw an error if we are missing a token', async () => {
await expect(stripePayments.checkout({
user,
gift,
sub,
groupId,
email,
headers,
coupon,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
message: 'Missing req.body.id',
});
});
it('should throw an error when coupon code is missing', async () => {
sub.discount = 40;
await expect(stripePayments.checkout({
token,
user,
gift,
sub,
groupId,
email,
headers,
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(stripePayments.checkout({
token,
user,
gift,
sub,
groupId,
email,
headers,
coupon,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
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);
await stripePayments.checkout({
token,
user,
gift,
sub,
groupId,
email,
headers,
coupon,
}, stripe);
expect(stripeCreateCustomerSpy).to.be.calledOnce;
expect(stripeCreateCustomerSpy).to.be.calledWith({
email,
metadata: { uuid: user._id },
card: token,
plan: sub.key,
});
expect(stripePaymentsCreateSubSpy).to.be.calledOnce;
expect(stripePaymentsCreateSubSpy).to.be.calledWith({
user,
customerId: customerIdResponse,
paymentMethod: 'Stripe',
sub,
headers,
groupId: undefined,
subscriptionId: undefined,
});
cc.validate.restore();
});
it('subscribes a user', async () => {
sub = data.sub;
await stripePayments.checkout({
token,
user,
gift,
sub,
groupId,
email,
headers,
coupon,
}, stripe);
expect(stripeCreateCustomerSpy).to.be.calledOnce;
expect(stripeCreateCustomerSpy).to.be.calledWith({
email,
metadata: { uuid: user._id },
card: token,
plan: sub.key,
});
expect(stripePaymentsCreateSubSpy).to.be.calledOnce;
expect(stripePaymentsCreateSubSpy).to.be.calledWith({
user,
customerId: customerIdResponse,
paymentMethod: 'Stripe',
sub,
headers,
groupId: undefined,
subscriptionId: undefined,
});
});
it('subscribes a group', async () => {
token = 'test-token';
sub = data.sub;
groupId = group._id;
email = 'test@test.com';
headers = {};
await stripePayments.checkout({
token,
user,
gift,
sub,
groupId,
email,
headers,
coupon,
}, stripe);
expect(stripeCreateCustomerSpy).to.be.calledOnce;
expect(stripeCreateCustomerSpy).to.be.calledWith({
email,
metadata: { uuid: user._id },
card: token,
plan: sub.key,
quantity: 3,
});
expect(stripePaymentsCreateSubSpy).to.be.calledOnce;
expect(stripePaymentsCreateSubSpy).to.be.calledWith({
user,
customerId: customerIdResponse,
paymentMethod: 'Stripe',
sub,
headers,
groupId,
subscriptionId,
});
});
});
describe('edit subscription', () => {
let user, groupId, group, token;
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
user.purchased.plan.customerId = 'customer-id';
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 = 'customer-id';
group.purchased.plan.planId = subKey;
await group.save();
groupId = group._id;
token = 'test-token';
});
it('throws an error if there is no customer id', async () => {
user.purchased.plan.customerId = undefined;
await expect(stripePayments.editSubscription({
user,
groupId: undefined,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('missingSubscription'),
});
});
it('throws an error if a token is not provided', async () => {
await expect(stripePayments.editSubscription({
user,
groupId: undefined,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
message: 'Missing req.body.id',
});
});
it('throws an error if the group is not found', async () => {
await expect(stripePayments.editSubscription({
token,
user,
groupId: 'fake-group',
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 404,
name: 'NotFound',
message: i18n.t('groupNotFound'),
});
});
it('throws an error if user is not the group leader', async () => {
let nonLeader = new User();
nonLeader.guilds.push(groupId);
await nonLeader.save();
await expect(stripePayments.editSubscription({
token,
user: nonLeader,
groupId,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
});
});
describe('success', () => {
let stripeListSubscriptionStub, stripeUpdateSubscriptionStub, subscriptionId;
beforeEach(() => {
subscriptionId = 'subId';
stripeListSubscriptionStub = sinon.stub(stripe.customers, 'listSubscriptions')
.returnsPromise().resolves({
data: [{id: subscriptionId}],
});
stripeUpdateSubscriptionStub = sinon.stub(stripe.customers, 'updateSubscription').returnsPromise().resolves({});
});
afterEach(() => {
stripe.customers.listSubscriptions.restore();
stripe.customers.updateSubscription.restore();
});
it('edits a user subscription', async () => {
await stripePayments.editSubscription({
token,
user,
groupId: undefined,
}, stripe);
expect(stripeListSubscriptionStub).to.be.calledOnce;
expect(stripeListSubscriptionStub).to.be.calledWith(user.purchased.plan.customerId);
expect(stripeUpdateSubscriptionStub).to.be.calledOnce;
expect(stripeUpdateSubscriptionStub).to.be.calledWith(
user.purchased.plan.customerId,
subscriptionId,
{ card: token }
);
});
it('edits a group subscription', async () => {
await stripePayments.editSubscription({
token,
user,
groupId,
}, stripe);
expect(stripeListSubscriptionStub).to.be.calledOnce;
expect(stripeListSubscriptionStub).to.be.calledWith(group.purchased.plan.customerId);
expect(stripeUpdateSubscriptionStub).to.be.calledOnce;
expect(stripeUpdateSubscriptionStub).to.be.calledWith(
group.purchased.plan.customerId,
subscriptionId,
{ card: token }
);
});
});
});
describe('cancel subscription', () => {
let user, groupId, group;
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
user.purchased.plan.customerId = 'customer-id';
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 = 'customer-id';
group.purchased.plan.planId = subKey;
await group.save();
groupId = group._id;
});
it('throws an error if there is no customer id', async () => {
user.purchased.plan.customerId = undefined;
await expect(stripePayments.cancelSubscription({
user,
groupId: undefined,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('missingSubscription'),
});
});
it('throws an error if the group is not found', async () => {
await expect(stripePayments.cancelSubscription({
user,
groupId: 'fake-group',
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 404,
name: 'NotFound',
message: i18n.t('groupNotFound'),
});
});
it('throws an error if user is not the group leader', async () => {
let nonLeader = new User();
nonLeader.guilds.push(groupId);
await nonLeader.save();
await expect(stripePayments.cancelSubscription({
user: nonLeader,
groupId,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
});
});
describe('success', () => {
let stripeDeleteCustomerStub, paymentsCancelSubStub, stripeRetrieveStub, subscriptionId, currentPeriodEndTimeStamp;
beforeEach(() => {
subscriptionId = 'subId';
stripeDeleteCustomerStub = sinon.stub(stripe.customers, 'del').returnsPromise().resolves({});
paymentsCancelSubStub = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
currentPeriodEndTimeStamp = (new Date()).getTime();
stripeRetrieveStub = sinon.stub(stripe.customers, 'retrieve')
.returnsPromise().resolves({
subscriptions: {
data: [{id: subscriptionId, current_period_end: currentPeriodEndTimeStamp}], // eslint-disable-line camelcase
},
});
});
afterEach(() => {
stripe.customers.del.restore();
stripe.customers.retrieve.restore();
payments.cancelSubscription.restore();
});
it('cancels a user subscription', async () => {
await stripePayments.cancelSubscription({
user,
groupId: undefined,
}, stripe);
expect(stripeDeleteCustomerStub).to.be.calledOnce;
expect(stripeDeleteCustomerStub).to.be.calledWith(user.purchased.plan.customerId);
expect(stripeRetrieveStub).to.be.calledOnce;
expect(stripeRetrieveStub).to.be.calledWith(user.purchased.plan.customerId);
expect(paymentsCancelSubStub).to.be.calledOnce;
expect(paymentsCancelSubStub).to.be.calledWith({
user,
groupId: undefined,
nextBill: currentPeriodEndTimeStamp * 1000, // timestamp in seconds
paymentMethod: 'Stripe',
});
});
it('cancels a group subscription', async () => {
await stripePayments.cancelSubscription({
user,
groupId,
}, stripe);
expect(stripeDeleteCustomerStub).to.be.calledOnce;
expect(stripeDeleteCustomerStub).to.be.calledWith(group.purchased.plan.customerId);
expect(stripeRetrieveStub).to.be.calledOnce;
expect(stripeRetrieveStub).to.be.calledWith(user.purchased.plan.customerId);
expect(paymentsCancelSubStub).to.be.calledOnce;
expect(paymentsCancelSubStub).to.be.calledWith({
user,
groupId,
nextBill: currentPeriodEndTimeStamp * 1000, // timestamp in seconds
paymentMethod: 'Stripe',
});
});
});
});
});

View File

@@ -2,6 +2,7 @@ import {
createTasks,
getTasks,
syncableAttrs,
moveTask,
} from '../../../../../website/server/libs/taskManager';
import i18n from '../../../../../website/common/script/i18n';
import {
@@ -169,4 +170,12 @@ describe('taskManager', () => {
expect(syncableTask.notes).to.not.exist;
expect(syncableTask.updatedAt).to.not.exist;
});
it('moves tasks to a specified position', async() => {
let order = ['task-id-1', 'task-id-2'];
moveTask(order, 'task-id-2', 0);
expect(order).to.eql(['task-id-2', 'task-id-1']);
});
});

View File

@@ -170,15 +170,19 @@ describe('Group Model', () => {
});
context('Boss Quests', () => {
let sendChatStub;
beforeEach(async () => {
party.quest.key = 'whale';
await party.startQuest(questLeader);
await party.save();
sandbox.stub(Group.prototype, 'sendChat');
sendChatStub = sandbox.stub(Group.prototype, 'sendChat');
});
afterEach(() => sendChatStub.restore());
it('applies user\'s progress to quest boss hp', async () => {
await Group.processQuestProgress(participatingMember, progress);
@@ -322,15 +326,19 @@ describe('Group Model', () => {
});
context('Collection Quests', () => {
let sendChatStub;
beforeEach(async () => {
party.quest.key = 'atom1';
await party.startQuest(questLeader);
await party.save();
sandbox.stub(Group.prototype, 'sendChat');
sendChatStub = sandbox.stub(Group.prototype, 'sendChat');
});
afterEach(() => sendChatStub.restore());
it('applies user\'s progress to found quest items', async () => {
await Group.processQuestProgress(participatingMember, progress);
@@ -365,6 +373,7 @@ describe('Group Model', () => {
party.quest.active = false;
await party.startQuest(questLeader);
Group.prototype.sendChat.reset();
await party.save();
await Group.processQuestProgress(participatingMember, progress);
@@ -383,6 +392,7 @@ describe('Group Model', () => {
party.quest.active = false;
await party.startQuest(questLeader);
Group.prototype.sendChat.reset();
await party.save();
await Group.processQuestProgress(participatingMember, progress);
@@ -809,6 +819,20 @@ describe('Group Model', () => {
expect(party.chat).to.have.a.lengthOf(200);
});
it('cuts down chat to 400 messages when group is subcribed', () => {
party.purchased.plan.customerId = 'test-customer-id';
for (let i = 0; i < 420; i++) {
party.chat.push({ text: 'a message' });
}
expect(party.chat).to.have.a.lengthOf(420);
party.sendChat('message');
expect(party.chat).to.have.a.lengthOf(400);
});
it('updates users about new messages in party', () => {
party.sendChat('message');

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();
}
});

View File

@@ -240,6 +240,5 @@ describe('Groups Controller', function() {
describe.skip("clickMember", function() { });
describe.skip("removeMember", function() { });
describe.skip("confirmRemoveMember", function() { });
describe.skip("openInviteModal", function() { });
describe.skip("quickReply", function() { });
});

View File

@@ -0,0 +1,63 @@
describe('Group Tasks Meta Actions Controller', () => {
let rootScope, scope, user, userSerivce;
beforeEach(() => {
module(function($provide) {
$provide.value('User', {});
});
inject(($rootScope, $controller) => {
rootScope = $rootScope;
user = specHelper.newUser();
user._id = "unique-user-id";
userSerivce = {user: user};
scope = $rootScope.$new();
scope.task = {
group: {
assignedUsers: [],
approval: {
required: false,
}
},
};
scope.task._edit = angular.copy(scope.task);
$controller('GroupTaskActionsCtrl', {$scope: scope, User: userSerivce});
});
});
describe('toggleTaskRequiresApproval', function () {
it('toggles task approval required field from false to true', function () {
scope.toggleTaskRequiresApproval();
expect(scope.task._edit.group.approval.required).to.be.true;
});
it('toggles task approval required field from true to false', function () {
scope.task._edit.group.approval.required = true;
scope.toggleTaskRequiresApproval();
expect(scope.task._edit.group.approval.required).to.be.false;
});
});
describe('assign events', function () {
it('adds a group member to assigned users on "addedGroupMember" event ', () => {
var testId = 'test-id';
rootScope.$broadcast('addedGroupMember', testId);
expect(scope.task.group.assignedUsers).to.contain(testId);
expect(scope.task._edit.group.assignedUsers).to.contain(testId);
});
it('removes a group member to assigned users on "addedGroupMember" event ', () => {
var testId = 'test-id';
scope.task.group.assignedUsers.push(testId);
scope.task._edit.group.assignedUsers.push(testId);
rootScope.$broadcast('removedGroupMember', testId);
expect(scope.task.group.assignedUsers).to.not.contain(testId);
expect(scope.task._edit.group.assignedUsers).to.not.contain(testId);
});
});
});

View File

@@ -0,0 +1,42 @@
describe('Group Task Actions Controller', () => {
let scope, user, userSerivce;
beforeEach(() => {
module(function($provide) {
$provide.value('User', {});
});
inject(($rootScope, $controller) => {
user = specHelper.newUser();
user._id = "unique-user-id";
userSerivce = {user: user};
userSerivce.sync = sandbox.stub();
scope = $rootScope.$new();
$controller('GroupTaskMetaActionsCtrl', {$scope: scope, User: userSerivce});
scope.task = {
group: {
assignedUsers: [],
},
};
});
});
describe('claim', () => {
beforeEach(() => {
sandbox.stub(window, 'confirm').returns(true);
});
it('adds user to assigned users of scope task ', () => {
scope.claim();
expect(scope.task.group.assignedUsers).to.contain(user._id);
});
it('syncs user tasks ', () => {
scope.claim();
expect(userSerivce.sync).to.be.calledOnce;
});
});
});

View File

@@ -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!');
});
});
});
});

View File

@@ -44,6 +44,7 @@ module.exports = function karmaConfig (config) {
'../../../website/client-old/js/filters/**/*.js',
'../../../website/client-old/js/directives/**/*.js',
'../../../website/client-old/js/controllers/**/*.js',
'../../../website/client-old/js/components/**/*.js',
'../../../test/client-old/spec/specHelper.js',
'../../../test/client-old/spec/**/*.js',

View File

@@ -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() {

View File

@@ -1,7 +1,7 @@
'use strict';
describe('groupServices', function() {
var $httpBackend, $http, groups, user;
var $httpBackend, $http, groups, user, $rootScope;
var groupApiUrlPrefix = '/api/v3/groups';
beforeEach(function() {
@@ -13,8 +13,10 @@ describe('groupServices', function() {
$provide.value('User', {user: user});
});
inject(function(_$httpBackend_, Groups, User) {
inject(function(_$httpBackend_, _$rootScope_, Groups, User) {
$httpBackend = _$httpBackend_;
$rootScope = _$rootScope_;
$rootScope.openModal = function() {}
groups = Groups;
});
});
@@ -166,4 +168,33 @@ describe('groupServices', function() {
$httpBackend.flush()
});
it('sets a "sendInviteText" property on a party to "Send Invitations"', function() {
var sendInviteText = window.env.t('sendInvitations');
var party = {
type: 'party',
data: {
_id: '1234',
},
};
groups.inviteOrStartParty(party);
expect(party.sendInviteText).to.eql(sendInviteText);
});
it('sets a "sendInviteText" proptery on a guild to "Send Invitations +$3.00/month/user"', function() {
var sendInviteText = window.env.t('sendInvitations');
var guild = {
type: 'guild',
data: {
_id: '12345',
},
purchased: {
plan: {
customerId: '123',
},
},
};
groups.inviteOrStartParty(guild);
expect(guild.sendInviteText).to.eql(sendInviteText + window.env.t('groupAdditionalUserCost'));
});
});

View File

@@ -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 = {

View File

@@ -17,7 +17,14 @@ describe('Tasks Service', function() {
tasks = Tasks;
});
rootScope.openModal = function () {};
rootScope.openModal = function() {
return {
result: {
then: function() {},
catch: function() {},
},
};
};
});
it('calls get user tasks endpoint', function() {
@@ -83,6 +90,14 @@ describe('Tasks Service', function() {
$httpBackend.flush();
});
it('calls group move task endpoint', function() {
var taskId = 1;
var position = 0;
$httpBackend.expectPOST('/api/v3/group-tasks/' + taskId + '/move/to/' + position).respond({});
tasks.moveGroupTask(taskId, position);
$httpBackend.flush();
});
it('calls add check list item endpoint', function() {
var taskId = 1;
$httpBackend.expectPOST(apiV3Prefix + '/' + taskId + '/checklist').respond({});

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,
}

6
test/client/.eslintrc Normal file
View File

@@ -0,0 +1,6 @@
{
"env": {
"node": true,
"browser": true,
}
}

View File

@@ -7,7 +7,7 @@
// for how to write custom assertions see
// http://nightwatchjs.org/guide#writing-custom-assertions
exports.assertion = function (selector, count) {
this.message = 'Testing if element <' + selector + '> has count: ' + count;
this.message = `Testing if element <${selector}> has count: ${count}`;
this.expected = count;
this.pass = function (val) {
return val === this.expected;
@@ -16,11 +16,10 @@ exports.assertion = function (selector, count) {
return res.value;
};
this.command = function (cb) {
var self = this;
return this.api.execute(function (selector) {
return document.querySelectorAll(selector).length;
}, [selector], function (res) {
cb.call(self, res);
return this.api.execute((el) => {
return document.querySelectorAll(el).length;
}, [selector], (res) => {
cb.call(this, res);
});
};
};

View File

@@ -1,45 +1,49 @@
/* eslint-disable camelcase */
require('babel-register');
var config = require('../../../webpack/config');
const config = require('../../../webpack/config');
const chromeDriverPath = require('chromedriver').path;
const seleniumServerPath = require('selenium-server').path;
// http://nightwatchjs.org/guide#settings-file
module.exports = {
'src_folders': ['test/client/e2e/specs'],
'output_folder': 'test/client/e2e/reports',
'custom_assertions_path': ['test/client/e2e/custom-assertions'],
src_folders: ['test/client/e2e/specs'],
output_folder: 'test/client/e2e/reports',
custom_assertions_path: ['test/client/e2e/custom-assertions'],
'selenium': {
'start_process': true,
'server_path': 'node_modules/selenium-server/lib/runner/selenium-server-standalone-2.53.0.jar',
'host': '127.0.0.1',
'port': 4444,
'cli_args': {
'webdriver.chrome.driver': require('chromedriver').path,
selenium: {
start_process: true,
server_path: seleniumServerPath,
host: '127.0.0.1',
port: 4444,
cli_args: {
'webdriver.chrome.driver': chromeDriverPath,
},
},
'test_settings': {
'default': {
'selenium_port': 4444,
'selenium_host': 'localhost',
'silent': true,
'globals': {
'devServerURL': 'http://localhost:' + (process.env.PORT || config.dev.port),
test_settings: {
default: {
selenium_port: 4444,
selenium_host: 'localhost',
silent: true,
globals: {
devServerURL: `http://localhost:${process.env.PORT || config.dev.port}`, // eslint-disable-line no-process-env
},
},
'chrome': {
'desiredCapabilities': {
'browserName': 'chrome',
'javascriptEnabled': true,
'acceptSslCerts': true,
chrome: {
desiredCapabilities: {
browserName: 'chrome',
javascriptEnabled: true,
acceptSslCerts: true,
},
},
'firefox': {
'desiredCapabilities': {
'browserName': 'firefox',
'javascriptEnabled': true,
'acceptSslCerts': true,
firefox: {
desiredCapabilities: {
browserName: 'firefox',
javascriptEnabled: true,
acceptSslCerts: true,
},
},
},

View File

@@ -1,6 +1,6 @@
// 1. start the dev server using production config
process.env.NODE_ENV = 'testing';
var server = require('../../../webpack/dev-server.js');
process.env.NODE_ENV = 'testing'; // eslint-disable-line no-process-env
const server = require('../../../webpack/dev-server.js');
// 2. run the nightwatch test suite against it
// to run in additional browsers:
@@ -9,7 +9,7 @@ var server = require('../../../webpack/dev-server.js');
// or override the environment flag, for example: `npm run e2e -- --env chrome,firefox`
// For more information on Nightwatch's config file, see
// http://nightwatchjs.org/guide#settings-file
var opts = process.argv.slice(2);
let opts = process.argv.slice(2);
if (opts.indexOf('--config') === -1) {
opts = opts.concat(['--config', 'test/client/e2e/nightwatch.conf.js']);
}
@@ -17,8 +17,8 @@ if (opts.indexOf('--env') === -1) {
opts = opts.concat(['--env', 'chrome']);
}
var spawn = require('cross-spawn');
var runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' });
const spawn = require('cross-spawn');
const runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' });
runner.on('exit', function (code) {
server.close();

View File

@@ -2,12 +2,11 @@
// http://nightwatchjs.org/guide#usage
module.exports = {
'default e2e tests': function (browser) {
'default e2e tests' (browser) {
// automatically uses dev Server port from /config.index.js
// default: http://localhost:8080
// see nightwatch.conf.js
var devServer = browser.globals.devServerURL;
const devServer = browser.globals.devServerURL;
browser
.url(devServer)

View File

@@ -1,11 +1,11 @@
// TODO verify if it's needed, added because Vuex require Promise in the global scope
// TODO verify if it's needed, added because Axios require Promise in the global scope
// and babel-runtime doesn't affect external libraries
require('babel-polyfill');
// require all test files (files that ends with .spec.js)
var testsContext = require.context('./specs', true, /\.spec$/);
let testsContext = require.context('./specs', true, /\.spec$/);
testsContext.keys().forEach(testsContext);
// require all .vue and .js files except main.js for coverage.
var srcContext = require.context('../../../website/client', true, /^\.\/(?=(?!main(\.js)?$))(?=(.*\.(vue|js)$))/);
let srcContext = require.context('../../../website/client', true, /^\.\/(?=(?!main(\.js)?$))(?=(.*\.(vue|js)$))/);
srcContext.keys().forEach(srcContext);

View File

@@ -3,52 +3,12 @@
// we are also using it with karma-webpack
// https://github.com/webpack/karma-webpack
var path = require('path');
var merge = require('webpack-merge');
var baseConfig = require('../../../webpack/webpack.base.conf');
var utils = require('../../../webpack/utils');
var webpack = require('webpack');
var projectRoot = path.resolve(__dirname, '../../../');
// 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
var 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': require('../../../webpack/config/test.env'),
}),
],
});
// 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(function (loader, i) {
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({

View File

@@ -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);
});
});

View File

@@ -0,0 +1,19 @@
import i18n from 'client/plugins/i18n';
import commoni18n from 'common/script/i18n';
import Vue from 'vue';
describe('i18n plugin', () => {
before(() => {
i18n.install(Vue);
});
it('adds $t to Vue.prototype', () => {
expect(Vue.prototype.$t).to.be.a.function;
});
it('$t is a proxy for common/i18n.t', () => {
const result = (new Vue()).$t('reportBug');
expect(result).to.equal(commoni18n.t('reportBug'));
expect(result).to.equal('Report a Bug');
});
});

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';

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