Compare commits

..

307 Commits

Author SHA1 Message Date
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
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
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
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
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
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
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
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
Matteo Pagliazzi 4394772ee3 Revert "Small Updates (#10701)" (#10702)
This reverts commit dd7fa73961.
2018-09-20 22:36:46 +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
Keith Holliday 1c51e62e43 Merged in develop 2018-09-10 09:42:51 -05: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
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
Keith Holliday f049d29d1b Added username invite 2018-08-16 17:40:53 -05: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
negue b75e65f42d move modals to notifications (to open the modals) 2018-07-20 23:56:36 +02:00
957 changed files with 52953 additions and 44790 deletions
+2 -7
View File
@@ -17,7 +17,7 @@
"NODE_DB_URI":"mongodb://localhost/habitrpg",
"TEST_DB_URI":"mongodb://localhost/habitrpg_test",
"NODE_ENV":"development",
"ENABLE_CONSOLE_LOGS_IN_TEST": false,
"ENABLE_CONSOLE_LOGS_IN_TEST": "false",
"CRON_SAFE_MODE":"false",
"CRON_SEMI_SAFE_MODE":"false",
"MAINTENANCE_MODE": "false",
@@ -39,6 +39,7 @@
"NEW_RELIC_API_KEY":"NEW_RELIC_API_KEY",
"GA_ID": "GA_ID",
"AMPLITUDE_KEY": "AMPLITUDE_KEY",
"AMPLITUDE_SECRET": "AMPLITUDE_SECRET",
"AMAZON_PAYMENTS": {
"SELLER_ID": "SELLER_ID",
"CLIENT_ID": "CLIENT_ID",
@@ -88,12 +89,6 @@
"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/",
@@ -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;
@@ -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;
@@ -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;
@@ -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;
+9 -2
View File
@@ -17,5 +17,12 @@ function setUpServer () {
setUpServer();
// Replace this with your migration
const processUsers = require('./users/takeThis.js');
processUsers();
const processUsers = require('./users/20181122_turkey_day.js');
processUsers()
.then(function success () {
process.exit(0);
})
.catch(function failure (err) {
console.log(err);
process.exit(1);
});
@@ -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
}
};
@@ -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;
+109
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
}
};
+99
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;
+1 -1
View File
@@ -8,7 +8,7 @@ const authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is do
/*
* Award this month's mystery items to subscribers
*/
const MYSTERY_ITEMS = ['armor_mystery_201809', 'head_mystery_201809'];
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 });
+8260 -8709
View File
File diff suppressed because it is too large Load Diff
+55 -55
View File
@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "4.62.1",
"version": "4.73.1",
"main": "./website/server/index.js",
"dependencies": {
"@slack/client": "^3.8.1",
@@ -10,13 +10,13 @@
"amplitude": "^3.5.0",
"apidoc": "^0.17.5",
"apn": "^2.2.0",
"autoprefixer": "^8.6.5",
"aws-sdk": "^2.317.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",
"babel-eslint": "^8.2.6",
"babel-loader": "^7.1.5",
"babel-eslint": "^8.2.3",
"babel-loader": "^7.1.4",
"babel-plugin-syntax-async-functions": "^6.13.0",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
@@ -26,90 +26,90 @@
"babel-preset-es2015": "^6.6.0",
"babel-register": "^6.6.0",
"babel-runtime": "^6.11.6",
"bcrypt": "github:MylesBorins/node.bcrypt.js#update-nan",
"bcrypt": "^3.0.1",
"body-parser": "^1.18.3",
"bootstrap": "^4.1.3",
"bootstrap": "^4.1.1",
"bootstrap-vue": "^2.0.0-rc.9",
"compression": "^1.7.3",
"compression": "^1.7.2",
"cookie-session": "^1.2.0",
"coupon-code": "^0.4.5",
"cross-env": "^5.2.0",
"cross-env": "^5.1.5",
"css-loader": "^0.28.11",
"csv-stringify": "^3.0.0",
"csv-stringify": "^4.3.1",
"cwait": "^1.1.1",
"domain-middleware": "~0.1.0",
"express": "^4.16.3",
"express-basic-auth": "^1.1.5",
"express-validator": "^5.3.0",
"express-validator": "^5.2.0",
"extract-text-webpack-plugin": "^3.0.2",
"glob": "^7.1.3",
"got": "^9.2.2",
"glob": "^7.1.2",
"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.17.1",
"hellojs": "^1.15.1",
"html-webpack-plugin": "^3.2.0",
"image-size": "^0.6.3",
"in-app-purchase": "^1.10.1",
"image-size": "^0.6.2",
"in-app-purchase": "^1.10.2",
"intro.js": "^2.9.3",
"jquery": ">=3.0.0",
"js2xmlparser": "^3.0.0",
"lodash": "^4.17.11",
"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.2.15",
"morgan": "^1.9.1",
"mongoose": "^5.3.4",
"morgan": "^1.7.0",
"nconf": "^0.10.0",
"node-gcm": "^0.14.4",
"node-sass": "^4.9.3",
"nodemailer": "^4.6.8",
"ora": "^2.1.0",
"node-gcm": "^1.0.2",
"node-sass": "^4.9.0",
"nodemailer": "^4.6.4",
"ora": "^3.0.0",
"pageres": "^4.1.1",
"passport": "^0.4.0",
"passport-facebook": "^2.0.0",
"passport-google-oauth20": "1.0.0",
"paypal-ipn": "3.0.0",
"paypal-rest-sdk": "^1.8.1",
"popper.js": "^1.14.4",
"popper.js": "^1.14.3",
"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.1.0",
"sass-loader": "^7.0.0",
"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.1.1",
"svgo-loader": "^2.2.0",
"universal-analytics": "^0.4.17",
"svgo": "^1.0.5",
"svgo-loader": "^2.1.0",
"universal-analytics": "^0.4.16",
"update": "^0.7.4",
"upgrade": "^1.1.0",
"url-loader": "^1.1.1",
"url-loader": "^1.0.0",
"useragent": "^2.1.9",
"uuid": "^3.3.2",
"validator": "^10.7.1",
"uuid": "^3.0.1",
"validator": "^10.5.0",
"vinyl-buffer": "^1.0.1",
"vue": "^2.5.17",
"vue": "^2.5.16",
"vue-loader": "^14.2.2",
"vue-mugen-scroll": "^0.2.1",
"vue-router": "^3.0.0",
"vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.5.17",
"vue-style-loader": "^4.1.0",
"vue-template-compiler": "^2.5.16",
"vuedraggable": "^2.15.0",
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#5d237615463a84a23dd6f3f77c6ab577d68593ec",
"webpack": "^3.12.0",
"webpack-merge": "^4.1.4",
"winston": "^2.4.4",
"winston-loggly-bulk": "^2.0.3",
"webpack-merge": "^4.0.0",
"winston": "^2.4.2",
"winston-loggly-bulk": "^2.0.2",
"xml2js": "^0.4.4"
},
"private": true,
@@ -144,25 +144,25 @@
"apidoc": "gulp apidoc"
},
"devDependencies": {
"@vue/test-utils": "^1.0.0-beta.25",
"@vue/test-utils": "^1.0.0-beta.16",
"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.41.0",
"chromedriver": "^2.38.3",
"connect-history-api-fallback": "^1.1.0",
"coveralls": "^3.0.2",
"coveralls": "^3.0.1",
"cross-spawn": "^6.0.5",
"eslint": "^4.19.1",
"eslint-config-habitrpg": "^4.0.0",
"eslint-friendly-formatter": "^4.0.1",
"eslint-loader": "^2.1.0",
"eslint-plugin-html": "^4.0.5",
"eslint-plugin-mocha": "^5.2.0",
"eslint-loader": "^2.0.0",
"eslint-plugin-html": "^4.0.3",
"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": "^3.0.0",
"karma-babel-preprocessor": "^7.0.0",
@@ -175,20 +175,20 @@
"karma-sinon-stub-promise": "^1.0.0",
"karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "0.0.32",
"karma-webpack": "^3.0.5",
"karma-webpack": "^3.0.0",
"lcov-result-merger": "^3.0.0",
"mocha": "^5.1.1",
"monk": "^6.0.6",
"nightwatch": "^0.9.21",
"puppeteer": "^1.8.0",
"puppeteer": "^1.4.0",
"require-again": "^2.0.0",
"selenium-server": "^3.14.0",
"sinon": "^4.5.0",
"sinon-chai": "^3.2.0",
"selenium-server": "^3.12.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.24.0"
"webpack-hot-middleware": "^2.22.2"
},
"optionalDependencies": {
"memwatch-next": "^0.3.0",
+88
View File
@@ -0,0 +1,88 @@
/* 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) {
await User.update(
{_id: user._id},
{$set: {
'auth.local.passwordHashMethod': 'bcrypt',
'auth.local.hashed_password': '$2a$10$QDnNh1j1yMPnTXDEOV38xOePEWFd4X8DSYwAM8XTmqmacG5X0DKjW',
}}
);
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); // eslint-disable-line no-await-in-loop
}
}
}
function deleteUserData (emails) {
const emailPromises = emails.map(_processEmailAddress);
return Promise.all(emailPromises);
}
module.exports = deleteUserData;
+8 -1
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);
@@ -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});
@@ -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'),
@@ -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');
});
@@ -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);
+12 -12
View File
@@ -24,16 +24,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(() => {
@@ -70,7 +70,7 @@ describe('Apple Payments', () => {
});
it('errors if the user cannot purchase gems', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
sinon.stub(user, 'canGetGems').resolves(false);
await expect(applePayments.verifyGemPurchase(user, receipt, headers))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
@@ -82,7 +82,7 @@ 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',
@@ -130,7 +130,7 @@ describe('Apple Payments', () => {
transactionId: token,
}]);
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
sinon.stub(user, 'canGetGems').resolves(true);
await applePayments.verifyGemPurchase(user, receipt, headers);
expect(iapSetupStub).to.be.calledOnce;
@@ -167,9 +167,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 +186,7 @@ describe('Apple Payments', () => {
productId: sku,
transactionId: token,
}]);
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({});
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').resolves({});
});
afterEach(() => {
@@ -297,9 +297,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 +314,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 () {
+11 -11
View File
@@ -24,12 +24,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(() => {
@@ -64,7 +64,7 @@ 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))
.to.eventually.be.rejected.and.to.eql({
@@ -77,7 +77,7 @@ describe('Google Payments', () => {
});
it('purchases gems', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
sinon.stub(user, 'canGetGems').resolves(true);
await googlePayments.verifyGemPurchase(user, receipt, signature, headers);
expect(iapSetupStub).to.be.calledOnce;
@@ -116,12 +116,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 +193,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 +210,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 () {
@@ -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
@@ -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);
@@ -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(() => {
@@ -42,7 +42,7 @@ describe('checkout', () => {
beforeEach(() => {
approvalHerf = 'approval_href';
paypalPaymentCreateStub = sinon.stub(paypalPayments, 'paypalPaymentCreate')
.returnsPromise().resolves({
.resolves({
links: [{ rel: 'approval_url', href: approvalHerf }],
});
});
@@ -80,7 +80,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,
@@ -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 () {
@@ -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 () {
@@ -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(() => {
@@ -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 }],
});
});
@@ -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
},
@@ -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;
@@ -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,
@@ -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(() => {
@@ -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: {
@@ -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);
+8
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']);
});
});
+103 -47
View File
@@ -477,7 +477,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);
@@ -496,7 +496,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);
@@ -569,7 +569,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',
@@ -579,7 +579,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',
@@ -589,7 +589,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',
@@ -599,27 +599,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',
@@ -639,7 +639,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',
@@ -657,33 +657,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;
});
});
@@ -1020,32 +1020,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');
@@ -1869,6 +1843,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',
@@ -1958,28 +1988,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;
});
});
});
});
+33 -2
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();
@@ -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,
@@ -105,6 +113,14 @@ describe('GET /challenges/:challengeId', () => {
_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,
@@ -131,6 +147,14 @@ describe('GET /challenges/:challengeId', () => {
_id: challengeLeader._id,
id: challengeLeader._id,
profile: {name: challengeLeader.profile.name},
auth: {
local: {
username: challengeLeader.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
});
});
@@ -179,6 +203,14 @@ describe('GET /challenges/:challengeId', () => {
_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,
@@ -205,6 +237,14 @@ describe('GET /challenges/:challengeId', () => {
_id: challengeLeader._id,
id: challengeLeader._id,
profile: {name: challengeLeader.profile.name},
auth: {
local: {
username: challengeLeader.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
});
});
@@ -60,6 +60,14 @@ describe('GET /challenges/:challengeId/members', () => {
_id: groupLeader._id,
id: groupLeader._id,
profile: {name: groupLeader.profile.name},
auth: {
local: {
username: groupLeader.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
});
@@ -73,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']);
});
@@ -88,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']);
});
@@ -107,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']);
});
});
@@ -126,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']);
});
});
@@ -145,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']);
});
});
@@ -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);
});
@@ -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,
},
});
});
});
@@ -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,
@@ -79,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);
});
@@ -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.');
@@ -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 () => {
@@ -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
@@ -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,7 +82,7 @@ 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', 'disableClasses',
@@ -95,7 +103,7 @@ 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', 'disableClasses',
@@ -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']);
});
});
@@ -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'),
});
});
@@ -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);
});
});
@@ -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);
});
@@ -38,6 +38,7 @@ describe('GET /inbox/messages', () => {
// 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');
@@ -34,7 +34,7 @@ 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', 'disableClasses',
@@ -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(() => {
@@ -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(() => {
@@ -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(() => {
@@ -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(() => {
@@ -13,7 +13,7 @@ describe('payments : apple #verify', () => {
let verifyStub;
beforeEach(async () => {
verifyStub = sinon.stub(applePayments, 'verifyGemPurchase').returnsPromise().resolves({});
verifyStub = sinon.stub(applePayments, 'verifyGemPurchase').resolves({});
});
afterEach(() => {
@@ -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(() => {
@@ -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(() => {
@@ -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(() => {
@@ -13,7 +13,7 @@ describe('payments : google #verify', () => {
let verifyStub;
beforeEach(async () => {
verifyStub = sinon.stub(googlePayments, 'verifyGemPurchase').returnsPromise().resolves({});
verifyStub = sinon.stub(googlePayments, 'verifyGemPurchase').resolves({});
});
afterEach(() => {
@@ -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(() => {
@@ -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(() => {
@@ -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(() => {
@@ -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(() => {
@@ -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(() => {
@@ -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(() => {
@@ -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(() => {
@@ -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(() => {
@@ -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(() => {
@@ -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';
@@ -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';
@@ -5,7 +5,7 @@ import {
} from '../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
import { quests as questScrolls } from '../../../../../website/common/script/content';
import { model as Chat } from '../../../../../website/server/models/chat';
import { chatModel as Chat } from '../../../../../website/server/models/message';
import apiError from '../../../../../website/server/libs/apiError';
describe('POST /groups/:groupId/quests/invite/:questKey', () => {
@@ -5,7 +5,7 @@ import {
sleep,
} from '../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
import { model as Chat } from '../../../../../website/server/models/chat';
import { chatModel as Chat } from '../../../../../website/server/models/message';
describe('POST /groups/:groupId/quests/reject', () => {
let questingGroup;
@@ -1,7 +1,7 @@
import {
createAndPopulateGroup,
createAndPopulateGroup, translate as t,
} from '../../../../../helpers/api-integration/v3';
import { find } from 'lodash';
import {find} from 'lodash';
describe('PUT /tasks/:id', () => {
let user, guild, member, member2, task;
@@ -38,16 +38,64 @@ describe('PUT /tasks/:id', () => {
it('updates a group task', async () => {
let savedHabit = await user.put(`/tasks/${task._id}`, {
text: 'some new text',
up: false,
down: false,
notes: 'some new notes',
});
expect(savedHabit.text).to.eql('some new text');
expect(savedHabit.notes).to.eql('some new notes');
expect(savedHabit.up).to.eql(false);
expect(savedHabit.down).to.eql(false);
});
it('updates a group task - approval is required', async () => {
// allow to manage
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member._id,
});
// change the todo
task = await member.put(`/tasks/${task._id}`, {
text: 'new text!',
requiresApproval: true,
});
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, (memberTask) => memberTask.group.taskId === task._id);
// score up to trigger approval
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
});
it('updates a group task with checklist', async () => {
// add a new todo
task = await user.post(`/tasks/group/${guild._id}`, {
text: 'todo',
type: 'todo',
checklist: [
{
text: 'checklist 1',
},
],
});
await user.post(`/tasks/${task._id}/assign/${member._id}`);
// change the checklist text
task = await user.put(`/tasks/${task._id}`, {
checklist: [
{
id: task.checklist[0].id,
text: 'checklist 1 - edit',
},
{
text: 'checklist 2 - edit',
},
],
});
expect(task.checklist.length).to.eql(2);
});
it('updates the linked tasks', async () => {
@@ -20,8 +20,8 @@ describe('DELETE user message', () => {
messagesId = Object.keys(userRes.inbox.messages);
expect(messagesId.length).to.eql(2);
expect(userRes.inbox.messages[messagesId[0]].text).to.eql('first');
expect(userRes.inbox.messages[messagesId[1]].text).to.eql('second');
expect(userRes.inbox.messages[messagesId[0]].text).to.eql('second');
expect(userRes.inbox.messages[messagesId[1]].text).to.eql('first');
});
it('one message', async () => {
@@ -31,7 +31,7 @@ describe('DELETE user message', () => {
let userRes = await user.get('/user');
expect(Object.keys(userRes.inbox.messages).length).to.eql(1);
expect(userRes.inbox.messages[messagesId[0]].text).to.eql('second');
expect(userRes.inbox.messages[messagesId[0]].text).to.eql('first');
});
it('clear all', async () => {
@@ -39,14 +39,16 @@ describe('POST /user/push-devices', () => {
});
});
it('returns an error if user already has the push device', async () => {
it('fails silently if user already has the push device', async () => {
await user.post('/user/push-devices', {type, regId});
await expect(user.post('/user/push-devices', {type, regId}))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('pushDeviceAlreadyAdded'),
});
const response = await user.post('/user/push-devices', {type, regId});
await user.sync();
expect(response.message).to.equal(t('pushDeviceAdded'));
expect(response.data[0].type).to.equal(type);
expect(response.data[0].regId).to.equal(regId);
expect(user.pushDevices[0].type).to.equal(type);
expect(user.pushDevices[0].regId).to.equal(regId);
});
it('adds a push device to the user', async () => {
+18 -2
View File
@@ -54,7 +54,7 @@ describe('PUT /user', () => {
});
it('profile.name cannot be an empty string or null', async () => {
it('validates profile.name', async () => {
await expect(user.put('/user', {
'profile.name': ' ', // string should be trimmed
})).to.eventually.be.rejected.and.eql({
@@ -76,7 +76,23 @@ describe('PUT /user', () => {
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'User validation failed',
message: t('invalidReqParams'),
});
await expect(user.put('/user', {
'profile.name': 'this is a very long display name that will not be allowed due to length',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('displaynameIssueLength'),
});
await expect(user.put('/user', {
'profile.name': 'TESTPLACEHOLDERSLURWORDHERE',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('displaynameIssueSlur'),
});
});
});
@@ -41,6 +41,23 @@ describe('POST /user/auth/local/register', () => {
expect(user.newUser).to.eql(true);
});
it('registers a new user and sets verifiedUsername to true', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user._id).to.exist;
expect(user.apiToken).to.exist;
expect(user.flags.verifiedUsername).to.eql(true);
});
xit('remove spaces from username', async () => {
// TODO can probably delete this test now
let username = ' usernamewithspaces ';
@@ -259,7 +276,7 @@ describe('POST /user/auth/local/register', () => {
});
});
it('enrolls new users in an A/B test', async () => {
xit('enrolls new users in an A/B test', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
@@ -1,102 +0,0 @@
/* eslint-disable camelcase */
import {
generateUser,
requester,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
describe('POST /user/auth/pusher', () => {
let user;
let endpoint = '/user/auth/pusher';
beforeEach(async () => {
user = await generateUser();
});
it('requires authentication', async () => {
let api = requester();
await expect(api.post(endpoint)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('missingAuthHeaders'),
});
});
it('returns an error if req.body.socket_id is missing', async () => {
await expect(user.post(endpoint, {
channel_name: '123',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('returns an error if req.body.channel_name is missing', async () => {
await expect(user.post(endpoint, {
socket_id: '123',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('returns an error if req.body.channel_name is badly formatted', async () => {
await expect(user.post(endpoint, {
channel_name: '123',
socket_id: '123',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Invalid Pusher channel type.',
});
});
it('returns an error if an invalid channel type is passed', async () => {
await expect(user.post(endpoint, {
channel_name: 'invalid-group-123',
socket_id: '123',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Invalid Pusher channel type.',
});
});
it('returns an error if an invalid resource type is passed', async () => {
await expect(user.post(endpoint, {
channel_name: 'presence-user-123',
socket_id: '123',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Invalid Pusher resource type.',
});
});
it('returns an error if an invalid resource id is passed', async () => {
await expect(user.post(endpoint, {
channel_name: 'presence-group-123',
socket_id: '123',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Invalid Pusher resource id, must be a UUID.',
});
});
it('returns an error if the passed resource id doesn\'t match the user\'s party', async () => {
await expect(user.post(endpoint, {
channel_name: `presence-group-${generateUUID()}`,
socket_id: '123',
})).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: 'Resource id must be the user\'s party.',
});
});
});
@@ -39,7 +39,7 @@ describe('POST /user/auth/social', () => {
});
it('registers a new user', async () => {
let response = await api.post(endpoint, {
const response = await api.post(endpoint, {
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
network,
});
@@ -47,7 +47,10 @@ describe('POST /user/auth/social', () => {
expect(response.apiToken).to.exist;
expect(response.id).to.exist;
expect(response.newUser).to.be.true;
expect(response.username).to.exist;
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a facebook user');
await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.exist;
});
it('logs an existing user in', async () => {
@@ -77,7 +80,7 @@ describe('POST /user/auth/social', () => {
expect(response.newUser).to.be.false;
});
it('enrolls a new user in an A/B test', async () => {
xit('enrolls a new user in an A/B test', async () => {
await api.post(endpoint, {
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
network,
@@ -133,7 +136,7 @@ describe('POST /user/auth/social', () => {
expect(response.newUser).to.be.false;
});
it('enrolls a new user in an A/B test', async () => {
xit('enrolls a new user in an A/B test', async () => {
await api.post(endpoint, {
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
network,
@@ -12,14 +12,14 @@ const ENDPOINT = '/user/auth/update-username';
describe('PUT /user/auth/update-username', async () => {
let user;
let newUsername = 'new-username';
let password = 'password'; // from habitrpg/test/helpers/api-integration/v3/object-generators.js
let password = 'password'; // from habitrpg/test/helpers/api-integration/v4/object-generators.js
beforeEach(async () => {
user = await generateUser();
});
it('successfully changes username', async () => {
it('successfully changes username with password', async () => {
let newUsername = 'new-username';
let response = await user.put(ENDPOINT, {
username: newUsername,
password,
@@ -29,6 +29,38 @@ describe('PUT /user/auth/update-username', async () => {
expect(user.auth.local.username).to.eql(newUsername);
});
it('successfully changes username without password', async () => {
let newUsername = 'new-username-nopw';
let response = await user.put(ENDPOINT, {
username: newUsername,
});
expect(response).to.eql({ username: newUsername });
await user.sync();
expect(user.auth.local.username).to.eql(newUsername);
});
it('successfully changes username containing number and underscore', async () => {
let newUsername = 'new_username9';
let response = await user.put(ENDPOINT, {
username: newUsername,
});
expect(response).to.eql({ username: newUsername });
await user.sync();
expect(user.auth.local.username).to.eql(newUsername);
});
it('sets verifiedUsername when changing username', async () => {
user.flags.verifiedUsername = false;
await user.sync();
let newUsername = 'new-username-verify';
let response = await user.put(ENDPOINT, {
username: newUsername,
});
expect(response).to.eql({ username: newUsername });
await user.sync();
expect(user.flags.verifiedUsername).to.eql(true);
});
it('converts user with SHA1 encrypted password to bcrypt encryption', async () => {
let myNewUsername = 'my-new-username';
let textPassword = 'mySecretPassword';
@@ -80,6 +112,7 @@ describe('PUT /user/auth/update-username', async () => {
});
it('errors if password is wrong', async () => {
let newUsername = 'new-username';
await expect(user.put(ENDPOINT, {
username: newUsername,
password: 'wrong-password',
@@ -90,19 +123,6 @@ describe('PUT /user/auth/update-username', async () => {
});
});
it('prevents social-only user from changing username', async () => {
let socialUser = await generateUser({ 'auth.local': { ok: true } });
await expect(socialUser.put(ENDPOINT, {
username: newUsername,
password,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('userHasNoLocalRegistration'),
});
});
it('errors if new username is not provided', async () => {
await expect(user.put(ENDPOINT, {
password,
@@ -112,5 +132,93 @@ describe('PUT /user/auth/update-username', async () => {
message: t('invalidReqParams'),
});
});
it('errors if new username is a slur', async () => {
await expect(user.put(ENDPOINT, {
username: 'TESTPLACEHOLDERSLURWORDHERE',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: [t('usernameIssueLength'), t('usernameIssueSlur')].join(' '),
});
});
it('errors if new username contains a slur', async () => {
await expect(user.put(ENDPOINT, {
username: 'TESTPLACEHOLDERSLURWORDHERE_otherword',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: [t('usernameIssueLength'), t('usernameIssueSlur')].join(' '),
});
await expect(user.put(ENDPOINT, {
username: 'something_TESTPLACEHOLDERSLURWORDHERE',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: [t('usernameIssueLength'), t('usernameIssueSlur')].join(' '),
});
await expect(user.put(ENDPOINT, {
username: 'somethingTESTPLACEHOLDERSLURWORDHEREotherword',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: [t('usernameIssueLength'), t('usernameIssueSlur')].join(' '),
});
});
it('errors if new username is not allowed', async () => {
await expect(user.put(ENDPOINT, {
username: 'support',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('usernameIssueForbidden'),
});
});
it('errors if new username is not allowed regardless of casing', async () => {
await expect(user.put(ENDPOINT, {
username: 'SUppORT',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('usernameIssueForbidden'),
});
});
it('errors if username has incorrect length', async () => {
await expect(user.put(ENDPOINT, {
username: 'thisisaverylongusernameover20characters',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('usernameIssueLength'),
});
});
it('errors if new username contains invalid characters', async () => {
await expect(user.put(ENDPOINT, {
username: 'Eichhörnchen',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('usernameIssueInvalidCharacters'),
});
await expect(user.put(ENDPOINT, {
username: 'test.name',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('usernameIssueInvalidCharacters'),
});
await expect(user.put(ENDPOINT, {
username: '🤬',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('usernameIssueInvalidCharacters'),
});
});
});
});
@@ -53,4 +53,15 @@ describe('POST /user/buy-gear/:key', () => {
message: 'You need to purchase a lower level gear before this one.',
});
});
it('returns an error if tries to buy gear from a different class', async () => {
let key = 'armor_rogue_1';
return expect(user.post(`/user/buy-gear/${key}`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: 'You can\'t buy this item.',
});
});
});
@@ -8,7 +8,11 @@ describe('POST /user/allocate', () => {
let user;
beforeEach(async () => {
user = await generateUser();
user = await generateUser({
'stats.lvl': 10,
'flags.classSelected': true,
'preferences.disableClasses': false,
});
});
// More tests in common code unit tests
@@ -31,6 +35,16 @@ describe('POST /user/allocate', () => {
});
});
it('returns an error if the user hasn\'t selected class', async () => {
await user.update({'flags.classSelected': false});
await expect(user.post('/user/allocate'))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('classNotSelected'),
});
});
it('allocates attribute points', async () => {
await user.update({'stats.points': 1});
let res = await user.post('/user/allocate?stat=con');
@@ -13,7 +13,11 @@ describe('POST /user/allocate-bulk', () => {
};
beforeEach(async () => {
user = await generateUser();
user = await generateUser({
'stats.lvl': 10,
'flags.classSelected': true,
'preferences.disableClasses': false,
});
});
// More tests in common code unit tests
@@ -27,6 +31,16 @@ describe('POST /user/allocate-bulk', () => {
});
});
it('returns an error if user has not selected class', async () => {
await user.update({'flags.classSelected': false});
await expect(user.post('/user/allocate-bulk', statsUpdate))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('classNotSelected'),
});
});
it('allocates attribute points', async () => {
await user.update({'stats.points': 3});
@@ -0,0 +1,62 @@
import {
generateUser,
translate as t,
resetHabiticaDB,
} from '../../../helpers/api-integration/v4';
describe('POST /coupons/enter/:code', () => {
let user;
let sudoUser;
before(async () => {
await resetHabiticaDB();
});
beforeEach(async () => {
user = await generateUser();
sudoUser = await generateUser({
'contributor.sudo': true,
});
});
it('returns an error if code is missing', async () => {
await expect(user.post('/coupons/enter')).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: 'Not found.',
});
});
it('returns an error if code is invalid', async () => {
await expect(user.post('/coupons/enter/notValid')).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidCoupon'),
});
});
it('returns an error if coupon has been used', async () => {
let [coupon] = await sudoUser.post('/coupons/generate/wondercon?count=1');
await user.post(`/coupons/enter/${coupon._id}`); // use coupon
await expect(user.post(`/coupons/enter/${coupon._id}`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('couponUsed'),
});
});
it('should apply the coupon to the user', async () => {
let [coupon] = await sudoUser.post('/coupons/generate/wondercon?count=1');
let userRes = await user.post(`/coupons/enter/${coupon._id}`);
expect(userRes._id).to.equal(user._id);
expect(userRes.items.gear.owned.eyewear_special_wondercon_red).to.be.true;
expect(userRes.items.gear.owned.eyewear_special_wondercon_black).to.be.true;
expect(userRes.items.gear.owned.back_special_wondercon_black).to.be.true;
expect(userRes.items.gear.owned.back_special_wondercon_red).to.be.true;
expect(userRes.items.gear.owned.body_special_wondercon_red).to.be.true;
expect(userRes.items.gear.owned.body_special_wondercon_black).to.be.true;
expect(userRes.items.gear.owned.body_special_wondercon_gold).to.be.true;
expect(userRes.extra).to.eql({signupEvent: 'wondercon'});
});
});
@@ -0,0 +1,30 @@
import {
generateUser,
} from '../../../helpers/api-integration/v4';
describe('DELETE /inbox/clear', () => {
it('removes all inbox messages for the user', async () => {
const [user, otherUser] = await Promise.all([generateUser(), generateUser()]);
await otherUser.post('/members/send-private-message', {
toUserId: user.id,
message: 'first',
});
await user.post('/members/send-private-message', {
toUserId: otherUser.id,
message: 'second',
});
await otherUser.post('/members/send-private-message', {
toUserId: user.id,
message: 'third',
});
let messages = await user.get('/inbox/messages');
expect(messages.length).to.equal(3);
await user.del('/inbox/clear/');
messages = await user.get('/inbox/messages');
expect(messages.length).to.equal(0);
});
});
+58
View File
@@ -0,0 +1,58 @@
import {
generateUser,
} from '../../../helpers/api-integration/v4';
import common from '../../../../website/common';
describe('GET /user', () => {
let user;
before(async () => {
user = await generateUser();
});
it('returns the authenticated user with computed stats', async () => {
let returnedUser = await user.get('/user');
expect(returnedUser._id).to.equal(user._id);
expect(returnedUser.stats.maxMP).to.exist;
expect(returnedUser.stats.maxHealth).to.equal(common.maxHealth);
expect(returnedUser.stats.toNextLevel).to.equal(common.tnl(returnedUser.stats.lvl));
});
it('does not return private paths (and apiToken)', async () => {
let returnedUser = await user.get('/user');
expect(returnedUser.auth.local.hashed_password).to.not.exist;
expect(returnedUser.auth.local.passwordHashMethod).to.not.exist;
expect(returnedUser.auth.local.salt).to.not.exist;
expect(returnedUser.apiToken).to.not.exist;
});
it('returns only user properties requested', async () => {
let returnedUser = await user.get('/user?userFields=achievements,items.mounts');
expect(returnedUser._id).to.equal(user._id);
expect(returnedUser.achievements).to.exist;
expect(returnedUser.items.mounts).to.exist;
// Notifications are always returned
expect(returnedUser.notifications).to.exist;
expect(returnedUser.stats).to.not.exist;
});
it('does not return new inbox messages', async () => {
const otherUser = await generateUser();
await otherUser.post('/members/send-private-message', {
toUserId: user.id,
message: 'first',
});
await otherUser.post('/members/send-private-message', {
toUserId: user.id,
message: 'second',
});
let returnedUser = await user.get('/user');
expect(returnedUser._id).to.equal(user._id);
expect(returnedUser.inbox.messages).to.be.undefined;
});
});
@@ -0,0 +1,324 @@
import {
generateUser,
translate as t,
createAndPopulateGroup,
generateGroup,
generateChallenge,
sleep,
} from '../../../helpers/api-integration/v4';
import { v4 as generateUUID } from 'uuid';
import { find } from 'lodash';
import apiError from '../../../../website/server/libs/apiError';
describe('POST /user/class/cast/:spellId', () => {
let user;
beforeEach(async () => {
user = await generateUser();
});
it('returns an error if spell does not exist', async () => {
await user.update({'stats.class': 'rogue'});
let spellId = 'invalidSpell';
await expect(user.post(`/user/class/cast/${spellId}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: apiError('spellNotFound', {spellId}),
});
});
it('returns an error if spell does not exist in user\'s class', async () => {
let spellId = 'pickPocket';
await expect(user.post(`/user/class/cast/${spellId}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: apiError('spellNotFound', {spellId}),
});
});
it('returns an error if spell.mana > user.mana', async () => {
await user.update({'stats.class': 'rogue'});
await expect(user.post('/user/class/cast/backStab'))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('notEnoughMana'),
});
});
it('returns an error if spell.value > user.gold', async () => {
await expect(user.post('/user/class/cast/birthday'))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageNotEnoughGold'),
});
});
it('returns an error if spell.lvl > user.level', async () => {
await user.update({'stats.mp': 200, 'stats.class': 'wizard'});
await expect(user.post('/user/class/cast/earth'))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('spellLevelTooHigh', {level: 13}),
});
});
it('returns an error if user doesn\'t own the spell', async () => {
await expect(user.post('/user/class/cast/snowball'))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('spellNotOwned'),
});
});
it('returns an error if targetId is not an UUID', async () => {
await expect(user.post('/user/class/cast/spellId?targetId=notAnUUID'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('returns an error if targetId is required but missing', async () => {
await user.update({'stats.class': 'rogue', 'stats.lvl': 11});
await expect(user.post('/user/class/cast/pickPocket'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('targetIdUUID'),
});
});
it('returns an error if targeted task doesn\'t exist', async () => {
await user.update({'stats.class': 'rogue', 'stats.lvl': 11});
await expect(user.post(`/user/class/cast/pickPocket?targetId=${generateUUID()}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('taskNotFound'),
});
});
it('returns an error if a challenge task was targeted', async () => {
let {group, groupLeader} = await createAndPopulateGroup();
let challenge = await generateChallenge(groupLeader, group);
await groupLeader.post(`/challenges/${challenge._id}/join`);
await groupLeader.post(`/tasks/challenge/${challenge._id}`, [
{type: 'habit', text: 'task text'},
]);
await groupLeader.update({'stats.class': 'rogue', 'stats.lvl': 11});
await sleep(0.5);
await groupLeader.sync();
await expect(groupLeader.post(`/user/class/cast/pickPocket?targetId=${groupLeader.tasksOrder.habits[0]}`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('challengeTasksNoCast'),
});
});
it('returns an error if a group task was targeted', async () => {
let {group, groupLeader} = await createAndPopulateGroup();
let groupTask = await groupLeader.post(`/tasks/group/${group._id}`, {
text: 'todo group',
type: 'todo',
});
await groupLeader.post(`/tasks/${groupTask._id}/assign/${groupLeader._id}`);
let memberTasks = await groupLeader.get('/tasks/user');
let syncedGroupTask = find(memberTasks, function findAssignedTask (memberTask) {
return memberTask.group.id === group._id;
});
await groupLeader.update({'stats.class': 'rogue', 'stats.lvl': 11});
await sleep(0.5);
await groupLeader.sync();
await expect(groupLeader.post(`/user/class/cast/pickPocket?targetId=${syncedGroupTask._id}`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('groupTasksNoCast'),
});
});
it('returns an error if targeted party member doesn\'t exist', async () => {
let {groupLeader} = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
members: 1,
});
await groupLeader.update({'items.special.snowball': 3});
let target = generateUUID();
await expect(groupLeader.post(`/user/class/cast/snowball?targetId=${target}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('userWithIDNotFound', {userId: target}),
});
});
it('returns an error if party does not exists', async () => {
await user.update({'items.special.snowball': 3});
await expect(user.post(`/user/class/cast/snowball?targetId=${generateUUID()}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('partyNotFound'),
});
});
it('send message in party chat if party && !spell.silent', async () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
members: 1,
});
await groupLeader.update({'stats.mp': 200, 'stats.class': 'wizard', 'stats.lvl': 13});
await groupLeader.post('/user/class/cast/earth');
await sleep(1);
const groupMessages = await groupLeader.get(`/groups/${group._id}/chat`);
expect(groupMessages[0]).to.exist;
expect(groupMessages[0].uuid).to.equal('system');
});
it('Ethereal Surge does not recover mp of other mages', async () => {
let group = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
members: 4,
});
let promises = [];
promises.push(group.groupLeader.update({'stats.mp': 200, 'stats.class': 'wizard', 'stats.lvl': 20}));
promises.push(group.members[0].update({'stats.mp': 0, 'stats.class': 'warrior', 'stats.lvl': 20}));
promises.push(group.members[1].update({'stats.mp': 0, 'stats.class': 'wizard', 'stats.lvl': 20}));
promises.push(group.members[2].update({'stats.mp': 0, 'stats.class': 'rogue', 'stats.lvl': 20}));
promises.push(group.members[3].update({'stats.mp': 0, 'stats.class': 'healer', 'stats.lvl': 20}));
await Promise.all(promises);
await group.groupLeader.post('/user/class/cast/mpheal');
promises = [];
promises.push(group.members[0].sync());
promises.push(group.members[1].sync());
promises.push(group.members[2].sync());
promises.push(group.members[3].sync());
await Promise.all(promises);
expect(group.members[0].stats.mp).to.be.greaterThan(0); // warrior
expect(group.members[1].stats.mp).to.equal(0); // wizard
expect(group.members[2].stats.mp).to.be.greaterThan(0); // rogue
expect(group.members[3].stats.mp).to.be.greaterThan(0); // healer
});
it('cast bulk', async () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
members: 1,
});
await groupLeader.update({'stats.mp': 200, 'stats.class': 'wizard', 'stats.lvl': 13});
await groupLeader.post('/user/class/cast/earth', {quantity: 2});
await sleep(1);
group = await groupLeader.get(`/groups/${group._id}`);
expect(group.chat[0]).to.exist;
expect(group.chat[0].uuid).to.equal('system');
});
it('searing brightness does not affect challenge or group tasks', async () => {
let guild = await generateGroup(user);
let challenge = await generateChallenge(user, guild);
await user.post(`/challenges/${challenge._id}/join`);
await user.post(`/tasks/challenge/${challenge._id}`, {
text: 'test challenge habit',
type: 'habit',
});
let groupTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'todo group',
type: 'todo',
});
await user.update({'stats.class': 'healer', 'stats.mp': 200, 'stats.lvl': 15});
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
await user.post('/user/class/cast/brightness');
await user.sync();
let memberTasks = await user.get('/tasks/user');
let syncedGroupTask = find(memberTasks, function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
});
let userChallengeTask = find(memberTasks, function findAssignedTask (memberTask) {
return memberTask.challenge.id === challenge._id;
});
expect(userChallengeTask).to.exist;
expect(syncedGroupTask).to.exist;
expect(userChallengeTask.value).to.equal(0);
expect(syncedGroupTask.value).to.equal(0);
});
it('increases both user\'s achievement values', async () => {
let party = await createAndPopulateGroup({
members: 1,
});
let leader = party.groupLeader;
let recipient = party.members[0];
await leader.update({'stats.gp': 10});
await leader.post(`/user/class/cast/birthday?targetId=${recipient._id}`);
await leader.sync();
await recipient.sync();
expect(leader.achievements.birthday).to.equal(1);
expect(recipient.achievements.birthday).to.equal(1);
});
it('only increases user\'s achievement one if target == caster', async () => {
await user.update({'stats.gp': 10});
await user.post(`/user/class/cast/birthday?targetId=${user._id}`);
await user.sync();
expect(user.achievements.birthday).to.equal(1);
});
it('passes correct target to spell when targetType === \'task\'', async () => {
await user.update({'stats.class': 'wizard', 'stats.lvl': 11});
let task = await user.post('/tasks/user', {
text: 'test habit',
type: 'habit',
});
let result = await user.post(`/user/class/cast/fireball?targetId=${task._id}`);
expect(result.task._id).to.equal(task._id);
});
it('passes correct target to spell when targetType === \'self\'', async () => {
await user.update({'stats.class': 'wizard', 'stats.lvl': 14, 'stats.mp': 50});
let result = await user.post('/user/class/cast/frost');
expect(result.user.stats.mp).to.equal(10);
});
// TODO find a way to have sinon working in integration tests
// it doesn't work when tests are running separately from server
it('passes correct target to spell when targetType === \'tasks\'');
it('passes correct target to spell when targetType === \'party\'');
it('passes correct target to spell when targetType === \'user\'');
it('passes correct target to spell when targetType === \'party\' and user is not in a party');
it('passes correct target to spell when targetType === \'user\' and user is not in a party');
});
@@ -0,0 +1,60 @@
import {
generateUser,
generateDaily,
generateReward,
translate as t,
} from '../../../helpers/api-integration/v4';
describe('POST /user/rebirth', () => {
let user;
beforeEach(async () => {
user = await generateUser();
});
it('returns an error when user balance is too low', async () => {
await expect(user.post('/user/rebirth'))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('notEnoughGems'),
});
});
// More tests in common code unit tests
it('resets user\'s tasks', async () => {
await user.update({
balance: 1.5,
});
let daily = await generateDaily({
text: 'test habit',
type: 'daily',
value: 1,
streak: 1,
userId: user._id,
});
let reward = await generateReward({
text: 'test reward',
type: 'reward',
value: 1,
userId: user._id,
});
let response = await user.post('/user/rebirth');
await user.sync();
expect(user.notifications.length).to.equal(1);
expect(user.notifications[0].type).to.equal('REBIRTH_ACHIEVEMENT');
let updatedDaily = await user.get(`/tasks/${daily._id}`);
let updatedReward = await user.get(`/tasks/${reward._id}`);
expect(response.message).to.equal(t('rebirthComplete'));
expect(updatedDaily.streak).to.equal(0);
expect(updatedDaily.value).to.equal(0);
expect(updatedReward.value).to.equal(1);
});
});
+54
View File
@@ -0,0 +1,54 @@
import {
generateUser,
generateDaily,
generateReward,
translate as t,
} from '../../../helpers/api-integration/v4';
describe('POST /user/reroll', () => {
let user;
beforeEach(async () => {
user = await generateUser();
});
it('returns an error when user balance is too low', async () => {
await expect(user.post('/user/reroll'))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('notEnoughGems'),
});
});
// More tests in common code unit tests
it('resets user\'s tasks', async () => {
await user.update({
balance: 2,
});
let daily = await generateDaily({
text: 'test habit',
type: 'daily',
userId: user._id,
});
let reward = await generateReward({
text: 'test reward',
type: 'reward',
value: 1,
userId: user._id,
});
let response = await user.post('/user/reroll');
await user.sync();
let updatedDaily = await user.get(`/tasks/${daily._id}`);
let updatedReward = await user.get(`/tasks/${reward._id}`);
expect(response.message).to.equal(t('fortifyComplete'));
expect(updatedDaily.value).to.equal(0);
expect(updatedReward.value).to.equal(1);
});
});
+121
View File
@@ -0,0 +1,121 @@
import {
generateUser,
generateGroup,
generateChallenge,
translate as t,
} from '../../../helpers/api-integration/v4';
import { find } from 'lodash';
describe('POST /user/reset', () => {
let user;
beforeEach(async () => {
user = await generateUser();
});
// More tests in common code unit tests
it('resets user\'s habits', async () => {
let task = await user.post('/tasks/user', {
text: 'test habit',
type: 'habit',
});
await user.post('/user/reset');
await user.sync();
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('taskNotFound'),
});
expect(user.tasksOrder.habits).to.be.empty;
});
it('resets user\'s dailys', async () => {
let task = await user.post('/tasks/user', {
text: 'test daily',
type: 'daily',
});
await user.post('/user/reset');
await user.sync();
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('taskNotFound'),
});
expect(user.tasksOrder.dailys).to.be.empty;
});
it('resets user\'s todos', async () => {
let task = await user.post('/tasks/user', {
text: 'test todo',
type: 'todo',
});
await user.post('/user/reset');
await user.sync();
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('taskNotFound'),
});
expect(user.tasksOrder.todos).to.be.empty;
});
it('resets user\'s rewards', async () => {
let task = await user.post('/tasks/user', {
text: 'test reward',
type: 'reward',
});
await user.post('/user/reset');
await user.sync();
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('taskNotFound'),
});
expect(user.tasksOrder.rewards).to.be.empty;
});
it('does not delete challenge or group tasks', async () => {
let guild = await generateGroup(user);
let challenge = await generateChallenge(user, guild);
await user.post(`/challenges/${challenge._id}/join`);
await user.post(`/tasks/challenge/${challenge._id}`, {
text: 'test challenge habit',
type: 'habit',
});
let groupTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'todo group',
type: 'todo',
});
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
await user.post('/user/reset');
await user.sync();
let memberTasks = await user.get('/tasks/user');
let syncedGroupTask = find(memberTasks, function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
});
let userChallengeTask = find(memberTasks, function findAssignedTask (memberTask) {
return memberTask.challenge.id === challenge._id;
});
expect(userChallengeTask).to.exist;
expect(syncedGroupTask).to.exist;
});
});
+256
View File
@@ -0,0 +1,256 @@
import {
generateUser,
translate as t,
} from '../../../helpers/api-integration/v4';
import { each, get } from 'lodash';
describe('PUT /user', () => {
let user;
beforeEach(async () => {
user = await generateUser();
});
context('Allowed Operations', () => {
it('updates the user', async () => {
await user.put('/user', {
'profile.name': 'Frodo',
'preferences.costume': true,
'stats.hp': 14,
});
await user.sync();
expect(user.profile.name).to.eql('Frodo');
expect(user.preferences.costume).to.eql(true);
expect(user.stats.hp).to.eql(14);
});
it('tags must be an array', async () => {
await expect(user.put('/user', {
tags: {
tag: true,
},
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'mustBeArray',
});
});
it('update tags', async () => {
let userTags = user.tags;
await user.put('/user', {
tags: [...user.tags, {
name: 'new tag',
}],
});
await user.sync();
expect(user.tags.length).to.be.eql(userTags.length + 1);
});
it('profile.name cannot be an empty string or null', async () => {
await expect(user.put('/user', {
'profile.name': ' ', // string should be trimmed
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'User validation failed',
});
await expect(user.put('/user', {
'profile.name': '',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'User validation failed',
});
await expect(user.put('/user', {
'profile.name': null,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
});
context('Top Level Protected Operations', () => {
let protectedOperations = {
'gem balance': {balance: 100},
auth: {'auth.blocked': true, 'auth.timestamps.created': new Date()},
contributor: {'contributor.level': 9, 'contributor.admin': true, 'contributor.text': 'some text'},
backer: {'backer.tier': 10, 'backer.npc': 'Bilbo'},
subscriptions: {'purchased.plan.extraMonths': 500, 'purchased.plan.consecutive.trinkets': 1000},
'customization gem purchases': {'purchased.background.tavern': true, 'purchased.skin.bear': true},
notifications: [{type: 123}],
webhooks: {webhooks: [{url: 'https://foobar.com'}]},
};
each(protectedOperations, (data, testName) => {
it(`does not allow updating ${testName}`, async () => {
let errorText = t('messageUserOperationProtected', { operation: Object.keys(data)[0] });
await expect(user.put('/user', data)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: errorText,
});
});
});
});
context('Sub-Level Protected Operations', () => {
let protectedOperations = {
'class stat': {'stats.class': 'wizard'},
'flags unless whitelisted': {'flags.dropsEnabled': true},
webhooks: {'preferences.webhooks': [1, 2, 3]},
sleep: {'preferences.sleep': true},
'disable classes': {'preferences.disableClasses': true},
};
each(protectedOperations, (data, testName) => {
it(`does not allow updating ${testName}`, async () => {
let errorText = t('messageUserOperationProtected', { operation: Object.keys(data)[0] });
await expect(user.put('/user', data)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: errorText,
});
});
});
});
context('Default Appearance Preferences', () => {
let testCases = {
shirt: 'yellow',
skin: 'ddc994',
'hair.color': 'blond',
'hair.bangs': 2,
'hair.base': 1,
'hair.flower': 4,
size: 'broad',
};
each(testCases, (item, type) => {
const update = {};
update[`preferences.${type}`] = item;
it(`updates user with ${type} that is a default`, async () => {
let dbUpdate = {};
dbUpdate[`purchased.${type}.${item}`] = true;
await user.update(dbUpdate);
// Sanity checks to make sure user is not already equipped with item
expect(get(user.preferences, type)).to.not.eql(item);
let updatedUser = await user.put('/user', update);
expect(get(updatedUser.preferences, type)).to.eql(item);
});
});
it('returns an error if user tries to update body size with invalid type', async () => {
await expect(user.put('/user', {
'preferences.size': 'round',
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('mustPurchaseToSet', { val: 'round', key: 'preferences.size' }),
});
});
it('can set beard to default', async () => {
await user.update({
'purchased.hair.beard': 3,
'preferences.hair.beard': 3,
});
let updatedUser = await user.put('/user', {
'preferences.hair.beard': 0,
});
expect(updatedUser.preferences.hair.beard).to.eql(0);
});
it('can set mustache to default', async () => {
await user.update({
'purchased.hair.mustache': 2,
'preferences.hair.mustache': 2,
});
let updatedUser = await user.put('/user', {
'preferences.hair.mustache': 0,
});
expect(updatedUser.preferences.hair.mustache).to.eql(0);
});
});
context('Purchasable Appearance Preferences', () => {
let testCases = {
background: 'volcano',
shirt: 'convict',
skin: 'cactus',
'hair.base': 7,
'hair.beard': 2,
'hair.color': 'rainbow',
'hair.mustache': 2,
};
each(testCases, (item, type) => {
const update = {};
update[`preferences.${type}`] = item;
it(`returns an error if user tries to update ${type} with ${type} the user does not own`, async () => {
await expect(user.put('/user', update)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('mustPurchaseToSet', {val: item, key: `preferences.${type}`}),
});
});
it(`updates user with ${type} user does own`, async () => {
let dbUpdate = {};
dbUpdate[`purchased.${type}.${item}`] = true;
await user.update(dbUpdate);
// Sanity check to make sure user is not already equipped with item
expect(get(user.preferences, type)).to.not.eql(item);
let updatedUser = await user.put('/user', update);
expect(get(updatedUser.preferences, type)).to.eql(item);
});
});
});
context('Improvement Categories', () => {
it('sets valid categories', async () => {
await user.put('/user', {
'preferences.improvementCategories': ['work', 'school'],
});
await user.sync();
expect(user.preferences.improvementCategories).to.eql(['work', 'school']);
});
it('discards invalid categories', async () => {
await expect(user.put('/user', {
'preferences.improvementCategories': ['work', 'procrastination', 'school'],
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'User validation failed',
});
});
});
});
@@ -0,0 +1,739 @@
import {
generateUser,
requester,
translate as t,
createAndPopulateGroup,
getProperty,
} from '../../../../helpers/api-integration/v4';
import { ApiUser } from '../../../../helpers/api-integration/api-classes';
import { v4 as uuid } from 'uuid';
import { each } from 'lodash';
import { encrypt } from '../../../../../website/server/libs/encryption';
function generateRandomUserName () {
return (Date.now() + uuid()).substring(0, 20);
}
describe('POST /user/auth/local/register', () => {
context('username and email are free', () => {
let api;
beforeEach(async () => {
api = requester();
});
it('registers a new user', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user._id).to.exist;
expect(user.apiToken).to.exist;
expect(user.auth.local.username).to.eql(username);
expect(user.profile.name).to.eql(username);
expect(user.newUser).to.eql(true);
});
xit('remove spaces from username', async () => {
// TODO can probably delete this test now
let username = ' usernamewithspaces ';
let email = 'test@example.com';
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.auth.local.username).to.eql(username.trim());
expect(user.profile.name).to.eql(username.trim());
});
context('validates username', () => {
const email = 'test@example.com';
const password = 'password';
it('requires to username to be less than 20', async () => {
const username = (Date.now() + uuid()).substring(0, 21);
await expect(api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Invalid request parameters.',
});
});
it('rejects chracters not in [-_a-zA-Z0-9]', async () => {
const username = 'a-zA_Z09*';
await expect(api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Invalid request parameters.',
});
});
it('allows only [-_a-zA-Z0-9] characters', async () => {
const username = 'a-zA_Z09';
const user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.auth.local.username).to.eql(username);
});
});
context('provides default tags and tasks', async () => {
it('for a generic API consumer', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
let requests = new ApiUser(user);
let habits = await requests.get('/tasks/user?type=habits');
let dailys = await requests.get('/tasks/user?type=dailys');
let todos = await requests.get('/tasks/user?type=todos');
let rewards = await requests.get('/tasks/user?type=rewards');
let tags = await requests.get('/tags');
expect(habits).to.have.a.lengthOf(0);
expect(dailys).to.have.a.lengthOf(0);
expect(todos).to.have.a.lengthOf(1);
expect(rewards).to.have.a.lengthOf(0);
expect(tags).to.have.a.lengthOf(7);
expect(tags[0].name).to.eql(t('defaultTag1'));
expect(tags[1].name).to.eql(t('defaultTag2'));
expect(tags[2].name).to.eql(t('defaultTag3'));
expect(tags[3].name).to.eql(t('defaultTag4'));
expect(tags[4].name).to.eql(t('defaultTag5'));
expect(tags[5].name).to.eql(t('defaultTag6'));
expect(tags[6].name).to.eql(t('defaultTag7'));
});
xit('for Web', async () => {
api = requester(
null,
{'x-client': 'habitica-web'},
);
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
let requests = new ApiUser(user);
let habits = await requests.get('/tasks/user?type=habits');
let dailys = await requests.get('/tasks/user?type=dailys');
let todos = await requests.get('/tasks/user?type=todos');
let rewards = await requests.get('/tasks/user?type=rewards');
let tags = await requests.get('/tags');
expect(habits).to.have.a.lengthOf(3);
expect(habits[0].text).to.eql(t('defaultHabit1Text'));
expect(habits[0].notes).to.eql('');
expect(habits[1].text).to.eql(t('defaultHabit2Text'));
expect(habits[1].notes).to.eql('');
expect(habits[2].text).to.eql(t('defaultHabit3Text'));
expect(habits[2].notes).to.eql('');
expect(dailys).to.have.a.lengthOf(0);
expect(todos).to.have.a.lengthOf(1);
expect(todos[0].text).to.eql(t('defaultTodo1Text'));
expect(todos[0].notes).to.eql(t('defaultTodoNotes'));
expect(rewards).to.have.a.lengthOf(1);
expect(rewards[0].text).to.eql(t('defaultReward1Text'));
expect(rewards[0].notes).to.eql('');
expect(tags).to.have.a.lengthOf(7);
expect(tags[0].name).to.eql(t('defaultTag1'));
expect(tags[1].name).to.eql(t('defaultTag2'));
expect(tags[2].name).to.eql(t('defaultTag3'));
expect(tags[3].name).to.eql(t('defaultTag4'));
expect(tags[4].name).to.eql(t('defaultTag5'));
expect(tags[5].name).to.eql(t('defaultTag6'));
expect(tags[6].name).to.eql(t('defaultTag7'));
});
});
context('does not provide default tags and tasks', async () => {
it('for Android', async () => {
api = requester(
null,
{'x-client': 'habitica-android'},
);
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
let requests = new ApiUser(user);
let habits = await requests.get('/tasks/user?type=habits');
let dailys = await requests.get('/tasks/user?type=dailys');
let todos = await requests.get('/tasks/user?type=todos');
let rewards = await requests.get('/tasks/user?type=rewards');
let tags = await requests.get('/tags');
expect(habits).to.have.a.lengthOf(0);
expect(dailys).to.have.a.lengthOf(0);
expect(todos).to.have.a.lengthOf(0);
expect(rewards).to.have.a.lengthOf(0);
expect(tags).to.have.a.lengthOf(0);
});
it('for iOS', async () => {
api = requester(
null,
{'x-client': 'habitica-ios'},
);
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
let requests = new ApiUser(user);
let habits = await requests.get('/tasks/user?type=habits');
let dailys = await requests.get('/tasks/user?type=dailys');
let todos = await requests.get('/tasks/user?type=todos');
let rewards = await requests.get('/tasks/user?type=rewards');
let tags = await requests.get('/tags');
expect(habits).to.have.a.lengthOf(0);
expect(dailys).to.have.a.lengthOf(0);
expect(todos).to.have.a.lengthOf(0);
expect(rewards).to.have.a.lengthOf(0);
expect(tags).to.have.a.lengthOf(0);
});
});
it('enrolls new users in an A/B test', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
});
it('includes items awarded by default when creating a new user', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.items.quests.dustbunnies).to.equal(1);
expect(user.purchased.background.violet).to.be.ok;
expect(user.preferences.background).to.equal('violet');
});
it('requires password and confirmPassword to match', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let confirmPassword = 'not password';
await expect(api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('requires a username', async () => {
let email = `${generateRandomUserName()}@example.com`;
let password = 'password';
let confirmPassword = 'password';
await expect(api.post('/user/auth/local/register', {
email,
password,
confirmPassword,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('requires an email', async () => {
let username = generateRandomUserName();
let password = 'password';
await expect(api.post('/user/auth/local/register', {
username,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('requires a valid email', async () => {
let username = generateRandomUserName();
let email = 'notanemail@sdf';
let password = 'password';
await expect(api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('sanitizes email params to a lowercase string before creating the user', async () => {
let username = generateRandomUserName();
let email = 'ISANEmAiL@ExAmPle.coM';
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.auth.local.email).to.equal(email.toLowerCase());
});
it('fails on a habitica.com email', async () => {
let username = generateRandomUserName();
let email = `${username}@habitica.com`;
let password = 'password';
await expect(api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'User validation failed',
});
});
it('fails on a habitrpg.com email', async () => {
let username = generateRandomUserName();
let email = `${username}@habitrpg.com`;
let password = 'password';
await expect(api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'User validation failed',
});
});
it('requires a password', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
let confirmPassword = 'password';
await expect(api.post('/user/auth/local/register', {
username,
email,
confirmPassword,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
});
context('attach to facebook user', () => {
let user;
let email = 'some@email.net';
let username = 'some-username';
let password = 'some-password';
beforeEach(async () => {
user = await generateUser();
});
it('checks onlySocialAttachLocal', async () => {
await expect(user.post('/user/auth/local/register', {
email,
username,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlySocialAttachLocal'),
});
});
it('succeeds', async () => {
await user.update({ 'auth.facebook.id': 'some-fb-id', 'auth.local': { ok: true } });
await user.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
await user.sync();
expect(user.auth.local.username).to.eql(username);
expect(user.auth.local.email).to.eql(email);
});
});
context('login is already taken', () => {
let username, email, api;
beforeEach(async () => {
api = requester();
username = generateRandomUserName();
email = `${username}@example.com`;
return generateUser({
'auth.local.username': username,
'auth.local.lowerCaseUsername': username,
'auth.local.email': email,
});
});
it('rejects if username is already taken', async () => {
let uniqueEmail = `${generateRandomUserName()}@example.com`;
let password = 'password';
await expect(api.post('/user/auth/local/register', {
username,
email: uniqueEmail,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('usernameTaken'),
});
});
it('rejects if email is already taken', async () => {
let uniqueUsername = generateRandomUserName();
let password = 'password';
await expect(api.post('/user/auth/local/register', {
username: uniqueUsername,
email,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('emailTaken'),
});
});
});
context('req.query.groupInvite', () => {
let api, username, email, password;
beforeEach(() => {
api = requester();
username = generateRandomUserName();
email = `${username}@example.com`;
password = 'password';
});
it('does not crash the signup process when it\'s invalid', async () => {
let user = await api.post('/user/auth/local/register?groupInvite=aaaaInvalid', {
username,
email,
password,
confirmPassword: password,
});
expect(user._id).to.be.a('string');
});
it('supports invite using req.query.groupInvite', async () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
});
let invite = encrypt(JSON.stringify({
id: group._id,
inviter: groupLeader._id,
sentAt: Date.now(), // so we can let it expire
}));
let user = await api.post(`/user/auth/local/register?groupInvite=${invite}`, {
username,
email,
password,
confirmPassword: password,
});
expect(user.invitations.parties[0].id).to.eql(group._id);
expect(user.invitations.parties[0].name).to.eql(group.name);
expect(user.invitations.parties[0].inviter).to.eql(groupLeader._id);
});
it('awards achievement to inviter', async () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
});
let invite = encrypt(JSON.stringify({
id: group._id,
inviter: groupLeader._id,
sentAt: Date.now(),
}));
await api.post(`/user/auth/local/register?groupInvite=${invite}`, {
username,
email,
password,
confirmPassword: password,
});
await groupLeader.sync();
expect(groupLeader.achievements.invitedFriend).to.be.true;
});
it('user not added to a party on expired invite', async () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
});
let invite = encrypt(JSON.stringify({
id: group._id,
inviter: groupLeader._id,
sentAt: Date.now() - 6.912e8, // 8 days old
}));
let user = await api.post(`/user/auth/local/register?groupInvite=${invite}`, {
username,
email,
password,
confirmPassword: password,
});
expect(user.invitations.party).to.eql({});
});
it('adds a user to a guild on an invite of type other than party', async () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
});
let invite = encrypt(JSON.stringify({
id: group._id,
inviter: groupLeader._id,
sentAt: Date.now(),
}));
let user = await api.post(`/user/auth/local/register?groupInvite=${invite}`, {
username,
email,
password,
confirmPassword: password,
});
expect(user.invitations.guilds[0]).to.eql({
id: group._id,
name: group.name,
inviter: groupLeader._id,
});
});
});
context('successful login via api', () => {
let api, username, email, password;
beforeEach(() => {
api = requester();
username = generateRandomUserName();
email = `${username}@example.com`;
password = 'password';
});
it('sets all site tour values to -2 (already seen)', async () => {
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.flags.tour).to.not.be.empty;
each(user.flags.tour, (value) => {
expect(value).to.eql(-2);
});
});
it('populates user with default todos, not no other task types', async () => {
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.tasksOrder.todos).to.not.be.empty;
expect(user.tasksOrder.dailys).to.be.empty;
expect(user.tasksOrder.habits).to.be.empty;
expect(user.tasksOrder.rewards).to.be.empty;
});
it('populates user with default tags', async () => {
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.tags).to.not.be.empty;
});
});
context('successful login with habitica-web header', () => {
let api, username, email, password;
beforeEach(() => {
api = requester({}, {'x-client': 'habitica-web'});
username = generateRandomUserName();
email = `${username}@example.com`;
password = 'password';
});
it('sets all common tutorial flags to true', async () => {
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.flags.tour).to.not.be.empty;
each(user.flags.tutorial.common, (value) => {
expect(value).to.eql(true);
});
});
it('populates user with default todos, habits, and rewards', async () => {
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.tasksOrder.todos).to.be.empty;
expect(user.tasksOrder.dailys).to.be.empty;
expect(user.tasksOrder.habits).to.be.empty;
expect(user.tasksOrder.rewards).to.be.empty;
});
it('populates user with default tags', async () => {
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.tags).to.not.be.empty;
});
it('adds the correct tags to the correct tasks', async () => {
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
let requests = new ApiUser(user);
let habits = await requests.get('/tasks/user?type=habits');
let todos = await requests.get('/tasks/user?type=todos');
expect(habits).to.have.a.lengthOf(0);
expect(todos).to.have.a.lengthOf(0);
});
});
});
@@ -0,0 +1,57 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v4';
const ENDPOINT = '/user/auth/verify-display-name';
describe('POST /user/auth/verify-display-name', async () => {
let user;
beforeEach(async () => {
user = await generateUser();
});
it('successfully verifies display name including funky characters', async () => {
let newDisplayName = 'Sabé 🤬';
let response = await user.post(ENDPOINT, {
displayName: newDisplayName,
});
expect(response).to.eql({ isUsable: true });
});
context('errors', async () => {
it('errors if display name is not provided', async () => {
await expect(user.post(ENDPOINT, {
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('errors if display name is a slur', async () => {
await expect(user.post(ENDPOINT, {
displayName: 'TESTPLACEHOLDERSLURWORDHERE',
})).to.eventually.eql({ isUsable: false, issues: [t('displaynameIssueSlur')] });
});
it('errors if display name contains a slur', async () => {
await expect(user.post(ENDPOINT, {
displayName: 'TESTPLACEHOLDERSLURWORDHERE_otherword',
})).to.eventually.eql({ isUsable: false, issues: [t('displaynameIssueLength'), t('displaynameIssueSlur')] });
await expect(user.post(ENDPOINT, {
displayName: 'something_TESTPLACEHOLDERSLURWORDHERE',
})).to.eventually.eql({ isUsable: false, issues: [t('displaynameIssueLength'), t('displaynameIssueSlur')] });
await expect(user.post(ENDPOINT, {
displayName: 'somethingTESTPLACEHOLDERSLURWORDHEREotherword',
})).to.eventually.eql({ isUsable: false, issues: [t('displaynameIssueLength'), t('displaynameIssueSlur')] });
});
it('errors if display name has incorrect length', async () => {
await expect(user.post(ENDPOINT, {
displayName: 'this is a very long display name over 30 characters',
})).to.eventually.eql({ isUsable: false, issues: [t('displaynameIssueLength')] });
});
});
});
@@ -0,0 +1,89 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v4';
const ENDPOINT = '/user/auth/verify-username';
describe('POST /user/auth/verify-username', async () => {
let user;
beforeEach(async () => {
user = await generateUser();
});
it('successfully verifies username', async () => {
let newUsername = 'new-username';
let response = await user.post(ENDPOINT, {
username: newUsername,
});
expect(response).to.eql({ isUsable: true });
});
it('successfully verifies username with allowed characters', async () => {
let newUsername = 'new-username_123';
let response = await user.post(ENDPOINT, {
username: newUsername,
});
expect(response).to.eql({ isUsable: true });
});
context('errors', async () => {
it('errors if username is not provided', async () => {
await expect(user.post(ENDPOINT, {
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('errors if username is a slur', async () => {
await expect(user.post(ENDPOINT, {
username: 'TESTPLACEHOLDERSLURWORDHERE',
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueLength'), t('usernameIssueSlur')] });
});
it('errors if username contains a slur', async () => {
await expect(user.post(ENDPOINT, {
username: 'TESTPLACEHOLDERSLURWORDHERE_otherword',
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueLength'), t('usernameIssueSlur')] });
await expect(user.post(ENDPOINT, {
username: 'something_TESTPLACEHOLDERSLURWORDHERE',
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueLength'), t('usernameIssueSlur')] });
await expect(user.post(ENDPOINT, {
username: 'somethingTESTPLACEHOLDERSLURWORDHEREotherword',
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueLength'), t('usernameIssueSlur')] });
});
it('errors if username is not allowed', async () => {
await expect(user.post(ENDPOINT, {
username: 'support',
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueForbidden')] });
});
it('errors if username is not allowed regardless of casing', async () => {
await expect(user.post(ENDPOINT, {
username: 'SUppORT',
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueForbidden')] });
});
it('errors if username has incorrect length', async () => {
await expect(user.post(ENDPOINT, {
username: 'thisisaverylongusernameover20characters',
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueLength')] });
});
it('errors if username contains invalid characters', async () => {
await expect(user.post(ENDPOINT, {
username: 'Eichhörnchen',
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueInvalidCharacters')] });
await expect(user.post(ENDPOINT, {
username: 'test.name',
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueInvalidCharacters')] });
await expect(user.post(ENDPOINT, {
username: '🤬',
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueInvalidCharacters')] });
});
});
});
+1 -1
View File
@@ -4,7 +4,7 @@ require('babel-polyfill');
// Automatically setup SinonJS' sandbox for each test
beforeEach(() => {
global.sandbox = sinon.sandbox.create();
global.sandbox = sinon.createSandbox();
});
afterEach(() => {
@@ -2,12 +2,14 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import NotificationsComponent from 'client/components/notifications.vue';
import Store from 'client/libs/store';
import { hasClass } from 'client/store/getters/members';
import { toNextLevel } from 'common/script/statHelpers';
const localVue = createLocalVue();
localVue.use(Store);
describe('Notifications', () => {
let store;
let wrapper;
beforeEach(() => {
store = new Store({
@@ -29,16 +31,22 @@ describe('Notifications', () => {
actions: {
'user:fetch': () => {},
'tasks:fetchUserTasks': () => {},
'snackbars:add': () => {},
},
getters: {
'members:hasClass': hasClass,
},
});
wrapper = shallowMount(NotificationsComponent, {
store,
localVue,
mocks: {
$t: (string) => string,
},
});
});
it('set user has class computed prop', () => {
const wrapper = shallowMount(NotificationsComponent, { store, localVue });
expect(wrapper.vm.userHasClass).to.be.false;
store.state.user.data.stats.lvl = 10;
@@ -47,4 +55,130 @@ describe('Notifications', () => {
expect(wrapper.vm.userHasClass).to.be.true;
});
describe('user exp notifcation', () => {
it('notifies when user gets more exp', () => {
const expSpy = sinon.spy(wrapper.vm, 'exp');
const userLevel = 10;
store.state.user.data.stats.lvl = userLevel;
const userExpBefore = 10;
const userExpAfter = 12;
wrapper.vm.displayUserExpAndLvlNotifications(userExpAfter, userExpBefore, userLevel, userLevel);
expect(expSpy).to.be.calledWith(userExpAfter - userExpBefore);
expSpy.restore();
});
it('when user levels with exact xp', () => {
const expSpy = sinon.spy(wrapper.vm, 'exp');
const userLevelBefore = 9;
const userLevelAfter = 10;
store.state.user.data.stats.lvl = userLevelAfter;
const expEarned = 5;
const userExpBefore = toNextLevel(userLevelBefore) - expEarned;
const userExpAfter = 0;
wrapper.vm.displayUserExpAndLvlNotifications(userExpAfter, userExpBefore, userLevelAfter, userLevelBefore);
expect(expSpy).to.be.calledWith(expEarned);
expSpy.restore();
});
it('when user levels with exact more exp than needed', () => {
const expSpy = sinon.spy(wrapper.vm, 'exp');
const userLevelBefore = 9;
const userLevelAfter = 10;
store.state.user.data.stats.lvl = userLevelAfter;
const expEarned = 10;
const expNeeded = 5;
const userExpBefore = toNextLevel(userLevelBefore) - expNeeded;
const userExpAfter = 5;
wrapper.vm.displayUserExpAndLvlNotifications(userExpAfter, userExpBefore, userLevelAfter, userLevelBefore);
expect(expSpy).to.be.calledWith(expEarned);
expSpy.restore();
});
it('when user has more exp than needed then levels', () => {
const expSpy = sinon.spy(wrapper.vm, 'exp');
const userLevelBefore = 9;
const userLevelAfter = 10;
store.state.user.data.stats.lvl = userLevelAfter;
const expEarned = 10;
const expNeeded = -5;
const userExpBefore = toNextLevel(userLevelBefore) - expNeeded;
const userExpAfter = 15;
wrapper.vm.displayUserExpAndLvlNotifications(userExpAfter, userExpBefore, userLevelAfter, userLevelBefore);
expect(expSpy).to.be.calledWith(expEarned);
expSpy.restore();
});
it('when user multilevels', () => {
const expSpy = sinon.spy(wrapper.vm, 'exp');
const userLevelBefore = 8;
const userLevelAfter = 10;
store.state.user.data.stats.lvl = userLevelAfter;
const expEarned = 10 + toNextLevel(userLevelBefore + 1);
const expNeeded = 5;
const userExpBefore = toNextLevel(userLevelBefore) - expNeeded;
const userExpAfter = 5;
wrapper.vm.displayUserExpAndLvlNotifications(userExpAfter, userExpBefore, userLevelAfter, userLevelBefore);
expect(expSpy).to.be.calledWith(expEarned);
expSpy.restore();
});
it('when user looses xp', () => {
const expSpy = sinon.spy(wrapper.vm, 'exp');
const userLevel = 10;
store.state.user.data.stats.lvl = userLevel;
const userExpBefore = 10;
const userExpAfter = 5;
wrapper.vm.displayUserExpAndLvlNotifications(userExpAfter, userExpBefore, userLevel, userLevel);
expect(expSpy).to.be.calledWith(userExpAfter - userExpBefore);
expSpy.restore();
});
it('when user looses xp under 0', () => {
const expSpy = sinon.spy(wrapper.vm, 'exp');
const userLevel = 10;
store.state.user.data.stats.lvl = userLevel;
const userExpBefore = 5;
const userExpAfter = -3;
wrapper.vm.displayUserExpAndLvlNotifications(userExpAfter, userExpBefore, userLevel, userLevel);
expect(expSpy).to.be.calledWith(userExpAfter - userExpBefore);
expSpy.restore();
});
it('when user dies', () => {
const expSpy = sinon.spy(wrapper.vm, 'exp');
const userLevelBefore = 10;
const userLevelAfter = 9;
store.state.user.data.stats.lvl = userLevelAfter;
const expEarned = -20;
const userExpBefore = 20;
const userExpAfter = 0;
wrapper.vm.displayUserExpAndLvlNotifications(userExpAfter, userExpBefore, userLevelAfter, userLevelBefore);
expect(expSpy).to.be.calledWith(expEarned);
expSpy.restore();
});
});
});
@@ -0,0 +1,19 @@
import generateStore from 'client/store';
describe('canDelete getter', () => {
it('cannot delete active challenge task', () => {
const store = generateStore();
const task = {userId: 1, challenge: {id: 2}};
expect(store.getters['tasks:canDelete'](task)).to.equal(false);
});
it('can delete broken challenge task', () => {
const store = generateStore();
const task = {userId: 1, challenge: {id: 2, broken: true}};
expect(store.getters['tasks:canDelete'](task)).to.equal(true);
});
});
@@ -0,0 +1,141 @@
import generateStore from 'client/store';
describe('getTaskClasses getter', () => {
let store, getTaskClasses;
beforeEach(() => {
store = generateStore();
store.state.user.data = {
preferences: {
},
};
getTaskClasses = store.getters['tasks:getTaskClasses'];
});
it('returns reward edit-modal-bg class', () => {
const task = {type: 'reward'};
expect(getTaskClasses(task, 'edit-modal-bg')).to.equal('task-purple-modal-bg');
});
it('returns worst task edit-modal-bg class', () => {
const task = {type: 'todo', value: -21};
expect(getTaskClasses(task, 'edit-modal-bg')).to.equal('task-worst-modal-bg');
});
it('returns worse task edit-modal-bg class', () => {
const task = {type: 'todo', value: -11};
expect(getTaskClasses(task, 'edit-modal-bg')).to.equal('task-worse-modal-bg');
});
it('returns bad task edit-modal-bg class', () => {
const task = {type: 'todo', value: -6};
expect(getTaskClasses(task, 'edit-modal-bg')).to.equal('task-bad-modal-bg');
});
it('returns neutral task edit-modal-bg class', () => {
const task = {type: 'todo', value: 0};
expect(getTaskClasses(task, 'edit-modal-bg')).to.equal('task-neutral-modal-bg');
});
it('returns good task edit-modal-bg class', () => {
const task = {type: 'todo', value: 2};
expect(getTaskClasses(task, 'edit-modal-bg')).to.equal('task-good-modal-bg');
});
it('returns better task edit-modal-bg class', () => {
const task = {type: 'todo', value: 6};
expect(getTaskClasses(task, 'edit-modal-bg')).to.equal('task-better-modal-bg');
});
it('returns best task edit-modal-bg class', () => {
const task = {type: 'todo', value: 12};
expect(getTaskClasses(task, 'edit-modal-bg')).to.equal('task-best-modal-bg');
});
it('returns best task edit-modal-text class', () => {
const task = {type: 'todo', value: 12};
expect(getTaskClasses(task, 'edit-modal-text')).to.equal('task-best-modal-text');
});
it('returns best task edit-modal-icon class', () => {
const task = {type: 'todo', value: 12};
expect(getTaskClasses(task, 'edit-modal-icon')).to.equal('task-best-modal-icon');
});
it('returns best task edit-modal-option-disabled class', () => {
const task = {type: 'todo', value: 12};
expect(getTaskClasses(task, 'edit-modal-option-disabled')).to.equal('task-best-modal-option-disabled');
});
it('returns best task edit-modal-control-disabled class', () => {
const task = {type: 'todo', value: 12};
expect(getTaskClasses(task, 'edit-modal-habit-control-disabled')).to.equal('task-best-modal-habit-control-disabled');
});
it('returns create-modal-bg class', () => {
const task = {type: 'todo'};
expect(getTaskClasses(task, 'create-modal-bg')).to.equal('task-purple-modal-bg');
});
it('returns create-modal-text class', () => {
const task = {type: 'todo'};
expect(getTaskClasses(task, 'create-modal-text')).to.equal('task-purple-modal-text');
});
it('returns create-modal-icon class', () => {
const task = {type: 'todo'};
expect(getTaskClasses(task, 'create-modal-icon')).to.equal('task-purple-modal-icon');
});
it('returns create-modal-option-disabled class', () => {
const task = {type: 'todo'};
expect(getTaskClasses(task, 'create-modal-option-disabled')).to.equal('task-purple-modal-option-disabled');
});
it('returns create-modal-habit-control-disabled class', () => {
const task = {type: 'todo'};
expect(getTaskClasses(task, 'create-modal-habit-control-disabled')).to.equal('task-purple-modal-habit-control-disabled');
});
it('returns completed todo classes', () => {
const task = {type: 'todo', value: 2, completed: true};
expect(getTaskClasses(task, 'control')).to.deep.equal({
bg: 'task-disabled-daily-todo-control-bg',
checkbox: 'task-disabled-daily-todo-control-checkbox',
inner: 'task-disabled-daily-todo-control-inner',
content: 'task-disabled-daily-todo-control-content',
});
});
xit('returns good todo classes', () => {
const task = {type: 'todo', value: 2};
expect(getTaskClasses(task, 'control')).to.deep.equal({
bg: 'task-good-control-bg',
checkbox: 'task-good-control-checkbox',
inner: 'task-good-control-inner-daily-todo`',
});
});
it('returns reward classes', () => {
const task = {type: 'reward'};
expect(getTaskClasses(task, 'control')).to.deep.equal({
bg: 'task-reward-control-bg',
});
});
it('returns habit up classes', () => {
const task = {type: 'habit', value: 2, up: true};
expect(getTaskClasses(task, 'control')).to.deep.equal({
up: {
bg: 'task-good-control-bg',
inner: 'task-good-control-inner-habit',
},
down: {
bg: 'task-disabled-habit-control-bg',
inner: 'task-disabled-habit-control-inner',
},
});
});
});

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