Compare commits

...

726 Commits

Author SHA1 Message Date
Matteo Pagliazzi
a63ba51497 4.80.7 2019-01-23 17:12:13 +01:00
Matteo Pagliazzi
04a7fd25a6 allow skipping SSL check with secret key (#10962) 2019-01-23 17:10:11 +01:00
Sabe Jones
7cb045781b 4.80.6 2019-01-18 15:13:34 -06:00
Sabe Jones
f8d799d55c chore(news): Bailey 2019-01-18 15:13:25 -06:00
Matteo Pagliazzi
4f9d97d38f 4.80.5 2019-01-18 12:10:15 +01:00
Matteo Pagliazzi
40060e8ff5 update ssl check 2019-01-18 11:43:02 +01:00
Sabe Jones
508d97d374 4.80.4 2019-01-16 10:22:14 -06:00
Sabe Jones
fd125352b7 fix(g1g1): remove additional banner 2019-01-16 10:22:00 -06:00
Sabe Jones
f8bd1be4a3 4.80.3 2019-01-16 06:24:07 -06:00
Sabe Jones
ec37524164 chore(promo): end G1G1 deal 2019-01-15 18:57:10 -06:00
Sabe Jones
87b9e72b56 4.80.2 2019-01-14 12:39:03 -06:00
Sabe Jones
962662fe7c chore(news): Bailey 2019-01-14 12:38:35 -06:00
Sabe Jones
349a1032b6 4.80.1 2019-01-11 20:00:31 +00:00
Sabe Jones
476131835d chore(i18n): update locales 2019-01-11 19:59:54 +00:00
Sabe Jones
8237b7f2de chore(news): Bailey 2019-01-11 13:37:58 -06:00
Sabe Jones
6ee2b3690a chore(i18n): update locales 2019-01-10 23:28:33 +00:00
Sabe Jones
a08cca807a 4.80.0 2019-01-09 01:38:46 +00:00
Sabe Jones
8c51f36784 chore(i18n): update locales 2019-01-09 01:35:24 +00:00
Sabe Jones
d35f81cdae chore(sprites): compile 2019-01-08 19:30:49 -06:00
Sabe Jones
1d1b25391f feat(content): customizations and dinosaurs 2019-01-08 19:30:41 -06:00
Sabe Jones
ec81c02d72 4.79.0 2019-01-04 20:04:19 +00:00
Sabe Jones
166da3c2f8 chore(i18n): update locales 2019-01-04 20:04:12 +00:00
Sabe Jones
23b72a673d chore(sprites): compile 2019-01-04 13:59:27 -06:00
Sabe Jones
d22b4bb2f7 feat(content): Armoire and Backgrounds Jan 2019 2019-01-04 13:58:51 -06:00
Sabe Jones
aaebd4da77 4.78.2 2019-01-03 22:31:27 +00:00
Sabe Jones
b128e7874e chore(i18n): update locales 2019-01-03 22:31:16 +00:00
Juliusdotsh
1e10e20a24 apidocs should say how to provide comment when flagging #10916 (#10925) 2019-01-03 11:31:03 +01:00
Sabe Jones
b1aeb8ed87 Merge branch 'release' into develop 2019-01-02 17:08:50 -06:00
Sabe Jones
2cb80e2275 4.78.1 2019-01-02 22:14:24 +00:00
Sabe Jones
ee32e24ff2 chore(i18n): update locales 2019-01-02 22:14:05 +00:00
Sabe Jones
af40c437be chore(news): Bailey
also fix erroneously active hatching potion and remove concluded bundle 
from pinned items
2019-01-02 16:10:57 -06:00
Sabe Jones
a99150c485 Merge branch 'release' into develop 2019-01-01 07:14:04 -06:00
Sabe Jones
696b67204d fix(lint): ’ 2019-01-01 07:13:41 -06:00
Sabe Jones
4f3536e887 Merge branch 'release' into develop 2018-12-31 23:48:59 +00:00
Sabe Jones
07e5bf1437 4.78.0 2018-12-31 23:48:21 +00:00
Sabe Jones
d2ca738256 chore(i18n): update locales 2018-12-31 23:48:11 +00:00
Sabe Jones
f7983f39eb feat(event): New Year's 2018-19 2018-12-31 17:44:13 -06:00
Rene Cordier
7c954f7073 Prevent progress being cleared when quest ends (#10870)
* Prevent progress being cleared when quest ends

changing group tests to make sure it keeps user's progress

fix and remove .only() from tests

* fix tests and check null case for clearing up user's quest without resetting progress
2018-12-28 19:16:21 +01:00
Sabe Jones
82d0e737a6 4.77.6 2018-12-27 15:47:36 +00:00
Sabe Jones
f8213aaf1b chore(i18n): update locales 2018-12-27 15:47:25 +00:00
Matteo Pagliazzi
2335ad4167 fix(gems modal): remove duplicate id property, fix #10919 2018-12-27 15:04:14 +01:00
negue
d84631255b extract site urls from translations (#10909) 2018-12-23 19:56:21 +01:00
Raymond Grumney
0b352b9103 Unit Tests; 1 fix to appFooter debug menu (#10894)
* Built

* Update .editorconfig

* Fixed filename ('translaotr.js' => 'translator.js')

* Fixed Filename

* User Unit Tests

* Writing tests for avatar.vue

* writing tests for getters/user.js

* Writing Tests for avatar.vue

* Writing Tests for avatar.vue

* writing tests for store/getters/user.js

* renamed file

* Integrated test into user.test.js

* Reverting

* Fetching most recent version

* +1 Level now adds a level

* updating to current version

* Minor edits to pass lint test

* Removing non-functional Tests

* Cleaning up
2018-12-23 19:45:59 +01:00
negue
19b75c6257 drag tags to reorder (#10911)
* drag tags to reorder

* change color, remove unneeded sortTag-call
2018-12-23 19:45:05 +01:00
negue
b66904a3a7 notification click always shows the modal, setting only controls the notification timeout (#10913) 2018-12-23 19:35:30 +01:00
Phillip Thelen
cfbfec34aa Allow gems and subs to be gifted through in-app-purchases (#10892)
* Allow gems to be gifted through IAPs

* implement non recurring IAP subscriptions

* fix localization issue in error

* fix non renewing subscription handling

* Fix lint error

* fix tests

* move findbyId mock to helper file

* undo package-lock changes

* Fix lint error
2018-12-23 19:20:14 +01:00
Sabe Jones
88f28188a1 4.77.5 2018-12-21 20:45:20 +00:00
Sabe Jones
fce5be2e8f chore(i18n): update locales 2018-12-21 20:44:16 +00:00
Sabe Jones
1d68cbfaa2 chore(news): more Bailey 2018-12-21 14:42:11 -06:00
Sabe Jones
a44222a350 4.77.4 2018-12-20 16:37:04 -06:00
Sabe Jones
4d2510e322 fix(sprites): add missing dragon icon 2018-12-20 16:36:57 -06:00
Sabe Jones
3b5ed33e03 4.77.3 2018-12-20 21:29:58 +00:00
Sabe Jones
0a6bf92b6b chore(i18n): update locales 2018-12-20 21:29:24 +00:00
Sabe Jones
05ae40bc2e fix(content): correct timeframe tag for string 2018-12-20 15:26:17 -06:00
Sabe Jones
57e96ea092 4.77.2 2018-12-20 12:14:38 -06:00
Sabe Jones
ea49b5b8e0 fix(content): add set string for Dec Mystery 2018-12-20 12:14:28 -06:00
Sabe Jones
11a5de714a 4.77.1 2018-12-20 17:25:33 +00:00
Sabe Jones
f74b4d3e73 Merge branch 'develop' into release 2018-12-20 17:24:59 +00:00
Sabe Jones
ab6fdb99af 4.77.0 2018-12-20 17:23:29 +00:00
Sabe Jones
e3efa557dd chore(i18n): update locales 2018-12-20 17:22:57 +00:00
Sabe Jones
a6cea47789 feat(event): Winter Wonderland 2018 2018-12-20 11:11:50 -06:00
Sabe Jones
5a200a88bd chore(sprites): compile 2018-12-20 09:42:04 -06:00
Sabe Jones
545499ea0b WIP(event): partial content build 2018-12-20 09:40:54 -06:00
Sabe Jones
3e7f4229ec Merge branch 'release' into develop 2018-12-19 13:03:20 +00:00
Sabe Jones
56d5f77b6e 4.76.1 2018-12-19 13:02:58 +00:00
Sabe Jones
efdf5a2e16 chore(i18n): update locales 2018-12-19 13:01:40 +00:00
Sabe Jones
add3a2887f fix(css): restore close svg defaults 2018-12-19 06:58:56 -06:00
Sabe Jones
0ed7c7596a Merge branch 'release' into develop 2018-12-18 21:32:39 +00:00
Sabe Jones
55a452694f 4.76.0 2018-12-18 21:32:12 +00:00
Sabe Jones
cd4d5f83ff chore(i18n): update locales 2018-12-18 21:31:56 +00:00
Sabe Jones
8220199e49 Gift 1 Get 1 Promo 2018-19! (#10915)
* feat(subscription): promo banner in modal

* feat(subscription): promo banner on main page

* fix(banners): remove extraneous margin adjustment

* fix(banners): various

* feat(promotion): gift 1, get 1

* fix(promo): various

* chore(promo): add Bailey

* fix(promo): use different email template for promo beneficiary

* fix(promo): turns out Winter is meaningful
2018-12-18 15:28:53 -06:00
Sabe Jones
bd1f6918ba fix(sprites): wolf and penguin tweaks 2018-12-18 12:22:46 -06:00
Matteo Pagliazzi
5b21e62647 fix(i18n): format 2018-12-18 15:02:03 +01:00
Matteo Pagliazzi
5e76d6df21 fix(i18n): position of $ sign 2018-12-17 13:12:35 +01:00
Matteo Pagliazzi
7348145b7d fix(success-modal): misc fixes 2018-12-16 15:39:37 +01:00
Sabe Jones
ba2832d21f 4.75.5 2018-12-14 21:22:05 +00:00
Sabe Jones
688f5084a1 chore(i18n): update locales 2018-12-14 21:19:56 +00:00
Sabe Jones
d5955b8889 Merge branch 'develop' into release 2018-12-14 15:15:06 -06:00
Sabe Jones
01fd17ee3f fix(script): revert email query 2018-12-14 15:14:50 -06:00
Sabe Jones
979497dd35 fix(deletion): user delete bugs
Correct lookup in GDPR script, and address a TypeError when deleting a user with no tasks
2018-12-14 00:54:59 +00:00
Sabe Jones
1d3db244ba 4.75.4 2018-12-11 18:26:58 -06:00
Sabe Jones
80ca074352 fix(deploy): correct Dockerfile 2018-12-11 18:26:07 -06:00
Sabe Jones
6d4f9e0759 4.75.3 2018-12-11 20:48:40 +00:00
Sabe Jones
d096695559 Usernames Misc: Bulk Email and New Party Modal (#10898)
* feat(migrations): genericize bulk email

* chore(migrations): archive one-offs

* feat(usernames): change create party modal to copy username

* fix(modal): styling

* fix(modal): add Chrome clipboard implementation
2018-12-11 20:48:34 +00:00
Sabe Jones
a2c8b8b05c Usernames Misc: Bulk Email and New Party Modal (#10898)
* feat(migrations): genericize bulk email

* chore(migrations): archive one-offs

* feat(usernames): change create party modal to copy username

* fix(modal): styling

* fix(modal): add Chrome clipboard implementation
2018-12-11 14:47:50 -06:00
Sabe Jones
8750701c08 Merge branch 'release' into develop 2018-12-11 20:45:44 +00:00
Sabe Jones
900676bf0a 4.75.2 2018-12-11 20:45:15 +00:00
Sabe Jones
e219daf44c chore(i18n): update locales 2018-12-11 20:44:53 +00:00
Sabe Jones
1b12a6b51f feat(content): Bird Buddies Bundle 2018-12-11 14:41:09 -06:00
Matteo Pagliazzi
8441b0a3d6 fix http auth env var 2018-12-09 20:12:50 +01:00
Matteo Pagliazzi
0667695390 Payments: Success Messages (#10903)
* fix v-once

* gems purchase success dialog

* amazon and stripe support for gems message, translations

* subscriptions success message

* subscriptions success message

* group plans success message and many fixes

* finish success messages for payments

* add success modal when gifting from balance
2018-12-09 20:08:01 +01:00
Matteo Pagliazzi
4d322c1bf6 Merge branch 'sukh0128-develop' into develop 2018-12-07 15:20:27 +01:00
Matteo Pagliazzi
a855ddacc7 Merge branch 'develop' of https://github.com/sukh0128/habitica into sukh0128-develop 2018-12-07 15:20:12 +01:00
Matteo Pagliazzi
9d48ef7322 Merge pull request #10896 from AyoubElk/patch-1
Fixed typo in folder path
2018-12-07 15:13:08 +01:00
Sabe Jones
2991f7acfb 4.75.1 2018-12-06 16:23:28 +00:00
Sabe Jones
9dbfb565bb chore(i18n): update locales 2018-12-06 16:22:06 +00:00
Sabe Jones
f42e22b58f Make env vars more palatable for containerization (#10895)
* refactor(env-vars): remove object/colon syntax

* fix(tests): correct config expectations
2018-12-06 10:13:49 -06:00
Ayoub El Khattabi
95f3315796 Fixed typo in folder path
The tast test:api:unit:watch should watch the path 'test/api/unit/**/*' instead of the non existent 'test/api/v3/unit/**/*'
2018-12-06 11:26:22 +00:00
Sabe Jones
18ab57eb91 Merge branch 'release' into develop 2018-12-05 20:44:02 +00:00
Sabe Jones
0dcbd8ccb8 4.75.0 2018-12-05 20:43:39 +00:00
Sabe Jones
ed82b46f7b chore(i18n): update locales 2018-12-05 20:43:23 +00:00
Sabe Jones
dcc3044685 chore(sprites): compile 2018-12-05 14:39:09 -06:00
Sabe Jones
5cd62d7052 feat(content): Armoire and BGs 2018-12 2018-12-05 14:38:53 -06:00
Erdenesukh Tsendjav
5e232d8c9f Merge branch 'develop' of github.com:sukh0128/habitica into develop 2018-12-04 17:29:16 -06:00
Erdenesukh Tsendjav
13793f8b3c revert 2018-12-04 17:28:27 -06:00
Erdenesukh Tsendjav
55f875f95a Delete package-lock.json 2018-12-04 17:04:55 -06:00
Erdenesukh Tsendjav
14576be374 merged 2018-12-04 16:03:11 -06:00
Erdenesukh Tsendjav
4cb2c26475 The istanbul command string in the gulp-tests.js file for each of the tests had to be changed to just the keyword istanbul instead of pointing to the istanbul file location on WINDOWS machines 2018-12-04 15:55:09 -06:00
Matteo Pagliazzi
b66a0b76ef Merge pull request #10886 from jeongjinhwi/develop
Accoount Deletion Feedback:update email #10880
2018-12-04 09:38:33 +01:00
Sabe Jones
dd313b17b5 fix(migration): correct push op 2018-12-03 21:06:23 -06:00
Sabe Jones
bb01475e02 Merge branch 'release' into develop 2018-12-03 22:54:08 +00:00
Sabe Jones
757959529b 4.74.7 2018-12-03 22:53:23 +00:00
Sabe Jones
71ad1957b1 chore(i18n): update locales 2018-12-03 22:52:42 +00:00
Sabe Jones
3ba5ea1d2d chore(news): Bailey 2018-12-03 16:50:02 -06:00
Sabe Jones
902da35f2b fix(migration): syntax 2018-12-03 22:06:58 +00:00
Sabe Jones
aaa16a9527 refactor(migrations): move Take This to new template
also fix linting error in server lib
2018-12-03 22:04:52 +00:00
Sabe Jones
b98e95ee45 chore(items): deactivate premium potions 2018-12-03 15:49:09 -06:00
Matteo Pagliazzi
757160d6b7 update karma to fix tests 2018-12-03 13:33:58 +01:00
Matteo Pagliazzi
4cf68eb018 update package-lock 2018-12-03 13:22:23 +01:00
Matteo Pagliazzi
e91d5e5664 Merge pull request #10863 from HabitRPG/payments/paypal-amazon-checkout
Paypal and Amazon: Improve checkout experience
2018-12-03 12:55:53 +01:00
Matteo Pagliazzi
b9e12aca3e success modal and fix redirects 2018-12-02 17:33:21 +01:00
Jeong Jin Hwi
53ca9475ee Accoount Deletion Feedback:update email #10880 2018-12-03 01:04:42 +09:00
Matteo Pagliazzi
e212842b50 fix redirects and basic success message 2018-12-02 15:44:52 +01:00
Matteo Pagliazzi
d2f7cba43d Merge branch 'develop' into payments/paypal-amazon-checkout 2018-12-02 14:47:56 +01:00
Matteo Pagliazzi
84558f79d6 Merge pull request #10885 from HabitRPG/greenkeeper/initial
Update dependencies to enable Greenkeeper 🌴
2018-12-02 14:46:11 +01:00
Matteo Pagliazzi
178e59f287 update npm files 2018-12-02 14:44:19 +01:00
greenkeeper[bot]
7acccc0763 chore(package): update dependencies 2018-12-02 13:36:14 +00:00
Matteo Pagliazzi
8ac21d2fd4 update package-lock.json 2018-12-01 18:53:12 +01:00
Matteo Pagliazzi
7873800f87 fix(kafka queue): remove leftover code 2018-12-01 12:16:34 +01:00
Sabe Jones
727041f020 fix(chat): escape regexes so parentheses etc. don't bust it 2018-11-30 16:19:22 -06:00
Sabe Jones
de0c62a37f Merge branch 'release' into develop 2018-11-30 20:28:23 +00:00
Sabe Jones
68f420991e 4.74.6 2018-11-30 20:27:53 +00:00
Sabe Jones
f211ebeb0a chore(i18n): update locales 2018-11-30 20:25:45 +00:00
Sabe Jones
b91ef9f539 chore(news): Last Chance Bailey
also give a few more days for items
2018-11-30 14:21:12 -06:00
Matteo Pagliazzi
ac0601630e Merge branch 'develop' into payments/paypal-amazon-checkout 2018-11-30 16:11:55 +01:00
Matteo Pagliazzi
3239491144 fix(lint): remove unused params 2018-11-30 16:04:56 +01:00
Sabe Jones
c3b0a73507 4.74.5 2018-11-29 14:52:02 +00:00
Sabe Jones
b218eb2c00 Merge remote-tracking branch 'origin/greenkeeper/update-to-node-10' into release
also remove Kafka
2018-11-29 14:51:40 +00:00
Sabe Jones
37d9f76fea 4.74.4 2018-11-29 07:48:06 -06:00
Matteo Pagliazzi
7f2e12ba23 remove memwatch-next, try/catch 2018-11-29 14:35:22 +01:00
Sabe Jones
21b43287e3 4.74.3 2018-11-29 12:11:35 +00:00
Sabe Jones
357b48dc8f fix(server): update package lock, attempt to log crashes 2018-11-29 12:11:03 +00:00
Matteo Pagliazzi
2cbd78b139 Merge branch 'develop' into payments/paypal-amazon-checkout 2018-11-29 10:55:13 +01:00
Sabe Jones
ee5dd5842b 4.74.2 2018-11-28 16:18:20 -06:00
Sabe Jones
074b8138de Merge branch 'develop' into release 2018-11-28 16:18:10 -06:00
Sabe Jones
cd0b9c0a96 4.74.1 2018-11-28 15:57:54 -06:00
Sabe Jones
a09516944d fix(packages): revert package lock 2018-11-28 15:57:36 -06:00
Sabe Jones
b82660823d Merge branch 'release' into develop 2018-11-28 20:57:37 +00:00
Sabe Jones
fde4402fbb 4.74.0 2018-11-28 20:57:14 +00:00
Sabe Jones
5fe0776074 chore(i18n): update locales 2018-11-28 20:55:35 +00:00
Sabe Jones
d218f316d3 chore(sprites): compile 2018-11-28 14:50:54 -06:00
Sabe Jones
f19e69948a feat(content): mystery items 2018-11 2018-11-28 14:50:43 -06:00
Sabe Jones
e6807d36b5 fix(analytics): record usernames 2018-11-28 13:02:48 -06:00
Phillip Thelen
1ece230621 remove console call 2018-11-28 12:52:08 -06:00
Phillip Thelen
d934d9d759 Move group chat analytics event to server 2018-11-28 12:52:00 -06:00
Matteo Pagliazzi
bf7fabb20a fix(amazon): add new env variable to specify environment 2018-11-28 11:07:06 +01:00
Sabe Jones
88a2f317d8 Merge branch 'release' into develop 2018-11-27 19:46:39 -06:00
SabreCat
a30c4379a6 fix(tests): correct string update errors 2018-11-28 01:00:46 +00:00
Matteo Pagliazzi
b509c6631d update package-lock.json 2018-11-27 22:19:25 +01:00
Matteo Pagliazzi
5a725fa4b0 update package-lock.json 2018-11-27 22:17:43 +01:00
Sabe Jones
dbe2143b7a fix(mystery-items): add missing set sprite 2018-11-26 14:23:18 -06:00
Sabe Jones
7b687280d7 Merge branch 'release' into develop 2018-11-26 19:10:05 +00:00
Sabe Jones
8db6c8bd4f 4.73.2 2018-11-26 19:09:42 +00:00
Sabe Jones
bf91dacb94 chore(i18n): update locales 2018-11-26 19:01:55 +00:00
Sabe Jones
33e0892e95 chore(event): end Thanksgiving tweaks 2018-11-26 12:59:23 -06:00
Phillip Thelen
42b146d5d0 Attach client to chat messages (#10845)
* Attach client to chat messages

* Word

* Design tweaks

* Fix potential error
2018-11-26 10:45:42 +01:00
Nathan Zimmerman
2bebaf2cf8 Fixes issue #10857 ("Tags have extra space at the bottom when they should be centered") (#10861)
* Fix for #10857 centered category tag text

* Fixes #10857 and #10856 display tag markdown.
2018-11-26 10:44:41 +01:00
Matteo Pagliazzi
6181328ac1 feat(footer): always show expanded footer (#10862) 2018-11-26 10:37:29 +01:00
Sabe Jones
c64d4b0914 4.73.1 2018-11-23 20:20:56 +00:00
Sabe Jones
f94fd0d69d chore(i18n): update locales 2018-11-23 20:17:56 +00:00
Sabe Jones
2cd66436bc Merge branch 'release' into develop 2018-11-22 18:05:28 -06:00
Sabe Jones
81a17738b8 4.73.0 2018-11-22 21:09:31 +00:00
Sabe Jones
b6b953ec46 chore(i18n): update locales 2018-11-22 21:09:20 +00:00
Sabe Jones
ab34c83a9d chore(sprites): compile 2018-11-22 15:05:33 -06:00
Sabe Jones
9cea86f4e0 feat(content): Turkey Day 2018 2018-11-22 15:05:03 -06:00
Sabe Jones
1b7a705bf9 Merge branch 'paglias/migration-no-recursion' into release 2018-11-22 13:44:46 -06:00
negue
e2c5b9058b more checks on the item.klass, also added the specialClass checks (#10859) 2018-11-22 14:35:34 +01:00
Matteo Pagliazzi
cc751960ac save state and close tab afetr success 2018-11-21 11:42:32 +01:00
Matteo Pagliazzi
433c73c9d3 paypal: restore url after purchase 2018-11-21 11:25:36 +01:00
Matteo Pagliazzi
680c2162a7 paypal: new redirects 2018-11-21 11:19:55 +01:00
Alys
a2b38ffb02 add two slurs - TRIGGER / CONTENT WARNING: assault, slurs, swearwords, etc 2018-11-21 19:48:50 +10:00
Matteo Pagliazzi
6395870c00 fix(stable): remove progress number from petItem 2018-11-21 10:35:59 +01:00
Sabe Jones
9562ba432f Merge branch 'release' into develop 2018-11-20 21:02:17 +00:00
Sabe Jones
8a3a83de37 4.72.0 2018-11-20 21:01:41 +00:00
Sabe Jones
745edd731d chore(i18n): update locales 2018-11-20 20:59:23 +00:00
Sabe Jones
53b195931c chore(sprites): compile 2018-11-20 14:55:56 -06:00
Sabe Jones
37ae467fff feat(content): Frost Hatching Potions 2018-11-20 14:55:45 -06:00
negue
d7d7d64b45 move computed-props to methods - refactor mountItem to use the states inside (#10853) 2018-11-20 12:28:26 +01:00
Matteo Pagliazzi
e76bdbd62d Fix for #10814, prevent ParallelSave errors (#10852)
* fix(group leave): prevent ParallelSave errors while leaving a group with multiple group or challenge tasks

* fix typo
2018-11-18 20:48:16 +01:00
Matteo Pagliazzi
067e869141 fix(chat): prevent duplicate messages, closes #10823 2018-11-18 19:38:56 +01:00
greenkeeper[bot]
51224a69d9 Update superagent to the latest version 🚀 (#10848)
* fix(package): update superagent to version 4.0.0

* chore(package): update lockfile package-lock.json
2018-11-18 19:24:39 +01:00
Dimini
f56018d46a Very large Guild member counts overflow the badge #10753 (#10812) 2018-11-17 11:06:26 +01:00
Ian Oxley
b846185f8a Set width on .custom-control-label (#10840)
Set `width: 100%` on the `.custom-control-label`.

Although `overflow-wrap: break-word` is set on the parent `.checklist-item` element, it doesn't seem to take effect unless a width is set on the label.
2018-11-17 11:01:36 +01:00
Nathanael Farley
ff81e55839 groupChatReceived webhook fix (#10802)
* Moved sendGroupChatReceivedWebhooks to group.sendChat function.

* Added test for new functionality.
2018-11-17 10:48:41 +01:00
Sabe Jones
9a3cdb5deb 4.71.0 2018-11-15 22:03:32 +00:00
Sabe Jones
47ad7305f5 chore(i18n): update locales 2018-11-15 22:02:46 +00:00
Sabe Jones
d9b5bbe2a9 chore(sprites): compile 2018-11-15 15:58:15 -06:00
Sabe Jones
c2fe04367f feat(content): Oddballs Bundle
Also includes one more tweak to @mention text highlighting
2018-11-15 15:58:07 -06:00
Sabe Jones
abcc77b7d6 fix(chat): more width tweakage 2018-11-14 16:46:03 -06:00
Sabe Jones
07cbf45265 fix(chat): less intrusive highlight and better margins 2018-11-14 16:31:35 -06:00
Sabe Jones
c035435476 4.70.0 2018-11-14 14:39:39 +00:00
Sabe Jones
89fdd8a8bb chore(i18n): update locales 2018-11-14 14:39:23 +00:00
Sabe Jones
d406da4081 chore(news): Bailey 2018-11-14 08:30:34 -06:00
Sabe Jones
d74786ef85 Merge branch 'sabrecat/usernames-master' into develop 2018-11-14 08:21:49 -06:00
Sabe Jones
64a3d08ce3 fix(tests): linting & more expects
Also one more tweak for invite validation responsiveness
2018-11-14 07:43:08 -06:00
SabreCat
f635f178da fix(tests): correct expects 2018-11-14 13:07:44 +00:00
Matteo Pagliazzi
1a7461a8a2 move the update username route to v3 (#10836) 2018-11-14 10:40:27 +01:00
Sabe Jones
cc13c4f28e fix(invites): more responsive validation 2018-11-13 19:50:07 -06:00
SabreCat
239f78674b fix(usernames): address failing tests 2018-11-14 01:44:09 +00:00
Sabe Jones
d691dee2ca fix(usernames): filter @ on server side for username lookup 2018-11-13 15:34:50 -06:00
Sabe Jones
481bd6727d fix(usernames): exclude unverified users during query 2018-11-13 15:13:41 -06:00
Sabe Jones
d0fc1e0751 fix(invites): show errors inline 2018-11-13 15:10:35 -06:00
Sabe Jones
b07dbb7752 Merge branch 'develop' into sabrecat/usernames-master 2018-11-13 14:15:23 -06:00
Sabe Jones
34e7690c38 fix(usernames): various
Disappearing input fields
Text replacement in @mentions
UUID visibility in profiles
Purple dot for @mentioned usernames
TypeError preventing RYA
Group Plan member list error
2018-11-13 14:14:02 -06:00
negue
eca7382545 prevent buying market gear if class doesn't match (#10818)
* prevent buying market gear if class doesn't match

* add test
2018-11-12 21:32:54 +01:00
Matteo Pagliazzi
be95cd967a upgrade gulp-imagemin, closes #10817 2018-11-10 12:29:26 +01:00
Matteo Pagliazzi
ce03f837c7 use lean and .update 2018-11-09 13:10:55 +01:00
Matteo Pagliazzi
808885425f select more fields 2018-11-09 13:04:25 +01:00
Matteo Pagliazzi
39a35f44ef fixes 2018-11-09 13:01:19 +01:00
Matteo Pagliazzi
2b2e1d4b9a rewrite mongoose migration to avoid using recursion 2018-11-09 12:58:39 +01:00
Sabe Jones
869411c0e9 fix(migration): improve model-based script 2018-11-08 16:55:49 -06:00
Sabe Jones
7484ecf729 Merge branch 'develop' into sabrecat/usernames-master 2018-11-08 15:13:28 -06:00
Sabe Jones
3265440bc4 4.69.2 2018-11-08 18:47:54 +00:00
Sabe Jones
acf514e9cb chore(i18n): update locales 2018-11-08 18:47:32 +00:00
Sabe Jones
2789d44dbf chore(news): Bailey 2018-11-08 12:43:50 -06:00
Sabe Jones
b579f31e9e fix(emails): correct unsub link handling 2018-11-08 18:09:51 +00:00
Sabe Jones
5e781017ab fix(script): correct import path and template slug 2018-11-08 10:29:37 -06:00
Sabe Jones
b48f850eac fix(script): don't send to users who have already opted out of notifs 2018-11-08 10:14:54 -06:00
Sabe Jones
5d6b6ed29a feat(usernames): follow-up email and setting for email opt-out 2018-11-08 10:00:25 -06:00
negue
7fbc68511b fix: special are not "AllowedToFeed" (#10808)
*  fix: special are not "AllowedToFeed"

* fix lint
2018-11-08 11:11:04 +01:00
Matteo Pagliazzi
ee2858199b update(http-proxy-middleware): update to 0.19, closes #10650 2018-11-08 11:08:42 +01:00
Sabe Jones
b1dd79f75c fix(invites): bogus validation errors 2018-11-07 15:58:17 -06:00
Sabe Jones
1ac4dd8171 feat(usernames): invite by username 2018-11-07 15:00:22 -06:00
Sabe Jones
4f86abd6b2 Merge branch 'develop' into sabrecat/usernames-master 2018-11-07 08:39:57 -06:00
Sabe Jones
23b0688abb Merge branch 'develop' into sabrecat/usernames-master 2018-11-06 19:56:45 -06:00
Sabe Jones
38efe83cc7 fix(usernames): various
Partial fixage for autocomplete @ing
Don't add username to chat message if user is unverified
Fix flying pets
Fix console error about avatar intro
2018-11-06 19:55:06 -06:00
Sabe Jones
2dadd74097 Merge branch 'release' into develop 2018-11-06 16:53:35 -06:00
Sabe Jones
9f494360ef Merge branch 'release' into develop 2018-11-06 15:02:24 -06:00
Sabe Jones
4122bbdecf 4.69.1 2018-11-06 15:02:03 -06:00
Sabe Jones
256a3abc26 fix(event): de-Festivalize class change modal 2018-11-06 15:01:57 -06:00
Sabe Jones
b7f3c0f389 Merge branch 'release' into develop 2018-11-06 20:43:12 +00:00
Sabe Jones
dca00bf4b7 4.69.0 2018-11-06 20:42:50 +00:00
Sabe Jones
d31c8913d3 chore(i18n): update locales 2018-11-06 20:40:54 +00:00
Sabe Jones
d839d57299 chore(sprites): compile 2018-11-06 14:32:55 -06:00
Sabe Jones
ccc3b4d337 feat(content): Armoire and Backgrounds 2018/11 2018-11-06 14:31:50 -06:00
Matteo Pagliazzi
7195ac15b9 fix(group-plan-tasks): show checkmark when task completed 2018-11-05 15:04:45 +01:00
Sabe Jones
a5ef6a129e fix(auth): new users should start verified 2018-11-03 16:03:53 -05:00
Sabe Jones
38f5d63d29 fix(likes): accessibility tweaks 2018-11-03 13:16:32 -05:00
Sabe Jones
43194b71ce fix(usernames): RYA positioning and contrib tier in PMs 2018-11-03 12:50:59 -05:00
Matteo Pagliazzi
e4a347a3cb update gulp-nodemon 2018-11-03 14:08:46 +01:00
Sabe Jones
7eaf3e04ab fix(modals): button styling 2018-11-02 16:48:35 -05:00
Sabe Jones
b6b03751c4 fix(modals): maybe got it?!? 2018-11-02 16:27:08 -05:00
Sabe Jones
818d5e4eb6 fix(modals): better stack??? 2018-11-02 14:58:32 -05:00
Sabe Jones
f871c7cf63 fix(modal): various 2018-11-02 14:26:39 -05:00
Kirsty
e9eddec0c4 check pet is hatchable before highlighting (#10797) 2018-11-02 17:16:11 +01:00
Nathanael Farley
a48a6a292d If user's cron will happen later today, start the task yesterday. (#10783)
* If user's cron will happen later today, start the task yesterday.

* Added default dayStart to taskDefaults.

* Removed the need to call shouldDo twice to calculate nextDue.

* Revert "Removed the need to call shouldDo twice to calculate nextDue."

This reverts commit e1467f2fc33cfb11e6a4fc667460df6a48b69d45.

* Removed defaults from taskDefault arguments.

* Got user from $store in copyAsTodoModal.vue.

* Fixed tests for taskDefaults to include mock user.

* Fix shouldDo tests when run in GMT timezone.

* Added test to taskDefault; added utcOffset to taskDefault.

* Replaced utcOffset with zone.

* Removed erroneous import.
2018-11-02 16:58:01 +01:00
Matteo Pagliazzi
12aef475c8 update package-lock.json 2018-11-02 12:10:49 +01:00
Sabe Jones
112e4e1d76 Merge branch 'develop' into sabrecat/usernames-master 2018-11-01 21:51:33 -05:00
Sabe Jones
90eebbcd70 Merge branch 'release' into develop 2018-11-02 02:50:25 +00:00
Sabe Jones
1ad9ba4e71 4.68.1 2018-11-02 02:50:05 +00:00
Sabe Jones
c42b72f8a8 chore(i18n): update locales 2018-11-02 02:46:22 +00:00
Sabe Jones
830c8d3104 chore(event): end Habitoween/Fall Fest
Also announce November challenges
2018-11-01 21:44:00 -05:00
Sabe Jones
86ae5f3e44 fix(inbox): don't show likes in inbox
Also remove convo list contrib styling for now
2018-11-01 19:34:57 -05:00
Sabe Jones
3922415314 fix(inbox): more UN display fixes 2018-11-01 17:41:27 -05:00
Sabe Jones
6c71abfac8 fix(inbox): display correct UN for outbound user 2018-11-01 16:05:14 -05:00
Sabe Jones
6ab08a7d52 fix(usernames): don't supply username in public fields if unverified 2018-11-01 15:32:40 -05:00
Sabe Jones
dc46127fc7 refactor(auth): only import needed validator module 2018-11-01 15:22:20 -05:00
Sabe Jones
b54f031acd fix(chat): replace autocomplete at @ 2018-11-01 15:17:01 -05:00
Sabe Jones
eafa2f8cdd Merge branch 'release' into sabrecat/usernames-master 2018-11-01 14:51:28 -05:00
Matteo Pagliazzi
1815d2b6d3 docker: use 10 2018-10-31 17:11:59 +01:00
Matteo Pagliazzi
14cba76ba8 Merge branch 'develop' into greenkeeper/update-to-node-10 2018-10-31 17:09:08 +01:00
greenkeeper[bot]
fe45940d46 fix(package): update ora to version 3.0.0 (#10529) 2018-10-31 16:59:47 +01:00
Matteo Pagliazzi
7dac53867b fix(settings): remove kicked from group from list of push notifications, fixes #10796 2018-10-31 14:42:11 +01:00
Sabe Jones
6f64cb7d9b Merge branch 'release' into develop 2018-10-30 21:26:39 +00:00
Sabe Jones
7d989bcf50 4.68.0 2018-10-30 21:26:19 +00:00
Sabe Jones
7bd29c2dd7 chore(i18n): update locales 2018-10-30 21:24:55 +00:00
Sabe Jones
9e10490102 chore(sprites): compile 2018-10-30 16:21:51 -05:00
Sabe Jones
5792bc0000 feat(content): Habitoween 2018 2018-10-30 16:21:42 -05:00
Matteo Pagliazzi
06812878b5 Merge branch 'negue/stable_img_states' into develop 2018-10-30 16:37:04 +01:00
Matteo Pagliazzi
8714c7d162 fix lint 2018-10-30 16:36:30 +01:00
Matteo Pagliazzi
e66f4e7812 Merge branch 'develop' into negue/stable_img_states 2018-10-30 16:35:30 +01:00
negue
c73f565f65 refactor animal methods / vue methods 2018-10-29 20:46:16 +01:00
Kirsty
82e21df943 fix filter checkboxes in seasonal shop (#10792) 2018-10-29 16:04:42 +01:00
Sabe Jones
18ed148320 Merge branch 'release' into develop 2018-10-28 20:46:13 +00:00
Sabe Jones
a5fc909f0d 4.67.1 2018-10-28 20:45:38 +00:00
Sabe Jones
30a5192e19 chore(i18n): update locales 2018-10-28 20:45:27 +00:00
Matteo Pagliazzi
79c0499672 Mongoose: use $type as the typeKey (#10789)
* use $type as the typeKey in mongoose

* fix and add tests
2018-10-28 15:42:48 -05:00
Matteo Pagliazzi
dadb752087 Mongoose: use $type as the typeKey (#10789)
* use $type as the typeKey in mongoose

* fix and add tests
2018-10-28 15:23:41 +01:00
Matteo Pagliazzi
37b29d3449 fix(tests): increase timeout 2018-10-28 13:14:49 +01:00
Matteo Pagliazzi
bb2ed249b9 fix deprecation warning for sinon.js 2018-10-28 12:44:34 +01:00
Sabe Jones
bb90dde1b6 feat(usernames): verification step during Justin intro for social users 2018-10-27 12:37:22 -05:00
Sabe Jones
5299c8d406 fix(comments): validator for emails, remove prefixes, allow length 1 2018-10-26 16:25:15 -05:00
Sabe Jones
8b81e38538 fix(PR): more cleanup 2018-10-26 15:48:53 -05:00
Sabe Jones
8e05a1b489 Merge branch 'develop' into sabrecat/usernames-master 2018-10-26 15:45:55 -05:00
Sabe Jones
aafcbe60a3 fix(PR): remove unrelated changes 2018-10-26 15:42:41 -05:00
Matteo Pagliazzi
56d1b77215 Upgrade sinon (#10773)
* upgrade sinon

* sinon changes

* fix unit tests
2018-10-26 18:15:28 +02:00
Nathanael Farley
61da558a5d Added explanatory webhook text to UI. (#10782)
* Added explanatory webhook text to UI.

* Made new webhook API info translateable.
2018-10-26 16:46:36 +02:00
Sabe Jones
490531cc76 4.67.0 2018-10-25 21:36:13 +00:00
Sabe Jones
8cd4c502bc chore(i18n): update locales 2018-10-25 21:35:58 +00:00
Sabe Jones
d8cacb653e chore(sprites): compile 2018-10-25 16:31:13 -05:00
Sabe Jones
59436a8bf7 feat(content): Mystery Items Oct 2018 2018-10-25 16:31:00 -05:00
Sabe Jones
6fade19f27 4.66.2 2018-10-25 20:32:06 +00:00
Sabe Jones
15d028a281 chore(i18n): update locales 2018-10-25 20:31:20 +00:00
Sabe Jones
95b283676a Merge branch 'sabrecat/username-display' into sabrecat/usernames-master 2018-10-25 05:32:41 -05:00
Sabe Jones
6e7e81206a fix(profile): better username/UUID styling 2018-10-25 05:29:02 -05:00
Sabe Jones
af74cc7c64 Merge branch 'sabrecat/username-display' into sabrecat/usernames-master 2018-10-24 20:01:00 -05:00
Sabe Jones
a9e2a17077 fix(chat): no min height on autocomplete dropdown 2018-10-24 19:59:54 -05:00
Sabe Jones
92057dbe17 Merge branch 'sabrecat/username-display' into sabrecat/usernames-master 2018-10-24 19:41:21 -05:00
Sabe Jones
b4ab525be5 fix(chat): better @ searching, no chat card borders 2018-10-24 19:39:50 -05:00
Sabe Jones
d3c464d5ea Merge branch 'sabrecat/force-username-modal' into sabrecat/usernames-master 2018-10-24 18:40:43 -05:00
Sabe Jones
804fe1c6d5 fix(usernames): various
z-index modals above Resting banner
force reload after verify username
add missing e-mail validation on frontpage
let Yesterdaily modal float behind username modal
2018-10-24 18:39:54 -05:00
Erdenesukh Tsendjav
3d757c7814 trying something 2018-10-24 16:31:46 -05:00
negue
3c5025a78e fix background empty / resetCallback - refactor pet methods/components 2018-10-24 20:43:15 +02:00
Sabe Jones
e028232527 Merge branch 'release' into develop 2018-10-24 18:35:55 +00:00
Sabe Jones
e3b270a62e 4.66.1 2018-10-24 18:35:34 +00:00
Sabe Jones
fceeacec3b chore(i18n): update locales 2018-10-24 18:35:27 +00:00
Matteo Pagliazzi
f93c67e57c fix(amazon gift): do not reset the gift amount when opening the amazon modal 2018-10-24 20:29:27 +02:00
Sabe Jones
192dc26fbe Merge branch 'release' into develop 2018-10-23 22:50:49 +00:00
Sabe Jones
1c1f270f64 4.66.0 2018-10-23 22:50:25 +00:00
Sabe Jones
483768f4a7 chore(i18n): update locales 2018-10-23 22:49:30 +00:00
SabreCat
65031cef3a chore(sprites): compile 2018-10-23 22:45:47 +00:00
Sabe Jones
2fc1f46359 Veteran Pet ladder award for users affected by username changes (#10765)
* feat(usernames): Veteran Pet ladder award for affected users

* feat(content): Vet Pet Bailey etc.
2018-10-23 17:38:30 -05:00
Matteo Pagliazzi
30fd530576 fix(tests): more timeouts fixes 2018-10-23 20:23:52 +02:00
Matteo Pagliazzi
f79999fde7 minor deps updates 2018-10-23 16:48:59 +02:00
Tressley Cahill
90d6e443ba Corrects the white bar above the header and updates the text styling (#10772) 2018-10-23 13:50:02 +02:00
Derek Kim
4ed1082558 fix for #10496: Newly subscribed accounts receive erroneous notification that they have Mystery Items (#10759)
* fix
Newly subscribed accounts receive erroneous notification that they have Mystery Items

* Added unit test for #10496

* Restored a previous unit test
2018-10-23 13:47:15 +02:00
Matteo Pagliazzi
00717eda76 fix(tests): increase timeout 2018-10-23 13:30:38 +02:00
Matteo Pagliazzi
d1b86e6c14 Remove code for Pusher (#10774)
* remove pusher

* fix linting
2018-10-23 13:25:52 +02:00
Matteo Pagliazzi
c813afba44 Upgrade mongoose (#10767)
* fix mongoose and upgrade

* fix more validations
2018-10-23 13:25:14 +02:00
Matteo Pagliazzi
d49db6d367 upgrade csv-stringify (#10776) 2018-10-23 13:22:22 +02:00
Matteo Pagliazzi
d6835aec56 upgrade method override (#10775) 2018-10-23 11:48:15 +02:00
Matteo Pagliazzi
960f7b5886 upgrade node-gcm (#10777) 2018-10-23 11:47:26 +02:00
Sabe Jones
cd9630332d Merge branch 'sabrecat/username-display' into sabrecat/usernames-master 2018-10-22 16:30:12 -05:00
Sabe Jones
ed21a37e5a feat(usernames): show in party header, profiles, inbox convo list 2018-10-22 16:29:47 -05:00
negue
16256ee190 fix issues 2018-10-22 21:22:26 +02:00
Matteo Pagliazzi
ff57e31f4f 4.65.7 2018-10-20 16:17:26 +02:00
Matteo Pagliazzi
6e21d154ae Do not throw an error when adding the same push device twice (#10770)
* do not throw an error when adding the same push device twice

* fix spelling

* fix linting
2018-10-20 14:52:02 +02:00
Sabe Jones
fdecc8ce16 Merge branch 'sabrecat/username-display' into sabrecat/usernames-master 2018-10-19 17:44:52 -05:00
Sabe Jones
3cc49f6637 fix(chat): match hyphen in @name regex 2018-10-19 17:44:18 -05:00
Sabe Jones
47f49f4256 Merge branch 'sabrecat/username-display' into sabrecat/usernames-master 2018-10-19 16:22:11 -05:00
Sabe Jones
4f4bb52360 fix(chat): padding adjustment and tooltip placement 2018-10-19 16:20:13 -05:00
Sabe Jones
3748b3046b Merge branch 'sabrecat/force-username-modal' into sabrecat/usernames-master 2018-10-19 16:04:23 -05:00
Sabe Jones
5cd0f56811 fix(usernames): let verify modal grow to content 2018-10-19 16:03:48 -05:00
Tressley Cahill
fe5beac91b Custom reward action background and font-color updates (#10769)
* fixes custom reward action background and font-color

* update to -10
2018-10-19 10:01:02 -05:00
Sabe Jones
52fd6a1451 4.65.6 2018-10-18 23:09:12 +00:00
Sabe Jones
ae445555e9 fix(groups): don't show add manager for non-groups and non-leaders 2018-10-18 18:08:30 -05:00
Sabe Jones
c4fc6671b4 4.65.5 2018-10-18 20:05:29 +00:00
Sabe Jones
e7a096158e chore(i18n): update locales 2018-10-18 20:05:15 +00:00
Sabe Jones
98473fcfaa chore(news): Bailey 2018-10-18 15:00:21 -05:00
Sabe Jones
e4300fc714 fix(registration): localize reg form placeholders 2018-10-18 14:48:46 -05:00
negue
456c5e57bc refactor petItem - pet image states 2018-10-18 19:58:14 +02:00
Matteo Pagliazzi
ffba435923 fix #10756: do not show push notification settings for email only notifications 2018-10-18 14:21:35 +02:00
Matteo Pagliazzi
1f44444a50 Fix subcriptions remaining time disappearing after cancelling (#10761)
* add hasCancelled method for group/user, prevent cancelling a subscription twice

* wip

* paypal: do not cancel a subscription twice

* make sure hasCancelled and hasNotCancelled return a boolean result
2018-10-18 12:14:07 +02:00
Sabe Jones
185b20995a Merge branch 'sabrecat/username-display' into sabrecat/usernames-master 2018-10-17 18:31:16 -05:00
Sabe Jones
fdf2e590ea fix(chat): better likes and display name font sizing 2018-10-17 18:28:58 -05:00
Sabe Jones
994123c387 chore(sprites): compile 2018-10-17 15:32:20 -05:00
Sabe Jones
273590716c Merge branch 'sabrecat/veteran-verified' into sabrecat/usernames-master 2018-10-17 15:29:44 -05:00
Sabe Jones
6818a094ee Merge branch 'sabrecat/username-display' into sabrecat/usernames-master 2018-10-17 15:29:35 -05:00
Sabe Jones
c99855cef4 Merge branch 'sabrecat/force-username-modal' into sabrecat/usernames-master 2018-10-17 15:29:23 -05:00
Sabe Jones
6845943ed0 feat(usernames): vet pet announcement 2018-10-17 15:27:54 -05:00
Sabe Jones
044fe17757 feat(usernames): Veteran Pet ladder award for affected users 2018-10-17 14:43:33 -05:00
Sabe Jones
ad0ede8d01 fix(model): don't break if auth.local undefined 2018-10-17 13:59:39 -05:00
Sabe Jones
23815e89e1 WIP(usernames): display in chat areas 2018-10-17 12:57:57 -05:00
Sabe Jones
061d990e39 Merge branch 'release' into develop 2018-10-15 20:20:55 +00:00
Sabe Jones
71f4e6bc08 4.65.4 2018-10-15 20:20:33 +00:00
Sabe Jones
659f160e22 chore(i18n): update locales 2018-10-15 20:20:25 +00:00
Trevor Ford
5f27bc5f90 Issue 10728: sort equipment by stat descending on Market page (#10734)
* sort equipment by stat descending in Market (issue #10728)

* fix sorting equipment by PER in Market (new issue?)

* move filter logic into method when sorting equipment in Market

* consolidate sorting in sortedGearItems() into one _orderBy call
2018-10-15 21:12:53 +02:00
negue
074837b274 check every armoire gear 'canOwn' method (#10760) 2018-10-15 20:11:28 +02:00
Sabe Jones
cfd19ac694 fix(usernames): various
Better modal positioning
Correct text colors for input field
Don't show "already taken" if username has other errors
2018-10-15 11:27:49 -05:00
SabreCat
0897ab5dc9 fix(settings): don't center align display name issues 2018-10-14 23:25:03 +00:00
Sabe Jones
5c6e8a7331 fix(usernames): add display name verification to site settings
also corrrect some validation logic in force-verify modal
2018-10-14 23:06:41 +00:00
Sabe Jones
c576c5261e fix(username): Add @ prepend 2018-10-14 12:21:52 -05:00
Sabe Jones
bbd98517ff fix(test): copypasta and string trouble 2018-10-13 21:36:14 -05:00
Sabe Jones
392b54aa7b fix(usernames): remove more ratzen fratzen dupe strings 2018-10-13 21:05:07 -05:00
Sabe Jones
60b26d4ec0 fix(test): string is miscapitalized :( 2018-10-13 20:56:09 -05:00
Matteo Pagliazzi
aa517e0ad6 Merge branch 'negue/modal-notifications' into develop 2018-10-13 21:18:30 +02:00
Matteo Pagliazzi
5ca489dee7 Merge branch 'develop' into negue/modal-notifications 2018-10-13 21:18:16 +02:00
Rene Cordier
fe39ef72ff Show accurate experience notifications (#10676)
* Show accurate experience notifications

Add unit tests for exp notifications

* use array to compute exp and lvl values for notification changes

* Add tests for user loosing xp cases
2018-10-13 20:24:23 +02:00
Carl Vuorinen
eee5f2f1df No matching Guilds/Challenges message (#10744)
* Display message on My Guilds page when filters dont' match anything

* Display message on Discover Guilds page when filters dont' match anything

* Display message on My Challenges page when filters dont' match anything

* Display message on Discover Challenges page when filters dont' match anything

* Don't show Load More button when there is nothing to load

* Fix Guild search

Previously was not possible to clear after searching
2018-10-13 20:19:03 +02:00
Sabe Jones
fd8572c28a Group Management Menu Fixes (#10704)
* fix(groups): more intelligent member actions

* fix(groups): further member action improvements

* fix(groups): don't show "Remove Manager" if user doesn't have authority

* fix(lint): bad if syntax

* fix(groups): unnecessary if on icon
2018-10-13 20:15:46 +02:00
Kirsty
f161987e1e check officialPinnedItems for gala gear in market (#10745) 2018-10-13 20:07:30 +02:00
aszlig
2304d970a5 api: Fix a few API documentation typos (#10749)
Just fixes a few syntactic errors and typos.

Signed-off-by: aszlig <aszlig@nix.build>
2018-10-13 20:03:40 +02:00
Sabe Jones
25ed05ab0a Analytics: clean up old A/B test code & add username verify flag (#10754)
* chore(analytics): clean up old A/B test code & add username verify

* fix(lint): more AB cleanup
2018-10-13 13:03:20 -05:00
Sabe Jones
fa1fef11d6 feat(usernames): modal to force verification 2018-10-13 12:53:16 -05:00
Sabe Jones
6f5b9ef119 fix(scripts): better error handling for script runner and GDPR 2018-10-12 15:27:31 +00:00
Sabe Jones
c64ea0a9a9 4.65.3 2018-10-11 21:05:46 +00:00
Sabe Jones
2e36b896d4 chore(i18n): update locales 2018-10-11 21:04:30 +00:00
Sabe Jones
6fe73d431e Merge branch 'develop' into release 2018-10-11 16:01:56 -05:00
Sabe Jones
998621cefe feat(content): fall avatar customization 2018-10-11 16:01:32 -05:00
Matteo Pagliazzi
67bb179c25 gifts: prevent users from sending the same gift twice by clicking many times on the Send button 2018-10-11 19:01:15 +02:00
Sabe Jones
c875861dab Merge branch 'release' into develop 2018-10-10 16:26:28 +00:00
Sabe Jones
418c18ddb2 4.65.2 2018-10-09 23:40:09 -05:00
Sabe Jones
0caab5c8d0 fix(news): missing footnote 2018-10-09 23:39:48 -05:00
Sabe Jones
218e65b04b Merge branch 'release' into develop 2018-10-09 21:33:49 -05:00
Sabe Jones
fcd7ba77a7 4.65.1 2018-10-09 21:32:58 -05:00
Sabe Jones
b0d177643c chore(sprites): compile 2018-10-09 21:32:38 -05:00
Sabe Jones
c0e0b10a95 Merge branch 'release' into develop 2018-10-10 01:20:18 +00:00
Sabe Jones
0bee2caf2e 4.65.0 2018-10-10 01:19:52 +00:00
Sabe Jones
e56d097b3a chore(i18n): update locales 2018-10-10 01:16:28 +00:00
Sabe Jones
8c63a9e31f feat(content): Alligator Pets and Spoopy Sporples 2018-10-09 20:12:12 -05:00
Sabe Jones
28ed9d8bcc fix(script): log, not warn, so all output goes to both stdout and tee 2018-10-09 20:27:52 +00:00
Matteo Pagliazzi
36ead77e0c Fixes group plan verify username (#10747)
Misc fixes
2018-10-09 20:07:50 +02:00
Robert Kojima
e7969987ec Guild textarea at list positioning (#10663)
* autocomplete dialog now has ternary operator to determine placement

* added min height to textbox

* fixed spacing according to travisCI

* heightToUse function now retrieves argument from props
2018-10-08 22:25:49 +02:00
titchimoto
97021e3422 Issue 10414 - Remove Member Option from View Party link. (#10639)
* Add remove member option from main task page

* Code refactor for remove member options

* code refactor to avoid loading party multiple times

* fix dispatch to ensure only pulling once from server
2018-10-08 22:22:09 +02:00
Carl Vuorinen
218d47d64a Don't show "no guilds" texts while loading (#10665)
* Don't show "no guilds" texts while loading

Unified styling of "no guilds" message with my challenges page
Fixes #10662

* Don't show "no challenges" texts while loading

Add loading indicator (similar to find challenges & my guilds pages)

* Change gray color

* Set challenge icon color
2018-10-08 22:18:11 +02:00
Rene Cordier
bdfc23717e Css font home page update (#10672)
* css font home page update

finish home page font change

* Small fixes on css font update on home page
2018-10-08 22:16:14 +02:00
Kirsty
464cd87736 Decrease mana when removing stat points from int (#10713)
* Decrease mana when removing stat points from int

* Revert "Decrease mana when removing stat points from int"

This reverts commit 5e25e13552.

* add mana when stat updates are saved

* don't allow users to deallocate saved stat points in the ui

* use flag to determine whether to add mana points

* add test for not adding mana points when flag is set

* Revert "add test for not adding mana points when flag is set"

This reverts commit 6e8ff36a79.

* Revert "use flag to determine whether to add mana points"

This reverts commit 274e2d0d33.

* Revert "add mana when stat updates are saved"

This reverts commit 422bd49191.

* move client side stat allocation to when save is pressed

* update displayed total stats during editing

* Fix lint errors
2018-10-08 22:11:26 +02:00
Kirsty
67a8eebb96 add errors for any param validation failures to the snackbar (#10724) 2018-10-08 21:54:05 +02:00
Kirsty
cfc0f6a3ac remove items with that have been lost from Class:None (#10735) 2018-10-08 21:38:16 +02:00
Matteo Pagliazzi
9f76db12bd update aws-sdk, in-app-purchase, fix #10733 and #10725 2018-10-08 10:55:41 +02:00
Sabe Jones
70192e4935 Scripts October 2018 (#10741)
* chore(scripts): BTS Challenge archive and username email jobbing

* refactor(migration): use batching and sendTxn

* fix(script): introduce delay for batching

* fix(migration): correct import, fix delay promise, slower batching

* fix(migration): add daterange

* WIP(script): deletion helper for GDPR

* fix(script): address code comments

* refactor(script): use for loop

* fix(script-runner): bad catch syntax

* fix(script-runner): oops I did it again

* fix(lint): name functions
2018-10-07 14:20:30 -05:00
Sabe Jones
5cd4ead9d1 Merge branch 'release' into develop 2018-10-06 14:36:10 +00:00
Sabe Jones
87cd000bb8 4.64.2 2018-10-06 14:35:48 +00:00
Sabe Jones
0de5d8273b chore(i18n): update locales 2018-10-06 14:35:39 +00:00
Sabe Jones
379898cc4d fix(sprites): add new spritesheet to app manifest 2018-10-06 09:33:35 -05:00
Sabe Jones
adeaa6c754 Merge branch 'release' into develop 2018-10-05 19:55:45 +00:00
Sabe Jones
539f0e33e2 4.64.1 2018-10-05 19:55:23 +00:00
Sabe Jones
405e053377 chore(i18n): update locales 2018-10-05 19:54:21 +00:00
Phillip Thelen
52fbb8f899 Verify username as valid if user is re-checking their current name (#10737)
* Verify username as valid if user is re-checking their current name

* Fix lint error and existingUser check.
2018-10-05 14:51:21 -05:00
Matteo Pagliazzi
c880596a77 Cleanup after inbox migration (#10487) 2018-10-05 19:34:42 +02:00
Matteo Pagliazzi
a35f04be46 migrations: move inbox migration to archive 2018-10-05 19:34:21 +02:00
Sabe Jones
8682cf1cf7 4.64.0 2018-10-04 23:16:50 +00:00
Sabe Jones
6e922cfb44 chore(i18n): update locales 2018-10-04 23:15:19 +00:00
Sabe Jones
cafabd93e1 fix(passport): use graph API v2.8 2018-10-04 18:09:58 -05:00
Sabe Jones
1001d48eb7 chore(sprites): compile 2018-10-04 18:09:01 -05:00
Sabe Jones
b5c4618d56 feat(content): Armoire and BGs 2018/10 2018-10-04 18:08:49 -05:00
Sabe Jones
92a4ba93d2 4.63.3 2018-10-03 20:57:22 +00:00
Sabe Jones
90d35d2f1f fix(auth): Don't try to check existing username on new reg 2018-10-03 15:56:09 -05:00
Sabe Jones
fead027cd2 4.63.2 2018-10-03 19:54:53 +00:00
Sabe Jones
5578426985 chore(i18n): update locales 2018-10-03 19:49:45 +00:00
Sabe Jones
1c39fae127 fix(auth): alert on successful addLocal 2018-10-03 14:30:35 -05:00
Sabe Jones
45a757b589 fix(auth): account for new username paradigm in add-local flow 2018-10-03 14:01:45 -05:00
Sabe Jones
8b610d771c fix(usernames): various
Reword invalid characters error
Correct typo in slur error
Remove extraneous Confirm button
Reset username field if empty on blur
Restore ability to add local auth to social login
2018-10-03 13:13:47 -05:00
Sabe Jones
bd81d27145 4.63.1 2018-10-03 02:33:32 +00:00
Sabe Jones
8eb430cbcb chore(i18n): update locales 2018-10-03 02:33:09 +00:00
Sabe Jones
f218133d25 chore(news): Bailey 2018-10-02 21:31:16 -05:00
Sabe Jones
5f440d9097 4.63.0 2018-10-02 23:12:11 +00:00
Sabe Jones
0294868747 chore(i18n): update locales 2018-10-02 23:11:49 +00:00
Sabe Jones
9c8d870d16 Merge branch 'develop' into release 2018-10-02 23:07:08 +00:00
Sabe Jones
a7acd863f3 fix(lint): comma spacing 2018-10-02 16:59:39 -05:00
Sabe Jones
f32ef0a6ba fix(lint): comma 2018-10-02 16:38:47 -05:00
Phillip Thelen
ebf3b4aa47 Username announcement (#10729)
* Change update username API call

The call no longer requires a password and also validates the username.

* Implement API call to verify username without setting it

* Improve coding style

* Apply username verification to registration

* Update error messages

* Validate display names.

* Fix API early Stat Point allocation (#10680)

* Refactor hasClass check to common so it can be used in shared & server-side code

* Check that user has selected class before allocating stat points

* chore(event): end Ember Hatching Potions

* chore(analytics): reenable navigation tracking

* update bcrypt

* Point achievement modal links to main site (#10709)

* Animal ears after death (#10691)

* Animal Ears purchasable with Gold if lost in Death

* remove ears from pinned items when set is bought

* standardise css and error handling for gems and coins

* revert accidental new line

* fix client tests

* Reduce margin-bottom of checklist-item from 10px to -3px. (#10684)

* chore(i18n): update locales

* 4.61.1

* feat(content): Subscriber Items and Magic Potions

* chore(sprites): compile

* chore(i18n): update locales

* 4.62.0

* Display notification for users to confirm their username

* fix typo

* WIP(usernames): Changes to address #10694

* WIP(usernames): Further changes for #10694

* fix(usernames): don't show spurious headings

* Change verify username notification to new version

* Improve feedback for invalid usernames

* Allow user to set their username again to confirm it

* Improve validation display for usernames

* Temporarily move display name validation outside of schema

* Improve rendering banner about sleeping in the inn

See #10695

* Display settings in one column

* Position inn banner when window is resized

* Update inn banner handling

* Fix banner offset on initial load

* Fix minor issues.

* Issue: 10660 - Fixed. Changed default to Please Enter A Value (#10718)

* Issue: 10660 - Fixed. Changed default to Please Enter A Value

* Issue: 10660 - Fixed/revision 2 Changed default to Enter A Value

* chore(news): Bailey announcements

* chore(i18n): update locales

* 4.62.1

* adjust wiki link for usernameInfo string

https://github.com/HabitRPG/habitica-private/issues/7#issuecomment-425405425

* raise coverage for tasks api calls (#10029)

* - updates a group task - approval is required
- updates a group task with checklist

* add expect to test the new checklist length

* - moves tasks to a specified position out of length

* remove unused line

* website getter tasks tests

* re-add sanitizeUserChallengeTask

* change config.json.example variable to be a string not a boolean

* fix tests - pick the text / up/down props too

* fix test - remove changes on text/up/down - revert sanitize condition - revert sanitization props

* Change update username API call

The call no longer requires a password and also validates the username.

* feat(content): Subscriber Items and Magic Potions

* Re-add register call

* Fix merge issue

* Fix issue with setting username

* Implement new alert style

* Display username confirmation status in settings

* Add disclaimer to change username field

* validate username in settings

* Allow specific fields to be focused when opening site settings

* Implement requested changes.

* Fix merge issue

* Fix failing tests

* verify username when users register with username and password

* Set ID for change username notification

* Disable submit button if username is invalid

* Improve username confirmation handling

* refactor(settings): address remaining code comments on auth form

* Revert "refactor(settings): address remaining code comments on auth form"

This reverts commit 9b6609ad64.

* Social user username (#10620)

* Refactored private functions to library

* Refactored social login code

* Added username to social registration

* Changed id library

* Added new local auth check

* Fixed export error. Fixed password check error

* fix(settings): password not available on client

* refactor(settings): more sensible placement of methods

* chore(migration): script to hand out procgen usernames

* fix(migration): don't give EVERYONE new names you doofus

* fix(migration): limit data retrieved, be extra careful about updates

* fix(migration): use missing field, not migration tag, for query

* fix(migration): unused var

* fix(usernames): only generate 20 characters

* fix(migration): set lowerCaseUsername
2018-10-02 16:17:06 -05:00
Matteo Pagliazzi
5a8366468b inbox: fix avatar display and order 2018-10-02 22:30:07 +02:00
Sabe Jones
df57518815 4.62.3 2018-10-02 14:24:31 +00:00
Sabe Jones
7d342b5115 chore(i18n): update locales 2018-10-02 14:24:17 +00:00
Sabe Jones
388de9a97d chore(news): Bailey 2018-10-02 09:19:55 -05:00
Sabe Jones
28c79d9d20 4.62.2 2018-10-01 19:19:24 +00:00
Sabe Jones
85cf322b30 chore(i18n): update locales 2018-10-01 19:18:33 +00:00
negue
362ca73c94 raise coverage for tasks api calls (#10029)
* - updates a group task - approval is required
- updates a group task with checklist

* add expect to test the new checklist length

* - moves tasks to a specified position out of length

* remove unused line

* website getter tasks tests

* re-add sanitizeUserChallengeTask

* change config.json.example variable to be a string not a boolean

* fix tests - pick the text / up/down props too

* fix test - remove changes on text/up/down - revert sanitize condition - revert sanitization props
2018-10-01 13:29:14 +02:00
negue
5632031f16 reload page if the user closes the modal or not clicking on the notification 2018-09-30 17:22:44 +02:00
Alys
90273362c4 adjust wiki link for usernameInfo string
https://github.com/HabitRPG/habitica-private/issues/7#issuecomment-425405425
2018-09-29 15:56:17 +10:00
Sabe Jones
7aadc10fab Merge branch 'release' into develop 2018-09-28 15:47:45 -05:00
Sabe Jones
bfd45596b5 4.62.1 2018-09-27 19:14:07 +00:00
Sabe Jones
2eed4d38ae chore(i18n): update locales 2018-09-27 19:13:07 +00:00
Sabe Jones
29bbe8534b chore(news): Bailey announcements 2018-09-27 14:03:16 -05:00
beatscribe
9e008890b2 Issue: 10660 - Fixed. Changed default to Please Enter A Value (#10718)
* Issue: 10660 - Fixed. Changed default to Please Enter A Value

* Issue: 10660 - Fixed/revision 2 Changed default to Enter A Value
2018-09-27 12:26:31 +02:00
Phillip Thelen
5505bf1e45 Merge pull request #10700 from phillipthelen/mobile-fixes
Fix website issues on mobile devices
2018-09-27 11:21:53 +02:00
Phillip Thelen
d40781ce07 Fix minor issues. 2018-09-27 10:34:56 +02:00
Phillip Thelen
d9719cdc05 Fix banner offset on initial load 2018-09-26 18:10:24 +02:00
Phillip Thelen
8cc6a96be0 Update inn banner handling 2018-09-26 15:59:57 +02:00
Sabe Jones
c5fb2d6506 Merge branch 'release' into develop 2018-09-25 21:54:32 +00:00
Sabe Jones
afc336461e 4.62.0 2018-09-25 21:54:08 +00:00
Sabe Jones
31376c8461 chore(i18n): update locales 2018-09-25 21:36:14 +00:00
Sabe Jones
3a849bac18 chore(sprites): compile 2018-09-25 16:30:59 -05:00
Sabe Jones
563f3e2012 feat(content): Subscriber Items and Magic Potions 2018-09-25 16:30:50 -05:00
Phillip Thelen
e24a024091 Position inn banner when window is resized 2018-09-25 15:29:55 +02:00
Sabe Jones
dc7d3816fd Merge branch 'release' into develop 2018-09-24 20:33:23 +00:00
Sabe Jones
a094e13352 4.61.1 2018-09-24 20:30:37 +00:00
Sabe Jones
83376a38de chore(i18n): update locales 2018-09-24 20:28:09 +00:00
lucubro
db9c13a05d Reduce margin-bottom of checklist-item from 10px to -3px. (#10684) 2018-09-24 17:46:15 +02:00
Matteo Pagliazzi
8c8aa78a1a Merge branch 'develop' of github.com:HabitRPG/habitica into develop 2018-09-24 17:45:45 +02:00
Matteo Pagliazzi
6e3f7c005a fix client tests 2018-09-24 17:42:50 +02:00
Kirsty
1395380dfe Animal ears after death (#10691)
* Animal Ears purchasable with Gold if lost in Death

* remove ears from pinned items when set is bought

* standardise css and error handling for gems and coins

* revert accidental new line
2018-09-24 17:36:26 +02:00
J.D. Sandifer
833ceb3bf3 Point achievement modal links to main site (#10709) 2018-09-24 17:33:47 +02:00
Matteo Pagliazzi
0522aa1551 update bcrypt 2018-09-24 17:11:34 +02:00
Sabe Jones
58a9e4a439 chore(analytics): reenable navigation tracking 2018-09-21 16:24:18 -05:00
Sabe Jones
84e2b2f45e chore(event): end Ember Hatching Potions 2018-09-21 16:24:07 -05:00
Carl Vuorinen
71c0939a15 Fix API early Stat Point allocation (#10680)
* Refactor hasClass check to common so it can be used in shared & server-side code

* Check that user has selected class before allocating stat points
2018-09-21 16:55:55 +02:00
Matteo Pagliazzi
26c8323e70 Move inbox to its own model (#10428)
* shared model for chat and inbox

* disable inbox schema

* inbox: use separate model

* remove old code that used group.chat

* add back chat field (not used) and remove old tests

* remove inbox exclusions when loading user

* add GET /api/v3/inbox/messages

* add comment

* implement DELETE /inbox/messages/:messageid in v4

* implement GET /inbox/messages in v4 and update tests

* implement DELETE /api/v4/inbox/clear

* fix url

* fix doc

* update /export/inbox.html

* update other data exports

* add back messages in user schema

* add user.toJSONWithInbox

* add compativility until migration is done

* more compatibility

* fix tojson called twice

* add compatibility methods

* fix common tests

* fix v4 integration tests

* v3 get user -> with inbox

* start to fix tests

* fix v3 integration tests

* wip

* wip, client use new route

* update tests for members/send-private-message

* tests for get user in v4

* add tests for DELETE /inbox/messages/:messageId

* add tests for DELETE /inbox/clear in v4

* update docs

* fix tests

* initial migration

* fix migration

* fix migration

* migration fixes

* migrate api.enterCouponCode

* migrate api.castSpell

* migrate reset, reroll, rebirth

* add routes to v4 version

* fix tests

* fixes

* api.updateUser

* remove .only

* get user -> userLib

* refactor inbox.vue to work with new data model

* fix return message when messaging yourself

* wip fix bug with new conversation

* wip

* fix remaining ui issues

* move api.registerLocal, fixes

* keep only v3 version of GET /inbox/messages
2018-09-21 15:12:20 +02:00
Sabe Jones
bb7d447003 Merge branch 'release' into develop 2018-09-20 21:24:30 +00:00
Sabe Jones
97ea510a34 4.61.0 2018-09-20 21:24:05 +00:00
Sabe Jones
99610b4916 chore(i18n): update locales 2018-09-20 21:23:48 +00:00
Sabe Jones
9a43b85492 chore(sprites): compile 2018-09-20 16:19:42 -05:00
Sabe Jones
ecbf39cee4 feat(event): Fall Festival 2018 2018-09-20 16:19:29 -05:00
Matteo Pagliazzi
4394772ee3 Revert "Small Updates (#10701)" (#10702)
This reverts commit dd7fa73961.
2018-09-20 22:36:46 +02:00
Matteo Pagliazzi
dd7fa73961 Small Updates (#10701)
* small updates

* fix client unit test

* fix uuid validation
2018-09-20 15:01:12 +02:00
Phillip Thelen
6ec23ce790 Display settings in one column 2018-09-19 18:42:35 +02:00
Phillip Thelen
b953519e2d Improve rendering banner about sleeping in the inn
See #10695
2018-09-19 16:38:40 +02:00
Sabe Jones
33a8072d23 Merge branch 'release' into develop 2018-09-18 23:11:09 +00:00
Sabe Jones
213316d807 4.60.5 2018-09-18 23:10:46 +00:00
Sabe Jones
44cd4d0708 chore(i18n): update locales 2018-09-18 23:10:30 +00:00
Sabe Jones
063b7a9af0 chore(news): Bailey 2018-09-18 18:08:27 -05:00
negue
c08b5a4f1e add pinUtils-mixin - fixes #10682 (#10683) 2018-09-16 12:54:05 +02:00
Alys
90117625d7 add swear words - TRIGGER / CONTENT WARNING: assault, slurs, swearwords, etc 2018-09-15 14:45:39 +10:00
Sabe Jones
1b3dad749e 4.60.4 2018-09-13 22:21:10 +00:00
Sabe Jones
a622a3ebe3 chore(i18n): update locales 2018-09-13 22:21:02 +00:00
Sabe Jones
2c83c16644 fix(bcrypt): install fork compatible with Node 8 2018-09-12 12:23:20 -05:00
Sabe Jones
1034675184 Merge branch 'release' into develop 2018-09-11 20:44:08 +00:00
Sabe Jones
a420876697 4.60.3 2018-09-11 20:43:40 +00:00
Sabe Jones
dc265e26b3 chore(i18n): update locales 2018-09-11 20:43:05 +00:00
Sabe Jones
b5203dda61 chore(sprites): compile 2018-09-11 15:38:52 -05:00
Sabe Jones
80e92a8767 feat(content): Forest Friends Quest Bundle 2018-09-11 15:38:12 -05:00
Keith Holliday
1c51e62e43 Merged in develop 2018-09-10 09:42:51 -05:00
Matteo Pagliazzi
a265bfac9d fix typo when importing component 2018-09-09 14:46:47 +02:00
negue
92e4d5cd68 Refactor/market vue (#10601)
* extract inventoryDrawer from market

* show scrollbar only if needed

* extract featuredItemsHeader / pinUtils

* extract pageLayout

* extract layoutSection / filterDropdown - fix sortByNumber

* rollback sortByNumber order-fix

* move equipment lists out of the layout-section (for now)

* refactor sellModal

* extract checkbox

* extract equipment section

* extract category row

* revert scroll - remove sellModal item template

* fix(lint): commas and semis

* Created category item component (#10613)

* extract filter sidebar

* fix gemCount - fix raising the item count if the item wasn't previously owned

* fixes #10659

* remove unneeded method
2018-09-09 12:05:33 +02:00
lucubro
a18e9b3b18 Fix initial position item info when selecting one item after another (fixes #10077) (#10661)
* Update lastMouseMoveEvent even when dragging an egg or potion.

* Update lastMouseMoveEvent even when dragging a food item.
2018-09-09 11:59:50 +02:00
Forrest Hatfield
c1a6ba6242 Saved sort selection into local storage for later use - fixes #10432 (#10655)
* Saved sort selection into local storage for later use

* Updated code to use userLocalManager module
2018-09-09 11:58:02 +02:00
Rene Cordier
ed761a8b7b Fix new party member cannot join pending quest (#10648) 2018-09-09 11:56:51 +02:00
Carl Vuorinen
81d5971829 Correct Challenges tooltip in Guild view (#10667) 2018-09-09 11:55:30 +02:00
Alys
eb2d320d1f allow challenge leader/owner to view/join/modify challenge in private group they've left - fixes #9753 (#10606)
* rename hasAccess to canJoin for challenges

This is so the function won't be used accidentally for other
purposes, since hasAccess could be misinterpretted.

* add isLeader function for challenges

* allow challenge leader to join/modify/end challenge when they're not in the private group it's in

* delete duplicate test

* clarify title of existing tests

* add tests and adjust existing tests to reduce privileges of test users

* fix lint errors

* remove pointless isLeader check (it's checked in canJoin)
2018-09-09 11:53:59 +02:00
Matteo Pagliazzi
67538a368e Merge branch 'TheHollidayInn-mana-lvl-10' into develop 2018-09-09 11:52:45 +02:00
Matteo Pagliazzi
d55b95834d remove .only 2018-09-09 11:52:37 +02:00
Matteo Pagliazzi
9ff9cd3b35 Merge branch 'mana-lvl-10' of https://github.com/TheHollidayInn/habitrpg into TheHollidayInn-mana-lvl-10 2018-09-09 11:50:31 +02:00
Alys
b1f24de3c4 remove tests that are no longer needed because we won't be purging private messages (#10670)
Ref: this comment from paglias: https://github.com/HabitRPG/habitica/issues/7940#issuecomment-406489506
2018-09-09 11:24:52 +02:00
Sabe Jones
2ce2100f89 4.60.2 2018-09-06 19:39:46 +00:00
Sabe Jones
dbaae4183e chore(i18n): update locales 2018-09-06 19:30:35 +00:00
Keith Holliday
2009bb97cb Fixed class check 2018-09-05 14:10:59 -05:00
Sabe Jones
3fc9501bac Merge branch 'release' into develop 2018-09-04 17:29:40 -05:00
Sabe Jones
2c2ded2b70 4.60.1 2018-09-04 17:29:18 -05:00
Sabe Jones
d689010e38 fix(news): correct Bailey URL 2018-09-04 17:28:52 -05:00
Sabe Jones
e173b7784c 4.60.0 2018-09-04 21:30:00 +00:00
Sabe Jones
c3db59aae8 chore(i18n): update locales 2018-09-04 21:29:10 +00:00
Sabe Jones
44e063c035 chore(sprites): compile 2018-09-04 16:24:12 -05:00
Sabe Jones
4e2c08cfed feat(content): Armoire and Backgrounds 201809 2018-09-04 16:24:04 -05:00
negue
c845c337df unsubscribe events for a specific method (#10652) 2018-09-01 19:27:32 +02:00
Robert Kojima
418b57f9fb Press kit pointer cursor (#10640)
* cursor while hovering over press-kit faq now a pointer instead of text

* deleted extraneous spaces
2018-09-01 19:25:55 +02:00
Keith Holliday
9725da258e Added getter use 2018-09-01 09:37:47 -05:00
Keith Holliday
4191ea1968 Fixed invite group listener (#10630) 2018-09-01 09:30:44 -05:00
Sabe Jones
a9340ee60f 4.59.2 2018-08-31 16:02:35 -05:00
Sabe Jones
c8d874d28a Revert "Show accurate XP gain in notification on level up (#10590)"
This reverts commit 1f7dd421d4.
2018-08-31 16:02:14 -05:00
Sabe Jones
32a22f1545 Revert "Check user version before adding notifications (#10628)"
This reverts commit 0002148326.
2018-08-31 16:00:31 -05:00
Jacob Frericks
8ffe302a49 Update member API doc (fixes #[8087]) (#10610)
* Update member API doc

* Adding Body/Path/Query parameters to api doc
2018-08-30 14:55:04 -05:00
legitmaxwu
5c50a40f39 Added Contributor Titles to Names on Hover (fixes #10611) (#10624)
* Added Contributor Titles to Names on Hover

* Added Contributor Titles to Names on Hover

* added contributor title text on hover

* added contributor titles on hover in chat

* added contributor titles to text on hover

* Delete .project
2018-08-30 14:52:37 -05:00
Matteo Pagliazzi
84329e5fad New inbox client (#10644)
* new inbox client

* add tests for sendPrivateMessage returning the message

* update DELETE user message tests

* port v3 GET-inbox_messages

* use v4 delete message route

* sendPrivateMessage: return sent message

* fix
2018-08-30 14:50:03 -05:00
Phillip Thelen
64507a161e Add android FAQ answers to content call (#10649) 2018-08-30 14:49:36 -05:00
Lucas Heim
0f7fc27663 Creating default encryption test to improve coverage (#10651) 2018-08-30 14:48:56 -05:00
Sabe Jones
1545685a5b 4.59.1 2018-08-30 19:00:15 +00:00
Sabe Jones
410355c3f1 chore(i18n): update locales 2018-08-30 18:59:45 +00:00
Sabe Jones
ac27cabf6a chore(news): Bailey 2018-08-30 13:56:36 -05:00
Sabe Jones
972631e7ac Merge branch 'release' into develop 2018-08-29 20:26:39 +00:00
Sabe Jones
d27ed7c406 4.59.0 2018-08-29 20:25:50 +00:00
Sabe Jones
031783b1d7 chore(i18n): update locales 2018-08-29 20:25:24 +00:00
Sabe Jones
318aa7cbd9 chore(sprites): compile 2018-08-29 15:20:36 -05:00
Sabe Jones
f802a41f75 feat(content): Animal Tails 2018-08-29 15:20:09 -05:00
Alys
1d597039ca prevent Quest progress message in Party chat when user is Resting in the Inn (#10636)
* prevent quest progress message in party chat when user is resting in the inn

* improve comment

* update tests now that the test group includes a new member (sleeping quest participant)

* adjust a test to fix lint failure (and make the test better)

* fix order of element assignments in test array
2018-08-28 15:04:16 +02:00
negue
07bc374078 fix ultimate gear notification length - allow longer notifications but with a-like border-radius 2018-08-27 20:08:09 +02:00
Keith Holliday
8153674dc0 Added in class checks and notification tests 2018-08-26 17:41:55 -05:00
Keith Holliday
0002148326 Check user version before adding notifications (#10628) 2018-08-26 15:13:36 -05:00
Keith Holliday
d198e23de6 Fixed editing categories (#10627) 2018-08-25 11:15:47 -05:00
Keith Holliday
4f4e141806 Added prize back after deleting challenge (#10631) 2018-08-25 11:15:00 -05:00
Keith Holliday
05e8d6f032 Cleaned mp displaed in fixed value (#10626) 2018-08-25 11:14:41 -05:00
Matteo Pagliazzi
39847893d2 remove use mobile apps banner (#10634) 2018-08-25 15:02:44 +02:00
Keith Holliday
e4dbf09dda Prevented users with lvl less than 10 from seeing mana 2018-08-24 23:05:36 -05:00
Forrest Hatfield
eb99b709e0 Allow login buttons to expand vertically - fixes #9861 (#10622)
* Allow login buttons to expand vertically

* whitespace matching
2018-08-24 16:38:42 -05:00
Alex Figueroa
862b3453f8 Fix members modal showing stale data (#10619)
Resolves: #10544
2018-08-24 16:00:49 -05:00
Jacob Frericks
7f48853d32 Fixing misspelling and inconsistent punctuation in the api doc (#10617) 2018-08-24 15:48:51 -05:00
Rene Cordier
5c4f763bb1 Fix lostMasterclasser achievement issue (#10616) 2018-08-24 15:23:43 -05:00
Forrest Hatfield
bc9401b2f7 Added smartbanner code to suggest iphone/android apps for mobile users - fixes #9901 (#10604)
* Added smartbanner code to suggest iphone/android apps for mobile users

* Installed smartbanner.js as a module and imported css through app.vue

* Changed the logos to use the ones in the existing presskit directory and fixed the import line for the smartbanner component

* Changed smartbanner import to a src include for css and updated js import
2018-08-24 15:08:34 -05:00
negue
6fb9030b96 reload completed tasks after resync is finished - always reload completed tasks (#10614) 2018-08-24 15:04:59 -05:00
Sabe Jones
ba307af963 Correct timing on updating Group Plan member quantities (#10589)
* fix(groups): correct timing on updating member quantities

* fix(groups): don't run group cancellation check if we're in invite flow

* fix(groups): update leader when memberCount is 1

* fix(groups): move leader update back--unrelated to group plans fix
2018-08-24 14:57:05 -05:00
Sabe Jones
cf4b920a67 4.58.0 2018-08-23 20:13:15 +00:00
Sabe Jones
b0ff35a8f1 chore(i18n): update locales 2018-08-23 20:04:40 +00:00
Sabe Jones
85b4c7825e chore(sprites): compile 2018-08-23 14:57:28 -05:00
Sabe Jones
5b7ea8ec5c feat(content): Mystery Items Aug 2018 2018-08-23 14:57:11 -05:00
Sabe Jones
5cfd0c863e Merge branch 'release' into develop 2018-08-22 16:46:44 +00:00
Sabe Jones
10c6244c0c 4.57.4 2018-08-22 16:46:14 +00:00
Sabe Jones
20e65be8bf chore(i18n): update locales 2018-08-22 16:45:38 +00:00
Sabe Jones
8bac324ba7 fix(content): September end date for Ember Potions 2018-08-22 11:42:38 -05:00
Matteo Pagliazzi
2ee0288aaa fix stripe sub cancellation test 2018-08-22 14:36:20 +02:00
Sabe Jones
b7ef4c50b2 Merge branch 'release' into develop 2018-08-21 18:32:52 +00:00
Sabe Jones
52be9c750f 4.57.3 2018-08-21 18:32:30 +00:00
Sabe Jones
b0200026aa chore(i18n): update locales 2018-08-21 18:32:13 +00:00
Sabe Jones
e6c8b977c8 feat(content): enable Ember Potions 2018-08-21 13:29:52 -05:00
Sabe Jones
c78b5ecf7c Analytics: More / improved tracking (#10608)
* WIP(analytics): add / improve tracking

* fix(groups): revert attempt at tracking on group model

* fix(analytics): track questing based on user data

* each buy-operation now has a getItemType method - typo getItemKey - removed unneeded overrides
2018-08-20 14:13:22 -05:00
Sabe Jones
f27e9b02d8 Merge branch 'release' into develop 2018-08-20 14:16:36 +00:00
Sabe Jones
c06c19ca41 4.57.2 2018-08-20 14:16:01 +00:00
Sabe Jones
d5d894b8a9 chore(i18n): update locales 2018-08-20 14:15:23 +00:00
Sabe Jones
7bd4e6a5a9 Merge branch 'remove-auth-with-url' into release 2018-08-20 09:12:59 -05:00
Matteo Pagliazzi
f13eed5663 fix inbox modal header 2018-08-20 15:40:31 +02:00
Keith Holliday
a9a2fe6314 Fixed mp rounding (#10599)
* Fixed mp rounding

* Fixed toFixed rounding
2018-08-18 21:08:56 -05:00
Keith Holliday
d6514bce8b Fixed inbox id after add (#10609) 2018-08-18 21:08:32 -05:00
negue
c862bdb76a Merge branch 'develop' of https://github.com/HabitRPG/habitica into negue/modal-notifications
# Conflicts:
#	website/client/components/notifications.vue
2018-08-18 14:25:20 +02:00
negue
b596576c53 rollback death modal changes 2018-08-18 14:22:16 +02:00
Alys
603fc8c4dd combine cron's Resting in the Inn code with non-sleeping code - fixes #5232 etc (#10577)
* remove commented-out code for purging PMs - no longer needed

https://github.com/HabitRPG/habitica/issues/7940#issuecomment-406489506

* adjust comments

* move cron code when sleeping / resting back into main body of cron code

* rename tests to use consistent terminology for sleeping

* add tests for cron when user is sleeping

* move sleeping tests to same place as non-sleeping test

This matches how the code has sleeping and non-sleeping code mingled.

* replace a broken test with new tests

The deleted test wasn't working correctly. The check that the user's
health hadn't decreased would have worked even if the user wasn't
sleeping because the Daily had been marked completed.
The new tests test both no damage from incomplete Dailies and
that Dailies are reset.

* add tests for Perfect Day buff and rename existing tests for consistent terminology

* remove old test code
2018-08-18 12:47:07 +02:00
Sabe Jones
3c602351f9 Merge branch 'release' into develop 2018-08-18 03:32:39 +00:00
Sabe Jones
29ed33461c 4.57.1 2018-08-18 03:32:14 +00:00
Sabe Jones
fbc1044100 chore(i18n): update locales 2018-08-18 03:32:02 +00:00
Matteo Pagliazzi
35e02d2871 fix hall of heroes 2018-08-17 22:29:47 -05:00
Keith Holliday
6aa204c3f5 Fixed concurrency issues with push devices (#10598)
* Fixed concurrency issues with push devices

* Fixed push notificaiton response and model adding
2018-08-17 07:01:41 -05:00
Keith Holliday
eaaa5ad7f3 Added amoire food to user immediately (#10596)
* Added amoire food to user immediately

* Fixed user item set
2018-08-17 06:29:32 -05:00
Keith Holliday
54468ff499 Added existence check (#10595) 2018-08-17 06:20:45 -05:00
Brian Fenton
53405aa586 Handleless wheelchair options (#10572)
* removing duplicate keys

* adding chair assets and wiring them to customize screen

* adding customization data for new wheelchair types

* removing an unused locale key and moving the code style override closer to the affected area

* explicitly re-enabilng linting rule

* adding button-sized chair assets

* updating assets to new resolution

* moving chair keys into component data
2018-08-17 12:23:43 +02:00
Rene Cordier
7630c02e13 Fixing healing light not being castable when user full hp (#10603)
* Fixing healing light not being castable on server and client sides when user has already full health

Adding integration test for spell cast of healing light when full health

Adding test for heal cast if user has full health

* Fixing ESLint syntax in the spells test files
2018-08-17 12:06:58 +02:00
Isabelle Lavandero
ec444384f4 Add error notification for deleted user (#10600)
* snackbar notification for deleted user

* check for 404

* localize text
2018-08-17 12:02:43 +02:00
Isabelle Lavandero
cce9b33844 Filter dailies by due/not due in group plan and challenge page (#10582)
* sort by isDue works but only on refresh

* update isDue for new tasks

* apply correct filter to challenge page
2018-08-17 11:58:43 +02:00
Matteo Pagliazzi
b977d42402 fix hall of heroes 2018-08-17 11:52:15 +02:00
Matteo Pagliazzi
2672cbd790 fix stripe cance 2018-08-17 11:12:48 +02:00
Keith Holliday
f049d29d1b Added username invite 2018-08-16 17:40:53 -05:00
Keith Holliday
b7ca5be6ee Closed modal when removing challenge task (#10597) 2018-08-16 16:54:57 -05:00
Keith Holliday
5ae89761b0 Prevent tour from displaying twice (#10594)
* Prevent tour from displaying twice

* Removed forced and prevent overlay click
2018-08-16 16:53:37 -05:00
Sabe Jones
8b0101c74c 4.57.0 2018-08-16 19:45:53 +00:00
Sabe Jones
dbd295e35b chore(i18n): update locales 2018-08-16 19:45:45 +00:00
Sabe Jones
b5428f4ac9 Merge branch 'develop' into release 2018-08-16 14:41:09 -05:00
Sabe Jones
5b213b4f94 chore(sprites): compile 2018-08-16 14:40:27 -05:00
Sabe Jones
0142e332e8 feat(content): Kangaroo Pet Quest 2018-08-16 14:40:09 -05:00
Keith Holliday
b4f955333b Revert mute date (#10602)
* Revert mute date

* Removed extra moment
2018-08-16 11:28:03 -05:00
Matteo Pagliazzi
696121fb24 remove auth with url 2018-08-15 10:40:25 +02:00
Keith Holliday
2a7dfff88a Added mute end date (#10566)
* Added mute end date

* Added indefinite mute for users using slurs

* Fixed user reload. Added no longer muted message. Added format for date

* Fixed lint
2018-08-12 12:09:12 -05:00
Alys
2c921609c1 improve apidocs related to allocating Stat Points and user/unlock - fixes #10557 (#10592)
* correct curl parameter (-X for request method; -x for proxy information)

* fix typo in error message

* fix mistakes in apidocs for allocating Stat Points
2018-08-12 12:11:01 +02:00
Rene Cordier
1f7dd421d4 Show accurate XP gain in notification on level up (#10590) 2018-08-12 11:55:10 +02:00
Matteo Pagliazzi
45ca090105 Revert "update packege-lock.json"
This reverts commit 02b22170e2.
2018-08-11 12:40:23 +02:00
Matteo Pagliazzi
02b22170e2 update packege-lock.json 2018-08-11 10:29:09 +02:00
Sabe Jones
1134c7748b Make private message character limit obvious on client (#10579)
* fix(messages): make character limit obvious on client
Fixes #10549

* fix(messages): localize hardcoded button text
2018-08-10 08:46:46 -05:00
Keith Holliday
7019e32eed Reverted css loader (#10588) 2018-08-10 15:41:45 +02:00
Sabe Jones
485c528b45 Greenkeeper cleanup round 1 (#10585)
* fix(package): update csv-stringify to version 3.0.0

* chore(package): update lcov-result-merger to version 3.0.0

* chore(package): update karma-sinon-chai to version 2.0.0

* fix(package): update bcrypt to version 3.0.0

* fix(package): update validator to version 10.5.0

Closes #10320

* fix(package): update got to version 9.0.0

* chore(package): update karma to version 3.0.0

* Merge branch 'greenkeeper-css' into greenkeeper
2018-08-09 17:02:14 -05:00
Keith Holliday
f1c1ba8efa Minor responsive updates to the spell bar (#10580) 2018-08-09 15:24:44 -05:00
Sabe Jones
b0e4c2cb11 4.56.3 2018-08-09 19:07:50 +00:00
Sabe Jones
0e346f7050 chore(i18n): update locales 2018-08-09 19:07:37 +00:00
Sabe Jones
1eb1fe76a8 Merge branch 'release' into develop 2018-08-08 16:55:21 -05:00
Sabe Jones
72a0e05804 4.56.2 2018-08-08 21:07:15 +00:00
Sabe Jones
5f37b9727a chore(i18n): update locales 2018-08-08 21:06:54 +00:00
Sabe Jones
bf17b49046 chore(sprites): compile 2018-08-08 16:04:09 -05:00
Sabe Jones
36edf5265f chore(news): Bailey for BTS Challenge 2018-08-08 16:03:57 -05:00
Keith Holliday
6da243e034 Added context message to streak (#10565)
* Added context message to streak

* Updated text
2018-08-08 03:52:53 -05:00
Alys
b87ff03210 remove unused messageGroupNotFound string (has been replaced with groupNotFound) 2018-08-07 13:01:02 +10:00
Alys
9d994f8a77 allow notification screen text to be translated (#10576)
The `noNotifications` string was not being used so changing it for
this purpose makes sense. Non-English users will see meaningful
text even before the new text is translated.
2018-08-05 10:48:36 +02:00
Sabe Jones
75c3f7214b fix(members): Don't show "View Progress" if not a Challenge 2018-08-03 14:38:31 -05:00
Sabe Jones
f3f8fa3a42 feat(pets): Prebuild Kangaroo mount sprites 2018-08-03 14:19:11 -05:00
Sabe Jones
2e34dab9a6 4.56.1 2018-08-03 10:34:43 -05:00
Sabe Jones
0f9b274059 fix(news): Correct credit with apologies to @thefifthisa! 2018-08-03 10:34:29 -05:00
Matteo Pagliazzi
041bde0cba amazon: fix styling 2018-08-03 12:23:50 +02:00
Alys
8888e63005 limit chat message flagging ability for new players - fixes #10069 (#10567)
* remove duplicate module.exports statement

* remove commented-out footer in Slack slur notification

There's no need for anything to replace this footer.

* swap order of flag actions to put most critical first

This causes moderators to be notified before the flagged message's flagCount is incremented, because if something happens to prevent the flagGroupMessage Promise from resolving, we still want to mods to see the notification.

* limit chat message flagging ability for new players

Players who created accounts less than three days ago can flag posts
but that does not contribute to the posts' flagCount. This prevents
a troll from maliciously hiding innocent messages by creating new
accounts to flag them.

* add tests

* fix other tests
2018-08-03 12:04:01 +02:00
Isabelle Lavandero
7aa2fac14a Localize time for due dates and chat messages (#10555)
* localize time for pt_BR and zh

* add zh_TW to moment langs mapping
2018-08-03 11:57:43 +02:00
FergusonSean
4493e1d98c Fix path to detect when group is the tavern or the user's party and set paths appropriately (#10570)
* Fix path to detect when group is the tavern or the user's party and set path's appropriately

* Fix lint issues
2018-08-03 11:54:32 +02:00
Sabe Jones
fcbc2acda7 4.56.0 2018-08-02 18:46:57 +00:00
Sabe Jones
729ba36ed3 chore(i18n): update locales 2018-08-02 18:45:32 +00:00
Sabe Jones
896495cac5 Merge branch 'develop' into release 2018-08-02 18:41:36 +00:00
Sabe Jones
3f89dae8c9 chore(sprites): compile 2018-08-02 13:39:34 -05:00
Sabe Jones
4ec5df170c feat(content): Backgrounds, Armoire, minor sprite fixes 2018-08-02 13:38:51 -05:00
Matteo Pagliazzi
fdbcd99525 remove hatching modal every time the stable is loaded 2018-08-02 08:37:01 +02:00
Sabe Jones
99726bdc2f Merge branch 'release' into develop 2018-08-01 17:17:07 -05:00
Sabe Jones
b00d1a067e 4.55.1 2018-08-01 19:52:27 +00:00
Sabe Jones
0910ca7470 chore(i18n): update locales 2018-08-01 19:51:43 +00:00
Sabe Jones
24f5e7c19f chore(sprites): compile 2018-08-01 14:48:50 -05:00
Sabe Jones
4f34443b84 chore(event): end Summer Splash + Potions
Also Bailey news
2018-08-01 14:48:38 -05:00
Alys
714706f925 add quest participant list to collection quests (#10568) 2018-08-01 10:43:33 +02:00
Sabe Jones
0899dddb42 Merge branch 'release' into develop 2018-07-31 15:31:48 -05:00
Sabe Jones
f123fcd1b3 4.55.0 2018-07-31 19:16:31 +00:00
Sabe Jones
6ade7b08c8 chore(i18n): update locales 2018-07-31 19:13:36 +00:00
Sabe Jones
c88b9b80b5 feat(event): Habitica Naming Day
and Bailey announcements
2018-07-31 14:08:56 -05:00
Matteo Pagliazzi
33149e1afa fix conflict from previous PR 2018-07-31 09:47:17 +02:00
negue
c8becbccb5 prevent multiple notifications (#10524)
* WIP - prevent multiple notifications

* merge promises to one

* update test, iterate each user

* revert changes in `groups.js` - filter duplicate notifications in `convertNotificationsToSafeJson`
2018-07-30 16:11:56 +02:00
Matteo Pagliazzi
c9465cbfdd Merge branch 'thefifthisa-clickout' into develop 2018-07-30 16:10:01 +02:00
Isabelle Lavandero
965b7a3be7 Mana bar no longer shown on profile if user has opted out of class system (#10560)
* no more mana bar shown on profile if user has opted out of class system

* edit tests for preferences
2018-07-30 16:05:00 +02:00
Isabelle Lavandero
6b8784cf04 esc only closes tags popup if open (#10547) 2018-07-30 16:04:35 +02:00
Isabelle Lavandero
508d832d73 View participant list of active quest (#10531)
* participant list modal opens, nothing displayed yet

* display participants!

* only need to filter

* change button to link

* prevent scrolling back up when modal opens

* style link as h4

* move css
2018-07-30 16:04:04 +02:00
Dexx Mandele
734e4a963f Re-enable start quest button (#10532)
* Check for scroll during quest pre-selection

* Re-enable start quest btn after error

* Review: remove unused start quest method
2018-07-30 16:02:17 +02:00
Texas Toland
40495aaacb Fix drag scrolling tasks when character has abilities (#10552)
The default scroll sensitivity of the task columns is 30 px from the bottom of the screen. The collapsed spells drawer renders 32 px from the bottom of the screen intercepting most drag events. Increases the scroll sensitivity to double the height of the blocking element.

Also ignores the Yarn lockfile. `yarn --ignore-engines` builds successfully and tests pass with Node 10.6.0 and MongoDB 4.0.0.
2018-07-30 16:00:55 +02:00
Alex Figueroa
5566460541 Fix user receiving Joined Challenged achievement when creating a challenge (#10559)
* Fix joinedChallenge achievement being awarded when creating a challenge

* Modify test to check that achievement is not awarded for creating a challenge
2018-07-30 16:00:05 +02:00
Alex Figueroa
774a1d9a96 Fix advanced settings from always starting collapsed (#10561)
Previously the user's preference for whether the advanced settings starts opened or collapsed was ignored.
Resolves: #10556
2018-07-30 15:58:43 +02:00
Matteo Pagliazzi
60df912dcc Merge branch 'clickout' of https://github.com/thefifthisa/habitica into thefifthisa-clickout 2018-07-30 15:55:20 +02:00
Keith Holliday
7325bc0871 Separated out modal components (#10545)
* Seprated out modal components

* Removed extra css

* Fixed import case
2018-07-30 14:38:29 +08:00
Keith Holliday
2fc233e70f Removed duplicate code and added member modal event (#10542)
* Removed duplicate code and added member modal event

* Removed console

* Removed console log
2018-07-30 13:56:17 +08:00
negue
9fd26a88ea Update notification.vue
remove duplicate methods props
2018-07-29 21:55:24 +02:00
negue
76860fe3f8 Merge branch 'develop' of https://github.com/HabitRPG/habitica into negue/modal-notifications
# Conflicts:
#	website/client/components/notifications.vue
2018-07-29 20:21:51 +02:00
negue
b16e700de5 auto revive 2018-07-29 20:12:30 +02:00
Sabe Jones
9205ec10b3 chore(i18n): update locales 2018-07-26 19:05:35 +00:00
Sabe Jones
9aa4cce3b9 fix(strings): add missing Mystery Set name 2018-07-26 13:51:07 -05:00
Sabe Jones
42d823ab44 4.54.0 2018-07-26 18:20:56 +00:00
Sabe Jones
f19331cfcc chore(i18n): update locales 2018-07-26 18:20:18 +00:00
Sabe Jones
b7de7335ed Merge branch 'develop' into release 2018-07-26 18:15:33 +00:00
Sabe Jones
2e00ec5534 chore(news): Bailey 2018-07-26 13:12:15 -05:00
thefifthisa
f30d4b2cbf works for all outside clicks! 2018-07-24 19:07:59 -04:00
Sabe Jones
a3af39ed25 4.53.0 2018-07-24 20:05:16 +00:00
Sabe Jones
b57518732e chore(i18n): update locales 2018-07-24 20:04:40 +00:00
Sabe Jones
a6a6aac400 chore(sprites): compile 2018-07-24 15:00:09 -05:00
Sabe Jones
48a92e77be feat(content): July Subscriber Items and Summer Splash Orcas 2018-07-24 14:59:49 -05:00
thefifthisa
a31f1f19fc works inside modal too (mostly) 2018-07-23 20:45:50 -04:00
Matteo Pagliazzi
d2a39a5124 fix #10511 (#10551) 2018-07-23 19:47:32 +02:00
thefifthisa
9b69b640c3 close on click out works for background but not inside modal 2018-07-22 19:47:57 -04:00
Keith Holliday
7bead74b49 Removed extra options and fixed copy userID (#10538) 2018-07-21 08:11:29 -05:00
Keith Holliday
91e91788ce Change streak achievement snack to text type (#10534) 2018-07-21 08:11:09 -05:00
Keith Holliday
62c60ce520 Added copy to detail blocks wont work on mods (#10539)
* Added copy to detail blocks wont work on mods

* fix(wording): "moderator" clearer than "mod"
2018-07-21 08:10:54 -05:00
Keith Holliday
423eafbd4d Reset gift message (#10540) 2018-07-21 08:10:34 -05:00
Keith Holliday
004ab51c46 Allow for stat edits on mobile (#10535) 2018-07-21 08:09:57 -05:00
Keith Holliday
f464403623 Display exact date on hover (#10536) 2018-07-21 08:09:36 -05:00
Keith Holliday
0fc66bef4e Fixed guild badge color text (#10537) 2018-07-21 08:09:16 -05:00
Keith Holliday
71636cd25e fixed new message count alignment (#10541) 2018-07-21 08:08:56 -05:00
Keith Holliday
d5efb50d9b Update stat point title (#10543) 2018-07-21 08:08:36 -05:00
Keith Holliday
510e01effd Added user field limit to some routes (#10546)
* Added user field limit to some routes

* Added taskOrder and perferences

* Added contributor field
2018-07-20 23:24:33 -05:00
negue
b75e65f42d move modals to notifications (to open the modals) 2018-07-20 23:56:36 +02:00
Texas Toland
0e648d85a0 Fix #10477 (#10530) 2018-07-20 15:55:18 -05:00
negue
7b562c45cf Fix Modal Stack - reopening modals (#10493)
* use fallback target.id - only scroll modal content (not the page)

* fix lint

* debug information

* add snackbar onClick callback - use notification instead of modal for joined-challenge

* revert console.log / fix lint
2018-07-20 15:50:40 -05:00
negue
b7a46637d5 check genericPurchase rejections and show error (#10526) 2018-07-20 15:33:23 -05:00
Sabe Jones
284b2cc413 Group Tasks Shared Completion (#10515)
* WIP(groups): add shared completion prop
Also fix an issue where the Needs Approval toggle would not read/save 
correctly.

* fix(groups): save group options on task create
Also, correct count of assigned members when viewing user is among 
assignments

* fix(groups): display correct messages in two places

* fix(tasks): eliminate console error related to filtering
Also localize a group plans string

* WIP(groups): implement single completion for approval workflow

* WIP(groups): Add shared completion handling to no-approval-needed flow

* WIP(groups): cover approval flow case for all-assigned
Also save new field on initial task creation

* fix(tasks): use default sharedCompletion value when creating tasks

* WIP(tests): non-working draft test

* Added completed todo to group query

* WIP(group-tasks): fix bugs, add tests

* refactor(group-tasks): deleteMany op, add more tests

* refactor(group-tasks): move shared completion handling to lib

* WIP(group-tasks): broken refactor

* WIP(group-tasks): await all the things

* Turned complete master task to save

* WIP(group-tasks): show completed

* fix(filtering): don't try to filter if no list is passed

* refactor(group-tasks): load completed to-dos on demand, not at start

* fix(group-tasks): don't double up on repeat visits

* fix(group-tasks): include brief explanation in dropdown

* fix(group-tasks): improve wording some more
2018-07-20 12:29:44 -05:00
negue
8b69540e71 FIX: Challenge Creation without Group not found error (#10525)
* prevent loading the party, if the user isn't part of one

* check for the party id too
2018-07-19 22:48:17 -05:00
Keith Holliday
8d9a4e97a8 Fixed display points for the stat save button (#10522) 2018-07-19 22:03:43 -05:00
Sabe Jones
f31a82c8f2 4.52.2 2018-07-19 19:04:53 +00:00
Sabe Jones
8bc02e82ee chore(i18n): update locales 2018-07-19 19:04:38 +00:00
Sabe Jones
9040f9f04e 4.52.1 2018-07-19 14:01:16 -05:00
Sabe Jones
ff82c37d5f chore(news): Bailey 2018-07-19 14:00:53 -05:00
Sabe Jones
37364b0700 Merge branch 'develop' into release 2018-07-19 13:38:00 -05:00
Matteo Pagliazzi
8eac8732c5 fix(tasks): do not load completed todos if not necessary 2018-07-16 12:02:37 +02:00
Keith Holliday
896a1b74b6 Added new award flow to challenges (#10512) 2018-07-13 21:58:28 -05:00
Keith Holliday
3b36046a6a Removed member count code (#10518) 2018-07-13 06:06:33 -05:00
negue
07991817e7 check balanceRemoved for analytics call (#10506) 2018-07-13 10:51:49 +02:00
negue
47b75156fa reset flag comment (#10507) 2018-07-13 10:50:17 +02:00
Sabe Jones
c630486fef fix(errors): handle non-array-style errors again 2018-07-12 16:47:59 -05:00
negue
0a070316b5 fix xml export (#10505)
* add /export webpack-proxy - fix xml export

* fix lint / add xmlMode
2018-07-12 15:49:22 -05:00
negue
f6b34e85df Max 3000 Character Limit on Chat-Messages (#10494)
* limit chat length to 3000

* add test
2018-07-12 15:40:04 -05:00
Isabelle Lavandero
2946f0df15 Update signup error messages (#10483)
* prints first error message only

* update signup error messages, missing password not working (wip)

* remove alerts, show notEmpty, first error only per param, update unit test

* move changes to client side
2018-07-12 15:27:02 -05:00
Vinicius
614d9a920a Change track-habits.png to the correct pictures available on Zeplin. (#10514) 2018-07-12 15:13:56 -05:00
aszlig
454524fb5b member-details: Only add 1px margin when condensed (#10504)
As reported in #10502, adding a 1 pixel right margin to *all* nodes with
the member-stats class will affect the display when you click on the
avatar as well, which will then break the layout because of that
additional margin.

Instead of adding the margin to .member-stats in general, let's just add
it to the condensed version so that it really just addresses the
flickering on Chrome/Chromium as reported in #10379.

I've tested whether the flickering still happens via a small
xdotool-loop (just to make sure I don't get too shaky with the mouse):

for i in $(seq 500 508); do xdotool mousemove "$i" 250; sleep 5; done

The flickering doesn't happen anymore and the layout in the party
members overview and the stats overview when you click on the avatar is
no longer distorted.

Signed-off-by: aszlig <aszlig@nix.build>
Fixes: #10502
2018-07-12 15:12:40 -05:00
Vinicius
abc0777412 Add word-break: break-word to .sortable-tasks class to prevent links and words to get out of the task box (#10495) 2018-07-12 15:10:23 -05:00
James Robinson
6972eb8f8f Prevent login error text overflow (#10450) 2018-07-12 15:08:13 -05:00
Brian Fenton
535ee2b2a7 Adding hand cursor to FAQ headings, and ability to address answers via URL hash (#10260)
* Turning H2s into anchors to add hand cursor and to create addressable page fragments
adding ref to target individual entries
adding scroll handler to make sure expanded result is in view.

* combining click handler directives as per CR

* changing question display to always render, then hide via CSS, vs only render when state changes.

* updating pug template to include heading in each URL fragment

* simplifying logic & moving to vue-bootstrap accordion since multiple open panels is not required

* adding pointer cursor to FAQ headings

* moving initial hash checking to data prop instead of mounted so it does not trigger oddities on on hash change (re-mount)

* using new pug HTML for bootstrap collapse

* removing extraneous markup

* updating styling to match existing page

* removing fancier than necessary markup, and attendant styles

* using more standard event property
2018-07-12 15:07:49 -05:00
Jim Pollaro
c9755bee7c Logout Changes #9915 (#10022)
* Added session check before route changes, but express isn't finding route

* Added a logout component. Changed route to logout on server. Typing 'logout' in URL will logout of Vue + Express

* Removed commented text from previous version

* Updated logout function to comply with formatting and eliminate unused blocks

* Added package-lock.json back

* package-lock.json

* recreated package-lock file

* fix(auth): allow logout from direct visit to /logout path

* fix(merge): clean up more misc changes

* fix(merge): remove extra file
2018-07-12 15:07:08 -05:00
Matteo Pagliazzi
fd700f92ae remove node 8 2018-06-18 10:17:38 +02:00
Matteo Pagliazzi
f41665f5a9 update deps 2018-06-18 10:04:03 +02:00
Matteo Pagliazzi
8b385c0b7b update deps 2018-06-18 10:01:09 +02:00
Matteo Pagliazzi
282f8db933 Merge branch 'develop' into greenkeeper/update-to-node-10 2018-06-18 09:57:39 +02:00
Matteo Pagliazzi
662b08c242 update engines 2018-06-18 09:57:35 +02:00
Matteo Pagliazzi
b7e601be16 Merge branch 'develop' into greenkeeper/update-to-node-10 2018-04-27 19:49:11 +02:00
greenkeeper[bot]
395676fcb1 Update engines to node 10 in package.json 2018-04-27 11:41:03 +00:00
greenkeeper[bot]
cc4df1c995 Update to node 10 in .nvmrc 2018-04-27 11:41:01 +00:00
greenkeeper[bot]
48eada2c37 Update to node 10 in .travis.yml 2018-04-27 11:40:58 +00:00
1659 changed files with 79637 additions and 59120 deletions

1
.gitignore vendored
View File

@@ -39,6 +39,7 @@ dist-client
test/client/unit/coverage
test/client/e2e/reports
test/client-old/spec/mocks/translations.js
yarn.lock
# Elastic Beanstalk Files
.elasticbeanstalk/*

2
.nvmrc
View File

@@ -1 +1 @@
8
10

View File

@@ -1,6 +1,6 @@
language: node_js
node_js:
- '8'
- '10'
services:
- mongodb
cache:

View File

@@ -1,4 +1,4 @@
FROM node:8
FROM node:10
ENV ADMIN_EMAIL admin@habitica.com
ENV AMAZON_PAYMENTS_CLIENT_ID amzn1.application-oa2-client.68ed9e6904ef438fbc1bf86bf494056e
@@ -8,6 +8,7 @@ ENV BASE_URL https://habitica.com
ENV FACEBOOK_KEY 128307497299777
ENV GA_ID UA-33510635-1
ENV GOOGLE_CLIENT_ID 1035232791481-32vtplgnjnd1aufv3mcu1lthf31795fq.apps.googleusercontent.com
ENV LOGGLY_CLIENT_TOKEN ab5663bf-241f-4d14-8783-7d80db77089a
ENV NODE_ENV production
ENV STRIPE_PUB_KEY pk_85fQ0yMECHNfHTSsZoxZXlPSwSNfA

View File

@@ -1,4 +1,4 @@
FROM node:8
FROM node:10
# Install global packages
RUN npm install -g gulp-cli mocha

View File

@@ -1,120 +1,82 @@
{
"PORT":3000,
"ENABLE_CONSOLE_LOGS_IN_PROD":"false",
"IP":"0.0.0.0",
"WEB_CONCURRENCY":1,
"BASE_URL":"http://localhost:3000",
"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",
"ENABLE_CONSOLE_LOGS_IN_TEST": false,
"CRON_SAFE_MODE":"false",
"CRON_SEMI_SAFE_MODE":"false",
"MAINTENANCE_MODE": "false",
"SESSION_SECRET":"YOUR SECRET HERE",
"SESSION_SECRET_KEY": "1234567891234567891234567891234567891234567891234567891234567891",
"SESSION_SECRET_IV": "12345678912345678912345678912345",
"ADMIN_EMAIL": "you@example.com",
"SMTP_USER":"user@example.com",
"SMTP_PASS":"password",
"SMTP_SERVICE":"Gmail",
"SMTP_HOST":"example.com",
"SMTP_PORT": 587,
"SMTP_TLS": true,
"STRIPE_API_KEY":"aaaabbbbccccddddeeeeffff00001111",
"STRIPE_PUB_KEY":"22223333444455556666777788889999",
"NEW_RELIC_LICENSE_KEY":"NEW_RELIC_LICENSE_KEY",
"NEW_RELIC_NO_CONFIG_FILE":"true",
"NEW_RELIC_APPLICATION_ID":"NEW_RELIC_APPLICATION_ID",
"NEW_RELIC_API_KEY":"NEW_RELIC_API_KEY",
"GA_ID": "GA_ID",
"AMPLITUDE_KEY": "AMPLITUDE_KEY",
"AMAZON_PAYMENTS": {
"SELLER_ID": "SELLER_ID",
"CLIENT_ID": "CLIENT_ID",
"MWS_KEY": "",
"MWS_SECRET": ""
},
"FLAG_REPORT_EMAIL": "email@mod.com,email2@mod.com",
"EMAIL_SERVER": {
"url": "http://example.com",
"authUser": "user",
"authPassword": "password"
},
"S3":{
"bucket":"bucket",
"accessKeyId":"accessKeyId",
"secretAccessKey":"secretAccessKey"
},
"SLACK_URL": "https://hooks.slack.com/services/some-url",
"TRANSIFEX_SLACK_CHANNEL": "transifex",
"PAYPAL":{
"billing_plans": {
"basic_earned":"basic_earned",
"basic_3mo":"basic_3mo",
"basic_6mo":"basic_6mo",
"google_6mo":"google_6mo",
"basic_12mo":"basic_12mo"
},
"mode":"sandbox",
"client_id":"client_id",
"client_secret":"client_secret",
"experience_profile_id": ""
},
"IAP_GOOGLE_KEYDIR": "/path/to/google/public/key/dir/",
"LOGGLY_TOKEN": "token",
"LOGGLY_CLIENT_TOKEN": "token",
"LOGGLY_ACCOUNT": "account",
"PUSH_CONFIGS": {
"GCM_SERVER_API_KEY": "",
"APN_ENABLED": "false",
"APN_KEY_ID": "xxxxxxxxxx",
"APN_KEY": "xxxxxxxxxx",
"APN_TEAM_ID": "aaabbbcccd",
"FCM_SERVER_API_KEY": ""
},
"SITE_HTTP_AUTH": {
"ENABLED": "false",
"USERNAME": "admin",
"PASSWORD": "password"
},
"PUSHER": {
"ENABLED": "false",
"APP_ID": "appId",
"KEY": "key",
"SECRET": "secret"
},
"SLACK": {
"FLAGGING_URL": "https://hooks.slack.com/services/id/id/id",
"FLAGGING_FOOTER_LINK": "https://habitrpg.github.io/flag-o-rama/",
"SUBSCRIPTIONS_URL": "https://hooks.slack.com/services/id/id/id"
},
"ITUNES_SHARED_SECRET": "aaaabbbbccccddddeeeeffff00001111",
"EMAILS" : {
"COMMUNITY_MANAGER_EMAIL" : "admin@habitica.com",
"TECH_ASSISTANCE_EMAIL" : "admin@habitica.com",
"PRESS_ENQUIRY_EMAIL" : "admin@habitica.com"
},
"LOGGLY" : {
"TOKEN" : "example-token",
"SUBDOMAIN" : "exmaple-subdomain"
},
"KAFKA": {
"GROUP_ID": "",
"CLOUDKARAFKA_BROKERS": "",
"CLOUDKARAFKA_USERNAME": "",
"CLOUDKARAFKA_PASSWORD": "",
"CLOUDKARAFKA_TOPIC_PREFIX": ""
},
"MIGRATION_CONNECT_STRING": "mongodb://localhost:27017/habitrpg?auto_reconnect=true"
"ADMIN_EMAIL": "you@example.com",
"AMAZON_PAYMENTS_CLIENT_ID": "CLIENT_ID",
"AMAZON_PAYMENTS_MODE": "sandbox",
"AMAZON_PAYMENTS_MWS_KEY": "MWS_KEY",
"AMAZON_PAYMENTS_MWS_SECRET": "MWS_SECRET",
"AMAZON_PAYMENTS_SELLER_ID": "SELLER_ID",
"AMPLITUDE_KEY": "AMPLITUDE_KEY",
"AMPLITUDE_SECRET": "AMPLITUDE_SECRET",
"BASE_URL": "http://localhost:3000",
"CRON_SAFE_MODE": "false",
"CRON_SEMI_SAFE_MODE": "false",
"DISABLE_REQUEST_LOGGING": "true",
"EMAILS_COMMUNITY_MANAGER_EMAIL": "admin@habitica.com",
"EMAILS_PRESS_ENQUIRY_EMAIL": "admin@habitica.com",
"EMAILS_TECH_ASSISTANCE_EMAIL": "admin@habitica.com",
"EMAIL_SERVER_AUTH_PASSWORD": "password",
"EMAIL_SERVER_AUTH_USER": "user",
"EMAIL_SERVER_URL": "http://example.com",
"ENABLE_CONSOLE_LOGS_IN_PROD": "false",
"ENABLE_CONSOLE_LOGS_IN_TEST": "false",
"FACEBOOK_KEY": "123456789012345",
"FACEBOOK_SECRET": "aaaabbbbccccddddeeeeffff00001111",
"FLAG_REPORT_EMAIL": "email@example.com, email2@example.com",
"GA_ID": "GA_ID",
"GOOGLE_CLIENT_ID": "123456789012345",
"GOOGLE_CLIENT_SECRET": "aaaabbbbccccddddeeeeffff00001111",
"IAP_GOOGLE_KEYDIR": "/path/to/google/public/key/dir/",
"IGNORE_REDIRECT": "true",
"ITUNES_SHARED_SECRET": "aaaabbbbccccddddeeeeffff00001111",
"LOGGLY_CLIENT_TOKEN": "token",
"LOGGLY_SUBDOMAIN": "example-subdomain",
"LOGGLY_TOKEN": "example-token",
"MAINTENANCE_MODE": "false",
"NODE_DB_URI": "mongodb://localhost/habitrpg",
"NODE_ENV": "development",
"PATH": "bin:node_modules/.bin:/usr/local/bin:/usr/bin:/bin",
"PAYPAL_BILLING_PLANS_basic_12mo": "basic_12mo",
"PAYPAL_BILLING_PLANS_basic_3mo": "basic_3mo",
"PAYPAL_BILLING_PLANS_basic_6mo": "basic_6mo",
"PAYPAL_BILLING_PLANS_basic_earned": "basic_earned",
"PAYPAL_BILLING_PLANS_google_6mo": "google_6mo",
"PAYPAL_CLIENT_ID": "client_id",
"PAYPAL_CLIENT_SECRET": "client_secret",
"PAYPAL_EXPERIENCE_PROFILE_ID": "xp_profile_id",
"PAYPAL_MODE": "sandbox",
"PLAY_API_ACCESS_TOKEN": "aaaabbbbccccddddeeeeffff00001111",
"PLAY_API_CLIENT_ID": "aaaabbbbccccddddeeeeffff00001111",
"PLAY_API_CLIENT_SECRET": "aaaabbbbccccddddeeeeffff00001111",
"PLAY_API_REFRESH_TOKEN": "aaaabbbbccccddddeeeeffff00001111",
"PORT": 3000,
"PUSH_CONFIGS_APN_ENABLED": "false",
"PUSH_CONFIGS_APN_KEY": "xxxxxxxxxx",
"PUSH_CONFIGS_APN_KEY_ID": "xxxxxxxxxx",
"PUSH_CONFIGS_APN_TEAM_ID": "aaabbbcccd",
"PUSH_CONFIGS_FCM_SERVER_API_KEY": "aaabbbcccd",
"S3_ACCESS_KEY_ID": "accessKeyId",
"S3_BUCKET": "bucket",
"S3_SECRET_ACCESS_KEY": "secretAccessKey",
"SESSION_SECRET": "YOUR SECRET HERE",
"SESSION_SECRET_IV": "12345678912345678912345678912345",
"SESSION_SECRET_KEY": "1234567891234567891234567891234567891234567891234567891234567891",
"SITE_HTTP_AUTH_ENABLED": "false",
"SITE_HTTP_AUTH_PASSWORD": "password",
"SITE_HTTP_AUTH_USERNAME": "admin",
"SLACK_FLAGGING_FOOTER_LINK": "https://habitrpg.github.io/flag-o-rama/",
"SLACK_FLAGGING_URL": "https://hooks.slack.com/services/id/id/id",
"SLACK_SUBSCRIPTIONS_URL": "https://hooks.slack.com/services/id/id/id",
"SLACK_URL": "https://hooks.slack.com/services/some-url",
"SMTP_HOST": "example.com",
"SMTP_PASS": "password",
"SMTP_PORT": 587,
"SMTP_SERVICE": "Gmail",
"SMTP_TLS": "true",
"SMTP_USER": "user@example.com",
"STRIPE_API_KEY": "aaaabbbbccccddddeeeeffff00001111",
"STRIPE_PUB_KEY": "22223333444455556666777788889999",
"TEST_DB_URI": "mongodb://localhost/habitrpg_test",
"TRANSIFEX_SLACK_CHANNEL": "transifex",
"WEB_CONCURRENCY": 1,
"SKIP_SSL_CHECK_KEY": "key"
}

View File

@@ -0,0 +1,48 @@
import monk from 'monk';
import nconf from 'nconf';
/*
* Output data on users who completed all the To-Do tasks in the 2018 Back-to-School Challenge.
* User ID,Profile Name
*/
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING');
const CHALLENGE_ID = '0acb1d56-1660-41a4-af80-9259f080b62b';
let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false });
let dbTasks = monk(CONNECTION_STRING).get('tasks', { castIds: false });
function usersReport() {
console.info('User ID,Profile Name');
let userCount = 0;
dbUsers.find(
{challenges: CHALLENGE_ID},
{fields:
{_id: 1, 'profile.name': 1}
},
).each((user, {close, pause, resume}) => {
pause();
userCount++;
let completedTodos = 0;
return dbTasks.find(
{
userId: user._id,
'challenge.id': CHALLENGE_ID,
type: 'todo',
},
{fields: {completed: 1}}
).each((task) => {
if (task.completed) completedTodos++;
}).then(() => {
if (completedTodos >= 7) {
console.info(`${user._id},${user.profile.name}`);
}
resume();
});
}).then(() => {
console.info(`${userCount} users reviewed`);
return process.exit(0);
});
}
module.exports = usersReport;

View File

@@ -2,9 +2,13 @@ version: "3"
services:
client:
environment:
- NODE_ENV=development
volumes:
- '.:/usr/src/habitrpg'
server:
environment:
- NODE_ENV=development
volumes:
- '.:/usr/src/habitrpg'

View File

@@ -167,7 +167,7 @@ gulp.task('test:content:safe', gulp.series('test:prepare:build', (cb) => {
gulp.task('test:api:unit', (done) => {
let runner = exec(
testBin('node_modules/.bin/istanbul cover --dir coverage/api-unit node_modules/mocha/bin/_mocha -- test/api/unit --recursive --require ./test/helpers/start-server'),
testBin('istanbul cover --dir coverage/api-unit node_modules/mocha/bin/_mocha -- test/api/unit --recursive --require ./test/helpers/start-server'),
(err) => {
if (err) {
process.exit(1);
@@ -180,12 +180,12 @@ gulp.task('test:api:unit', (done) => {
});
gulp.task('test:api:unit:watch', () => {
return gulp.watch(['website/server/libs/*', 'test/api/v3/unit/**/*', 'website/server/controllers/**/*'], gulp.series('test:api:unit', done => done()));
return gulp.watch(['website/server/libs/*', 'test/api/unit/**/*', 'website/server/controllers/**/*'], gulp.series('test:api:unit', done => done()));
});
gulp.task('test:api-v3:integration', (done) => {
let runner = exec(
testBin('node_modules/.bin/istanbul cover --dir coverage/api-v3-integration --report lcovonly node_modules/mocha/bin/_mocha -- test/api/v3/integration --recursive --require ./test/helpers/start-server'),
testBin('istanbul cover --dir coverage/api-v3-integration --report lcovonly node_modules/mocha/bin/_mocha -- test/api/v3/integration --recursive --require ./test/helpers/start-server'),
{maxBuffer: 500 * 1024},
(err) => {
if (err) {
@@ -217,7 +217,7 @@ gulp.task('test:api-v3:integration:separate-server', (done) => {
gulp.task('test:api-v4:integration', (done) => {
let runner = exec(
testBin('node_modules/.bin/istanbul cover --dir coverage/api-v4-integration --report lcovonly node_modules/mocha/bin/_mocha -- test/api/v4 --recursive --require ./test/helpers/start-server'),
testBin('istanbul cover --dir coverage/api-v4-integration --report lcovonly node_modules/mocha/bin/_mocha -- test/api/v4 --recursive --require ./test/helpers/start-server'),
{maxBuffer: 500 * 1024},
(err) => {
if (err) {
@@ -254,4 +254,4 @@ gulp.task('test:api-v3', gulp.series(
'test:api:unit',
'test:api-v3:integration',
done => done()
));
));

View File

@@ -0,0 +1,99 @@
let migrationName = '20180724_summer-splash-orcas.js'; // Update per month
let authorName = 'Sabe'; // in case script author needs to know when their ...
let authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
/*
* Award ladder items to participants in this year's Summer Splash festivities
*/
import monk from 'monk';
import nconf from 'nconf';
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING');
let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false });
function processUsers (lastId) {
// specify a query to limit the affected users (empty for all users):
let query = {
migration: {$ne: migrationName},
'auth.timestamps.loggedin': {$gt: new Date('2018-07-01')}, // rerun without date restriction after initial run
};
if (lastId) {
query._id = {
$gt: lastId,
};
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 250,
fields: [
'items.mounts',
], // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
})
.then(updateUsers)
.catch((err) => {
console.log(err);
return exiting(1, `ERROR! ${ err}`);
});
}
let progressCount = 1000;
let count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
displayData();
return;
}
let userPromises = users.map(updateUser);
let lastUser = users[users.length - 1];
return Promise.all(userPromises)
.then(() => {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
let set = {};
if (user && user.items && user.items.pets && typeof user.items.pets['Orca-Base'] !== 'undefined') {
set = {migration: migrationName};
} else if (user && user.items && user.items.mounts && typeof user.items.mounts['Orca-Base'] !== 'undefined') {
set = {migration: migrationName, 'items.pets.Orca-Base': 5};
} else {
set = {migration: migrationName, 'items.mounts.Orca-Base': true};
}
dbUsers.update({_id: user._id}, {$set: set});
if (count % progressCount === 0) console.warn(`${count } ${ user._id}`);
if (user._id === authorUuid) console.warn(`${authorName } processed`);
}
function displayData () {
console.warn(`\n${ count } users processed\n`);
return exiting(0);
}
function exiting (code, msg) {
code = code || 0; // 0 = success
if (code && !msg) {
msg = 'ERROR!';
}
if (msg) {
if (code) {
console.error(msg);
} else {
console.log(msg);
}
}
process.exit(code);
}
module.exports = processUsers;

View File

@@ -0,0 +1,123 @@
let migrationName = '20180731_naming-day.js'; // Update when running in future years
let authorName = 'Sabe'; // in case script author needs to know when their ...
let authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
/*
* Award Naming Day ladder items to participants in this month's Naming Day festivities
*/
import monk from 'monk';
import nconf from 'nconf';
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING'); // FOR TEST DATABASE
let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false });
function processUsers (lastId) {
// specify a query to limit the affected users (empty for all users):
let query = {
migration: {$ne: migrationName},
};
if (lastId) {
query._id = {
$gt: lastId,
};
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 250,
fields: [
'items.gear.owned',
'items.mounts',
'items.pets',
], // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
})
.then(updateUsers)
.catch((err) => {
console.log(err);
return exiting(1, `ERROR! ${ err}`);
});
}
let progressCount = 1000;
let count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
displayData();
return;
}
let userPromises = users.map(updateUser);
let lastUser = users[users.length - 1];
return Promise.all(userPromises)
.then(() => {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
let set = {};
let push;
const inc = {
'items.food.Cake_Base': 1,
'items.food.Cake_CottonCandyBlue': 1,
'items.food.Cake_CottonCandyPink': 1,
'items.food.Cake_Desert': 1,
'items.food.Cake_Golden': 1,
'items.food.Cake_Red': 1,
'items.food.Cake_Shade': 1,
'items.food.Cake_Skeleton': 1,
'items.food.Cake_White': 1,
'items.food.Cake_Zombie': 1,
'achievements.habiticaDays': 1,
};
if (user && user.items && user.items.gear && user.items.gear.owned && typeof user.items.gear.owned.head_special_namingDay2017 !== 'undefined') {
set = {migration: migrationName, 'items.gear.owned.body_special_namingDay2018': false};
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.body_special_namingDay2018', _id: monk.id()}};
} else if (user && user.items && user.items.pets && typeof user.items.pets['Gryphon-RoyalPurple'] !== 'undefined') {
set = {migration: migrationName, 'items.gear.owned.head_special_namingDay2017': false};
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.head_special_namingDay2017', _id: monk.id()}};
} else if (user && user.items && user.items.mounts && typeof user.items.mounts['Gryphon-RoyalPurple'] !== 'undefined') {
set = {migration: migrationName, 'items.pets.Gryphon-RoyalPurple': 5};
} else {
set = {migration: migrationName, 'items.mounts.Gryphon-RoyalPurple': true};
}
if (push) {
dbUsers.update({_id: user._id}, {$set: set, $push: push, $inc: inc});
} else {
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,147 @@
const migrationName = '20180811_inboxOutsideUser.js';
const authorName = 'paglias'; // in case script author needs to know when their ...
const authorUuid = 'ed4c688c-6652-4a92-9d03-a5a79844174a'; // ... own data is done
/*
* Move inbox messages from the user model to their own collection
*/
const monk = require('monk');
const nconf = require('nconf');
const uuid = require('uuid').v4;
const Inbox = require('../website/server/models/message').inboxModel;
const connectionString = nconf.get('MIGRATION_CONNECT_STRING'); // FOR TEST DATABASE
const dbInboxes = monk(connectionString).get('inboxes', { castIds: false });
const dbUsers = monk(connectionString).get('users', { castIds: false });
function processUsers (lastId) {
let query = {
migration: {$ne: migrationName},
};
if (lastId) {
query._id = {
$gt: lastId,
};
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 1000,
fields: ['_id', 'inbox'],
})
.then(updateUsers)
.catch((err) => {
console.log(err);
return exiting(1, `ERROR! ${ err}`);
});
}
let progressCount = 1000;
let count = 0;
let msgCount = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users and their tasks found and modified.');
displayData();
return;
}
let usersPromises = users.map(updateUser);
let lastUser = users[users.length - 1];
return Promise.all(usersPromises)
.then(() => {
return processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
if (count % progressCount === 0) console.warn(`${count } ${ user._id}`);
if (msgCount % progressCount === 0) console.warn(`${msgCount } messages processed`);
if (user._id === authorUuid) console.warn(`${authorName } being processed`);
const oldInboxMessages = user.inbox.messages || {};
const oldInboxMessagesIds = Object.keys(oldInboxMessages);
msgCount += oldInboxMessagesIds.length;
const newInboxMessages = oldInboxMessagesIds.map(msgId => {
const msg = oldInboxMessages[msgId];
if (!msg || (!msg.id && !msg._id)) { // eslint-disable-line no-extra-parens
console.log('missing message or message _id and id', msg);
throw new Error('error!');
}
if (msg.id && !msg._id) msg._id = msg.id;
if (msg._id && !msg.id) msg.id = msg._id;
const newMsg = new Inbox(msg);
newMsg.ownerId = user._id;
return newMsg.toJSON();
});
const promises = newInboxMessages.map(newMsg => {
return (async function fn () {
const existing = await dbInboxes.find({_id: newMsg._id});
if (existing.length > 0) {
if (
existing[0].ownerId === newMsg.ownerId &&
existing[0].text === newMsg.text &&
existing[0].uuid === newMsg.uuid &&
existing[0].sent === newMsg.sent
) {
return null;
}
newMsg.id = newMsg._id = uuid();
}
return newMsg;
})();
});
return Promise.all(promises)
.then((filteredNewMsg) => {
filteredNewMsg = filteredNewMsg.filter(m => Boolean(m && m.id && m._id && m.id == m._id));
return dbInboxes.insert(filteredNewMsg);
}).then(() => {
return dbUsers.update({_id: user._id}, {
$set: {
migration: migrationName,
'inbox.messages': {},
},
});
}).catch((err) => {
console.log(err);
return exiting(1, `ERROR! ${ err}`);
});
}
function displayData () {
console.warn(`\n${ count } users processed\n`);
console.warn(`\n${ msgCount } messages 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,99 @@
let authorName = 'Sabe'; // in case script author needs to know when their ...
let authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
/*
* Generate usernames for users who lack them
*/
import monk from 'monk';
import nconf from 'nconf';
import { generateUsername } from '../../website/server/libs/auth/utils';
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING'); // FOR TEST DATABASE
let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false });
function processUsers (lastId) {
// specify a query to limit the affected users (empty for all users):
let query = {
'auth.local.username': {$exists: false},
'auth.timestamps.loggedin': {$gt: new Date('2018-04-01')}, // Initial coverage for users active within last 6 months
};
if (lastId) {
query._id = {
$gt: lastId,
};
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 250,
fields: [
'auth',
], // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
})
.then(updateUsers)
.catch((err) => {
console.log(err);
return exiting(1, `ERROR! ${ err}`);
});
}
let progressCount = 1000;
let count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
displayData();
return;
}
let userPromises = users.map(updateUser);
let lastUser = users[users.length - 1];
return Promise.all(userPromises)
.then(() => {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
if (!user.auth.local.username) {
const newName = generateUsername();
dbUsers.update(
{_id: user._id},
{$set:
{
'auth.local.username': newName,
'auth.local.lowerCaseUsername': newName,
},
}
);
}
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,107 @@
const MIGRATION_NAME = '20181003_username_email.js';
let authorName = 'Sabe'; // in case script author needs to know when their ...
let authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
/*
* Send emails to eligible users announcing upcoming username changes
*/
import monk from 'monk';
import nconf from 'nconf';
import { sendTxn } from '../../website/server/libs/email';
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING');
let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false });
function processUsers (lastId) {
// specify a query to limit the affected users (empty for all users):
let query = {
migration: {$ne: MIGRATION_NAME},
'auth.timestamps.loggedin': {$gt: new Date('2018-04-01')},
};
if (lastId) {
query._id = {
$gt: lastId,
};
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 100,
fields: [
'_id',
'auth',
'preferences',
'profile',
], // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
})
.then(updateUsers)
.catch((err) => {
console.log(err);
return exiting(1, `ERROR! ${ err}`);
});
}
let progressCount = 1000;
let count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
displayData();
return;
}
let userPromises = users.map(updateUser);
let lastUser = users[users.length - 1];
return Promise.all(userPromises)
.then(() => delay(7000))
.then(() => {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
dbUsers.update({_id: user._id}, {$set: {migration: MIGRATION_NAME}});
sendTxn(
user,
'username-change',
[{name: 'UNSUB_EMAIL_TYPE_URL', content: '/user/settings/notifications?unsubFrom=importantAnnouncements'},
{name: 'LOGIN_NAME', content: user.auth.local.username}]
);
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 delay (t, v) {
return new Promise(function batchPause (resolve) {
setTimeout(resolve.bind(null, v), t);
});
}
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,66 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20181023_veteran_pet_ladder';
import { model as User } from '../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
const set = {};
set.migration = MIGRATION_NAME;
if (user.items.pets['Bear-Veteran']) {
set['items.pets.Fox-Veteran'] = 5;
} else if (user.items.pets['Lion-Veteran']) {
set['items.pets.Bear-Veteran'] = 5;
} else if (user.items.pets['Tiger-Veteran']) {
set['items.pets.Lion-Veteran'] = 5;
} else if (user.items.pets['Wolf-Veteran']) {
set['items.pets.Tiger-Veteran'] = 5;
} else {
set['items.pets.Wolf-Veteran'] = 5;
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.update({_id: user._id}, {$set: set}).exec();
}
module.exports = async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
'flags.verifiedUsername': true,
};
const fields = {
_id: 1,
items: 1,
migration: 1,
flags: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.lean()
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1],
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -0,0 +1,116 @@
/*
* Award Habitoween ladder items to participants in this month's Habitoween festivities
*/
import monk from 'monk';
import nconf from 'nconf';
const MIGRATION_NAME = '20181030_habitoween_ladder.js'; // Update when running in future years
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING');
const AUTHOR_NAME = 'Sabe'; // in case script author needs to know when their ...
const AUTHOR_UUID = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false });
function processUsers (lastId) {
// specify a query to limit the affected users (empty for all users):
let query = {
migration: {$ne: MIGRATION_NAME},
'auth.timestamps.loggedin': {$gt: new Date('2018-10-01')},
};
if (lastId) {
query._id = {
$gt: lastId,
};
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 250,
fields: [
'items.mounts',
'items.pets',
], // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
})
.then(updateUsers)
.catch((err) => {
console.log(err);
return exiting(1, `ERROR! ${err}`);
});
}
const PROGRESS_COUNT = 1000;
let count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
displayData();
return;
}
let userPromises = users.map(updateUser);
let lastUser = users[users.length - 1];
return Promise.all(userPromises)
.then(() => {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
let set = {};
let inc = {
'items.food.Candy_Skeleton': 1,
'items.food.Candy_Base': 1,
'items.food.Candy_CottonCandyBlue': 1,
'items.food.Candy_CottonCandyPink': 1,
'items.food.Candy_Shade': 1,
'items.food.Candy_White': 1,
'items.food.Candy_Golden': 1,
'items.food.Candy_Zombie': 1,
'items.food.Candy_Desert': 1,
'items.food.Candy_Red': 1,
};
if (user && user.items && user.items.pets && user.items.mounts['JackOLantern-Ghost']) {
set['items.pets.JackOLantern-Glow'] = 5;
} else if (user && user.items && user.items.pets && user.items.pets['JackOLantern-Ghost']) {
set['items.mounts.JackOLantern-Ghost'] = true;
} else if (user && user.items && user.items.mounts && user.items.mounts['JackOLantern-Base']) {
set['items.pets.JackOLantern-Ghost'] = 5;
} else if (user && user.items && user.items.pets && user.items.pets['JackOLantern-Base']) {
set['items.mounts.JackOLantern-Base'] = true;
} else {
set['items.pets.JackOLantern-Base'] = 5;
}
dbUsers.update({_id: user._id}, {$set: set, $inc: inc});
if (count % PROGRESS_COUNT === 0) console.warn(`${count} ${user._id}`);
if (user._id === AUTHOR_UUID) console.warn(`${AUTHOR_NAME} 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 @@
const MIGRATION_NAME = '20181108_username_email.js';
const AUTHOR_NAME = 'Sabe'; // in case script author needs to know when their ...
const AUTHOR_UUID = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
/*
* Send emails to eligible users announcing upcoming username changes
*/
import monk from 'monk';
import nconf from 'nconf';
import { sendTxn } from '../../../website/server/libs/email';
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING');
const BASE_URL = nconf.get('BASE_URL');
let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false });
function processUsers (lastId) {
// specify a query to limit the affected users (empty for all users):
let query = {
migration: {$ne: MIGRATION_NAME},
'flags.verifiedUsername': {$ne: true},
'auth.timestamps.loggedin': {$gt: new Date('2018-10-25')},
};
if (lastId) {
query._id = {
$gt: lastId,
};
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 100,
fields: [
'_id',
'auth',
'preferences',
'profile',
], // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
})
.then(updateUsers)
.catch((err) => {
console.log(err);
return exiting(1, `ERROR! ${ err}`);
});
}
let progressCount = 1000;
let count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
displayData();
return;
}
let userPromises = users.map(updateUser);
let lastUser = users[users.length - 1];
return Promise.all(userPromises)
.then(() => delay(7000))
.then(() => {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
dbUsers.update({_id: user._id}, {$set: {migration: MIGRATION_NAME}});
sendTxn(
user,
'username-change-follow-up',
[{name: 'LOGIN_NAME', content: user.auth.local.username},
{name: 'BASE_URL', content: BASE_URL}]
);
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
if (user._id === AUTHOR_UUID) console.warn(`${AUTHOR_NAME} processed`);
}
function displayData () {
console.warn(`\n${count} users processed\n`);
return exiting(0);
}
function delay (t, v) {
return new Promise(function batchPause (resolve) {
setTimeout(resolve.bind(null, v), t);
});
}
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 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20181122_turkey_day';
import mongoose from 'mongoose';
import { model as User } from '../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
const set = {};
let push;
set.migration = MIGRATION_NAME;
if (typeof user.items.gear.owned.armor_special_turkeyArmorBase !== 'undefined') {
set['items.gear.owned.head_special_turkeyHelmGilded'] = false;
set['items.gear.owned.armor_special_turkeyArmorGilded'] = false;
set['items.gear.owned.back_special_turkeyTailGilded'] = false;
push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_turkeyHelmGilded',
_id: new mongoose.Types.ObjectId(),
},
{
type: 'marketGear',
path: 'gear.flat.armor_special_turkeyArmorGilded',
_id: new mongoose.Types.ObjectId(),
},
{
type: 'marketGear',
path: 'gear.flat.back_special_turkeyTailGilded',
_id: new mongoose.Types.ObjectId(),
},
];
} else if (user.items && user.items.mounts && user.items.mounts['Turkey-Gilded']) {
set['items.gear.owned.head_special_turkeyHelmBase'] = false;
set['items.gear.owned.armor_special_turkeyArmorBase'] = false;
set['items.gear.owned.back_special_turkeyTailBase'] = false;
push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_turkeyHelmBase',
_id: new mongoose.Types.ObjectId(),
},
{
type: 'marketGear',
path: 'gear.flat.armor_special_turkeyArmorBase',
_id: new mongoose.Types.ObjectId(),
},
{
type: 'marketGear',
path: 'gear.flat.back_special_turkeyTailBase',
_id: new mongoose.Types.ObjectId(),
},
];
} else if (user.items && user.items.pets && user.items.pets['Turkey-Gilded']) {
set['items.mounts.Turkey-Gilded'] = true;
} else if (user.items && user.items.mounts && user.items.mounts['Turkey-Base']) {
set['items.pets.Turkey-Gilded'] = 5;
} else if (user.items && user.items.pets && user.items.pets['Turkey-Base']) {
set['items.mounts.Turkey-Base'] = true;
} else {
set['items.pets.Turkey-Base'] = 5;
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
if (push) {
return await User.update({_id: user._id}, {$set: set, $push: {pinnedItems: {$each: push}}}).exec();
} else {
return await User.update({_id: user._id}, {$set: set}).exec();
}
}
module.exports = async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
};
const fields = {
_id: 1,
items: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.lean()
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1],
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -0,0 +1,110 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20181231_nye';
import { model as User } from '../../../website/server/models/user';
import mongoose from 'mongoose';
import { v4 as uuid } from 'uuid';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
const set = {'flags.newStuff': true};
let push;
set.migration = MIGRATION_NAME;
if (typeof user.items.gear.owned.head_special_nye2017 !== 'undefined') {
set['items.gear.owned.head_special_nye2018'] = false;
push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_nye2018',
_id: uuid(),
},
];
} else if (typeof user.items.gear.owned.head_special_nye2016 !== 'undefined') {
set['items.gear.owned.head_special_nye2017'] = false;
push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_nye2017',
_id: uuid(),
},
];
} else if (typeof user.items.gear.owned.head_special_nye2015 !== 'undefined') {
set['items.gear.owned.head_special_nye2016'] = false;
push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_nye2016',
_id: uuid(),
},
];
} else if (typeof user.items.gear.owned.head_special_nye2014 !== 'undefined') {
set['items.gear.owned.head_special_nye2015'] = false;
push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_nye2015',
_id: uuid(),
},
];
} else if (typeof user.items.gear.owned.head_special_nye !== 'undefined') {
set['items.gear.owned.head_special_nye2014'] = false;
push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_nye2014',
_id: uuid(),
},
];
} else {
set['items.gear.owned.head_special_nye'] = false;
push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_nye',
_id: uuid(),
},
];
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.update({_id: user._id}, {$set: set, $push: {pinnedItems: {$each: push}}}).exec();
}
module.exports = async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
};
const fields = {
_id: 1,
items: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.lean()
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1],
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -0,0 +1,110 @@
import monk from 'monk';
import nconf from 'nconf';
const migrationName = 'mystery-items-201808.js'; // Update per month
const authorName = 'Sabe'; // in case script author needs to know when their ...
const authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
/*
* Award this month's mystery items to subscribers
*/
const MYSTERY_ITEMS = ['armor_mystery_201810', 'head_mystery_201810'];
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING');
let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false });
let UserNotification = require('../../website/server/models/userNotification').model;
function processUsers (lastId) {
// specify a query to limit the affected users (empty for all users):
let query = {
migration: {$ne: migrationName},
'purchased.plan.customerId': { $ne: null },
$or: [
{ 'purchased.plan.dateTerminated': { $gte: new Date() } },
{ 'purchased.plan.dateTerminated': { $exists: false } },
{ 'purchased.plan.dateTerminated': { $eq: null } },
],
};
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((err) => {
console.log(err);
return exiting(1, `ERROR! ${ err}`);
});
}
let progressCount = 1000;
let count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
displayData();
return;
}
let userPromises = users.map(updateUser);
let lastUser = users[users.length - 1];
return Promise.all(userPromises)
.then(() => {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
const addToSet = {
'purchased.plan.mysteryItems': {
$each: MYSTERY_ITEMS,
},
};
const push = {
notifications: (new UserNotification({
type: 'NEW_MYSTERY_ITEMS',
data: {
MYSTERY_ITEMS,
},
})).toJSON(),
};
dbUsers.update({_id: user._id}, {$addToSet: addToSet, $push: push});
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

@@ -1,4 +1,4 @@
let migrationName = '20180702_takeThis.js'; // Update per month
let migrationName = '20180904_takeThis.js'; // Update per month
let authorName = 'Sabe'; // in case script author needs to know when their ...
let authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
@@ -15,7 +15,7 @@ function processUsers (lastId) {
// specify a query to limit the affected users (empty for all users):
let query = {
migration: {$ne: migrationName},
challenges: {$in: ['f0481f95-1dde-4ae7-a876-d19502a45d61']}, // Update per month
challenges: {$in: ['1044ec0c-4a85-48c5-9f36-d51c0c62c7d3']}, // Update per month
};
if (lastId) {

View File

@@ -17,5 +17,12 @@ function setUpServer () {
setUpServer();
// Replace this with your migration
const processUsers = require('./tasks/habits-one-history-entry-per-day-challenges.js');
processUsers();
const processUsers = require('./archive/2018/20181231_nye.js');
processUsers()
.then(function success () {
process.exit(0);
})
.catch(function failure (err) {
console.log(err);
process.exit(1);
});

View File

@@ -0,0 +1,61 @@
/* eslint-disable no-console */
import { sendTxn } from '../../../website/server/libs/email';
import { model as User } from '../../website/server/models/user';
import moment from 'moment';
import nconf from 'nconf';
const BASE_URL = nconf.get('BASE_URL');
const EMAIL_SLUG = 'mandrill-email-slug'; // Set email template to send
const MIGRATION_NAME = 'bulk-email';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
sendTxn(
user,
EMAIL_SLUG,
[{name: 'BASE_URL', content: BASE_URL}] // Add variables from template
);
return await User.update({_id: user._id}, {$set: {migration: MIGRATION_NAME}}).exec();
}
module.exports = async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
'auth.timestamps.loggedin': {$gt: moment().subtract(2, 'weeks').toDate()}, // customize or remove to target different populations
};
const fields = {
_id: 1,
auth: 1,
preferences: 1,
profile: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.lean()
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1],
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -1,70 +1,13 @@
import monk from 'monk';
import nconf from 'nconf';
/* eslint-disable no-console */
const MIGRATION_NAME = 'mystery_items_201812';
const MYSTERY_ITEMS = ['headAccessory_mystery_201812', 'back_mystery_201812'];
import { model as User } from '../../website/server/models/user';
import { model as UserNotification } from '../../website/server/models/userNotification';
const migrationName = 'mystery-items-201806.js'; // Update per month
const authorName = 'Sabe'; // in case script author needs to know when their ...
const authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
/*
* Award this month's mystery items to subscribers
*/
const MYSTERY_ITEMS = ['armor_mystery_201806', 'head_mystery_201806'];
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING');
let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false });
let UserNotification = require('../../website/server/models/userNotification').model;
function processUsers (lastId) {
// specify a query to limit the affected users (empty for all users):
let query = {
migration: {$ne: migrationName},
'purchased.plan.customerId': { $ne: null },
$or: [
{ 'purchased.plan.dateTerminated': { $gte: new Date() } },
{ 'purchased.plan.dateTerminated': { $exists: false } },
{ 'purchased.plan.dateTerminated': { $eq: null } },
],
};
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((err) => {
console.log(err);
return exiting(1, `ERROR! ${ err}`);
});
}
let progressCount = 1000;
const progressCount = 1000;
let count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
displayData();
return;
}
let userPromises = users.map(updateUser);
let lastUser = users[users.length - 1];
return Promise.all(userPromises)
.then(() => {
processUsers(lastUser._id);
});
}
function updateUser (user) {
async function updateUser (user) {
count++;
const addToSet = {
@@ -80,31 +23,49 @@ function updateUser (user) {
},
})).toJSON(),
};
const set = {
migration: MIGRATION_NAME,
};
dbUsers.update({_id: user._id}, {$addToSet: addToSet, $push: push});
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`);
return await User.update({_id: user._id}, {$set: set, $push: push, $addToSet: addToSet}).exec();
}
function displayData () {
console.warn(`\n${ count } users processed\n`);
return exiting(0);
}
module.exports = async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
'purchased.plan.customerId': { $ne: null },
$or: [
{ 'purchased.plan.dateTerminated': { $gte: new Date() } },
{ 'purchased.plan.dateTerminated': { $exists: false } },
{ 'purchased.plan.dateTerminated': { $eq: null } },
],
};
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);
const fields = {
_id: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.lean()
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1],
};
}
}
process.exit(code);
}
module.exports = processUsers;
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -0,0 +1,81 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20181203_take_this';
import { v4 as uuid } from 'uuid';
import { model as User } from '../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
const set = {};
let push;
set.migration = MIGRATION_NAME;
if (typeof user.items.gear.owned.back_special_takeThis !== 'undefined') {
push = false;
} else if (typeof user.items.gear.owned.body_special_takeThis !== 'undefined') {
set['items.gear.owned.back_special_takeThis'] = false;
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.back_special_takeThis', _id: uuid()}};
} else if (typeof user.items.gear.owned.head_special_takeThis !== 'undefined') {
set['items.gear.owned.body_special_takeThis'] = false;
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.body_special_takeThis', _id: uuid()}};
} else if (typeof user.items.gear.owned.armor_special_takeThis !== 'undefined') {
set['items.gear.owned.head_special_takeThis'] = false;
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.head_special_takeThis', _id: uuid()}};
} else if (typeof user.items.gear.owned.weapon_special_takeThis !== 'undefined') {
set['items.gear.owned.armor_special_takeThis'] = false;
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.armor_special_takeThis', _id: uuid()}};
} else if (typeof user.items.gear.owned.shield_special_takeThis !== 'undefined') {
set['items.gear.owned.weapon_special_takeThis'] = false;
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.weapon_special_takeThis', _id: uuid()}};
} else {
set['items.gear.owned.shield_special_takeThis'] = false;
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.shield_special_takeThis', _id: uuid()}};
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
if (push) {
return await User.update({_id: user._id}, {$set: set, $push: push}).exec();
} else {
return await User.update({_id: user._id}, {$set: set}).exec();
}
}
module.exports = async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
challenges: '00708425-d477-41a5-bf27-6270466e7976',
};
const fields = {
_id: 1,
items: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.lean()
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1],
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

18757
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "4.52.0",
"version": "4.80.7",
"main": "./website/server/index.js",
"dependencies": {
"@slack/client": "^3.8.1",
@@ -9,9 +9,9 @@
"amazon-payments": "^0.2.7",
"amplitude": "^3.5.0",
"apidoc": "^0.17.5",
"autoprefixer": "^8.5.0",
"aws-sdk": "^2.239.1",
"apn": "^2.2.0",
"autoprefixer": "^8.5.0",
"aws-sdk": "^2.329.0",
"axios": "^0.18.0",
"axios-progress-bar": "^1.2.0",
"babel-core": "^6.26.3",
@@ -26,16 +26,16 @@
"babel-preset-es2015": "^6.6.0",
"babel-register": "^6.6.0",
"babel-runtime": "^6.11.6",
"bcrypt": "^2.0.0",
"bcrypt": "^3.0.1",
"body-parser": "^1.18.3",
"bootstrap": "^4.1.1",
"bootstrap-vue": "^2.0.0-rc.9",
"compression": "^1.7.2",
"cookie-session": "^1.2.0",
"coupon-code": "^0.4.5",
"cross-env": "^5.1.5",
"cross-env": "^5.2.0",
"css-loader": "^0.28.11",
"csv-stringify": "^2.1.0",
"csv-stringify": "^4.3.1",
"cwait": "^1.1.1",
"domain-middleware": "~0.1.0",
"express": "^4.16.3",
@@ -43,32 +43,32 @@
"express-validator": "^5.2.0",
"extract-text-webpack-plugin": "^3.0.2",
"glob": "^7.1.2",
"got": "^8.3.1",
"got": "^9.0.0",
"gulp": "^4.0.0",
"gulp-babel": "^7.0.1",
"gulp-imagemin": "^4.1.0",
"gulp-nodemon": "^2.2.1",
"gulp-imagemin": "^5.0.3",
"gulp-nodemon": "^2.4.1",
"gulp.spritesmith": "^6.9.0",
"habitica-markdown": "^1.3.0",
"hellojs": "^1.15.1",
"html-webpack-plugin": "^3.2.0",
"image-size": "^0.6.2",
"in-app-purchase": "^1.9.4",
"in-app-purchase": "^1.10.2",
"intro.js": "^2.9.3",
"jquery": ">=3.0.0",
"js2xmlparser": "^3.0.0",
"lodash": "^4.17.10",
"merge-stream": "^1.0.0",
"method-override": "^2.3.5",
"method-override": "^3.0.0",
"moment": "^2.22.1",
"moment-recur": "^1.0.7",
"mongoose": "^5.1.2",
"mongoose": "^5.3.4",
"morgan": "^1.7.0",
"nconf": "^0.10.0",
"node-gcm": "^0.14.4",
"node-gcm": "^1.0.2",
"node-sass": "^4.9.0",
"nodemailer": "^4.6.4",
"ora": "^2.1.0",
"ora": "^3.0.0",
"pageres": "^4.1.1",
"passport": "^0.4.0",
"passport-facebook": "^2.0.0",
@@ -79,23 +79,24 @@
"postcss-easy-import": "^3.0.0",
"ps-tree": "^1.0.0",
"pug": "^2.0.3",
"pusher": "^1.3.0",
"rimraf": "^2.4.3",
"sass-loader": "^7.0.0",
"sass-loader": "^7.0.3",
"shelljs": "^0.8.2",
"short-uuid": "^3.0.0",
"smartbanner.js": "^1.9.1",
"stripe": "^5.9.0",
"superagent": "^3.8.3",
"superagent": "^4.0.0",
"svg-inline-loader": "^0.8.0",
"svg-url-loader": "^2.3.2",
"svgo": "^1.0.5",
"svgo-loader": "^2.1.0",
"universal-analytics": "^0.4.16",
"universal-analytics": "^0.4.17",
"update": "^0.7.4",
"upgrade": "^1.1.0",
"url-loader": "^1.0.0",
"useragent": "^2.1.9",
"uuid": "^3.0.1",
"validator": "^9.4.1",
"validator": "^10.5.0",
"vinyl-buffer": "^1.0.1",
"vue": "^2.5.16",
"vue-loader": "^14.2.2",
@@ -106,15 +107,15 @@
"vuedraggable": "^2.15.0",
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#5d237615463a84a23dd6f3f77c6ab577d68593ec",
"webpack": "^3.12.0",
"webpack-merge": "^4.0.0",
"winston": "^2.4.2",
"webpack-merge": "^4.1.3",
"winston": "^2.4.3",
"winston-loggly-bulk": "^2.0.2",
"xml2js": "^0.4.4"
},
"private": true,
"engines": {
"node": "^8.9.4",
"npm": "^5.6.0"
"node": "^10",
"npm": "^6"
},
"scripts": {
"lint": "eslint --ext .js,.vue .",
@@ -143,13 +144,13 @@
"apidoc": "gulp apidoc"
},
"devDependencies": {
"@vue/test-utils": "^1.0.0-beta.16",
"@vue/test-utils": "^1.0.0-beta.19",
"babel-plugin-istanbul": "^4.1.6",
"babel-plugin-syntax-object-rest-spread": "^6.13.0",
"chai": "^4.1.2",
"chai-as-promised": "^7.1.1",
"chalk": "^2.4.1",
"chromedriver": "^2.38.3",
"chromedriver": "^2.40.0",
"connect-history-api-fallback": "^1.1.0",
"coveralls": "^3.0.1",
"cross-spawn": "^6.0.5",
@@ -161,36 +162,33 @@
"eslint-plugin-mocha": "^5.0.0",
"eventsource-polyfill": "^0.9.6",
"expect.js": "^0.3.1",
"http-proxy-middleware": "^0.18.0",
"http-proxy-middleware": "^0.19.0",
"istanbul": "^1.1.0-alpha.1",
"karma": "^2.0.2",
"karma": "^3.1.3",
"karma-babel-preprocessor": "^7.0.0",
"karma-chai-plugins": "^0.9.0",
"karma-chrome-launcher": "^2.2.0",
"karma-coverage": "^1.1.2",
"karma-mocha": "^1.3.0",
"karma-mocha-reporter": "^2.2.5",
"karma-sinon-chai": "^1.3.4",
"karma-sinon-chai": "^2.0.0",
"karma-sinon-stub-promise": "^1.0.0",
"karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "0.0.32",
"karma-webpack": "^3.0.0",
"lcov-result-merger": "^2.0.0",
"lcov-result-merger": "^3.0.0",
"mocha": "^5.1.1",
"monk": "^6.0.6",
"nightwatch": "^0.9.21",
"puppeteer": "^1.4.0",
"puppeteer": "^1.5.0",
"require-again": "^2.0.0",
"selenium-server": "^3.12.0",
"sinon": "^4.5.0",
"sinon": "^6.3.5",
"sinon-chai": "^3.0.0",
"sinon-stub-promise": "^4.0.0",
"webpack-bundle-analyzer": "^2.12.0",
"webpack-dev-middleware": "^2.0.5",
"webpack-hot-middleware": "^2.22.2"
},
"optionalDependencies": {
"memwatch-next": "^0.3.0",
"node-rdkafka": "^2.3.0"
}
"optionalDependencies": {}
}

View File

@@ -0,0 +1,89 @@
/* eslint-disable no-console */
import axios from 'axios';
import { model as User } from '../website/server/models/user';
import nconf from 'nconf';
const AMPLITUDE_KEY = nconf.get('AMPLITUDE_KEY');
const AMPLITUDE_SECRET = nconf.get('AMPLITUDE_SECRET');
const BASE_URL = nconf.get('BASE_URL');
async function _deleteAmplitudeData (userId, email) {
const response = await axios.post(
'https://amplitude.com/api/2/deletions/users',
{
user_ids: userId, // eslint-disable-line camelcase
requester: email,
},
{
auth: {
username: AMPLITUDE_KEY,
password: AMPLITUDE_SECRET,
},
}
).catch((err) => {
console.log(err.response.data);
});
if (response) console.log(`${response.status} ${response.statusText}`);
}
async function _deleteHabiticaData (user, email) {
await User.update(
{_id: user._id},
{$set: {
'auth.local.email': email,
'auth.local.hashed_password': '$2a$10$QDnNh1j1yMPnTXDEOV38xOePEWFd4X8DSYwAM8XTmqmacG5X0DKjW',
'auth.local.passwordHashMethod': 'bcrypt',
}}
);
const response = await axios.delete(
`${BASE_URL}/api/v3/user`,
{
data: {
password: 'test',
},
headers: {
'x-api-user': user._id,
'x-api-key': user.apiToken,
},
}
).catch((err) => {
console.log(err.response.data);
});
if (response) {
console.log(`${response.status} ${response.statusText}`);
if (response.status === 200) console.log(`${user._id} removed. Last login: ${user.auth.timestamps.loggedin}`);
}
}
async function _processEmailAddress (email) {
const emailRegex = new RegExp(`^${email}`, 'i');
const users = await User.find({
$or: [
{'auth.local.email': emailRegex},
{'auth.facebook.emails.value': emailRegex},
{'auth.google.emails.value': emailRegex},
]},
{
_id: 1,
apiToken: 1,
auth: 1,
}).exec();
if (users.length < 1) {
console.log(`No users found with email address ${email}`);
} else {
for (const user of users) {
await _deleteAmplitudeData(user._id, email); // eslint-disable-line no-await-in-loop
await _deleteHabiticaData(user, email); // eslint-disable-line no-await-in-loop
}
}
}
function deleteUserData (emails) {
const emailPromises = emails.map(_processEmailAddress);
return Promise.all(emailPromises);
}
module.exports = deleteUserData;

View File

@@ -12,28 +12,27 @@ const nconf = require('nconf');
const _ = require('lodash');
const paypal = require('paypal-rest-sdk');
const blocks = require('../website/common').content.subscriptionBlocks;
const live = nconf.get('PAYPAL:mode') === 'live';
const live = nconf.get('PAYPAL_MODE') === 'live';
nconf.argv().env().file('user', path.join(path.resolve(__dirname, '../config.json')));
let OP = 'create'; // list create update remove
let OP = 'create'; // list get update create create-webprofile
paypal.configure({
mode: nconf.get('PAYPAL:mode'), // sandbox or live
client_id: nconf.get('PAYPAL:client_id'),
client_secret: nconf.get('PAYPAL:client_secret'),
mode: nconf.get('PAYPAL_MODE'), // sandbox or live
client_id: nconf.get('PAYPAL_CLIENT_ID'),
client_secret: nconf.get('PAYPAL_CLIENT_SECRET'),
});
// https://developer.paypal.com/docs/api/#billing-plans-and-agreements
let billingPlanTitle = 'Habitica Subscription';
let billingPlanAttributes = {
name: billingPlanTitle,
description: billingPlanTitle,
type: 'INFINITE',
merchant_preferences: {
auto_bill_amount: 'yes',
cancel_url: live ? 'https://habitica.com' : 'http://localhost:3000',
return_url: `${live ? 'https://habitica.com' : 'http://localhost:3000' }/paypal/subscribe/success`,
return_url: `${live ? 'https://habitica.com' : 'http://localhost:3000'}/paypal/subscribe/success`,
},
payment_definitions: [{
type: 'REGULAR',
@@ -45,7 +44,7 @@ let billingPlanAttributes = {
_.each(blocks, (block) => {
block.definition = _.cloneDeep(billingPlanAttributes);
_.merge(block.definition.payment_definitions[0], {
name: `${billingPlanTitle } ($${block.price} every ${block.months} months, recurring)`,
name: `${billingPlanTitle} ($${block.price} every ${block.months} months, recurring)`,
frequency_interval: `${block.months}`,
amount: {
currency: 'USD',
@@ -63,7 +62,7 @@ switch (OP) {
});
break;
case 'get':
paypal.billingPlan.get(nconf.get('PAYPAL:billing_plans:12'), (err, plan) => {
paypal.billingPlan.get(nconf.get('PAYPAL_BILLING_PLANS_basic_12mo'), (err, plan) => {
console.log({err, plan});
});
break;
@@ -75,7 +74,7 @@ switch (OP) {
cancel_url: 'https://habitica.com',
},
};
paypal.billingPlan.update(nconf.get('PAYPAL:billing_plans:12'), updatePayload, (err, res) => {
paypal.billingPlan.update(nconf.get('PAYPAL_BILLING_PLANS_basic_12mo'), updatePayload, (err, res) => {
console.log({err, plan: res});
});
break;
@@ -101,9 +100,6 @@ switch (OP) {
});
});
break;
case 'remove': break;
case 'create-webprofile':
let webexpinfo = {
name: 'HabiticaProfile',
@@ -116,4 +112,4 @@ switch (OP) {
console.log(error, result);
});
break;
}
}

View File

@@ -5,10 +5,17 @@ describe('Base model plugin', () => {
let schema;
beforeEach(() => {
schema = new mongoose.Schema();
schema = new mongoose.Schema({}, {
typeKey: '$type',
});
sandbox.stub(schema, 'add');
});
it('throws if "typeKey" is not set to $type', () => {
const schemaWithoutTypeKey = new mongoose.Schema();
expect(() => schemaWithoutTypeKey.plugin(baseModel)).to.throw;
});
it('adds a _id field to the schema', () => {
schema.plugin(baseModel);

View File

@@ -65,6 +65,12 @@ describe('cron', () => {
expect(analytics.track.callCount).to.equal(1);
});
it('calls analytics when user is sleeping', () => {
user.preferences.sleep = true;
cron({user, tasksByType, daysMissed, analytics});
expect(analytics.track.callCount).to.equal(1);
});
describe('end of the month perks', () => {
beforeEach(() => {
user.purchased.plan.customerId = 'subscribedId';
@@ -655,76 +661,6 @@ describe('cron', () => {
});
});
describe('user is sleeping', () => {
beforeEach(() => {
user.preferences.sleep = true;
});
it('calls analytics', () => {
cron({user, tasksByType, daysMissed, analytics});
expect(analytics.track.callCount).to.equal(1);
});
it('clears user buffs', () => {
user.stats.buffs = {
str: 1,
int: 1,
per: 1,
con: 1,
stealth: 1,
streaks: true,
};
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.buffs.str).to.equal(0);
expect(user.stats.buffs.int).to.equal(0);
expect(user.stats.buffs.per).to.equal(0);
expect(user.stats.buffs.con).to.equal(0);
expect(user.stats.buffs.stealth).to.equal(0);
expect(user.stats.buffs.streaks).to.be.false;
});
it('resets all dailies without damaging user', () => {
let daily = {
text: 'test daily',
type: 'daily',
frequency: 'daily',
everyX: 5,
startDate: new Date(),
};
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line new-cap
tasksByType.dailys.push(task);
tasksByType.dailys[0].completed = true;
let healthBefore = user.stats.hp;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].completed).to.be.false;
expect(user.stats.hp).to.equal(healthBefore);
});
it('sets isDue for daily', () => {
let daily = {
text: 'test daily',
type: 'daily',
frequency: 'daily',
everyX: 5,
startDate: new Date(),
};
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line new-cap
tasksByType.dailys.push(task);
tasksByType.dailys[0].completed = true;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].isDue).to.be.exist;
});
});
describe('todos', () => {
beforeEach(() => {
let todo = {
@@ -846,6 +782,15 @@ describe('cron', () => {
expect(tasksByType.dailys[0].isDue).to.be.false;
});
it('computes isDue when user is sleeping', () => {
user.preferences.sleep = true;
tasksByType.dailys[0].frequency = 'daily';
tasksByType.dailys[0].everyX = 5;
tasksByType.dailys[0].startDate = moment().toDate();
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].isDue).to.exist;
});
it('computes nextDue', () => {
tasksByType.dailys[0].frequency = 'daily';
tasksByType.dailys[0].everyX = 5;
@@ -865,6 +810,13 @@ describe('cron', () => {
expect(tasksByType.dailys[0].completed).to.be.false;
});
it('should set tasks completed to false when user is sleeping', () => {
user.preferences.sleep = true;
tasksByType.dailys[0].completed = true;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].completed).to.be.false;
});
it('should reset task checklist for completed dailys', () => {
tasksByType.dailys[0].checklist.push({title: 'test', completed: false});
tasksByType.dailys[0].completed = true;
@@ -872,6 +824,14 @@ describe('cron', () => {
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
});
it('should reset task checklist for completed dailys when user is sleeping', () => {
user.preferences.sleep = true;
tasksByType.dailys[0].checklist.push({title: 'test', completed: false});
tasksByType.dailys[0].completed = true;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
});
it('should reset task checklist for dailys with scheduled misses', () => {
daysMissed = 10;
tasksByType.dailys[0].checklist.push({title: 'test', completed: false});
@@ -884,12 +844,19 @@ describe('cron', () => {
daysMissed = 1;
let hpBefore = user.stats.hp;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.hp).to.be.lessThan(hpBefore);
});
it('should not do damage for missing a daily when user is sleeping', () => {
user.preferences.sleep = true;
daysMissed = 1;
let hpBefore = user.stats.hp;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.hp).to.equal(hpBefore);
});
it('should not do damage for missing a daily when CRON_SAFE_MODE is set', () => {
sandbox.stub(nconf, 'get').withArgs('CRON_SAFE_MODE').returns('true');
let cronOverride = requireAgain(pathToCronLib).cron;
@@ -930,7 +897,7 @@ describe('cron', () => {
expect(hpDifferenceOfPartiallyIncompleteDaily).to.be.lessThan(hpDifferenceOfFullyIncompleteDaily);
});
it('should decrement quest progress down for missing a daily', () => {
it('should decrement quest.progress.down for missing a daily', () => {
daysMissed = 1;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
@@ -939,6 +906,16 @@ describe('cron', () => {
expect(progress.down).to.equal(-1);
});
it('should not decrement quest.progress.down for missing a daily when user is sleeping', () => {
user.preferences.sleep = true;
daysMissed = 1;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
let progress = cron({user, tasksByType, daysMissed, analytics});
expect(progress.down).to.equal(0);
});
it('should do damage for only yesterday\'s dailies', () => {
daysMissed = 3;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
@@ -1017,7 +994,7 @@ describe('cron', () => {
expect(tasksByType.habits[0].counterDown).to.equal(0);
});
it('should reset habit counters even if user is resting in the Inn', () => {
it('should reset habit counters even if user is sleeping', () => {
user.preferences.sleep = true;
tasksByType.habits[0].counterUp = 1;
tasksByType.habits[0].counterDown = 1;
@@ -1278,7 +1255,23 @@ describe('cron', () => {
expect(user.achievements.perfect).to.equal(0);
});
it('increments user buffs if all (at least 1) due dailies were completed', () => {
it('gives perfect day buff if all (at least 1) due dailies were completed', () => {
daysMissed = 1;
tasksByType.dailys[0].completed = true;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
let previousBuffs = user.stats.buffs.toObject();
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
expect(user.stats.buffs.int).to.be.greaterThan(previousBuffs.int);
expect(user.stats.buffs.per).to.be.greaterThan(previousBuffs.per);
expect(user.stats.buffs.con).to.be.greaterThan(previousBuffs.con);
});
it('gives perfect day buff if all (at least 1) due dailies were completed when user is sleeping', () => {
user.preferences.sleep = true;
daysMissed = 1;
tasksByType.dailys[0].completed = true;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
@@ -1317,6 +1310,31 @@ describe('cron', () => {
expect(user.stats.buffs.streaks).to.be.false;
});
it('clears buffs if user does not have a perfect day (no due dailys) when user is sleeping', () => {
user.preferences.sleep = true;
daysMissed = 1;
tasksByType.dailys[0].completed = true;
tasksByType.dailys[0].startDate = moment(new Date()).add({days: 1});
user.stats.buffs = {
str: 1,
int: 1,
per: 1,
con: 1,
stealth: 0,
streaks: true,
};
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.buffs.str).to.equal(0);
expect(user.stats.buffs.int).to.equal(0);
expect(user.stats.buffs.per).to.equal(0);
expect(user.stats.buffs.con).to.equal(0);
expect(user.stats.buffs.stealth).to.equal(0);
expect(user.stats.buffs.streaks).to.be.false;
});
it('clears buffs if user does not have a perfect day (at least one due daily not completed)', () => {
daysMissed = 1;
tasksByType.dailys[0].completed = false;
@@ -1341,7 +1359,50 @@ describe('cron', () => {
expect(user.stats.buffs.streaks).to.be.false;
});
it('still grants a perfect day when CRON_SAFE_MODE is set', () => {
it('clears buffs if user does not have a perfect day (at least one due daily not completed) when user is sleeping', () => {
user.preferences.sleep = true;
daysMissed = 1;
tasksByType.dailys[0].completed = false;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
user.stats.buffs = {
str: 1,
int: 1,
per: 1,
con: 1,
stealth: 0,
streaks: true,
};
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.buffs.str).to.equal(0);
expect(user.stats.buffs.int).to.equal(0);
expect(user.stats.buffs.per).to.equal(0);
expect(user.stats.buffs.con).to.equal(0);
expect(user.stats.buffs.stealth).to.equal(0);
expect(user.stats.buffs.streaks).to.be.false;
});
it('always grants a perfect day buff when CRON_SAFE_MODE is set', () => {
sandbox.stub(nconf, 'get').withArgs('CRON_SAFE_MODE').returns('true');
let cronOverride = requireAgain(pathToCronLib).cron;
daysMissed = 1;
tasksByType.dailys[0].completed = false;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
let previousBuffs = user.stats.buffs.toObject();
cronOverride({user, tasksByType, daysMissed, analytics});
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
expect(user.stats.buffs.int).to.be.greaterThan(previousBuffs.int);
expect(user.stats.buffs.per).to.be.greaterThan(previousBuffs.per);
expect(user.stats.buffs.con).to.be.greaterThan(previousBuffs.con);
});
it('always grants a perfect day buff when CRON_SAFE_MODE is set when user is sleeping', () => {
user.preferences.sleep = true;
sandbox.stub(nconf, 'get').withArgs('CRON_SAFE_MODE').returns('true');
let cronOverride = requireAgain(pathToCronLib).cron;
daysMissed = 1;
@@ -1373,6 +1434,20 @@ describe('cron', () => {
common.statsComputed.restore();
});
it('should not add mp to user when user is sleeping', () => {
const statsComputedRes = common.statsComputed(user);
const stubbedStatsComputed = sinon.stub(common, 'statsComputed');
user.preferences.sleep = true;
let mpBefore = user.stats.mp;
tasksByType.dailys[0].completed = true;
stubbedStatsComputed.returns(Object.assign(statsComputedRes, {maxMP: 100}));
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.mp).to.equal(mpBefore);
common.statsComputed.restore();
});
it('set user\'s mp to statsComputed.maxMP when user.stats.mp is greater', () => {
const statsComputedRes = common.statsComputed(user);
const stubbedStatsComputed = sinon.stub(common, 'statsComputed');
@@ -1514,27 +1589,6 @@ describe('cron', () => {
flagCount: 0,
};
});
xit('does not clear pms under 200', () => {
cron({user, tasksByType, daysMissed, analytics});
expect(user.inbox.messages[lastMessageId]).to.exist;
});
xit('clears pms over 200', () => {
let messageId = common.uuid();
user.inbox.messages[messageId] = {
id: messageId,
text: `test ${messageId}`,
timestamp: Number(new Date()),
likes: {},
flags: {},
flagCount: 0,
};
cron({user, tasksByType, daysMissed, analytics});
expect(user.inbox.messages[messageId]).to.not.exist;
});
});
describe('login incentives', () => {
@@ -1568,7 +1622,7 @@ describe('cron', () => {
expect(user.loginIncentives).to.eql(1);
});
it('increments loginIncentives by 1 even if user has Dailies paused', () => {
it('increments loginIncentives by 1 even if user is sleeping', () => {
user.preferences.sleep = true;
cron({user, tasksByType, daysMissed, analytics});
expect(user.loginIncentives).to.eql(1);

View File

@@ -107,6 +107,25 @@ describe('Password Utilities', () => {
}
});
it('defaults to SHA1 encryption if salt is provided', async () => {
let textPassword = 'mySecretPassword';
let salt = sha1MakeSalt();
let hashedPassword = sha1EncryptPassword(textPassword, salt);
let user = {
auth: {
local: {
hashed_password: hashedPassword,
salt,
passwordHashMethod: '',
},
},
};
let isValidPassword = await compare(user, textPassword);
expect(isValidPassword).to.eql(true);
});
it('throws an error if an invalid hashing method is used', async () => {
try {
await compare({
@@ -226,7 +245,9 @@ describe('Password Utilities', () => {
it('returns false if the user has no local auth', async () => {
let user = await generateUser({
auth: 'not an object with valid fields',
auth: {
facebook: {},
},
});
let res = await validatePasswordResetCodeAndFindUser(encrypt(JSON.stringify({
userId: user._id,

View File

@@ -48,7 +48,6 @@ describe('Amazon Payments - Cancel Subscription', () => {
function expectBillingAggreementDetailSpy () {
getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails')
.returnsPromise()
.resolves({
BillingAgreementDetails: {
BillingAgreementStatus: {State: 'Open'},
@@ -80,14 +79,14 @@ describe('Amazon Payments - Cancel Subscription', () => {
headers = {};
getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails');
getBillingAgreementDetailsSpy.returnsPromise().resolves({
getBillingAgreementDetailsSpy.resolves({
BillingAgreementDetails: {
BillingAgreementStatus: {State: 'Closed'},
},
});
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription');
paymentCancelSubscriptionSpy.returnsPromise().resolves({});
paymentCancelSubscriptionSpy.resolves({});
});
afterEach(function () {
@@ -118,7 +117,7 @@ describe('Amazon Payments - Cancel Subscription', () => {
it('should close a user subscription if amazon not closed', async () => {
amzLib.getBillingAgreementDetails.restore();
expectBillingAggreementDetailSpy();
let closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').returnsPromise().resolves({});
let closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').resolves({});
billingAgreementId = user.purchased.plan.customerId;
await amzLib.cancelSubscription({user, headers});
@@ -164,7 +163,7 @@ describe('Amazon Payments - Cancel Subscription', () => {
it('should close a group subscription if amazon not closed', async () => {
amzLib.getBillingAgreementDetails.restore();
expectBillingAggreementDetailSpy();
let closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').returnsPromise().resolves({});
let closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').resolves({});
billingAgreementId = group.purchased.plan.customerId;
await amzLib.cancelSubscription({user, groupId: group._id, headers});

View File

@@ -68,22 +68,22 @@ describe('Amazon Payments - Checkout', () => {
orderReferenceId = 'orderReferenceId';
setOrderReferenceDetailsSpy = sinon.stub(amzLib, 'setOrderReferenceDetails');
setOrderReferenceDetailsSpy.returnsPromise().resolves({});
setOrderReferenceDetailsSpy.resolves({});
confirmOrderReferenceSpy = sinon.stub(amzLib, 'confirmOrderReference');
confirmOrderReferenceSpy.returnsPromise().resolves({});
confirmOrderReferenceSpy.resolves({});
authorizeSpy = sinon.stub(amzLib, 'authorize');
authorizeSpy.returnsPromise().resolves({});
authorizeSpy.resolves({});
closeOrderReferenceSpy = sinon.stub(amzLib, 'closeOrderReference');
closeOrderReferenceSpy.returnsPromise().resolves({});
closeOrderReferenceSpy.resolves({});
paymentBuyGemsStub = sinon.stub(payments, 'buyGems');
paymentBuyGemsStub.returnsPromise().resolves({});
paymentBuyGemsStub.resolves({});
paymentCreateSubscritionStub = sinon.stub(payments, 'createSubscription');
paymentCreateSubscritionStub.returnsPromise().resolves({});
paymentCreateSubscritionStub.resolves({});
sinon.stub(common, 'uuid').returns('uuid-generated');
});
@@ -111,7 +111,7 @@ describe('Amazon Payments - Checkout', () => {
}
it('should purchase gems', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
sinon.stub(user, 'canGetGems').resolves(true);
await amzLib.checkout({user, orderReferenceId, headers});
expectBuyGemsStub(amzLib.constants.PAYMENT_METHOD);
@@ -140,7 +140,7 @@ describe('Amazon Payments - Checkout', () => {
});
it('should error if user cannot get gems gems', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
sinon.stub(user, 'canGetGems').resolves(false);
await expect(amzLib.checkout({user, orderReferenceId, headers})).to.eventually.be.rejected.and.to.eql({
httpCode: 401,
message: i18n.t('groupPolicyCannotGetGems'),

View File

@@ -46,16 +46,16 @@ describe('Amazon Payments - Subscribe', () => {
headers = {};
amazonSetBillingAgreementDetailsSpy = sinon.stub(amzLib, 'setBillingAgreementDetails');
amazonSetBillingAgreementDetailsSpy.returnsPromise().resolves({});
amazonSetBillingAgreementDetailsSpy.resolves({});
amazonConfirmBillingAgreementSpy = sinon.stub(amzLib, 'confirmBillingAgreement');
amazonConfirmBillingAgreementSpy.returnsPromise().resolves({});
amazonConfirmBillingAgreementSpy.resolves({});
amazonAuthorizeOnBillingAgreementSpy = sinon.stub(amzLib, 'authorizeOnBillingAgreement');
amazonAuthorizeOnBillingAgreementSpy.returnsPromise().resolves({});
amazonAuthorizeOnBillingAgreementSpy.resolves({});
createSubSpy = sinon.stub(payments, 'createSubscription');
createSubSpy.returnsPromise().resolves({});
createSubSpy.resolves({});
sinon.stub(common, 'uuid').returns('uuid-generated');
});

View File

@@ -37,7 +37,7 @@ describe('#upgradeGroupPlan', () => {
await group.save();
spy = sinon.stub(amzLib, 'authorizeOnBillingAgreement');
spy.returnsPromise().resolves([]);
spy.resolves([]);
uuidString = 'uuid-v4';
sinon.stub(uuid, 'v4').returns(uuidString);

View File

@@ -6,6 +6,7 @@ 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';
import {mockFindById, restoreFindById} from '../../../../helpers/mongoose.helper';
const i18n = common.i18n;
@@ -24,16 +25,16 @@ describe('Apple Payments', () => {
headers = {};
iapSetupStub = sinon.stub(iapModule, 'setup')
.returnsPromise().resolves();
.resolves();
iapValidateStub = sinon.stub(iapModule, 'validate')
.returnsPromise().resolves({});
.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({});
paymentBuyGemsStub = sinon.stub(payments, 'buyGems').resolves({});
});
afterEach(() => {
@@ -49,7 +50,7 @@ describe('Apple Payments', () => {
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
.returns(false);
await expect(applePayments.verifyGemPurchase(user, receipt, headers))
await expect(applePayments.verifyGemPurchase({user, receipt, headers}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
@@ -61,7 +62,7 @@ describe('Apple Payments', () => {
iapGetPurchaseDataStub.restore();
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData').returns([]);
await expect(applePayments.verifyGemPurchase(user, receipt, headers))
await expect(applePayments.verifyGemPurchase({user, receipt, headers}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
@@ -70,8 +71,8 @@ describe('Apple Payments', () => {
});
it('errors if the user cannot purchase gems', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
await expect(applePayments.verifyGemPurchase(user, receipt, headers))
sinon.stub(user, 'canGetGems').resolves(false);
await expect(applePayments.verifyGemPurchase({user, receipt, headers}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
@@ -82,14 +83,14 @@ describe('Apple Payments', () => {
});
it('errors if amount does not exist', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
sinon.stub(user, 'canGetGems').resolves(true);
iapGetPurchaseDataStub.restore();
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
.returns([{productId: 'badProduct',
transactionId: token,
}]);
await expect(applePayments.verifyGemPurchase(user, receipt, headers))
await expect(applePayments.verifyGemPurchase({user, receipt, headers}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
@@ -130,8 +131,8 @@ describe('Apple Payments', () => {
transactionId: token,
}]);
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
await applePayments.verifyGemPurchase(user, receipt, headers);
sinon.stub(user, 'canGetGems').resolves(true);
await applePayments.verifyGemPurchase({user, receipt, headers});
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
@@ -151,6 +152,38 @@ describe('Apple Payments', () => {
user.canGetGems.restore();
});
});
it('gifts gems', async () => {
const receivingUser = new User();
await receivingUser.save();
mockFindById(receivingUser);
iapGetPurchaseDataStub.restore();
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
.returns([{productId: gemsCanPurchase[0].productId,
transactionId: token,
}]);
const gift = {uuid: receivingUser._id};
await applePayments.verifyGemPurchase({user, gift, 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: receivingUser,
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
amount: gemsCanPurchase[0].amount,
headers,
});
restoreFindById();
});
});
describe('subscribe', () => {
@@ -167,9 +200,9 @@ describe('Apple Payments', () => {
nextPaymentProcessing = moment.utc().add({days: 2});
iapSetupStub = sinon.stub(iapModule, 'setup')
.returnsPromise().resolves();
.resolves();
iapValidateStub = sinon.stub(iapModule, 'validate')
.returnsPromise().resolves({});
.resolves({});
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
.returns(true);
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
@@ -186,7 +219,7 @@ describe('Apple Payments', () => {
productId: sku,
transactionId: token,
}]);
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({});
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').resolves({});
});
afterEach(() => {
@@ -297,9 +330,9 @@ describe('Apple Payments', () => {
expirationDate = moment.utc();
iapSetupStub = sinon.stub(iapModule, 'setup')
.returnsPromise().resolves();
.resolves();
iapValidateStub = sinon.stub(iapModule, 'validate')
.returnsPromise().resolves({
.resolves({
expirationDate,
});
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
@@ -314,7 +347,7 @@ describe('Apple Payments', () => {
user.purchased.plan.planId = subKey;
user.purchased.plan.additionalData = receipt;
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').resolves({});
});
afterEach(function () {

View File

@@ -6,6 +6,7 @@ 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';
import {mockFindById, restoreFindById} from '../../../../helpers/mongoose.helper';
const i18n = common.i18n;
@@ -24,12 +25,12 @@ describe('Google Payments', () => {
headers = {};
iapSetupStub = sinon.stub(iapModule, 'setup')
.returnsPromise().resolves();
.resolves();
iapValidateStub = sinon.stub(iapModule, 'validate')
.returnsPromise().resolves({});
.resolves({});
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
.returns(true);
paymentBuyGemsStub = sinon.stub(payments, 'buyGems').returnsPromise().resolves({});
paymentBuyGemsStub = sinon.stub(payments, 'buyGems').resolves({});
});
afterEach(() => {
@@ -44,7 +45,7 @@ describe('Google Payments', () => {
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
.returns(false);
await expect(googlePayments.verifyGemPurchase(user, receipt, signature, headers))
await expect(googlePayments.verifyGemPurchase({user, receipt, signature, headers}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
@@ -55,7 +56,7 @@ describe('Google Payments', () => {
it('should throw an error if productId is invalid', async () => {
receipt = `{"token": "${token}", "productId": "invalid"}`;
await expect(googlePayments.verifyGemPurchase(user, receipt, signature, headers))
await expect(googlePayments.verifyGemPurchase({user, receipt, signature, headers}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
@@ -64,9 +65,9 @@ describe('Google Payments', () => {
});
it('should throw an error if user cannot purchase gems', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
sinon.stub(user, 'canGetGems').resolves(false);
await expect(googlePayments.verifyGemPurchase(user, receipt, signature, headers))
await expect(googlePayments.verifyGemPurchase({user, receipt, signature, headers}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
@@ -77,8 +78,8 @@ describe('Google Payments', () => {
});
it('purchases gems', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
await googlePayments.verifyGemPurchase(user, receipt, signature, headers);
sinon.stub(user, 'canGetGems').resolves(true);
await googlePayments.verifyGemPurchase({user, receipt, signature, headers});
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
@@ -99,6 +100,34 @@ describe('Google Payments', () => {
expect(user.canGetGems).to.be.calledOnce;
user.canGetGems.restore();
});
it('gifts gems', async () => {
const receivingUser = new User();
await receivingUser.save();
mockFindById(receivingUser);
const gift = {uuid: receivingUser._id};
await googlePayments.verifyGemPurchase({user, gift, 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: receivingUser,
paymentMethod: googlePayments.constants.PAYMENT_METHOD_GOOGLE,
amount: 5.25,
headers,
});
restoreFindById();
});
});
describe('subscribe', () => {
@@ -116,12 +145,12 @@ describe('Google Payments', () => {
nextPaymentProcessing = moment.utc().add({days: 2});
iapSetupStub = sinon.stub(iapModule, 'setup')
.returnsPromise().resolves();
.resolves();
iapValidateStub = sinon.stub(iapModule, 'validate')
.returnsPromise().resolves({});
.resolves({});
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
.returns(true);
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({});
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').resolves({});
});
afterEach(() => {
@@ -193,9 +222,9 @@ describe('Google Payments', () => {
expirationDate = moment.utc();
iapSetupStub = sinon.stub(iapModule, 'setup')
.returnsPromise().resolves();
.resolves();
iapValidateStub = sinon.stub(iapModule, 'validate')
.returnsPromise().resolves({
.resolves({
expirationDate,
});
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
@@ -210,7 +239,7 @@ describe('Google Payments', () => {
user.purchased.plan.planId = subKey;
user.purchased.plan.additionalData = {data: receipt, signature};
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').resolves({});
});
afterEach(function () {

View File

@@ -69,11 +69,11 @@ describe('Purchasing a group plan for group', () => {
};
let subscriptionId = 'subId';
sinon.stub(stripe.customers, 'del').returnsPromise().resolves({});
sinon.stub(stripe.customers, 'del').resolves({});
let currentPeriodEndTimeStamp = moment().add(3, 'months').unix();
sinon.stub(stripe.customers, 'retrieve')
.returnsPromise().resolves({
.resolves({
subscriptions: {
data: [{id: subscriptionId, current_period_end: currentPeriodEndTimeStamp}], // eslint-disable-line camelcase
},
@@ -216,7 +216,6 @@ describe('Purchasing a group plan for group', () => {
it('sends one email to subscribed member of group, stating subscription is cancelled (Amazon)', async () => {
sinon.stub(amzLib, 'getBillingAgreementDetails')
.returnsPromise()
.resolves({
BillingAgreementDetails: {
BillingAgreementStatus: {State: 'Closed'},
@@ -251,9 +250,9 @@ describe('Purchasing a group plan for group', () => {
});
it('sends one email to subscribed member of group, stating subscription is cancelled (PayPal)', async () => {
sinon.stub(paypalPayments, 'paypalBillingAgreementCancel').returnsPromise().resolves({});
sinon.stub(paypalPayments, 'paypalBillingAgreementCancel').resolves({});
sinon.stub(paypalPayments, 'paypalBillingAgreementGet')
.returnsPromise().resolves({
.resolves({
agreement_details: { // eslint-disable-line camelcase
next_billing_date: moment().add(3, 'months').toDate(), // eslint-disable-line camelcase
cycles_completed: 1, // eslint-disable-line camelcase
@@ -449,7 +448,6 @@ describe('Purchasing a group plan for group', () => {
it('adds months to members with existing recurring subscription (Amazon)', async () => {
sinon.stub(amzLib, 'getBillingAgreementDetails')
.returnsPromise()
.resolves({
BillingAgreementDetails: {
BillingAgreementStatus: {State: 'Closed'},
@@ -478,9 +476,9 @@ describe('Purchasing a group plan for group', () => {
});
it('adds months to members with existing recurring subscription (Paypal)', async () => {
sinon.stub(paypalPayments, 'paypalBillingAgreementCancel').returnsPromise().resolves({});
sinon.stub(paypalPayments, 'paypalBillingAgreementCancel').resolves({});
sinon.stub(paypalPayments, 'paypalBillingAgreementGet')
.returnsPromise().resolves({
.resolves({
agreement_details: { // eslint-disable-line camelcase
next_billing_date: moment().add(3, 'months').toDate(), // eslint-disable-line camelcase
cycles_completed: 1, // eslint-disable-line camelcase

View File

@@ -446,6 +446,19 @@ describe('payments/index', () => {
fakeClock.restore();
});
it('does not add a notification for mystery items if none was awarded', async () => {
const noMysteryItemTimeframe = 1462183920000; // May 2nd 2016
let fakeClock = sinon.useFakeTimers(noMysteryItemTimeframe);
data = { paymentMethod: 'PaymentMethod', user, sub: { key: 'basic_3mo' } };
await api.createSubscription(data);
expect(user.purchased.plan.mysteryItems).to.have.a.lengthOf(0);
expect(user.notifications.find(n => n.type === 'NEW_MYSTERY_ITEMS')).to.be.undefined;
fakeClock.restore();
});
it('does not award mystery item when user already owns the item', async () => {
let mayMysteryItemTimeframe = 1464725113000; // May 31st 2016
let fakeClock = sinon.useFakeTimers(mayMysteryItemTimeframe);

View File

@@ -13,9 +13,9 @@ describe('checkout success', () => {
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({});
paypalPaymentExecuteStub = sinon.stub(paypalPayments, 'paypalPaymentExecute').resolves({});
paymentBuyGemsStub = sinon.stub(payments, 'buyGems').resolves({});
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').resolves({});
});
afterEach(() => {

View File

@@ -15,6 +15,7 @@ describe('checkout', () => {
function getPaypalCreateOptions (description, amount) {
return {
experience_profile_id: 'xp_profile_id',
intent: 'sale',
payer: { payment_method: 'Paypal' },
redirect_urls: {
@@ -42,7 +43,7 @@ describe('checkout', () => {
beforeEach(() => {
approvalHerf = 'approval_href';
paypalPaymentCreateStub = sinon.stub(paypalPayments, 'paypalPaymentCreate')
.returnsPromise().resolves({
.resolves({
links: [{ rel: 'approval_url', href: approvalHerf }],
});
});
@@ -80,7 +81,7 @@ describe('checkout', () => {
it('should error if the user cannot get gems', async () => {
let user = new User();
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
sinon.stub(user, 'canGetGems').resolves(false);
await expect(paypalPayments.checkout({user})).to.eventually.be.rejected.and.to.eql({
httpCode: 401,

View File

@@ -34,8 +34,8 @@ describe('ipn', () => {
group.purchased.plan.lastBillingDate = new Date();
await group.save();
ipnVerifyAsyncStub = sinon.stub(paypalPayments, 'ipnVerifyAsync').returnsPromise().resolves({});
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
ipnVerifyAsyncStub = sinon.stub(paypalPayments, 'ipnVerifyAsync').resolves({});
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').resolves({});
});
afterEach(function () {

View File

@@ -38,15 +38,15 @@ describe('subscribeCancel', () => {
nextBillingDate = new Date();
paypalBillingAgreementCancelStub = sinon.stub(paypalPayments, 'paypalBillingAgreementCancel').returnsPromise().resolves({});
paypalBillingAgreementCancelStub = sinon.stub(paypalPayments, 'paypalBillingAgreementCancel').resolves({});
paypalBillingAgreementGetStub = sinon.stub(paypalPayments, 'paypalBillingAgreementGet')
.returnsPromise().resolves({
.resolves({
agreement_details: {
next_billing_date: nextBillingDate,
cycles_completed: 1,
},
});
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').resolves({});
});
afterEach(function () {

View File

@@ -28,10 +28,10 @@ describe('subscribeSuccess', () => {
customerId = 'test-customerId';
paypalBillingAgreementExecuteStub = sinon.stub(paypalPayments, 'paypalBillingAgreementExecute')
.returnsPromise({}).resolves({
.resolves({
id: customerId,
});
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({});
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').resolves({});
});
afterEach(() => {

View File

@@ -18,7 +18,7 @@ describe('subscribe', () => {
sub = Object.assign({}, common.content.subscriptionBlocks[subKey]);
paypalBillingAgreementCreateStub = sinon.stub(paypalPayments, 'paypalBillingAgreementCreate')
.returnsPromise().resolves({
.resolves({
links: [{ rel: 'approval_url', href: approvalHerf }],
});
});

View File

@@ -82,12 +82,12 @@ describe('cancel subscription', () => {
beforeEach(() => {
subscriptionId = 'subId';
stripeDeleteCustomerStub = sinon.stub(stripe.customers, 'del').returnsPromise().resolves({});
paymentsCancelSubStub = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
stripeDeleteCustomerStub = sinon.stub(stripe.customers, 'del').resolves({});
paymentsCancelSubStub = sinon.stub(payments, 'cancelSubscription').resolves({});
currentPeriodEndTimeStamp = (new Date()).getTime();
stripeRetrieveStub = sinon.stub(stripe.customers, 'retrieve')
.returnsPromise().resolves({
.resolves({
subscriptions: {
data: [{id: subscriptionId, current_period_end: currentPeriodEndTimeStamp}], // eslint-disable-line camelcase
},

View File

@@ -54,7 +54,7 @@ describe('checkout with subscription', () => {
token = 'test-token';
spy = sinon.stub(stripe.subscriptions, 'update');
spy.returnsPromise().resolves;
spy.resolves;
stripeCreateCustomerSpy = sinon.stub(stripe.customers, 'create');
let stripCustomerResponse = {
@@ -63,10 +63,10 @@ describe('checkout with subscription', () => {
data: [{id: subscriptionId}],
},
};
stripeCreateCustomerSpy.returnsPromise().resolves(stripCustomerResponse);
stripeCreateCustomerSpy.resolves(stripCustomerResponse);
stripePaymentsCreateSubSpy = sinon.stub(payments, 'createSubscription');
stripePaymentsCreateSubSpy.returnsPromise().resolves({});
stripePaymentsCreateSubSpy.resolves({});
data.groupId = group._id;
data.sub.quantity = 3;

View File

@@ -26,9 +26,9 @@ describe('checkout', () => {
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({});
stripeChargeStub = sinon.stub(stripe.charges, 'create').resolves(stripCustomerResponse);
paymentBuyGemsStub = sinon.stub(payments, 'buyGems').resolves({});
paymentCreateSubscritionStub = sinon.stub(payments, 'createSubscription').resolves({});
});
afterEach(() => {
@@ -82,7 +82,7 @@ describe('checkout', () => {
it('should error if user cannot get gems', async () => {
gift = undefined;
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
sinon.stub(user, 'canGetGems').resolves(false);
await expect(stripePayments.checkout({
token,
@@ -101,7 +101,7 @@ describe('checkout', () => {
it('should purchase gems', async () => {
gift = undefined;
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
sinon.stub(user, 'canGetGems').resolves(true);
await stripePayments.checkout({
token,

View File

@@ -98,11 +98,11 @@ describe('edit subscription', () => {
beforeEach(() => {
subscriptionId = 'subId';
stripeListSubscriptionStub = sinon.stub(stripe.customers, 'listSubscriptions')
.returnsPromise().resolves({
.resolves({
data: [{id: subscriptionId}],
});
stripeUpdateSubscriptionStub = sinon.stub(stripe.customers, 'updateSubscription').returnsPromise().resolves({});
stripeUpdateSubscriptionStub = sinon.stub(stripe.customers, 'updateSubscription').resolves({});
});
afterEach(() => {

View File

@@ -22,7 +22,7 @@ describe('Stripe - Webhooks', () => {
const eventRetrieved = {type: eventType};
beforeEach(() => {
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves(eventRetrieved);
sinon.stub(stripe.events, 'retrieve').resolves(eventRetrieved);
sinon.stub(logger, 'error');
});
@@ -52,8 +52,8 @@ describe('Stripe - Webhooks', () => {
const eventType = 'customer.subscription.deleted';
beforeEach(() => {
sinon.stub(stripe.customers, 'del').returnsPromise().resolves({});
sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
sinon.stub(stripe.customers, 'del').resolves({});
sinon.stub(payments, 'cancelSubscription').resolves({});
});
afterEach(() => {
@@ -62,7 +62,7 @@ describe('Stripe - Webhooks', () => {
});
it('does not do anything if event.request is null (subscription cancelled manually)', async () => {
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
sinon.stub(stripe.events, 'retrieve').resolves({
id: 123,
type: eventType,
request: 123,
@@ -79,7 +79,7 @@ describe('Stripe - Webhooks', () => {
describe('user subscription', () => {
it('throws an error if the user is not found', async () => {
const customerId = 456;
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
sinon.stub(stripe.events, 'retrieve').resolves({
id: 123,
type: eventType,
data: {
@@ -113,7 +113,7 @@ describe('Stripe - Webhooks', () => {
subscriber.purchased.plan.paymentMethod = 'Stripe';
await subscriber.save();
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
sinon.stub(stripe.events, 'retrieve').resolves({
id: 123,
type: eventType,
data: {
@@ -146,7 +146,7 @@ describe('Stripe - Webhooks', () => {
describe('group plan subscription', () => {
it('throws an error if the group is not found', async () => {
const customerId = 456;
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
sinon.stub(stripe.events, 'retrieve').resolves({
id: 123,
type: eventType,
data: {
@@ -185,7 +185,7 @@ describe('Stripe - Webhooks', () => {
subscriber.purchased.plan.paymentMethod = 'Stripe';
await subscriber.save();
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
sinon.stub(stripe.events, 'retrieve').resolves({
id: 123,
type: eventType,
data: {
@@ -227,7 +227,7 @@ describe('Stripe - Webhooks', () => {
subscriber.purchased.plan.paymentMethod = 'Stripe';
await subscriber.save();
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
sinon.stub(stripe.events, 'retrieve').resolves({
id: 123,
type: eventType,
data: {

View File

@@ -38,7 +38,7 @@ describe('Stripe - Upgrade Group Plan', () => {
await group.save();
spy = sinon.stub(stripe.subscriptions, 'update');
spy.returnsPromise().resolves([]);
spy.resolves([]);
data.groupId = group._id;
data.sub.quantity = 3;
stripePayments.setStripeApi(stripe);

View File

@@ -58,7 +58,7 @@ describe('slack', () => {
title: 'Flag in Some group - (private guild)',
title_link: undefined,
text: 'some text',
footer: sandbox.match(/<.*?groupId=group-id&chatId=chat-id\|Flag this message>/),
footer: sandbox.match(/<.*?groupId=group-id&chatId=chat-id\|Flag this message.>/),
mrkdwn_in: [
'text',
],
@@ -110,7 +110,7 @@ describe('slack', () => {
});
it('noops if no flagging url is provided', () => {
sandbox.stub(nconf, 'get').withArgs('SLACK:FLAGGING_URL').returns('');
sandbox.stub(nconf, 'get').withArgs('SLACK_FLAGGING_URL').returns('');
sandbox.stub(logger, 'error');
let reRequiredSlack = requireAgain('../../../../website/server/libs/slack');

View File

@@ -178,4 +178,12 @@ describe('taskManager', () => {
expect(order).to.eql(['task-id-2', 'task-id-1']);
});
it('moves tasks to a specified position out of length', async () => {
let order = ['task-id-1'];
moveTask(order, 'task-id-2', 2);
expect(order).to.eql(['task-id-1', 'task-id-2']);
});
});

View File

@@ -73,6 +73,39 @@ describe('redirects middleware', () => {
expect(res.redirect).to.have.not.been.called;
});
it('does not redirect if passed skip ssl request param is passed with corrrect key', () => {
let nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
nconfStub.withArgs('IS_PROD').returns(true);
nconfStub.withArgs('SKIP_SSL_CHECK_KEY').returns('test-key');
req.header = sandbox.stub().withArgs('x-forwarded-proto').returns('http');
req.originalUrl = '/static/front';
req.query.skipSSLCheck = 'test-key';
const attachRedirects = requireAgain(pathToRedirectsMiddleware);
attachRedirects.forceSSL(req, res, next);
expect(res.redirect).to.have.not.been.called;
});
it('does redirect if skip ssl request param is passed with incorrrect key', () => {
let nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
nconfStub.withArgs('IS_PROD').returns(true);
nconfStub.withArgs('SKIP_SSL_CHECK_KEY').returns('test-key');
req.header = sandbox.stub().withArgs('x-forwarded-proto').returns('http');
req.originalUrl = '/static/front?skipSSLCheck=INVALID';
req.query.skipSSLCheck = 'INVALID';
const attachRedirects = requireAgain(pathToRedirectsMiddleware);
attachRedirects.forceSSL(req, res, next);
expect(res.redirect).to.be.calledOnce;
expect(res.redirect).to.be.calledWith('https://habitica.com/static/front?skipSSLCheck=INVALID');
});
});
context('forceHabitica', () => {

View File

@@ -20,7 +20,7 @@ import { TAVERN_ID } from '../../../../website/common/script/';
import shared from '../../../../website/common';
describe('Group Model', () => {
let party, questLeader, participatingMember, nonParticipatingMember, undecidedMember;
let party, questLeader, participatingMember, sleepingParticipatingMember, nonParticipatingMember, undecidedMember;
beforeEach(async () => {
sandbox.stub(email, 'sendTxn');
@@ -32,8 +32,19 @@ describe('Group Model', () => {
privacy: 'private',
});
let _progress = {
up: 10,
down: 8,
collectedItems: 5,
};
questLeader = new User({
party: { _id: party._id },
party: {
_id: party._id,
quest: {
progress: _progress,
},
},
profile: { name: 'Quest Leader' },
items: {
quests: {
@@ -45,15 +56,40 @@ describe('Group Model', () => {
party.leader = questLeader._id;
participatingMember = new User({
party: { _id: party._id },
party: {
_id: party._id,
quest: {
progress: _progress,
},
},
profile: { name: 'Participating Member' },
});
sleepingParticipatingMember = new User({
party: {
_id: party._id,
quest: {
progress: _progress,
},
},
profile: { name: 'Sleeping Participating Member' },
preferences: { sleep: true },
});
nonParticipatingMember = new User({
party: { _id: party._id },
party: {
_id: party._id,
quest: {
progress: _progress,
},
},
profile: { name: 'Non-Participating Member' },
});
undecidedMember = new User({
party: { _id: party._id },
party: {
_id: party._id,
quest: {
progress: _progress,
},
},
profile: { name: 'Undecided Member' },
});
@@ -61,6 +97,7 @@ describe('Group Model', () => {
party.save(),
questLeader.save(),
participatingMember.save(),
sleepingParticipatingMember.save(),
nonParticipatingMember.save(),
undecidedMember.save(),
]);
@@ -80,6 +117,7 @@ describe('Group Model', () => {
party.quest.members = {
[questLeader._id]: true,
[participatingMember._id]: true,
[sleepingParticipatingMember._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
};
@@ -175,6 +213,34 @@ describe('Group Model', () => {
expect(party._processBossQuest).to.not.be.called;
expect(Group.prototype._processCollectionQuest).to.be.calledOnce;
});
it('does not call _processBossQuest when user is resting in the inn', async () => {
party.quest.key = 'whale';
await party.startQuest(questLeader);
await party.save();
await Group.processQuestProgress(sleepingParticipatingMember, progress);
party = await Group.findOne({_id: party._id});
expect(party._processBossQuest).to.not.be.called;
expect(party._processCollectionQuest).to.not.be.called;
});
it('does not call _processCollectionQuest when user is resting in the inn', async () => {
party.quest.key = 'evilsanta2';
await party.startQuest(questLeader);
await party.save();
await Group.processQuestProgress(sleepingParticipatingMember, progress);
party = await Group.findOne({_id: party._id});
expect(party._processBossQuest).to.not.be.called;
expect(party._processCollectionQuest).to.not.be.called;
});
});
context('Boss Quests', () => {
@@ -216,17 +282,20 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
updatedNonParticipatingMember,
updatedUndecidedMember,
] = await Promise.all([
User.findById(questLeader._id),
User.findById(participatingMember._id),
User.findById(sleepingParticipatingMember._id),
User.findById(nonParticipatingMember._id),
User.findById(undecidedMember._id),
]);
expect(updatedLeader.stats.hp).to.eql(42.5);
expect(updatedParticipatingMember.stats.hp).to.eql(42.5);
expect(updatedSleepingParticipatingMember.stats.hp).to.eql(42.5);
expect(updatedNonParticipatingMember.stats.hp).to.eql(50);
expect(updatedUndecidedMember.stats.hp).to.eql(50);
});
@@ -236,6 +305,7 @@ describe('Group Model', () => {
party.quest.members = {
[questLeader._id]: true,
[participatingMember._id]: true,
[sleepingParticipatingMember._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
};
@@ -248,17 +318,20 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
updatedNonParticipatingMember,
updatedUndecidedMember,
] = await Promise.all([
User.findById(questLeader._id),
User.findById(participatingMember._id),
User.findById(sleepingParticipatingMember._id),
User.findById(nonParticipatingMember._id),
User.findById(undecidedMember._id),
]);
expect(updatedLeader.stats.hp).to.eql(42.5);
expect(updatedParticipatingMember.stats.hp).to.eql(42.5);
expect(updatedSleepingParticipatingMember.stats.hp).to.eql(42.5);
expect(updatedNonParticipatingMember.stats.hp).to.eql(50);
expect(updatedUndecidedMember.stats.hp).to.eql(50);
});
@@ -435,7 +508,7 @@ describe('Group Model', () => {
party.quest.active = false;
await party.startQuest(questLeader);
Group.prototype.sendChat.reset();
Group.prototype.sendChat.resetHistory();
await party.save();
await Group.processQuestProgress(participatingMember, progress);
@@ -454,7 +527,7 @@ describe('Group Model', () => {
party.quest.active = false;
await party.startQuest(questLeader);
Group.prototype.sendChat.reset();
Group.prototype.sendChat.resetHistory();
await party.save();
await Group.processQuestProgress(participatingMember, progress);
@@ -497,9 +570,11 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
] = await Promise.all([
User.findById(questLeader._id),
User.findById(participatingMember._id),
User.findById(sleepingParticipatingMember._id),
]);
expect(updatedLeader.achievements.quests[party.quest.key]).to.eql(1);
@@ -508,6 +583,9 @@ describe('Group Model', () => {
expect(updatedParticipatingMember.achievements.quests[party.quest.key]).to.eql(1);
expect(updatedParticipatingMember.stats.exp).to.be.greaterThan(0);
expect(updatedParticipatingMember.stats.gp).to.be.greaterThan(0);
expect(updatedSleepingParticipatingMember.achievements.quests[party.quest.key]).to.eql(1);
expect(updatedSleepingParticipatingMember.stats.exp).to.be.greaterThan(0);
expect(updatedSleepingParticipatingMember.stats.gp).to.be.greaterThan(0);
});
});
});
@@ -522,7 +600,7 @@ describe('Group Model', () => {
});
it('throws an error if no uuids or emails are passed in', async () => {
await expect(Group.validateInvitations(null, null, res)).to.eventually.be.rejected.and.eql({
await expect(Group.validateInvitations({}, res)).to.eventually.be.rejected.and.eql({
httpCode: 400,
message: 'Bad request.',
name: 'BadRequest',
@@ -532,7 +610,7 @@ describe('Group Model', () => {
});
it('throws an error if only uuids are passed in, but they are not an array', async () => {
await expect(Group.validateInvitations({ uuid: 'user-id'}, null, res)).to.eventually.be.rejected.and.eql({
await expect(Group.validateInvitations({ uuids: 'user-id'}, res)).to.eventually.be.rejected.and.eql({
httpCode: 400,
message: 'Bad request.',
name: 'BadRequest',
@@ -542,7 +620,7 @@ describe('Group Model', () => {
});
it('throws an error if only emails are passed in, but they are not an array', async () => {
await expect(Group.validateInvitations(null, { emails: 'user@example.com'}, res)).to.eventually.be.rejected.and.eql({
await expect(Group.validateInvitations({emails: 'user@example.com'}, res)).to.eventually.be.rejected.and.eql({
httpCode: 400,
message: 'Bad request.',
name: 'BadRequest',
@@ -552,27 +630,27 @@ describe('Group Model', () => {
});
it('throws an error if emails are not passed in, and uuid array is empty', async () => {
await expect(Group.validateInvitations([], null, res)).to.eventually.be.rejected.and.eql({
await expect(Group.validateInvitations({uuids: []}, res)).to.eventually.be.rejected.and.eql({
httpCode: 400,
message: 'Bad request.',
name: 'BadRequest',
});
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('inviteMissingUuid');
expect(res.t).to.be.calledWith('inviteMustNotBeEmpty');
});
it('throws an error if uuids are not passed in, and email array is empty', async () => {
await expect(Group.validateInvitations(null, [], res)).to.eventually.be.rejected.and.eql({
await expect(Group.validateInvitations({emails: []}, res)).to.eventually.be.rejected.and.eql({
httpCode: 400,
message: 'Bad request.',
name: 'BadRequest',
});
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('inviteMissingEmail');
expect(res.t).to.be.calledWith('inviteMustNotBeEmpty');
});
it('throws an error if uuids and emails are passed in as empty arrays', async () => {
await expect(Group.validateInvitations([], [], res)).to.eventually.be.rejected.and.eql({
await expect(Group.validateInvitations({emails: [], uuids: []}, res)).to.eventually.be.rejected.and.eql({
httpCode: 400,
message: 'Bad request.',
name: 'BadRequest',
@@ -592,7 +670,7 @@ describe('Group Model', () => {
uuids.push('one-more-uuid'); // to put it over the limit
await expect(Group.validateInvitations(uuids, emails, res)).to.eventually.be.rejected.and.eql({
await expect(Group.validateInvitations({uuids, emails}, res)).to.eventually.be.rejected.and.eql({
httpCode: 400,
message: 'Bad request.',
name: 'BadRequest',
@@ -610,33 +688,33 @@ describe('Group Model', () => {
emails.push(`user-${i}@example.com`);
}
await Group.validateInvitations(uuids, emails, res);
await Group.validateInvitations({uuids, emails}, res);
expect(res.t).to.not.be.called;
});
it('does not throw an error if only user ids are passed in', async () => {
await Group.validateInvitations(['user-id', 'user-id2'], null, res);
await Group.validateInvitations({uuids: ['user-id', 'user-id2']}, res);
expect(res.t).to.not.be.called;
});
it('does not throw an error if only emails are passed in', async () => {
await Group.validateInvitations(null, ['user1@example.com', 'user2@example.com'], res);
await Group.validateInvitations({emails: ['user1@example.com', 'user2@example.com']}, res);
expect(res.t).to.not.be.called;
});
it('does not throw an error if both uuids and emails are passed in', async () => {
await Group.validateInvitations(['user-id', 'user-id2'], ['user1@example.com', 'user2@example.com'], res);
await Group.validateInvitations({uuids: ['user-id', 'user-id2'], emails: ['user1@example.com', 'user2@example.com']}, res);
expect(res.t).to.not.be.called;
});
it('does not throw an error if uuids are passed in and emails are an empty array', async () => {
await Group.validateInvitations(['user-id', 'user-id2'], [], res);
await Group.validateInvitations({uuids: ['user-id', 'user-id2'], emails: []}, res);
expect(res.t).to.not.be.called;
});
it('does not throw an error if emails are passed in and uuids are an empty array', async () => {
await Group.validateInvitations([], ['user1@example.com', 'user2@example.com'], res);
await Group.validateInvitations({uuids: [], emails: ['user1@example.com', 'user2@example.com']}, res);
expect(res.t).to.not.be.called;
});
});
@@ -647,6 +725,7 @@ describe('Group Model', () => {
it('returns an array of members whose quest status set to true', () => {
party.quest.members = {
[participatingMember._id]: true,
[sleepingParticipatingMember._id]: true,
[questLeader._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
@@ -654,6 +733,7 @@ describe('Group Model', () => {
expect(party.getParticipatingQuestMembers()).to.eql([
participatingMember._id,
sleepingParticipatingMember._id,
questLeader._id,
]);
});
@@ -756,11 +836,12 @@ describe('Group Model', () => {
it('removes user from group quest', async () => {
party.quest.members = {
[participatingMember._id]: true,
[sleepingParticipatingMember._id]: true,
[questLeader._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
};
party.memberCount = 4;
party.memberCount = 5;
await party.save();
await party.leave(participatingMember);
@@ -768,6 +849,7 @@ describe('Group Model', () => {
party = await Group.findOne({_id: party._id});
expect(party.quest.members).to.eql({
[questLeader._id]: true,
[sleepingParticipatingMember._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
});
@@ -775,6 +857,7 @@ describe('Group Model', () => {
it('deletes a private party when the last member leaves', async () => {
await party.leave(participatingMember);
await party.leave(sleepingParticipatingMember);
await party.leave(questLeader);
await party.leave(nonParticipatingMember);
await party.leave(undecidedMember);
@@ -846,6 +929,7 @@ describe('Group Model', () => {
party.privacy = 'public';
await party.leave(participatingMember);
await party.leave(sleepingParticipatingMember);
await party.leave(questLeader);
await party.leave(nonParticipatingMember);
await party.leave(undecidedMember);
@@ -967,32 +1051,6 @@ describe('Group Model', () => {
expect(chat.user).to.not.exist;
});
it('cuts down chat to 200 messages', () => {
for (let i = 0; i < 220; i++) {
party.chat.push({ text: 'a message' });
}
expect(party.chat).to.have.a.lengthOf(220);
party.sendChat('message');
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');
@@ -1074,6 +1132,7 @@ describe('Group Model', () => {
party.quest.members = {
[questLeader._id]: true,
[participatingMember._id]: true,
[sleepingParticipatingMember._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
};
@@ -1130,33 +1189,44 @@ describe('Group Model', () => {
let expectedQuestMembers = {};
expectedQuestMembers[questLeader._id] = true;
expectedQuestMembers[participatingMember._id] = true;
expectedQuestMembers[sleepingParticipatingMember._id] = true;
expect(party.quest.members).to.eql(expectedQuestMembers);
});
it('applies updates to user object directly if user is participating', async () => {
it('applies updates to user object directly if user is participating (without resetting progress, except progress.down)', async () => {
await party.startQuest(participatingMember);
expect(participatingMember.party.quest.key).to.eql('whale');
expect(participatingMember.party.quest.progress.up).to.eql(10);
expect(participatingMember.party.quest.progress.down).to.eql(0);
expect(participatingMember.party.quest.progress.collectedItems).to.eql(0);
expect(participatingMember.party.quest.progress.collectedItems).to.eql(5);
expect(participatingMember.party.quest.completed).to.eql(null);
});
it('applies updates to other participating members', async () => {
it('applies updates to other participating members (without resetting progress, except progress.down)', async () => {
await party.startQuest(nonParticipatingMember);
questLeader = await User.findById(questLeader._id);
participatingMember = await User.findById(participatingMember._id);
sleepingParticipatingMember = await User.findById(sleepingParticipatingMember._id);
expect(participatingMember.party.quest.key).to.eql('whale');
expect(participatingMember.party.quest.progress.up).to.eql(10);
expect(participatingMember.party.quest.progress.down).to.eql(0);
expect(participatingMember.party.quest.progress.collectedItems).to.eql(0);
expect(participatingMember.party.quest.progress.collectedItems).to.eql(5);
expect(participatingMember.party.quest.completed).to.eql(null);
expect(sleepingParticipatingMember.party.quest.key).to.eql('whale');
expect(sleepingParticipatingMember.party.quest.progress.up).to.eql(10);
expect(sleepingParticipatingMember.party.quest.progress.down).to.eql(0);
expect(sleepingParticipatingMember.party.quest.progress.collectedItems).to.eql(5);
expect(sleepingParticipatingMember.party.quest.completed).to.eql(null);
expect(questLeader.party.quest.key).to.eql('whale');
expect(questLeader.party.quest.progress.up).to.eql(10);
expect(questLeader.party.quest.progress.down).to.eql(0);
expect(questLeader.party.quest.progress.collectedItems).to.eql(0);
expect(questLeader.party.quest.progress.collectedItems).to.eql(5);
expect(questLeader.party.quest.completed).to.eql(null);
});
@@ -1167,14 +1237,19 @@ describe('Group Model', () => {
undecidedMember = await User.findById(undecidedMember._id);
expect(nonParticipatingMember.party.quest.key).to.not.eql('whale');
expect(nonParticipatingMember.party.quest.progress.up).to.eql(10);
expect(nonParticipatingMember.party.quest.progress.down).to.eql(8);
expect(nonParticipatingMember.party.quest.progress.collectedItems).to.eql(5);
expect(undecidedMember.party.quest.key).to.not.eql('whale');
});
it('sends email to participating members that quest has started', async () => {
participatingMember.preferences.emailNotifications.questStarted = true;
sleepingParticipatingMember.preferences.emailNotifications.questStarted = true;
questLeader.preferences.emailNotifications.questStarted = true;
await Promise.all([
participatingMember.save(),
sleepingParticipatingMember.save(),
questLeader.save(),
]);
@@ -1187,8 +1262,9 @@ describe('Group Model', () => {
let memberIds = _.map(email.sendTxn.args[0][0], '_id');
let typeOfEmail = email.sendTxn.args[0][1];
expect(memberIds).to.have.a.lengthOf(2);
expect(memberIds).to.have.a.lengthOf(3);
expect(memberIds).to.include(participatingMember._id);
expect(memberIds).to.include(sleepingParticipatingMember._id);
expect(memberIds).to.include(questLeader._id);
expect(typeOfEmail).to.eql('quest-started');
});
@@ -1202,6 +1278,13 @@ describe('Group Model', () => {
questStarted: true,
},
}];
sleepingParticipatingMember.webhooks = [{
type: 'questActivity',
url: 'http://someurl.com',
options: {
questStarted: true,
},
}];
questLeader.webhooks = [{
type: 'questActivity',
url: 'http://someurl.com',
@@ -1210,13 +1293,13 @@ describe('Group Model', () => {
},
}];
await Promise.all([participatingMember.save(), questLeader.save()]);
await Promise.all([participatingMember.save(), sleepingParticipatingMember.save(), questLeader.save()]);
await party.startQuest(nonParticipatingMember);
await sleep(0.5);
expect(questActivityWebhook.send).to.be.calledTwice; // for 2 participating members
expect(questActivityWebhook.send).to.be.calledThrice; // for 3 participating members
let args = questActivityWebhook.send.args[0];
let webhooks = args[0].webhooks;
@@ -1226,6 +1309,8 @@ describe('Group Model', () => {
expect(webhooks).to.have.a.lengthOf(1);
if (webhookOwner === questLeader._id) {
expect(webhooks[0].id).to.eql(questLeader.webhooks[0].id);
} else if (webhookOwner === sleepingParticipatingMember._id) {
expect(webhooks[0].id).to.eql(sleepingParticipatingMember.webhooks[0].id);
} else {
expect(webhooks[0].id).to.eql(participatingMember.webhooks[0].id);
}
@@ -1236,9 +1321,11 @@ describe('Group Model', () => {
it('sends email only to members who have not opted out', async () => {
participatingMember.preferences.emailNotifications.questStarted = false;
sleepingParticipatingMember.preferences.emailNotifications.questStarted = false;
questLeader.preferences.emailNotifications.questStarted = true;
await Promise.all([
participatingMember.save(),
sleepingParticipatingMember.save(),
questLeader.save(),
]);
@@ -1252,14 +1339,17 @@ describe('Group Model', () => {
expect(memberIds).to.have.a.lengthOf(1);
expect(memberIds).to.not.include(participatingMember._id);
expect(memberIds).to.not.include(sleepingParticipatingMember._id);
expect(memberIds).to.include(questLeader._id);
});
it('does not send email to initiating member', async () => {
participatingMember.preferences.emailNotifications.questStarted = true;
sleepingParticipatingMember.preferences.emailNotifications.questStarted = true;
questLeader.preferences.emailNotifications.questStarted = true;
await Promise.all([
participatingMember.save(),
sleepingParticipatingMember.save(),
questLeader.save(),
]);
@@ -1271,8 +1361,9 @@ describe('Group Model', () => {
let memberIds = _.map(email.sendTxn.args[0][0], '_id');
expect(memberIds).to.have.a.lengthOf(1);
expect(memberIds).to.have.a.lengthOf(2);
expect(memberIds).to.not.include(participatingMember._id);
expect(memberIds).to.include(sleepingParticipatingMember._id);
expect(memberIds).to.include(questLeader._id);
});
@@ -1281,7 +1372,7 @@ describe('Group Model', () => {
await party.startQuest(nonParticipatingMember);
let members = [questLeader._id, participatingMember._id];
let members = [questLeader._id, participatingMember._id, sleepingParticipatingMember._id];
expect(User.update).to.be.calledWith(
{ _id: { $in: members } },
@@ -1316,8 +1407,9 @@ describe('Group Model', () => {
let userQuest = participatingMember.party.quest;
expect(userQuest.key).to.eql('whale');
expect(userQuest.progress.up).to.eql(10);
expect(userQuest.progress.down).to.eql(0);
expect(userQuest.progress.collectedItems).to.eql(0);
expect(userQuest.progress.collectedItems).to.eql(5);
expect(userQuest.completed).to.eql(null);
});
@@ -1346,6 +1438,7 @@ describe('Group Model', () => {
party.quest.members = {
[questLeader._id]: true,
[participatingMember._id]: true,
[sleepingParticipatingMember._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
};
@@ -1368,7 +1461,7 @@ describe('Group Model', () => {
await party.finishQuest(quest);
expect(User.update).to.be.calledTwice;
expect(User.update).to.be.calledThrice;
});
it('stops retrying when a successful update has occurred', async () => {
@@ -1378,7 +1471,7 @@ describe('Group Model', () => {
await party.finishQuest(quest);
expect(User.update).to.be.calledThrice;
expect(User.update.callCount).to.equal(4);
});
it('retries failed updates at most five times per user', async () => {
@@ -1386,7 +1479,7 @@ describe('Group Model', () => {
await expect(party.finishQuest(quest)).to.eventually.be.rejected;
expect(User.update.callCount).to.eql(10);
expect(User.update.callCount).to.eql(15); // for 3 users
});
});
@@ -1396,17 +1489,19 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
] = await Promise.all([
User.findById(questLeader._id),
User.findById(participatingMember._id),
User.findById(sleepingParticipatingMember._id),
]);
expect(updatedLeader.achievements.quests[quest.key]).to.eql(1);
expect(updatedParticipatingMember.achievements.quests[quest.key]).to.eql(1);
expect(updatedSleepingParticipatingMember.achievements.quests[quest.key]).to.eql(1);
});
// Disable test, it fails on TravisCI, but only there
xit('gives out super awesome Masterclasser achievement to the deserving', async () => {
it('gives out super awesome Masterclasser achievement to the deserving', async () => {
quest = questScrolls.lostMasterclasser4;
party.quest.key = quest.key;
@@ -1433,17 +1528,19 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
] = await Promise.all([
User.findById(questLeader._id).exec(),
User.findById(participatingMember._id).exec(),
User.findById(sleepingParticipatingMember._id).exec(),
]);
expect(updatedLeader.achievements.lostMasterclasser).to.eql(true);
expect(updatedParticipatingMember.achievements.lostMasterclasser).to.not.eql(true);
expect(updatedSleepingParticipatingMember.achievements.lostMasterclasser).to.not.eql(true);
});
// Disable test, it fails on TravisCI, but only there
xit('gives out super awesome Masterclasser achievement when quests done out of order', async () => {
it('gives out super awesome Masterclasser achievement when quests done out of order', async () => {
quest = questScrolls.lostMasterclasser1;
party.quest.key = quest.key;
@@ -1470,13 +1567,16 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
] = await Promise.all([
User.findById(questLeader._id).exec(),
User.findById(participatingMember._id).exec(),
User.findById(sleepingParticipatingMember._id).exec(),
]);
expect(updatedLeader.achievements.lostMasterclasser).to.eql(true);
expect(updatedParticipatingMember.achievements.lostMasterclasser).to.not.eql(true);
expect(updatedSleepingParticipatingMember.achievements.lostMasterclasser).to.not.eql(true);
});
it('gives xp and gold', async () => {
@@ -1485,15 +1585,19 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
] = await Promise.all([
User.findById(questLeader._id),
User.findById(participatingMember._id),
User.findById(sleepingParticipatingMember._id),
]);
expect(updatedLeader.stats.exp).to.eql(quest.drop.exp);
expect(updatedLeader.stats.gp).to.eql(quest.drop.gp);
expect(updatedParticipatingMember.stats.exp).to.eql(quest.drop.exp);
expect(updatedParticipatingMember.stats.gp).to.eql(quest.drop.gp);
expect(updatedSleepingParticipatingMember.stats.exp).to.eql(quest.drop.exp);
expect(updatedSleepingParticipatingMember.stats.gp).to.eql(quest.drop.gp);
});
context('drops', () => {
@@ -1593,25 +1697,35 @@ describe('Group Model', () => {
sandbox.spy(User, 'update');
await party.finishQuest(quest);
expect(User.update).to.be.calledTwice;
expect(User.update).to.be.calledThrice;
expect(User.update).to.be.calledWithMatch({
_id: questLeader._id,
});
expect(User.update).to.be.calledWithMatch({
_id: participatingMember._id,
});
expect(User.update).to.be.calledWithMatch({
_id: sleepingParticipatingMember._id,
});
});
it('sets user quest object to a clean state', async () => {
it('updates participating members quest object to a clean state (except for progress)', async () => {
await party.finishQuest(quest);
let updatedLeader = await User.findById(questLeader._id);
questLeader = await User.findById(questLeader._id);
participatingMember = await User.findById(participatingMember._id);
expect(updatedLeader.party.quest.completed).to.eql('whale');
expect(updatedLeader.party.quest.progress.up).to.eql(0);
expect(updatedLeader.party.quest.progress.down).to.eql(0);
expect(updatedLeader.party.quest.progress.collectedItems).to.eql(0);
expect(updatedLeader.party.quest.RSVPNeeded).to.eql(false);
expect(questLeader.party.quest.completed).to.eql('whale');
expect(questLeader.party.quest.progress.up).to.eql(10);
expect(questLeader.party.quest.progress.down).to.eql(8);
expect(questLeader.party.quest.progress.collectedItems).to.eql(5);
expect(questLeader.party.quest.RSVPNeeded).to.eql(false);
expect(participatingMember.party.quest.completed).to.eql('whale');
expect(participatingMember.party.quest.progress.up).to.eql(10);
expect(participatingMember.party.quest.progress.down).to.eql(8);
expect(participatingMember.party.quest.progress.collectedItems).to.eql(5);
expect(participatingMember.party.quest.RSVPNeeded).to.eql(false);
});
});
@@ -1632,7 +1746,7 @@ describe('Group Model', () => {
},
}];
await Promise.all([participatingMember.save(), questLeader.save()]);
await Promise.all([participatingMember.save(), sleepingParticipatingMember.save(), questLeader.save()]);
await party.finishQuest(quest);
@@ -1775,6 +1889,62 @@ describe('Group Model', () => {
expect(options.chat).to.eql(chat);
});
it('sends webhooks for users with webhooks triggered by system messages', async () => {
let guild = new Group({
name: 'some guild',
type: 'guild',
});
let memberWithWebhook = new User({
guilds: [guild._id],
webhooks: [{
type: 'groupChatReceived',
url: 'http://someurl.com',
options: {
groupId: guild._id,
},
}],
});
let memberWithoutWebhook = new User({
guilds: [guild._id],
});
let nonMemberWithWebhooks = new User({
webhooks: [{
type: 'groupChatReceived',
url: 'http://a-different-url.com',
options: {
groupId: generateUUID(),
},
}],
});
await Promise.all([
memberWithWebhook.save(),
memberWithoutWebhook.save(),
nonMemberWithWebhooks.save(),
]);
guild.leader = memberWithWebhook._id;
await guild.save();
const groupMessage = guild.sendChat('Test message.');
await groupMessage.save();
await sleep();
expect(groupChatReceivedWebhook.send).to.be.calledOnce;
let args = groupChatReceivedWebhook.send.args[0];
let webhooks = args[0].webhooks;
let options = args[1];
expect(webhooks).to.have.a.lengthOf(1);
expect(webhooks[0].id).to.eql(memberWithWebhook.webhooks[0].id);
expect(options.group).to.eql(guild);
expect(options.chat).to.eql(groupMessage);
});
it('sends webhooks for each user with webhooks in group', async () => {
let guild = new Group({
name: 'some guild',
@@ -1864,28 +2034,54 @@ describe('Group Model', () => {
context('hasNotCancelled', () => {
it('returns false if group does not have customer id', () => {
expect(party.hasNotCancelled()).to.be.undefined;
expect(party.hasNotCancelled()).to.be.false;
});
it('returns true if party does not have plan.dateTerminated', () => {
it('returns true if group does not have plan.dateTerminated', () => {
party.purchased.plan.customerId = 'test-id';
expect(party.hasNotCancelled()).to.be.true;
});
it('returns false if party if plan.dateTerminated is after today', () => {
it('returns false if group if plan.dateTerminated is after today', () => {
party.purchased.plan.customerId = 'test-id';
party.purchased.plan.dateTerminated = moment().add(1, 'days').toDate();
expect(party.hasNotCancelled()).to.be.false;
});
it('returns false if party if plan.dateTerminated is before today', () => {
it('returns false if group if plan.dateTerminated is before today', () => {
party.purchased.plan.customerId = 'test-id';
party.purchased.plan.dateTerminated = moment().subtract(1, 'days').toDate();
expect(party.hasNotCancelled()).to.be.false;
});
});
context('hasCancelled', () => {
it('returns false if group does not have customer id', () => {
expect(party.hasCancelled()).to.be.false;
});
it('returns false if group does not have plan.dateTerminated', () => {
party.purchased.plan.customerId = 'test-id';
expect(party.hasCancelled()).to.be.false;
});
it('returns true if group if plan.dateTerminated is after today', () => {
party.purchased.plan.customerId = 'test-id';
party.purchased.plan.dateTerminated = moment().add(1, 'days').toDate();
expect(party.hasCancelled()).to.be.true;
});
it('returns false if group if plan.dateTerminated is before today', () => {
party.purchased.plan.customerId = 'test-id';
party.purchased.plan.dateTerminated = moment().subtract(1, 'days').toDate();
expect(party.hasCancelled()).to.be.false;
});
});
});
});

View File

@@ -315,9 +315,8 @@ describe('User Model', () => {
user = new User();
});
it('returns false if user does not have customer id', () => {
expect(user.hasNotCancelled()).to.be.undefined;
expect(user.hasNotCancelled()).to.be.false;
});
it('returns true if user does not have plan.dateTerminated', () => {
@@ -341,6 +340,38 @@ describe('User Model', () => {
});
});
context('hasCancelled', () => {
let user;
beforeEach(() => {
user = new User();
});
it('returns false if user does not have customer id', () => {
expect(user.hasCancelled()).to.be.false;
});
it('returns false if user does not have plan.dateTerminated', () => {
user.purchased.plan.customerId = 'test-id';
expect(user.hasCancelled()).to.be.false;
});
it('returns true if user if plan.dateTerminated is after today', () => {
user.purchased.plan.customerId = 'test-id';
user.purchased.plan.dateTerminated = moment().add(1, 'days').toDate();
expect(user.hasCancelled()).to.be.true;
});
it('returns false if user if plan.dateTerminated is before today', () => {
user.purchased.plan.customerId = 'test-id';
user.purchased.plan.dateTerminated = moment().subtract(1, 'days').toDate();
expect(user.hasCancelled()).to.be.false;
});
});
context('pre-save hook', () => {
it('does not try to award achievements when achievements or items not selected in query', async () => {
let user = new User();

View File

@@ -47,6 +47,14 @@ describe('GET /challenges/:challengeId', () => {
_id: groupLeader._id,
id: groupLeader._id,
profile: {name: groupLeader.profile.name},
auth: {
local: {
username: groupLeader.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
expect(chal.group).to.eql({
_id: group._id,
@@ -63,45 +71,56 @@ describe('GET /challenges/:challengeId', () => {
context('private guild', () => {
let groupLeader;
let challengeLeader;
let group;
let challenge;
let members;
let user;
let nonMember;
let otherMember;
beforeEach(async () => {
user = await generateUser();
nonMember = await generateUser();
let populatedGroup = await createAndPopulateGroup({
groupDetails: {type: 'guild', privacy: 'private'},
members: 1,
members: 2,
});
groupLeader = populatedGroup.groupLeader;
group = populatedGroup.group;
members = populatedGroup.members;
challenge = await generateChallenge(groupLeader, group);
await members[0].post(`/challenges/${challenge._id}/join`);
await groupLeader.post(`/challenges/${challenge._id}/join`);
challengeLeader = members[0];
otherMember = members[1];
challenge = await generateChallenge(challengeLeader, group);
});
it('fails if user doesn\'t have access to the challenge', async () => {
await expect(user.get(`/challenges/${challenge._id}`)).to.eventually.be.rejected.and.eql({
it('fails if user isn\'t in the guild and isn\'t challenge leader', async () => {
await expect(nonMember.get(`/challenges/${challenge._id}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('challengeNotFound'),
});
});
it('should return challenge data', async () => {
let chal = await members[0].get(`/challenges/${challenge._id}`);
it('returns challenge data for any user in the guild', async () => {
let chal = await otherMember.get(`/challenges/${challenge._id}`);
expect(chal.name).to.equal(challenge.name);
expect(chal._id).to.equal(challenge._id);
expect(chal.leader).to.eql({
_id: groupLeader._id,
id: groupLeader._id,
profile: {name: groupLeader.profile.name},
_id: challengeLeader._id,
id: challengeLeader._id,
profile: {name: challengeLeader.profile.name},
auth: {
local: {
username: challengeLeader.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
expect(chal.group).to.eql({
_id: group._id,
@@ -114,53 +133,88 @@ describe('GET /challenges/:challengeId', () => {
leader: groupLeader.id,
});
});
it('returns challenge data if challenge leader isn\'t in the guild or challenge', async () => {
await challengeLeader.post(`/groups/${group._id}/leave`);
await challengeLeader.sync();
expect(challengeLeader.guilds).to.be.empty; // check that leaving worked
let chal = await challengeLeader.get(`/challenges/${challenge._id}`);
expect(chal.name).to.equal(challenge.name);
expect(chal._id).to.equal(challenge._id);
expect(chal.leader).to.eql({
_id: challengeLeader._id,
id: challengeLeader._id,
profile: {name: challengeLeader.profile.name},
auth: {
local: {
username: challengeLeader.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
});
});
context('party', () => {
let groupLeader;
let challengeLeader;
let group;
let challenge;
let members;
let user;
let nonMember;
let otherMember;
beforeEach(async () => {
user = await generateUser();
nonMember = await generateUser();
let populatedGroup = await createAndPopulateGroup({
groupDetails: {type: 'party'},
members: 1,
groupDetails: {type: 'party', privacy: 'private'},
members: 2,
});
groupLeader = populatedGroup.groupLeader;
group = populatedGroup.group;
members = populatedGroup.members;
challenge = await generateChallenge(groupLeader, group);
await members[0].post(`/challenges/${challenge._id}/join`);
await groupLeader.post(`/challenges/${challenge._id}/join`);
challengeLeader = members[0];
otherMember = members[1];
challenge = await generateChallenge(challengeLeader, group);
});
it('fails if user doesn\'t have access to the challenge', async () => {
await expect(user.get(`/challenges/${challenge._id}`)).to.eventually.be.rejected.and.eql({
it('fails if user isn\'t in the party and isn\'t challenge leader', async () => {
await expect(nonMember.get(`/challenges/${challenge._id}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('challengeNotFound'),
});
});
it('should return challenge data', async () => {
let chal = await members[0].get(`/challenges/${challenge._id}`);
it('returns challenge data for any user in the party', async () => {
let chal = await otherMember.get(`/challenges/${challenge._id}`);
expect(chal.name).to.equal(challenge.name);
expect(chal._id).to.equal(challenge._id);
expect(chal.leader).to.eql({
_id: groupLeader._id,
id: groupLeader.id,
profile: {name: groupLeader.profile.name},
_id: challengeLeader._id,
id: challengeLeader._id,
profile: {name: challengeLeader.profile.name},
auth: {
local: {
username: challengeLeader.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
expect(chal.group).to.eql({
_id: group._id,
id: group.id,
id: group._id,
categories: [],
name: group.name,
summary: group.name,
@@ -169,5 +223,29 @@ describe('GET /challenges/:challengeId', () => {
leader: groupLeader.id,
});
});
it('returns challenge data if challenge leader isn\'t in the party or challenge', async () => {
await challengeLeader.post('/groups/party/leave');
await challengeLeader.sync();
expect(challengeLeader.party._id).to.be.undefined; // check that leaving worked
let chal = await challengeLeader.get(`/challenges/${challenge._id}`);
expect(chal.name).to.equal(challenge.name);
expect(chal._id).to.equal(challenge._id);
expect(chal.leader).to.eql({
_id: challengeLeader._id,
id: challengeLeader._id,
profile: {name: challengeLeader.profile.name},
auth: {
local: {
username: challengeLeader.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
});
});
});

View File

@@ -1,6 +1,7 @@
import {
generateUser,
generateGroup,
createAndPopulateGroup,
generateChallenge,
translate as t,
} from '../../../../helpers/api-integration/v3';
@@ -10,7 +11,7 @@ describe('GET /challenges/:challengeId/members', () => {
let user;
beforeEach(async () => {
user = await generateUser();
user = await generateUser({ balance: 1 });
});
it('validates optional req.query.lastId to be an UUID', async () => {
@@ -21,7 +22,7 @@ describe('GET /challenges/:challengeId/members', () => {
});
});
it('fails if challenge doesn\'t exists', async () => {
it('fails if challenge doesn\'t exist', async () => {
await expect(user.get(`/challenges/${generateUUID()}/members`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
@@ -29,8 +30,8 @@ describe('GET /challenges/:challengeId/members', () => {
});
});
it('fails if user doesn\'t have access to the challenge', async () => {
let group = await generateGroup(user);
it('fails if user isn\'t in the private group and isn\'t challenge leader', async () => {
let group = await generateGroup(user, {type: 'party', privacy: 'private'});
let challenge = await generateChallenge(user, group);
let anotherUser = await generateUser();
@@ -41,6 +42,35 @@ describe('GET /challenges/:challengeId/members', () => {
});
});
it('works if user isn\'t in the private group but is challenge leader', async () => {
let populatedGroup = await createAndPopulateGroup({
groupDetails: {type: 'party', privacy: 'private'},
members: 1,
});
let groupLeader = populatedGroup.groupLeader;
let challengeLeader = populatedGroup.members[0];
let challenge = await generateChallenge(challengeLeader, populatedGroup.group);
await groupLeader.post(`/challenges/${challenge._id}/join`);
await challengeLeader.post('/groups/party/leave');
await challengeLeader.sync();
expect(challengeLeader.party._id).to.be.undefined; // check that leaving worked
let res = await challengeLeader.get(`/challenges/${challenge._id}/members`);
expect(res[0]).to.eql({
_id: groupLeader._id,
id: groupLeader._id,
profile: {name: groupLeader.profile.name},
auth: {
local: {
username: groupLeader.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
});
it('works with challenges belonging to public guild', async () => {
let leader = await generateUser({balance: 4});
let group = await generateGroup(leader, {type: 'guild', privacy: 'public', name: generateUUID()});
@@ -51,8 +81,16 @@ describe('GET /challenges/:challengeId/members', () => {
_id: leader._id,
id: leader._id,
profile: {name: leader.profile.name},
auth: {
local: {
username: leader.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
expect(res[0]).to.have.all.keys(['_id', 'id', 'profile']);
expect(res[0]).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile']);
expect(res[0].profile).to.have.all.keys(['name']);
});
@@ -66,8 +104,16 @@ describe('GET /challenges/:challengeId/members', () => {
_id: anotherUser._id,
id: anotherUser._id,
profile: {name: anotherUser.profile.name},
auth: {
local: {
username: anotherUser.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
expect(res[0]).to.have.all.keys(['_id', 'id', 'profile']);
expect(res[0]).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile']);
expect(res[0].profile).to.have.all.keys(['name']);
});
@@ -85,7 +131,7 @@ describe('GET /challenges/:challengeId/members', () => {
let res = await user.get(`/challenges/${challenge._id}/members?includeAllMembers=not-true`);
expect(res.length).to.equal(30);
res.forEach(member => {
expect(member).to.have.all.keys(['_id', 'id', 'profile']);
expect(member).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile']);
expect(member.profile).to.have.all.keys(['name']);
});
});
@@ -104,7 +150,7 @@ describe('GET /challenges/:challengeId/members', () => {
let res = await user.get(`/challenges/${challenge._id}/members`);
expect(res.length).to.equal(30);
res.forEach(member => {
expect(member).to.have.all.keys(['_id', 'id', 'profile']);
expect(member).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile']);
expect(member.profile).to.have.all.keys(['name']);
});
});
@@ -123,7 +169,7 @@ describe('GET /challenges/:challengeId/members', () => {
let res = await user.get(`/challenges/${challenge._id}/members?includeAllMembers=true`);
expect(res.length).to.equal(32);
res.forEach(member => {
expect(member).to.have.all.keys(['_id', 'id', 'profile']);
expect(member).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile']);
expect(member.profile).to.have.all.keys(['name']);
});
});

View File

@@ -81,7 +81,7 @@ describe('GET /challenges/:challengeId/members/:memberId', () => {
await groupLeader.post(`/tasks/challenge/${challenge._id}`, [{type: 'habit', text: taskText}]);
let memberProgress = await user.get(`/challenges/${challenge._id}/members/${groupLeader._id}`);
expect(memberProgress).to.have.all.keys(['_id', 'id', 'profile', 'tasks']);
expect(memberProgress).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile', 'tasks']);
expect(memberProgress.profile).to.have.all.keys(['name']);
expect(memberProgress.tasks.length).to.equal(1);
});

View File

@@ -39,6 +39,14 @@ describe('GET challenges/groups/:groupId', () => {
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: {name: user.profile.name},
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundChallenge2).to.exist;
@@ -46,6 +54,14 @@ describe('GET challenges/groups/:groupId', () => {
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: {name: user.profile.name},
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
});
@@ -58,6 +74,14 @@ describe('GET challenges/groups/:groupId', () => {
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: {name: user.profile.name},
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundChallenge2).to.exist;
@@ -65,6 +89,14 @@ describe('GET challenges/groups/:groupId', () => {
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: {name: user.profile.name},
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
});
@@ -125,6 +157,14 @@ describe('GET challenges/groups/:groupId', () => {
_id: privateGuild.leader._id,
id: privateGuild.leader._id,
profile: {name: user.profile.name},
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundChallenge2).to.exist;
@@ -132,6 +172,14 @@ describe('GET challenges/groups/:groupId', () => {
_id: privateGuild.leader._id,
id: privateGuild.leader._id,
profile: {name: user.profile.name},
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
});
});
@@ -235,6 +283,14 @@ describe('GET challenges/groups/:groupId', () => {
_id: party.leader._id,
id: party.leader._id,
profile: {name: user.profile.name},
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundChallenge2).to.exist;
@@ -242,6 +298,14 @@ describe('GET challenges/groups/:groupId', () => {
_id: party.leader._id,
id: party.leader._id,
profile: {name: user.profile.name},
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
});
@@ -254,6 +318,14 @@ describe('GET challenges/groups/:groupId', () => {
_id: party.leader._id,
id: party.leader._id,
profile: {name: user.profile.name},
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundChallenge2).to.exist;
@@ -261,6 +333,14 @@ describe('GET challenges/groups/:groupId', () => {
_id: party.leader._id,
id: party.leader._id,
profile: {name: user.profile.name},
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
});
});
@@ -288,6 +368,14 @@ describe('GET challenges/groups/:groupId', () => {
_id: user._id,
id: user._id,
profile: {name: user.profile.name},
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundChallenge2).to.exist;
@@ -295,6 +383,14 @@ describe('GET challenges/groups/:groupId', () => {
_id: user._id,
id: user._id,
profile: {name: user.profile.name},
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
});
@@ -307,6 +403,14 @@ describe('GET challenges/groups/:groupId', () => {
_id: user._id,
id: user._id,
profile: {name: user.profile.name},
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundChallenge2).to.exist;
@@ -314,6 +418,14 @@ describe('GET challenges/groups/:groupId', () => {
_id: user._id,
id: user._id,
profile: {name: user.profile.name},
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
});
});

View File

@@ -40,6 +40,14 @@ describe('GET challenges/user', () => {
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: {name: user.profile.name},
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
expect(foundChallenge.group).to.eql({
_id: publicGuild._id,
@@ -62,6 +70,14 @@ describe('GET challenges/user', () => {
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: {name: user.profile.name},
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
expect(foundChallenge1.group).to.eql({
_id: publicGuild._id,
@@ -79,6 +95,14 @@ describe('GET challenges/user', () => {
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: {name: user.profile.name},
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
expect(foundChallenge2.group).to.eql({
_id: publicGuild._id,
@@ -101,6 +125,14 @@ describe('GET challenges/user', () => {
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: {name: user.profile.name},
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
expect(foundChallenge1.group).to.eql({
_id: publicGuild._id,
@@ -118,6 +150,14 @@ describe('GET challenges/user', () => {
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: {name: user.profile.name},
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
expect(foundChallenge2.group).to.eql({
_id: publicGuild._id,

View File

@@ -94,16 +94,6 @@ describe('POST /challenges', () => {
});
});
it('returns an error when non-leader member creates a challenge in leaderOnly group', async () => {
await expect(groupMember.post('/challenges', {
group: group._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlyGroupLeaderChal'),
});
});
it('allows non-leader member to create a challenge', async () => {
let populatedGroup = await createAndPopulateGroup({
members: 1,
@@ -304,14 +294,14 @@ describe('POST /challenges', () => {
expect(groupLeader.challenges.length).to.equal(0);
});
it('awards achievement if this is creator\'s first challenge', async () => {
it('does not award joinedChallenge achievement for creating a challenge', async () => {
await groupLeader.post('/challenges', {
group: group._id,
name: 'Test Challenge',
shortName: 'TC Label',
});
groupLeader = await groupLeader.sync();
expect(groupLeader.achievements.joinedChallenge).to.be.true;
expect(groupLeader.achievements.joinedChallenge).to.not.be.true;
});
it('sets summary to challenges name when not supplied', async () => {

View File

@@ -46,7 +46,7 @@ describe('POST /challenges/:challengeId/join', () => {
await groupLeader.post(`/challenges/${challenge._id}/join`);
});
it('returns an error when user doesn\'t have permissions to access the challenge', async () => {
it('returns an error when user isn\'t in the private group and isn\'t challenge leader', async () => {
let unauthorizedUser = await generateUser();
await expect(unauthorizedUser.post(`/challenges/${challenge._id}/join`)).to.eventually.be.rejected.and.eql({
@@ -56,6 +56,16 @@ describe('POST /challenges/:challengeId/join', () => {
});
});
it('succeeds when user isn\'t in the private group but is challenge leader', async () => {
await groupLeader.post(`/challenges/${challenge._id}/leave`);
await groupLeader.post(`/groups/${group._id}/leave`);
await groupLeader.sync();
expect(groupLeader.guilds).to.be.empty; // check that leaving worked
let res = await groupLeader.post(`/challenges/${challenge._id}/join`);
expect(res.name).to.equal(challenge.name);
});
it('returns challenge data', async () => {
let res = await authorizedUser.post(`/challenges/${challenge._id}/join`);
@@ -69,6 +79,14 @@ describe('POST /challenges/:challengeId/join', () => {
_id: groupLeader._id,
id: groupLeader._id,
profile: {name: groupLeader.profile.name},
auth: {
local: {
username: groupLeader.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
expect(res.name).to.equal(challenge.name);
});

View File

@@ -79,6 +79,14 @@ describe('PUT /challenges/:challengeId', () => {
_id: member._id,
id: member._id,
profile: {name: member.profile.name},
auth: {
local: {
username: member.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
expect(res.name).to.equal('New Challenge Name');
expect(res.description).to.equal('New challenge description.');

View File

@@ -3,15 +3,23 @@ import {
translate as t,
} from '../../../../helpers/api-integration/v3';
import { find } from 'lodash';
import moment from 'moment';
import nconf from 'nconf';
import { IncomingWebhook } from '@slack/client';
const BASE_URL = nconf.get('BASE_URL');
describe('POST /chat/:chatId/flag', () => {
let user, admin, anotherUser, group;
let user, admin, anotherUser, newUser, group;
const TEST_MESSAGE = 'Test Message';
const USER_AGE_FOR_FLAGGING = 3;
beforeEach(async () => {
user = await generateUser({balance: 1});
user = await generateUser({balance: 1, 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate()});
admin = await generateUser({balance: 1, 'contributor.admin': true});
anotherUser = await generateUser();
anotherUser = await generateUser({'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate()});
newUser = await generateUser({'auth.timestamps.created': moment().subtract(1, 'days').toDate()});
sandbox.stub(IncomingWebhook.prototype, 'send');
group = await user.post('/groups', {
name: 'Test Guild',
@@ -20,6 +28,10 @@ describe('POST /chat/:chatId/flag', () => {
});
});
afterEach(() => {
sandbox.restore();
});
it('Returns an error when chat message is not found', async () => {
await expect(user.post(`/groups/${group._id}/chat/incorrectMessage/flag`))
.to.eventually.be.rejected.and.eql({
@@ -34,7 +46,7 @@ describe('POST /chat/:chatId/flag', () => {
await expect(user.post(`/groups/${group._id}/chat/${message.message.id}/flag`)).to.eventually.be.ok;
});
it('Flags a chat', async () => {
it('Flags a chat and sends normal message to moderator Slack when user is not new', async () => {
let { message } = await anotherUser.post(`/groups/${group._id}/chat`, {message: TEST_MESSAGE});
let flagResult = await user.post(`/groups/${group._id}/chat/${message.id}/flag`);
@@ -45,6 +57,62 @@ describe('POST /chat/:chatId/flag', () => {
let messageToCheck = find(groupWithFlags.chat, {id: message.id});
expect(messageToCheck.flags[user._id]).to.equal(true);
// Slack message to mods
const timestamp = `${moment(message.timestamp).utc().format('YYYY-MM-DD HH:mm')} UTC`;
/* eslint-disable camelcase */
expect(IncomingWebhook.prototype.send).to.be.calledWith({
text: `${user.profile.name} (${user.id}; language: en) flagged a message`,
attachments: [{
fallback: 'Flag Message',
color: 'danger',
author_name: `${anotherUser.profile.name} - ${anotherUser.auth.local.email} - ${anotherUser._id}\n${timestamp}`,
title: 'Flag in Test Guild',
title_link: `${BASE_URL}/groups/guild/${group._id}`,
text: TEST_MESSAGE,
footer: `<https://habitrpg.github.io/flag-o-rama/?groupId=${group._id}&chatId=${message.id}|Flag this message.>`,
mrkdwn_in: [
'text',
],
}],
});
/* eslint-ensable camelcase */
});
it('Does not increment message flag count and sends different message to moderator Slack when user is new', async () => {
let automatedComment = `The post's flag count has not been increased because the flagger's account is less than ${USER_AGE_FOR_FLAGGING} days old.`;
let { message } = await newUser.post(`/groups/${group._id}/chat`, {message: TEST_MESSAGE});
let flagResult = await newUser.post(`/groups/${group._id}/chat/${message.id}/flag`);
expect(flagResult.flags[newUser._id]).to.equal(true);
expect(flagResult.flagCount).to.equal(0);
let groupWithFlags = await admin.get(`/groups/${group._id}`);
let messageToCheck = find(groupWithFlags.chat, {id: message.id});
expect(messageToCheck.flags[newUser._id]).to.equal(true);
// Slack message to mods
const timestamp = `${moment(message.timestamp).utc().format('YYYY-MM-DD HH:mm')} UTC`;
/* eslint-disable camelcase */
expect(IncomingWebhook.prototype.send).to.be.calledWith({
text: `${newUser.profile.name} (${newUser.id}; language: en) flagged a message`,
attachments: [{
fallback: 'Flag Message',
color: 'danger',
author_name: `${newUser.profile.name} - ${newUser.auth.local.email} - ${newUser._id}\n${timestamp}`,
title: 'Flag in Test Guild',
title_link: `${BASE_URL}/groups/guild/${group._id}`,
text: TEST_MESSAGE,
footer: `<https://habitrpg.github.io/flag-o-rama/?groupId=${group._id}&chatId=${message.id}|Flag this message.> ${automatedComment}`,
mrkdwn_in: [
'text',
],
}],
});
/* eslint-ensable camelcase */
});
it('Flags a chat when the author\'s account was deleted', async () => {
@@ -117,7 +185,7 @@ describe('POST /chat/:chatId/flag', () => {
});
});
it('Returns an error when user tries to flag a message that is already flagged', async () => {
it('Returns an error when user tries to flag a message that they already flagged', async () => {
let { message } = await anotherUser.post(`/groups/${group._id}/chat`, {message: TEST_MESSAGE});
await user.post(`/groups/${group._id}/chat/${message.id}/flag`);

View File

@@ -1,3 +1,5 @@
import { IncomingWebhook } from '@slack/client';
import nconf from 'nconf';
import {
createAndPopulateGroup,
generateUser,
@@ -15,8 +17,6 @@ import { getMatchesByWordArray } from '../../../../../website/server/libs/string
import bannedWords from '../../../../../website/server/libs/bannedWords';
import guildsAllowingBannedWords from '../../../../../website/server/libs/guildsAllowingBannedWords';
import * as email from '../../../../../website/server/libs/email';
import { IncomingWebhook } from '@slack/client';
import nconf from 'nconf';
const BASE_URL = nconf.get('BASE_URL');
@@ -80,12 +80,14 @@ describe('POST /chat', () => {
});
});
it('returns an error when chat privileges are revoked when sending a message to a public guild', async () => {
let userWithChatRevoked = await member.update({'flags.chatRevoked': true});
await expect(userWithChatRevoked.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('chatPrivilegesRevoked'),
describe('mute user', () => {
it('returns an error when chat privileges are revoked when sending a message to a public guild', async () => {
const userWithChatRevoked = await member.update({'flags.chatRevoked': true});
await expect(userWithChatRevoked.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('chatPrivilegesRevoked'),
});
});
});
@@ -259,7 +261,6 @@ describe('POST /chat', () => {
title: 'Slur in Test Guild',
title_link: `${BASE_URL}/groups/guild/${groupWithChat.id}`,
text: testSlurMessage,
// footer: sandbox.match(/<.*?groupId=group-id&chatId=chat-id\|Flag this message>/),
mrkdwn_in: [
'text',
],
@@ -274,6 +275,7 @@ describe('POST /chat', () => {
message: t('chatPrivilegesRevoked'),
});
// @TODO: The next test should not depend on this. We should reset the user test in a beforeEach
// Restore chat privileges to continue testing
user.flags.chatRevoked = false;
await user.update({'flags.chatRevoked': false});
@@ -312,7 +314,6 @@ describe('POST /chat', () => {
title: 'Slur in Party - (private party)',
title_link: undefined,
text: testSlurMessage,
// footer: sandbox.match(/<.*?groupId=group-id&chatId=chat-id\|Flag this message>/),
mrkdwn_in: [
'text',
],
@@ -388,6 +389,23 @@ describe('POST /chat', () => {
expect(groupMessages[0].id).to.exist;
});
it('creates a chat with a max length of 3000 chars', async () => {
const veryLongMessage = `
123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789.
THIS PART WON'T BE IN THE MESSAGE (over 3000)
`;
const newMessage = await user.post(`/groups/${groupWithChat._id}/chat`, { message: veryLongMessage});
const groupMessages = await user.get(`/groups/${groupWithChat._id}/chat`);
expect(newMessage.message.id).to.exist;
expect(groupMessages[0].id).to.exist;
expect(newMessage.message.text.length).to.eql(3000);
expect(newMessage.message.text).to.not.contain('MESSAGE');
expect(groupMessages[0].text.length).to.eql(3000);
});
it('creates a chat with user styles', async () => {
const mount = 'test-mount';
const pet = 'test-pet';

View File

@@ -4,23 +4,24 @@ import {
translate as t,
} from '../../../../helpers/api-integration/v3';
import config from '../../../../../config.json';
import moment from 'moment';
import { v4 as generateUUID } from 'uuid';
describe('POST /groups/:id/chat/:id/clearflags', () => {
const USER_AGE_FOR_FLAGGING = 3;
let groupWithChat, message, author, nonAdmin, admin;
before(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'public',
},
members: 1,
});
groupWithChat = group;
author = groupLeader;
nonAdmin = members[0];
nonAdmin = await generateUser({'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate()});
admin = await generateUser({'contributor.admin': true});
message = await author.post(`/groups/${groupWithChat._id}/chat`, { message: 'Some message' });
@@ -69,9 +70,14 @@ describe('POST /groups/:id/chat/:id/clearflags', () => {
privateMessage = privateMessage.message;
await admin.post(`/groups/${group._id}/chat/${privateMessage.id}/flag`);
// first test that the flag was actually successful
let messages = await members[0].get(`/groups/${group._id}/chat`);
expect(messages[0].flagCount).to.eql(5);
await admin.post(`/groups/${group._id}/chat/${privateMessage.id}/clearflags`);
let messages = await members[0].get(`/groups/${group._id}/chat`);
messages = await members[0].get(`/groups/${group._id}/chat`);
expect(messages[0].flagCount).to.eql(0);
});
@@ -100,7 +106,7 @@ describe('POST /groups/:id/chat/:id/clearflags', () => {
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('messageCannotFlagSystemMessages', {communityManagerEmail: config.EMAILS.COMMUNITY_MANAGER_EMAIL}),
message: t('messageCannotFlagSystemMessages', {communityManagerEmail: config.EMAILS_COMMUNITY_MANAGER_EMAIL}),
});
// let messages = await members[0].get(`/groups/${group._id}/chat`);
// expect(messages[0].id).to.eql(skillMsg.id);

View File

@@ -23,6 +23,17 @@ describe('GET /export/userdata.xml', () => {
]);
// add pinnedItem
await user.get('/user/toggle-pinned-item/marketGear/gear.flat.shield_rogue_5');
// add a private message
let receiver = await generateUser();
user.post('/members/send-private-message', {
message: 'Your first message, hi!',
toUserId: receiver._id,
});
let response = await user.get('/export/userdata.xml');
let {user: res} = await parseStringAsync(response, {explicitArray: false});

View File

@@ -203,7 +203,7 @@ describe('GET /groups', () => {
let page2 = await expect(user.get('/groups?type=publicGuilds&paginate=true&page=2'))
.to.eventually.have.a.lengthOf(1 + 4); // 1 created now, 4 by other tests
expect(page2[4].name).to.equal('guild with less members');
});
}).timeout(10000);
});
it('returns all the user\'s guilds when guilds passed in as query', async () => {

View File

@@ -50,6 +50,14 @@ describe('GET /groups/:groupId/invites', () => {
_id: invited._id,
id: invited._id,
profile: {name: invited.profile.name},
auth: {
local: {
username: invited.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
});
@@ -58,7 +66,7 @@ describe('GET /groups/:groupId/invites', () => {
let invited = await generateUser();
await user.post(`/groups/${group._id}/invite`, {uuids: [invited._id]});
let res = await user.get('/groups/party/invites');
expect(res[0]).to.have.all.keys(['_id', 'id', 'profile']);
expect(res[0]).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile']);
expect(res[0].profile).to.have.all.keys(['name']);
});
@@ -76,10 +84,10 @@ describe('GET /groups/:groupId/invites', () => {
let res = await leader.get(`/groups/${group._id}/invites`);
expect(res.length).to.equal(30);
res.forEach(member => {
expect(member).to.have.all.keys(['_id', 'id', 'profile']);
expect(member).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile']);
expect(member.profile).to.have.all.keys(['name']);
});
});
}).timeout(10000);
it('supports using req.query.lastId to get more invites', async function () {
this.timeout(30000); // @TODO: times out after 8 seconds

View File

@@ -56,13 +56,21 @@ describe('GET /groups/:groupId/members', () => {
_id: user._id,
id: user._id,
profile: {name: user.profile.name},
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
});
it('populates only some fields', async () => {
await generateGroup(user, {type: 'party', name: generateUUID()});
let res = await user.get('/groups/party/members');
expect(res[0]).to.have.all.keys(['_id', 'id', 'profile']);
expect(res[0]).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile']);
expect(res[0].profile).to.have.all.keys(['name']);
});
@@ -74,10 +82,10 @@ describe('GET /groups/:groupId/members', () => {
'_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party',
'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives', 'flags',
]);
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
expect(Object.keys(memberRes.auth)).to.eql(['local', 'timestamps']);
expect(Object.keys(memberRes.preferences).sort()).to.eql([
'size', 'hair', 'skin', 'shirt',
'chair', 'costume', 'sleep', 'background', 'tasks',
'chair', 'costume', 'sleep', 'background', 'tasks', 'disableClasses',
].sort());
expect(memberRes.stats.maxMP).to.exist;
@@ -95,10 +103,10 @@ describe('GET /groups/:groupId/members', () => {
'_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party',
'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives', 'flags',
]);
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
expect(Object.keys(memberRes.auth)).to.eql(['local', 'timestamps']);
expect(Object.keys(memberRes.preferences).sort()).to.eql([
'size', 'hair', 'skin', 'shirt',
'chair', 'costume', 'sleep', 'background', 'tasks',
'chair', 'costume', 'sleep', 'background', 'tasks', 'disableClasses',
].sort());
expect(memberRes.stats.maxMP).to.exist;
@@ -120,7 +128,7 @@ describe('GET /groups/:groupId/members', () => {
let res = await user.get('/groups/party/members');
expect(res.length).to.equal(30);
res.forEach(member => {
expect(member).to.have.all.keys(['_id', 'id', 'profile']);
expect(member).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile']);
expect(member.profile).to.have.all.keys(['name']);
});
});
@@ -137,7 +145,7 @@ describe('GET /groups/:groupId/members', () => {
let res = await user.get('/groups/party/members?includeAllMembers=true');
expect(res.length).to.equal(30);
res.forEach(member => {
expect(member).to.have.all.keys(['_id', 'id', 'profile']);
expect(member).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile']);
expect(member.profile).to.have.all.keys(['name']);
});
});

View File

@@ -23,6 +23,73 @@ describe('Post /groups/:groupId/invite', () => {
});
});
describe('username invites', () => {
it('returns an error when invited user is not found', async () => {
const fakeID = 'fakeuserid';
await expect(inviter.post(`/groups/${group._id}/invite`, {
usernames: [fakeID],
}))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('userWithUsernameNotFound', {username: fakeID}),
});
});
it('returns an error when inviting yourself to a group', async () => {
await expect(inviter.post(`/groups/${group._id}/invite`, {
usernames: [inviter.auth.local.lowerCaseUsername],
}))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('cannotInviteSelfToGroup'),
});
});
it('invites a user to a group by username', async () => {
const userToInvite = await generateUser();
await expect(inviter.post(`/groups/${group._id}/invite`, {
usernames: [userToInvite.auth.local.lowerCaseUsername],
})).to.eventually.deep.equal([{
id: group._id,
name: groupName,
inviter: inviter._id,
publicGuild: false,
}]);
await expect(userToInvite.get('/user'))
.to.eventually.have.nested.property('invitations.guilds[0].id', group._id);
});
it('invites multiple users to a group by uuid', async () => {
const userToInvite = await generateUser();
const userToInvite2 = await generateUser();
await expect(inviter.post(`/groups/${group._id}/invite`, {
usernames: [userToInvite.auth.local.lowerCaseUsername, userToInvite2.auth.local.lowerCaseUsername],
})).to.eventually.deep.equal([
{
id: group._id,
name: groupName,
inviter: inviter._id,
publicGuild: false,
},
{
id: group._id,
name: groupName,
inviter: inviter._id,
publicGuild: false,
},
]);
await expect(userToInvite.get('/user')).to.eventually.have.nested.property('invitations.guilds[0].id', group._id);
await expect(userToInvite2.get('/user')).to.eventually.have.nested.property('invitations.guilds[0].id', group._id);
});
});
describe('user id invites', () => {
it('returns an error when inviter has no chat privileges', async () => {
let inviterMuted = await inviter.update({'flags.chatRevoked': true});
@@ -93,7 +160,7 @@ describe('Post /groups/:groupId/invite', () => {
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('inviteMissingUuid'),
message: t('inviteMustNotBeEmpty'),
});
});
@@ -228,7 +295,7 @@ describe('Post /groups/:groupId/invite', () => {
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('inviteMissingEmail'),
message: t('inviteMustNotBeEmpty'),
});
});
@@ -266,7 +333,7 @@ describe('Post /groups/:groupId/invite', () => {
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('inviteLimitReached', {techAssistanceEmail: nconf.get('EMAILS:TECH_ASSISTANCE_EMAIL')}),
message: t('inviteLimitReached', {techAssistanceEmail: nconf.get('EMAILS_TECH_ASSISTANCE_EMAIL')}),
});
});
@@ -417,7 +484,7 @@ describe('Post /groups/:groupId/invite', () => {
expect(await inviter.post(`/groups/${group._id}/invite`, {
uuids: generatedInvites.map(invite => invite._id),
})).to.be.an('array');
});
}).timeout(10000);
// @TODO: Add this after we are able to mock the group plan route
xit('returns an error when a non-leader invites to a group plan', async () => {
@@ -564,7 +631,7 @@ describe('Post /groups/:groupId/invite', () => {
expect(await inviter.post(`/groups/${party._id}/invite`, {
uuids: generatedInvites.map(invite => invite._id),
})).to.be.an('array');
});
}).timeout(10000);
it('does not allow 30+ members in a party', async () => {
let invitesToGenerate = [];
@@ -582,6 +649,6 @@ describe('Post /groups/:groupId/invite', () => {
error: 'BadRequest',
message: t('partyExceedsMembersLimit', {maxMembersParty: PARTY_LIMIT_MEMBERS}),
});
});
}).timeout(10000);
});
});

View File

@@ -56,5 +56,5 @@ describe('GET /hall/patrons', () => {
expect(morePatrons.length).to.equal(2);
expect(morePatrons[0].backer.tier).to.equal(2);
expect(morePatrons[1].backer.tier).to.equal(1);
});
}).timeout(10000);
});

View File

@@ -1,6 +1,6 @@
import {
generateUser,
} from '../../../helpers/api-integration/v4';
} from '../../../../helpers/api-integration/v3';
describe('GET /inbox/messages', () => {
let user;
@@ -22,17 +22,27 @@ describe('GET /inbox/messages', () => {
message: 'third',
});
// message to yourself
await user.post('/members/send-private-message', {
toUserId: user.id,
message: 'fourth',
});
await user.sync();
});
it('returns the user inbox messages as an array of ordered messages (from most to least recent)', async () => {
const messages = await user.get('/inbox/messages');
expect(messages.length).to.equal(3);
expect(messages.length).to.equal(Object.keys(user.inbox.messages).length);
expect(messages.length).to.equal(4);
expect(messages[0].text).to.equal('third');
expect(messages[1].text).to.equal('second');
expect(messages[2].text).to.equal('first');
// message to yourself
expect(messages[0].text).to.equal('fourth');
expect(messages[0].sent).to.equal(false);
expect(messages[0].uuid).to.equal(user._id);
expect(messages[1].text).to.equal('third');
expect(messages[2].text).to.equal('second');
expect(messages[3].text).to.equal('first');
});
});
});

View File

@@ -34,10 +34,10 @@ describe('GET /members/:memberId', () => {
'_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party',
'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives', 'flags',
]);
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
expect(Object.keys(memberRes.auth)).to.eql(['local', 'timestamps']);
expect(Object.keys(memberRes.preferences).sort()).to.eql([
'size', 'hair', 'skin', 'shirt',
'chair', 'costume', 'sleep', 'background', 'tasks',
'chair', 'costume', 'sleep', 'background', 'tasks', 'disableClasses',
].sort());
expect(memberRes.stats.maxMP).to.exist;

View File

@@ -100,7 +100,7 @@ describe('POST /members/send-private-message', () => {
let receiver = await generateUser();
// const initialNotifications = receiver.notifications.length;
await userToSendMessage.post('/members/send-private-message', {
const response = await userToSendMessage.post('/members/send-private-message', {
message: messageToSend,
toUserId: receiver._id,
});
@@ -116,6 +116,9 @@ describe('POST /members/send-private-message', () => {
return message.uuid === receiver._id && message.text === messageToSend;
});
expect(response.message.text).to.deep.equal(sendersMessageInSendersInbox.text);
expect(response.message.uuid).to.deep.equal(sendersMessageInSendersInbox.uuid);
// @TODO waiting for mobile support
// expect(updatedReceiver.notifications.length).to.equal(initialNotifications + 1);
// const notification = updatedReceiver.notifications[updatedReceiver.notifications.length - 1];

View File

@@ -0,0 +1,39 @@
import {
createAndPopulateGroup,
} from '../../../../helpers/api-integration/v3';
describe('Prevent multiple notifications', () => {
let partyLeader, partyMembers, party;
before(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
type: 'party',
privacy: 'private',
},
members: 4,
});
party = group;
partyLeader = groupLeader;
partyMembers = members;
});
it('does not add the same notification twice', async () => {
const multipleChatMessages = [];
for (let i = 0; i < 4; i++) {
for (let memberIndex = 0; memberIndex < partyMembers.length; memberIndex++) {
multipleChatMessages.push(
partyMembers[memberIndex].post(`/groups/${party._id}/chat`, { message: `Message ${i}_${memberIndex}`}),
);
}
}
await Promise.all(multipleChatMessages);
const userWithNotification = await partyLeader.get('/user');
expect(userWithNotification.notifications.length).to.be.eq(1);
});
});

View File

@@ -23,7 +23,7 @@ describe('payments : amazon #subscribeCancel', () => {
describe('success', () => {
beforeEach(() => {
amazonSubscribeCancelStub = sinon.stub(amzLib, 'cancelSubscription').returnsPromise().resolves({});
amazonSubscribeCancelStub = sinon.stub(amzLib, 'cancelSubscription').resolves({});
});
afterEach(() => {

View File

@@ -21,7 +21,7 @@ describe('payments - amazon - #checkout', () => {
describe('success', () => {
beforeEach(async () => {
amazonCheckoutStub = sinon.stub(amzLib, 'checkout').returnsPromise().resolves({});
amazonCheckoutStub = sinon.stub(amzLib, 'checkout').resolves({});
});
afterEach(() => {

View File

@@ -27,7 +27,7 @@ describe('payments - amazon - #subscribe', () => {
let coupon;
beforeEach(() => {
subscribeWithAmazonStub = sinon.stub(amzLib, 'subscribe').returnsPromise().resolves({});
subscribeWithAmazonStub = sinon.stub(amzLib, 'subscribe').resolves({});
});
afterEach(() => {

View File

@@ -13,7 +13,7 @@ describe('payments : apple #cancelSubscribe', () => {
let cancelStub;
beforeEach(async () => {
cancelStub = sinon.stub(applePayments, 'cancelSubscribe').returnsPromise().resolves({});
cancelStub = sinon.stub(applePayments, 'cancelSubscribe').resolves({});
});
afterEach(() => {

View File

@@ -0,0 +1,67 @@
import {generateUser, translate as t} from '../../../../../helpers/api-integration/v3';
import applePayments from '../../../../../../website/server/libs/payments/apple';
describe('payments : apple #norenewsubscribe', () => {
let endpoint = '/iap/ios/norenew-subscribe';
let sku = 'com.habitrpg.ios.habitica.subscription.3month';
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'),
});
});
it('verifies receipt existence', async () => {
await expect(user.post(endpoint, {
sku,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingReceipt'),
});
});
describe('success', () => {
let subscribeStub;
beforeEach(async () => {
subscribeStub = sinon.stub(applePayments, 'noRenewSubscribe').resolves({});
});
afterEach(() => {
applePayments.noRenewSubscribe.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, {
sku,
transaction: {receipt: 'receipt'},
gift: {
uuid: '1',
},
});
expect(subscribeStub).to.be.calledOnce;
expect(subscribeStub.args[0][0].user._id).to.eql(user._id);
expect(subscribeStub.args[0][0].sku).to.eql(sku);
expect(subscribeStub.args[0][0].receipt).to.eql('receipt');
expect(subscribeStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken);
expect(subscribeStub.args[0][0].headers['x-api-user']).to.eql(user._id);
});
});
});

View File

@@ -21,7 +21,7 @@ describe('payments : apple #subscribe', () => {
let subscribeStub;
beforeEach(async () => {
subscribeStub = sinon.stub(applePayments, 'subscribe').returnsPromise().resolves({});
subscribeStub = sinon.stub(applePayments, 'subscribe').resolves({});
});
afterEach(() => {

View File

@@ -1,4 +1,4 @@
import {generateUser} from '../../../../../helpers/api-integration/v3';
import {generateUser, translate as t} from '../../../../../helpers/api-integration/v3';
import applePayments from '../../../../../../website/server/libs/payments/apple';
describe('payments : apple #verify', () => {
@@ -9,11 +9,19 @@ describe('payments : apple #verify', () => {
user = await generateUser();
});
it('verifies receipt existence', async () => {
await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingReceipt'),
});
});
describe('success', () => {
let verifyStub;
beforeEach(async () => {
verifyStub = sinon.stub(applePayments, 'verifyGemPurchase').returnsPromise().resolves({});
verifyStub = sinon.stub(applePayments, 'verifyGemPurchase').resolves({});
});
afterEach(() => {
@@ -31,10 +39,31 @@ describe('payments : apple #verify', () => {
}});
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);
expect(verifyStub.args[0][0].user._id).to.eql(user._id);
expect(verifyStub.args[0][0].receipt).to.eql('receipt');
expect(verifyStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken);
expect(verifyStub.args[0][0].headers['x-api-user']).to.eql(user._id);
});
it('gifts a purchase', async () => {
user = await generateUser({
balance: 2,
});
await user.post(endpoint, {
transaction: {
receipt: 'receipt',
},
gift: {
uuid: '1',
}});
expect(verifyStub).to.be.calledOnce;
expect(verifyStub.args[0][0].user._id).to.eql(user._id);
expect(verifyStub.args[0][0].receipt).to.eql('receipt');
expect(verifyStub.args[0][0].gift.uuid).to.eql('1');
expect(verifyStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken);
expect(verifyStub.args[0][0].headers['x-api-user']).to.eql(user._id);
});
});
});

View File

@@ -13,7 +13,7 @@ describe('payments : google #cancelSubscribe', () => {
let cancelStub;
beforeEach(async () => {
cancelStub = sinon.stub(googlePayments, 'cancelSubscribe').returnsPromise().resolves({});
cancelStub = sinon.stub(googlePayments, 'cancelSubscribe').resolves({});
});
afterEach(() => {

View File

@@ -0,0 +1,97 @@
import {generateUser, translate as t} from '../../../../../helpers/api-integration/v3';
import googlePayments from '../../../../../../website/server/libs/payments/google';
describe('payments : google #norenewsubscribe', () => {
let endpoint = '/iap/android/norenew-subscribe';
let sku = 'com.habitrpg.android.habitica.subscription.3month';
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'),
});
});
it('verifies receipt existence', async () => {
await expect(user.post(endpoint, {
sku,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingReceipt'),
});
});
describe('success', () => {
let subscribeStub;
beforeEach(async () => {
subscribeStub = sinon.stub(googlePayments, 'noRenewSubscribe').resolves({});
});
afterEach(() => {
googlePayments.noRenewSubscribe.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, {
sku,
transaction: {
receipt: 'receipt',
signature: 'signature',
},
});
expect(subscribeStub).to.be.calledOnce;
expect(subscribeStub.args[0][0].user._id).to.eql(user._id);
expect(subscribeStub.args[0][0].sku).to.eql(sku);
expect(subscribeStub.args[0][0].receipt).to.eql('receipt');
expect(subscribeStub.args[0][0].signature).to.eql('signature');
expect(subscribeStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken);
expect(subscribeStub.args[0][0].headers['x-api-user']).to.eql(user._id);
});
it('gifts 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, {
sku,
transaction: {
receipt: 'receipt',
signature: 'signature',
},
gift: {
uuid: '1',
},
});
expect(subscribeStub).to.be.calledOnce;
expect(subscribeStub.args[0][0].user._id).to.eql(user._id);
expect(subscribeStub.args[0][0].sku).to.eql(sku);
expect(subscribeStub.args[0][0].receipt).to.eql('receipt');
expect(subscribeStub.args[0][0].signature).to.eql('signature');
expect(subscribeStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken);
expect(subscribeStub.args[0][0].headers['x-api-user']).to.eql(user._id);
});
});
});

View File

@@ -21,7 +21,7 @@ describe('payments : google #subscribe', () => {
let subscribeStub;
beforeEach(async () => {
subscribeStub = sinon.stub(googlePayments, 'subscribe').returnsPromise().resolves({});
subscribeStub = sinon.stub(googlePayments, 'subscribe').resolves({});
});
afterEach(() => {

View File

@@ -1,4 +1,4 @@
import {generateUser} from '../../../../../helpers/api-integration/v3';
import {generateUser, translate as t} from '../../../../../helpers/api-integration/v3';
import googlePayments from '../../../../../../website/server/libs/payments/google';
describe('payments : google #verify', () => {
@@ -9,11 +9,19 @@ describe('payments : google #verify', () => {
user = await generateUser();
});
it('verifies receipt existence', async () => {
await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingReceipt'),
});
});
describe('success', () => {
let verifyStub;
beforeEach(async () => {
verifyStub = sinon.stub(googlePayments, 'verifyGemPurchase').returnsPromise().resolves({});
verifyStub = sinon.stub(googlePayments, 'verifyGemPurchase').resolves({});
});
afterEach(() => {
@@ -30,11 +38,30 @@ describe('payments : google #verify', () => {
});
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);
expect(verifyStub.args[0][0].user._id).to.eql(user._id);
expect(verifyStub.args[0][0].receipt).to.eql('receipt');
expect(verifyStub.args[0][0].signature).to.eql('signature');
expect(verifyStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken);
expect(verifyStub.args[0][0].headers['x-api-user']).to.eql(user._id);
});
it('gifts a purchase', async () => {
user = await generateUser({
balance: 2,
});
await user.post(endpoint, {
transaction: {receipt: 'receipt', signature: 'signature'},
gift: {uuid: '1'},
});
expect(verifyStub).to.be.calledOnce;
expect(verifyStub.args[0][0].user._id).to.eql(user._id);
expect(verifyStub.args[0][0].receipt).to.eql('receipt');
expect(verifyStub.args[0][0].signature).to.eql('signature');
expect(verifyStub.args[0][0].gift.uuid).to.eql('1');
expect(verifyStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken);
expect(verifyStub.args[0][0].headers['x-api-user']).to.eql(user._id);
});
});
});

View File

@@ -15,7 +15,7 @@ describe('payments : paypal #checkout', () => {
let checkoutStub;
beforeEach(async () => {
checkoutStub = sinon.stub(paypalPayments, 'checkout').returnsPromise().resolves('/');
checkoutStub = sinon.stub(paypalPayments, 'checkout').resolves('/');
});
afterEach(() => {

View File

@@ -34,7 +34,7 @@ describe('payments : paypal #checkoutSuccess', () => {
let checkoutSuccessStub;
beforeEach(async () => {
checkoutSuccessStub = sinon.stub(paypalPayments, 'checkoutSuccess').returnsPromise().resolves({});
checkoutSuccessStub = sinon.stub(paypalPayments, 'checkoutSuccess').resolves({});
});
afterEach(() => {

View File

@@ -25,7 +25,7 @@ describe('payments : paypal #subscribe', () => {
let subscribeStub;
beforeEach(async () => {
subscribeStub = sinon.stub(paypalPayments, 'subscribe').returnsPromise().resolves('/');
subscribeStub = sinon.stub(paypalPayments, 'subscribe').resolves('/');
});
afterEach(() => {

View File

@@ -25,7 +25,7 @@ describe('payments : paypal #subscribeCancel', () => {
let subscribeCancelStub;
beforeEach(async () => {
subscribeCancelStub = sinon.stub(paypalPayments, 'subscribeCancel').returnsPromise().resolves('/');
subscribeCancelStub = sinon.stub(paypalPayments, 'subscribeCancel').resolves('/');
});
afterEach(() => {

View File

@@ -24,7 +24,7 @@ describe('payments : paypal #subscribeSuccess', () => {
let subscribeSuccessStub;
beforeEach(async () => {
subscribeSuccessStub = sinon.stub(paypalPayments, 'subscribeSuccess').returnsPromise().resolves({});
subscribeSuccessStub = sinon.stub(paypalPayments, 'subscribeSuccess').resolves({});
});
afterEach(() => {

View File

@@ -20,7 +20,7 @@ describe('payments - paypal - #ipn', () => {
let ipnStub;
beforeEach(async () => {
ipnStub = sinon.stub(paypalPayments, 'ipn').returnsPromise().resolves({});
ipnStub = sinon.stub(paypalPayments, 'ipn').resolves({});
});
afterEach(() => {

View File

@@ -6,7 +6,7 @@ import {
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
describe('payments - stripe - #subscribeCancel', () => {
let endpoint = '/stripe/subscribe/cancel?redirect=none';
let endpoint = '/stripe/subscribe/cancel?noRedirect=true';
let user, group, stripeCancelSubscriptionStub;
beforeEach(async () => {
@@ -23,7 +23,7 @@ describe('payments - stripe - #subscribeCancel', () => {
describe('success', () => {
beforeEach(async () => {
stripeCancelSubscriptionStub = sinon.stub(stripePayments, 'cancelSubscription').returnsPromise().resolves({});
stripeCancelSubscriptionStub = sinon.stub(stripePayments, 'cancelSubscription').resolves({});
});
afterEach(() => {

View File

@@ -24,7 +24,7 @@ describe('payments - stripe - #checkout', () => {
let stripeCheckoutSubscriptionStub;
beforeEach(async () => {
stripeCheckoutSubscriptionStub = sinon.stub(stripePayments, 'checkout').returnsPromise().resolves({});
stripeCheckoutSubscriptionStub = sinon.stub(stripePayments, 'checkout').resolves({});
});
afterEach(() => {

View File

@@ -25,7 +25,7 @@ describe('payments - stripe - #subscribeEdit', () => {
let stripeEditSubscriptionStub;
beforeEach(async () => {
stripeEditSubscriptionStub = sinon.stub(stripePayments, 'editSubscription').returnsPromise().resolves({});
stripeEditSubscriptionStub = sinon.stub(stripePayments, 'editSubscription').resolves({});
});
afterEach(() => {

View File

@@ -4,7 +4,7 @@ import {
generateUser,
sleep,
} from '../../../../helpers/api-integration/v3';
import { model as Chat } from '../../../../../website/server/models/chat';
import { chatModel as Chat } from '../../../../../website/server/models/message';
describe('POST /groups/:groupId/quests/accept', () => {
const PET_QUEST = 'whale';

View File

@@ -4,7 +4,7 @@ import {
generateUser,
sleep,
} from '../../../../helpers/api-integration/v3';
import { model as Chat } from '../../../../../website/server/models/chat';
import { chatModel as Chat } from '../../../../../website/server/models/message';
describe('POST /groups/:groupId/quests/force-start', () => {
const PET_QUEST = 'whale';

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