Compare commits

..

349 Commits

Author SHA1 Message Date
Sabe Jones a4951c6478 3.104.1 2017-07-20 16:08:27 +00:00
Matteo Pagliazzi 95285cd85a do not send password to loggly (#8887) 2017-07-20 15:07:38 +02:00
SabreCat bf8b2db6b3 fix(Travis): explicitly start Mongo 2017-07-19 22:50:57 +00:00
Sabe Jones 116ec0f9d9 3.104.0 2017-07-19 18:38:43 +00:00
Sabe Jones a89633d17b chore(i18n): update locales 2017-07-19 18:37:11 +00:00
SabreCat 2e3dd27414 chore(sprites): compile 2017-07-19 18:25:14 +00:00
SabreCat 3af756a90d feat(cards): Good Luck card and achievement 2017-07-19 18:24:10 +00:00
Sabe Jones e4d006e5cd 3.103.0 2017-07-16 16:25:07 +00:00
Sabe Jones 801b53857f fix(payments): short circuit for non-gift txns (#8881) 2017-07-16 09:24:17 -07:00
Keith Holliday f8571ec5d5 Moved notifications to be read after yesterdailies (#8872)
* Moved notifications to be read after yesterdailies

* Prevent when user needs cron

* Updated tests
2017-07-16 09:24:09 -07:00
Matteo Pagliazzi 78ba596504 Groups can prevent members from getting gems (#8870)
* add possibility for group to block members from getting gems

* fixes

* fix tests

* adds some tests

* unit tests

* finish unit tests

* remove old code
2017-07-16 09:23:57 -07:00
Sabe Jones fe9521a63f 3.102.3 2017-07-14 12:11:08 +00:00
Sabe Jones c4348d8e47 Merge branch 'release' into develop 2017-07-13 22:29:35 +00:00
Sabe Jones 9b0ee6a726 3.102.2 2017-07-13 22:29:03 +00:00
Sabe Jones c23fad3077 chore(i18n): update locales 2017-07-13 22:28:23 +00:00
Sabe Jones 6850a1fcb4 chore(news): Blog Bailey 2017-07-13 22:23:35 +00:00
Keith Holliday bc477455bb Tasks cron ryamodal fixes (#8871)
* Changed assumption of timezone location

* Added checks for RYA and moved cron check

* Fixed modal scope issue
2017-07-13 15:11:27 -07:00
Sabe Jones 88c56c9877 Merge branch 'release' into develop 2017-07-12 20:42:23 +00:00
Sabe Jones 4cd272f99c 3.102.1 2017-07-12 20:41:58 +00:00
Sabe Jones 0292501387 chore(i18n): update locales 2017-07-12 20:40:13 +00:00
Keith Holliday 28b56256d2 Added catch for cron error (#8864)
* Added catch for cron error

* Updated logger usage
2017-07-12 13:35:08 -07:00
Sabe Jones a4ee3aaa7e Merge branch 'release' into develop 2017-07-11 21:48:18 +00:00
Sabe Jones 7a2432a1a0 3.102.0 2017-07-11 21:42:06 +00:00
Sabe Jones 0bf515d0f9 chore(i18n): update locales 2017-07-11 21:40:44 +00:00
SabreCat cbed198f02 chore(sprites): compile 2017-07-11 21:32:53 +00:00
SabreCat 90d77cee81 feat(content): Orca Pets and Splashy Pals 2017-07-11 21:31:26 +00:00
SabreCat 3668a9d3ac fix(sprites): correct canvases
Also adds sprites for Trapper Santa collection quest.
2017-07-11 21:29:48 +00:00
Keith Holliday 052c653cd3 Added initial inbox (#8852)
* Added initial inbox

* Minor styles

* Added more styles, selected conversations, sending messages

* Added translations and colors
2017-07-10 21:09:31 -06:00
Keith Holliday 3aa7b4b631 Fixed or logic break (#8857) 2017-07-10 21:08:35 -06:00
Sabe Jones 045378b820 Merge branch 'release' into develop 2017-07-10 20:20:24 +00:00
Sabe Jones a256b6df03 3.101.2 2017-07-10 20:07:46 +00:00
Keith Holliday 92fdc13adf Added assumption that the date passed is in the users timezone (#8859) 2017-07-10 13:07:14 -07:00
negue dd29c60d87 [WIP] drag/drop fixes (#8851)
* Stable: Highlight pet on dragging food / add drag* events

* Stable: hatch dialog instead of popover / .btn-flat / update bootstrap-vie

* Layout: change sidebar width to fixed 236px - removed .col-2/.col-10

* Stable: Drag&Drop Food Image + Text - refactor directive to use custom event names

* Stable: fixes

* Stable: click to select food + attached food info box

* fix lint issues

* Drag&Drop&Click: Potions on Eggs - fix click on item + attached infobox-position in click mode
2017-07-10 10:07:23 +02:00
Sabe Jones 78fd79931e Merge branch 'release' into develop 2017-07-08 01:49:25 +00:00
Sabe Jones 5055a57ae9 3.101.1 2017-07-08 01:48:50 +00:00
Keith Holliday 3fa0b72ffe Added assumption when no time is supplied (#8855)
* Added assumption when no time is supplied

* Changed format of date

* Set now options when date is specified
2017-07-07 18:46:54 -07:00
Keith Holliday b3f9fd09c6 Check for app loading instead of daily count (#8856) 2017-07-07 12:35:26 -07:00
Keith Holliday 30437e158e Added check to prevent double cron (#8854)
* Added check to prevent double cron

* Added then and fix style issues
2017-07-07 11:14:36 -07:00
Keith Holliday 19ba1290f6 Added new web profile for checkout (#8699)
* Added new web profile for checkout

* Fixed es6 syntax

* Fixed config path
2017-07-06 15:16:54 -07:00
Grayson Gilmore c509c8e04f Test /api/v3/tasks/unlink-all and unlink-one (#8810)
* Add tests for POST /tasks/unlink-all/:challengeId

* Add test for keep=keep-all query

* Cleanup

* Setup

* Cleanup

* Add tests for unlink-one
2017-07-06 14:25:31 -07:00
Todd Medema 00d4393024 reduce top margins on task filter bar to reduce dead space below 'add multiple' link / make UI more space efficient (#8806) 2017-07-06 14:13:35 -07:00
yugensoft c502b1997b Updated new-groups / group-plans static page #8674 (#8729)
* dummy

* Renamed internationalized strings to more meaningful names

* moved the new group creation state out to its own URL, so it can also be linked to by the static/plans page

* Added redirect-through-login functionality from the static/plans page new-group button
This includes a static non-modal login page (similar to how other sites have both a login page and login modal)
The login body has been abstracted out from its modal-specific view into mixins to accomplish this

* deleted bak files added by mistake

* deleted scripts added by mistake

* changed static/plans Create Group button text

* Added form link (https://github.com/HabitRPG/habitica/issues/8674#issuecomment-303518039)
Removed changes to non-EN locale files (https://github.com/HabitRPG/habitica/pull/8729#issuecomment-303555211)

* reverted key name changes as per https://github.com/HabitRPG/habitica/pull/8729#issuecomment-304515534

* changed $rootScope to $scope
https://github.com/HabitRPG/habitica/pull/8729#discussion_r120695874
2017-07-06 14:08:02 -07:00
madpink 4435862ff2 Updating User API Doc (part 4) (#8792)
* Updating User API Doc (part 4)

* Fixed trailing space
2017-07-06 13:48:00 -07:00
Keith Holliday e901850a6f continuation of PR #8161 Display error notification when attempting to purchase invalid amount of gems - fixes #8145 (#8688)
* Translation string for error notification

* Use function instead of a link for paypal

* Inject notification service, function to check the amount of gems to purchase, function to handle payments with paypal

* Throw error if amount of gems is zero or negative

* Add condition to raise error if amount is negative

* Added gem errors for gifts 0 or less

* Fixed linting and broken test

* Fixed test syntax

* Added back needed strings

* Fixed group locales
2017-07-06 13:43:43 -07:00
LlamaLad 48bbc22fb4 Dailies new subheading and summary change (#8799)
* Dailies new subheading and summary change

* summary change
2017-07-06 13:31:13 -07:00
Alys c1e5d8b573 add missing string for mountNowOwned error message (#8843) 2017-07-06 13:23:20 -07:00
Andrew Schultz 6951b79b95 two typos in questsContent (#8837) 2017-07-06 13:20:34 -07:00
Keith Holliday 1c3a12f37d [WIP] Added initial tavern updates (#8845)
* Added initial tavern updates

* Translations and styles

* Fixed locales and lint
2017-07-06 09:38:52 -06:00
Keith Holliday 3c71748a1b Renamed production docker file (#8850) 2017-07-05 13:29:25 -07:00
Sabe Jones 8e56222fc7 Merge branch 'release' into develop 2017-07-05 20:11:13 +00:00
Sabe Jones ac3ac73737 3.101.0 2017-07-05 20:10:53 +00:00
Sabe Jones 8004c1e759 chore(i18n): update locales 2017-07-05 20:10:13 +00:00
SabreCat d9e50e7632 Merge remote-tracking branch 'TheHollidayInn/yesterdailies-2' into release 2017-07-05 20:04:26 +00:00
SabreCat 4cd742c19a feat(event): Summer Splashy Foam 2017-07-05 20:03:23 +00:00
Keith Holliday 6bc6c09c75 New client creator (#8841)
* Added initial creator

* Added initial styles and functionality for creator

* More style changes

* Translations and styles

* More active classes

* Removed extra locales
2017-07-04 13:11:08 -05:00
Sabe Jones 0cbf9354cc Merge branch 'release' into develop 2017-07-03 23:05:52 +00:00
Sabe Jones 03ca65f2b8 3.100.0 2017-07-03 23:05:28 +00:00
Sabe Jones ea74271484 chore(i18n): update locales 2017-07-03 23:04:47 +00:00
SabreCat 0897367538 chore(sprites): compile 2017-07-03 22:56:48 +00:00
SabreCat 2566fcecb7 feat(content): Backgrounds and Armoire 2017-07 2017-07-03 22:54:58 +00:00
Keith Holliday c99863f339 Added yesterdaily field (#8847)
* Added yesterdaily field

* Added yesterdaily migration

* Added description
2017-07-03 13:55:53 -05:00
Keith Holliday 71d632f6f0 Merge remote-tracking branch 'upstream/release' into yesterdailies-3 2017-07-03 09:46:34 -05:00
Joe P 06c53677f6 Group Approvals Formatting - Fix #8677 (#8784)
* Add task indication to Group Plans Tasks Awaiting Approval page

* Render markdown tasks on Group Plans Tasks Awaiting Approval page

* Fix panel code- fixes formatting issue of Approve button on Group Plans Tasks Awaiting Approval page
2017-07-01 09:33:24 -07:00
Sabe Jones aa91c5dbae Kubernetes support for dev environments (#8753)
* Run Habitica in Kubernetes

* fix(docs): Address PR comments
2017-07-01 09:30:08 -07:00
Keith Holliday 5b7c7b77c8 Added docker production file (#8846)
* Added docker production file

* Added tag clone

* Removed docker prod
2017-07-01 09:28:51 -07:00
Vicente d3d221dccb Repeat monthly fix https://github.com/HabitRPG/habitica/issues/8831 (#8835)
* checking nexDueDate if it's an array before applying .map

* used array created

* change let for 'strict mode'

* Used ajax when canceling from the website (#8697)

* Used ajax when canceling from the website

* Fixed grammar issue

* Payments gem reset (#8712)

* Added gem reset if user does not have date last updated set

* Fixed login of adding updated date

* fix bug that prevented sending of emails to admin addresses (#8832)

* feat(content): June 2017 subscriber items

* chore(sprites): compile

* chore(i18n): update locales

* 3.98.0

* [WIP] client/inventory/stable (#8737)

* Stable: basic layout (based on equipment)

* extract item popover-content as component

# Conflicts:
#	website/client/components/inventory/item.vue

* extract item-content as slot - list standard pets with image

* dynamic list petGroups in sidebar / content - with selected / open filter

* itemContentClass for pets

* fix filter - extract filter labels

* Filter: Hide Missing

* fix position of pets

* sort by: A-Z, Color, Hatchable

* refactor animalList - created the list once per type and cache it - sort now works before viewing one or all pets

* custom petItem to show the progress

* list specialPets - rename petGroup to animalGroup (more generic)

* equip a pet

* filter animals by input

* count pets

* list mounts

* hatchable pet popover

* hatchable pet popover #2

* PixelPaw Opacity for not owned and not hatchable pets - change item background for unowned pets

* Hold to hatch the pet - cleanup

* add food drawer + countBadge - special mounts - hide un-owned special animals - fixes

* Sliding Drawer: Buttons to scroll left/right

* Drag&Drop: food on pets

* fix hold to hatch - use mouseleave event

* 'Show All' / 'Show Less' - Animals

* Matts Image + Popover + use image width as sidebar width (removed col-2 / col-10)

* fixes: colors, v-once, drawer-errorMessage, strings

* drawerSlider - split items to pages / add divider / add first item of the next page - ResizeDirective

* ResizeDirective - throttle emits by `v-resize="value"` - fix drawer left padding

* show animals by available content width

* change sortBy button / label

* fix pet colors / backgrounds

* DragDropDirective - grabbing cursor

* remove browser specific prefixes

* fix lint issues

* show welcome dialog

* change translationkey (noFood, already exists)

* used array created

* change let for 'strict mode'
2017-07-01 09:25:04 -07:00
negue 53c610236b Client/stable fixes - part 1 (#8840)
* fixes: show ten pets in a row - show pet / no feeding progress if mount already owned

* fixes: disabled filter instead of hiding them

* fixes: don't hide special pets - same item style for mounts

* fixes: avatar changes of pet / mount

* fixes: unfocus first dropdown-item by css

* fixes: unfocus first dropdown-item by css (remove :focus style)- added "What does my pet like to eat?" popover
2017-07-01 17:46:24 +02:00
Sabe Jones 548cc2ceb0 3.99.1 2017-06-30 21:05:16 +00:00
Sabe Jones bf03ac8e44 chore(i18n): update locales 2017-06-30 20:59:13 +00:00
SabreCat 1853113aed Merge branch 'develop' into release 2017-06-30 20:50:18 +00:00
SabreCat 1ce1ed7c6b fix(test): allow for long months 2017-06-30 20:47:18 +00:00
Sabe Jones c9f870a929 3.99.0 2017-06-29 21:34:53 +00:00
Sabe Jones 135df1dc48 chore(build): update Dockerfile 2017-06-29 21:34:39 +00:00
Sabe Jones a240d9581f chore(i18n): update locales 2017-06-29 21:30:15 +00:00
SabreCat 3cd591ed4f chore(sprites): compile 2017-06-29 21:22:11 +00:00
SabreCat 87076ed07c feat(content): Aqua Potions 2017-06-29 21:21:20 +00:00
Matteo Pagliazzi 33a39d3683 Client fixes (#8844)
* client: fix router when not authenticated, small fixes for tasks

* load the user only when necessary

* fix tests
2017-06-29 20:49:05 +02:00
Keith Holliday e4f5950ffc Added required field 2017-06-29 08:11:02 -06:00
Keith Holliday f2d81a8d9c Merge remote-tracking branch 'upstream/develop' into yesterdailies-3 2017-06-28 15:25:59 -06:00
Keith Holliday 43b6f71044 Fixed lint issue 2017-06-28 15:25:47 -06:00
Matteo Pagliazzi 06de1670b4 client: fix build and gradient 2017-06-28 10:53:35 +02:00
Keith Holliday 7fd2522e93 Merged in develop 2017-06-27 22:23:13 -06:00
Keith Holliday acb4b79078 Added option for getting isDue field for specified date 2017-06-27 22:22:20 -06:00
Keith Holliday 8299982484 Ensured damage was only done for dailies that were due yesterday 2017-06-27 22:01:01 -06:00
Keith Holliday 207b1e91ca Added regsiter page and styles (#8836)
* Added regsiter page and styles

* Added style updates

* Added login server connection and logout

* Added login

* Added social auth

* Moved image assests

* Added trasnlations

* Added social icons

* Removed duplicate

* Updated shrinkwrap
2017-06-27 21:53:59 -06:00
Keith Holliday aee21edd5f [WIP] Began adding tavern and party (#8814)
* Began adding tavern and party

* Fixed routing conflicts and tavern constant

* Updated button styles

* Added not on quest block

* Added no challenge block

* Began adding quest details

* Began updating group create modal to have party info

* Added create party modal

* Added share userid menu

* Began adding toggle

* Finished toggle styles

* Added start quest modal

* Ported over party ctrl code

* Finished porting over party ctrl code

* Added more quest actions

* Added own quests modal and quest modal details

* Fixed member modal styles and icons

* Added many random style updates

* Small text align fix

* Removed extra update

* Removed config and extra key

* Removed client string extras
2017-06-27 14:02:55 -06:00
Sabe Jones 6e1bbd05bc Merge branch 'release' into develop 2017-06-27 17:43:13 +00:00
Sabe Jones 763c2c28ea 3.98.1 2017-06-27 17:42:52 +00:00
Sabe Jones 7aa9ba45f6 chore(i18n): update locales 2017-06-27 17:39:08 +00:00
Sabe Jones d67398fba6 chore(news): Guild Spotlight Bailey 2017-06-27 17:33:54 +00:00
Matteo Pagliazzi 6fb4c1b576 Client Tasks v1 (#8823)
* remove unused elements from tasks page

* remove components

* client: tasks: wip

* tasks: order, start styling them

* more tasks works

* habits controls

* more work

* tasks icons

* split columns in their own component

* implement tags for tasks

* wip

* add columns description
2017-06-26 23:55:14 +02:00
Alys 7c5bd526b1 minor text fixes: accurate flavour text for Golden Knight testimonies collection quest, etc (#8826)
* make comment more accurate: members are removed, not banned

They can rejoin with an invitation in a private group or at any time in a public group.

* change windows line breaks to unix line breaks

* change flavour text of Golden Knight collection quest to reduce number of testimonies

* fix grammatical error noticed by mandyzhou

* improve message about not being able to send PMs because we often see people report it as a bug

* update instructions for cancelling Google subscriptions (thanks to Scea for noticing)

* change Delete Completed on-hover message - fixes #8598

* correct the Orb of Rebirth's text about pets and mounts (they are not locked)
2017-06-26 22:35:29 +01:00
negue 187a9f5f19 [WIP] client/inventory/stable (#8737)
* Stable: basic layout (based on equipment)

* extract item popover-content as component

# Conflicts:
#	website/client/components/inventory/item.vue

* extract item-content as slot - list standard pets with image

* dynamic list petGroups in sidebar / content - with selected / open filter

* itemContentClass for pets

* fix filter - extract filter labels

* Filter: Hide Missing

* fix position of pets

* sort by: A-Z, Color, Hatchable

* refactor animalList - created the list once per type and cache it - sort now works before viewing one or all pets

* custom petItem to show the progress

* list specialPets - rename petGroup to animalGroup (more generic)

* equip a pet

* filter animals by input

* count pets

* list mounts

* hatchable pet popover

* hatchable pet popover #2

* PixelPaw Opacity for not owned and not hatchable pets - change item background for unowned pets

* Hold to hatch the pet - cleanup

* add food drawer + countBadge - special mounts - hide un-owned special animals - fixes

* Sliding Drawer: Buttons to scroll left/right

* Drag&Drop: food on pets

* fix hold to hatch - use mouseleave event

* 'Show All' / 'Show Less' - Animals

* Matts Image + Popover + use image width as sidebar width (removed col-2 / col-10)

* fixes: colors, v-once, drawer-errorMessage, strings

* drawerSlider - split items to pages / add divider / add first item of the next page - ResizeDirective

* ResizeDirective - throttle emits by `v-resize="value"` - fix drawer left padding

* show animals by available content width

* change sortBy button / label

* fix pet colors / backgrounds

* DragDropDirective - grabbing cursor

* remove browser specific prefixes

* fix lint issues

* show welcome dialog

* change translationkey (noFood, already exists)
2017-06-23 13:24:10 +02:00
Sabe Jones 7954763738 Merge branch 'release' into develop 2017-06-22 23:05:23 +00:00
Sabe Jones d062fd8507 3.98.0 2017-06-22 23:04:42 +00:00
Sabe Jones ceb10137a2 chore(i18n): update locales 2017-06-22 23:04:09 +00:00
SabreCat 58330a4d01 chore(sprites): compile 2017-06-22 22:55:36 +00:00
SabreCat 678f48fde9 feat(content): June 2017 subscriber items 2017-06-22 22:54:17 +00:00
Alys a642d94443 fix bug that prevented sending of emails to admin addresses (#8832) 2017-06-22 14:33:27 -07:00
Keith Holliday 39a112b605 Payments gem reset (#8712)
* Added gem reset if user does not have date last updated set

* Fixed login of adding updated date
2017-06-22 14:21:09 -07:00
Keith Holliday 8c8f0ea201 Used ajax when canceling from the website (#8697)
* Used ajax when canceling from the website

* Fixed grammar issue
2017-06-22 14:13:53 -07:00
Keith Holliday ca8541e8c4 Added needsCron field 2017-06-21 13:28:12 -06:00
Sabe Jones 773d546e4f Merge branch 'release' into develop 2017-06-21 04:01:33 +00:00
Sabe Jones f269d381da 3.97.2 2017-06-21 04:00:53 +00:00
Sabe Jones 3483f69559 fix(sprites): realign healer fins 2017-06-21 03:59:51 +00:00
Sabe Jones 9ada0c9b02 Merge branch 'release' into develop 2017-06-21 01:52:36 +00:00
Sabe Jones 8bbe9ac36e 3.97.1 2017-06-21 01:52:08 +00:00
Sabe Jones 75b93e6ec4 fix(sprites): unbleach Matt 2017-06-21 01:51:50 +00:00
Sabe Jones bd9bebe591 Merge branch 'release' into develop 2017-06-21 01:26:18 +00:00
Sabe Jones f7b298d506 3.97.0 2017-06-21 01:25:54 +00:00
Sabe Jones 4bd2932955 chore(i18n): update locales 2017-06-20 22:18:38 +00:00
SabreCat 7060f5941d chore(sprites): compile 2017-06-20 22:10:04 +00:00
SabreCat 21379ee357 feat(event): Summer Splash 2017 2017-06-20 22:08:38 +00:00
Keith Holliday fe62713809 Added migration to go from prod to test db (#8816) 2017-06-20 06:17:10 -06:00
Sabe Jones ca7272a987 Merge branch 'release' into develop 2017-06-19 22:44:02 +00:00
Sabe Jones cb46cd8eeb 3.96.2 2017-06-19 22:43:34 +00:00
Sabe Jones d9d02ca81d chore(i18n): update locales 2017-06-19 22:43:18 +00:00
Sabe Jones 50c216eb41 chore(news): Bailey announcement 2017-06-19 22:38:06 +00:00
SabreCat 2c60fade01 Merge branch 'release' into develop 2017-06-17 20:20:52 +00:00
SabreCat 6bdf8fdabc 3.96.1 2017-06-17 20:19:35 +00:00
SabreCat 3db304b6bf fix(potions): disable Florals
Also add sprite for Bars of Soap in Attack of the Mundane 1
2017-06-17 20:18:36 +00:00
Matteo Pagliazzi ea2788ab2f fix gulp in production 2017-06-17 19:02:03 +02:00
Sabe Jones 5e111f6f6c Merge branch 'release' into develop 2017-06-16 20:42:52 +00:00
Sabe Jones ea85a8a3a8 3.96.0 2017-06-16 20:42:15 +00:00
Sabe Jones e811a2700e feat(migration): update achievements (#8825) 2017-06-16 13:41:25 -07:00
Keith Holliday 136dcd27a9 Prevented modal close and showed correct due class in modal 2017-06-16 12:28:42 -06:00
Sabe Jones bf76f823d5 chore(i18n): update locales 2017-06-16 17:43:26 +00:00
Sabe Jones 83b573dcfc chore(i18n): update locales 2017-06-16 17:42:28 +00:00
Matteo Pagliazzi c4fa9426b3 Client Tasks v1 / Bootstrap configurable (#8822)
* make bs4 configurable, change gutters to match zeplin\s

* correctly customize gutters
2017-06-16 18:58:34 +02:00
Matteo Pagliazzi 042e5a8d63 Client Fixes (#8821)
* new client: fix animation flickering

* fix transitions

* update copy
2017-06-16 18:07:24 +02:00
Keith Holliday 7a8857010e Resized tasks, filter completed, add cron for non yesterdailies 2017-06-16 09:01:23 -06:00
Keith Holliday a52bd66871 Fixed strings, yesterdaily filtering and cron check 2017-06-15 10:13:33 -06:00
Keith Holliday f7ce269f3c Add false return when repeats are empty (#8777)
* Add false return when repeats are empty

* Added front end check for repeats on monthly-daysOfWeek

* Fixed tests with static date
2017-06-14 11:27:50 -07:00
Sabe Jones c084f8a2b9 Merge branch 'release' into develop 2017-06-13 21:25:41 +00:00
Sabe Jones c1c42e17b8 3.95.0 2017-06-13 21:21:55 +00:00
Sabe Jones 306a782e7a chore(i18n): update locales 2017-06-13 21:20:48 +00:00
SabreCat 20178c0722 chore(sprites): compile 2017-06-13 21:10:02 +00:00
SabreCat 256e2e809c feat(content): Nudibranch Pets 2017-06-13 21:09:13 +00:00
Matteo Pagliazzi 592345e22c Party members in header v2 (#8815)
* update comemnt

* flyout on hover

* fix hasClass and isBuffed

* polish members in party header
2017-06-13 20:55:45 +02:00
Keith Holliday f738f550e7 Updated copy 2017-06-12 07:01:14 -06:00
Keith Holliday 495bc9aa50 Removed let from angular 2017-06-11 22:55:35 -06:00
Matteo Pagliazzi 292b2acb1e client: fix production path for chunks 2017-06-08 19:17:35 -07:00
Matteo Pagliazzi 977f9d5174 Client: party members in header (#8804)
* wip party members in header

* wip

* add inbox routes back

* polishing
2017-06-08 18:24:40 -07:00
Keith Holliday 36fa3ab06f Added cron check 2017-06-08 17:10:22 -07:00
Keith Holliday 7422d020b1 Ported over UI code 2017-06-08 16:03:20 -07:00
Keith Holliday 5d0fe0aac3 Added yesterdailiy to model 2017-06-08 15:37:36 -07:00
Matteo Pagliazzi 85644fdc1b client: user -list-detail -> member-details 2017-06-08 14:36:56 -07:00
Matteo Pagliazzi 138b5c4bdb wip client/header-party-members (#8803) 2017-06-08 14:33:23 -07:00
Keith Holliday 52edb8a8da New client members (#8795)
* Began styling member modal

* Added store and updated modal styles

* Began converting angular

* Ported over angular routes

* Fixed lint issues
2017-06-08 14:27:22 -07:00
Keith Holliday 60de7c8f21 Added cron route 2017-06-08 14:07:33 -07:00
Keith Holliday 137636cb40 Removed cron from every route 2017-06-08 14:03:30 -07:00
Keith Holliday 1999e1098e Allow guilds edit (#8800)
* test: test that admin users can update guilds

* test: test admin removeMember privileges

* fix: allow admins to edit guilds

* fix: add edit guild options for admins

* test: test that admin can't remove current leader

* Add error msg for removing current leader

* Taskwoods Quest Line (#8156)

* feat(content): Gold Quest 2016-10

* chore(news): Bailey

* chore(i18n): update locales

* chore(sprites): compile

* 3.49.0

* chore: update express

* Fix for the ReDOS vulnerability

habitica is currently affected by the high-severity [ReDOS vulnerability](https://snyk.io/vuln/npm:tough-cookie:20160722). 

Vulnerable module: `tough-cookie`
Introduced through: ` request`

This PR fixes the ReDOS vulnerability by upgrading ` request` to version 2.74.0

Check out the [Snyk test report](https://snyk.io/test/github/HabitRPG/habitica) to review other vulnerabilities that affect this repo. 

[Watch the repo](https://snyk.io/add) to 
* get alerts if newly disclosed vulnerabilities affect this repo in the future. 
* generate pull requests with the fixes you want, or let us do the work: when a newly disclosed vulnerability affects you, we'll submit a fix to you right away. 

Stay secure, 
The Snyk team

* Documentation - coupon

closes #8109

* fix(client): Allow member hp to be clickable

fixes #8016
closes #8155

* chore(npm): shrinkwrap

* test: test isAbleToEditGroup

* Add isAbleToEditGroup to groupsCtrl

* Remove unnecessary ternary

* Fix linting

* Move edit permission logic out to groupsCtrl

* fix: change ternary to boolean

* Fix linting

* Fixed merge issues
2017-06-08 13:45:24 -07:00
Keith Holliday 4d3a0c0571 Fixed issue with repeat settings turning false (#8773) 2017-06-08 12:18:49 -07:00
Matteo Pagliazzi 706de95458 Client: Header & Menu & Icons (#8770)
* header revamp - wip

* fix webpack fonts

* wip icons

* fix compilation errors

* implement icons loading without iconmoo

* new svg implementation

* wip

* fix issues with svgs

* fix issues with svgs

* fix bits svg

* fix displaying of pet in avatar

* avatar class icon

* no party header

* update navigation

* split code by route

* round gems and gp

* add string for faqs

* fix icons in css
2017-06-08 12:04:19 -07:00
Vince Campanale e3c1eaa9d2 Preventing cardRead from notifying user. (#8473)
* added condition to prevent readCard operations from sending a notification

* created constant array to contain opNames for notifications we want to suppress and adjusted condition to accordingly

* replaced const with var to past karma test
2017-06-08 11:59:37 -07:00
Keith Holliday 17c0f795cc Began styling member modal 2017-06-08 11:03:06 -07:00
Sabe Jones cb5ac9014e Include apidoc in test script (#8797)
* test(docs): include apidoc in script

* fix(test): also run apidoc on Travis
2017-06-08 10:40:10 -07:00
joe-salomon 0be681b7a2 Dailies performance fix - fixes #8756 (#8767)
* Changed recurring logic to not use moment-recur plugin for performance reasons

* change only nextDue calculations
add tests to make sure proper nextDue values are calculated
revert schedule.matches logic to original
revert shouldDo.test.js to original

* fix monthly nextDue logic
move tests to shouldDo.test.js

* typos

* revert to original logic. change not needed

* add failure cases
2017-06-08 10:34:05 -07:00
Sabe Jones 5360f9e587 Align doubled achievement popovers (#8798)
* bug(profile): align both achievement popups (hover vs. click)

* refactor(style): move to CSS/Stylus
2017-06-07 21:05:24 -07:00
Keith Holliday 4553a411f6 Paypal ipn options (#8713)
* Added more acceptable ipn cancelation options

* Fixed lint issue

* Fixed spelling issue
2017-06-07 10:31:44 -07:00
Alys 613f51b08d use new email template when joining a group plan for customisation of subscription cancellation information (#8637)
* use new email template when subscription is cancelled from joining a group plan

* use new email template when subscription is cancelled from joining a group plan - needs more code, tests

* change from sending new email as a cancel-subscription option to sending as a group plan join email

Uses a new group-member-join email template instead of old group-member-joining because new template includes mandril conditional merge tags.

Also adds tests and comments. Edits some comments for accuracy and typo fixes.

* adapt group-member-join email template for manual cancel message for iOS and Android subscriptions

* save test user so its profile name can be read by calls to sendTxn

* add documentation for the user model cancelSubscription function

* add constants for strings passed to mandrill email templates
2017-06-07 10:25:37 -07:00
joe-salomon 2292ba2694 Fix subscriptions ending early - fixes #8600 (#8746)
* Use “now” for calculation of the subscription end date instead of plan.dateUpdated

* add test to show previously incorrect logic does not affect sub end date.
2017-06-07 10:16:55 -07:00
joe-salomon befacca457 Keep existing Mystery Items and Hourglasses when adding to group - fixes 8643 (#8745)
* Modified addSubToGroupUser to save existing mysteryItems and trinkets from an expired subscription
Added unit test

* fix eslint error
2017-06-07 09:59:09 -07:00
Airu 5cd30b430d Added Arashi's theme as a new audio theme (#8707)
* Add existing file

* Update menu.jade
2017-06-07 09:53:11 -07:00
Kevin Smith c5d9ee1e0a Implemented new Achievement and Badge: Joined a Challenge (Fixes #8613) (#8761)
* Added image

* Added new achievement to user schema

* Added new achievement to content

* Added new achievement to libs

* Added achievement text to locale

* Added achievement to notification model and controller

* Grant achievement on joining or creating first challenge

* Added achievement to modal template

* Compiled new sprites

* Added integration tests

* Fix linting error
2017-06-07 09:43:16 -07:00
Sabe Jones 234328f2ba Reduce difficulty of collection quests (#8754)
* create script to insert message into party chat because collection quest is now easier

See https://github.com/HabitRPG/habitrpg/pull/7987 for more details.

* fix(quests): make collection less burdensome

* refactor(migration): return groups directly
2017-06-06 20:14:26 -07:00
SabreCat 029afa197e fix(achievements): move year-round cards out of seasonal 2017-06-07 02:41:13 +00:00
SabreCat 05b35c5147 fix(manifest): remove deleted files from manifest 2017-06-07 02:11:47 +00:00
MathWhiz c9427ad34c New cards — Congratulations, Get Well (#8655)
* Add card and achievement sprite for Congrats card

* Add data regarding Congrats card

* Add Get Well card

* Add Get Well images

* Add schema

* Remove `if (!target.flags) target.flags = {};` code from cards

* Remove white backgrounds for congrats sprites

* add inital tests for cards

* Fix card tests

* Fix invalid urls in tests

* Update POST-user_class_cast_spellId.test.js

* Update POST-user_class_cast_spellId.test.js

* Update POST-user_class_cast_spellId.test.js

* Update congrats card sprite

* Fix card logic

* Fix user schema

* Change achievement values for new cards to Number

* Resize congrats and getwell cards

This will make them be sized properly

* Separate Market from Drops

* Extract cards to new section

* fix(sprites): revert spritesheet changes

* Add flags if target does not have them
2017-06-06 19:04:54 -07:00
madpink d6c62262f1 Updating User API Doc (part 3) (#8720)
* Updating User API Doc (part 3)

* Updating User API Doc (part 3)

Fixed trailing spaces

* Updating User API Doc (part 3)

Made changes to @apiParamExample to make multi-line (which may have been cause of apiDoc failing)

* Updated quests to add questKey
2017-06-06 18:57:17 -07:00
MathWhiz ec1d378504 Flagged chat messages are visible to the users that posted them (#8726)
* Allow users to see their chat messages that are hidden to others

* Fix lint

* Fix failing api test

* Add test
2017-06-06 18:55:12 -07:00
beatscribe 3bb88f450a New Beatscribe 8-bit sound theme (#8727) 2017-06-06 18:53:10 -07:00
Rick Kasten 97a38e68c5 Clean up references to repo as HabitRPG/habitrpg (#8742)
* Confirmed changes

* Removed bad link
This was apparantly missed in #8051

* Confirmed changes

* Fixed links to milestones
2017-06-06 18:51:54 -07:00
Alys be948a1bf2 adjust postinstall command so that it works in Windows as well as *nix (#8744)
Semicolons in postinstall commands don't work in Windows.

'&&' works in *nix and in at least some versions of Windows.

This changes the meaning of the postinstall line slightly because
now the later commands won't run if the earlier ones failed but I
don't see that being a problem.
2017-06-06 18:49:31 -07:00
Sabe Jones 018976a723 Disallow interactions by blocked users; new "get objections" Members API route (#8755)
* Make flags.chatRevoked prevent sending private messages (issue #7971)

* Disallow sending gems when messages aren't allowed.

* Created function to check for objections to an interaction to user model and wired it into the API (issue #7971)

* Fixes for issues raised by reviewers.

* Added allowed values to apidoc for api.getObjectionsToInteraction.

* Refactoring of getObjectionsToInteraction and minor API changes.

* fix(objections): address PR comments

* fix(strings): use US English for base edits

* refactor(test): typos and phrasing
2017-06-06 18:49:05 -07:00
Grayson Gilmore 00e5896ac6 Add test for GET /shops/backgrounds (#8771) 2017-06-06 18:45:41 -07:00
Kevin Smith 36bc693545 Turtleheads (Fixes #8560) (#8776)
* Added new turtle head icons

* Recompiled spritesheets
2017-06-06 18:43:19 -07:00
Atte Kortesmaa f27706cb4b Improved API documentation for hall #8087 (#8536)
* Improved API documentation for hall

* Fixes typos, removes apiHeader definitions and curl example

* Fixes @apiParam and capitalization errors. Moves @apiDefines to website/server/api-doc.js
2017-06-06 11:48:11 -07:00
MathWhiz f6f99ec57e Require Dailies to have a Start Date (#8649)
* Require Dailies to have a Start Date

* Add preliminary test

* Fix lint errors
2017-06-06 10:05:17 -07:00
MathWhiz c852d9d581 Add new favicon (#8732)
* Add new favicon

* Update 192x192 favicon image
2017-06-05 22:55:44 -07:00
Sabe Jones 4a78514308 3.94.1 2017-06-04 23:41:14 +00:00
Sabe Jones 265b48752d Merge branch 'develop' into release 2017-06-04 23:40:26 +00:00
Sabe Jones db0b0d6b6d fix(migrations): return to generic state 2017-06-04 02:59:49 +00:00
Sabe Jones 20f1087552 fix(migrations): return to generic state 2017-06-04 02:58:42 +00:00
Keith Holliday 2e9bc2c31c New client guilds (#8736)
* add colors palette

* add secondary menu component and style it

* add box shadow to secondary menu

* misc css, fixes for secondary menu

* client: add equipment page with grouping, css: add some styles

* add typography

* more equipment

* stable: fix linting

* equipment: add styles (lots of general styles too)

* remove duplicate google fonts loading

* add dropdowns

* design: white search input background, remove gray from items

* start adding drawer and selected indicator

* wip equipment

* fix equipment

* equipment: correctly bind new properties on items.gear.equipped

* equipment: fix vue binding. version 2

* equipment: fix vue binding. version 3

* back to first fix for equip op, fix for sourcemaps, send http request when an item is equipped, load bootstrap-vue components where needed

* checkboxes and radio buttons

* correctly renders selected items in first postion during the first render

* add search

* general changes, constants part of app state, add popovers

* add toggle switch, rename css

* correct offset

* upgrade deps

* upgrade deps

* drawer and lot of other work

* update equipping mechanism

* finish equipment

* fix compilation and upgrade deps

* use v-show in place of v-if to fix ui issues

* v-show -> v-if

* Start of guild syyles

* fix linting in test/client

* fix es6 compilation in test/client

* fix babel compilation for tests

* fix groupsUtilities mixin tests

* More designs

* Added public guild state

* Added my guilds store

* client: buttons

* client: buttons: fix colors

* Added join and leave

* Began adding new guild form

* Create form updates

* Added search to local data

* Added filtering

* Added initial code for group create

* Added more create checks

* Added more guild routes

* Added styles to guild page

* Added more chat styles

* Began porting over angular functions

* Moved over group service functions

* Added paging

* Updated sidebar

* Updated join/leave and minor text

* Added new sidebar functions

* Updated paging

* Added some form updates

* Added more translations and styles

* Updated shrinkwrap

* Removed features config

* Lint cleanup

* Added member modal

* Added more member actions

* Updated nav

* Fixed filter toggling

* Updated create guild

* Added no guild page

* Added sort select

* Added more styles

* Added update guild form

* Removed extra css and other minor changes

* Many css and syntax fixes

* Fixed color and merge conflic

* Removed paging from my guilds

* Removed extra strings

* Many requests updates

* Small style fixes
2017-06-02 14:55:02 -06:00
SabreCat b606dd1c40 fix(strings): remove extraneous title text 2017-06-02 16:19:42 +00:00
SabreCat db1c2fd5a2 fix(shops): don't push if empty
Also corrects text on hatching potions
2017-06-02 15:28:28 +00:00
Sabe Jones de1e477ce2 Merge branch 'release' into develop 2017-06-02 00:49:00 +00:00
Sabe Jones 67318177a2 3.94.0 2017-06-01 23:18:07 +00:00
Sabe Jones cd1be828ca chore(i18n): update locales 2017-06-01 23:17:32 +00:00
SabreCat 9ffebc10a7 chore(sprites): compile 2017-06-01 23:09:05 +00:00
SabreCat 5cd11ed343 feat(content): Armoire and BGs 2017-06-01 23:08:00 +00:00
SabreCat 9de118f0d9 Merge branch 'release' into develop 2017-05-30 21:00:53 +00:00
SabreCat 5fbec4069e 3.93.2 2017-05-30 20:59:24 +00:00
SabreCat a0f10cbf4b Merge branch 'THI/sleep-dailies-fix' into release 2017-05-30 20:58:33 +00:00
Keith Holliday 0e069e78d5 Set isDue and NextDue during sleep (#8769) 2017-05-30 15:57:38 -05:00
SabreCat dea847ba1a chore(news): Bailey 2017-05-30 20:11:21 +00:00
Sabe Jones 46ed1813c6 Optional feedback on account deletion (#8750)
* Fixed rebase.

* Removed commented out mail sending to pass linting. Styles from settings.styl still not propagating to app.css

* fix(feedback): address PR comments

* fix(style): linting errors
2017-05-30 11:54:42 -05:00
Keith Holliday e9750353a7 Set isDue and NextDue during sleep 2017-05-30 09:12:09 -06:00
Sabe Jones 05ea2c1ce6 Merge branch 'release' into develop 2017-05-29 01:11:00 +00:00
Sabe Jones 74d1c7763e 3.93.1 2017-05-29 01:08:31 +00:00
Keith Holliday 6f034bb5dd Fixed issue when repeat object is malformed (#8765)
* Fixed issue when repeat object is malformed

* Removed only

* Changed numeric check to lodash isFinite

* Removed newer lodash function
2017-05-28 20:07:29 -05:00
Alys aeb8d4f500 fix typo in mageText: Habit > Habitica (thanks to Janmetdepet for finding it) (#8748) 2017-05-27 18:56:10 +10:00
Keith Holliday 58d910fe62 Added fix for double click on amazon pay (#8708) 2017-05-25 16:10:59 -05:00
SabreCat 71f2f31606 Merge branch 'release' into develop 2017-05-25 02:51:10 +00:00
SabreCat feae40cf0a 3.93.0 2017-05-25 01:00:21 +00:00
SabreCat 0d3fe53155 chore(news): Bailey 2017-05-25 00:59:37 +00:00
SabreCat c3a3c1514a Merge branch 'monthlies' into release 2017-05-25 00:50:19 +00:00
Keith Holliday cc532fa993 Enabled repeatables (#8572)
* Enabled repeatables

* Added every x to weekly

* Updated new recur logic to work with tests

* Added repeatable tests back

* Added custom day start support

* Moved back to zone function

* Added zone back

* Added nextDue field

* Abstracted set next due logic, set offset, and mapped to ISO

* Removed extra codes

* Removed clone deep

* Added summary local

* Fixed every x weekly

* Prevented edit of repeats on

* Added next due date

* Fixed display of next due dates

* Fixed broken tests

* added next due date as today for weekly

* Fixed integration tests

* Updated common test

* Use user's format

* Allow user to deselect all days during week

* Removed let from front end
2017-05-24 19:49:33 -05:00
Kevin Smith ba66a1c098 Removed cancel sub button and added info for apple/google subs (Fixes #8642) (#8666)
* Removed cancel sub button and added info for apple/google subs

* Refactored logic and constants from subscriptions view
2017-05-24 10:13:44 -06:00
Josh Holland b4d5c634b3 Close dropdowns when user clicks outside of them (fixes #5490) (#8657)
* Close dropdowns when user clicks outside of them

Fixes #5490

* Remove expandMenu and closeMenu directives and tests

* Remove unnecessary HTML attributes
2017-05-24 10:12:38 -06:00
Sabe Jones 216006beab Merge branch 'release' into develop 2017-05-23 22:16:32 +00:00
Sabe Jones 7c236e7e0e 3.92.0 2017-05-23 22:15:40 +00:00
Sabe Jones 0221d2d7f9 chore(i18n): update locales 2017-05-23 22:12:33 +00:00
SabreCat 567eb1d98b chore(sprites): compile 2017-05-23 22:04:26 +00:00
SabreCat 3cf533f261 feat(content): new Mystery Items
and reenable Floral Potions
2017-05-23 22:03:31 +00:00
taldin c30c51f386 Fixes apidoc error with Cast Skill (#8709)
* Fixes apidoc error with Cast Skill

Changes Body to Query, changed example from  POST body

* Updated to remove trailing space

* Wording fix per Lady Alys

* Update user.js

Kicking off another test.

* Update user.js
2017-05-23 14:06:58 -06:00
Céline O'Neil 2de794c32b Show task alias advanced option for Rewards (#8705) 2017-05-23 13:58:38 -06:00
Keith Holliday 7d000d2cf6 Removed let from front end 2017-05-23 08:56:36 -06:00
Keith Holliday 280b720c13 Allow user to deselect all days during week 2017-05-22 08:37:45 -06:00
Matteo Pagliazzi 9e1f7f3811 Client/Inventory/Items (#8734)
* client: start working on Inventory/Items

* i18n changes and fixes

* initial displaying of eggs, food and potions + sorting

* add missing files

* remove comment

* show food, eggs and potions

* add label to dropdowns acting as select menus

* popovers

* move badge to slot and component if necessary, general refactor

* fix quantity ordering

* some special items, reorganize
2017-05-22 16:30:52 +02:00
Keith Holliday fefaed368a Use user's format 2017-05-22 08:16:12 -06:00
Keith Holliday 6f370395ea Updated common test 2017-05-22 08:06:14 -06:00
Keith Holliday 65566f7607 Fixed integration tests 2017-05-22 07:54:07 -06:00
Keith Holliday c0117706e4 Merge remote-tracking branch 'upstream/develop' into enable-repeatables 2017-05-22 07:49:01 -06:00
Matteo Pagliazzi f267456a30 client: fix drawer 2017-05-21 15:38:33 +02:00
Matteo Pagliazzi 228b724d52 fix missing trailing comma 2017-05-21 14:43:25 +02:00
Keith Holliday 19b09b4894 Merged in develop 2017-05-20 19:24:25 -06:00
Keith Holliday f49d21d7b4 added next due date as today for weekly 2017-05-20 18:38:53 -06:00
Keith Holliday c08c0685f3 Fixed broken tests 2017-05-20 17:19:35 -06:00
negue 59bfe66c94 Client/item content (#8738)
* extract item popover-content as component

# Conflicts:
#	website/client/components/inventory/item.vue

* extract item-content as slot

* scoped context to pass the item

* itemContentClass instead of itemContent-slot
2017-05-20 19:59:45 +02:00
SabreCat 023fd6e6b0 3.91.2 2017-05-19 20:51:10 +00:00
SabreCat 7ee2f90f37 fix(docs): move apiParamExamples to newlines 2017-05-19 20:49:59 +00:00
SabreCat 46709ddadd 3.91.1 2017-05-19 20:22:06 +00:00
SabreCat a3ee09e764 chore(event): disable Fairy Potions 2017-05-19 20:17:51 +00:00
Sabe Jones 4127f36c02 chore(i18n): update locales 2017-05-19 20:16:18 +00:00
SabreCat dad924ac7d chore(sprites): compile 2017-05-19 20:05:55 +00:00
Sabe Jones 547c87dee7 Guild A/B test and Achievement (#8740)
* WIP(guilds): AB test pester modal

* WIP(AB-test): guild pester cont'd

* fix(style): linting error

* fix(AB-test): markModified and notif enum

* fix(tests): update AB expectations

* fix(modal): remove extra includes

* feat(achievements): add Joined Guild cheevo
Also removes unused achievement sprites, and properly saves counter used in A/B testing

* fix(style): linting error from conflict
2017-05-19 14:45:11 -05:00
Keith Holliday 99a2013767 Fixed display of next due dates 2017-05-18 14:12:36 -06:00
Keith Holliday a5e0e171cc Added next due date 2017-05-18 10:42:11 -06:00
Keith Holliday 1d93943458 Prevented edit of repeats on 2017-05-18 10:26:57 -06:00
Keith Holliday 7f2719a75c Fixed every x weekly 2017-05-18 10:11:29 -06:00
Sabe Jones 8a9ed04f5e Merge branch 'release' into develop 2017-05-18 02:13:14 +00:00
Sabe Jones 4e66c038a6 3.91.0 2017-05-18 02:11:56 +00:00
Sabe Jones bd9618e1c6 chore(i18n): update locales 2017-05-18 02:08:40 +00:00
SabreCat 342034a80d chore(sprites): compile 2017-05-18 01:59:44 +00:00
SabreCat f2aed3cb27 chore(news): Bailey
Also rolls back a leftover bit of the Spring Fling.
2017-05-18 01:58:37 +00:00
SabreCat 4f5f15d162 fix(test): account for cumulative balance 2017-05-18 01:44:07 +00:00
SabreCat b5c1b78789 docs(content): comment bundle structure 2017-05-18 01:44:07 +00:00
SabreCat f2ace5bb63 test(bundles): check balance deduction 2017-05-18 01:44:07 +00:00
SabreCat 72b8ba2d5c fix(test): remove only 2017-05-18 01:44:07 +00:00
SabreCat 8555e4bea8 test(bundles): shop and purchase tests 2017-05-18 01:44:07 +00:00
SabreCat 4de5b4253b fix(style): address linting errors 2017-05-18 01:44:07 +00:00
SabreCat 410004f8c6 feat(purchases): sell bundled quests 2017-05-18 01:44:07 +00:00
SabreCat 76c7e15497 refactor(content): split quests file 2017-05-18 01:44:06 +00:00
Sabe Jones e6f605f23a Discount Bundled Quests (#8731)
* refactor(content): split quests file

* feat(purchases): sell bundled quests

* fix(style): address linting errors

* test(bundles): shop and purchase tests

* fix(test): remove only

* test(bundles): check balance deduction

* docs(content): comment bundle structure

* fix(test): account for cumulative balance
2017-05-17 20:36:34 -05:00
Matteo Pagliazzi 0af1203832 Client Redesign: Inventory pages, secondary menu, misc css and design items (#8631)
* add colors palette

* add secondary menu component and style it

* add box shadow to secondary menu

* misc css, fixes for secondary menu

* client: add equipment page with grouping, css: add some styles

* add typography

* more equipment

* stable: fix linting

* equipment: add styles (lots of general styles too)

* remove duplicate google fonts loading

* add dropdowns

* design: white search input background, remove gray from items

* start adding drawer and selected indicator

* wip equipment

* fix equipment

* equipment: correctly bind new properties on items.gear.equipped

* equipment: fix vue binding. version 2

* equipment: fix vue binding. version 3

* back to first fix for equip op, fix for sourcemaps, send http request when an item is equipped, load bootstrap-vue components where needed

* checkboxes and radio buttons

* correctly renders selected items in first postion during the first render

* add search

* general changes, constants part of app state, add popovers

* add toggle switch, rename css

* correct offset

* upgrade deps

* upgrade deps

* drawer and lot of other work

* update equipping mechanism

* finish equipment

* fix compilation and upgrade deps

* use v-show in place of v-if to fix ui issues

* v-show -> v-if

* fix linting in test/client

* fix es6 compilation in test/client

* fix babel compilation for tests

* fix groupsUtilities mixin tests

* client: buttons

* client: buttons: fix colors

* client: finish buttons and dropdowns

* upgrade bootstrap-vue, finish buttons and dropdowns

* fix tasks page layout

* misc fixes for buttons

* add textareas

* fix app menu

* add inputs

* fixes for toggleSwitch

* typography

* checkboxes and radio buttons

* add checkbox icon

* fix equip.js

* extract strings to newClient.json

* add Popover above 'Use Costume' / 'Auto Equip' slider - disable item select if costume-mode and 'useCostume' isn't active

* show "you have disabled your costume" error above the drawer items

* check errorMessage for null

* hide star if costume not enabled

* fix errorMessage (!errorMessage seems not to work for string)

* show minimize / expand icon - always centered by css

* drawer test

* drawer: fix centering on large screens

* fix show more button

* add margin when two dropdowns are next to each other

* adjust the page padding based on the drawer, misc fixes

* drawer fixes
2017-05-16 21:09:55 +02:00
Alys 1de379a2c3 adjust banned words. TRIGGER / CONTENT WARNING: assault, slurs, swearwords, etc 2017-05-16 13:08:09 +10:00
Keith Holliday 388861b503 Added summary local 2017-05-15 09:15:14 -06:00
Alys f52806ed69 minor text changes: Shield-Hand Item; space in quest completion modal; Rebirth clarification (#8681)
* replace Shield with Shield-Hand Item where appropriate for consistency and clarity

* add space in quest completion modal heading between name of quest and "Completed!"

* clarify Rebirth text - restart your current character, not create a new one

* correct the instructions for editing a group (edit button not pencil icon)

* correct typo in questVice3Completion: breath > breathe

* fix type: Starststemic > Starsystemic
2017-05-14 20:53:15 +10:00
Alys d89b9e08af update / delete outdated README.md files (#8719)
Information from test/api/README.md and test/api/v3/README.md has been moved to http://habitica.wikia.com/wiki/Using_Your_Local_Install_to_Modify_Habitica's_Website_and_API for consistency with other Blacksmith documentation.
2017-05-14 14:40:13 +10:00
Keith Holliday f8a99bd127 Removed clone deep 2017-05-12 11:15:09 -06:00
Keith Holliday a82b60f144 Removed extra codes 2017-05-12 09:21:29 -06:00
Alys 65e71140ee adjust banned words list -- TRIGGER / CONTENT WARNING: assault, slurs, swearwords, etc 2017-05-13 00:48:35 +10:00
Keith Holliday f192ca4c6f Abstracted set next due logic, set offset, and mapped to ISO 2017-05-12 07:39:32 -06:00
SabreCat 36b09d40b9 Merge branch 'release' into develop 2017-05-11 22:30:23 +00:00
Sabe Jones 4fdd754b31 3.90.2 2017-05-11 20:16:28 +00:00
Sabe Jones 36ff063991 chore(i18n): update locales 2017-05-11 20:14:21 +00:00
SabreCat 20dcb1cb8a chore(sprites): compile 2017-05-11 20:08:33 +00:00
SabreCat 3a1527073f chore(news): Bailey 2017-05-11 20:08:12 +00:00
Keith Holliday 1292f9a3d5 Added nextDue field 2017-05-11 13:11:16 -06:00
Keith Holliday e7418472f6 Added zone back 2017-05-10 22:06:31 -06:00
Keith Holliday 850f332ddc MErged in develop 2017-05-10 15:51:50 -06:00
Keith Holliday 59fb32ea2e Moved back to zone function 2017-05-10 15:47:32 -06:00
Keith Holliday 727cdc9402 Tasks is due (#8711)
* Added isDue field and isDue set on create

* Added isDue update on update task

* Add isdue calc to score task

* Added isdue calc to cron

* Fixed lint issue

* Added isDue to no set and updated grammar
2017-05-10 07:40:45 -06:00
SabreCat 638c9dee89 Merge branch 'release' into develop 2017-05-09 19:36:42 +00:00
SabreCat 168ed02226 3.90.1 2017-05-09 19:30:31 +00:00
Sabe Jones aa253cf72e chore(i18n): update locales 2017-05-09 19:25:30 +00:00
SabreCat 247742c60d chore(news): Bailey
also adds Managers info to Group Plans page
2017-05-09 19:18:29 +00:00
yugensoft 7d89deb094 Continuation of PR #8675, fix internationalization (#8698)
* Made uneditable habits show counter reset frequency
Added counter reset frequency to counter tooltip
Solves issue https://github.com/HabitRPG/habitica/issues/8571

* Internationalization fix, pursuant to https://github.com/HabitRPG/habitica/pull/8675#discussion_r112982947

(cherry picked from commit 2adac35e31)
2017-05-09 18:54:38 +00:00
Sabe Jones c128b701fa chore(i18n): update locales 2017-05-09 18:52:38 +00:00
yugensoft 2adac35e31 Continuation of PR #8675, fix internationalization (#8698)
* Made uneditable habits show counter reset frequency
Added counter reset frequency to counter tooltip
Solves issue https://github.com/HabitRPG/habitica/issues/8571

* Internationalization fix, pursuant to https://github.com/HabitRPG/habitica/pull/8675#discussion_r112982947
2017-05-09 13:22:09 -05:00
SabreCat f01e13ffc0 3.90.0 2017-05-09 17:53:03 +00:00
Alys 4e9d631b71 adjust banned words list -- TRIGGER / CONTENT WARNING: words about assault, slurs, etc 2017-05-09 09:21:06 +10:00
SabreCat f1de7c02e9 Merge branch 'develop' into release 2017-05-08 23:14:45 +00:00
SabreCat 15d4f7d6ab Revert "Add tests for shouldDo - fixes #8585 (#8660)"
This reverts commit 4bbebdd237. The change to moment.utcOffset() from moment.zone() broke cron code for determining today's date.
2017-05-08 22:08:24 +00:00
Keith Holliday 76222ac344 Added custom day start support 2017-05-08 10:20:47 -06:00
Keith Holliday 2659a4117b Added repeatable tests back 2017-05-08 09:52:15 -06:00
Keith Holliday a0ee73e944 Updated new recur logic to work with tests 2017-05-08 09:39:50 -06:00
Keith Holliday 6174624b89 Merged develop 2017-05-08 08:32:03 -06:00
Keith Holliday c8b6e8ea7c Updated docker file to use Node (#8704) 2017-05-08 07:40:55 -06:00
madpink 409b5d5965 Updating User API Doc (part 2) (#8602)
* Updating APIDOC for issue 8087

* Updating User API Doc (round 2)

cleaned up trailing sapces

* Updating User API Doc (round 2)

Changed mpHeal to mpheal
2017-05-08 07:37:01 -06:00
MathWhiz e7209511ca Challenge API Doc Updates (#8626)
* Improve API Documentation for Challenges API

* Fix previously raised issues

* Change suggestions by @Alys
2017-05-08 07:36:02 -06:00
Mateus Etto 8c76ccd39b Fix bugs with approved tasks in a Group Plan (#8629)
* Store all approved tasks in an array

* Created bulkScore using score callback

* Removed unnecessary code

* Added verification to run the code only for Approved Tasks

* Created scoreTasks on server and necessary code on client

* Revert "Created scoreTasks on server and necessary code on client"

This reverts commit b786c0e71a.

* Fixed gold/xp earn-lose-earnAgain problem

* Do not read already read notifications

* Removed unnecessary variable
2017-05-08 07:35:08 -06:00
Jaka Kranjc 399c91ccab travis: don't pollute the log with npm output (#8706)
unless it fails; saves ~7k lines
2017-05-08 07:33:49 -06:00
Grace Chen 4bbebdd237 Add tests for shouldDo - fixes #8585 (#8660)
* Fix #8585 - Add shouldDo CDS and timezone variation test suite

* Fix #8585 - resolve feedback
2017-05-08 07:32:38 -06:00
yugensoft 4f305bd505 Updated vagrant script to fix bcrypt missing error (#8670)
* Updated npm version to match http://habitica.wikia.com/wiki/Setting_up_Habitica_Locally; added npm-pre-gyp dependency necessary for bcrypt to install

* changed npm version as per request https://github.com/HabitRPG/habitica/pull/8670#discussion_r112010106
2017-05-07 07:57:50 +10:00
Alys 024feaa2f4 add banned words to be blocked in Tavern Chat -- CONTENT WARNING: this commit contains slurs, swear words, etc 2017-05-06 07:37:17 +10:00
Sabe Jones d2dc8f1856 Release Mergeback 2017/05/03 (#8716)
* Content 2017/05 (#8714)

* feat(content): May 2017
Add Armoire, add BGs, end Spring Fling

* chore(sprites): compile

* chore(i18n): update locales

* 3.89.0
2017-05-03 15:02:54 -05:00
Keith Holliday e3a08c1905 Added loggly integration (#8690)
* Added loggly integration

* Added back console logger

* Updated shrinkwrap
2017-05-02 09:17:23 -06:00
Sabe Jones dad0eea9e0 Merge branch 'release' into develop 2017-04-28 19:31:22 +00:00
Alys 864ca91144 modify Market intro for eggs to clarify that quest eggs don't drop (#8680)
Ref: https://habitica.slack.com/archives/C02RK7DKF/p1491847171061587?thread_ts=1491779437.902223&cid=C02RK7DKF
2017-04-27 21:10:43 +10:00
Cai Lu 78816dd4cb Fix character values (#8541)
* Display an error when invalid values are given

* Add character values tests for settingsCtrl

* Only check for invalid level values

* Remove unnecessary validation function

* Min level of 1 on user model

* (tests) Removed stubbing on restore
2017-04-26 15:10:56 -06:00
Keith Holliday 6a99daebac continuation of PR #8074 Adding spam prevention - fixes #8060 (#8687)
* Adding code to look over the most recent messages to look for spam from a user

* Adding in translatable error message

* Adding 2 tests for spam detection

* Fixing changes requested for pull request

* Adding unit tests for group and fixing requested changes

* Fixing message and tests

* Forgot to remove this import

* Fixing lint errors

* Cleaning up the code and tests to be more readable

* Fixing lint errors

* Fixed linting issues

* Syntax fixes

* Updated grammar
2017-04-26 13:37:18 -06:00
SabreCat c9ee6c7f73 Merge branch 'release' into develop 2017-04-26 17:40:51 +00:00
Keith Holliday e2f4b0e3dc Group managers (#8591)
* Added abiltiy to add group managers

* Added ability to remove managers

* Added ability for managers to add group tasks

* Allower managers to assign tasks

* Allowed managers to unassign tasks

* Allow managers to delete group tasks

* Allowed managers to approve

* Added initial ui

* Added approval view for managers

* Allowed managers to edit

* Fixed lint issues

* Added spacing to buttons

* Removed leader from selection of group managers

* Code review updates

* Ensured approvals are only done once

* Added ability for parties to add managers

* Add notifications to all managers when approval is requests

* Removed tasks need approval notifications from all managers when task is approve

* Fixed linting issues

* Hid add managers UI from groups that are not subscribed

* Removed let from front end

* Fixed issues with post task url params

* Fixed string locales

* Removed extra limited strings

* Added cannotedit tasks function

* Added limit fields and notification check by taskId

* Localized string and other minor issues

* Added manager and leader indicator

* Added group notifications refresh on sync

* Added close button for group notifications

* Removed group approval notifications when manager is removed

* Moved leader/manager indicators to after hp

* Added manager fields to groups

* Spelling and syntax fixes
2017-04-25 08:28:56 -06:00
Keith Holliday 369702884a Prevented ios and android subs from being upgrade to group plans (#8646)
* Prevented ios and android subs from being upgrade to group plans

* Add admin email sender

* Updated admin email

* Updated get user email

* Fixed grammar issue
2017-04-24 08:01:27 -06:00
Keith Holliday d438990d18 Added block when user types a swear word listed in banned words (#8197)
* Added block when user types a swear word listed in banned words

* Moved banned words check to server

* Removed unused code

* Moved banned words to separate file and fixed grammar.

* Updated chat test

* Changed error to BadRequest

* Fixed regex matching

* Updated test banned word

* Moved banned words and cached regex

* Updated banned word message

* Add ban filter only for tavern

* Added tavern id constant

* Added more tests for banned words

* Added warning to banned words

* Added alert

* Added new regex to capture markdown

* Fixed lint, spelling and importing
2017-04-24 07:55:42 -06:00
yugensoft 7d3213fd66 Made uneditable habits show counter reset frequency (#8675)
Added counter reset frequency to counter tooltip
Solves issue https://github.com/HabitRPG/habitica/issues/8571
2017-04-22 14:01:06 -05:00
Alys bde4eafc05 add mountText for all drop eggs and quest eggs (#8678)
I've checked that all these added strings do exist in locales/en/content.json
2017-04-22 14:00:18 -05:00
Travis 5a2ba27808 Creating api for gifting Hall Of Heroes Item Reference (#8118)
* Creating specific api to vend item reference for gifting items to a user through the hall of heroes. closes #8100

* removed 'When in doubt, ask Tyler' from the hall of heroes line

* Moving hall of heroes item path call from server side to client side

* updating let to var

* Addressing PR comments
2017-04-22 13:57:41 -05:00
Keith Holliday 024268a51e Added wintery skins to content (#8645)
* Added wintery skins to content

* Removed features json

* Fixed some name issues
2017-04-22 13:54:57 -05:00
MathWhiz f7281e71e8 Change icon for NPC achievement (#8628)
* Change icon for NPC achievement

* Add npc icon
2017-04-22 13:52:45 -05:00
SabreCat 91438aff90 Merge branch 'release' into develop 2017-04-20 22:21:33 +00:00
Sabe Jones 91ed55cf66 Release mergeback v3.86.0 (#8685)
* 3.85.0

* New User Tasks for Mobile (#8682)

* feat(mobile): different default tasks

* fix(linting): missing space

* fix(user): correct client type logic

* test(integration): tasks by platform

* fix(test): remove only

* test(user): deeper checks on tasks

* refactor(test): whitespace for readability

* feat(subs): Jackalope Pets (#8684)

* chore(sprites): compile

* chore(i18n): update locales

* 3.86.0
2017-04-18 19:23:24 -05:00
Alys 4275da0a2e fix Shiny Seeds info about achievement (not given to caster) (#8679)
Ref: https://habitica.slack.com/archives/C02RK7DKF/p1492032261365388
2017-04-19 08:56:15 +10:00
Mateus Etto d9f48dcbb0 Fixed text of check-in prize when it is a set of backgrounds (#8599)
* Fixed text of check-in prize when it is a set of backgrounds

* Use existing i18n string for BGs name

* Added user.preferences.language as second parameter
2017-04-17 14:39:05 +10:00
Tyler Nychka bd6f901ccf Fix for automatic allocation not persisting #8641 (#8661) 2017-04-17 06:49:16 +10:00
SabreCat 884bf02961 Merge branch 'sabrecat/butterflies' into develop 2017-04-13 20:31:19 +00:00
Keith Holliday 7d42e8fc71 Added email invite limit (#8664)
* Added email invite limit

* change error message for sending too many invitations to instruct them to email us

* fix test error message to use variable in locales string

* add comment to warn about keeping INVITES_LIMIT low

If INVITES_LIMIT is allowed to be greater than MAX_EMAIL_INVITES_BY_USER
then the inviter can send more than MAX_EMAIL_INVITES_BY_USER invitations
at once.
2017-04-12 15:54:35 -05:00
SabreCat 0442b87608 fix(news): merge conflict 2017-04-11 22:23:27 +00:00
SabreCat c1d1a3e14e fix(translation): resolve merge conflict 2017-04-11 22:04:55 +00:00
SabreCat dd5a9aa6cc Merge branch 'sabrecat/egg-quest' into develop 2017-04-11 20:10:49 +00:00
Alys 0bb6e5f3fc edit the new issue template to emphasise that the Report a Bug guild should be used first (#8659) 2017-04-11 14:52:19 -05:00
Matteo Pagliazzi 69347df050 Merge branch 'Yutsuten-orb-rebirth' into develop 2017-04-11 17:10:50 +02:00
Matteo Pagliazzi 9cad5525e6 Merge branch 'orb-rebirth' of https://github.com/Yutsuten/habitica into Yutsuten-orb-rebirth 2017-04-11 17:10:42 +02:00
Matteo Pagliazzi b1e6aceffe Merge branch 'Yutsuten-checkin-complete-msg' into develop 2017-04-11 17:08:51 +02:00
Matteo Pagliazzi 17068875f4 Merge branch 'checkin-complete-msg' of https://github.com/Yutsuten/habitica into Yutsuten-checkin-complete-msg 2017-04-11 17:08:43 +02:00
Matteo Pagliazzi 342fc2e344 Merge branch 'paglias/fix-wrong-subs' into develop 2017-04-11 17:07:15 +02:00
Matteo Pagliazzi b8b1557e49 Merge branch 'develop' into paglias/fix-wrong-subs 2017-04-11 17:07:05 +02:00
Matteo Pagliazzi f90ef04e83 Merge branch 'eastwood-develop' into develop 2017-04-11 17:03:20 +02:00
Matteo Pagliazzi d0561512de Merge branch 'develop' of https://github.com/eastwood/habitica into eastwood-develop 2017-04-11 17:03:09 +02:00
Matteo Pagliazzi b84c672f33 remove wrong subscriptions from gift modal 2017-04-11 10:15:54 +02:00
Mateus Etto aafbb889be Merge branch 'develop' into checkin-complete-msg 2017-04-09 19:58:53 -03:00
SabreCat 13865bcf49 Merge branch 'viirus/androidFixes' into develop 2017-04-06 22:10:02 +00:00
SabreCat f9ca69196a fix(test): unpend party cap test 2017-04-06 19:58:33 +00:00
SabreCat 0e77df6e7b Merge branch 'sabrecat/shiny-seeds' into develop 2017-04-06 19:56:20 +00:00
Mateus Etto f2d9bdf7ae Updated comment 2017-03-28 06:57:20 -03:00
Mateus Etto 2d7e280598 Special message when complete all check-in prizes 2017-03-27 22:24:47 -03:00
Mateus Etto d1de41290d Remove Beat Master unlock for Rebirth 2017-03-27 19:59:24 -03:00
TheHollidayInn 0fd85c0d60 Added every x to weekly 2017-03-27 09:52:32 -06:00
Clint Ryan 6956f5345e Removes test focus 2017-03-24 23:45:56 +11:00
Clint Ryan cc68abb67e Mock ENV function 2017-03-24 00:36:05 +11:00
Clint Ryan b48be4e619 Remoed logging 2017-03-23 00:43:59 +11:00
Clint Ryan 7a543d07a4 Fixes tests 2017-03-23 00:41:28 +11:00
Clint Ryan 9115be68b2 Fixes achievements injection. Now trying with Guide 2017-03-19 10:11:11 +11:00
Keith Holliday 8c68f450c6 Enabled repeatables 2017-03-15 14:13:15 -06:00
Clint Ryan 83bd4dcf60 Add failing test, seeking help/advice 2017-03-14 12:35:48 +11:00
Clint Ryan 92b02295b5 Issue 8432: Pre-test for initial fix 2017-03-11 23:29:51 +11:00
1349 changed files with 83807 additions and 61770 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# Reporting Bugs
[Please see these instructions for reporting bugs](https://github.com/HabitRPG/habitrpg/issues/2760)
[Please see these instructions for reporting bugs](https://github.com/HabitRPG/habitica/issues/2760)
# Pull Request
+1 -1
View File
@@ -4,7 +4,7 @@
[//]: # (If you have a feature request, use "Help > Request a Feature", not GitHub or the Report a Bug guild.)
[//]: # (For more guidelines see https://github.com/HabitRPG/habitrpg/issues/2760)
[//]: # (For more guidelines see https://github.com/HabitRPG/habitica/issues/2760)
[//]: # (Fill out relevant information - UUID is found in Settings -> API)
### General Info
+5
View File
@@ -2,6 +2,8 @@ language: node_js
node_js:
- '6'
sudo: required
services:
- mongodb
addons:
apt:
sources:
@@ -12,6 +14,8 @@ before_install:
- $CXX --version
- npm install -g npm@4
- if [ $REQUIRES_SERVER ]; then sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10; echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list; sudo apt-get update; sudo apt-get install mongodb-org-server; fi
install:
- npm install &> npm.install.log || (cat npm.install.log; false)
before_script:
- npm run test:build
- cp config.json.example config.json
@@ -31,3 +35,4 @@ env:
- TEST="test:common" COVERAGE=true
- TEST="test:karma" COVERAGE=true
- TEST="client:unit" COVERAGE=true
- TEST="apidoc"
+18 -45
View File
@@ -1,45 +1,18 @@
FROM ubuntu:trusty
MAINTAINER Sabe Jones <sabe@habitica.com>
# Avoid ERROR: invoke-rc.d: policy-rc.d denied execution of start.
RUN echo -e '#!/bin/sh\nexit 0' > /usr/sbin/policy-rc.d
# Install prerequisites
RUN apt-get update
RUN apt-get install -y \
build-essential \
curl \
git \
libfontconfig1 \
libfreetype6 \
libkrb5-dev \
python
# Install NodeJS
RUN curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
RUN apt-get install -y nodejs
# Install npm@latest
RUN curl -sL https://www.npmjs.org/install.sh | sh
# Clean up package management
RUN apt-get clean
RUN rm -rf /var/lib/apt/lists/*
# Install global packages
RUN npm install -g gulp grunt-cli bower mocha
# Clone Habitica repo and install dependencies
WORKDIR /habitrpg
RUN git clone https://github.com/HabitRPG/habitica.git /habitrpg
RUN npm install
RUN bower install --allow-root
# Create environment config file and build directory
RUN cp config.json.example config.json
RUN mkdir -p ./website/build
# Start Habitica
EXPOSE 3000
CMD ["npm", "start"]
FROM node:boron
# Install global packages
RUN npm install -g gulp grunt-cli bower mocha
# Clone Habitica repo and install dependencies
RUN mkdir -p /usr/src/habitrpg
WORKDIR /usr/src/habitrpg
RUN git clone https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
RUN npm install
RUN bower install --allow-root
# Create Build dir
RUN mkdir -p ./website/build
# Start Habitica
EXPOSE 3000
CMD ["npm", "start"]
+19
View File
@@ -0,0 +1,19 @@
FROM node:boron
# Install global packages
RUN npm install -g gulp grunt-cli bower mocha
# Clone Habitica repo and install dependencies
RUN mkdir -p /usr/src/habitrpg
WORKDIR /usr/src/habitrpg
RUN git clone --branch v3.102.2 https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
RUN npm install
RUN bower install --allow-root
RUN gulp build:prod --force
# Create Build dir
RUN mkdir -p ./website/build
# Start Habitica
EXPOSE 3000
CMD ["node", "./website/transpiled-babel/index.js"]
+2 -3
View File
@@ -5,8 +5,7 @@ Habitica [![Build Status](https://travis-ci.org/HabitRPG/habitica.svg?branch=dev
We need more programmers! Your assistance will be greatly appreciated.
For an introduction to the technologies used and how the software is organized, refer to [Contributing to Habitica](http://habitica.wikia.com/wiki/Contributing_to_Habitica#Coders_.28Web_.26_Mobile.29) - "Coders (Web & Mobile)" section.
For an introduction to the technologies used and how the software is organized, refer to [Guidance for Blacksmiths](http://habitica.wikia.com/wiki/Guidance_for_Blacksmiths).
To set up a local install of Habitica for development and testing, see [Setting up Habitica Locally](http://habitica.wikia.com/wiki/Setting_up_Habitica_Locally), which contains instructions for Windows, *nix / Mac OS, and Vagrant.
To set up a local install of Habitica for development and testing on various platforms, see [Setting up Habitica Locally](http://habitica.wikia.com/wiki/Setting_up_Habitica_Locally).
Then read [Guidance for Blacksmiths](http://habitica.wikia.com/wiki/Guidance_for_Blacksmiths) for additional instructions and useful tips.
+6 -1
View File
@@ -66,7 +66,8 @@
},
"mode":"sandbox",
"client_id":"client_id",
"client_secret":"client_secret"
"client_secret":"client_secret",
"experience_profile_id": ""
},
"IAP_GOOGLE_KEYDIR": "/path/to/google/public/key/dir/",
"LOGGLY_TOKEN": "token",
@@ -97,5 +98,9 @@
"COMMUNITY_MANAGER_EMAIL" : "leslie@habitica.com",
"TECH_ASSISTANCE_EMAIL" : "admin@habitica.com",
"PRESS_ENQUIRY_EMAIL" : "leslie@habitica.com"
},
"LOGGLY" : {
"TOKEN" : "example-token",
"SUBDOMAIN" : "exmaple-subdomain"
}
}
@@ -3,7 +3,7 @@ var authorUuid = 'd904bd62-da08-416b-a816-ba797c9ee265'; //... own data is done
/**
* database_reports/count_users_who_own_specified_gear.js
* https://github.com/HabitRPG/habitrpg/pull/3884
* https://github.com/HabitRPG/habitica/pull/3884
*/
var thingsOfInterest = {
+36
View File
@@ -0,0 +1,36 @@
import gulp from 'gulp';
import fs from 'fs';
// Copy Bootstrap 4 config variables from /website /node_modules so we can check
// them into Git
const BOOSTRAP_NEW_CONFIG_PATH = 'website/client/assets/scss/bootstrap_config.scss';
const BOOTSTRAP_ORIGINAL_CONFIG_PATH = 'node_modules/bootstrap/scss/_custom.scss';
// https://stackoverflow.com/a/14387791/969528
function copyFile(source, target, cb) {
let cbCalled = false;
function done(err) {
if (!cbCalled) {
cb(err);
cbCalled = true;
}
}
let rd = fs.createReadStream(source);
rd.on('error', done);
let wr = fs.createWriteStream(target);
wr.on('error', done);
wr.on('close', () => done());
rd.pipe(wr);
}
gulp.task('bootstrap', (done) => {
// use new config
copyFile(
BOOSTRAP_NEW_CONFIG_PATH,
BOOTSTRAP_ORIGINAL_CONFIG_PATH,
done,
);
});
+1 -1
View File
@@ -49,7 +49,7 @@ gulp.task('sprites:checkCompiledDimensions', ['sprites:main', 'sprites:largeSpri
});
if (numberOfSheetsThatAreTooBig > 0) {
console.error(`${numberOfSheetsThatAreTooBig} sheets might too big for mobile Safari to be able to handle them, but there is a margin of error in these calculations so it is probably okay. Mention this to an admin so they can test a staging site on mobile Safari after your PR is merged.`); // https://github.com/HabitRPG/habitrpg/pull/6683#issuecomment-185462180
console.error(`${numberOfSheetsThatAreTooBig} sheets might too big for mobile Safari to be able to handle them, but there is a margin of error in these calculations so it is probably okay. Mention this to an admin so they can test a staging site on mobile Safari after your PR is merged.`); // https://github.com/HabitRPG/habitica/pull/6683#issuecomment-185462180
} else {
console.log('All images are within the correct dimensions');
}
+1
View File
@@ -13,6 +13,7 @@ if (process.env.NODE_ENV === 'production') {
require('./gulp/gulp-newstuff');
require('./gulp/gulp-build');
require('./gulp/gulp-babelify');
require('./gulp/gulp-bootstrap');
} else {
require('glob').sync('./gulp/gulp-*').forEach(require);
require('gulp').task('default', ['test']);
+60
View File
@@ -0,0 +1,60 @@
# Habitica in Kubernetes
This is a set of sample Kubernetes configuration files to launch Habitica under AWS, both as a single-node web frontend as well as a multi-node web frontend.
## Prerequisites
* An AWS account.
* A working Kubernetes installation.
* A basic understanding of how to use Kubernetes. https://kubernetes.io/
* A persistent volume for MongoDB data.
* Docker images of Habitica.
+ You can use your own, or use the one included in the YAML files.
+ If you use your own, you'll need a fork of the Habitica GitHub repo and your own Docker Hub repo, both of which are free.
## Before you begin
1. Set up Kubernetes.
2. Create an EBS volume for MongoDB data.
+ Make a note of the name, you'll need it later.
## Starting MongoDB
1. Edit mongo.yaml
+ Find the volumeID line.
+ Change the volume to the one created in the section above.
2. Run the following commands:
+ `kubectl.sh create -f mongo.yaml`
+ `kubectl.sh create -f mongo-service.yaml`
3. Wait for the MongoDB pod to start up.
## Starting a Single Web Frontend
1. Run the following commands:
+ `kubectl.sh create -f habitica.yaml`
+ `kubectl.sh create -f habitica-service.yaml`
2. Wait for the frontend to start up.
## Starting Multi-node Web Frontend
1. Run the following commands :
+ `kubectl.sh create -f habitica-rc.yaml`
+ `kubectl.sh create -f habitica-service.yaml`
2. Wait for the frontend to start up.
## Accessing Your Habitica web interface
Using `kubectl describe svc habiticaweb` get the hostname generated for the Habitica service. Open a browser and go to http://hostname:3000 to access the web front-end for the installations above.
## Shutting down
Shutting down is basically done by reversing the steps above:
+ `kubectl.sh delete -f habitica-service.yaml`
+ `kubectl.sh delete -f habitica.yaml (or habitica-rc.yaml)`
+ `kubectl.sh delete -f mongo-service.yaml`
+ `kubectl.sh delete -f mongo.yaml`
You can also just shut down all of Kubernetes as well.
## Notes
+ MongoDB data will be persistent! If you need to start with a fresh database, you'll need to remove the volume and re-create it.
+ On AWS, you probably want to use at least t2.medium minion nodes for Kubernetes. The default t2.small is too small for more than two Habitica nodes.
## Future Plans
+ Multi-node MongoDB.
+ Monitoring
+ Instructions for a better hostname. The default generated ones stink.
+ More to come....
+25
View File
@@ -0,0 +1,25 @@
apiVersion: v1
kind: ReplicationController
metadata:
name: habitica
labels:
name: habitica
spec:
replicas: 4
selector:
name: habitica
template:
metadata:
labels:
name: habitica
spec:
containers:
- name: habitica
image: ksonney/habitrpg:latest
env:
- name: NODE_DB_URI
value: mongodb://mongosvc/habitrpg
ports:
- containerPort: 3000
hostPort: 3000
name: habitica
+14
View File
@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
labels:
name: habiticaweb
name: habiticaweb
spec:
ports:
# the port that this service should serve on
- port: 3000
# label keys and values that must match in order to receive traffic for this service
selector:
name: habitica
type: LoadBalancer
+22
View File
@@ -0,0 +1,22 @@
apiVersion: v1
kind: Pod
metadata:
name: habitica
labels:
name: habitica
spec:
containers:
# - image: mongo:latest
# name: mongo
# ports:
# - containerPort: 27017
# name: mongo
- image: ksonney/habitrpg:latest
name: habitica
env:
- name: NODE_DB_URI
value: mongodb://mongosvc/habitrpg
ports:
- containerPort: 3000
hostPort: 3000
name: habitica
+13
View File
@@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
labels:
name: mongosvc
name: mongosvc
spec:
ports:
# the port that this service should serve on
- port: 27017
# label keys and values that must match in order to receive traffic for this service
selector:
name: mongodb
+28
View File
@@ -0,0 +1,28 @@
apiVersion: v1
kind: Pod
metadata:
name: mongodb
labels:
name: mongodb
spec:
containers:
- resources:
limits :
cpu: 0.5
image: mongo
name: mongodb
ports:
- containerPort: 27017
hostPort: 27017
name: mongo
volumeMounts:
# # name must match the volume name below
- name: mongo-persistent-storage
# # mount path within the container
mountPath: /data/db
volumes:
- name: mongo-persistent-storage
awsElasticBlockStore:
volumeID: aws://YOUR-REGION/YOUR-VOLNAME
fsType: ext3
@@ -0,0 +1,110 @@
'use strict';
/****************************************
* Author: @Alys
*
* Reason: Collection quests are being changed
* to require fewer items collected:
* https://github.com/HabitRPG/habitrpg/pull/7987
* This will cause existing quests to end sooner
* than the party is expecting.
* This script inserts an explanatory `system`
* message into the chat for affected parties.
***************************************/
global.Promise = require('bluebird');
const uuid = require('uuid');
const TaskQueue = require('cwait').TaskQueue;
const logger = require('./utils/logger');
const Timer = require('./utils/timer');
const connectToDb = require('./utils/connect').connectToDb;
const closeDb = require('./utils/connect').closeDb;
const message = '`This party\'s collection quest has been made easier! For details, refer to http://habitica.wikia.com/wiki/User_blog:LadyAlys/Collection_Quests_are_Now_Easier`';
const timer = new Timer();
// PROD: Enable prod db
// const DB_URI = 'mongodb://username:password@dsXXXXXX-a0.mlab.com:XXXXX,dsXXXXXX-a1.mlab.com:XXXXX/habitica?replicaSet=rs-dsXXXXXX';
const DB_URI = 'mongodb://localhost/habitrpg';
const COLLECTION_QUESTS = [
'vice2',
'egg',
'moonstone1',
'goldenknight1',
'dilatoryDistress1',
];
let Groups;
connectToDb(DB_URI).then((db) => {
Groups = db.collection('groups');
return Promise.resolve();
})
.then(findPartiesWithCollectionQuest)
// .then(displayGroups) // for testing only
.then(addMessageToGroups)
.then(() => {
timer.stop();
closeDb();
}).catch(reportError);
function reportError (err) {
logger.error('Uh oh, an error occurred');
closeDb();
timer.stop();
throw err;
}
function findPartiesWithCollectionQuest () {
logger.info('Looking up groups on collection quests...');
return Groups.find({'quest.key': {$in: COLLECTION_QUESTS}}, ['name','quest']).toArray().then((groups) => {
logger.success('Found', groups.length, 'parties on collection quests');
return Promise.resolve(groups);
})
}
function displayGroups (groups) { // for testing only
logger.info('Displaying parties...');
console.log(groups);
return Promise.resolve(groups);
}
function updateGroupById (group) {
var newMessage = {
'id' : uuid.v4(),
'text' : message,
'timestamp': Date.now(),
'likes': {},
'flags': {},
'flagCount': 0,
'uuid': 'system'
};
return Groups.findOneAndUpdate({_id: group._id}, {$push:{"chat" :{$each: [newMessage], $position:0}}}, {returnOriginal: false});
// Does not set the newMessage flag for all party members because I don't think it's essential and
// I don't want to run the extra code (extra database load, extra opportunity for bugs).
}
function addMessageToGroups (groups) {
let queue = new TaskQueue(Promise, 300);
logger.info('About to update', groups.length, 'parties...');
return Promise.map(groups, queue.wrap(updateGroupById)).then((result) => {
let updates = result.filter(res => res.lastErrorObject && res.lastErrorObject.updatedExisting)
let failures = result.filter(res => !(res.lastErrorObject && res.lastErrorObject.updatedExisting));
logger.success(updates.length, 'parties have been notified');
if (failures.length > 0) {
logger.error(failures.length, 'parties could not be notified');
}
return Promise.resolve();
});
}
+114
View File
@@ -0,0 +1,114 @@
var migrationName = '20170616_achievements';
var authorName = 'Sabe'; // in case script author needs to know when their ...
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
/*
* Updates to achievements for June 16, 2017 biweekly merge
* 1. Multiply various collection quest achievements based on difficulty reduction
* 2. Award Joined Challenge achievement to those who should have it already
*/
import monk from 'monk';
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
var dbUsers = monk(connectionString).get('users', { castIds: false });
function processUsers(lastId) {
// specify a query to limit the affected users (empty for all users):
var query = {
$or: [
{'achievements.quests.dilatoryDistress1': {$gt:0}},
{'achievements.quests.egg': {$gt:0}},
{'achievements.quests.goldenknight1': {$gt:0}},
{'achievements.quests.moonstone1': {$gt:0}},
{'achievements.quests.vice2': {$gt:0}},
{'achievements.challenges': {$exists: true, $ne: []}},
{'challenges': {$exists: true, $ne: []}},
],
};
if (lastId) {
query._id = {
$gt: lastId
}
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 250,
fields: [ // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
'achievements',
'challenges',
],
})
.then(updateUsers)
.catch(function (err) {
console.log(err);
return exiting(1, 'ERROR! ' + err);
});
}
var progressCount = 1000;
var count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
displayData();
return;
}
var userPromises = users.map(updateUser);
var lastUser = users[users.length - 1];
return Promise.all(userPromises)
.then(function () {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
var set = {'migration': migrationName};
if (user.challenges.length > 0 || user.achievements.challenges.length > 0) {
set['achievements.joinedChallenge'] = true;
}
if (user.achievements.quests.dilatoryDistress1) {
set['achievements.quests.dilatoryDistress1'] = Math.ceil(user.achievements.quests.dilatoryDistress1 * 1.25);
}
if (user.achievements.quests.egg) {
set['achievements.quests.egg'] = Math.ceil(user.achievements.quests.egg * 2.5);
}
if (user.achievements.quests.goldenknight1) {
set['achievements.quests.goldenknight1'] = user.achievements.quests.goldenknight1 * 5;
}
if (user.achievements.quests.moonstone1) {
set['achievements.quests.moonstone1'] = user.achievements.quests.moonstone1 * 5;
}
if (user.achievements.quests.vice2) {
set['achievements.quests.vice2'] = Math.ceil(user.achievements.quests.vice2 * 1.5);
}
dbUsers.update({_id: user._id}, {$set:set});
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
if (user._id == authorUuid) console.warn(authorName + ' processed');
}
function displayData() {
console.warn('\n' + count + ' users processed\n');
return exiting(0);
}
function exiting(code, msg) {
code = code || 0; // 0 = success
if (code && !msg) { msg = 'ERROR!'; }
if (msg) {
if (code) { console.error(msg); }
else { console.log( msg); }
}
process.exit(code);
}
module.exports = processUsers;
+90
View File
@@ -0,0 +1,90 @@
var migrationName = '20170711_orcas.js';
var authorName = 'Sabe'; // in case script author needs to know when their ...
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
/*
* Award Orca pets to owners of Orca mount, and Orca mount to everyone else
*/
var monk = require('monk');
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
var dbUsers = monk(connectionString).get('users', { castIds: false });
function processUsers(lastId) {
// specify a query to limit the affected users (empty for all users):
var query = {
'migration':{$ne:migrationName},
};
if (lastId) {
query._id = {
$gt: lastId
}
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 250,
fields: [
'items.mounts',
] // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
})
.then(updateUsers)
.catch(function (err) {
console.log(err);
return exiting(1, 'ERROR! ' + err);
});
}
var progressCount = 1000;
var count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
displayData();
return;
}
var userPromises = users.map(updateUser);
var lastUser = users[users.length - 1];
return Promise.all(userPromises)
.then(function () {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
var set = {};
if (user.items.mounts['Orca-Base']) {
set = {'migration':migrationName, 'items.pets.Orca-Base': 5};
} else {
set = {'migration':migrationName, 'items.mounts.Orca-Base': true};
}
dbUsers.update({_id: user._id}, {$set:set});
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
if (user._id == authorUuid) console.warn(authorName + ' processed');
}
function displayData() {
console.warn('\n' + count + ' users processed\n');
return exiting(0);
}
function exiting(code, msg) {
code = code || 0; // 0 = success
if (code && !msg) { msg = 'ERROR!'; }
if (msg) {
if (code) { console.error(msg); }
else { console.log( msg); }
}
process.exit(code);
}
module.exports = processUsers;
+47 -47
View File
@@ -1,47 +1,47 @@
import Bluebird from 'Bluebird';
import { model as Challenges } from '../../website/server/models/challenge';
import { model as User } from '../../website/server/models/user';
async function syncChallengeToMembers (challenges) {
let challengSyncPromises = challenges.map(async function (challenge) {
let users = await User.find({
// _id: '',
challenges: challenge._id,
}).exec();
let promises = [];
users.forEach(function (user) {
promises.push(challenge.syncToUser(user));
promises.push(challenge.save());
promises.push(user.save());
});
return Bluebird.all(promises);
});
return await Bluebird.all(challengSyncPromises);
}
async function syncChallenges (lastChallengeDate) {
let query = {
// _id: '',
};
if (lastChallengeDate) {
query.createdOn = { $lte: lastChallengeDate };
}
let challengesFound = await Challenges.find(query)
.limit(10)
.sort('-createdAt')
.exec();
let syncedChallenges = await syncChallengeToMembers(challengesFound)
.catch(reason => console.error(reason));
let lastChallenge = challengesFound[challengesFound.length - 1];
if (lastChallenge) syncChallenges(lastChallenge.createdAt);
return syncedChallenges;
};
module.exports = syncChallenges;
import Bluebird from 'Bluebird';
import { model as Challenges } from '../../website/server/models/challenge';
import { model as User } from '../../website/server/models/user';
async function syncChallengeToMembers (challenges) {
let challengSyncPromises = challenges.map(async function (challenge) {
let users = await User.find({
// _id: '',
challenges: challenge._id,
}).exec();
let promises = [];
users.forEach(function (user) {
promises.push(challenge.syncToUser(user));
promises.push(challenge.save());
promises.push(user.save());
});
return Bluebird.all(promises);
});
return await Bluebird.all(challengSyncPromises);
}
async function syncChallenges (lastChallengeDate) {
let query = {
// _id: '',
};
if (lastChallengeDate) {
query.createdOn = { $lte: lastChallengeDate };
}
let challengesFound = await Challenges.find(query)
.limit(10)
.sort('-createdAt')
.exec();
let syncedChallenges = await syncChallengeToMembers(challengesFound)
.catch(reason => console.error(reason));
let lastChallenge = challengesFound[challengesFound.length - 1];
if (lastChallenge) syncChallenges(lastChallenge.createdAt);
return syncedChallenges;
};
module.exports = syncChallenges;
+1 -1
View File
@@ -21,4 +21,4 @@ var processUsers = require('./groups/update-groups-with-group-plans');
processUsers()
.catch(function (err) {
console.log(err)
})
})
+1 -1
View File
@@ -2,7 +2,7 @@ var _id = '';
var update = {
$addToSet: {
'purchased.plan.mysteryItems':{
$each:['back_mystery_201704','armor_mystery_201704']
$each:['body_mystery_201706','back_mystery_201706']
}
}
};
@@ -0,0 +1,83 @@
var migrationName = 'tasks-set-yesterdaily';
var authorName = 'TheHollidayInn'; // in case script author needs to know when their ...
var authorUuid = ''; //... own data is done
/*
* Iterates over all tasks and sets the yseterDaily field to True
*/
import monk from 'monk';
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
var dbTasks = monk(connectionString).get('tasks', { castIds: false });
function processTasks(lastId) {
// specify a query to limit the affected tasks (empty for all tasks):
var query = {
yesterDaily: false,
};
if (lastId) {
query._id = {
$gt: lastId
}
}
dbTasks.find(query, {
sort: {_id: 1},
limit: 250,
fields: [ // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
],
})
.then(updateTasks)
.catch(function (err) {
console.log(err);
return exiting(1, 'ERROR! ' + err);
});
}
var progressCount = 1000;
var count = 0;
function updateTasks (tasks) {
if (!tasks || tasks.length === 0) {
console.warn('All appropriate tasks found and modified.');
displayData();
return;
}
var taskPromises = tasks.map(updatetask);
var lasttask = tasks[tasks.length - 1];
return Promise.all(taskPromises)
.then(function () {
processtasks(lasttask._id);
});
}
function updatetask (task) {
count++;
var set = {'yesterDaily': true};
dbTasks.update({_id: task._id}, {$set:set});
if (count % progressCount == 0) console.warn(count + ' ' + task._id);
if (task._id == authorUuid) console.warn(authorName + ' processed');
}
function displayData() {
console.warn('\n' + count + ' tasks 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 = processtasks;
+109
View File
@@ -0,0 +1,109 @@
var migrationName = 'UserFromProdToTest';
var authorName = 'TheHollidayInn'; // in case script author needs to know when their ...
var authorUuid = ''; //... own data is done
/*
* This migraition will copy user data from prod to test
*/
var monk = require('monk');
var testConnectionSting = ''; // FOR TEST DATABASE
var usersTest = monk(testConnectionSting).get('users', { castIds: false });
var groupsTest = monk(testConnectionSting).get('groups', { castIds: false });
var challengesTest = monk(testConnectionSting).get('challenges', { castIds: false });
var tasksTest = monk(testConnectionSting).get('tasks', { castIds: false });
var monk2 = require('monk');
var liveConnectString = ''; // FOR TEST DATABASE
var userLive = monk2(liveConnectString).get('users', { castIds: false });
var groupsLive = monk2(liveConnectString).get('groups', { castIds: false });
var challengesLive = monk2(liveConnectString).get('challenges', { castIds: false });
var tasksLive = monk2(liveConnectString).get('tasks', { castIds: false });
import uniq from 'lodash/uniq';
import Bluebird from 'bluebird';
// Variabls for updating
let userIds = [
'206039c6-24e4-4b9f-8a31-61cbb9aa3f66',
];
let groupIds = [];
let challengeIds = [];
let tasksIds = [];
async function processUsers () {
let userPromises = [];
//{_id: {$in: userIds}}
return userLive.find({guilds: 'b0764d64-8276-45a1-afa5-5ca9a5c64ca0'})
.each((user, {close, pause, resume}) => {
if (user.guilds.length > 0) groupIds = groupIds.concat(user.guilds);
if (user.party._id) groupIds.push(user.party._id);
if (user.challenges.length > 0) challengeIds = challengeIds.concat(user.challenges);
if (user.tasksOrder.rewards.length > 0) tasksIds = tasksIds.concat(user.tasksOrder.rewards);
if (user.tasksOrder.todos.length > 0) tasksIds = tasksIds.concat(user.tasksOrder.todos);
if (user.tasksOrder.dailys.length > 0) tasksIds = tasksIds.concat(user.tasksOrder.dailys);
if (user.tasksOrder.habits.length > 0) tasksIds = tasksIds.concat(user.tasksOrder.habits);
let userPromise = usersTest.update({'_id': user._id}, user, {upsert:true});
userPromises.push(userPromise);
}).then(() => {
return Bluebird.all(userPromises);
})
.then(() => {
console.log("Done User");
});
}
function processGroups () {
let promises = [];
let groupsToQuery = uniq(groupIds);
return groupsLive.find({_id: {$in: groupsToQuery}})
.each((group, {close, pause, resume}) => {
let promise = groupsTest.update({_id: group._id}, group, {upsert:true});
promises.push(promise);
}).then(() => {
return Bluebird.all(promises);
})
.then(() => {
console.log("Done Group");
});
}
function processChallenges () {
let promises = [];
let challengesToQuery = uniq(challengeIds);
return challengesLive.find({_id: {$in: challengesToQuery}})
.each((challenge, {close, pause, resume}) => {
let promise = challengesTest.update({_id: challenge._id}, challenge, {upsert:true});
promises.push(promise);
}).then(() => {
return Bluebird.all(promises);
})
.then(() => {
console.log("Done Challenge");
});
}
function processTasks () {
let promises = [];
let tasksToQuery = uniq(tasksIds);
return tasksLive.find({_id: {$in: tasksToQuery}})
.each((task, {close, pause, resume}) => {
let promise = tasksTest.update({_id: task._id}, task, {upsert:true});
promises.push(promise);
}).then(() => {
return Bluebird.all(promises);
})
.then(() => {
console.log("Done Tasks");
});
}
module.exports = async function prodToTest () {
await processUsers();
await processGroups();
await processChallenges();
await processTasks();
};
+951 -639
View File
File diff suppressed because it is too large Load Diff
+22 -12
View File
@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "3.89.0",
"version": "3.104.1",
"main": "./website/server/index.js",
"dependencies": {
"@slack/client": "^3.8.1",
@@ -13,10 +13,12 @@
"async": "^1.5.0",
"autoprefixer": "^6.4.0",
"aws-sdk": "^2.0.25",
"axios": "^0.15.3",
"axios": "^0.16.0",
"babel-core": "^6.0.0",
"babel-eslint": "^7.2.3",
"babel-loader": "^6.0.0",
"babel-plugin-syntax-async-functions": "^6.13.0",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-plugin-transform-async-to-module-method": "^6.8.0",
"babel-plugin-transform-object-rest-spread": "^6.16.0",
"babel-plugin-transform-regenerator": "^6.16.1",
@@ -29,13 +31,14 @@
"bluebird": "^3.3.5",
"body-parser": "^1.15.0",
"bootstrap": "^4.0.0-alpha.6",
"bootstrap-vue": "^0.16.1",
"bower": "~1.3.12",
"browserify": "~12.0.1",
"compression": "^1.6.1",
"connect-ratelimit": "0.0.7",
"cookie-session": "^1.2.0",
"coupon-code": "^0.4.5",
"css-loader": "^0.26.1",
"css-loader": "^0.28.0",
"csv-stringify": "^1.0.2",
"cwait": "^1.0.0",
"domain-middleware": "~0.1.0",
@@ -66,6 +69,7 @@
"gulp-uglify": "^1.4.2",
"gulp.spritesmith": "^4.1.0",
"habitica-markdown": "^1.3.0",
"hellojs": "^1.15.1",
"html-webpack-plugin": "^2.8.1",
"image-size": "~0.3.2",
"in-app-purchase": "^1.1.6",
@@ -96,7 +100,7 @@
"postcss-easy-import": "^2.0.0",
"pretty-data": "^0.40.0",
"ps-tree": "^1.0.0",
"pug": "^2.0.0-beta11",
"pug": "^2.0.0-beta.12",
"push-notify": "habitrpg/push-notify#v1.2.0",
"pusher": "^1.3.0",
"request": "~2.74.0",
@@ -108,6 +112,9 @@
"shelljs": "^0.7.6",
"stripe": "^4.2.0",
"superagent": "^3.4.3",
"svg-inline-loader": "^0.7.1",
"svg-url-loader": "^2.0.2",
"svgo-loader": "^1.2.1",
"universal-analytics": "~0.3.2",
"url-loader": "^0.5.7",
"useragent": "^2.1.9",
@@ -119,11 +126,12 @@
"vue-loader": "^11.0.0",
"vue-mugen-scroll": "^0.2.1",
"vue-router": "^2.0.0-rc.5",
"vue-style-loader": "^2.0.0",
"vue-style-loader": "^3.0.0",
"vue-template-compiler": "^2.1.10",
"webpack": "^2.2.1",
"webpack-merge": "^2.6.1",
"webpack-merge": "^4.0.0",
"winston": "^2.1.0",
"winston-loggly-bulk": "^1.4.2",
"xml2js": "^0.4.4"
},
"private": true,
@@ -133,7 +141,7 @@
},
"scripts": {
"lint": "eslint --ext .js,.vue .",
"test": "npm run lint && gulp test && npm run client:unit",
"test": "npm run lint && gulp test && npm run client:unit && gulp apidoc",
"test:build": "gulp test:prepare:build",
"test:api-v3": "gulp test:api-v3",
"test:api-v3:unit": "gulp test:api-v3:unit",
@@ -150,14 +158,15 @@
"test:nodemon": "gulp test:nodemon",
"coverage": "COVERAGE=true mocha --require register-handlers.js --reporter html-cov > coverage.html; open coverage.html",
"sprites": "gulp sprites:compile",
"client:dev": "node webpack/dev-server.js",
"client:build": "node webpack/build.js",
"client:dev": "gulp bootstrap && node webpack/dev-server.js",
"client:build": "gulp bootstrap && node webpack/build.js",
"client:unit": "cross-env NODE_ENV=test karma start test/client/unit/karma.conf.js --single-run",
"client:unit:watch": "cross-env NODE_ENV=test karma start test/client/unit/karma.conf.js",
"client:e2e": "node test/client/e2e/runner.js",
"client:test": "npm run client:unit && npm run client:e2e",
"start": "gulp run:dev",
"postinstall": "bower --config.interactive=false install -f; gulp build; npm run client:build"
"postinstall": "bower --config.interactive=false install -f && gulp build && npm run client:build",
"apidoc": "gulp apidoc"
},
"devDependencies": {
"babel-plugin-istanbul": "^4.0.0",
@@ -167,7 +176,7 @@
"chromedriver": "^2.27.2",
"connect-history-api-fallback": "^1.1.0",
"coveralls": "^2.11.2",
"cross-env": "^3.1.4",
"cross-env": "^4.0.0",
"cross-spawn": "^5.0.1",
"csv": "~0.3.6",
"deep-diff": "~0.1.4",
@@ -191,7 +200,7 @@
"karma-mocha": "^0.2.0",
"karma-mocha-reporter": "^1.1.1",
"karma-phantomjs-launcher": "^1.0.0",
"karma-sinon-chai": "^1.2.0",
"karma-sinon-chai": "~1.2.0",
"karma-sinon-stub-promise": "^1.0.0",
"karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "0.0.24",
@@ -205,6 +214,7 @@
"nightwatch": "^0.9.12",
"phantomjs-prebuilt": "^2.1.12",
"protractor": "^3.1.1",
"raw-loader": "^0.5.1",
"require-again": "^2.0.0",
"rewire": "^2.3.3",
"selenium-server": "^3.0.1",
+19 -2
View File
@@ -1,3 +1,5 @@
require("babel-register");
require("babel-polyfill");
// This file is used for creating paypal billing plans. PayPal doesn't have a web interface for setting up recurring
// payment plan definitions, instead you have to create it via their REST SDK and keep it updated the same way. So this
// file will be used once for initing your billing plan (then you get the resultant plan.id to store in config.json),
@@ -7,10 +9,10 @@ var path = require('path');
var nconf = require('nconf');
var _ = require('lodash');
var paypal = require('paypal-rest-sdk');
var blocks = require('../../../../common').content.subscriptionBlocks;
var blocks = require('../website/common').content.subscriptionBlocks;
var live = nconf.get('PAYPAL:mode')=='live';
nconf.argv().env().file('user', path.join(path.resolve(__dirname, '../../../config.json')));
nconf.argv().env().file('user', path.join(path.resolve(__dirname, '../config.json')));
var OP = 'create'; // list create update remove
@@ -49,6 +51,8 @@ _.each(blocks, function(block){
});
})
// @TODO: Add cli library for this
switch(OP) {
case "list":
paypal.billingPlan.list({status: 'ACTIVE'}, function(err, plans){
@@ -91,4 +95,17 @@ switch(OP) {
});
break;
case "remove": break;
case 'create-webprofile':
let webexpinfo = {
"name": "HabiticaProfile",
"input_fields": {
"no_shipping": 1,
},
};
paypal.webProfile.create(webexpinfo, (error, result) => {
console.log(error, result)
})
break;
}
+1
View File
@@ -0,0 +1 @@
For information about writing and running tests, see [Using Your Local Install to Modify Habitica's Website and API](http://habitica.wikia.com/wiki/Using_Your_Local_Install_to_Modify_Habitica%27s_Website_and_API).
-123
View File
@@ -1,123 +0,0 @@
# So you want to write API integration tests?
@TODO rewrite
That's great! This README will serve as a quick primer for style conventions and practices for these tests.
## What is this?
These are integration tests for the Habitica API. They are performed by making HTTP requests to the API's endpoints and asserting on the data that is returned.
If the javascript looks weird to you, that's because it's written in [ES2015](http://www.ecma-international.org/ecma-262/6.0/) and transpiled by [Babel](https://babeljs.io/docs/learn-es2015/).
## How to run the tests
First, install gulp.
```bash
$ npm install -g gulp
```
To run the api tests, make sure the mongo db is up and running and then type this on the command line:
```bash
$ gulp test:api-v2
```
It may take a little while to get going, since it requires the web server to start up before the tests can run.
You can also run a watch command for the api tests. This will allow the tests to re-run automatically when you make changes in the `/test/api/v2/`.
```bash
$ gulp test:api-v2:watch
```
One caveat. If you have a severe syntax error in your files, the tests may fail to run, but they won't alert you that they are not running. If you ever find your test hanging for a while, run this to get the stackstrace. Once you've fixed the problem, you can resume running the watch comand.
```bash
$ gulp test:api:safe
```
If you'd like to run the tests individually and inspect the output from the server, in one pane you can run:
```bash
$ gulp test:nodemon
```
And run your tests in another pane:
```bash
$ mocha path/to/file.js
# Mark a test with the `.only` attribute
$ mocha
```
## Structure
Each top level route has it's own directory. So, all the routes that begin with `/groups/` live in `/test/api/groups/`.
Each test should:
* encompase a single route
* begin with the REST parameter for the route
* display the full name of the route, swapping out `/` for `_`
* end with test.js
So, for the `POST` route `/groups/:id/leave`, it would be
```bash
POST-groups_id_leave.test.js
```
## Promises
To mitigate [callback hell](http://callbackhell.com/) :imp:, we've written a helper method to generate a user object that can make http requests that [return promises](https://babeljs.io/docs/learn-es2015/#promises). This makes it very easy to chain together commands. All you need to do to make a subsequent request is return another promise and then call `.then((result) => {})` on the surrounding block, like so:
```js
it('does something', () => {
let user;
return generateUser().then((_user) => { // We return the initial promise so this test can be run asyncronously
user = _user;
return user.post('/groups', {
type: 'party',
});
}).then((party) => { // the result of the promise above is the argument of the function
return user.put(`/groups/${party._id}`, {
name: 'My party',
});
}).then((result) => {
return user.get('/groups/party');
}).then((party) => {
expect(party.name).to.eql('My party');
});
});
```
If the test is simple, you can use the [chai-as-promised](http://chaijs.com/plugins/chai-as-promised) `return expect(somePromise).to.eventually` syntax to make your assertion.
```js
it('makes the party creator the leader automatically', () => {
return expect(user.post('/groups', {
type: 'party',
})).to.eventually.have.deep.property('leader._id', user._id);
});
```
If the test is checking that the request returns an error, use the `.eventually.be.rejected.and.eql` syntax.
```js
it('returns an error', () => {
return expect(user.get('/groups/id-of-a-party-that-user-does-not-belong-to'))
.to.eventually.be.rejected.and.eql({
code: 404,
text: t('messageGroupNotFound'),
});
});
```
## Questions?
Ask in the [Aspiring Coder's Guild](https://habitica.com/#/options/groups/guilds/68d4a01e-db97-4786-8ee3-05d612c5af6f)!
-4
View File
@@ -1,4 +0,0 @@
# How to run tests:
1. `npm test` is equivalent to `gulp test:api-v3` which will run, in order, `gulp lint`, `gulp test:api-v3:unit` and `gulp test:api-v3:integration`. If one of these fails, the whole `npm test` command blocks and fails. Each of these commands can also be run as a standalone command.
2. To run the server and the integrations tests in two different terminals (to better inspect the output in the server) run `npm start` in one and `npm test:api-v3:integration:separate-server` in the other
@@ -304,5 +304,15 @@ describe('POST /challenges', () => {
await expect(groupLeader.sync()).to.eventually.have.property('challenges').to.include(challenge._id);
});
it('awards achievement if this is creator\'s first challenge', async () => {
await groupLeader.post('/challenges', {
group: group._id,
name: 'Test Challenge',
shortName: 'TC Label',
});
groupLeader = await groupLeader.sync();
expect(groupLeader.achievements.joinedChallenge).to.be.true;
});
});
});
@@ -123,5 +123,12 @@ describe('POST /challenges/:challengeId/join', () => {
await expect(authorizedUser.get('/tags')).to.eventually.have.length(userTagsLength + 1);
});
it('awards achievement if this is user\'s first challenge', async () => {
await authorizedUser.post(`/challenges/${challenge._id}/join`);
await authorizedUser.sync();
expect(authorizedUser.achievements.joinedChallenge).to.be.true;
});
});
});
@@ -100,8 +100,8 @@ describe('POST /challenges/:challengeId/winner/:winnerId', () => {
await sleep(0.5);
await expect(winningUser.sync()).to.eventually.have.deep.property('achievements.challenges').to.include(challenge.name);
expect(winningUser.notifications.length).to.equal(1);
expect(winningUser.notifications[0].type).to.equal('WON_CHALLENGE');
expect(winningUser.notifications.length).to.equal(2); // 2 because winningUser just joined the challenge, which now awards an achievement
expect(winningUser.notifications[1].type).to.equal('WON_CHALLENGE');
});
it('gives winner gems as reward', async () => {
@@ -114,6 +114,26 @@ describe('POST /challenges/:challengeId/winner/:winnerId', () => {
await expect(winningUser.sync()).to.eventually.have.property('balance', oldBalance + challenge.prize / 4);
});
it('doesn\'t gives winner gems if group policy prevents it', async () => {
let oldBalance = winningUser.balance;
let oldLeaderBalance = (await groupLeader.sync()).balance;
await winningUser.update({
'purchased.plan.customerId': 'group-plan',
});
await group.update({
'leaderOnly.getGems': true,
'purchased.plan.customerId': 123,
});
await groupLeader.post(`/challenges/${challenge._id}/selectWinner/${winningUser._id}`);
await sleep(0.5);
await expect(winningUser.sync()).to.eventually.have.property('balance', oldBalance);
await expect(groupLeader.sync()).to.eventually.have.property('balance', oldLeaderBalance + challenge.prize / 4);
});
it('doesn\'t refund gems to group leader', async () => {
let oldBalance = (await groupLeader.sync()).balance;
@@ -84,6 +84,10 @@ describe('POST /chat/:chatId/flag', () => {
type: 'party',
privacy: 'private',
});
await user.post(`/groups/${privateGroup._id}/invite`, {
uuids: [anotherUser._id],
});
await anotherUser.post(`/groups/${privateGroup._id}/join`);
let { message } = await user.post(`/groups/${privateGroup._id}/chat`, {message: TEST_MESSAGE});
let flagResult = await admin.post(`/groups/${privateGroup._id}/chat/${message.id}/flag`);
@@ -91,7 +95,7 @@ describe('POST /chat/:chatId/flag', () => {
expect(flagResult.flags[admin._id]).to.equal(true);
expect(flagResult.flagCount).to.equal(5);
let groupWithFlags = await user.get(`/groups/${privateGroup._id}`);
let groupWithFlags = await anotherUser.get(`/groups/${privateGroup._id}`);
let messageToCheck = find(groupWithFlags.chat, {id: message.id});
expect(messageToCheck).to.not.exist;
@@ -125,4 +129,20 @@ describe('POST /chat/:chatId/flag', () => {
message: t('messageGroupChatFlagAlreadyReported'),
});
});
it('shows a hidden message to the original poster', async () => {
let { message } = await user.post(`/groups/${group._id}/chat`, {message: TEST_MESSAGE});
await admin.post(`/groups/${group._id}/chat/${message.id}/flag`);
let groupWithFlags = await user.get(`/groups/${group._id}`);
let messageToCheck = find(groupWithFlags.chat, {id: message.id});
expect(messageToCheck).to.exist;
let auGroupWithFlags = await anotherUser.get(`/groups/${group._id}`);
let auMessageToCheck = find(auGroupWithFlags.chat, {id: message.id});
expect(auMessageToCheck).to.not.exist;
});
});
+128 -5
View File
@@ -4,11 +4,17 @@ import {
sleep,
server,
} from '../../../../helpers/api-v3-integration.helper';
import {
SPAM_MESSAGE_LIMIT,
SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
TAVERN_ID,
} from '../../../../../website/server/models/group';
import { v4 as generateUUID } from 'uuid';
describe('POST /chat', () => {
let user, groupWithChat, userWithChatRevoked, member;
let user, groupWithChat, member, additionalMember;
let testMessage = 'Test Message';
let testBannedWordMessage = 'TEST_PLACEHOLDER_SWEAR_WORD_HERE';
before(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
@@ -22,8 +28,8 @@ describe('POST /chat', () => {
user = groupLeader;
groupWithChat = group;
userWithChatRevoked = await members[0].update({'flags.chatRevoked': true});
member = members[0];
additionalMember = members[1];
});
it('Returns an error when no message is provided', async () => {
@@ -62,10 +68,101 @@ describe('POST /chat', () => {
});
it('returns an error when chat privileges are revoked when sending a message to a public guild', async () => {
let userWithChatRevoked = await member.update({'flags.chatRevoked': true});
await expect(userWithChatRevoked.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage})).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: 'Your chat privileges have been revoked.',
code: 401,
error: 'NotAuthorized',
message: t('chatPrivilegesRevoked'),
});
});
context('banned word', () => {
it('returns an error when chat message contains a banned word in tavern', async () => {
await expect(user.post('/groups/habitrpg/chat', { message: testBannedWordMessage}))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedWordUsed'),
});
});
it('errors when word is part of a phrase', async () => {
let wordInPhrase = `phrase ${testBannedWordMessage} end`;
await expect(user.post('/groups/habitrpg/chat', { message: wordInPhrase}))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedWordUsed'),
});
});
it('errors when word is surrounded by non alphabet characters', async () => {
let wordInPhrase = `_!${testBannedWordMessage}@_`;
await expect(user.post('/groups/habitrpg/chat', { message: wordInPhrase}))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedWordUsed'),
});
});
it('does not error when bad word is suffix of a word', async () => {
let wordAsSuffix = `prefix${testBannedWordMessage}`;
let message = await user.post('/groups/habitrpg/chat', { message: wordAsSuffix});
expect(message.message.id).to.exist;
});
it('does not error when bad word is prefix of a word', async () => {
let wordAsPrefix = `${testBannedWordMessage}suffix`;
let message = await user.post('/groups/habitrpg/chat', { message: wordAsPrefix});
expect(message.message.id).to.exist;
});
it('does not error when sending a chat message containing a banned word to a party', async () => {
let { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'Party',
type: 'party',
privacy: 'private',
},
members: 1,
});
let message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage});
expect(message.message.id).to.exist;
});
it('does not error when sending a chat message containing a banned word to a public guild', async () => {
let { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'public guild',
type: 'guild',
privacy: 'public',
},
members: 1,
});
let message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage});
expect(message.message.id).to.exist;
});
it('does not error when sending a chat message containing a banned word to a private guild', async () => {
let { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'private guild',
type: 'guild',
privacy: 'private',
},
members: 1,
});
let message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage});
expect(message.message.id).to.exist;
});
});
@@ -173,4 +270,30 @@ describe('POST /chat', () => {
expect(message.message.id).to.exist;
expect(memberWithNotification.newMessages[`${group._id}`]).to.exist;
});
context('Spam prevention', () => {
it('Returns an error when the user has been posting too many messages', async () => {
// Post as many messages are needed to reach the spam limit
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
let result = await additionalMember.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage }); // eslint-disable-line no-await-in-loop
expect(result.message.id).to.exist;
}
await expect(additionalMember.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage })).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageGroupChatSpam'),
});
});
it('contributor should not receive spam alert', async () => {
let userSocialite = await member.update({'contributor.level': SPAM_MIN_EXEMPT_CONTRIB_LEVEL, 'flags.chatRevoked': false});
// Post 1 more message than the spam limit to ensure they do not reach the limit
for (let i = 0; i < SPAM_MESSAGE_LIMIT + 1; i++) {
let result = await userSocialite.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage }); // eslint-disable-line no-await-in-loop
expect(result.message.id).to.exist;
}
});
});
});
@@ -0,0 +1,93 @@
import {
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
import { find } from 'lodash';
describe('POST /group/:groupId/remove-manager', () => {
let leader, nonLeader, groupToUpdate;
let groupName = 'Test Public Guild';
let groupType = 'guild';
let nonManager;
function findAssignedTask (memberTask) {
return memberTask.group.id === groupToUpdate._id;
}
beforeEach(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: groupName,
type: groupType,
privacy: 'public',
},
members: 1,
});
groupToUpdate = group;
leader = groupLeader;
nonLeader = members[0];
nonManager = members[0];
});
it('returns an error when a non group leader tries to add member', async () => {
await expect(nonLeader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
managerId: nonLeader._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageGroupOnlyLeaderCanUpdate'),
});
});
it('returns an error when manager does not exist', async () => {
await expect(leader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
managerId: nonManager._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('userIsNotManager'),
});
});
it('allows a leader to remove managers', async () => {
await leader.post(`/groups/${groupToUpdate._id}/add-manager`, {
managerId: nonLeader._id,
});
let updatedGroup = await leader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
managerId: nonLeader._id,
});
expect(updatedGroup.managers[nonLeader._id]).to.not.exist;
});
it('removes group approval notifications from a manager that is removed', async () => {
await leader.post(`/groups/${groupToUpdate._id}/add-manager`, {
managerId: nonLeader._id,
});
let task = await leader.post(`/tasks/group/${groupToUpdate._id}`, {
text: 'test todo',
type: 'todo',
requiresApproval: true,
});
await nonLeader.post(`/tasks/${task._id}/assign/${leader._id}`);
let memberTasks = await leader.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(leader.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
let updatedGroup = await leader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
managerId: nonLeader._id,
});
await nonLeader.sync();
expect(nonLeader.notifications.length).to.equal(0);
expect(updatedGroup.managers[nonLeader._id]).to.not.exist;
});
});
@@ -74,6 +74,18 @@ describe('POST /group', () => {
expect(updatedUser.guilds).to.include(guild._id);
});
it('awards the Joined Guild achievement', async () => {
await user.post('/groups', {
name: 'some guild',
type: 'guild',
privacy: 'public',
});
let updatedUser = await user.get('/user');
expect(updatedUser.achievements.joinedGuild).to.eql(true);
});
context('public guild', () => {
it('creates a group', async () => {
let groupName = 'Test Public Guild';
@@ -68,6 +68,12 @@ describe('POST /group/:groupId/join', () => {
await expect(joiningUser.get(`/groups/${publicGuild._id}`)).to.eventually.have.property('memberCount', oldMemberCount + 1);
});
it('awards Joined Guild achievement', async () => {
await joiningUser.post(`/groups/${publicGuild._id}/join`);
await expect(joiningUser.get('/user')).to.eventually.have.deep.property('achievements.joinedGuild', true);
});
});
context('Joining a private guild', () => {
@@ -147,8 +153,14 @@ describe('POST /group/:groupId/join', () => {
}),
};
expect(inviter.notifications[0].type).to.eql('GROUP_INVITE_ACCEPTED');
expect(inviter.notifications[0].data).to.eql(expectedData);
expect(inviter.notifications[1].type).to.eql('GROUP_INVITE_ACCEPTED');
expect(inviter.notifications[1].data).to.eql(expectedData);
});
it('awards Joined Guild achievement', async () => {
await invitedUser.post(`/groups/${guild._id}/join`);
await expect(invitedUser.get('/user')).to.eventually.have.deep.property('achievements.joinedGuild', true);
});
});
});
@@ -11,6 +11,7 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
let guild;
let member;
let member2;
let adminUser;
beforeEach(async () => {
let { group, groupLeader, invitees, members } = await createAndPopulateGroup({
@@ -28,6 +29,7 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
invitedUser = invitees[0];
member = members[0];
member2 = members[1];
adminUser = await generateUser({ 'contributor.admin': true });
});
context('All Groups', () => {
@@ -42,7 +44,7 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
});
});
it('returns an error when user is a non-leader member of a group', async () => {
it('returns an error when user is a non-leader member of a group and not an admin', async () => {
expect(member2.post(`/groups/${guild._id}/removeMember/${member._id}`))
.to.eventually.be.rejected.and.eql({
code: 401,
@@ -87,7 +89,30 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
let invitedUserWithoutInvite = await invitedUser.get('/user');
expect(_.findIndex(invitedUserWithoutInvite.invitations.guilds, {id: guild._id})).eql(-1);
expect(_.findIndex(invitedUserWithoutInvite.invitations.guilds, { id: guild._id })).eql(-1);
});
it('allows an admin to remove other members', async () => {
await adminUser.post(`/groups/${guild._id}/removeMember/${member._id}`);
let memberRemoved = await member.get('/user');
expect(memberRemoved.guilds.indexOf(guild._id)).eql(-1);
});
it('allows an admin to remove other invites', async () => {
await adminUser.post(`/groups/${guild._id}/removeMember/${invitedUser._id}`);
let invitedUserWithoutInvite = await invitedUser.get('/user');
expect(_.findIndex(invitedUserWithoutInvite.invitations.guilds, { id: guild._id })).eql(-1);
});
it('does not allow an admin to remove a leader', async () => {
expect(adminUser.post(`/groups/${guild._id}/removeMember/${leader._id}`))
.to.eventually.be.rejected.and.eql({
code: 401,
text: t('cannotRemoveCurrentLeader'),
});
});
it('sends email to user with rescinded invite', async () => {
@@ -0,0 +1,85 @@
import {
generateUser,
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
describe('POST /group/:groupId/add-manager', () => {
let leader, nonLeader, groupToUpdate;
let groupName = 'Test Public Guild';
let groupType = 'guild';
let nonMember;
context('Guilds', () => {
beforeEach(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: groupName,
type: groupType,
privacy: 'public',
},
members: 1,
});
groupToUpdate = group;
leader = groupLeader;
nonLeader = members[0];
nonMember = await generateUser();
});
it('returns an error when a non group leader tries to add member', async () => {
await expect(nonLeader.post(`/groups/${groupToUpdate._id}/add-manager`, {
managerId: nonLeader._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageGroupOnlyLeaderCanUpdate'),
});
});
it('returns an error when trying to promote a non member', async () => {
await expect(leader.post(`/groups/${groupToUpdate._id}/add-manager`, {
managerId: nonMember._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('userMustBeMember'),
});
});
it('allows a leader to add managers', async () => {
let updatedGroup = await leader.post(`/groups/${groupToUpdate._id}/add-manager`, {
managerId: nonLeader._id,
});
expect(updatedGroup.managers[nonLeader._id]).to.be.true;
});
});
context('Party', () => {
let party, partyLeader, partyNonLeader;
beforeEach(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: groupName,
type: 'party',
privacy: 'private',
},
members: 1,
});
party = group;
partyLeader = groupLeader;
partyNonLeader = members[0];
});
it('allows leader of party to add managers', async () => {
let updatedGroup = await partyLeader.post(`/groups/${party._id}/add-manager`, {
managerId: partyNonLeader._id,
});
expect(updatedGroup.managers[partyNonLeader._id]).to.be.true;
});
});
});
@@ -1,10 +1,11 @@
import {
createAndPopulateGroup,
generateUser,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
describe('PUT /group', () => {
let leader, nonLeader, groupToUpdate;
let leader, nonLeader, groupToUpdate, adminUser;
let groupName = 'Test Public Guild';
let groupType = 'guild';
let groupUpdatedName = 'Test Public Guild Updated';
@@ -18,13 +19,13 @@ describe('PUT /group', () => {
},
members: 1,
});
adminUser = await generateUser({ 'contributor.admin': true });
groupToUpdate = group;
leader = groupLeader;
nonLeader = members[0];
});
it('returns an error when a non group leader tries to update', async () => {
it('returns an error when a user that is not an admin or group leader tries to update', async () => {
await expect(nonLeader.put(`/groups/${groupToUpdate._id}`, {
name: groupUpdatedName,
})).to.eventually.be.rejected.and.eql({
@@ -44,6 +45,15 @@ describe('PUT /group', () => {
expect(updatedGroup.name).to.equal(groupUpdatedName);
});
it('allows an admin to update a guild', async () => {
let updatedGroup = await adminUser.put(`/groups/${groupToUpdate._id}`, {
name: groupUpdatedName,
});
expect(updatedGroup.leader._id).to.eql(leader._id);
expect(updatedGroup.leader.profile.name).to.eql(leader.profile.name);
expect(updatedGroup.name).to.equal(groupUpdatedName);
});
it('allows a leader to change leaders', async () => {
let updatedGroup = await leader.put(`/groups/${groupToUpdate._id}`, {
name: groupUpdatedName,
@@ -0,0 +1,64 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
describe('GET /members/:toUserId/objections/:interaction', () => {
let user;
before(async () => {
user = await generateUser();
});
it('validates req.params.memberId', async () => {
await expect(
user.get('/members/invalidUUID/objections/send-private-message')
).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('handles non-existing members', async () => {
let dummyId = generateUUID();
await expect(
user.get(`/members/${dummyId}/objections/send-private-message`)
).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('userWithIDNotFound', {userId: dummyId}),
});
});
it('handles non-existing interactions', async () => {
let receiver = await generateUser();
await expect(
user.get(`/members/${receiver._id}/objections/hug-a-whole-forest-of-trees`)
).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('returns an empty array if there are no objections', async () => {
let receiver = await generateUser();
await expect(
user.get(`/members/${receiver._id}/objections/send-private-message`)
).to.eventually.be.fulfilled.and.eql([]);
});
it('returns an array of objections if any exist', async () => {
let receiver = await generateUser({'inbox.blocks': [user._id]});
await expect(
user.get(`/members/${receiver._id}/objections/send-private-message`)
).to.eventually.be.fulfilled.and.eql([
t('notAuthorizedToSendMessageToThisUser'),
]);
});
});
@@ -82,6 +82,20 @@ describe('POST /members/send-private-message', () => {
});
});
it('returns an error when chat privileges are revoked', async () => {
let userWithChatRevoked = await generateUser({'flags.chatRevoked': true});
let receiver = await generateUser();
await expect(userWithChatRevoked.post('/members/send-private-message', {
message: messageToSend,
toUserId: receiver._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('chatPrivilegesRevoked'),
});
});
it('sends a private message to a user', async () => {
let receiver = await generateUser();
@@ -43,7 +43,7 @@ describe('POST /members/transfer-gems', () => {
});
});
it('returns error when to user is not found', async () => {
it('returns error when recipient is not found', async () => {
await expect(userToSendMessage.post('/members/transfer-gems', {
message,
gemAmount,
@@ -55,7 +55,7 @@ describe('POST /members/transfer-gems', () => {
});
});
it('returns error when to user attempts to send gems to themselves', async () => {
it('returns error when user attempts to send gems to themselves', async () => {
await expect(userToSendMessage.post('/members/transfer-gems', {
message,
gemAmount,
@@ -67,6 +67,64 @@ describe('POST /members/transfer-gems', () => {
});
});
it('returns error when recipient has blocked the sender', async () => {
let receiverWhoBlocksUser = await generateUser({'inbox.blocks': [userToSendMessage._id]});
await expect(userToSendMessage.post('/members/transfer-gems', {
message,
gemAmount,
toUserId: receiverWhoBlocksUser._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('notAuthorizedToSendMessageToThisUser'),
});
});
it('returns error when sender has blocked recipient', async () => {
let sender = await generateUser({'inbox.blocks': [receiver._id]});
await expect(sender.post('/members/transfer-gems', {
message,
gemAmount,
toUserId: receiver._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('notAuthorizedToSendMessageToThisUser'),
});
});
it('returns an error when chat privileges are revoked', async () => {
let userWithChatRevoked = await generateUser({'flags.chatRevoked': true});
await expect(userWithChatRevoked.post('/members/transfer-gems', {
message,
gemAmount,
toUserId: receiver._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('chatPrivilegesRevoked'),
});
});
it('works when only the recipient\'s chat privileges are revoked', async () => {
let receiverWithChatRevoked = await generateUser({'flags.chatRevoked': true});
await expect(userToSendMessage.post('/members/transfer-gems', {
message,
gemAmount,
toUserId: receiverWithChatRevoked._id,
})).to.eventually.be.fulfilled;
let updatedReceiver = await receiverWithChatRevoked.get('/user');
let updatedSender = await userToSendMessage.get('/user');
expect(updatedReceiver.balance).to.equal(gemAmount / 4);
expect(updatedSender.balance).to.equal(0);
});
it('returns error when there is no gemAmount', async () => {
await expect(userToSendMessage.post('/members/transfer-gems', {
message,
@@ -144,7 +202,7 @@ describe('POST /members/transfer-gems', () => {
expect(updatedSender.balance).to.equal(0);
});
it('does not requrie a message', async () => {
it('does not require a message', async () => {
await userToSendMessage.post('/members/transfer-gems', {
gemAmount,
toUserId: receiver._id,
@@ -0,0 +1,25 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('GET /shops/backgrounds', () => {
let user;
beforeEach(async () => {
user = await generateUser();
});
it('returns a valid shop object', async () => {
let shop = await user.get('/shops/backgrounds');
expect(shop.identifier).to.equal('backgroundShop');
expect(shop.text).to.eql(t('backgroundShop'));
expect(shop.notes).to.eql(t('backgroundShopText'));
expect(shop.imageName).to.equal('background_shop');
expect(shop.sets).to.be.an('array');
let sets = shop.sets.map(set => set.identifier);
expect(sets).to.include('incentiveBackgrounds');
expect(sets).to.include('backgrounds062014');
});
});
@@ -1,3 +1,4 @@
import moment from 'moment';
import {
generateUser,
} from '../../../../helpers/api-integration/v3';
@@ -127,4 +128,106 @@ describe('GET /tasks/user', () => {
let allCompletedTodos = await user.get('/tasks/user?type=_allCompletedTodos');
expect(allCompletedTodos.length).to.equal(numberOfTodos);
});
it('returns dailies with isDue for the date specified', async () => {
// @TODO Add required format
let startDate = moment().subtract('1', 'days').toISOString();
let createdTasks = await user.post('/tasks/user', [
{
text: 'test daily',
type: 'daily',
startDate,
frequency: 'daily',
everyX: 2,
},
]);
let dailys = await user.get('/tasks/user?type=dailys');
expect(dailys.length).to.be.at.least(1);
expect(dailys[0]._id).to.equal(createdTasks._id);
expect(dailys[0].isDue).to.be.false;
let dailys2 = await user.get(`/tasks/user?type=dailys&dueDate=${startDate}`);
expect(dailys2[0]._id).to.equal(createdTasks._id);
expect(dailys2[0].isDue).to.be.true;
});
it('returns dailies with isDue for the date specified and will add CDS offset if time is not supplied and assumes timezones', async () => {
let timezone = 420;
await user.update({
'preferences.dayStart': 0,
'preferences.timezoneOffset': timezone,
});
let startDate = moment().zone(timezone).subtract('4', 'days').startOf('day').toISOString();
await user.post('/tasks/user', [
{
text: 'test daily',
type: 'daily',
startDate,
frequency: 'daily',
everyX: 2,
},
]);
let today = moment().format('YYYY-MM-DD');
let dailys = await user.get(`/tasks/user?type=dailys&dueDate=${today}`);
expect(dailys[0].isDue).to.be.true;
let yesterday = moment().subtract('1', 'days').format('YYYY-MM-DD');
let dailys2 = await user.get(`/tasks/user?type=dailys&dueDate=${yesterday}`);
expect(dailys2[0].isDue).to.be.false;
});
it('returns dailies with isDue for the date specified and will add CDS offset if time is not supplied and assumes timezones', async () => {
let timezone = 240;
await user.update({
'preferences.dayStart': 0,
'preferences.timezoneOffset': timezone,
});
let startDate = moment().zone(timezone).subtract('4', 'days').startOf('day').toISOString();
await user.post('/tasks/user', [
{
text: 'test daily',
type: 'daily',
startDate,
frequency: 'daily',
everyX: 2,
},
]);
let today = moment().format('YYYY-MM-DD');
let dailys = await user.get(`/tasks/user?type=dailys&dueDate=${today}`);
expect(dailys[0].isDue).to.be.true;
let yesterday = moment().subtract('1', 'days').format('YYYY-MM-DD');
let dailys2 = await user.get(`/tasks/user?type=dailys&dueDate=${yesterday}`);
expect(dailys2[0].isDue).to.be.false;
});
it('returns dailies with isDue for the date specified and will add CDS offset if time is not supplied and assumes timezones', async () => {
let timezone = 540;
await user.update({
'preferences.dayStart': 0,
'preferences.timezoneOffset': timezone,
});
let startDate = moment().zone(timezone).subtract('4', 'days').startOf('day').toISOString();
await user.post('/tasks/user', [
{
text: 'test daily',
type: 'daily',
startDate,
frequency: 'daily',
everyX: 2,
},
]);
let today = moment().format('YYYY-MM-DD');
let dailys = await user.get(`/tasks/user?type=dailys&dueDate=${today}`);
expect(dailys[0].isDue).to.be.true;
let yesterday = moment().subtract('1', 'days').format('YYYY-MM-DD');
let dailys2 = await user.get(`/tasks/user?type=dailys&dueDate=${yesterday}`);
expect(dailys2[0].isDue).to.be.false;
});
});
@@ -208,6 +208,20 @@ describe('POST /tasks/:id/score/:direction', () => {
expect(task.completed).to.equal(false);
});
it('computes isDue', async () => {
await user.post(`/tasks/${daily._id}/score/up`);
let task = await user.get(`/tasks/${daily._id}`);
expect(task.isDue).to.equal(true);
});
it('computes nextDue', async () => {
await user.post(`/tasks/${daily._id}/score/up`);
let task = await user.get(`/tasks/${daily._id}`);
expect(task.nextDue.length).to.eql(6);
});
it('scores up daily even if it is already completed'); // Yes?
it('scores down daily even if it is already uncompleted'); // Yes?
@@ -0,0 +1,109 @@
import {
generateUser,
generateGroup,
generateChallenge,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('POST /tasks/unlink-all/:challengeId', () => {
let user;
let guild;
let challenge;
let tasksToTest = {
habit: {
text: 'test habit',
type: 'habit',
up: false,
down: true,
},
todo: {
text: 'test todo',
type: 'todo',
},
daily: {
text: 'test daily',
type: 'daily',
frequency: 'daily',
everyX: 5,
startDate: new Date(),
},
reward: {
text: 'test reward',
type: 'reward',
},
};
beforeEach(async () => {
user = await generateUser();
guild = await generateGroup(user);
challenge = await generateChallenge(user, guild);
});
it('fails if no keep query', async () => {
await expect(user.post(`/tasks/unlink-all/${challenge._id}`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('fails if invalid challenge id', async () => {
await expect(user.post('/tasks/unlink-all/123?keep=remove-all'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('fails on an unbroken challenge', async () => {
await user.post(`/tasks/challenge/${challenge._id}`, tasksToTest.daily);
await expect(user.post(`/tasks/unlink-all/${challenge._id}?keep=remove-all`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('cantOnlyUnlinkChalTask'),
});
});
it('unlinks all tasks from a challenge and deletes them on keep=remove-all', async () => {
const daily = await user.post(`/tasks/challenge/${challenge._id}`, tasksToTest.daily);
await user.post(`/tasks/challenge/${challenge._id}`, tasksToTest.habit);
await user.post(`/tasks/challenge/${challenge._id}`, tasksToTest.reward);
await user.post(`/tasks/challenge/${challenge._id}`, tasksToTest.todo);
await user.del(`/challenges/${challenge._id}`);
const response = await user.post(`/tasks/unlink-all/${challenge._id}?keep=remove-all`);
expect(response).to.eql({});
await expect(user.get(`/tasks/${daily._id}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('taskNotFound'),
});
});
it('unlinks a task from a challenge on keep=keep-all', async () => {
const daily = await user.post(`/tasks/challenge/${challenge._id}`, tasksToTest.daily);
const anotherUser = await generateUser();
await user.post(`/groups/${guild._id}/invite`, {
uuids: [anotherUser._id],
});
// Have the second user join the group and challenge
await anotherUser.post(`/groups/${guild._id}/join`);
await anotherUser.post(`/challenges/${challenge._id}/join`);
// Have the leader delete the challenge and unlink the tasks
await user.del(`/challenges/${challenge._id}`);
await user.post(`/tasks/unlink-all/${challenge._id}?keep=keep-all`);
// Get the second task for the second user
const [, anotherUserTask] = await anotherUser.get('/tasks/user');
// Expect the second user to still have the task, but unlinked
expect(anotherUserTask.challenge).to.eql({
taskId: daily._id,
id: challenge._id,
shortName: challenge.shortName,
broken: 'CHALLENGE_DELETED',
winner: null,
});
});
});
@@ -0,0 +1,110 @@
import {
generateUser,
generateGroup,
generateChallenge,
translate as t,
} from '../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
describe('POST /tasks/unlink-one/:taskId', () => {
let user;
let guild;
let challenge;
let tasksToTest = {
habit: {
text: 'test habit',
type: 'habit',
up: false,
down: true,
},
todo: {
text: 'test todo',
type: 'todo',
},
daily: {
text: 'test daily',
type: 'daily',
frequency: 'daily',
everyX: 5,
startDate: new Date(),
},
reward: {
text: 'test reward',
type: 'reward',
},
};
beforeEach(async () => {
user = await generateUser();
guild = await generateGroup(user);
challenge = await generateChallenge(user, guild);
});
it('fails if no keep query', async () => {
const daily = await user.post(`/tasks/challenge/${challenge._id}`, tasksToTest.daily);
await expect(user.post(`/tasks/unlink-one/${daily._id}`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('fails if invalid task id', async () => {
await expect(user.post('/tasks/unlink-one/123?keep=remove'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('fails on task not found', async () => {
await expect(user.post(`/tasks/unlink-one/${generateUUID()}?keep=keep`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('taskNotFound'),
});
});
it('fails on task unlinked to challenge', async () => {
let daily = await user.post('/tasks/user', tasksToTest.daily);
await expect(user.post(`/tasks/unlink-one/${daily._id}?keep=keep`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('cantOnlyUnlinkChalTask'),
});
});
it('fails on unbroken challenge', async () => {
await user.post(`/tasks/challenge/${challenge._id}`, tasksToTest.daily);
let [daily] = await user.get('/tasks/user');
await expect(user.post(`/tasks/unlink-one/${daily._id}?keep=keep`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('cantOnlyUnlinkChalTask'),
});
});
it('unlinks a task from a challenge and saves it on keep=keep', async () => {
await user.post(`/tasks/challenge/${challenge._id}`, tasksToTest.daily);
let [, daily] = await user.get('/tasks/user');
await user.del(`/challenges/${challenge._id}`);
await user.post(`/tasks/unlink-one/${daily._id}?keep=keep`);
[, daily] = await user.get('/tasks/user');
expect(daily.challenge).to.eql({});
});
it('unlinks a task from a challenge and deletes it on keep=remove', async () => {
await user.post(`/tasks/challenge/${challenge._id}`, tasksToTest.daily);
let [, daily] = await user.get('/tasks/user');
await user.del(`/challenges/${challenge._id}`);
await user.post(`/tasks/unlink-one/${daily._id}?keep=remove`);
const tasks = await user.get('/tasks/user');
// Only the default task should remain
expect(tasks.length).to.eql(1);
});
});
@@ -133,6 +133,7 @@ describe('POST /tasks/user', () => {
expect(task.completed).to.equal(false);
expect(task.streak).not.to.equal('never');
expect(task.value).not.to.equal(324);
expect(task.yesterDaily).to.equal(true);
});
it('ignores invalid fields', async () => {
@@ -509,6 +510,8 @@ describe('POST /tasks/user', () => {
expect(task.daysOfMonth).to.eql([15]);
expect(task.weeksOfMonth).to.eql([3]);
expect(new Date(task.startDate)).to.eql(now);
expect(task.isDue).to.be.true;
expect(task.nextDue.length).to.eql(6);
});
it('creates multiple dailys', async () => {
@@ -613,6 +616,18 @@ describe('POST /tasks/user', () => {
expect((new Date(task.startDate)).getDay()).to.eql(today);
});
it('returns an error if the start date is empty', async () => {
await expect(user.post('/tasks/user', {
text: 'test daily',
type: 'daily',
startDate: '',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'daily validation failed',
});
});
it('can create checklists', async () => {
let task = await user.post('/tasks/user', {
text: 'test daily',
@@ -1,3 +1,4 @@
import moment from 'moment';
import {
generateUser,
generateGroup,
@@ -395,12 +396,17 @@ describe('PUT /tasks/:id', () => {
notes: 'some new notes',
frequency: 'daily',
everyX: 5,
yesterDaily: false,
startDate: moment().add(1, 'days').toDate(),
});
expect(savedDaily.text).to.eql('some new text');
expect(savedDaily.notes).to.eql('some new notes');
expect(savedDaily.frequency).to.eql('daily');
expect(savedDaily.everyX).to.eql(5);
expect(savedDaily.isDue).to.be.false;
expect(savedDaily.nextDue.length).to.eql(6);
expect(savedDaily.yesterDaily).to.be.false;
});
it('can update checklists (replace it)', async () => {
@@ -4,7 +4,7 @@ import {
} from '../../../../../helpers/api-integration/v3';
import { find } from 'lodash';
describe('DELETE /tasks/:id', () => {
describe('Groups DELETE /tasks/:id', () => {
let user, guild, member, member2, task;
function findAssignedTask (memberTask) {
@@ -48,6 +48,21 @@ describe('DELETE /tasks/:id', () => {
});
});
it('allows a manager to delete a group task', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.del(`/tasks/${task._id}`);
await expect(user.get(`/tasks/${task._id}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('taskNotFound'),
});
});
it('unlinks assigned user', async () => {
await user.del(`/tasks/${task._id}`);
@@ -55,4 +55,13 @@ describe('GET /approvals/group/:groupId', () => {
let approvals = await user.get(`/approvals/group/${guild._id}`);
expect(approvals[0]._id).to.equal(syncedTask._id);
});
it('allows managers to get a list of task that need approval', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member._id,
});
let approvals = await member.get(`/approvals/group/${guild._id}`);
expect(approvals[0]._id).to.equal(syncedTask._id);
});
});
@@ -5,7 +5,7 @@ import {
import { find } from 'lodash';
describe('POST /tasks/:id/approve/:userId', () => {
let user, guild, member, task;
let user, guild, member, member2, task;
function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
@@ -17,12 +17,13 @@ describe('POST /tasks/:id/approve/:userId', () => {
name: 'Test Guild',
type: 'guild',
},
members: 1,
members: 2,
});
guild = group;
user = groupLeader;
member = members[0];
member2 = members[1];
task = await user.post(`/tasks/group/${guild._id}`, {
text: 'test todo',
@@ -69,4 +70,74 @@ describe('POST /tasks/:id/approve/:userId', () => {
expect(syncedTask.group.approval.approvingUser).to.equal(user._id);
expect(syncedTask.group.approval.dateApproved).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
});
it('allows a manager to approve an assigned user', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await member.sync();
expect(member.notifications.length).to.equal(2);
expect(member.notifications[0].type).to.equal('GROUP_TASK_APPROVED');
expect(member.notifications[0].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
expect(member.notifications[1].type).to.equal('SCORED_TASK');
expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
expect(syncedTask.group.approval.approved).to.be.true;
expect(syncedTask.group.approval.approvingUser).to.equal(member2._id);
expect(syncedTask.group.approval.dateApproved).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
});
it('removes approval pending notifications from managers', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(2);
expect(user.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
expect(member2.notifications.length).to.equal(1);
expect(member2.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(1);
expect(member2.notifications.length).to.equal(0);
});
it('prevents double approval on a task', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await expect(user.post(`/tasks/${task._id}/approve/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('canOnlyApproveTaskOnce'),
});
});
});
@@ -5,7 +5,7 @@ import {
import { find } from 'lodash';
describe('POST /tasks/:id/score/:direction', () => {
let user, guild, member, task;
let user, guild, member, member2, task;
function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
@@ -17,12 +17,13 @@ describe('POST /tasks/:id/score/:direction', () => {
name: 'Test Guild',
type: 'guild',
},
members: 1,
members: 2,
});
guild = group;
user = groupLeader;
member = members[0];
member2 = members[1];
task = await user.post(`/tasks/group/${guild._id}`, {
text: 'test todo',
@@ -51,18 +52,55 @@ describe('POST /tasks/:id/score/:direction', () => {
await user.sync();
expect(user.notifications.length).to.equal(1);
expect(user.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
expect(user.notifications[0].data.message).to.equal(t('userHasRequestedTaskApproval', {
expect(user.notifications.length).to.equal(2);
expect(user.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
expect(user.notifications[1].data.message).to.equal(t('userHasRequestedTaskApproval', {
user: member.auth.local.username,
taskName: updatedTask.text,
taskId: updatedTask._id,
}, 'cs')); // This test only works if we have the notification translated
expect(user.notifications[0].data.groupId).to.equal(guild._id);
expect(user.notifications[1].data.groupId).to.equal(guild._id);
expect(updatedTask.group.approval.requested).to.equal(true);
expect(updatedTask.group.approval.requestedDate).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
});
it('sends notifications to all managers', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
let updatedTask = await member.get(`/tasks/${syncedTask._id}`);
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(2);
expect(user.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
expect(user.notifications[1].data.message).to.equal(t('userHasRequestedTaskApproval', {
user: member.auth.local.username,
taskName: updatedTask.text,
taskId: updatedTask._id,
}));
expect(user.notifications[1].data.groupId).to.equal(guild._id);
expect(member2.notifications.length).to.equal(1);
expect(member2.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
expect(member2.notifications[0].data.message).to.equal(t('userHasRequestedTaskApproval', {
user: member.auth.local.username,
taskName: updatedTask.text,
taskId: updatedTask._id,
}));
expect(member2.notifications[0].data.groupId).to.equal(guild._id);
});
it('errors when approval has already been requested', async () => {
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
@@ -1,16 +1,29 @@
import {
generateUser,
generateGroup,
createAndPopulateGroup,
translate as t,
} from '../../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
describe('POST /tasks/group/:groupid', () => {
let user, guild;
let user, guild, manager;
let groupName = 'Test Public Guild';
let groupType = 'guild';
beforeEach(async () => {
user = await generateUser({balance: 1});
guild = await generateGroup(user, {type: 'guild'});
let { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: groupName,
type: groupType,
privacy: 'private',
},
members: 1,
});
guild = group;
user = groupLeader;
manager = members[0];
});
it('returns error when group is not found', async () => {
@@ -116,4 +129,27 @@ describe('POST /tasks/group/:groupid', () => {
expect(task.everyX).to.eql(5);
expect(new Date(task.startDate)).to.eql(now);
});
it('allows a manager to add a group task', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: manager._id,
});
let task = await manager.post(`/tasks/group/${guild._id}`, {
text: 'test habit',
type: 'habit',
up: false,
down: true,
notes: 1976,
});
let groupTask = await manager.get(`/tasks/group/${guild._id}`);
expect(groupTask[0].group.id).to.equal(guild._id);
expect(task.text).to.eql('test habit');
expect(task.notes).to.eql('1976');
expect(task.type).to.eql('habit');
expect(task.up).to.eql(false);
expect(task.down).to.eql(true);
});
});
@@ -6,7 +6,7 @@ import {
import { v4 as generateUUID } from 'uuid';
import { find } from 'lodash';
describe('POST /tasks/:taskId', () => {
describe('POST /tasks/:taskId/assign/:memberId', () => {
let user, guild, member, member2, task;
function findAssignedTask (memberTask) {
@@ -130,4 +130,19 @@ describe('POST /tasks/:taskId', () => {
expect(member1SyncedTask).to.exist;
expect(member2SyncedTask).to.exist;
});
it('allows a manager to assign tasks', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
let groupTask = await member2.get(`/tasks/group/${guild._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
expect(groupTask[0].group.assignedUsers).to.contain(member._id);
expect(syncedTask).to.exist;
});
});
@@ -114,4 +114,19 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
expect(groupTask[0].group.assignedUsers).to.contain(member2._id);
expect(member2SyncedTask).to.exist;
});
it('allows a manager to unassign a user from a task', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/unassign/${member._id}`);
let groupTask = await member2.get(`/tasks/group/${guild._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
expect(groupTask[0].group.assignedUsers).to.not.contain(member._id);
expect(syncedTask).to.not.exist;
});
});
@@ -89,4 +89,25 @@ describe('PUT /tasks/:id', () => {
expect(member2SyncedTask.up).to.eql(false);
expect(member2SyncedTask.down).to.eql(false);
});
it('updates the linked tasks', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.put(`/tasks/${task._id}`, {
text: 'some new text',
up: false,
down: false,
notes: 'some new notes',
});
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
expect(syncedTask.text).to.eql('some new text');
expect(syncedTask.up).to.eql(false);
expect(syncedTask.down).to.eql(false);
});
});
@@ -16,6 +16,7 @@ import {
sha1MakeSalt,
sha1Encrypt as sha1EncryptPassword,
} from '../../../../../website/server/libs/password';
import * as email from '../../../../../website/server/libs/email';
describe('DELETE /user', () => {
let user;
@@ -25,7 +26,7 @@ describe('DELETE /user', () => {
user = await generateUser({balance: 10});
});
it('returns an errors if password is wrong', async () => {
it('returns an error if password is wrong', async () => {
await expect(user.del('/user', {
password: 'wrong-password',
})).to.eventually.be.rejected.and.eql({
@@ -35,6 +36,33 @@ describe('DELETE /user', () => {
});
});
it('returns an error if password is not supplied', async () => {
await expect(user.del('/user', {
password: '',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingPassword'),
});
});
it('returns an error if excessive feedback is supplied', async () => {
let feedbackText = 'spam feedback ';
let feedback = feedbackText;
while (feedback.length < 10000) {
feedback = feedback + feedbackText;
}
await expect(user.del('/user', {
password,
feedback,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Account deletion feedback is limited to 10,000 characters. For lengthy feedback, email admin@habitica.com.',
});
});
it('returns an error if user has active subscription', async () => {
let userWithSubscription = await generateUser({'purchased.plan.customerId': 'fake-customer-id'});
@@ -96,6 +124,32 @@ describe('DELETE /user', () => {
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
});
it('sends feedback to the admin email', async () => {
sandbox.spy(email, 'sendTxn');
let feedback = 'Reasons for Deletion';
await user.del('/user', {
password,
feedback,
});
expect(email.sendTxn).to.be.calledOnce;
sandbox.restore();
});
it('does not send email if no feedback is supplied', async () => {
sandbox.spy(email, 'sendTxn');
await user.del('/user', {
password,
});
expect(email.sendTxn).to.not.be.called;
sandbox.restore();
});
it('deletes the user with a legacy sha1 password', async () => {
let textPassword = 'mySecretPassword';
let salt = sha1MakeSalt();
@@ -221,6 +221,27 @@ describe('POST /user/class/cast/:spellId', () => {
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);
});
// TODO find a way to have sinon working in integration tests
// it doesn't work when tests are running separately from server
it('passes correct target to spell when targetType === \'task\'');
@@ -1,5 +1,6 @@
import {
generateUser,
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-integration/v3';
@@ -31,4 +32,70 @@ describe('POST /user/purchase/:type/:key', () => {
expect(user.items[type][key]).to.equal(1);
});
it('can convert gold to gems if subscribed', async () => {
let oldBalance = user.balance;
await user.update({
'purchased.plan.customerId': 'group-plan',
'stats.gp': 1000,
});
await user.post('/user/purchase/gems/gem');
await user.sync();
expect(user.balance).to.equal(oldBalance + 0.25);
});
it('leader can convert gold to gems even if the group plan prevents it', async () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'test',
type: 'guild',
privacy: 'private',
},
});
await group.update({
'leaderOnly.getGems': true,
'purchased.plan.customerId': 123,
});
await groupLeader.sync();
let oldBalance = groupLeader.balance;
await groupLeader.update({
'purchased.plan.customerId': 'group-plan',
'stats.gp': 1000,
});
await groupLeader.post('/user/purchase/gems/gem');
await groupLeader.sync();
expect(groupLeader.balance).to.equal(oldBalance + 0.25);
});
it('cannot convert gold to gems if the group plan prevents it', async () => {
let { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'test',
type: 'guild',
privacy: 'private',
},
members: 1,
});
await group.update({
'leaderOnly.getGems': true,
'purchased.plan.customerId': 123,
});
let oldBalance = members[0].balance;
await members[0].update({
'purchased.plan.customerId': 'group-plan',
'stats.gp': 1000,
});
await expect(members[0].post('/user/purchase/gems/gem'))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('groupPolicyCannotGetGems'),
});
await members[0].sync();
expect(members[0].balance).to.equal(oldBalance);
});
});
@@ -251,7 +251,6 @@ describe('POST /user/auth/local/register', () => {
confirmPassword: password,
});
await expect(getProperty('users', user._id, '_ABtest')).to.eventually.be.a('string');
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
});
@@ -83,7 +83,7 @@ describe('POST /user/auth/social', () => {
network,
});
await expect(getProperty('users', user._id, '_ABtest')).to.eventually.be.a('string');
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
});
});
@@ -139,7 +139,7 @@ describe('POST /user/auth/social', () => {
network,
});
await expect(getProperty('users', user._id, '_ABtest')).to.eventually.be.a('string');
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
});
});
});
+38 -3
View File
@@ -102,6 +102,7 @@ describe('Amazon Payments', () => {
});
it('should purchase gems', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
await amzLib.checkout({user, orderReferenceId, headers});
expect(paymentBuyGemsStub).to.be.calledOnce;
@@ -111,22 +112,52 @@ describe('Amazon Payments', () => {
headers,
});
expectAmazonStubs();
expect(user.canGetGems).to.be.calledOnce;
user.canGetGems.restore();
});
it('should gift gems', async () => {
it('should error if gem amount is too low', async () => {
let receivingUser = new User();
receivingUser.save();
let gift = {
type: 'gems',
gems: {
amount: 16,
amount: 0,
uuid: receivingUser._id,
},
};
await expect(amzLib.checkout({gift, user, orderReferenceId, headers}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
message: 'Amount must be at least 1.',
name: 'BadRequest',
});
});
it('should error if user cannot get gems gems', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
await expect(amzLib.checkout({user, orderReferenceId, headers})).to.eventually.be.rejected.and.to.eql({
httpCode: 401,
message: i18n.t('groupPolicyCannotGetGems'),
name: 'NotAuthorized',
});
user.canGetGems.restore();
});
it('should gift gems', async () => {
let receivingUser = new User();
await receivingUser.save();
let gift = {
type: 'gems',
uuid: receivingUser._id,
gems: {
amount: 16,
},
};
amount = 16 / 4;
await amzLib.checkout({gift, user, orderReferenceId, headers});
gift.member = receivingUser;
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,
@@ -525,6 +556,7 @@ describe('Amazon Payments', () => {
nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: amzLib.constants.PAYMENT_METHOD,
headers,
cancellationReason: undefined,
});
expectAmazonStubs();
});
@@ -555,6 +587,7 @@ describe('Amazon Payments', () => {
nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: amzLib.constants.PAYMENT_METHOD,
headers,
cancellationReason: undefined,
});
amzLib.closeBillingAgreement.restore();
});
@@ -593,6 +626,7 @@ describe('Amazon Payments', () => {
nextBill: moment(group.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: amzLib.constants.PAYMENT_METHOD,
headers,
cancellationReason: undefined,
});
expectAmazonStubs();
});
@@ -623,6 +657,7 @@ describe('Amazon Payments', () => {
nextBill: moment(group.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: amzLib.constants.PAYMENT_METHOD,
headers,
cancellationReason: undefined,
});
amzLib.closeBillingAgreement.restore();
});
@@ -57,7 +57,20 @@ describe('Apple Payments', () => {
});
});
it('errors if the user cannot purchase gems', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
await expect(applePayments.verifyGemPurchase(user, receipt, headers))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('groupPolicyCannotGetGems'),
});
user.canGetGems.restore();
});
it('purchases gems', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
await applePayments.verifyGemPurchase(user, receipt, headers);
expect(iapSetupStub).to.be.calledOnce;
@@ -74,6 +87,8 @@ describe('Apple Payments', () => {
amount: 5.25,
headers,
});
expect(user.canGetGems).to.be.calledOnce;
user.canGetGems.restore();
});
});
+60
View File
@@ -79,6 +79,13 @@ describe('cron', () => {
expect(user.purchased.plan.gemsBought).to.equal(0);
});
it('resets plan.gemsBought on a new month if user does not have purchased.plan.dateUpdated', () => {
user.purchased.plan.gemsBought = 10;
user.purchased.plan.dateUpdated = undefined;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.gemsBought).to.equal(0);
});
it('does not reset plan.gemsBought within the month', () => {
let clock = sinon.useFakeTimers(moment().startOf('month').add(2, 'days').unix());
user.purchased.plan.dateUpdated = moment().startOf('month').toDate();
@@ -313,6 +320,24 @@ describe('cron', () => {
expect(tasksByType.dailys[0].completed).to.be.false;
expect(user.stats.hp).to.equal(healthBefore);
});
it('sets isDue for daily', () => {
let daily = {
text: 'test daily',
type: 'daily',
frequency: 'daily',
everyX: 5,
startDate: new Date(),
};
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line new-cap
tasksByType.dailys.push(task);
tasksByType.dailys[0].completed = true;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].isDue).to.be.exist;
});
});
describe('todos', () => {
@@ -358,6 +383,22 @@ describe('cron', () => {
};
});
it('computes isDue', () => {
tasksByType.dailys[0].frequency = 'daily';
tasksByType.dailys[0].everyX = 5;
tasksByType.dailys[0].startDate = moment().add(1, 'days').toDate();
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].isDue).to.be.false;
});
it('computes nextDue', () => {
tasksByType.dailys[0].frequency = 'daily';
tasksByType.dailys[0].everyX = 5;
tasksByType.dailys[0].startDate = moment().add(1, 'days').toDate();
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].nextDue.length).to.eql(6);
});
it('should add history', () => {
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].history).to.be.lengthOf(1);
@@ -442,6 +483,25 @@ describe('cron', () => {
expect(progress.down).to.equal(-1);
});
it('should do damage for only yesterday\'s dailies', () => {
daysMissed = 3;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
let daily = {
text: 'test daily',
type: 'daily',
};
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line new-cap
tasksByType.dailys.push(task);
tasksByType.dailys[1].startDate = moment(new Date()).subtract({days: 2});
tasksByType.dailys[1].everyX = 2;
tasksByType.dailys[1].frequency = 'daily';
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.hp).to.equal(48);
});
});
describe('habits', () => {
@@ -63,7 +63,21 @@ describe('Google Payments', () => {
});
});
it('should throw an error if user cannot purchase gems', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
await expect(googlePayments.verifyGemPurchase(user, receipt, signature, headers))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('groupPolicyCannotGetGems'),
});
user.canGetGems.restore();
});
it('purchases gems', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
await googlePayments.verifyGemPurchase(user, receipt, signature, headers);
expect(iapSetupStub).to.be.calledOnce;
@@ -82,6 +96,8 @@ describe('Google Payments', () => {
amount: 5.25,
headers,
});
expect(user.canGetGems).to.be.calledOnce;
user.canGetGems.restore();
});
});
+40
View File
@@ -16,6 +16,7 @@ describe('payments/index', () => {
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
await user.save();
group = generateGroup({
name: 'test group',
@@ -504,6 +505,18 @@ describe('payments/index', () => {
expect(daysTillTermination).to.be.within(13, 15);
});
it('terminates at next billing date even if dateUpdated is prior to now', async () => {
data.nextBill = moment().add({ days: 15 });
data.user.purchased.plan.dateUpdated = moment().subtract({ days: 10 });
await api.cancelSubscription(data);
let now = new Date();
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
expect(daysTillTermination).to.be.within(13, 15);
});
it('resets plan.extraMonths', async () => {
user.purchased.plan.extraMonths = 5;
@@ -653,5 +666,32 @@ describe('payments/index', () => {
expect(updatedUser.items.pets['Jackalope-RoyalPurple']).to.eql(5);
});
it('saves previously unused Mystery Items and Hourglasses for an expired subscription', async () => {
let planExpirationDate = new Date();
planExpirationDate.setDate(planExpirationDate.getDate() - 2);
let mysteryItem = 'item';
let mysteryItems = [mysteryItem];
let consecutive = {
trinkets: 3,
};
// set expired plan with unused items
plan.mysteryItems = mysteryItems;
plan.consecutive = consecutive;
plan.dateCreated = planExpirationDate;
plan.dateTerminated = planExpirationDate;
plan.customerId = null;
user.purchased.plan = plan;
await user.save();
await api.addSubToGroupUser(user, group);
let updatedUser = await User.findById(user._id).exec();
expect(updatedUser.purchased.plan.mysteryItems[0]).to.eql(mysteryItem);
expect(updatedUser.purchased.plan.consecutive.trinkets).to.equal(consecutive.trinkets);
});
});
});
@@ -138,7 +138,7 @@ describe('Canceling a subscription for group', () => {
]);
});
it('prevents non group leader from manging subscription', async () => {
it('prevents non group leader from managing subscription', async () => {
let groupMember = new User();
data.user = groupMember;
data.groupId = group._id;
@@ -1,5 +1,6 @@
import moment from 'moment';
import stripeModule from 'stripe';
import nconf from 'nconf';
import * as sender from '../../../../../../../website/server/libs/email';
import * as api from '../../../../../../../website/server/libs/payments';
@@ -12,17 +13,24 @@ import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
describe('Purchasing a subscription for group', () => {
describe('Purchasing a group plan for group', () => {
const EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_GOOGLE = 'Google_subscription';
const EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_IOS = 'iOS_subscription';
const EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NORMAL = 'normal_subscription';
const EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NONE = 'no_subscription';
let plan, group, user, data;
let stripe = stripeModule('test');
let groupLeaderName = 'sender';
let groupName = 'test group';
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
user.profile.name = groupLeaderName;
await user.save();
group = generateGroup({
name: 'test group',
name: groupName,
type: 'guild',
privacy: 'public',
leader: user._id,
@@ -81,7 +89,7 @@ describe('Purchasing a subscription for group', () => {
sender.sendTxn.restore();
});
it('creates a subscription', async () => {
it('creates a group plan', async () => {
expect(group.purchased.plan.planId).to.not.exist;
data.groupId = group._id;
@@ -157,7 +165,7 @@ describe('Purchasing a subscription for group', () => {
expect(updatedLeader.items.mounts['Jackalope-RoyalPurple']).to.be.true;
});
it('sends an email to members of group', async () => {
it('sends an email to member of group who was not a subscriber', async () => {
let recipient = new User();
recipient.profile.name = 'recipient';
recipient.guilds.push(group._id);
@@ -169,11 +177,181 @@ describe('Purchasing a subscription for group', () => {
expect(sender.sendTxn).to.be.calledTwice;
expect(sender.sendTxn.firstCall.args[0]._id).to.equal(recipient._id);
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-joining');
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-join');
expect(sender.sendTxn.firstCall.args[2]).to.eql([
{name: 'LEADER', content: user.profile.name},
{name: 'GROUP_NAME', content: group.name},
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NONE},
]);
// confirm that the other email sent is appropriate:
expect(sender.sendTxn.secondCall.args[0]._id).to.equal(group.leader);
expect(sender.sendTxn.secondCall.args[1]).to.equal('group-subscription-begins');
});
it('sends one email to subscribed member of group, stating subscription is cancelled (Stripe)', async () => {
let recipient = new User();
recipient.profile.name = 'recipient';
plan.key = 'basic_earned';
plan.paymentMethod = stripePayments.constants.PAYMENT_METHOD;
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
data.groupId = group._id;
await api.createSubscription(data);
expect(sender.sendTxn).to.be.calledTwice;
expect(sender.sendTxn.firstCall.args[0]._id).to.equal(recipient._id);
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-join');
expect(sender.sendTxn.firstCall.args[2]).to.eql([
{name: 'LEADER', content: user.profile.name},
{name: 'GROUP_NAME', content: group.name},
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NORMAL},
]);
// confirm that the other email sent is not a cancel-subscription email:
expect(sender.sendTxn.secondCall.args[0]._id).to.equal(group.leader);
expect(sender.sendTxn.secondCall.args[1]).to.equal('group-subscription-begins');
});
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'},
},
});
let recipient = new User();
recipient.profile.name = 'recipient';
plan.planId = 'basic_earned';
plan.paymentMethod = amzLib.constants.PAYMENT_METHOD;
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
data.groupId = group._id;
await api.createSubscription(data);
expect(sender.sendTxn).to.be.calledTwice;
expect(sender.sendTxn.firstCall.args[0]._id).to.equal(recipient._id);
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-join');
expect(sender.sendTxn.firstCall.args[2]).to.eql([
{name: 'LEADER', content: user.profile.name},
{name: 'GROUP_NAME', content: group.name},
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NORMAL},
]);
// confirm that the other email sent is not a cancel-subscription email:
expect(sender.sendTxn.secondCall.args[0]._id).to.equal(group.leader);
expect(sender.sendTxn.secondCall.args[1]).to.equal('group-subscription-begins');
amzLib.getBillingAgreementDetails.restore();
});
it('sends one email to subscribed member of group, stating subscription is cancelled (PayPal)', async () => {
sinon.stub(paypalPayments, 'paypalBillingAgreementCancel').returnsPromise().resolves({});
sinon.stub(paypalPayments, 'paypalBillingAgreementGet')
.returnsPromise().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
},
});
let recipient = new User();
recipient.profile.name = 'recipient';
plan.planId = 'basic_earned';
plan.paymentMethod = paypalPayments.constants.PAYMENT_METHOD;
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
data.groupId = group._id;
await api.createSubscription(data);
expect(sender.sendTxn).to.be.calledTwice;
expect(sender.sendTxn.firstCall.args[0]._id).to.equal(recipient._id);
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-join');
expect(sender.sendTxn.firstCall.args[2]).to.eql([
{name: 'LEADER', content: user.profile.name},
{name: 'GROUP_NAME', content: group.name},
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NORMAL},
]);
// confirm that the other email sent is not a cancel-subscription email:
expect(sender.sendTxn.secondCall.args[0]._id).to.equal(group.leader);
expect(sender.sendTxn.secondCall.args[1]).to.equal('group-subscription-begins');
paypalPayments.paypalBillingAgreementGet.restore();
paypalPayments.paypalBillingAgreementCancel.restore();
});
it('sends appropriate emails when subscribed member of group must manually cancel recurring Android subscription', async () => {
const TECH_ASSISTANCE_EMAIL = nconf.get('TECH_ASSISTANCE_EMAIL');
plan.customerId = 'random';
plan.paymentMethod = api.constants.GOOGLE_PAYMENT_METHOD;
let recipient = new User();
recipient.profile.name = 'recipient';
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
user.guilds.push(group._id);
await user.save();
data.groupId = group._id;
await api.createSubscription(data);
expect(sender.sendTxn).to.be.calledFourTimes;
expect(sender.sendTxn.args[0][0]._id).to.equal(TECH_ASSISTANCE_EMAIL);
expect(sender.sendTxn.args[0][1]).to.equal('admin-user-subscription-details');
expect(sender.sendTxn.args[1][0]._id).to.equal(recipient._id);
expect(sender.sendTxn.args[1][1]).to.equal('group-member-join');
expect(sender.sendTxn.args[1][2]).to.eql([
{name: 'LEADER', content: groupLeaderName},
{name: 'GROUP_NAME', content: groupName},
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_GOOGLE},
]);
expect(sender.sendTxn.args[2][0]._id).to.equal(group.leader);
expect(sender.sendTxn.args[2][1]).to.equal('group-member-join');
expect(sender.sendTxn.args[3][0]._id).to.equal(group.leader);
expect(sender.sendTxn.args[3][1]).to.equal('group-subscription-begins');
});
it('sends appropriate emails when subscribed member of group must manually cancel recurring iOS subscription', async () => {
const TECH_ASSISTANCE_EMAIL = nconf.get('TECH_ASSISTANCE_EMAIL');
plan.customerId = 'random';
plan.paymentMethod = api.constants.IOS_PAYMENT_METHOD;
let recipient = new User();
recipient.profile.name = 'recipient';
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
user.guilds.push(group._id);
await user.save();
data.groupId = group._id;
await api.createSubscription(data);
expect(sender.sendTxn).to.be.calledFourTimes;
expect(sender.sendTxn.args[0][0]._id).to.equal(TECH_ASSISTANCE_EMAIL);
expect(sender.sendTxn.args[0][1]).to.equal('admin-user-subscription-details');
expect(sender.sendTxn.args[1][0]._id).to.equal(recipient._id);
expect(sender.sendTxn.args[1][1]).to.equal('group-member-join');
expect(sender.sendTxn.args[1][2]).to.eql([
{name: 'LEADER', content: groupLeaderName},
{name: 'GROUP_NAME', content: groupName},
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_IOS},
]);
expect(sender.sendTxn.args[2][0]._id).to.equal(group.leader);
expect(sender.sendTxn.args[2][1]).to.equal('group-member-join');
expect(sender.sendTxn.args[3][0]._id).to.equal(group.leader);
expect(sender.sendTxn.args[3][1]).to.equal('group-subscription-begins');
});
it('adds months to members with existing gift subscription', async () => {
@@ -333,7 +511,7 @@ describe('Purchasing a subscription for group', () => {
});
it('adds months to members with existing recurring subscription (Android)');
it('adds months to members with existing recurring subscription (iOs)');
it('adds months to members with existing recurring subscription (iOS)');
it('adds months to members who already cancelled but not yet terminated recurring subscription', async () => {
let recipient = new User();
@@ -418,7 +596,7 @@ describe('Purchasing a subscription for group', () => {
let updatedUser = await User.findById(recipient._id).exec();
expect(updatedUser.purchased.plan.extraMonths).to.within(7, 8);
expect(updatedUser.purchased.plan.extraMonths).to.within(7, 9);
});
it('adds months to members with existing recurring subscription and ignores existing negative extraMonths', async () => {
@@ -603,6 +781,64 @@ describe('Purchasing a subscription for group', () => {
expect(updatedUser.purchased.plan.dateCreated).to.exist;
});
it('does not modify a user with an Android subscription', async () => {
plan.customerId = 'random';
plan.paymentMethod = api.constants.GOOGLE_PAYMENT_METHOD;
let recipient = new User();
recipient.profile.name = 'recipient';
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
user.guilds.push(group._id);
await user.save();
data.groupId = group._id;
await api.createSubscription(data);
let updatedUser = await User.findById(recipient._id).exec();
expect(updatedUser.purchased.plan.planId).to.eql('basic_3mo');
expect(updatedUser.purchased.plan.customerId).to.eql('random');
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
expect(updatedUser.purchased.plan.paymentMethod).to.eql(api.constants.GOOGLE_PAYMENT_METHOD);
expect(updatedUser.purchased.plan.extraMonths).to.eql(0);
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
expect(updatedUser.purchased.plan.lastBillingDate).to.exist;
expect(updatedUser.purchased.plan.dateCreated).to.exist;
});
it('does not modify a user with an iOS subscription', async () => {
plan.customerId = 'random';
plan.paymentMethod = api.constants.IOS_PAYMENT_METHOD;
let recipient = new User();
recipient.profile.name = 'recipient';
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
user.guilds.push(group._id);
await user.save();
data.groupId = group._id;
await api.createSubscription(data);
let updatedUser = await User.findById(recipient._id).exec();
expect(updatedUser.purchased.plan.planId).to.eql('basic_3mo');
expect(updatedUser.purchased.plan.customerId).to.eql('random');
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
expect(updatedUser.purchased.plan.paymentMethod).to.eql(api.constants.IOS_PAYMENT_METHOD);
expect(updatedUser.purchased.plan.extraMonths).to.eql(0);
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
expect(updatedUser.purchased.plan.lastBillingDate).to.exist;
expect(updatedUser.purchased.plan.dateCreated).to.exist;
});
it('updates a user with a cancelled but active group subscription', async () => {
plan.key = 'basic_earned';
plan.customerId = api.constants.GROUP_PLAN_CUSTOMER_ID;
+36 -3
View File
@@ -61,23 +61,54 @@ describe('Paypal Payments', () => {
});
it('creates a link for gem purchases', async () => {
let link = await paypalPayments.checkout();
let link = await paypalPayments.checkout({user: new User()});
expect(paypalPaymentCreateStub).to.be.calledOnce;
expect(paypalPaymentCreateStub).to.be.calledWith(getPaypalCreateOptions('Habitica Gems', 5.00));
expect(link).to.eql(approvalHerf);
});
it('creates a link for gifting gems', async () => {
it('should error if gem amount is too low', async () => {
let receivingUser = new User();
receivingUser.save();
let gift = {
type: 'gems',
gems: {
amount: 16,
amount: 0,
uuid: receivingUser._id,
},
};
await expect(paypalPayments.checkout({gift}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
message: 'Amount must be at least 1.',
name: 'BadRequest',
});
});
it('should error if the user cannot get gems', async () => {
let user = new User();
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
await expect(paypalPayments.checkout({user})).to.eventually.be.rejected.and.to.eql({
httpCode: 401,
message: i18n.t('groupPolicyCannotGetGems'),
name: 'NotAuthorized',
});
});
it('creates a link for gifting gems', async () => {
let receivingUser = new User();
await receivingUser.save();
let gift = {
type: 'gems',
uuid: receivingUser._id,
gems: {
amount: 16,
},
};
let link = await paypalPayments.checkout({gift});
expect(paypalPaymentCreateStub).to.be.calledOnce;
@@ -447,6 +478,7 @@ describe('Paypal Payments', () => {
groupId,
paymentMethod: 'Paypal',
nextBill: nextBillingDate,
cancellationReason: undefined,
});
});
@@ -464,6 +496,7 @@ describe('Paypal Payments', () => {
groupId: group._id,
paymentMethod: 'Paypal',
nextBill: nextBillingDate,
cancellationReason: undefined,
});
});
});
+56 -3
View File
@@ -48,7 +48,57 @@ describe('Stripe Payments', () => {
payments.createSubscription.restore();
});
it('should error if gem amount is too low', async () => {
let receivingUser = new User();
receivingUser.save();
gift = {
type: 'gems',
gems: {
amount: 0,
uuid: receivingUser._id,
},
};
await expect(stripePayments.checkout({
token,
user,
gift,
groupId,
email,
headers,
coupon,
}, stripe))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
message: 'Amount must be at least 1.',
name: 'BadRequest',
});
});
it('should error if user cannot get gems', async () => {
gift = undefined;
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
await expect(stripePayments.checkout({
token,
user,
gift,
groupId,
email,
headers,
coupon,
}, stripe)).to.eventually.be.rejected.and.to.eql({
httpCode: 401,
message: i18n.t('groupPolicyCannotGetGems'),
name: 'NotAuthorized',
});
});
it('should purchase gems', async () => {
gift = undefined;
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
await stripePayments.checkout({
token,
user,
@@ -73,16 +123,18 @@ describe('Stripe Payments', () => {
paymentMethod: 'Stripe',
gift,
});
expect(user.canGetGems).to.be.calledOnce;
user.canGetGems.restore();
});
it('should gift gems', async () => {
let receivingUser = new User();
receivingUser.save();
await receivingUser.save();
gift = {
type: 'gems',
uuid: receivingUser._id,
gems: {
amount: 16,
uuid: receivingUser._id,
},
};
@@ -96,7 +148,6 @@ describe('Stripe Payments', () => {
coupon,
}, stripe);
gift.member = receivingUser;
expect(stripeChargeStub).to.be.calledOnce;
expect(stripeChargeStub).to.be.calledWith({
amount: '400',
@@ -683,6 +734,7 @@ describe('Stripe Payments', () => {
groupId: undefined,
nextBill: currentPeriodEndTimeStamp * 1000, // timestamp in seconds
paymentMethod: 'Stripe',
cancellationReason: undefined,
});
});
@@ -702,6 +754,7 @@ describe('Stripe Payments', () => {
groupId,
nextBill: currentPeriodEndTimeStamp * 1000, // timestamp in seconds
paymentMethod: 'Stripe',
cancellationReason: undefined,
});
});
});
+101 -2
View File
@@ -2,7 +2,13 @@ import moment from 'moment';
import { v4 as generateUUID } from 'uuid';
import validator from 'validator';
import { sleep } from '../../../../helpers/api-unit.helper';
import { model as Group, INVITES_LIMIT } from '../../../../../website/server/models/group';
import {
SPAM_MESSAGE_LIMIT,
SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
SPAM_WINDOW_LENGTH,
INVITES_LIMIT,
model as Group,
} from '../../../../../website/server/models/group';
import { model as User } from '../../../../../website/server/models/user';
import { quests as questScrolls } from '../../../../../website/common/script/content';
import { groupChatReceivedWebhook } from '../../../../../website/server/libs/webhook';
@@ -222,7 +228,7 @@ describe('Group Model', () => {
});
it('applies damage only to participating members of party even under buggy conditions', async () => {
// stops unfair damage from mbugs like https://github.com/HabitRPG/habitrpg/issues/7653
// stops unfair damage from mbugs like https://github.com/HabitRPG/habitica/issues/7653
party.quest.members = {
[questLeader._id]: true,
[participatingMember._id]: true,
@@ -595,6 +601,99 @@ describe('Group Model', () => {
});
});
describe('#checkChatSpam', () => {
let testUser, testTime, tavern;
let testUserID = '1';
beforeEach(async () => {
testTime = Date.now();
tavern = new Group({
name: 'test tavern',
type: 'guild',
privacy: 'public',
});
tavern._id = TAVERN_ID;
testUser = {
_id: testUserID,
};
});
function generateTestMessage (overrides = {}) {
return Object.assign({}, {
text: 'test message',
uuid: testUserID,
timestamp: testTime,
}, overrides);
}
it('group that is not the tavern returns false, while tavern returns true', async () => {
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
party.chat.push(generateTestMessage());
}
expect(party.checkChatSpam(testUser)).to.eql(false);
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
tavern.chat.push(generateTestMessage());
}
expect(tavern.checkChatSpam(testUser)).to.eql(true);
});
it('high enough contributor returns false', async () => {
let highContributor = testUser;
highContributor.contributor = {
level: SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
};
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
tavern.chat.push(generateTestMessage());
}
expect(tavern.checkChatSpam(highContributor)).to.eql(false);
});
it('chat with no messages returns false', async () => {
expect(tavern.chat.length).to.eql(0);
expect(tavern.checkChatSpam(testUser)).to.eql(false);
});
it('user has not reached limit but another one has returns false', async () => {
let otherUserID = '2';
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
tavern.chat.push(generateTestMessage({uuid: otherUserID}));
}
expect(tavern.checkChatSpam(testUser)).to.eql(false);
});
it('user messages is less than the limit returns false', async () => {
for (let i = 0; i < SPAM_MESSAGE_LIMIT - 1; i++) {
tavern.chat.push(generateTestMessage());
}
expect(tavern.checkChatSpam(testUser)).to.eql(false);
});
it('user has reached the message limit outside of window returns false', async () => {
for (let i = 0; i < SPAM_MESSAGE_LIMIT - 1; i++) {
tavern.chat.push(generateTestMessage());
}
let earlierTimestamp = testTime - SPAM_WINDOW_LENGTH - 1;
tavern.chat.push(generateTestMessage({timestamp: earlierTimestamp}));
expect(tavern.checkChatSpam(testUser)).to.eql(false);
});
it('user has posted too many messages in limit returns true', async () => {
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
tavern.chat.push(generateTestMessage());
}
expect(tavern.checkChatSpam(testUser)).to.eql(true);
});
});
describe('#leaveGroup', () => {
it('removes user from group quest', async () => {
party.quest.members = {
+70
View File
@@ -1,6 +1,7 @@
import Bluebird from 'bluebird';
import moment from 'moment';
import { model as User } from '../../../../../website/server/models/user';
import { model as Group } from '../../../../../website/server/models/group';
import common from '../../../../../website/common';
describe('User Model', () => {
@@ -179,6 +180,75 @@ describe('User Model', () => {
});
});
context('canGetGems', () => {
let user;
let group;
beforeEach(() => {
user = new User();
let leader = new User();
group = new Group({
name: 'test',
type: 'guild',
privacy: 'private',
leader: leader._id,
});
});
it('returns true if user is not subscribed', async () => {
expect(await user.canGetGems()).to.equal(true);
});
it('returns true if user is not subscribed with a group plan', async () => {
user.purchased.plan.customerId = 123;
expect(await user.canGetGems()).to.equal(true);
});
it('returns true if user is subscribed with a group plan', async () => {
user.purchased.plan.customerId = 'group-plan';
expect(await user.canGetGems()).to.equal(true);
});
it('returns true if user is part of a group', async () => {
user.guilds.push(group._id);
expect(await user.canGetGems()).to.equal(true);
});
it('returns true if user is part of a group with a subscription', async () => {
user.guilds.push(group._id);
user.purchased.plan.customerId = 'group-plan';
group.purchased.plan.customerId = 123;
await group.save();
expect(await user.canGetGems()).to.equal(true);
});
it('returns true if leader is part of a group with a subscription and canGetGems: false', async () => {
user.guilds.push(group._id);
user.purchased.plan.customerId = 'group-plan';
group.purchased.plan.customerId = 123;
group.leader = user._id;
group.leaderOnly.getGems = true;
await group.save();
expect(await user.canGetGems()).to.equal(true);
});
it('returns true if user is part of a group with no subscription but canGetGems: false', async () => {
user.guilds.push(group._id);
user.purchased.plan.customerId = 'group-plan';
group.leaderOnly.getGems = true;
await group.save();
expect(await user.canGetGems()).to.equal(true);
});
it('returns false if user is part of a group with a subscription and canGetGems: false', async () => {
user.guilds.push(group._id);
user.purchased.plan.customerId = 'group-plan';
group.purchased.plan.customerId = 123;
group.leaderOnly.getGems = true;
await group.save();
expect(await user.canGetGems()).to.equal(false);
});
});
context('hasNotCancelled', () => {
let user;
beforeEach(() => {
@@ -112,6 +112,41 @@ describe('Groups Controller', function() {
});
});
describe('isAbleToEditGroup', () => {
var guild;
beforeEach(() => {
user.contributor = {};
guild = specHelper.newGroup({
_id: 'unique-guild-id',
type: 'guild',
members: ['not-user-id'],
$save: sandbox.spy(),
});
});
it('returns true if user is an admin', () => {
guild.leader = 'not-user-id';
user.contributor.admin = true;
expect(scope.isAbleToEditGroup(guild)).to.be.ok;
});
it('returns true if user is group leader', () => {
guild.leader = {_id: user._id}
expect(scope.isAbleToEditGroup(guild)).to.be.ok;
});
it('returns false is user is not a leader or admin', () => {
expect(scope.isAbleToEditGroup(guild)).to.not.be.ok;
});
it('returns false is user is an admin but group is a party', () => {
guild.type = 'party';
user.contributor.admin = true;
expect(scope.isAbleToEditGroup(guild)).to.not.be.ok;
});
});
describe('editGroup', () => {
var guild;
@@ -1,63 +1,63 @@
describe('Group Tasks Meta Actions Controller', () => {
let rootScope, scope, user, userSerivce;
beforeEach(() => {
module(function($provide) {
$provide.value('User', {});
});
inject(($rootScope, $controller) => {
rootScope = $rootScope;
user = specHelper.newUser();
user._id = "unique-user-id";
userSerivce = {user: user};
scope = $rootScope.$new();
scope.task = {
group: {
assignedUsers: [],
approval: {
required: false,
}
},
};
scope.task._edit = angular.copy(scope.task);
$controller('GroupTaskActionsCtrl', {$scope: scope, User: userSerivce});
});
});
describe('toggleTaskRequiresApproval', function () {
it('toggles task approval required field from false to true', function () {
scope.toggleTaskRequiresApproval();
expect(scope.task._edit.group.approval.required).to.be.true;
});
it('toggles task approval required field from true to false', function () {
scope.task._edit.group.approval.required = true;
scope.toggleTaskRequiresApproval();
expect(scope.task._edit.group.approval.required).to.be.false;
});
});
describe('assign events', function () {
it('adds a group member to assigned users on "addedGroupMember" event ', () => {
var testId = 'test-id';
rootScope.$broadcast('addedGroupMember', testId);
expect(scope.task.group.assignedUsers).to.contain(testId);
expect(scope.task._edit.group.assignedUsers).to.contain(testId);
});
it('removes a group member to assigned users on "addedGroupMember" event ', () => {
var testId = 'test-id';
scope.task.group.assignedUsers.push(testId);
scope.task._edit.group.assignedUsers.push(testId);
rootScope.$broadcast('removedGroupMember', testId);
expect(scope.task.group.assignedUsers).to.not.contain(testId);
expect(scope.task._edit.group.assignedUsers).to.not.contain(testId);
});
});
});
describe('Group Tasks Meta Actions Controller', () => {
let rootScope, scope, user, userSerivce;
beforeEach(() => {
module(function($provide) {
$provide.value('User', {});
});
inject(($rootScope, $controller) => {
rootScope = $rootScope;
user = specHelper.newUser();
user._id = "unique-user-id";
userSerivce = {user: user};
scope = $rootScope.$new();
scope.task = {
group: {
assignedUsers: [],
approval: {
required: false,
}
},
};
scope.task._edit = angular.copy(scope.task);
$controller('GroupTaskActionsCtrl', {$scope: scope, User: userSerivce});
});
});
describe('toggleTaskRequiresApproval', function () {
it('toggles task approval required field from false to true', function () {
scope.toggleTaskRequiresApproval();
expect(scope.task._edit.group.approval.required).to.be.true;
});
it('toggles task approval required field from true to false', function () {
scope.task._edit.group.approval.required = true;
scope.toggleTaskRequiresApproval();
expect(scope.task._edit.group.approval.required).to.be.false;
});
});
describe('assign events', function () {
it('adds a group member to assigned users on "addedGroupMember" event ', () => {
var testId = 'test-id';
rootScope.$broadcast('addedGroupMember', testId);
expect(scope.task.group.assignedUsers).to.contain(testId);
expect(scope.task._edit.group.assignedUsers).to.contain(testId);
});
it('removes a group member to assigned users on "addedGroupMember" event ', () => {
var testId = 'test-id';
scope.task.group.assignedUsers.push(testId);
scope.task._edit.group.assignedUsers.push(testId);
rootScope.$broadcast('removedGroupMember', testId);
expect(scope.task.group.assignedUsers).to.not.contain(testId);
expect(scope.task._edit.group.assignedUsers).to.not.contain(testId);
});
});
});
@@ -6,6 +6,7 @@ describe('Notification Controller', function() {
beforeEach(function() {
user = specHelper.newUser();
user._id = "unique-user-id";
user.needsCron = false;
var userSync = sinon.stub().returns({
then: function then (f) { f(); }
@@ -334,4 +334,34 @@ describe('Settings Controller', function () {
});
});
});
context('Fixing character values', function () {
describe('#restore', function () {
var blankRestoreValues = {
stats: {
hp: 0,
exp: 0,
gp: 0,
lvl: 0,
mp: 0,
},
achievements: {
streak: 0,
},
};
it('doesn\'t update character values when level is less than 1', function () {
scope.restoreValues = blankRestoreValues;
scope.restore();
expect(User.set).to.not.be.called;
});
it('updates character values when level is at least 1', function () {
scope.restoreValues = blankRestoreValues;
scope.restoreValues.stats.lvl = 1;
scope.restore();
expect(User.set).to.be.called;
});
});
});
});
@@ -1,35 +0,0 @@
'use strict';
describe('closeMenu Directive', function() {
var scope;
beforeEach(module('habitrpg'));
beforeEach(inject(function($rootScope) {
scope = $rootScope.$new();
scope.$digest();
}));
it('closes a connected menu when element is clicked', inject(function($compile) {
var menuElement = $compile('<a data-close-menu menu="mobile">')(scope);
scope._expandedMenu = { menu: 'mobile' };
menuElement.appendTo(document.body);
menuElement.triggerHandler('click');
expect(scope._expandedMenu.menu).to.eql(null)
}));
it('closes a connected menu when child element is clicked', inject(function($compile) {
var menuElementWithChild = $compile('<li></li>')(scope);
var menuElementChild = $compile('<a data-close-menu></a>')(scope);
scope._expandedMenu = { menu: 'mobile' };
menuElementWithChild.appendTo(document.body);
menuElementChild.appendTo(menuElementWithChild);
menuElementChild.triggerHandler('click');
expect(scope._expandedMenu.menu).to.eql(null)
}));
});
@@ -1,35 +0,0 @@
'use strict';
describe('expandMenu Directive', function() {
var menuElement, scope;
beforeEach(module('habitrpg'));
beforeEach(inject(function($rootScope, $compile) {
scope = $rootScope.$new();
var element = '<a data-expand-menu menu="mobile"></a>';
menuElement = $compile(element)(scope);
scope.$digest();
}));
it('expands a connected menu when element is clicked', function() {
expect(scope._expandedMenu).to.not.exist;
menuElement.appendTo(document.body);
menuElement.triggerHandler('click');
expect(scope._expandedMenu.menu).to.eql('mobile')
});
it('closes a connected menu when it is already open', function() {
scope._expandedMenu = {};
scope._expandedMenu.menu = 'mobile';
menuElement.appendTo(document.body);
menuElement.triggerHandler('click');
expect(scope._expandedMenu.menu).to.eql(null)
});
});
@@ -206,6 +206,20 @@ describe('Quests Service', function() {
});
});
context('quest bundles', function() {
it('sends bundle object', function(done) {
questsService.buyQuest('featheredFriends')
.then(function(res) {
expect(res).to.eql(content.bundles.featheredFriends);
expect(window.alert).to.not.be.called;
expect(rejectSpy).to.not.be.called;
done();
}, rejectSpy);
scope.$apply();
});
});
context('all other quests', function() {
it('sends quest object', function(done) {
questsService.buyQuest('whale')
+6 -1
View File
@@ -2,5 +2,10 @@
"env": {
"node": true,
"browser": true,
}
},
"extends": [
"habitrpg/browser",
"habitrpg/mocha",
"habitrpg/esnext",
],
}
@@ -0,0 +1,19 @@
import Vue from 'vue';
import DrawerComponent from 'client/components/inventory/drawer.vue';
describe('DrawerComponent', () => {
it('sets the correct default data', () => {
expect(DrawerComponent.data).to.be.a('function');
const defaultData = DrawerComponent.data();
expect(defaultData.open).to.be.true;
});
it('renders the correct title', () => {
const Ctor = Vue.extend(DrawerComponent);
const vm = new Ctor({propsData: {
title: 'My title',
}}).$mount();
expect(vm.$el.textContent).to.be.equal('My title');
});
});
@@ -0,0 +1,20 @@
import roundBigNumberFilter from 'client/filters/roundBigNumber';
describe('round big number filter', () => {
it('can round a decimal number', () => {
expect(roundBigNumberFilter(4.567)).to.equal(4.57);
expect(roundBigNumberFilter(4.562)).to.equal(4.56);
});
it('can round thousands', () => {
expect(roundBigNumberFilter(70065)).to.equal('70.1k');
});
it('can round milions', () => {
expect(roundBigNumberFilter(10000987)).to.equal('10.0m');
});
it('can round bilions', () => {
expect(roundBigNumberFilter(1000000000)).to.equal('1.0b');
});
});
@@ -1,5 +1,6 @@
import groupsUtilities from 'client/mixins/groupsUtilities';
import { TAVERN_ID } from 'common/script/constants';
import generateStore from 'client/store';
import Vue from 'vue';
describe('Groups Utilities Mixin', () => {
@@ -7,6 +8,7 @@ describe('Groups Utilities Mixin', () => {
before(() => {
instance = new Vue({
store: generateStore(),
mixins: [groupsUtilities],
});
@@ -9,7 +9,7 @@ describe('tasks actions', () => {
});
describe('fetchUserTasks', () => {
it('fetches user tasks', async () => {
xit('fetches user tasks', async () => {
expect(store.state.tasks.loadingStatus).to.equal('NOT_LOADED');
const tasks = [{_id: 1}];
sandbox.stub(axios, 'get').withArgs('/api/v3/tasks/user').returns(Promise.resolve({data: {data: tasks}}));
@@ -36,7 +36,7 @@ describe('tasks actions', () => {
expect(store.state.tasks.loadingStatus).to.equal('LOADED');
});
it('can reload tasks if forceLoad is true', async () => {
xit('can reload tasks if forceLoad is true', async () => {
store.state.tasks = {
loadingStatus: 'LOADED',
data: [{_id: 1}],
+1 -1
View File
@@ -1,7 +1,7 @@
import axios from 'axios';
import generateStore from 'client/store';
describe('tasks actions', () => {
describe('user actions', () => {
let store;
beforeEach(() => {
+12 -1
View File
@@ -121,6 +121,17 @@ describe('achievements', () => {
});
});
it('card achievements exist with counts', () => {
let cardTypes = ['greeting', 'thankyou', 'birthday', 'congrats', 'getwell', 'goodluck'];
cardTypes.forEach((card) => {
let cardAchiev = basicAchievs[`${card}Cards`];
expect(cardAchiev).to.exist;
expect(cardAchiev).to.have.property('optionalCount')
.that.is.a('number');
});
});
it('rebirth achievement exists with no count', () => {
let rebirth = basicAchievs.rebirth;
@@ -174,7 +185,7 @@ describe('achievements', () => {
});
it('card achievements exist with counts', () => {
let cardTypes = ['greeting', 'thankyou', 'nye', 'valentine', 'birthday'];
let cardTypes = ['nye', 'valentine'];
cardTypes.forEach((card) => {
let cardAchiev = seasonalAchievs[`${card}Cards`];
+18 -4
View File
@@ -13,6 +13,12 @@ describe('shops', () => {
expect(shopCategories.length).to.be.greaterThan(2);
});
it('does not contain an empty category', () => {
_.each(shopCategories, (category) => {
expect(category.items.length).to.be.greaterThan(0);
});
});
it('does not duplicate identifiers', () => {
let identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
@@ -47,11 +53,19 @@ describe('shops', () => {
it('items contain required fields', () => {
_.each(shopCategories, (category) => {
_.each(category.items, (item) => {
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'boss', 'class', 'collect', 'drop', 'unlockCondition', 'lvl'], (key) => {
expect(_.has(item, key)).to.eql(true);
if (category.identifier === 'bundle') {
_.each(category.items, (item) => {
_.each(['key', 'text', 'notes', 'value', 'currency', 'purchaseType', 'class'], (key) => {
expect(_.has(item, key)).to.eql(true);
});
});
});
} else {
_.each(category.items, (item) => {
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'boss', 'class', 'collect', 'drop', 'unlockCondition', 'lvl'], (key) => {
expect(_.has(item, key)).to.eql(true);
});
});
}
});
});
});
+25
View File
@@ -9,6 +9,8 @@ import i18n from '../../../website/common/script/i18n';
import {
generateUser,
} from '../../helpers/common.helper';
import forEach from 'lodash/forEach';
import moment from 'moment';
describe('shared.ops.purchase', () => {
const SEASONAL_FOOD = 'Meat';
@@ -200,5 +202,28 @@ describe('shared.ops.purchase', () => {
expect(user.items.gear.owned[key]).to.be.true;
});
it('purchases quest bundles', () => {
let startingBalance = user.balance;
let clock = sandbox.useFakeTimers(moment('2017-05-20').valueOf());
let type = 'bundles';
let key = 'featheredFriends';
let price = 1.75;
let questList = [
'falcon',
'harpy',
'owl',
];
purchase(user, {params: {type, key}});
forEach(questList, (bundledKey) => {
expect(user.items.quests[bundledKey]).to.equal(1);
});
expect(user.balance).to.equal(startingBalance - price);
clock.restore();
});
});
});
+1049 -217
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -2,8 +2,8 @@
## Babel Paths for Production Environment
In development, we [transpile at server start](https://github.com/HabitRPG/habitrpg/blob/1ed7e21542519abe7a3c601f396e1a07f9b050ae/website/server/index.js#L6-L8). This allows us to work quickly while developing, but is not suitable for production. So, in production we transpile the server code before the app starts.
In development, we [transpile at server start](https://github.com/HabitRPG/habitica/blob/1ed7e21542519abe7a3c601f396e1a07f9b050ae/website/server/index.js#L6-L8). This allows us to work quickly while developing, but is not suitable for production. So, in production we transpile the server code before the app starts.
This system means that requiring any files from `common/script` in `website/server/**/*.js` must be done through the `common/index.js` module. In development, it'll pass through to the pre-transpiled files, but in production it'll point to the transpiled versions. If you try to require or import a file directly, it will error in production as the server doesn't know what to do with some es2015isms (such as the import statement).
This system means that requiring any files from `website/common/script` in `website/server/**/*.js` must be done through the `website/common/index.js` module. In development, it'll pass through to the pre-transpiled files, but in production it'll point to the transpiled versions. If you try to require or import a file directly, it will error in production as the server doesn't know what to do with some es2015isms (such as the import statement).
This test just verifies that none of the files in the server code are calling the common files directly.
Executable → Regular
+2 -2
View File
@@ -16,7 +16,7 @@ nvm use
nvm alias default current
echo Update npm...
npm install -g npm@3
npm install -g npm@4
echo Installing global modules...
npm install -g gulp bower grunt-cli mocha
npm install -g gulp bower grunt-cli mocha node-pre-gyp
+1 -1
View File
@@ -10,7 +10,7 @@ module.exports = {
index: path.resolve(__dirname, '../../dist-client/index.html'),
assetsRoot: path.resolve(__dirname, '../../dist-client'),
assetsSubDirectory: 'static',
assetsPublicPath: '/new-app',
assetsPublicPath: '/new-app/',
staticAssetsDirectory,
productionSourceMap: true,
// Gzip off by default as many popular static hosts such as
+43 -11
View File
@@ -17,21 +17,31 @@ const baseConfig = {
path: config.build.assetsRoot,
publicPath: IS_PROD ? config.build.assetsPublicPath : config.dev.assetsPublicPath,
filename: '[name].js',
devtoolModuleFilenameTemplate (info) {
// Fix source maps, code from
// https://github.com/Darkside73/bbsmile.com.ua/commit/3596d3c42ef91b69d8380359c3e8908edc08acdb
let filename = info.resourcePath;
if (info.resource.match(/\.vue$/) && !info.allLoaders.match(/type=script/)) {
filename = 'generated';
}
return filename;
},
},
resolve: {
extensions: ['*', '.js', '.vue', '.json'],
modules: [
path.join(__dirname, '..', 'website'),
path.join(__dirname, '..', 'test/client/unit'),
path.join(__dirname, '..', 'node_modules'),
path.join(projectRoot, 'website'),
path.join(projectRoot, 'test/client/unit'),
path.join(projectRoot, 'node_modules'),
],
alias: {
jquery: 'jquery/src/jquery',
website: path.resolve(__dirname, '../website'),
common: path.resolve(__dirname, '../website/common'),
client: path.resolve(__dirname, '../website/client'),
assets: path.resolve(__dirname, '../website/client/assets'),
components: path.resolve(__dirname, '../website/client/components'),
website: path.resolve(projectRoot, 'website'),
common: path.resolve(projectRoot, 'website/common'),
client: path.resolve(projectRoot, 'website/client'),
assets: path.resolve(projectRoot, 'website/client/assets'),
components: path.resolve(projectRoot, 'website/client/components'),
},
},
plugins: [
@@ -63,11 +73,17 @@ const baseConfig = {
{
test: /\.js$/,
loader: 'babel-loader',
include: projectRoot,
exclude: /node_modules/,
include: [
path.join(projectRoot, 'test'),
path.join(projectRoot, 'website'),
path.join(projectRoot, 'node_modules', 'bootstrap-vue'),
],
options: {
cacheDirectory: true,
},
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
test: /\.(png|jpe?g|gif)(\?.*)?$/,
loader: 'url-loader',
query: {
limit: 10000,
@@ -82,6 +98,22 @@ const baseConfig = {
name: utils.assetsPath('fonts/[name].[hash:7].[ext]'),
},
},
{
test: /\.svg$/,
use: [
{ loader: 'svg-inline-loader' },
{ loader: 'svgo-loader' },
],
exclude: [path.resolve(projectRoot, 'website/client/assets/svg/for-css')],
},
{
test: /\.svg$/,
use: [
{ loader: 'svg-url-loader' },
{ loader: 'svgo-loader' },
],
include: [path.resolve(projectRoot, 'website/client/assets/svg/for-css')],
},
],
},
};
Binary file not shown.

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