Compare commits

..

314 Commits

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

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

* Issue-8341 removing extra comma from gift subscription copy

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

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

* Update modelsPaths.js

* Update error

* fix test

* Update GET-model_paths.test.js

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

* Added group task reorder to front end

* Added syncing with group task order

* Fixed linting issues

* Added missing exec and abstracted move code

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

* Add newsArchive string

* Translate news archive link

* Fix link?

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

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

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

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

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

* Fixes #8227

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

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

* Updated migrations to use monk

* Fixed line endings

* Moved monk to dev dependencies
2017-01-06 11:07:21 -06:00
Keith Holliday c707b6c99b Add check to ensure obj is not user (#8373) 2017-01-06 10:23:39 -06:00
Sabe Jones e4bd466cc7 3.66.1 2017-01-06 02:37:50 +00:00
Sabe Jones 001b8eb894 chore(news): Bailey 2017-01-05 2017-01-06 02:09:59 +00:00
Keith Holliday 9abcfe8614 Added fixes for party group plans (#8366) 2017-01-05 16:52:05 -06:00
Matteo Pagliazzi bc6102551d chore(i18n): update locales 2017-01-05 16:42:43 +01:00
Sabe Jones 959a3ff85b fix(menus): keep icon after group ack
Also corrects a TypeError in the menu closing function.
2017-01-04 22:19:57 +00:00
Matteo Pagliazzi 518b874f64 Always use .exec() for .find*() and .update() (#8361)
* add exec where missing in /models

* ix taskManager query

* fix top-level controllers

* fix api-v3 controllers
2017-01-04 16:49:43 +01:00
Travis 6cc359a935 Adding new user.addNotification method with Mongodb Update (#8272)
* Adding new method to user schema that pushes a new notification to the database with an update operation instead of a save. fixes #8264

* fixing test text

* changing the addUserNotificationUpdate method to a static method as requested.

* Renaming to push notification

* fixing comment documentation based on pr comments.

* Changed the update statement to do a multi update and added a validation step. Added tests for both these cases.

* Updating pushNotification method to allow a query to be passed instead of an array of userIds to make it more flexible.

* Removing createdAt field from tests as it's no longer used.

* Removing only from test suite
2017-01-04 15:27:54 +01:00
Sabe Jones 514d35c0be 3.66.0 2017-01-03 22:03:09 +00:00
Sabe Jones 13da92ea68 feat(content): Armoire and BGs 2017-01 2017-01-03 21:35:23 +00:00
Travis 03c4d82b7d fix: prevents blank messages from being posted to chat (#8257)
* fix: throws an error when the server receives a post chat request with a message containing only whitespace.

* Adding a test confirming behavior around messages that only contain newlines.

* Removing accidental only that was left on a test
2017-01-03 07:15:31 -07:00
Sabe Jones d905ab7f86 3.65.2 2017-01-03 00:50:46 +00:00
Sabe Jones c6560b6b1b chore(event): end New Year's bennies 2017-01-03 00:19:20 +00:00
Keith Holliday c61f660255 Added field existence checks (#8356) 2017-01-02 16:31:04 -07:00
Matteo Pagliazzi 2f1b683ec9 Avoid setting profile name to not found (#8357)
* avoid setting profile name to not found

* only set profile name when empty

* profile.name is required

* set profile name before validation

* fix and add tests
2017-01-03 00:00:01 +01:00
chan_gami 47bb217068 Fixes pressing enter to confirm character choice in IME creates new checklist line (#8326) (#8334) 2017-01-02 10:03:03 -07:00
Declan Ramsay f49fd05da1 Fixes on-hover notes on tasks do not update after you edit the notes, until a sync occurs (#8353)
* Shift taskPopover from one-time-binding to regular binding, allowing task note display to be updated

* Re-add Markdown filter to task popover
2017-01-02 10:00:35 -07:00
Matteo Pagliazzi b0341aa06f chore(i18n): update locales 2017-01-02 15:54:04 +01:00
Kaitlin Hipkin b07ec18e33 Correct party up/on achievement string names (#8313)
* Revert party up/on achievement string names

* fix missed references to party up/on strings
2017-01-01 10:54:49 -07:00
Alys 12930a2bac 3.65.1 2016-12-31 07:47:49 +00:00
Alys 91f5c47d9d Revert "feature: adding hp notification for boss damage" (#8340) 2016-12-31 17:31:25 +10:00
Sabe Jones fe7850d10f 3.65.0 2016-12-30 23:30:42 +00:00
Sabe Jones c5c2da75bf fix(shops): hardcode NYE card 2016-12-30 23:08:01 +00:00
Sabe Jones 969607cd3b feat(event): New Year's 2016 2016-12-30 22:19:29 +00:00
Travis 2a1f52a359 feature: adding hp notification for boss damage (#8249)
* feature: adding hp notification for boss damage. fixes #7749

* Updating boss damage text to 'Damage from Boss' to make it more clear
2016-12-30 13:29:20 -06:00
Keith Holliday 47d9594679 Group plans remove unlinked tasks (#8332)
* Added ability to delete tasks that are broken

* Added ability to delete group tasks after leaving group
2016-12-30 13:17:39 -06:00
Keith Holliday 97e40c81f3 Added error when nonleader attempts to invite to group plan (#8331) 2016-12-30 13:17:22 -06:00
Sabe Jones c8b61a2f7d feat(content): Armoire and BG strings 2017-01 2016-12-29 20:43:15 +00:00
Cai Lu e9543f0d28 Add 'balanceGemAmount' to Amplitude #8057 (#8323)
* Add balanceGemAmount property

* Add check for balanceGemAmount property

* Fix balanceGemAmount to be 4 times balance
2016-12-29 14:31:30 -06:00
Keith Holliday 77b88490e4 (WIP) Thehollidayin/front end updates (#8278)
* Added read me and Inbox Page

* Fixed inbox linting

* Added converstaion route

* Added temp data and style

* Added social page and nav

* Fixed Inbox routes

* Added basic layout for Tavern Page
2016-12-29 13:24:08 -06:00
Keith Holliday 7fc2500bfd Removed group plan option from gift modal (#8327) 2016-12-29 08:57:00 -06:00
Keith Holliday fb229acb58 Added per user cost message if group has subscription (#8328)
* Added per user cost message if group has subscription

* Added user specification to cost
2016-12-29 08:56:40 -06:00
Sabe Jones 6ce83d1fa4 3.64.1 2016-12-28 22:03:35 +00:00
Sabe Jones 2be4815aea chore(news): misc Bailey 2016-12-28 21:59:20 +00:00
Khwunchai Jaengsawang 1dbc42f48a Fixes character appears too high in avatar on profile when no pet or mount is equipped (#7916) (#8318) 2016-12-28 09:36:56 -06:00
Matteo Pagliazzi 89279c8aed chore(i18n): update locales 2016-12-28 09:53:20 +01:00
Matteo Pagliazzi faedb13598 add schema field to keep track of onboarding emails 2016-12-28 09:47:14 +01:00
Rick Kasten c0c74659c5 Docker improvements (#8297)
* Improved docker, bower

* npm install missing mocha

* Fix for 'npm install -g npm@4' not resulting in a functional npm

* Improve speed of 'docker start' by withholding directories not used by image environment

* Reverting changes to bower.json
2016-12-28 09:33:24 +01:00
Travis bf5ad2db1f Fixing Exponential Quest Reward Scrolls (#7800)
* adding quest owner specific rewards. closes #2715

* Updating model to prevent this from being a breaking change.

* Removing duplicate translatable string and readding accidentally deleted portion

* capitalizing according to pr.

* fixing according to comments on pr

* removing final mistakes

* fixing whitespace

* re-adding the onlyOwner field that got deleted when the index.js file was moved and fixed console errors.

* moving cleaning of empty obejct for quest owner updates into quest owner updates method

* Fixing so tests pass by updating variable name and removing unnecessary parameter definition.

* adding a new test and refactoring client side code to use controller method.
2016-12-28 01:38:52 -06:00
Megan Tiu 7d99873960 Add inline save-close buttons to top of task modal (#8319) 2016-12-28 01:24:58 -06:00
Keith Holliday e02ef00397 Add leader check for challenge tasks on delete icon (#8325) 2016-12-28 01:08:37 -06:00
Alys 23c5c4211c decapitalise "A" in a title; Windows character removal; spacing fixes
- decapitaliseed "A" in "Create A Group"
- converted non-ascii apostrophes to single-quote for consistency with other strings
- removed a double space within a string
- removed Windows line endings on every line
- converted all leading four spaces to two spaces (normally wouldn't do that but since every line was already touched by the line ending change, why not)
2016-12-28 05:07:21 +00:00
Matteo Pagliazzi 69cc134fff 3.64.0 2016-12-27 17:18:59 +01:00
Matteo Pagliazzi ffd9400cb7 fix: eslint: re-order packages 2016-12-26 16:04:36 +01:00
Matteo Pagliazzi 5be91ef842 fix test that was not passing because languages are not loaded in tests 2016-12-26 15:31:31 +01:00
Matteo Pagliazzi 3123183e46 chore(i18n): update locales 2016-12-26 11:59:44 +01:00
Matteo Pagliazzi 49cca7a601 WIP - Onboarding emails (#8309)
* add onboarding emails

* fix typo
2016-12-26 11:49:45 +01:00
Keith Holliday 7fbd38d18c Checked to ensure tasks has checklists before attempting to sync last checklist item (#8314) 2016-12-25 15:00:41 -06:00
padm0 1f95376d39 added missing pixels to robe fixes #8289 (#8296) 2016-12-26 03:06:34 +10:00
Keith Holliday 2da0a1e88c Task interaction fixes (#8306)
* Fixed interacting with a broken challenge

* Added fix for users using open tasks in edit mode
2016-12-22 11:40:00 -06:00
Keith Holliday afacd3e1cf Replaced array deconstruction with object (#8300) 2016-12-21 18:09:45 -06:00
Sabe Jones a69b9e6705 3.63.0 2016-12-21 23:41:41 +00:00
Sabe Jones e4e5d10316 Mixed type field for A/B testing (#8302)
* feat(AB-testing): mixed type field

* fix(AB-testing): lint errors

* fix(AB-testing): allow client access to _ABtests

* Revert "fix(AB-testing): allow client access to _ABtests"

This reverts commit 25832365ba.

* fix(AB-testing): preview check on server

* refactor(AB-testing): add comments
2016-12-21 15:19:00 -08:00
Sabe Jones 27c38bdf45 feat(content): subscriber items 2016-12 2016-12-21 20:34:45 +00:00
Keith Holliday ea24eeb019 Thehollidayinn/group plans part 2 (#8262)
* Added all ui components back

* Added group ui items back and initial group approval directive

* Added approval list view with approving functionality

* Added notification display for group approvals

* Fixed linting issues

* Removed expectation from beforeEach

* Moved string to locale

* Added per use group plan for stripe

* Added tests for stripe group plan upgrade

* Removed paypal option

* Abstract sub blocks. Hit group sub block from user settings page. Added group subscriptin beneifts display

* Fixed lint issue

* Added pricing and adjusted styles

* Moved text to translations

* Added group email types

* Fixed typo

* Fixed group plan abstraction and other style issues

* Fixed email unit test

* Added type to group plan to filter our group plans

* Removed dev protection from routes

* Removed hard coding and fixed upgrade plan

* Added error when group has subscription and tries to remove

* Fixed payment unit tests

* Added custom string and moved subscription check up in the logic

* Added ability for old leader to delete subscription the created

* Allowed old guild leader to edit their group subscription

* Fixed linting and tests

* Added group sub page to user sub settings

* Added approval and group tasks requests back. Hid user group sub on profile

* Added group tasks sync after adding to allow for editing

* Fixed promise chain when resolving group

* Added approvals to group promise chain

* Ensured compelted group todos are not delted at cron

* Updated copy and other minor styles

* Added group field to tags and recolored group tag.

* Added chat message when task is claimed

* Preventing task scoring when approval is needed

* Added approval requested indicator

* Updated column with for tasks on group page

* Added checklist sync on assign

* Added sync for checklist items

* Added checkilist sync when task is updated

* Added checklist sync remove

* Sanatized group tasks when updated

* Fixed lint issues

* Added instant scoring of approved task

* Added task modal

* Fixed editing of challenge and group tasks

* Added cancel button

* Added add new checklist option to update sync

* Added remove for checklist

* Added checklist update

* Added difference check and sync for checklist if there is a diff

* Fixed task syncing

* Fixed linting issues

* Fixed styles and karma tests

* Fixed minor style issues

* Fixed obj transfer on scope

* Fixed broken tests

* Added new benefits page

* Updated group page styles

* Updated benefits page style

* Added translations

* Prevented sync with empty trask list

* Added task title to edit modal

* Added new group plans page and upgrade redirect

* Added group plans redirect to upgrade

* Fixed party home page being hidden and home button click

* Fixed dynamic changing of task status and grey popup

* Fixed tag editing

* Hid benifites information if group has subscription

* Added quotes to task name

* Fixed issue with assigning multiple users

* Added new group plans ctrl

* Hid menu from public guilds

* Fixed task sync issue

* Updated placeholder for assign field

* Added correct cost to subscribe details

* Hid create, edit, delete task options from non group leaders

* Prevented some front end modifications to group tasks

* Hid tags option from group original task

* Added refresh for approvals and group tasks

* Prepend new group tasks

* Fix last checklist item sync

* Fixed casing issue with tags

* Added claimed by message on hover

* Prevent user from deleting assigned task

* Added single route for group plan sign up and payments

* Abstracted stripe payments and added initial tests

* Abstracted amazon and added initial tests

* Fixed create group message

* Update group id check and return group

* Updated to use the new returned group

* Fixed linting and promise issues

* Fixed broken leave test after merge issue

* Fixed undefined approval error and editing/deleting challenge tasks

* Add pricing to group plans, removed confirmation, and fixed redirect after payment

* Updated group plan cost text
2016-12-21 13:45:45 -06:00
Travis 55a8eef3e1 Fixing Duplicate tasks showing up after joining a challenge (#7787)
* fix: prevents double joining challenge by quickly hitting join button on challenge twice. fixes #7730

* Fixing client side parameter updates.
2016-12-20 19:52:18 -06:00
Matteo Pagliazzi 92cbb4a07d Upgrade ESLint to v3 (#8299)
* upgraded habitrpg-eslint-config to v2 and eslint to v3

* adapt to eslint3 rules

* update shrinkwrap

* update shrinkwrap again
2016-12-20 22:31:36 +01:00
Vince Campanale 3f96d05365 Fix gemgift spacing (#8294)
* included privateMessageGiftIntro in privateMessageGiftGemsMessage to take care of spacing error and make it easier for translators

* fixed spacing error in gems gift message and adjusted relevant tests

* removed privateMessageGiftIntro and privateMessageGiftSubscriptionMessage since they are no longer in use
2016-12-20 15:32:59 +01:00
Matteo Pagliazzi 0b72f6a613 chore(i18n): update locales 2016-12-19 21:44:04 +01:00
Matteo Pagliazzi 5e1e6be518 models: add required: true to id fields where missing 2016-12-18 17:16:49 +01:00
Matteo Pagliazzi 472ec99291 chore(i18n): update locales 2016-12-17 15:17:53 +01:00
Sabe Jones 0284e9a4e3 fix(event): sell Santa scrolls 2016-12-17 02:54:29 +00:00
Sabe Jones 1a0721c078 3.62.0 2016-12-17 02:17:50 +00:00
Sabe Jones 6b6b548ac5 chore(sprites): compile 2016-12-17 02:16:01 +00:00
Sabe Jones 30f3d786bb feat(event): Winter Wonderland 2016-2017 (#8290) 2016-12-16 17:49:22 -08:00
padm0 07bbba6789 newly designed peppermint panda mount. Original images were incorrect drawings. fixes #7982 (#8286) 2016-12-16 16:53:09 -08:00
padm0 6afb2bd0d4 adjust positioning on mounts fixes #7982 (#8285)
* adjust positioning on mounts fixes #7982

* fixing background on peppermint flying pig
2016-12-16 16:46:37 -08:00
Jaka Kranjc f1a3bd5001 transferGems: use receiver language translation for PM strings #7722 (#8173)
* transferGems: use receiver language translation for PM strings #7722

* chore(test): DRY up transfer gems test

* chore(test): Allow check for language in translation assertion

* chore(test): Add test that member locales are used in transfer gems msg

* sendMessage: optionally take also the message in sender's language

when present, it is stored in the sender's inbox instead of the version
in the target language.

* transferGems: prepare pm in both languages #7722

* sendMessage: take an object for the second parameter instead

* payments: made two more gift strings translatable

* buyGems: send both translations for gifted gems

* buyGems: send push notifications in target user's locale

* createSubscription: send both translations for gifted subs

* createSubscription: send push notifications in target user's locale

* transferGems: send push notifications in target user's locale

* tests: adjust payment tests for translation changes

* added function doc for sendMessage

* tests: added bilingual test for buyGems
2016-12-15 18:36:54 -06:00
Sabe Jones 3f6a13d209 fix(achievements): don't show unobtainable boss quests 2016-12-16 00:04:46 +00:00
Sabe Jones 3658e41fec fix(jade): eliminate space warning, for real 2016-12-15 21:42:00 +00:00
Sabe Jones c69d5c7ae6 fix(achievements): don't return undefined 2016-12-15 21:35:21 +00:00
Sabe Jones 747f9e6a99 fix(achievements): show boolean pet cheevos
Also fixes a spacing issue that threw Jade warnings.
2016-12-15 21:03:55 +00:00
Matteo Pagliazzi 7755ab090b chore(i18n): update locales 2016-12-15 21:57:45 +01:00
Travis 9ed17df1e3 Updating group.removeMember api to send an email for rescinded party invite (#8280)
* feature: updating group.removeMember api to send an email to a user when an invite was rescinded or when no message was provided by the performing user.

* Adding validation that the email is being sent to the right user.

* fixing linting error
2016-12-15 20:47:18 +01:00
Sabe Jones faeb040a83 fix(achievements): show Rebirths count 2016-12-15 17:49:15 +00:00
Sabe Jones 0a1ae1375e fix(achievements): camelcase altPath 2016-12-15 17:43:54 +00:00
Sabe Jones 9756030fa2 3.61.0 2016-12-15 17:35:51 +00:00
Sabe Jones c66172b74b chore(sprites): compile
and update Bailey date
2016-12-15 16:50:09 +00:00
Sabe Jones 281f6d1806 Holly Potions (#8281)
* feat(content): Wonderland 2016 gear strings

* feat(content): Holly Potions
and string data for Winter Wonderland and December subscriber gear

* fix(event): correct winter availability

* refactor(canBuy): concise return logic

* chore(news): Bailey
2016-12-15 08:36:28 -08:00
Matteo Pagliazzi 237095d109 Notifications: remove timestamps (#8287)
* user notifications: make updatedAt public

* notifications: disable timestamps
2016-12-15 17:33:24 +01:00
Travis fa788f49fc Preventing users from buying already gifted subscriber items (#7734)
* Adding the unopened mystery items to the call to get not obtained subscriber items. closes #7712

* refactoring according to pr

* Refactoring according to pr. moved time-travelers to it's own file and added new tests.
2016-12-15 10:00:49 -06:00
Kaitlin Hipkin 0817cf96e1 Achievement list renovation & Achievements API (#7904)
* pull apart achievements into different subcategories

* achievs previously hidden to others if unachieved are now always shown

* achievs previously always hidden if unachieved are now always shown

* pull apart ultimate gear achievs

* add achiev wrapper mixin

* add achiev mixin for simple counts

* add achiev mixin for singular/plural achievs

* add simpleAchiev mixin and support attributes

* always hide potentially unearnable achievs if unearned

* contributor achiev now uses string interpolation for readMore link

* transition to basic achiev grid layout

* fix npc achievement img bug introduced in c90f7e2

* move surveys and contributor achievs into special section so it is never empty

* double size of achievs in achievs grid

* achievs in grid are muted if unachieved (includes recompiled sprites)

* fix streak notification strings

* add counts to achievement badges for applicable achieved achievs

* list achievements by api

* fix achievement strings in new api

* unearned achievs now use dedicated (WIP) 'unearned' badge instead of muted versions of the normal badges

* fix & cleanup achievements api

* extract generation of the achievements result to a class

* clean up achievement counter css using existing classes

* simplify exports of new achievementBuilder lib

* remove class logic from achievementBuilder lib

* move achievs to common, add rebirth achiev logic, misc fixes

* replace achievs jade logic with results of api call

* fix linting errors

* achievs lib now returns achievs object subdivided by type (basic/seasonal/special

* add tests for new achievs lib

* fix linting errors

* update controllers and views for updated achievs lib

* add indices to achievements to preserve intended order

* move achiev popovers to left

* rename achievs lib to achievements

* adjust positioning of achieve popovers now that stats and achievs pages
are separate

* fix: achievements api correctly decides whether to append extra string for master and triadBingo achievs

* revert compiled sprites so they don't bog down the PR

* pull out achievs api integration tests

* parameterize ultimate gear achievements' text string

* break out static achievement data from user-specific data

* reorg content.achievements to add achiev data in related chunks

* cleanup, respond to feedback

* improve api documentation

* fix merge issues

* Helped Habit Grow --> Helped Habitica Grow

* achievement popovers are muted if the achiev is unearned

* fix singular achievement labels / achievement popover on click

* update apidoc for achievements (description, param-type, successExample, error-types)

* fix whitespace issues in members.js

* move html to a variable

* updated json example

* fix syntax after merge
2016-12-13 12:48:18 -06:00
Matteo Pagliazzi 97e1d75dce 3.60.0 2016-12-12 22:04:34 +01:00
Matteo Pagliazzi 52bf20c27d upgrade shrinkwrap 2016-12-12 22:01:39 +01:00
Matteo Pagliazzi 5dbaf39aba Node 6 and NPM 4 (#8081)
* upgrade node to version 6

* upgrade npm to v4

* update shrinkwrap

* use npm 4 in travis

* use mongoose 4.6.4

* update shrinkwrap

* fix async test and upgrade mongoose

* fix amazon test

* remove debugging code

* working tests with separate server

* update coupon code

* mupdate mongoose

* nvm: relax node version in .nvmrc
2016-12-12 21:51:53 +01:00
Sabe Jones 66d402c985 3.59.1 2016-12-12 20:33:12 +00:00
Sabe Jones 8048146223 chore(news): Bailey 2016-12-12 20:17:58 +00:00
Matteo Pagliazzi e2c07e458d client: fix action name 2016-12-12 21:05:51 +01:00
padm0 90a9e8e192 Fixing 112016 mystery set. (#8276) 2016-12-12 09:13:59 -08:00
Keith Holliday f8039f48a6 Styled merch button to have highlight (#8273) 2016-12-10 22:09:06 -06:00
PowerlinxJetfire 04337f8e83 Fix filter buttons when windows resizes fixes (partially) #7772 (#8258)
* Reloads the quest panel solving issue #7697

* Revert "Reloads the quest panel solving issue #7697"

This reverts commit 0d58fb0fd3.

* fix overlapping filter buttons when windows resizes

This fixes one of the two causes of issue #7772.
https://github.com/HabitRPG/habitica/issues/7772
2016-12-10 22:08:26 -06:00
Keith Holliday 45297f8bf9 Merge pull request #8256 from a2lin/equipment_search
Adds a free-text filter (search) to the equipment page.
2016-12-10 16:28:11 -06:00
Keith Holliday 6f112c29f2 Merge pull request #8268 from Tallestthomas/develop
Show link leading to the Food Preferences Wiki page.
2016-12-10 14:41:17 -06:00
Keith Holliday 4d1edb363c Merge pull request #8243 from 15313-platypi/develop
Quest Panel Reload (Issue #7697)
2016-12-10 14:34:43 -06:00
Alexander Lin 4e303cc592 Clean up code 2016-12-10 02:15:30 -08:00
Travis 798a975185 fix: confirm no user objects reference a group before deleting it when the member count reaches 0 (#8267)
* fix: confirm no user objects reference a group before deleting it when the member count reaches 0

* Updating mongo queries to return promises and use the select statement.
2016-12-09 12:14:25 -08:00
Keith Holliday eb2b46fc5d Merge pull request #8269 from Hus274/8265
Adding a merchandise link to the marketplace selector
2016-12-09 11:57:36 -08:00
Keith Holliday 29854d3bdb Merge pull request #8271 from Hus274/8266
Adding habitica mugs to the static merchandise page
2016-12-09 11:54:45 -08:00
Sabe Jones f8751b002c fix(subs): record creation for gifts 2016-12-09 19:52:17 +00:00
Travis cd545e08d5 Implementing retries on failed user updates when finishing a quest (#8251)
* Implementing retries on failed user updates when finishing a quest. fixes #8035

* Refactoring mongo db retries to use the same as code path as original call and moving retries to count based over time based.

* Adding tests for retry logic and updating retries to happen recursively

* Moving callbacks to promises and other tweaks according to pr.

* Chaging mongoose promise to use .catch() functionality

* If all retries fail, the system will now throw an error instead of returning an error message.
2016-12-09 11:30:17 -08:00
Travis Husman f69bb4f023 Adding habitica mugs to the static/merch page. fixes #8266 2016-12-09 10:35:00 -08:00
Tom Rasmussen 847081d2b2 Removed extra newlines 2016-12-09 12:01:05 -05:00
Travis Husman 8112d46ea4 Removing line break 2016-12-09 07:32:40 -08:00
Travis Husman d13bded647 Updating the merchandise link to look like a list item. 2016-12-09 07:31:48 -08:00
Matteo Pagliazzi 1de4ab3612 client: namespaces for actions and getteters 2016-12-08 23:01:59 -08:00
Keith Holliday f9f22f313f Merge pull request #8261 from b9chris/develop
Fix a bug where 1005-768 the avatars and health bar get covered up.
2016-12-08 18:50:18 -08:00
Sabe Jones f57eed85a8 3.59.0 2016-12-09 02:42:13 +00:00
Sabe Jones 10dd3318ab fix(subs): append Gift for troubleshooting clarity 2016-12-09 02:35:51 +00:00
tallestthomas cbef83c14a Removed yarn.lock and added it to the .gitignore 2016-12-08 21:31:14 -05:00
Sabe Jones 59709a8590 chore(sprites): compile
and Bailey
2016-12-09 02:30:39 +00:00
Sabe Jones f85f2a0c6d Gift Subscriptions Promo (#8270)
* WIP(promo): buy-1-get-1 subs

* WIP(subscriptions): Slack integration

* feat(Slack): notify on sub buy
2016-12-08 18:08:56 -08:00
Travis Husman 605a5a1d5c Adding a merchandise link to the marketplace selector. fixes #8265 2016-12-08 08:19:13 -08:00
tallestthomas 2d5d786c8e Added pet food link to pets.json and jade template (issue #8023) 2016-12-08 11:05:22 -05:00
Travis 5efe5b7b10 updating dragon mount images to all align. fixes #8253 (#8259) 2016-12-08 19:52:12 +10:00
Alexander Lin 3e92bb22fa Refactor, add spec tests for equipment search 2016-12-08 00:08:18 -08:00
Sabe Jones 1249b9d410 feat(content): Dec 2016 pets 2016-12-07 20:57:25 +00:00
Keith Holliday 197aafe092 Updated the button stlyes to make them closer (#8260) 2016-12-07 11:47:24 -08:00
tallestthomas 79829ca128 Added link to pet food wiki (#8023) 2016-12-07 10:30:29 -05:00
Chris Moschini adaa1d9a3e Fix a bug where 1005-768 the avatars and health bar get covered up. 2016-12-07 08:55:36 -05:00
Matteo Pagliazzi 3e6691dbbb client: test: getters 2016-12-06 18:53:03 -08:00
Matteo Pagliazzi 046761b9aa client: reorganize filters and add tests 2016-12-06 18:27:49 -08:00
Matteo Pagliazzi 0b0466b960 client: reorganize actions 2016-12-06 17:11:40 -08:00
Matteo Pagliazzi f8d4a2bd6b client: update blue and yellow colors 2016-12-06 14:05:01 -08:00
Matteo Pagliazzi 1af59a3770 client: move files again to fix tests 2016-12-06 13:17:23 -08:00
Matteo Pagliazzi bbcb13c91b client: reorganize store files 2016-12-06 12:47:47 -08:00
Matteo Pagliazzi d27dc46c50 chore(i18n): update locales 2016-12-05 16:13:45 -08:00
Alexander Lin 679459b83b Adds free-text filter to equipment page
Closes #8241
2016-12-05 01:06:56 -08:00
Blade Barringer 5a619773d5 chore(i18n): update locales 2016-12-05 01:12:42 -06:00
Marcel Oyuela-Bonzani ad76ab1315 Triple Equals sign 2016-12-04 13:40:46 -05:00
Marcel Oyuela-Bonzani 15eb8db925 Fix for issue noted. 2016-12-04 04:31:04 -05:00
MathWhiz a0e92c5605 change stable text (#8247) 2016-12-04 17:37:23 +10:00
Chris eac3e36c07 changed favicon - fixes #7720 (#8252)
* changed favicon

* Revert "changed favicon"

This reverts commit f28b9eb738.

* Changed Favicon

 fixes #7720
2016-12-04 17:30:35 +10:00
Alys 0b8def555b make default profile name more descriptive (ref https://github.com/HabitRPG/habitica/commit/dca958f2e23f38492fd9abbe426265aae4c71dd4) 2016-12-04 13:43:49 +10:00
Alys 5f5fa5c2eb rename Library of Shared Lists guild to Library of Tasks and Challenges 2016-12-04 12:49:43 +10:00
Sabe Jones 1eac8bbbbe fix(migration): correct comments 2016-12-02 09:48:26 -06:00
Alys 49c7580cd4 3.58.1 2016-12-02 19:41:31 +10:00
Matteo Pagliazzi dca958f2e2 make sure entire user is loaded when saved 2016-12-02 10:20:54 +01:00
Sabe Jones eae5f0d605 fix(sprites): staff position 2016-12-02 00:08:44 +00:00
Sabe Jones 6ab091645c 3.58.0 2016-12-01 21:44:23 +00:00
Sabe Jones d66041c280 chore(sprites): compile 2016-12-01 21:43:46 +00:00
Sabe Jones de070a450a feat(content): BGs and Armoire 2016-12 2016-12-01 21:22:22 +00:00
Sabe Jones eaaab35f31 fix(stats): back & body works
Also adds December Take This migration
2016-12-01 18:49:40 +00:00
Travis 6a63f080ad New feature that notifies a user when their group invite is accepted. (#8244)
* New notification feature that notifies a user when their group invite is accepted. fixes #7788

* Updating to a modal instead of a popup notification

* Making a generic modal template and using it for notifications of group invitation acceptance.

* Working with paglias's comments for doing translation server side.

* Final changes based on pr comments.
2016-12-01 19:04:57 +01:00
Myles Louis Dakan c42f81b629 changed quest images for egg and knight1 (#8245) 2016-12-01 10:58:38 -06:00
Sabe Jones 9a78a7b896 fix(npcs): remove foamy Daniel 2016-12-01 15:44:08 +00:00
Alys 8b70721137 change date for latest Bailey from 11/24 to 11/30 2016-12-01 20:51:36 +10:00
Alys 44ffbd716d make gem cap reset message more accurate for when you're reading it at the start of a month 2016-12-01 20:15:13 +10:00
Sabe Jones 5bfc3a5ff4 3.57.2 2016-11-30 21:07:17 +00:00
Sabe Jones 0ba5df4164 chore(news): Last Chance Bailey 2016-11-30 20:43:55 +00:00
Sabe Jones 52a59c8192 Revert "Display first login incentive reward when bailey is dismissed (#8234)"
This reverts commit ac732b2c85.
2016-11-30 20:42:39 +00:00
Matteo Pagliazzi c1a860494d chore(i18n): update locales 2016-11-30 14:08:34 +01:00
Keith Holliday 395dafa127 Merge pull request #8242 from TheHollidayInn/login-incentives-remove-multiple-notifications
Login incentives remove multiple notifications
2016-11-29 09:51:34 -06:00
Keith Holliday bab41647f5 Fixed lint issues 2016-11-29 09:18:07 -06:00
Keith Holliday 8582a67308 Fixed broken tests and style changes 2016-11-29 08:53:36 -06:00
Marcel Oyuela-Bonzani 0d58fb0fd3 Reloads the quest panel solving issue #7697 2016-11-28 22:29:20 -05:00
Keith Holliday 1d2482f8bc Fixed linting issues 2016-11-28 21:24:25 -06:00
Keith Holliday f4cf906127 Added remove when previous login incentive notifications exist 2016-11-28 21:19:53 -06:00
Sabe Jones 559f9b1825 3.57.1 2016-11-28 21:40:34 +00:00
Sabe Jones c7039bc9ea fix(incentives): pixel paws, purple background
Also turns off automatic Base Turkey pet award for new users.
2016-11-28 20:57:35 +00:00
Matteo Pagliazzi f929d36e1a chore(i18n): update locales 2016-11-28 19:43:06 +01:00
Keith Holliday 254d1a3465 Merge pull request #8237 from TheHollidayInn/login-progress-counter-fix
Fixed login counter on the first day
2016-11-26 17:36:01 -06:00
Alys 442aae8a35 fix spelling mistake in Check-In Incentives social media messages (consitent > consistent) 2016-11-27 07:29:47 +10:00
Keith Holliday bcb0ed0a5c Fixed login counter on the first day 2016-11-26 10:30:09 -06:00
Alys a48b8f0e34 add whitespace between GP and XP amount and label in quest modals 2016-11-26 18:32:37 +10:00
Alyssa Batula 7eeeda2aae Send a message to the party chat when a quest is aborted, fixes #4879 (#8150)
* Send a message to the party chat when a quest is aborted

* Added test cases for sending a message to party when quest is aborted

* Restore Group.prototype.sendChat after aborted quest test
2016-11-26 17:48:42 +10:00
Sabe Jones a5ad9c30f0 fix(mobile): temp remove new unlock styles 2016-11-24 06:06:11 +00:00
Keith Holliday ac732b2c85 Display first login incentive reward when bailey is dismissed (#8234) 2016-11-23 21:44:11 -06:00
Sabe Jones a56b2d68fb 3.57.0 2016-11-24 02:05:03 +00:00
Sabe Jones 25b0ff38c4 Login Incentives (#8230)
* feat(incentives): login bennies WIP

* feat(content): incentive prize content WIP

* fix(content): placeholders pass tests

* WIP(content): Bard instrument placeholder

* feat(content): Incentives build

* chore(sprites): compile
and fix some strings

* WIP(incentives): quests and backgrounds

* fix(quests): correct buy/launch handling

* [WIP] Incentives rewarding (#8226)

* Added login incentive rewards

* Updated incentive rewards

* Added incentive modal and updated notification structure

* Added analytics to sleeping

* Added login incentives to user analytics

* Fixed unit tests and ensured that prizes are incremented and not replaced

* Updated style of daily login incentive modal

* Added rewards modal

* Added translations

* Added loigin incentive ui elements to profile

* Updated login incentives structure and abstracted to common.content

* Added dynamic display for login incentives on profile

* Added purple potion image

* Updated daily login modal

* Fixed progress calculation

* Added bard gear

* Updated login incentive rewards

* Fixed styles and text

* Added multiple read for notifications

* Fixed lint issues

* Fixed styles and added 50 limit

* Updated quest keys

* Added login incentives reward page

* Fixed tests

* Fixed linting and tests

* Read named notifications route. Add image for backgrounds

* Fixed style issues and added tranlsations to login incentive notification

* Hided abiltiy to purchase incentive backgrounds and added message to detail how to unlock

* Updated awarded message

* Fixed text and updated progress counter to display better

* Fixed purple potion reward text

* Fixed check in backgrouns reward text

* fix(quest): pass tests

* Added display of multiple rewards

* Updated modal styles

* Fixed neagtive 50 issue

* Remvoed total count from daily login incentives modal

* Fixed magic paw display

* fix(awards): give bunnies again

* WIP(incentives): more progress on BG shop

* fix(incentives): actually award backgrounds

* fix(incentives): more BG fixy

* fix(backgrounds): don't gem-buy checkin bgs

* Added dust bunny notification

* fix(incentives): don't redisplay bunny award

* chore(news): Bailey
and different promo sprite
2016-11-23 19:34:09 -06:00
Sabe Jones dcc06931cc fix(test): disable unreliable XML test 2016-11-23 23:16:16 +00:00
Matteo Pagliazzi bc3ebbd095 pin mongoose 2016-11-23 21:22:35 +01:00
Matteo Pagliazzi e5b9581743 update vue 2016-11-23 20:02:35 +01:00
Matteo Pagliazzi 4b9fe49e3a skip randomly failing test 2016-11-23 14:46:23 +01:00
Alys ab4c8b0a46 delete broken newsArchive translation and link from Whats New page to fix https://github.com/HabitRPG/habitica/pull/8216#issuecomment-262430401 2016-11-23 21:39:14 +10:00
Sabe Jones f6c26fe869 3.56.0 2016-11-23 02:05:23 +00:00
Sabe Jones 80e9735b28 Turkey Day 2016 (#8231)
* feat(event): Turkey Day 2016

* fix(test): allow for free pet
2016-11-22 20:00:10 -06:00
Matteo Pagliazzi aa6f188bd9 new client: remove comments 2016-11-22 13:49:35 +01:00
MathWhiz e8b7660376 Add Costume Info to member modal (#7768)
* Add localization strings

* Change name of Equipment section

* Add costume section to member modal

* Add costume section to member modal

* Add current pet and current mount info

* Reorder Sections and Separate Active Mounts/Pets

* switch ng-show with ng-if

* Add `noActiveMount` to pets.json

* Breaking Stuff

* Add petservices.js to the manifest

* Remove Extra Parenthesis

* Progress towards backgrounds

* Add semicolons

* Add background information

* Add all methods in petServices to userCtrl and memberModalCtrl

* Add avatar settings

* Add semicolons

* Revert "Add avatar settings"

This reverts commit 6e8cca9736.

* Remove active-pet-and-mount

* Remove Content from memberModalCtrl

* Update costumeServices.js

* Make costumeservices.js more readable

* Update costumeServices.js

* Update costumeService logic

* Remove unused strings

* Fix include statements

* move service

* Update pet/mount logic

* fixes

* Fix background logic
2016-11-21 21:19:13 +10:00
Mich Elliott 7d76622410 Remove white backgrounds from mount sprites (#8217)
* Remove white background from single mount sprite

* Remove white background on ~40 mount sprites
2016-11-21 07:48:41 +10:00
MathWhiz 928e5f66c4 Add translation link to news (#8216)
* Add translation link to news

* Add newsArchive string

* Translate news archive link
2016-11-20 14:22:59 +10:00
MathWhiz 6a343535c0 Remove party joined option (#8212)
* Remove party joined option

* Make default sort sort by profile name

* Remove extraneous comment
2016-11-20 14:08:18 +10:00
Alys f58f6acb44 correct apidoc comments for updating and deleting a tag 2016-11-19 12:37:21 +10:00
Matteo Pagliazzi 64754777ed New Client: working navigation (#8131)
* initial work

* new client: working navigation and tasks showing up

* finish header menu and add avatar component

* fix sprites in new client

* initial header version

* initial styling for top menu

* more progress on the header menu

* almost complete menu and avatar

* correctly apply active class for /social and /help

* fix header colors and simplify css

* switch from Roboto to native fonts

* remove small avatar and add viewport

* fixes

* fix user menu with and progress bars

* fix avatar rendeting

* move bars colors to theme

* add site overrides

* fix tests

* shrinkwrap

* fix sprites path

* another try at fixing the sprites path

* another try at fixing the sprites path
2016-11-18 19:20:25 +01:00
Sabe Jones 3b5e4b6d84 3.55.0 2016-11-17 22:26:26 +00:00
Sabe Jones 9383578cb8 chore(news): Bailey 2016-11-17 21:29:26 +00:00
Sabe Jones 474672ec64 Merge pull request #8225 from HabitRPG/sabrecat/hairstyles
New Hairstyles
2016-11-17 15:20:55 -06:00
Keith Holliday 25c6691793 Added party sync and request sync events (#8223)
* Added party sync and request sync events

* Changed party member sync to be handled locally

* Optimized assignment to only use member variables

* Removed party sync event
2016-11-17 20:10:33 +01:00
Keith Holliday 3ea7b72024 Passed language param to text functions (#8220) 2016-11-17 19:32:07 +01:00
Sabe Jones 2d6f05a9a4 fix(hairstyles): base layer above bangs 2016-11-17 17:49:35 +00:00
Sabe Jones 28637286d6 fix(sprites): remove base outlines 2016-11-17 17:49:35 +00:00
Sabe Jones 874887b790 fix(hair): exclusivity and canvas tweaks 2016-11-17 17:49:34 +00:00
Sabe Jones c977e5ebb5 fix(sprites): adjust Y position 2016-11-17 17:49:34 +00:00
Sabe Jones f040e668f3 chore(sprites): add 2016-11-17 17:49:34 +00:00
Sabe Jones 55a15f938c feat(customize): new hairstyles 2016-11-17 17:45:31 +00:00
shalott 8c4f35daf4 Fixing test failure
This test seems to occasionally start failing (another coder reported the same thing happening to them in the blacksmiths’ guild) because the order in which the tasks are created can sometimes not match the order in the array. So I have sorted the tasks array after creation by the task name to ensure a consistent ordering, and slightly reordered the expect statements to match.
2016-11-16 21:52:23 +01:00
Matteo Pagliazzi 8f38ce3424 do not give _id to purchased.plan 2016-11-16 21:44:47 +01:00
Arashi007 b8f57a74d0 Added Airu's Theme (#8204)
* Airu's Theme
* Delete Minus_Habit.ogg
* Delete Minus_Habit.mp3
* Add files via upload
2016-11-16 20:11:22 +10:00
Sabe Jones 7ed26c0dbe 3.54.0 2016-11-16 02:12:33 +00:00
Sabe Jones e8f5b26d4d chore(sprites): compile
and Bailey
2016-11-16 02:04:10 +00:00
Sabe Jones 0273648b6b Merge pull request #8221 from HabitRPG/sabrecat/pets-201611
Ferret Pet
2016-11-15 19:48:33 -06:00
Sabe Jones b6fdac8885 feat(quest): Ferret Pet 2016-11-15 22:14:12 +00:00
Sabe Jones 00e6389672 3.53.5 2016-11-15 04:21:46 +00:00
Blade Barringer e02c669b61 Move hr to prevent UserID comment from showing (#8214) 2016-11-14 21:59:31 -06:00
Blade Barringer f0cb7c6bf3 Comment out group task fetching 2016-11-14 21:42:23 -06:00
Sabe Jones 571ef0b309 fix(news): add date 2016-11-15 03:34:50 +00:00
Blade Barringer 74328d1bcc chore(i18n): update locales 2016-11-14 21:33:01 -06:00
Sabe Jones d34a9d828c Removed task get approvals request (#8218) 2016-11-14 21:09:36 -06:00
Keith Holliday 2fd35b3a40 Removed task get approvals request 2016-11-14 21:05:54 -06:00
Sabe Jones e27512f626 3.53.4 2016-11-15 00:46:15 +00:00
Sabe Jones dbf9cb3b4e chore(news): misc Bailey 2016-11-15 00:27:40 +00:00
AccioBooks 34c1245519 Move hr to prevent UserID comment from showing 2016-11-14 10:58:03 -06:00
Keith Holliday f602bfe438 Removed group subscription options (#8211) 2016-11-13 20:24:59 +01:00
Alys 9aa4b8aa64 add 'month' to gift subscription message - fixes https://github.com/HabitRPG/habitica/issues/7747
I haven't pluralised this by using "month(s)", because the phrase "a 3 month subscription" is acceptable in English. Translators may use pluralisation as desired, although note that the same wording will be used for 1 month subscriptions.
2016-11-13 20:40:01 +10:00
Alys 5a150ebc5b change '/group/' to '/groups/' in docs for /api/v3/challenges/groups/:groupId 2016-11-13 17:43:52 +10:00
Keith Holliday cbe1892b50 Added note sync when user adds task to challenge, tests, and fixed challenge tests (#8200) 2016-11-12 23:48:22 +01:00
Keith Holliday 13df60e0dd Group approval ui (#8184)
* Added all ui components back

* Added group ui items back and initial group approval directive

* Added ability to mark tasks as requires approval. Added approvals ctrl. Added get approvals method to tasks service

* Added approval list view with approving functionality

* Added error to produce message when task requests approval

* Added notification display for group approvals

* Fixed notification read and adding task

* Fixed syncing with group approval required

* Added group id to notifications for redirect on client side

* Fixed approval request tests

* Fixed linting issues

* Removed expectation from beforeEach

* Moved string to locale

* Added eslint ignore

* Updated notification for group approved, added new icons, and updated styles

* Hid group plan ui
2016-11-12 23:47:45 +01:00
Blade Barringer 3ff7692528 chore(i18n): update locales 2016-11-11 08:08:46 -06:00
Sabe Jones 111bba84dc feat(content): 2016-11 pet quest strings 2016-11-10 23:12:42 +00:00
Keith Holliday b0d2b72b88 Updated buy special item to use function call wrapper (#8203) 2016-11-10 21:36:49 +01:00
Sabe Jones 696317ea8a fix(quests): Basilist error with no party 2016-11-07 15:30:44 +00:00
Sabe Jones 593178a46a fix(sprites): copy corrected Ian to prod path
fixes #7867 (again)
2016-11-07 15:20:04 +00:00
MathWhiz f8fe16482d Unsubscribe documentation
closes #8187
2016-11-06 21:41:12 -06:00
Romeeka Gayhart 5108480ec5 Get skipped/pending unit tests working for revive (#8193) 2016-11-06 21:17:52 -06:00
Sabe Jones 95968b1b1c 3.53.3 2016-11-06 22:05:28 +00:00
Sabe Jones 566569af98 fix(event): end Fall Fest f'real 2016-11-06 21:36:52 +00:00
Alys 6693e9fca9 replace candy food with normal food and enhance canBuy / canDrop code (#8194)
* change food to normal; add variables to choose type of food; add canBuy, canDrop to cake

* reinstate ability to control canBuy and canDrop separately
2016-11-06 15:33:19 -06:00
Romeeka Gayhart 431bde56d2 Convert test UUID to string to avoid test error (#8195) 2016-11-05 23:53:11 -04:00
Sabe Jones 7cf17c0e63 3.53.2 2016-11-04 21:15:10 +00:00
Sabe Jones 49561bfc8c fix(test): accommodate changing seasons 2016-11-04 20:38:29 +00:00
Sabe Jones 8cbbb58e78 chore(event): end Fall Fest 2016-11-04 20:20:53 +00:00
Sabe Jones 905549e379 3.53.1 2016-11-04 19:23:55 +00:00
Sabe Jones 5d45c7209a chore(news): blog Bailey 2016-11-04 19:02:29 +00:00
Rick Kasten 371cddfe17 Updated bossColl1, bossColl1Broken (#8148) 2016-11-04 19:38:12 +10:00
AccioBooks fcfac30caa Api doc status (#8165)
* Add example

* Update example
2016-11-03 08:31:30 -05:00
Corinna Jaschek b094fb1e52 added message for challenges that could not be found - fixes #5538
closes #8176
2016-11-03 07:52:43 -05:00
Keith Holliday a2dd82b6db Hid nav bar (#8181) 2016-11-02 21:58:17 -05:00
Sabe Jones e6071610e4 fix(migration): revert bogus connect info 2016-11-03 00:29:57 +00:00
Sabe Jones bdd0e2bb79 3.53.0 2016-11-03 00:12:32 +00:00
Sabe Jones 054a9a6f2b chore(sprites): compile 2016-11-02 23:29:12 +00:00
Sabe Jones 35b9ed6273 backgrounds and Armoire 2016-11 (#8178)
* feat(content): backgrounds and Armoire 2016-11

* chore(event): November Take This migration

* chore(news): Bailey
2016-11-02 18:27:32 -05:00
Keith Holliday e65277baa5 Added check to ensure config is defined (#8180) 2016-11-02 18:27:22 -05:00
Amanda Furrow 421bd8624c Add flagger language to flag message sent to slack
closes #8179
fixes #8140
2016-11-02 17:28:44 -05:00
Blade Barringer 4562c6422a chore(i18n): update locales 2016-11-02 17:20:46 -05:00
Matteo Pagliazzi a5cd9f2473 Merge branch 'TheHollidayInn-group-tasks-approval2' into develop 2016-11-01 21:55:32 +01:00
Matteo Pagliazzi 18bbdfa84b Merge branch 'group-tasks-approval' of https://github.com/TheHollidayInn/habitrpg into TheHollidayInn-group-tasks-approval2 2016-11-01 21:55:18 +01:00
Keith Holliday d8c37f6e2d Group plan subscription (#8153)
* Added payment to groups and pay with group plan with Stripe

* Added edit card for Stripe

* Added stripe cancel

* Added subscribe with Amazon payments

* Added Amazon cancel for group subscription

* Added group subscription with paypal

* Added paypal cancel

* Added ipn cancel for Group plan

* Added a subscription tab and hid only the task tab when group is not subscribed

* Fixed linting issues

* Fixed tests

* Added payment unit tests

* Added back refresh after stripe payment

* Fixed style issues

* Limited grouop query fields and checked access

* Abstracted subscription schema

* Added year group plan and more access checks

* Maded purchase fields private

* Removed id and timestampes

* Added else checks to ensure user subscription is not altered. Removed active field from group model

* Added toJSONTransform function

* Moved plan active check to other toJson function

* Added check to see if purchaed has been populated

* Added purchase details to private

* Added correct data usage when paying for group sub
2016-11-01 21:51:30 +01:00
Sabe Jones 7f38c61c70 3.52.0 2016-10-31 19:02:24 +00:00
Sabe Jones 1c018cedb1 chore(event): sprites and news 2016-10-31 18:45:39 +00:00
Sabe Jones 80892bd6a8 feat(event): JackOLantern ladder (#8174) 2016-10-31 08:14:06 -05:00
Keith Holliday 6801dae75d Fixed history test 2016-10-30 03:23:01 -05:00
Keith Holliday 59e1de6771 Moved approval to subdoc 2016-10-29 14:19:16 -05:00
Keith Holliday 5b240a1950 Updated notification name and other minor fixes 2016-10-29 14:19:16 -05:00
Keith Holliday 3ec3722038 Moved approval fields to group subdoc 2016-10-29 14:19:16 -05:00
Keith Holliday d798ebadfe Fixed line endings 2016-10-29 14:19:16 -05:00
Keith Holliday 6cbddef627 Added get approvals route 2016-10-29 14:19:16 -05:00
Keith Holliday 016de411c9 Added notifications 2016-10-29 14:19:16 -05:00
Keith Holliday 2173f53883 Added fields for more approver details 2016-10-29 14:19:15 -05:00
Keith Holliday f2e5bc52e5 Added requested approval fields and logic 2016-10-29 14:19:15 -05:00
Keith Holliday 393a9290e9 Added approval test and fixed line endings 2016-10-29 14:19:15 -05:00
Keith Holliday ad5045bc09 Added git score approved task test 2016-10-29 14:19:15 -05:00
Keith Holliday 9b515ebdd1 Added task approve route 2016-10-29 14:19:15 -05:00
Keith Holliday 97bf9ee8e8 Added inital group task approval 2016-10-29 14:19:15 -05:00
Blade Barringer f5ba636579 chore(i18n): update locales 2016-10-27 22:03:58 -05:00
Sabe Jones 4dd7e49552 3.51.1 2016-10-27 20:44:17 +00:00
Sabe Jones d2f673ef1e chore(news): Blog Bailey 2016-10-27 19:58:43 +00:00
Sabe Jones e198dd551a feat(content): strings for BGs/Armoire 2016-11 2016-10-26 20:28:44 +00:00
Travis 0bfc9d9516 fix: allows user to save an alias and checklistCollapsed properties of a challenge task. fixes #7875 (#8170) 2016-10-25 21:47:49 -05:00
2477 changed files with 102317 additions and 35968 deletions
+3
View File
@@ -0,0 +1,3 @@
node_modules
.git
website
+1 -1
View File
@@ -14,7 +14,7 @@ files:
owner: root
group: users
content: |
$(ls -td /opt/elasticbeanstalk/node-install/node-* | head -1)/bin/npm install -g npm@3
$(ls -td /opt/elasticbeanstalk/node-install/node-* | head -1)/bin/npm install -g npm@4
container_commands:
01_makeBabel:
command: "touch /tmp/.babel.json"
+1 -5
View File
@@ -20,8 +20,4 @@ website/common/browserify.js
test/content/**/*
Gruntfile.js
gulpfile.js
gulp
webpack
test/client/e2e
test/client/unit/index.js
test/client/unit/karma.conf.js
gulp
+2 -2
View File
@@ -4,7 +4,7 @@
"node": true,
},
"extends": [
"habitrpg/es6",
"habitrpg"
"habitrpg",
"habitrpg/esnext"
],
}
+1
View File
@@ -9,5 +9,6 @@ Fixes put_issue_url_here
[//]: # (Put User ID in here - found in Settings -> API)
----
UUID:
+1 -1
View File
@@ -1 +1 @@
4.3.1
6
+2 -2
View File
@@ -1,8 +1,8 @@
language: node_js
node_js:
- '4.3.1'
- '6'
before_install:
- npm install -g npm@3
- npm install -g npm@4
- if [ $REQUIRES_SERVER ]; then sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10; echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list; sudo apt-get update; sudo apt-get install mongodb-org-server; fi
before_script:
- npm run test:build
+6 -4
View File
@@ -17,20 +17,22 @@ RUN apt-get install -y \
python
# Install NodeJS
RUN curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash -
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 npm@3
RUN npm install -g gulp grunt-cli bower
RUN npm install -g gulp grunt-cli bower mocha
# Clone Habitica repo and install dependencies
WORKDIR /habitrpg
RUN git clone https://github.com/HabitRPG/habitrpg.git /habitrpg
RUN git clone https://github.com/HabitRPG/habitica.git /habitrpg
RUN npm install
RUN bower install --allow-root
+2 -1
View File
@@ -57,7 +57,7 @@ module.exports = function(grunt) {
files: [
{expand: true, cwd: 'website/client-old/', src: 'favicon.ico', dest: 'website/build/'},
{expand: true, cwd: 'website/client-old/', src: 'favicon_192x192.png', dest: 'website/build/'},
{expand: true, cwd: 'website/assets/sprites/dist/', src: 'spritesmith*.png', dest: 'website/build/'},
{expand: true, cwd: 'website/assets/sprites/dist/', src: 'spritesmith*.png', dest: 'website/build/static/sprites'},
{expand: true, cwd: 'website/assets/sprites/', src: 'backer-only/*.gif', dest: 'website/build/'},
{expand: true, cwd: 'website/assets/sprites/', src: 'npc_ian.gif', dest: 'website/build/'},
{expand: true, cwd: 'website/assets/sprites/', src: 'quest_*.gif', dest: 'website/build/'},
@@ -78,6 +78,7 @@ module.exports = function(grunt) {
'website/build/favicon.ico',
'website/build/favicon_192x192.png',
'website/build/*.png',
'website/build/static/sprites/*.png',
'website/build/*.gif',
'website/build/bower_components/bootstrap/dist/fonts/*'
],
+1 -1
View File
@@ -1,4 +1,4 @@
Habitica [![Build Status](https://travis-ci.org/HabitRPG/habitica.svg?branch=develop)](https://travis-ci.org/HabitRPG/habitica) [![Code Climate](https://codeclimate.com/github/HabitRPG/habitrpg.svg)](https://codeclimate.com/github/HabitRPG/habitrpg) [![Coverage Status](https://coveralls.io/repos/HabitRPG/habitrpg/badge.svg?branch=develop)](https://coveralls.io/r/HabitRPG/habitrpg?branch=develop) [![Bountysource](https://api.bountysource.com/badge/tracker?tracker_id=68393)](https://www.bountysource.com/trackers/68393-habitrpg?utm_source=68393&utm_medium=shield&utm_campaign=TRACKER_BADGE)
Habitica [![Build Status](https://travis-ci.org/HabitRPG/habitica.svg?branch=develop)](https://travis-ci.org/HabitRPG/habitica) [![Code Climate](https://codeclimate.com/github/HabitRPG/habitrpg.svg)](https://codeclimate.com/github/HabitRPG/habitrpg) [![Coverage Status](https://coveralls.io/repos/github/HabitRPG/habitica/badge.svg?branch=develop)](https://coveralls.io/github/HabitRPG/habitica?branch=develop) [![Bountysource](https://api.bountysource.com/badge/tracker?tracker_id=68393)](https://www.bountysource.com/trackers/68393-habitrpg?utm_source=68393&utm_medium=shield&utm_campaign=TRACKER_BADGE)
===============
[Habitica](https://habitica.com) is an open source habit building program which treats your life like a Role Playing Game. Level up as you succeed, lose HP as you fail, earn money to buy weapons and armor.
+2 -1
View File
@@ -79,6 +79,7 @@
},
"SLACK": {
"FLAGGING_URL": "https://hooks.slack.com/services/id/id/id",
"FLAGGING_FOOTER_LINK": "https://habitrpg.github.io/flag-o-rama/"
"FLAGGING_FOOTER_LINK": "https://habitrpg.github.io/flag-o-rama/",
"SUBSCRIPTIONS_URL": "https://hooks.slack.com/services/id/id/id"
}
}
+1 -1
View File
@@ -1,3 +1,3 @@
web:
volumes:
- '.:/habitrpg'
- '.:/habitrpg'
+4 -4
View File
@@ -1,13 +1,13 @@
web:
build: .
ports:
- "3000:3000"
- "3000:3000"
links:
- mongo
- mongo
environment:
- NODE_DB_URI=mongodb://mongo/habitrpg
- NODE_DB_URI=mongodb://mongo/habitrpg
mongo:
image: mongo
ports:
- "27017:27017"
- "27017:27017"
+12 -2
View File
@@ -12,6 +12,9 @@ import {each} from 'lodash';
const MAX_SPRITESHEET_SIZE = 1024 * 1024 * 3;
const DIST_PATH = 'website/assets/sprites/dist/';
const IMG_DIST_PATH_NEW_CLIENT = 'website/static/sprites/';
const CSS_DIST_PATH_NEW_CLIENT = 'website/client/assets/css/sprites/';
gulp.task('sprites:compile', ['sprites:clean', 'sprites:main', 'sprites:largeSprites', 'sprites:checkCompiledDimensions']);
gulp.task('sprites:main', () => {
@@ -25,7 +28,7 @@ gulp.task('sprites:largeSprites', () => {
});
gulp.task('sprites:clean', (done) => {
clean(`${DIST_PATH}spritesmith*`, done);
clean(`{${DIST_PATH}spritesmith*,${IMG_DIST_PATH_NEW_CLIENT}spritesmith*,${CSS_DIST_PATH_NEW_CLIENT}spritesmith*}`, done);
});
gulp.task('sprites:checkCompiledDimensions', ['sprites:main', 'sprites:largeSprites'], () => {
@@ -66,14 +69,16 @@ function createSpritesStream (name, src) {
algorithm: 'binary-tree',
padding: 1,
cssTemplate: 'website/assets/sprites/css/css.template.handlebars',
cssVarMap: cssVarMap
cssVarMap: cssVarMap,
}));
let imgStream = spriteData.img
.pipe(imagemin())
.pipe(gulp.dest(IMG_DIST_PATH_NEW_CLIENT))
.pipe(gulp.dest(DIST_PATH));
let cssStream = spriteData.css
.pipe(gulp.dest(CSS_DIST_PATH_NEW_CLIENT))
.pipe(gulp.dest(DIST_PATH));
stream.add(imgStream);
@@ -148,4 +153,9 @@ function cssVarMap (sprite) {
}
if (~sprite.name.indexOf('shirt'))
sprite.custom.px.offset_y = `-${ sprite.y + 30 }px`; // even more for shirts
if (~sprite.name.indexOf('hair_base')) {
let styleArray = sprite.name.split('_').slice(2,3);
if (Number(styleArray[0]) > 14)
sprite.custom.px.offset_y = `-${ sprite.y }px`; // don't crop updos
}
}
+1 -1
View File
@@ -318,7 +318,7 @@ gulp.task('test:api-v3:integration:watch', () => {
gulp.task('test:api-v3:integration:separate-server', (done) => {
let runner = exec(
testBin('mocha test/api/v3/integration --recursive', 'LOAD_SERVER=0'),
testBin('mocha test/api/v3/integration --recursive --require ./test/helpers/start-server', 'LOAD_SERVER=0'),
{maxBuffer: 500 * 1024},
(err, stdout, stderr) => done(err)
);
+2 -2
View File
@@ -84,8 +84,8 @@ gulp.task('transifex:malformedStrings', () => {
let malformedString = `${lang} - ${file} - ${key} - ${translationString}`;
stringsWithMalformedInterpolations.push(malformedString);
} else if (englishOccurences.length !== translationOccurences.length && !malformedStringExceptions[key]) {
let missingInterploationString = `${lang} - ${file} - ${key} - ${translationString}`;
stringsWithIncorrectNumberOfInterpolations.push(missingInterploationString);
let missingInterpolationString = `${lang} - ${file} - ${key} - ${translationString}`;
stringsWithIncorrectNumberOfInterpolations.push(missingInterpolationString);
}
});
});
+86
View File
@@ -0,0 +1,86 @@
var migrationName = '20161030-jackolanterns.js';
var authorName = 'Sabe'; // in case script author needs to know when their ...
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
/*
* set the newStuff flag in all user accounts so they see a Bailey message
*/
var mongo = require('mongoskin');
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
var dbUsers = mongo.db(connectionString).collection('users');
// specify a query to limit the affected users (empty for all users):
var query = {
'auth.timestamps.loggedin':{$gt:new Date('2016-10-01')} // remove when running migration a second time
};
// specify fields we are interested in to limit retrieved data (empty if we're not reading data):
var fields = {
'migration': 1,
'items.pets.JackOLantern-Base': 1,
'items.mounts.JackOLantern-Base': 1,
};
console.warn('Updating users...');
var progressCount = 1000;
var count = 0;
dbUsers.findEach(query, fields, {batchSize:250}, function(err, user) {
if (err) { return exiting(1, 'ERROR! ' + err); }
if (!user) {
console.warn('All appropriate users found and modified.');
setTimeout(displayData, 300000);
return;
}
count++;
// specify user data to change:
var set = {};
var inc = {};
if (user.migration !== migrationName) {
if (user.items.mounts['JackOLantern-Base']) {
set = {'migration':migrationName, 'items.pets.JackOLantern-Ghost':5};
} else if (user.items.pets['JackOLantern-Base']) {
set = {'migration':migrationName, 'items.mounts.JackOLantern-Base':true};
} else {
set = {'migration':migrationName, 'items.pets.JackOLantern-Base':5};
}
inc = {
'items.food.Candy_Base': 1,
'items.food.Candy_CottonCandyBlue': 1,
'items.food.Candy_CottonCandyPink': 1,
'items.food.Candy_Desert': 1,
'items.food.Candy_Golden': 1,
'items.food.Candy_Red': 1,
'items.food.Candy_Shade': 1,
'items.food.Candy_Skeleton': 1,
'items.food.Candy_White': 1,
'items.food.Candy_Zombie': 1,
}
}
dbUsers.update({_id:user._id}, {$set:set, $inc:inc});
if (count%progressCount == 0) console.warn(count + ' ' + user._id);
if (user._id == authorUuid) console.warn(authorName + ' processed');
});
function displayData() {
console.warn('\n' + count + ' users processed\n');
return exiting(0);
}
function exiting(code, msg) {
code = code || 0; // 0 = success
if (code && !msg) { msg = 'ERROR!'; }
if (msg) {
if (code) { console.error(msg); }
else { console.log( msg); }
}
process.exit(code);
}
+75
View File
@@ -0,0 +1,75 @@
var migrationName = '20161102_takeThis.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 Take This ladder items to participants in this month's challenge
*/
var mongo = require('mongoskin');
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
var dbUsers = mongo.db(connectionString).collection('users');
// specify a query to limit the affected users (empty for all users):
var query = {
'migration':{$ne:migrationName},
'challenges':{$in:['d1be0965-e909-4d30-82fa-9a0011f885b2']}
};
// specify fields we are interested in to limit retrieved data (empty if we're not reading data):
var fields = {
'items.gear.owned': 1
};
console.warn('Updating users...');
var progressCount = 1000;
var count = 0;
dbUsers.findEach(query, fields, {batchSize:250}, function(err, user) {
if (err) { return exiting(1, 'ERROR! ' + err); }
if (!user) {
console.warn('All appropriate users found and modified.');
setTimeout(displayData, 300000);
return;
}
count++;
// specify user data to change:
var set = {};
if (typeof user.items.gear.owned.head_special_takeThis !== 'undefined') {
set = {'migration':migrationName, 'items.gear.owned.body_special_takeThis':false};
} else if (typeof user.items.gear.owned.armor_special_takeThis !== 'undefined') {
set = {'migration':migrationName, 'items.gear.owned.head_special_takeThis':false};
} else if (typeof user.items.gear.owned.weapon_special_takeThis !== 'undefined') {
set = {'migration':migrationName, 'items.gear.owned.armor_special_takeThis':false};
} else if (typeof user.items.gear.owned.shield_special_takeThis !== 'undefined') {
set = {'migration':migrationName, 'items.gear.owned.weapon_special_takeThis':false};
} else {
set = {'migration':migrationName, 'items.gear.owned.shield_special_takeThis':false};
}
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);
}
+74
View File
@@ -0,0 +1,74 @@
var migrationName = '20161122_turkey_ladder.js';
var authorName = 'Sabe'; // in case script author needs to know when their ...
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
/*
* Yearly Turkey Day award. Turkey pet, Turkey mount, Gilded Turkey pet, Gilded Turkey mount
*/
var mongo = require('mongoskin');
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
var dbUsers = mongo.db(connectionString).collection('users');
// specify a query to limit the affected users (empty for all users):
var query = {
'migration':{$ne:migrationName},
'auth.timestamps.loggedin':{$gt:new Date('2016-10-31')} // Extend timeframe each run of migration
};
// specify fields we are interested in to limit retrieved data (empty if we're not reading data):
var fields = {
'migration': 1,
'items.mounts': 1,
'items.pets': 1,
};
console.warn('Updating users...');
var progressCount = 1000;
var count = 0;
dbUsers.findEach(query, fields, {batchSize:250}, function(err, user) {
if (err) { return exiting(1, 'ERROR! ' + err); }
if (!user) {
console.warn('All appropriate users found and modified.');
setTimeout(displayData, 300000);
return;
}
count++;
// specify user data to change:
var set = {};
if (user.items.pets['Turkey-Gilded']) {
set = {'migration':migrationName, 'items.mounts.Turkey-Gilded':true};
} else if (user.items.mounts['Turkey-Base']) {
set = {'migration':migrationName, 'items.pets.Turkey-Gilded':5};
} else if (user.items.pets['Turkey-Base']) {
set = {'migration':migrationName, 'items.mounts.Turkey-Base':true};
} else {
set = {'migration':migrationName, 'items.pets.Turkey-Base':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);
}
+73
View File
@@ -0,0 +1,73 @@
var migrationName = '20161230_nye_hats.js';
var authorName = 'Sabe'; // in case script author needs to know when their ...
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
/*
* Yearly New Year's party hat award
*/
var mongo = require('mongoskin');
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
var dbUsers = mongo.db(connectionString).collection('users');
// specify a query to limit the affected users (empty for all users):
var query = {
'migration':{$ne:migrationName},
'auth.timestamps.loggedin':{$gt:new Date('2016-11-30')} // Remove after first run
};
// specify fields we are interested in to limit retrieved data (empty if we're not reading data):
var fields = {
'items.gear.owned': 1,
};
console.warn('Updating users...');
var progressCount = 1000;
var count = 0;
dbUsers.findEach(query, fields, {batchSize:250}, function(err, user) {
if (err) { return exiting(1, 'ERROR! ' + err); }
if (!user) {
console.warn('All appropriate users found and modified.');
setTimeout(displayData, 300000);
return;
}
count++;
// specify user data to change:
var set = {};
if (typeof user.items.gear.owned.head_special_nye2015 !== 'undefined') {
set = {'migration':migrationName, 'items.gear.owned.head_special_nye2016':false};
} else if (typeof user.items.gear.owned.head_special_nye2014 !== 'undefined') {
set = {'migration':migrationName, 'items.gear.owned.head_special_nye2015':false};
} else if (typeof user.items.gear.owned.head_special_nye !== 'undefined') {
set = {'migration':migrationName, 'items.gear.owned.head_special_nye2014':false};
} else {
set = {'migration':migrationName, 'items.gear.owned.head_special_nye':false};
}
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);
}
+1 -1
View File
@@ -2,7 +2,7 @@ var _id = '';
var update = {
$addToSet: {
'purchased.plan.mysteryItems':{
$each:['head_mystery_201610','armor_mystery_201610']
$each:['head_mystery_201612','armor_mystery_201612']
}
}
};
+45 -22
View File
@@ -6,49 +6,70 @@ var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
* set the newStuff flag in all user accounts so they see a Bailey message
*/
var mongo = require('mongoskin');
var monk = require('monk');
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
var dbUsers = monk(connectionString).get('users', { castIds: false });
var dbUsers = mongo.db(connectionString).collection('users');
// specify a query to limit the affected users (empty for all users):
var query = {
'flags.newStuff':{$ne:true}
};
function processUsers(lastId) {
// specify a query to limit the affected users (empty for all users):
var query = {
'flags.newStuff': {$ne:true},
};
// specify fields we are interested in to limit retrieved data (empty if we're not reading data):
var fields = {
};
if (lastId) {
query._id = {
$gt: lastId
}
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 250,
fields: [] // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
})
.then(updateUsers)
.catch(function (err) {
console.log(err);
return exiting(1, 'ERROR! ' + err);
});
}
console.warn('Updating users...');
var progressCount = 1000;
var count = 0;
dbUsers.findEach(query, fields, {batchSize:250}, function(err, user) {
if (err) { return exiting(1, 'ERROR! ' + err); }
if (!user) {
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
setTimeout(displayData, 300000);
displayData();
return;
}
var userPaymentPromises = users.map(updateUser);
var lastUser = users[users.length - 1];
return Promise.all(userPaymentPromises)
.then(function () {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
// specify user data to change:
var set = {'flags.newStuff':true};
var set = {'flags.newStuff': true};
dbUsers.update({_id:user._id}, {$set:set});
dbUsers.update({_id: user._id}, {$set:set});
if (count%progressCount == 0) console.warn(count + ' ' + user._id);
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
if (user._id == authorUuid) console.warn(authorName + ' processed');
});
}
function displayData() {
console.warn('\n' + count + ' users processed\n');
return exiting(0);
}
function exiting(code, msg) {
code = code || 0; // 0 = success
if (code && !msg) { msg = 'ERROR!'; }
@@ -58,3 +79,5 @@ function exiting(code, msg) {
}
process.exit(code);
}
processUsers()
+5 -8
View File
@@ -6,14 +6,11 @@ var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
* Remove flag stating that the Enchanted Armoire is empty, for when new equipment is added
*/
var dbserver = 'localhost:27017'; // FOR TEST DATABASE
// var dbserver = 'username:password@ds031379-a0.mongolab.com:31379'; // FOR PRODUCTION DATABASE
var dbname = 'habitrpg';
var mongo = require('mongoskin');
var _ = require('lodash');
var dbUsers = mongo.db(dbserver + '/' + dbname + '?auto_reconnect').collection('users');
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
var dbUsers = mongo.db(connectionString).collection('users');
// specify a query to limit the affected users (empty for all users):
var query = {
@@ -22,7 +19,6 @@ var query = {
// specify fields we are interested in to limit retrieved data (empty if we're not reading data):
var fields = {
'flags.armoireEmpty':1
};
console.warn('Updating users...');
@@ -32,7 +28,8 @@ dbUsers.findEach(query, fields, {batchSize:250}, function(err, user) {
if (err) { return exiting(1, 'ERROR! ' + err); }
if (!user) {
console.warn('All appropriate users found and modified.');
return displayData();
setTimeout(displayData, 300000);
return;
}
count++;
+115
View File
@@ -0,0 +1,115 @@
var migrationName = 'restore_profile_data.js';
var authorName = 'ThehollidayInn'; // in case script author needs to know when their ...
var authorUuid = ''; //... own data is done
/*
* Check if users have empty profile data in new database and update it with old database info
*/
var monk = require('monk');
var connectionString = ''; // FOR TEST DATABASE
var dbUsers = monk(connectionString).get('users', { castIds: false });
var monk2 = require('monk');
var oldDbConnectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
var olDbUsers = monk2(oldDbConnectionString).get('users', { castIds: false });
function processUsers(lastId)
{
// specify a query to limit the affected users (empty for all users):
var query = {
// 'profile.name': 'profile name not found',
'profile.blurb': null,
// 'auth.timestamps.loggedin': {$gt: new Date('11/30/2016')},
};
if (lastId) {
query._id = {
$gt: lastId
}
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 250,
fields: ['_id', 'profile', 'auth.timestamps.loggedin'] // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
})
.then(updateUsers)
.catch(function (err) {
console.log(err);
return exiting(1, 'ERROR! ' + err);
});
}
var progressCount = 1000;
var count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
setTimeout(displayData, 300000);
return;
}
var userPaymentPromises = users.map(updateUser);
var lastUser = users[users.length - 1];
return Promise.all(userPaymentPromises)
.then(function () {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
if (!user.profile.name || user.profile.name === 'profile name not found' || !user.profile.imageUrl || !user.profile.blurb) {
return olDbUsers.findOne({_id: user._id}, '_id profile')
.then((oldUserData) => {
if (!oldUserData) return;
// specify user data to change:
var set = {};
if (oldUserData.profile.name === 'profile name not found') return;
var userNeedsProfileName = !user.profile.name || user.profile.name === 'profile name not found';
if (userNeedsProfileName && oldUserData.profile.name) {
set['profile.name'] = oldUserData.profile.name;
}
if (!user.profile.imageUrl && oldUserData.profile.imageUrl) {
set['profile.imageUrl'] = oldUserData.profile.imageUrl;
}
if (!user.profile.blurb && oldUserData.profile.blurb) {
set['profile.blurb'] = oldUserData.profile.blurb;
}
if (Object.keys(set).length !== 0 && set.constructor === Object) {
console.log(set)
return dbUsers.update({_id: user._id}, {$set:set});
}
});
}
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
if (user._id == authorUuid) console.warn(authorName + ' processed');
}
function displayData() {
console.warn('\n' + count + ' users processed\n');
return exiting(0);
}
function exiting(code, msg) {
code = code || 0; // 0 = success
if (code && !msg) { msg = 'ERROR!'; }
if (msg) {
if (code) { console.error(msg); }
else { console.log( msg); }
}
process.exit(code);
}
processUsers()
+80
View File
@@ -0,0 +1,80 @@
var migrationName = '20170103_takeThis.js'; // Update per month
var authorName = 'Sabe'; // in case script author needs to know when their ...
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
/*
* Award Take This ladder items to participants in this month's challenge
*/
var mongo = require('mongoskin');
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
var dbUsers = mongo.db(connectionString).collection('users');
// specify a query to limit the affected users (empty for all users):
var query = {
'migration':{$ne:migrationName},
'challenges':{$in:['ff674aba-a114-4a6f-8ebc-1de27ffb646e']}
};
// specify fields we are interested in to limit retrieved data (empty if we're not reading data):
var fields = {
'items.gear.owned': 1
};
console.warn('Updating users...');
var progressCount = 1000;
var count = 0;
dbUsers.findEach(query, fields, {batchSize:250}, function(err, user) {
if (err) { return exiting(1, 'ERROR! ' + err); }
if (!user) {
console.warn('All appropriate users found and modified.');
setTimeout(displayData, 300000);
return;
}
count++;
// specify user data to change:
var set = {};
if (typeof user.items.gear.owned.back_special_takeThis !== 'undefined') {
set = {'migration':migrationName};
{ else if (typeof user.items.gear.owned.body_special_takeThis !== 'undefined') {
set = {'migration':migrationName, 'items.gear.owned.back_special_takeThis':false};
} else if (typeof user.items.gear.owned.head_special_takeThis !== 'undefined') {
set = {'migration':migrationName, 'items.gear.owned.body_special_takeThis':false};
} else if (typeof user.items.gear.owned.armor_special_takeThis !== 'undefined') {
set = {'migration':migrationName, 'items.gear.owned.head_special_takeThis':false};
} else if (typeof user.items.gear.owned.weapon_special_takeThis !== 'undefined') {
set = {'migration':migrationName, 'items.gear.owned.armor_special_takeThis':false};
} else if (typeof user.items.gear.owned.shield_special_takeThis !== 'undefined') {
set = {'migration':migrationName, 'items.gear.owned.weapon_special_takeThis':false};
} else {
set = {'migration':migrationName, 'items.gear.owned.shield_special_takeThis':false};
}
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);
}
+4494 -736
View File
File diff suppressed because it is too large Load Diff
+19 -14
View File
@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "3.51.0",
"version": "3.68.0",
"main": "./website/server/index.js",
"dependencies": {
"@slack/client": "3.6.0",
@@ -13,10 +13,13 @@
"async": "^1.5.0",
"autoprefixer": "^6.4.0",
"aws-sdk": "^2.0.25",
"axios": "^0.15.3",
"babel-core": "^6.0.0",
"babel-loader": "^6.0.0",
"babel-plugin-syntax-async-functions": "^6.13.0",
"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",
"babel-polyfill": "^6.6.1",
"babel-preset-es2015": "^6.6.0",
"babel-register": "^6.6.0",
@@ -29,7 +32,7 @@
"compression": "^1.6.1",
"connect-ratelimit": "0.0.7",
"cookie-session": "^1.2.0",
"coupon-code": "^0.4.3",
"coupon-code": "^0.4.5",
"css-loader": "^0.23.1",
"csv-stringify": "^1.0.2",
"cwait": "^1.0.0",
@@ -64,17 +67,18 @@
"image-size": "~0.3.2",
"in-app-purchase": "^1.1.6",
"jade": "~1.11.0",
"jquery": "https://registry.npmjs.org/jquery/-/jquery-3.1.1.tgz",
"js2xmlparser": "~1.0.0",
"json-loader": "^0.5.4",
"less": "^2.7.1",
"less-loader": "^2.2.3",
"lodash": "^3.10.1",
"lodash.setwith": "^4.2.0",
"lodash.pickby": "^4.2.0",
"lodash.setwith": "^4.2.0",
"merge-stream": "^1.0.0",
"method-override": "^2.3.5",
"moment": "^2.13.0",
"mongoose": "^4.4.16",
"mongoose": "^4.7.1",
"mongoose-id-autoinc": "~2013.7.14-4",
"morgan": "^1.7.0",
"nconf": "~0.8.2",
@@ -90,6 +94,7 @@
"passport-google-oauth20": "1.0.0",
"paypal-ipn": "3.0.0",
"paypal-rest-sdk": "^1.2.1",
"postcss-easy-import": "^1.0.1",
"pretty-data": "^0.40.0",
"ps-tree": "^1.0.0",
"pug": "^2.0.0-beta6",
@@ -111,11 +116,11 @@
"validator": "^4.9.0",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0",
"vue": "^2.0.0-rc.6",
"vue": "^2.1.0",
"vue-hot-reload-api": "^1.2.0",
"vue-loader": "^9.4.0",
"vue-resource": "^1.0.2",
"vue-loader": "^10.0.0",
"vue-router": "^2.0.0-rc.5",
"vue-template-compiler": "^2.1.0",
"webpack": "^1.12.2",
"webpack-merge": "^0.8.3",
"winston": "^2.1.0",
@@ -123,8 +128,8 @@
},
"private": true,
"engines": {
"node": "^4.3.1",
"npm": "^3.8.9"
"node": "^6.9.1",
"npm": "^4.0.2"
},
"scripts": {
"lint": "eslint --ext .js,.vue .",
@@ -155,7 +160,6 @@
"postinstall": "bower --config.interactive=false install -f; gulp build; npm run client:build"
},
"devDependencies": {
"babel-eslint": "^6.0.0",
"chai": "^3.4.0",
"chai-as-promised": "^5.1.0",
"chalk": "^1.1.3",
@@ -165,13 +169,12 @@
"cross-spawn": "^2.1.5",
"csv": "~0.3.6",
"deep-diff": "~0.1.4",
"eslint": "~2.12.0",
"eslint-config-habitrpg": "^1.0.0",
"eslint": "^3.0.0",
"eslint-config-habitrpg": "^2.0.0",
"eslint-friendly-formatter": "^2.0.5",
"eslint-loader": "^1.3.0",
"eslint-plugin-babel": "^3.0.0",
"eslint-plugin-html": "^1.3.0",
"eslint-plugin-mocha": "^2.1.0",
"eslint-plugin-mocha": "^4.7.0",
"event-stream": "^3.2.2",
"eventsource-polyfill": "^0.9.6",
"expect.js": "~0.2.0",
@@ -196,6 +199,7 @@
"mocha": "^2.3.3",
"mongodb": "^2.0.46",
"mongoskin": "~2.1.0",
"monk": "^3.1.3",
"nightwatch": "^0.8.18",
"phantomjs-prebuilt": "^2.1.12",
"protractor": "^3.1.1",
@@ -204,6 +208,7 @@
"selenium-server": "2.53.0",
"sinon": "^1.17.2",
"sinon-chai": "^2.8.0",
"sinon-stub-promise": "^4.0.0",
"superagent-defaults": "^0.1.13",
"vinyl-transform": "^1.0.0",
"webpack-dev-middleware": "^1.4.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"extends": [
"habitrpg/mocha",
"habitrpg/babel"
"habitrpg/esnext"
],
"env": {
"node": true,
@@ -5,7 +5,7 @@ import {
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
describe('GET challenges/group/:groupId', () => {
describe('GET challenges/groups/:groupId', () => {
context('Public Guild', () => {
let publicGuild, user, nonMember, challenge, challenge2;
@@ -35,6 +35,24 @@ describe('POST /chat', () => {
});
});
it('Returns an error when an empty message is provided', async () => {
await expect(user.post(`/groups/${groupWithChat._id}/chat`, { message: ' '}))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('Returns an error when an message containing only newlines is provided', async () => {
await expect(user.post(`/groups/${groupWithChat._id}/chat`, { message: '\n\n'}))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('Returns an error when group is not found', async () => {
await expect(user.post('/groups/invalidID/chat', { message: testMessage})).to.eventually.be.rejected.and.eql({
code: 404,
@@ -7,15 +7,21 @@ import {
import moment from 'moment';
describe('GET /export/history.csv', () => {
it('should return a valid CSV file with tasks history data', async () => {
// TODO disabled because it randomly causes the build to fail
xit('should return a valid CSV file with tasks history data', async () => {
let user = await generateUser();
let tasks = await user.post('/tasks/user', [
{type: 'habit', text: 'habit 1'},
{type: 'daily', text: 'daily 1'},
{type: 'habit', text: 'habit 1'},
{type: 'habit', text: 'habit 2'},
{type: 'todo', text: 'todo 1'},
]);
// to handle occasional inconsistency in task creation order
tasks.sort(function (a, b) {
return a.text.localeCompare(b.text);
});
// score all the tasks twice
await user.post(`/tasks/${tasks[0]._id}/score/up`);
await user.post(`/tasks/${tasks[1]._id}/score/up`);
@@ -28,7 +34,7 @@ describe('GET /export/history.csv', () => {
await user.post(`/tasks/${tasks[3]._id}/score/up`);
// adding an history entry to daily 1 manually because cron didn't run yet
await updateDocument('tasks', tasks[1], {
await updateDocument('tasks', tasks[0], {
history: [{value: 3.2, date: Number(new Date())}],
});
@@ -41,11 +47,11 @@ describe('GET /export/history.csv', () => {
let splitRes = res.split('\n');
expect(splitRes[0]).to.equal('Task Name,Task ID,Task Type,Date,Value');
expect(splitRes[1]).to.equal(`habit 1,${tasks[0]._id},habit,${moment(tasks[0].history[0].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[0].history[0].value}`);
expect(splitRes[2]).to.equal(`habit 1,${tasks[0]._id},habit,${moment(tasks[0].history[1].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[0].history[1].value}`);
expect(splitRes[3]).to.equal(`habit 2,${tasks[2]._id},habit,${moment(tasks[2].history[0].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[2].history[0].value}`);
expect(splitRes[4]).to.equal(`habit 2,${tasks[2]._id},habit,${moment(tasks[2].history[1].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[2].history[1].value}`);
expect(splitRes[5]).to.equal(`daily 1,${tasks[1]._id},daily,${moment(tasks[1].history[0].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[1].history[0].value}`);
expect(splitRes[1]).to.equal(`daily 1,${tasks[0]._id},daily,${moment(tasks[0].history[0].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[0].history[0].value}`);
expect(splitRes[2]).to.equal(`habit 1,${tasks[1]._id},habit,${moment(tasks[1].history[0].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[1].history[0].value}`);
expect(splitRes[3]).to.equal(`habit 1,${tasks[1]._id},habit,${moment(tasks[1].history[1].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[1].history[1].value}`);
expect(splitRes[4]).to.equal(`habit 2,${tasks[2]._id},habit,${moment(tasks[2].history[0].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[2].history[0].value}`);
expect(splitRes[5]).to.equal(`habit 2,${tasks[2]._id},habit,${moment(tasks[2].history[1].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[2].history[1].value}`);
expect(splitRes[6]).to.equal('');
});
});
@@ -7,7 +7,8 @@ import Bluebird from 'bluebird';
let parseStringAsync = Bluebird.promisify(xml2js.parseString, {context: xml2js});
describe('GET /export/userdata.xml', () => {
it('should return a valid XML file with user data', async () => {
// TODO disabled because it randomly causes the build to fail
xit('should return a valid XML file with user data', async () => {
let user = await generateUser();
let tasks = await user.post('/tasks/user', [
{type: 'habit', text: 'habit 1'},
@@ -82,8 +82,10 @@ describe('GET /groups/:groupId/members', () => {
'backer', 'contributor', 'auth', 'items', 'inbox',
]);
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
expect(Object.keys(memberRes.preferences).sort()).to.eql(['size', 'hair', 'skin', 'shirt',
'chair', 'costume', 'sleep', 'background'].sort());
expect(Object.keys(memberRes.preferences).sort()).to.eql([
'size', 'hair', 'skin', 'shirt',
'chair', 'costume', 'sleep', 'background',
].sort());
expect(memberRes.stats.maxMP).to.exist;
expect(memberRes.stats.maxHealth).to.equal(common.maxHealth);
@@ -134,6 +134,22 @@ describe('POST /group/:groupId/join', () => {
await expect(user.get('/user')).to.eventually.have.deep.property('items.quests.basilist', 1);
});
it('notifies inviting user that their invitation was accepted', async () => {
await invitedUser.post(`/groups/${guild._id}/join`);
let inviter = await user.get('/user');
let expectedData = {
headerText: t('invitationAcceptedHeader'),
bodyText: t('invitationAcceptedBody', {
username: invitedUser.auth.local.username,
groupName: guild.name,
}),
};
expect(inviter.notifications[0].type).to.eql('GROUP_INVITE_ACCEPTED');
expect(inviter.notifications[0].data).to.eql(expectedData);
});
});
});
@@ -172,6 +188,23 @@ describe('POST /group/:groupId/join', () => {
await expect(invitedUser.get('/user')).to.eventually.have.deep.property('party._id', party._id);
});
it('notifies inviting user that their invitation was accepted', async () => {
await invitedUser.post(`/groups/${party._id}/join`);
let inviter = await user.get('/user');
let expectedData = {
headerText: t('invitationAcceptedHeader'),
bodyText: t('invitationAcceptedBody', {
username: invitedUser.auth.local.username,
groupName: party.name,
}),
};
expect(inviter.notifications[0].type).to.eql('GROUP_INVITE_ACCEPTED');
expect(inviter.notifications[0].data).to.eql(expectedData);
});
it('clears invitation from user when joining party', async () => {
await invitedUser.post(`/groups/${party._id}/join`);
@@ -3,6 +3,7 @@ import {
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
import * as email from '../../../../../website/server/libs/email';
describe('POST /groups/:groupId/removeMember/:memberId', () => {
let leader;
@@ -60,6 +61,14 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
});
context('Guilds', () => {
beforeEach(() => {
sandbox.spy(email, 'sendTxn');
});
afterEach(() => {
sandbox.restore();
});
it('can remove other members', async () => {
await leader.post(`/groups/${guild._id}/removeMember/${member._id}`);
let memberRemoved = await member.get('/user');
@@ -80,6 +89,22 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
expect(_.findIndex(invitedUserWithoutInvite.invitations.guilds, {id: guild._id})).eql(-1);
});
it('sends email to user with rescinded invite', async () => {
await leader.post(`/groups/${guild._id}/removeMember/${invitedUser._id}`);
expect(email.sendTxn).to.be.calledOnce;
expect(email.sendTxn.args[0][0]._id).to.be.eql(invitedUser._id);
expect(email.sendTxn.args[0][1]).to.be.eql('guild-invite-rescinded');
});
it('sends email to removed user', async () => {
await leader.post(`/groups/${guild._id}/removeMember/${member._id}`);
expect(email.sendTxn).to.be.calledOnce;
expect(email.sendTxn.args[0][0]._id).to.be.eql(member._id);
expect(email.sendTxn.args[0][1]).to.be.eql('kicked-from-guild');
});
});
context('Party', () => {
@@ -105,6 +130,11 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
partyInvitedUser = invitees[0];
partyMember = members[0];
removedMember = members[1];
sandbox.spy(email, 'sendTxn');
});
afterEach(() => {
sandbox.restore();
});
it('can remove other members', async () => {
@@ -187,5 +217,21 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
expect(party.quest.members[partyLeader._id]).to.be.true;
expect(party.quest.members[partyMember._id]).to.not.exist;
});
it('sends email to user with rescinded invite', async () => {
await partyLeader.post(`/groups/${party._id}/removeMember/${partyInvitedUser._id}`);
expect(email.sendTxn).to.be.calledOnce;
expect(email.sendTxn.args[0][0]._id).to.be.eql(partyInvitedUser._id);
expect(email.sendTxn.args[0][1]).to.be.eql('party-invite-rescinded');
});
it('sends email to removed user', async () => {
await partyLeader.post(`/groups/${party._id}/removeMember/${partyMember._id}`);
expect(email.sendTxn).to.be.calledOnce;
expect(email.sendTxn.args[0][0]._id).to.be.eql(partyMember._id);
expect(email.sendTxn.args[0][1]).to.be.eql('kicked-from-party');
});
});
});
@@ -300,6 +300,26 @@ describe('Post /groups/:groupId/invite', () => {
message: t('userAlreadyInGroup'),
});
});
// @TODO: Add this after we are able to mock the group plan route
xit('returns an error when a non-leader invites to a group plan', async () => {
let userToInvite = await generateUser();
let nonGroupLeader = await generateUser();
await inviter.post(`/groups/${group._id}/invite`, {
uuids: [nonGroupLeader._id],
});
await nonGroupLeader.post(`/groups/${group._id}/join`);
await expect(nonGroupLeader.post(`/groups/${group._id}/invite`, {
uuids: [userToInvite._id],
}))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlyGroupLeaderCanInviteToGroupPlan'),
});
});
});
describe('party invites', () => {
@@ -0,0 +1,44 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
describe('GET /members/:memberId/achievements', () => {
let user;
before(async () => {
user = await generateUser();
});
it('validates req.params.memberId', async () => {
await expect(user.get('/members/invalidUUID/achievements')).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('returns achievements based on given user', async () => {
let member = await generateUser({
contributor: {level: 1},
backer: {tier: 3},
});
let achievementsRes = await user.get(`/members/${member._id}/achievements`);
expect(achievementsRes.special.achievements.contributor.earned).to.equal(true);
expect(achievementsRes.special.achievements.contributor.value).to.equal(1);
expect(achievementsRes.special.achievements.kickstarter.earned).to.equal(true);
expect(achievementsRes.special.achievements.kickstarter.value).to.equal(3);
});
it('handles non-existing members', async () => {
let dummyId = generateUUID();
await expect(user.get(`/members/${dummyId}/achievements`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('userWithIDNotFound', {userId: dummyId}),
});
});
});
@@ -35,8 +35,10 @@ describe('GET /members/:memberId', () => {
'backer', 'contributor', 'auth', 'items', 'inbox',
]);
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
expect(Object.keys(memberRes.preferences).sort()).to.eql(['size', 'hair', 'skin', 'shirt',
'chair', 'costume', 'sleep', 'background'].sort());
expect(Object.keys(memberRes.preferences).sort()).to.eql([
'size', 'hair', 'skin', 'shirt',
'chair', 'costume', 'sleep', 'background',
].sort());
expect(memberRes.stats.maxMP).to.exist;
expect(memberRes.stats.maxHealth).to.equal(common.maxHealth);
@@ -4,6 +4,14 @@ import {
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
function findMessage (messages, receiverId) {
let message = _.find(messages, (inboxMessage) => {
return inboxMessage.uuid === receiverId;
});
return message;
}
describe('POST /members/transfer-gems', () => {
let userToSendMessage;
let receiver;
@@ -116,19 +124,14 @@ describe('POST /members/transfer-gems', () => {
let updatedReceiver = await receiver.get('/user');
let updatedSender = await userToSendMessage.get('/user');
let sendersMessageInReceiversInbox = _.find(updatedReceiver.inbox.messages, (inboxMessage) => {
return inboxMessage.uuid === userToSendMessage._id;
});
let sendersMessageInReceiversInbox = findMessage(updatedReceiver.inbox.messages, userToSendMessage._id);
let sendersMessageInSendersInbox = findMessage(updatedSender.inbox.messages, receiver._id);
let sendersMessageInSendersInbox = _.find(updatedSender.inbox.messages, (inboxMessage) => {
return inboxMessage.uuid === receiver._id;
});
let messageSentContent = t('privateMessageGiftIntro', {
let messageSentContent = t('privateMessageGiftGemsMessage', {
receiverName: receiver.profile.name,
senderName: userToSendMessage.profile.name,
gemAmount,
});
messageSentContent += t('privateMessageGiftGemsMessage', {gemAmount});
messageSentContent = `\`${messageSentContent}\` `;
messageSentContent += message;
@@ -150,19 +153,14 @@ describe('POST /members/transfer-gems', () => {
let updatedReceiver = await receiver.get('/user');
let updatedSender = await userToSendMessage.get('/user');
let sendersMessageInReceiversInbox = _.find(updatedReceiver.inbox.messages, (inboxMessage) => {
return inboxMessage.uuid === userToSendMessage._id;
});
let sendersMessageInReceiversInbox = findMessage(updatedReceiver.inbox.messages, userToSendMessage._id);
let sendersMessageInSendersInbox = findMessage(updatedSender.inbox.messages, receiver._id);
let sendersMessageInSendersInbox = _.find(updatedSender.inbox.messages, (inboxMessage) => {
return inboxMessage.uuid === receiver._id;
});
let messageSentContent = t('privateMessageGiftIntro', {
let messageSentContent = t('privateMessageGiftGemsMessage', {
receiverName: receiver.profile.name,
senderName: userToSendMessage.profile.name,
gemAmount,
});
messageSentContent += t('privateMessageGiftGemsMessage', {gemAmount});
messageSentContent = `\`${messageSentContent}\` `;
expect(sendersMessageInReceiversInbox).to.exist;
@@ -173,4 +171,40 @@ describe('POST /members/transfer-gems', () => {
expect(sendersMessageInSendersInbox.text).to.equal(messageSentContent);
expect(updatedSender.balance).to.equal(0);
});
it('sends transfer gems message in each participant\'s language', async () => {
await receiver.update({
'preferences.language': 'es',
});
await userToSendMessage.update({
'preferences.language': 'cs',
});
await userToSendMessage.post('/members/transfer-gems', {
gemAmount,
toUserId: receiver._id,
});
let updatedReceiver = await receiver.get('/user');
let updatedSender = await userToSendMessage.get('/user');
let sendersMessageInReceiversInbox = findMessage(updatedReceiver.inbox.messages, userToSendMessage._id);
let sendersMessageInSendersInbox = findMessage(updatedSender.inbox.messages, receiver._id);
let [receieversMessageContent, sendersMessageContent] = ['es', 'cs'].map((lang) => {
let messageContent = t('privateMessageGiftGemsMessage', {
receiverName: receiver.profile.name,
senderName: userToSendMessage.profile.name,
gemAmount,
}, lang);
return `\`${messageContent}\` `;
});
expect(sendersMessageInReceiversInbox).to.exist;
expect(sendersMessageInReceiversInbox.text).to.equal(receieversMessageContent);
expect(sendersMessageInSendersInbox).to.exist;
expect(sendersMessageInSendersInbox.text).to.equal(sendersMessageContent);
expect(updatedSender.balance).to.equal(0);
});
});
@@ -10,7 +10,7 @@ describe('GET /models/:model/paths', () => {
user = await generateUser();
});
it('returns an error when model is not accessible or doesn\'t exists', async () => {
it('returns an error when model is not accessible or doesn\'t exist', async () => {
await expect(user.get('/models/1234/paths')).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
@@ -0,0 +1,26 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
describe('POST /notifications/:notificationId/read', () => {
let user;
before(async () => {
user = await generateUser();
});
it('errors when notification is not found', async () => {
let dummyId = generateUUID();
await expect(user.post(`/notifications/${dummyId}/read`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('messageNotificationNotFound'),
});
});
xit('removes a notification', async () => {
});
});
@@ -10,13 +10,11 @@ describe('payments - amazon - #createOrderReferenceId', () => {
user = await generateUser();
});
it('verifies billingAgreementId', async (done) => {
try {
await user.post(endpoint);
} catch (e) {
// Parameter AWSAccessKeyId cannot be empty.
expect(e.error).to.eql('BadRequest');
done();
}
it('verifies billingAgreementId', async () => {
await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Missing req.body.billingAgreementId',
});
});
});
@@ -4,6 +4,7 @@ import {
generateUser,
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
import { model as Group } from '../../../../../website/server/models/group';
describe('POST /groups/:groupId/quests/abort', () => {
let questingGroup;
@@ -85,6 +86,7 @@ describe('POST /groups/:groupId/quests/abort', () => {
});
it('aborts a quest', async () => {
sandbox.stub(Group.prototype, 'sendChat');
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
await partyMembers[1].post(`/groups/${questingGroup._id}/quests/accept`);
@@ -123,5 +125,8 @@ describe('POST /groups/:groupId/quests/abort', () => {
},
members: {},
});
expect(Group.prototype.sendChat).to.be.calledOnce;
expect(Group.prototype.sendChat).to.be.calledWithMatch(/aborted the party quest Wail of the Whale.`/);
Group.prototype.sendChat.restore();
});
});
@@ -15,7 +15,7 @@ describe('GET /shops/seasonal', () => {
expect(shop.identifier).to.equal('seasonalShop');
expect(shop.text).to.eql(t('seasonalShop'));
expect(shop.notes).to.eql(t('seasonalShopFallText'));
expect(shop.notes).to.be.a('string');
expect(shop.imageName).to.be.a('string');
expect(shop.categories).to.be.an('array');
});
@@ -115,7 +115,7 @@ describe('GET /tasks/user', () => {
for (let i = 0; i < numberOfTodos; i++) {
let id = todos[i]._id;
await user.post(`/tasks/${id}/score/up`); // eslint-disable-line babel/no-await-in-loop
await user.post(`/tasks/${id}/score/up`); // eslint-disable-line no-await-in-loop
}
await user.sync();
@@ -5,7 +5,7 @@ import {
} from '../../../../helpers/api-integration/v3';
describe('POST /tasks/clearCompletedTodos', () => {
it('deletes all completed todos except the ones from a challenge', async () => {
it('deletes all completed todos except the ones from a challenge and group', async () => {
let user = await generateUser({balance: 1});
let guild = await generateGroup(user);
let challenge = await generateChallenge(user, guild);
@@ -24,12 +24,18 @@ describe('POST /tasks/clearCompletedTodos', () => {
type: 'todo',
});
let groupTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'todo 7',
type: 'todo',
});
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
let tasks = await user.get('/tasks/user?type=todos');
expect(tasks.length).to.equal(initialTodoCount + 6);
expect(tasks.length).to.equal(initialTodoCount + 7);
for (let task of tasks) {
if (['todo 2', 'todo 3', 'todo 6'].indexOf(task.text) !== -1) {
await user.post(`/tasks/${task._id}/score/up`); // eslint-disable-line babel/no-await-in-loop
await user.post(`/tasks/${task._id}/score/up`); // eslint-disable-line no-await-in-loop
}
}
@@ -38,6 +44,6 @@ describe('POST /tasks/clearCompletedTodos', () => {
let todos = await user.get('/tasks/user?type=todos');
let allTodos = todos.concat(completedTodos);
expect(allTodos.length).to.equal(initialTodoCount + 4); // + 6 - 3 completed (but one is from challenge)
expect(allTodos[allTodos.length - 1].text).to.equal('todo 6');
expect(allTodos[allTodos.length - 1].text).to.equal('todo 7');
});
});
@@ -74,6 +74,7 @@ describe('PUT /tasks/:id', () => {
checklist: [
{text: 123, completed: false},
],
collapseChecklist: false,
});
await sleep(2);
@@ -111,6 +112,7 @@ describe('PUT /tasks/:id', () => {
{text: 123, completed: false},
{text: 456, completed: true},
],
collapseChecklist: true,
notes: 'new notes',
attribute: 'per',
tags: [challengeUserTaskId],
@@ -143,6 +145,8 @@ describe('PUT /tasks/:id', () => {
expect(savedChallengeUserTask.streak).to.equal(25);
expect(savedChallengeUserTask.reminders.length).to.equal(2);
expect(savedChallengeUserTask.checklist.length).to.equal(2);
expect(savedChallengeUserTask.alias).to.equal('a-short-task-name');
expect(savedChallengeUserTask.collapseChecklist).to.equal(true);
});
});
@@ -5,12 +5,17 @@ import {
translate as t,
} from '../../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
import { find } from 'lodash';
describe('POST /tasks/challenge/:challengeId', () => {
let user;
let guild;
let challenge;
function findUserChallengeTask (memberTask) {
return memberTask.challenge.id === challenge._id;
}
beforeEach(async () => {
user = await generateUser({balance: 1});
guild = await generateGroup(user);
@@ -88,6 +93,9 @@ describe('POST /tasks/challenge/:challengeId', () => {
});
let challengeWithTask = await user.get(`/challenges/${challenge._id}`);
let memberTasks = await user.get('/tasks/user');
let userChallengeTask = find(memberTasks, findUserChallengeTask);
expect(challengeWithTask.tasksOrder.habits.indexOf(task._id)).to.be.above(-1);
expect(task.challenge.id).to.equal(challenge._id);
expect(task.text).to.eql('test habit');
@@ -95,6 +103,8 @@ describe('POST /tasks/challenge/:challengeId', () => {
expect(task.type).to.eql('habit');
expect(task.up).to.eql(false);
expect(task.down).to.eql(true);
expect(userChallengeTask.notes).to.eql(task.notes);
});
it('creates a todo', async () => {
@@ -105,11 +115,16 @@ describe('POST /tasks/challenge/:challengeId', () => {
});
let challengeWithTask = await user.get(`/challenges/${challenge._id}`);
let memberTasks = await user.get('/tasks/user');
let userChallengeTask = find(memberTasks, findUserChallengeTask);
expect(challengeWithTask.tasksOrder.todos.indexOf(task._id)).to.be.above(-1);
expect(task.challenge.id).to.equal(challenge._id);
expect(task.text).to.eql('test todo');
expect(task.notes).to.eql('1976');
expect(task.type).to.eql('todo');
expect(userChallengeTask.notes).to.eql(task.notes);
});
it('creates a daily', async () => {
@@ -124,6 +139,9 @@ describe('POST /tasks/challenge/:challengeId', () => {
});
let challengeWithTask = await user.get(`/challenges/${challenge._id}`);
let memberTasks = await user.get('/tasks/user');
let userChallengeTask = find(memberTasks, findUserChallengeTask);
expect(challengeWithTask.tasksOrder.dailys.indexOf(task._id)).to.be.above(-1);
expect(task.challenge.id).to.equal(challenge._id);
expect(task.text).to.eql('test daily');
@@ -132,5 +150,7 @@ describe('POST /tasks/challenge/:challengeId', () => {
expect(task.frequency).to.eql('daily');
expect(task.everyX).to.eql(5);
expect(new Date(task.startDate)).to.eql(now);
expect(userChallengeTask.notes).to.eql(task.notes);
});
});
@@ -69,4 +69,48 @@ describe('DELETE /tasks/:id', () => {
expect(syncedTask.group.broken).to.equal('TASK_DELETED');
expect(member2SyncedTask.group.broken).to.equal('TASK_DELETED');
});
it('prevents a user from deleting a task they are assigned to', async () => {
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.del(`/tasks/${syncedTask._id}`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('cantDeleteAssignedGroupTasks'),
});
});
it('allows a user to delete a broken task', async () => {
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await user.del(`/tasks/${task._id}`);
await member.del(`/tasks/${syncedTask._id}`);
await expect(member.get(`/tasks/${syncedTask._id}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: 'Task not found.',
});
});
it('allows a user to delete a task after leaving a group', async () => {
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/groups/${guild._id}/leave`);
await member.del(`/tasks/${syncedTask._id}`);
await expect(member.get(`/tasks/${syncedTask._id}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: 'Task not found.',
});
});
});
@@ -0,0 +1,58 @@
import {
createAndPopulateGroup,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import { find } from 'lodash';
describe('GET /approvals/group/:groupId', () => {
let user, guild, member, task, syncedTask;
function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
}
beforeEach(async () => {
let {group, members, groupLeader} = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
},
members: 1,
});
guild = group;
user = groupLeader;
member = members[0];
task = await user.post(`/tasks/group/${guild._id}`, {
text: 'test todo',
type: 'todo',
requiresApproval: true,
});
await user.post(`/tasks/${task._id}/assign/${member._id}`);
let memberTasks = await member.get('/tasks/user');
syncedTask = find(memberTasks, findAssignedTask);
try {
await member.post(`/tasks/${syncedTask._id}/score/up`);
} catch (e) {
// eslint-disable-next-line no-empty
}
});
it('errors when user is not the group leader', async () => {
await expect(member.get(`/approvals/group/${guild._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlyGroupLeaderCanEditTasks'),
});
});
it('gets a list of task that need approval', async () => {
let approvals = await user.get(`/approvals/group/${guild._id}`);
expect(approvals[0]._id).to.equal(syncedTask._id);
});
});
@@ -0,0 +1,72 @@
import {
createAndPopulateGroup,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import { find } from 'lodash';
describe('POST /tasks/:id/approve/:userId', () => {
let user, guild, member, task;
function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
}
beforeEach(async () => {
let {group, members, groupLeader} = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
},
members: 1,
});
guild = group;
user = groupLeader;
member = members[0];
task = await user.post(`/tasks/group/${guild._id}`, {
text: 'test todo',
type: 'todo',
requiresApproval: true,
});
});
it('errors when user is not assigned', async () => {
await expect(user.post(`/tasks/${task._id}/approve/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 404,
error: 'NotFound',
message: t('taskNotFound'),
});
});
it('errors when user is not the group leader', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await expect(member.post(`/tasks/${task._id}/approve/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlyGroupLeaderCanEditTasks'),
});
});
it('approves an assigned user', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await user.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(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
});
});
@@ -0,0 +1,93 @@
import {
createAndPopulateGroup,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import { find } from 'lodash';
describe('POST /tasks/:id/score/:direction', () => {
let user, guild, member, task;
function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
}
beforeEach(async () => {
let {group, members, groupLeader} = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
},
members: 1,
});
guild = group;
user = groupLeader;
member = members[0];
task = await user.post(`/tasks/group/${guild._id}`, {
text: 'test todo',
type: 'todo',
requiresApproval: true,
});
await user.post(`/tasks/${task._id}/assign/${member._id}`);
});
it('prevents user from scoring a task that needs to be approved', async () => {
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();
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', {
user: member.auth.local.username,
taskName: updatedTask.text,
}));
expect(user.notifications[0].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('errors when approval has already been requested', async () => {
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 expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskRequiresApproval'),
});
});
it('allows a user to score an apporoved task', async () => {
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await user.post(`/tasks/${task._id}/approve/${member._id}`);
await member.post(`/tasks/${syncedTask._id}/score/up`);
let updatedTask = await member.get(`/tasks/${syncedTask._id}`);
expect(updatedTask.completed).to.equal(true);
expect(updatedTask.dateCompleted).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
});
});
@@ -82,7 +82,7 @@ describe('POST /tasks/:taskId', () => {
});
});
it('allows user to assign themselves', async () => {
it('allows user to assign themselves (claim)', async () => {
await member.post(`/tasks/${task._id}/assign/${member._id}`);
let groupTask = await user.get(`/tasks/group/${guild._id}`);
@@ -93,6 +93,15 @@ describe('POST /tasks/:taskId', () => {
expect(syncedTask).to.exist;
});
it('sends a message to the group when a user claims a task', async () => {
await member.post(`/tasks/${task._id}/assign/${member._id}`);
let updateGroup = await user.get(`/groups/${guild._id}`);
expect(updateGroup.chat[0].text).to.equal(t('userIsClamingTask', {username: member.profile.name, task: task.text}));
expect(updateGroup.chat[0].uuid).to.equal('system');
});
it('assigns a task to a user', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
@@ -0,0 +1,49 @@
import {
generateUser,
generateGroup,
} from '../../../../../helpers/api-v3-integration.helper';
describe('POST group-tasks/:taskId/move/to/:position', () => {
let user, guild;
beforeEach(async () => {
user = await generateUser({balance: 1});
guild = await generateGroup(user, {type: 'guild'});
});
it('can move task to new position', async () => {
let tasks = await user.post(`/tasks/group/${guild._id}`, [
{type: 'habit', text: 'habit 1'},
{type: 'habit', text: 'habit 2'},
{type: 'daily', text: 'daily 1'},
{type: 'habit', text: 'habit 3'},
{type: 'habit', text: 'habit 4'},
{type: 'todo', text: 'todo 1'},
{type: 'habit', text: 'habit 5'},
]);
let taskToMove = tasks[1];
expect(taskToMove.text).to.equal('habit 2');
let newOrder = await user.post(`/group-tasks/${tasks[1]._id}/move/to/3`);
expect(newOrder[3]).to.equal(taskToMove._id);
expect(newOrder.length).to.equal(5);
});
it('can push to bottom', async () => {
let tasks = await user.post(`/tasks/group/${guild._id}`, [
{type: 'habit', text: 'habit 1'},
{type: 'habit', text: 'habit 2'},
{type: 'daily', text: 'daily 1'},
{type: 'habit', text: 'habit 3'},
{type: 'habit', text: 'habit 4'},
{type: 'todo', text: 'todo 1'},
{type: 'habit', text: 'habit 5'},
]);
let taskToMove = tasks[1];
expect(taskToMove.text).to.equal('habit 2');
let newOrder = await user.post(`/group-tasks/${tasks[1]._id}/move/to/-1`);
expect(newOrder[4]).to.equal(taskToMove._id);
expect(newOrder.length).to.equal(5);
});
});
@@ -62,15 +62,6 @@ describe('POST /user/buy/:key', () => {
await user.post(`/user/buy/${key}`);
await user.sync();
expect(user.items.gear.owned).to.eql({
armor_warrior_1: true,
eyewear_special_blackTopFrame: true,
eyewear_special_blueTopFrame: true,
eyewear_special_greenTopFrame: true,
eyewear_special_pinkTopFrame: true,
eyewear_special_redTopFrame: true,
eyewear_special_whiteTopFrame: true,
eyewear_special_yellowTopFrame: true,
});
expect(user.items.gear.owned.armor_warrior_1).to.eql(true);
});
});
@@ -31,15 +31,6 @@ describe('POST /user/buy-gear/:key', () => {
await user.post(`/user/buy-gear/${key}`);
await user.sync();
expect(user.items.gear.owned).to.eql({
armor_warrior_1: true,
eyewear_special_blackTopFrame: true,
eyewear_special_blueTopFrame: true,
eyewear_special_greenTopFrame: true,
eyewear_special_pinkTopFrame: true,
eyewear_special_redTopFrame: true,
eyewear_special_whiteTopFrame: true,
eyewear_special_yellowTopFrame: true,
});
expect(user.items.gear.owned.armor_warrior_1).to.eql(true);
});
});
@@ -7,6 +7,7 @@ import {
} from '../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
import { find } from 'lodash';
describe('POST /user/class/cast/:spellId', () => {
let user;
@@ -120,6 +121,31 @@ describe('POST /user/class/cast/:spellId', () => {
});
});
it('returns an error if a group task was targeted', async () => {
let {group, groupLeader} = await createAndPopulateGroup();
let groupTask = await groupLeader.post(`/tasks/group/${group._id}`, {
text: 'todo group',
type: 'todo',
});
await groupLeader.post(`/tasks/${groupTask._id}/assign/${groupLeader._id}`);
let memberTasks = await groupLeader.get('/tasks/user');
let syncedGroupTask = find(memberTasks, function findAssignedTask (memberTask) {
return memberTask.group.id === group._id;
});
await groupLeader.update({'stats.class': 'rogue', 'stats.lvl': 11});
await sleep(0.5);
await groupLeader.sync();
await expect(groupLeader.post(`/user/class/cast/pickPocket?targetId=${syncedGroupTask._id}`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('groupTasksNoCast'),
});
});
it('returns an error if targeted party member doesn\'t exist', async () => {
let {groupLeader} = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
@@ -20,6 +20,6 @@ describe('POST /user/purchase-hourglass/:type/:key', () => {
expect(response.message).to.eql(t('hourglassPurchase'));
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
expect(user.items.pets).to.eql({'MantisShrimp-Base': 5});
expect(user.items.pets['MantisShrimp-Base']).to.eql(5);
});
});
@@ -4,6 +4,7 @@ import {
generateChallenge,
translate as t,
} from '../../../../helpers/api-integration/v3';
import { find } from 'lodash';
describe('POST /user/reset', () => {
let user;
@@ -86,19 +87,34 @@ describe('POST /user/reset', () => {
expect(user.tasksOrder.rewards).to.be.empty;
});
it('does not delete challenge tasks', async () => {
it('does not delete challenge or group tasks', async () => {
let guild = await generateGroup(user);
let challenge = await generateChallenge(user, guild);
let task = await user.post(`/tasks/challenge/${challenge._id}`, {
await user.post(`/tasks/challenge/${challenge._id}`, {
text: 'test challenge habit',
type: 'habit',
});
let groupTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'todo group',
type: 'todo',
});
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
await user.post('/user/reset');
await user.sync();
let userChallengeTask = await user.get(`/tasks/${task._id}`);
let memberTasks = await user.get('/tasks/user');
expect(userChallengeTask).to.eql(task);
let syncedGroupTask = find(memberTasks, function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
});
let userChallengeTask = find(memberTasks, function findAssignedTask (memberTask) {
return memberTask.challenge.id === challenge._id;
});
expect(userChallengeTask).to.exist;
expect(syncedGroupTask).to.exist;
});
});
@@ -26,6 +26,32 @@ describe('PUT /user', () => {
expect(user.preferences.costume).to.eql(true);
expect(user.stats.hp).to.eql(14);
});
it('profile.name cannot be an empty string or null', async () => {
await expect(user.put('/user', {
'profile.name': ' ', // string should be trimmed
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'User validation failed',
});
await expect(user.put('/user', {
'profile.name': '',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'User validation failed',
});
await expect(user.put('/user', {
'profile.name': null,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'User validation failed',
});
});
});
context('Top Level Protected Operations', () => {
@@ -32,6 +32,7 @@ describe('POST /user/auth/local/register', () => {
expect(user._id).to.exist;
expect(user.apiToken).to.exist;
expect(user.auth.local.username).to.eql(username);
expect(user.profile.name).to.eql(username);
});
it('provides default tags and tasks', async () => {
@@ -66,6 +67,7 @@ describe('POST /user/auth/local/register', () => {
});
await expect(getProperty('users', user._id, '_ABtest')).to.eventually.be.a('string');
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
});
it('requires password and confirmPassword to match', async () => {
@@ -33,7 +33,7 @@ describe('POST /user/auth/social', () => {
describe('facebook', () => {
before(async () => {
let expectedResult = {id: facebookId};
let expectedResult = {id: facebookId, displayName: 'a facebook user'};
sandbox.stub(passport._strategies.facebook, 'userProfile').yields(null, expectedResult);
network = 'facebook';
});
@@ -47,6 +47,7 @@ describe('POST /user/auth/social', () => {
expect(response.apiToken).to.exist;
expect(response.id).to.exist;
expect(response.newUser).to.be.true;
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a facebook user');
});
it('logs an existing user in', async () => {
@@ -88,7 +89,7 @@ describe('POST /user/auth/social', () => {
describe('google', () => {
before(async () => {
let expectedResult = {id: googleId};
let expectedResult = {id: googleId, displayName: 'a google user'};
sandbox.stub(passport._strategies.google, 'userProfile').yields(null, expectedResult);
network = 'google';
});
@@ -102,6 +103,7 @@ describe('POST /user/auth/social', () => {
expect(response.apiToken).to.exist;
expect(response.id).to.exist;
expect(response.newUser).to.be.true;
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a google user');
});
it('logs an existing user in', async () => {
@@ -278,6 +278,7 @@ describe('analyticsService', () => {
todos: [{_id: 'todo'}],
rewards: [{_id: 'reward'}],
balance: 12,
loginIncentives: 1,
};
data.user = user;
@@ -302,6 +303,8 @@ describe('analyticsService', () => {
contributorLevel: 1,
subscription: 'foo-plan',
balance: 12,
balanceGemAmount: 48,
loginIncentives: 1,
},
});
});
@@ -351,7 +354,8 @@ describe('analyticsService', () => {
purchaseType: 'checkout',
gift: false,
quantity: 1,
headers: {'x-client': 'habitica-web',
headers: {
'x-client': 'habitica-web',
'user-agent': '',
},
};
+217 -18
View File
@@ -8,6 +8,7 @@ import { model as User } from '../../../../../website/server/models/user';
import * as Tasks from '../../../../../website/server/models/task';
import { clone } from 'lodash';
import common from '../../../../../website/common';
import analytics from '../../../../../website/server/libs/analyticsService';
// const scoreTask = common.ops.scoreTask;
@@ -17,9 +18,6 @@ describe('cron', () => {
let user;
let tasksByType = {habits: [], dailys: [], todos: [], rewards: []};
let daysMissed = 0;
let analytics = {
track: sinon.spy(),
};
beforeEach(() => {
user = new User({
@@ -34,11 +32,17 @@ describe('cron', () => {
},
});
sinon.spy(analytics, 'track');
user._statsComputed = {
mp: 10,
};
});
afterEach(() => {
analytics.track.restore();
});
it('updates user.preferences.timezoneOffsetAtLastCron', () => {
let timezoneOffsetFromUserPrefs = 1;
@@ -59,6 +63,11 @@ describe('cron', () => {
expect(user.flags.cronCount).to.be.greaterThan(cronCountBefore);
});
it('calls analytics', () => {
cron({user, tasksByType, daysMissed, analytics});
expect(analytics.track.callCount).to.equal(1);
});
describe('end of the month perks', () => {
beforeEach(() => {
user.purchased.plan.customerId = 'subscribedId';
@@ -257,6 +266,11 @@ describe('cron', () => {
user.preferences.sleep = true;
});
it('calls analytics', () => {
cron({user, tasksByType, daysMissed, analytics});
expect(analytics.track.callCount).to.equal(1);
});
it('clears user buffs', () => {
user.stats.buffs = {
str: 1,
@@ -286,7 +300,7 @@ describe('cron', () => {
startDate: new Date(),
};
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line babel/new-cap
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line new-cap
tasksByType.dailys.push(task);
tasksByType.dailys[0].completed = true;
@@ -307,7 +321,7 @@ describe('cron', () => {
value: 0,
};
let task = new Tasks.todo(Tasks.Task.sanitize(todo)); // eslint-disable-line babel/new-cap
let task = new Tasks.todo(Tasks.Task.sanitize(todo)); // eslint-disable-line new-cap
tasksByType.todos.push(task);
});
@@ -333,7 +347,7 @@ describe('cron', () => {
type: 'daily',
};
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line babel/new-cap
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line new-cap
tasksByType.dailys = [];
tasksByType.dailys.push(task);
@@ -435,7 +449,7 @@ describe('cron', () => {
type: 'habit',
};
let task = new Tasks.habit(Tasks.Task.sanitize(habit)); // eslint-disable-line babel/new-cap
let task = new Tasks.habit(Tasks.Task.sanitize(habit)); // eslint-disable-line new-cap
tasksByType.habits = [];
tasksByType.habits.push(task);
});
@@ -476,7 +490,7 @@ describe('cron', () => {
type: 'daily',
};
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line babel/new-cap
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line new-cap
tasksByType.dailys = [];
tasksByType.dailys.push(task);
@@ -619,7 +633,7 @@ describe('cron', () => {
type: 'daily',
};
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line babel/new-cap
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line new-cap
tasksByType.dailys = [];
tasksByType.dailys.push(task);
@@ -656,9 +670,9 @@ describe('cron', () => {
cron({user, tasksByType, daysMissed, analytics});
expect(user.notifications.length).to.equal(1);
expect(user.notifications[0].type).to.equal('CRON');
expect(user.notifications[0].data).to.eql({
expect(user.notifications.length).to.be.greaterThan(0);
expect(user.notifications[1].type).to.equal('CRON');
expect(user.notifications[1].data).to.eql({
hp: user.stats.hp - hpBefore,
mp: user.stats.mp - mpBefore,
});
@@ -675,13 +689,14 @@ describe('cron', () => {
cron({user, tasksByType, daysMissed, analytics});
expect(user.notifications.length).to.equal(1);
expect(user.notifications[0].type).to.equal('CRON');
expect(user.notifications[0].data).to.eql({
expect(user.notifications.length).to.be.greaterThan(0);
expect(user.notifications[1].type).to.equal('CRON');
expect(user.notifications[1].data).to.eql({
hp: user.stats.hp - hpBefore1,
mp: user.stats.mp - mpBefore1,
});
let notifsBefore2 = user.notifications.length;
let hpBefore2 = user.stats.hp;
let mpBefore2 = user.stats.mp;
@@ -689,12 +704,14 @@ describe('cron', () => {
cron({user, tasksByType, daysMissed, analytics});
expect(user.notifications.length).to.equal(1);
expect(user.notifications[0].type).to.equal('CRON');
expect(user.notifications[0].data).to.eql({
expect(user.notifications.length - notifsBefore2).to.equal(0);
expect(user.notifications[0].type).to.not.equal('CRON');
expect(user.notifications[1].type).to.equal('CRON');
expect(user.notifications[1].data).to.eql({
hp: user.stats.hp - hpBefore2 - (hpBefore2 - hpBefore1),
mp: user.stats.mp - mpBefore2 - (mpBefore2 - mpBefore1),
});
expect(user.notifications[0].type).to.not.equal('CRON');
});
});
@@ -747,6 +764,188 @@ describe('cron', () => {
expect(user.inbox.messages[messageId]).to.not.exist;
});
});
describe('login incentives', () => {
it('increments incentive counter each cron', () => {
cron({user, tasksByType, daysMissed, analytics});
expect(user.loginIncentives).to.eql(1);
user.lastCron = moment(new Date()).subtract({days: 1});
cron({user, tasksByType, daysMissed, analytics});
expect(user.loginIncentives).to.eql(2);
});
it('pushes a notification of the day\'s incentive each cron', () => {
cron({user, tasksByType, daysMissed, analytics});
expect(user.notifications.length).to.be.greaterThan(1);
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
});
it('replaces previous notifications', () => {
cron({user, tasksByType, daysMissed, analytics});
cron({user, tasksByType, daysMissed, analytics});
cron({user, tasksByType, daysMissed, analytics});
let filteredNotifications = user.notifications.filter(n => n.type === 'LOGIN_INCENTIVE');
expect(filteredNotifications.length).to.equal(1);
});
it('increments loginIncentives by 1 even if days are skipped in between', () => {
daysMissed = 3;
cron({user, tasksByType, daysMissed, analytics});
expect(user.loginIncentives).to.eql(1);
});
it('increments loginIncentives by 1 even if user has Dailies paused', () => {
user.preferences.sleep = true;
cron({user, tasksByType, daysMissed, analytics});
expect(user.loginIncentives).to.eql(1);
});
it('awards user bard robes if login incentive is 1', () => {
cron({user, tasksByType, daysMissed, analytics});
expect(user.loginIncentives).to.eql(1);
expect(user.items.gear.owned.armor_special_bardRobes).to.eql(true);
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
});
it('awards user incentive backgrounds if login incentive is 2', () => {
user.loginIncentives = 1;
cron({user, tasksByType, daysMissed, analytics});
expect(user.loginIncentives).to.eql(2);
expect(user.purchased.background.blue).to.eql(true);
expect(user.purchased.background.green).to.eql(true);
expect(user.purchased.background.purple).to.eql(true);
expect(user.purchased.background.red).to.eql(true);
expect(user.purchased.background.yellow).to.eql(true);
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
});
it('awards user Bard Hat if login incentive is 3', () => {
user.loginIncentives = 2;
cron({user, tasksByType, daysMissed, analytics});
expect(user.loginIncentives).to.eql(3);
expect(user.items.gear.owned.head_special_bardHat).to.eql(true);
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
});
it('awards user RoyalPurple Hatching Potion if login incentive is 4', () => {
user.loginIncentives = 3;
cron({user, tasksByType, daysMissed, analytics});
expect(user.loginIncentives).to.eql(4);
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
});
it('awards user a Chocolate, Meat and Pink Contton Candy if login incentive is 5', () => {
user.loginIncentives = 4;
cron({user, tasksByType, daysMissed, analytics});
expect(user.loginIncentives).to.eql(5);
expect(user.items.food.Chocolate).to.eql(1);
expect(user.items.food.Meat).to.eql(1);
expect(user.items.food.CottonCandyPink).to.eql(1);
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
});
it('awards user moon quest if login incentive is 7', () => {
user.loginIncentives = 6;
cron({user, tasksByType, daysMissed, analytics});
expect(user.loginIncentives).to.eql(7);
expect(user.items.quests.moon1).to.eql(1);
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
});
it('awards user RoyalPurple Hatching Potion if login incentive is 10', () => {
user.loginIncentives = 9;
cron({user, tasksByType, daysMissed, analytics});
expect(user.loginIncentives).to.eql(10);
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
});
it('awards user a Strawberry, Patato and Blue Contton Candy if login incentive is 14', () => {
user.loginIncentives = 13;
cron({user, tasksByType, daysMissed, analytics});
expect(user.loginIncentives).to.eql(14);
expect(user.items.food.Strawberry).to.eql(1);
expect(user.items.food.Potatoe).to.eql(1);
expect(user.items.food.CottonCandyBlue).to.eql(1);
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
});
it('awards user a bard instrument if login incentive is 18', () => {
user.loginIncentives = 17;
cron({user, tasksByType, daysMissed, analytics});
expect(user.loginIncentives).to.eql(18);
expect(user.items.gear.owned.weapon_special_bardInstrument).to.eql(true);
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
});
it('awards user second moon quest if login incentive is 22', () => {
user.loginIncentives = 21;
cron({user, tasksByType, daysMissed, analytics});
expect(user.loginIncentives).to.eql(22);
expect(user.items.quests.moon2).to.eql(1);
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
});
it('awards user a RoyalPurple hatching potion if login incentive is 26', () => {
user.loginIncentives = 25;
cron({user, tasksByType, daysMissed, analytics});
expect(user.loginIncentives).to.eql(26);
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
});
it('awards user Fish, Milk, Rotten Meat and Honey if login incentive is 30', () => {
user.loginIncentives = 29;
cron({user, tasksByType, daysMissed, analytics});
expect(user.loginIncentives).to.eql(30);
expect(user.items.food.Fish).to.eql(1);
expect(user.items.food.Milk).to.eql(1);
expect(user.items.food.RottenMeat).to.eql(1);
expect(user.items.food.Honey).to.eql(1);
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
});
it('awards user a RoyalPurple hatching potion if login incentive is 35', () => {
user.loginIncentives = 34;
cron({user, tasksByType, daysMissed, analytics});
expect(user.loginIncentives).to.eql(35);
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
});
it('awards user the third moon quest if login incentive is 40', () => {
user.loginIncentives = 39;
cron({user, tasksByType, daysMissed, analytics});
expect(user.loginIncentives).to.eql(40);
expect(user.items.quests.moon3).to.eql(1);
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
});
it('awards user a RoyalPurple hatching potion if login incentive is 45', () => {
user.loginIncentives = 44;
cron({user, tasksByType, daysMissed, analytics});
expect(user.loginIncentives).to.eql(45);
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
});
it('awards user a saddle if login incentive is 50', () => {
user.loginIncentives = 49;
cron({user, tasksByType, daysMissed, analytics});
expect(user.loginIncentives).to.eql(50);
expect(user.items.food.Saddle).to.eql(1);
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
});
});
});
describe('recoverCron', () => {
+399 -39
View File
@@ -3,15 +3,33 @@ import * as api from '../../../../../website/server/libs/payments';
import analytics from '../../../../../website/server/libs/analyticsService';
import notifications from '../../../../../website/server/libs/pushNotifications';
import { model as User } from '../../../../../website/server/models/user';
import { model as Group } from '../../../../../website/server/models/group';
import stripeModule from 'stripe';
import moment from 'moment';
import { translate as t } from '../../../../helpers/api-v3-integration.helper';
import {
generateGroup,
} from '../../../../helpers/api-unit.helper.js';
import i18n from '../../../../../website/common/script/i18n';
import amzLib from '../../../../../website/server/libs/amazonPayments';
describe('payments/index', () => {
let user, data, plan;
let user, group, data, plan;
beforeEach(() => {
let stripe = stripeModule('test');
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
await group.save();
sandbox.stub(sender, 'sendTxn');
sandbox.stub(user, 'sendMessage');
sandbox.stub(analytics, 'trackPurchase');
@@ -133,6 +151,14 @@ describe('payments/index', () => {
expect(recipient.purchased.plan.dateUpdated).to.exist;
});
it('sets plan.dateCreated if it did not previously exist', async () => {
expect(recipient.purchased.plan.dateCreated).to.not.exist;
await api.createSubscription(data);
expect(recipient.purchased.plan.dateCreated).to.exist;
});
it('does not change plan.customerId if it already exists', async () => {
recipient.purchased.plan = plan;
data.customerId = 'purchaserCustomerId';
@@ -161,9 +187,10 @@ describe('payments/index', () => {
it('sends a private message about the gift', async () => {
await api.createSubscription(data);
let msg = '\`Hello recipient, sender has sent you 3 months of subscription!\`';
expect(user.sendMessage).to.be.calledOnce;
expect(user.sendMessage).to.be.calledWith(recipient, '\`Hello recipient, sender has sent you 3 months of subscription!\`');
expect(user.sendMessage).to.be.calledWith(recipient, { receiverMsg: msg, senderMsg: msg });
});
it('sends an email about the gift', async () => {
@@ -186,6 +213,7 @@ describe('payments/index', () => {
expect(analytics.trackPurchase).to.be.calledOnce;
expect(analytics.trackPurchase).to.be.calledWith({
uuid: user._id,
groupId: undefined,
itemPurchased: 'Subscription',
sku: 'payment method-subscription',
purchaseType: 'subscribe',
@@ -276,6 +304,7 @@ describe('payments/index', () => {
expect(analytics.trackPurchase).to.be.calledOnce;
expect(analytics.trackPurchase).to.be.calledWith({
uuid: user._id,
groupId: undefined,
itemPurchased: 'Subscription',
sku: 'payment method-subscription',
purchaseType: 'subscribe',
@@ -291,6 +320,53 @@ describe('payments/index', () => {
});
});
context('Purchasing a subscription for group', () => {
it('creates a subscription', async () => {
expect(group.purchased.plan.planId).to.not.exist;
data.groupId = group._id;
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.planId).to.eql('basic_3mo');
expect(updatedGroup.purchased.plan.customerId).to.eql('customer-id');
expect(updatedGroup.purchased.plan.dateUpdated).to.exist;
expect(updatedGroup.purchased.plan.gemsBought).to.eql(0);
expect(updatedGroup.purchased.plan.paymentMethod).to.eql('Payment Method');
expect(updatedGroup.purchased.plan.extraMonths).to.eql(0);
expect(updatedGroup.purchased.plan.dateTerminated).to.eql(null);
expect(updatedGroup.purchased.plan.lastBillingDate).to.not.exist;
expect(updatedGroup.purchased.plan.dateCreated).to.exist;
});
it('sets extraMonths if plan has dateTerminated date', async () => {
group.purchased.plan = plan;
group.purchased.plan.dateTerminated = moment(new Date()).add(2, 'months');
await group.save();
expect(group.purchased.plan.extraMonths).to.eql(0);
data.groupId = group._id;
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.extraMonths).to.within(1.9, 2);
});
it('does not set negative extraMonths if plan has past dateTerminated date', async () => {
group.purchased.plan = plan;
group.purchased.plan.dateTerminated = moment(new Date()).subtract(2, 'months');
await group.save();
expect(group.purchased.plan.extraMonths).to.eql(0);
data.groupId = group._id;
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.extraMonths).to.eql(0);
});
});
context('Block subscription perks', () => {
it('adds block months to plan.consecutive.offset', async () => {
await api.createSubscription(data);
@@ -426,61 +502,169 @@ describe('payments/index', () => {
data = { user };
});
it('adds a month termination date by default', async () => {
await api.cancelSubscription(data);
context('Canceling a subscription for self', () => {
it('adds a month termination date by default', async () => {
await api.cancelSubscription(data);
let now = new Date();
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
let now = new Date();
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
expect(daysTillTermination).to.be.within(29, 30); // 1 month +/- 1 days
expect(daysTillTermination).to.be.within(29, 30); // 1 month +/- 1 days
});
it('adds extraMonths to dateTerminated value', async () => {
user.purchased.plan.extraMonths = 2;
await api.cancelSubscription(data);
let now = new Date();
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
expect(daysTillTermination).to.be.within(89, 90); // 3 months +/- 1 days
});
it('handles extra month fractions', async () => {
user.purchased.plan.extraMonths = 0.3;
await api.cancelSubscription(data);
let now = new Date();
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
expect(daysTillTermination).to.be.within(38, 39); // should be about 1 month + 1/3 month
});
it('terminates at next billing date if it exists', async () => {
data.nextBill = moment().add({ days: 15 });
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;
await api.cancelSubscription(data);
expect(user.purchased.plan.extraMonths).to.eql(0);
});
it('sends an email', async () => {
await api.cancelSubscription(data);
expect(sender.sendTxn).to.be.calledOnce;
expect(sender.sendTxn).to.be.calledWith(user, 'cancel-subscription');
});
});
it('adds extraMonths to dateTerminated value', async () => {
user.purchased.plan.extraMonths = 2;
context('Canceling a subscription for group', () => {
it('adds a month termination date by default', async () => {
data.groupId = group._id;
await api.cancelSubscription(data);
await api.cancelSubscription(data);
let now = new Date();
let updatedGroup = await Group.findById(group._id).exec();
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
let now = new Date();
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
expect(daysTillTermination).to.be.within(29, 30); // 1 month +/- 1 days
});
expect(daysTillTermination).to.be.within(89, 90); // 3 months +/- 1 days
});
it('adds extraMonths to dateTerminated value', async () => {
group.purchased.plan.extraMonths = 2;
await group.save();
data.groupId = group._id;
it('handles extra month fractions', async () => {
user.purchased.plan.extraMonths = 0.3;
await api.cancelSubscription(data);
await api.cancelSubscription(data);
let now = new Date();
let updatedGroup = await Group.findById(group._id).exec();
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
let now = new Date();
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
expect(daysTillTermination).to.be.within(89, 90); // 3 months +/- 1 days
});
expect(daysTillTermination).to.be.within(38, 39); // should be about 1 month + 1/3 month
});
it('handles extra month fractions', async () => {
group.purchased.plan.extraMonths = 0.3;
await group.save();
data.groupId = group._id;
it('terminates at next billing date if it exists', async () => {
data.nextBill = moment().add({ days: 15 });
await api.cancelSubscription(data);
await api.cancelSubscription(data);
let now = new Date();
let updatedGroup = await Group.findById(group._id).exec();
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
let now = new Date();
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
expect(daysTillTermination).to.be.within(38, 39); // should be about 1 month + 1/3 month
});
expect(daysTillTermination).to.be.within(13, 15);
});
it('terminates at next billing date if it exists', async () => {
data.nextBill = moment().add({ days: 15 });
data.groupId = group._id;
it('resets plan.extraMonths', async () => {
user.purchased.plan.extraMonths = 5;
await api.cancelSubscription(data);
await api.cancelSubscription(data);
let now = new Date();
let updatedGroup = await Group.findById(group._id).exec();
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
expect(user.purchased.plan.extraMonths).to.eql(0);
});
expect(daysTillTermination).to.be.within(13, 15);
});
it('sends an email', async () => {
await api.cancelSubscription(data);
it('resets plan.extraMonths', async () => {
group.purchased.plan.extraMonths = 5;
await group.save();
data.groupId = group._id;
expect(sender.sendTxn).to.be.calledOnce;
expect(sender.sendTxn).to.be.calledWith(user, 'cancel-subscription');
await api.cancelSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.extraMonths).to.eql(0);
});
it('sends an email', async () => {
data.groupId = group._id;
await api.cancelSubscription(data);
expect(sender.sendTxn).to.be.calledOnce;
expect(sender.sendTxn).to.be.calledWith(user, 'group-cancel-subscription');
});
it('prevents non group leader from manging subscription', async () => {
let groupMember = new User();
data.user = groupMember;
data.groupId = group._id;
await expect(api.cancelSubscription(data))
.eventually.be.rejected.and.to.eql({
httpCode: 401,
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
name: 'NotAuthorized',
});
});
it('allows old group leader to cancel if they created the subscription', async () => {
data.groupId = group._id;
data.sub = {
key: 'group_monthly',
};
data.paymentMethod = 'Payment Method';
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
let newLeader = new User();
updatedGroup.leader = newLeader._id;
await updatedGroup.save();
await api.cancelSubscription(data);
updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.dateTerminated).to.exist;
});
});
});
@@ -553,14 +737,190 @@ describe('payments/index', () => {
it('sends a message from purchaser to recipient', async () => {
await api.buyGems(data);
let msg = '\`Hello recipient, sender has sent you 4 gems!\`';
expect(user.sendMessage).to.be.calledWith(recipient, '\`Hello recipient, sender has sent you 4 gems!\`');
expect(user.sendMessage).to.be.calledWith(recipient, { receiverMsg: msg, senderMsg: msg });
});
it('sends a push notification if user did not gift to self', async () => {
await api.buyGems(data);
expect(notifications.sendNotification).to.be.calledOnce;
});
it('sends gem donation message in each participant\'s language', async () => {
// TODO using english for both users because other languages are not loaded
// for api.buyGems
await recipient.update({
'preferences.language': 'en',
});
await user.update({
'preferences.language': 'en',
});
await api.buyGems(data);
let [recipientsMessageContent, sendersMessageContent] = ['en', 'en'].map((lang) => {
let messageContent = t('giftedGemsFull', {
username: recipient.profile.name,
sender: user.profile.name,
gemAmount: data.gift.gems.amount,
}, lang);
return `\`${messageContent}\``;
});
expect(user.sendMessage).to.be.calledWith(recipient, { receiverMsg: recipientsMessageContent, senderMsg: sendersMessageContent });
});
});
});
describe('#upgradeGroupPlan', () => {
let spy;
beforeEach(function () {
spy = sinon.stub(stripe.subscriptions, 'update');
spy.returnsPromise().resolves([]);
data.groupId = group._id;
data.sub.quantity = 3;
});
afterEach(function () {
sinon.restore(stripe.subscriptions.update);
});
it('updates a group plan quantity', async () => {
data.paymentMethod = 'Stripe';
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.quantity).to.eql(3);
updatedGroup.memberCount += 1;
await updatedGroup.save();
await api.updateStripeGroupPlan(updatedGroup, stripe);
expect(spy.calledOnce).to.be.true;
expect(updatedGroup.purchased.plan.quantity).to.eql(4);
});
it('does not update a group plan quantity that has a payment method other than stripe', async () => {
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.quantity).to.eql(3);
updatedGroup.memberCount += 1;
await updatedGroup.save();
await api.updateStripeGroupPlan(updatedGroup, stripe);
expect(spy.calledOnce).to.be.false;
expect(updatedGroup.purchased.plan.quantity).to.eql(3);
});
});
describe('payWithStripe', () => {
let spy;
let stripeCreateCustomerSpy;
let createSubSpy;
beforeEach(function () {
spy = sinon.stub(stripe.subscriptions, 'update');
spy.returnsPromise().resolves;
stripeCreateCustomerSpy = sinon.stub(stripe.customers, 'create');
let stripCustomerResponse = {
subscriptions: {
data: [{id: 'test-id'}],
},
};
stripeCreateCustomerSpy.returnsPromise().resolves(stripCustomerResponse);
createSubSpy = sinon.stub(api, 'createSubscription');
createSubSpy.returnsPromise().resolves({});
data.groupId = group._id;
data.sub.quantity = 3;
});
afterEach(function () {
sinon.restore(stripe.subscriptions.update);
stripe.customers.create.restore();
api.createSubscription.restore();
});
it('subscribes with stripe', async () => {
let token = 'test-token';
let gift;
let sub = data.sub;
let groupId = group._id;
let email = 'test@test.com';
let headers = {};
let coupon;
await api.payWithStripe({
token,
user,
gift,
sub,
groupId,
email,
headers,
coupon,
}, stripe);
expect(stripeCreateCustomerSpy.calledOnce).to.be.true;
expect(createSubSpy.calledOnce).to.be.true;
});
});
describe('subscribeWithAmazon', () => {
let amazonSetBillingAgreementDetailsSpy;
let amazonConfirmBillingAgreementSpy;
let amazongAuthorizeOnBillingAgreementSpy;
let createSubSpy;
beforeEach(function () {
amazonSetBillingAgreementDetailsSpy = sinon.stub(amzLib, 'setBillingAgreementDetails');
amazonSetBillingAgreementDetailsSpy.returnsPromise().resolves({});
amazonConfirmBillingAgreementSpy = sinon.stub(amzLib, 'confirmBillingAgreement');
amazonConfirmBillingAgreementSpy.returnsPromise().resolves({});
amazongAuthorizeOnBillingAgreementSpy = sinon.stub(amzLib, 'authorizeOnBillingAgreement');
amazongAuthorizeOnBillingAgreementSpy.returnsPromise().resolves({});
createSubSpy = sinon.stub(api, 'createSubscription');
createSubSpy.returnsPromise().resolves({});
});
afterEach(function () {
amzLib.setBillingAgreementDetails.restore();
amzLib.confirmBillingAgreement.restore();
amzLib.authorizeOnBillingAgreement.restore();
api.createSubscription.restore();
});
it('subscribes with amazon', async () => {
let billingAgreementId = 'billingAgreementId';
let sub = data.sub;
let coupon;
let groupId = group._id;
let headers = {};
await api.subscribeWithAmazon({
billingAgreementId,
sub,
coupon,
user,
groupId,
headers,
});
expect(amazonSetBillingAgreementDetailsSpy.calledOnce).to.be.true;
expect(amazonConfirmBillingAgreementSpy.calledOnce).to.be.true;
expect(amazongAuthorizeOnBillingAgreementSpy.calledOnce).to.be.true;
expect(createSubSpy.calledOnce).to.be.true;
});
});
});
+4 -1
View File
@@ -19,6 +19,9 @@ describe('slack', () => {
profile: {
name: 'flagger',
},
preferences: {
language: 'flagger-lang',
},
},
group: {
id: 'group-id',
@@ -44,7 +47,7 @@ describe('slack', () => {
expect(IncomingWebhook.prototype.send).to.be.calledOnce;
expect(IncomingWebhook.prototype.send).to.be.calledWith({
text: 'flagger (flagger-id) flagged a message',
text: 'flagger (flagger-id) flagged a message (language: flagger-lang)',
attachments: [{
fallback: 'Flag Message',
color: 'danger',
+9
View File
@@ -2,6 +2,7 @@ import {
createTasks,
getTasks,
syncableAttrs,
moveTask,
} from '../../../../../website/server/libs/taskManager';
import i18n from '../../../../../website/common/script/i18n';
import {
@@ -169,4 +170,12 @@ describe('taskManager', () => {
expect(syncableTask.notes).to.not.exist;
expect(syncableTask.updatedAt).to.not.exist;
});
it('moves tasks to a specified position', async() => {
let order = ['task-id-1', 'task-id-2'];
moveTask(order, 'task-id-2', 0);
expect(order).to.eql(['task-id-2', 'task-id-1']);
});
});
-1
View File
@@ -98,7 +98,6 @@ describe('response middleware', () => {
{
type: notification.type,
id: notification.id,
createdAt: notification.createdAt,
data: {},
},
],
+35 -30
View File
@@ -6,32 +6,36 @@ import common from '../../../../../website/common/';
import { each, find } from 'lodash';
describe('Challenge Model', () => {
let guild, leader, challenge, task, tasksToTest;
let guild, leader, challenge, task;
let tasksToTest = {
habit: {
text: 'test habit',
type: 'habit',
up: false,
down: true,
notes: 'test notes',
},
todo: {
text: 'test todo',
type: 'todo',
notes: 'test notes',
},
daily: {
text: 'test daily',
type: 'daily',
frequency: 'daily',
everyX: 5,
startDate: new Date(),
notes: 'test notes',
},
reward: {
text: 'test reward',
type: 'reward',
notes: 'test notes',
},
};
beforeEach(async () => {
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',
},
};
guild = new Group({
name: 'test party',
type: 'guild',
@@ -77,6 +81,7 @@ describe('Challenge Model', () => {
});
expect(syncedTask).to.exist;
expect(syncedTask.notes).to.eql(task.notes);
});
it('syncs a challenge to a user', async () => {
@@ -96,8 +101,8 @@ describe('Challenge Model', () => {
});
expect(updatedNewMember.challenges).to.contain(challenge._id);
expect(updatedNewMember.tags[3].id).to.equal(challenge._id);
expect(updatedNewMember.tags[3].name).to.equal(challenge.shortName);
expect(updatedNewMember.tags[7].id).to.equal(challenge._id);
expect(updatedNewMember.tags[7].name).to.equal(challenge.shortName);
expect(syncedTask).to.exist;
});
@@ -161,7 +166,7 @@ describe('Challenge Model', () => {
context('type specific updates', () => {
it('updates habit specific field to challenge and challenge members', async () => {
task = new Tasks.habit(Tasks.Task.sanitize(tasksToTest.habit)); // eslint-disable-line babel/new-cap
task = new Tasks.habit(Tasks.Task.sanitize(tasksToTest.habit)); // eslint-disable-line new-cap
task.challenge.id = challenge._id;
await task.save();
@@ -180,7 +185,7 @@ describe('Challenge Model', () => {
});
it('updates todo specific field to challenge and challenge members', async () => {
task = new Tasks.todo(Tasks.Task.sanitize(tasksToTest.todo)); // eslint-disable-line babel/new-cap
task = new Tasks.todo(Tasks.Task.sanitize(tasksToTest.todo)); // eslint-disable-line new-cap
task.challenge.id = challenge._id;
await task.save();
@@ -196,7 +201,7 @@ describe('Challenge Model', () => {
});
it('does not update checklists on the user task', async () => {
task = new Tasks.todo(Tasks.Task.sanitize(tasksToTest.todo)); // eslint-disable-line babel/new-cap
task = new Tasks.todo(Tasks.Task.sanitize(tasksToTest.todo)); // eslint-disable-line new-cap
task.challenge.id = challenge._id;
await task.save();
@@ -214,7 +219,7 @@ describe('Challenge Model', () => {
});
it('updates daily specific field to challenge and challenge members', async () => {
task = new Tasks.daily(Tasks.Task.sanitize(tasksToTest.daily)); // eslint-disable-line babel/new-cap
task = new Tasks.daily(Tasks.Task.sanitize(tasksToTest.daily)); // eslint-disable-line new-cap
task.challenge.id = challenge._id;
await task.save();
+156 -12
View File
@@ -1,13 +1,16 @@
import { sleep } from '../../../../helpers/api-unit.helper';
import { model as Group, INVITES_LIMIT } from '../../../../../website/server/models/group';
import { model as User } from '../../../../../website/server/models/user';
import { BadRequest } from '../../../../../website/server/libs/errors';
import {
BadRequest,
} from '../../../../../website/server/libs/errors';
import { quests as questScrolls } from '../../../../../website/common/script/content';
import { groupChatReceivedWebhook } from '../../../../../website/server/libs/webhook';
import * as email from '../../../../../website/server/libs/email';
import validator from 'validator';
import { TAVERN_ID } from '../../../../../website/common/script/';
import { v4 as generateUUID } from 'uuid';
import shared from '../../../../../website/common';
describe('Group Model', () => {
let party, questLeader, participatingMember, nonParticipatingMember, undecidedMember;
@@ -628,24 +631,105 @@ describe('Group Model', () => {
});
});
it('deletes a private group when the last member leaves', async () => {
party.memberCount = 1;
it('deletes a private party when the last member leaves', async () => {
await party.leave(participatingMember);
await party.leave(questLeader);
await party.leave(nonParticipatingMember);
await party.leave(undecidedMember);
party = await Group.findOne({_id: party._id});
expect(party).to.not.exist;
});
it('does not delete a public group when the last member leaves', async () => {
it('does not delete a private group when the last member leaves and a subscription is active', async () => {
party.memberCount = 1;
party.purchased.plan.customerId = '110002222333';
await expect(party.leave(participatingMember))
.to.eventually.be.rejected.and.to.eql({
name: 'NotAuthorized',
httpCode: 401,
message: shared.i18n.t('cannotDeleteActiveGroup'),
});
party = await Group.findOne({_id: party._id});
expect(party).to.exist;
expect(party.memberCount).to.eql(1);
});
it('does not delete a public group when the last member leaves', async () => {
party.privacy = 'public';
await party.leave(participatingMember);
await party.leave(questLeader);
await party.leave(nonParticipatingMember);
await party.leave(undecidedMember);
party = await Group.findOne({_id: party._id});
expect(party).to.exist;
});
it('does not delete a private party when the member count reaches zero if there are still members', async () => {
party.memberCount = 1;
await party.leave(participatingMember);
party = await Group.findOne({_id: party._id});
expect(party).to.exist;
});
it('deletes a private guild when the last member leaves', async () => {
let guild = new Group({
name: 'test guild',
type: 'guild',
memberCount: 1,
});
let leader = new User({
guilds: [guild._id],
});
guild.leader = leader._id;
await Promise.all([
guild.save(),
leader.save(),
]);
await guild.leave(leader);
guild = await Group.findOne({_id: guild._id});
expect(guild).to.not.exist;
});
it('does not delete a private guild when the member count reaches zero if there are still members', async () => {
let guild = new Group({
name: 'test guild',
type: 'guild',
memberCount: 1,
});
let leader = new User({
guilds: [guild._id],
});
let member = new User({
guilds: [guild._id],
});
guild.leader = leader._id;
await Promise.all([
guild.save(),
leader.save(),
member.save(),
]);
await guild.leave(member);
guild = await Group.findOne({_id: guild._id});
expect(guild).to.exist;
});
});
describe('#sendChat', () => {
@@ -1061,8 +1145,45 @@ describe('Group Model', () => {
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
};
});
sandbox.spy(User, 'update');
describe('user update retry failures', () => {
let successfulMock = {
exec: () => {
return Promise.resolve({raw: 'sucess'});
},
};
let failedMock = {
exec: () => {
return Promise.reject(new Error('error'));
},
};
it('doesn\'t retry successful operations', async () => {
sandbox.stub(User, 'update').returns(successfulMock);
await party.finishQuest(quest);
expect(User.update).to.be.calledTwice;
});
it('stops retrying when a successful update has occurred', async () => {
let updateStub = sandbox.stub(User, 'update');
updateStub.onCall(0).returns(failedMock);
updateStub.returns(successfulMock);
await party.finishQuest(quest);
expect(User.update).to.be.calledThrice;
});
it('retries failed updates at most five times per user', async () => {
sandbox.stub(User, 'update').returns(failedMock);
await expect(party.finishQuest(quest)).to.eventually.be.rejected;
expect(User.update.callCount).to.eql(10);
});
});
it('gives out achievements', async () => {
@@ -1138,14 +1259,34 @@ describe('Group Model', () => {
expect(updatedParticipatingMember.items.hatchingPotions.Shade).to.eql(2);
});
it('awards quests', async () => {
it('awards quest scrolls to owner', async () => {
let questAwardQuest = questScrolls.vice2;
await party.finishQuest(questAwardQuest);
let updatedLeader = await User.findById(questLeader._id);
expect(updatedLeader.items.quests.vice3).to.eql(1);
});
it('awards non quest leader rewards to quest leader', async () => {
let gearQuest = questScrolls.vice3;
await party.finishQuest(gearQuest);
let updatedLeader = await User.findById(questLeader._id);
expect(updatedLeader.items.gear.owned.weapon_special_2).to.eql(true);
});
it('doesn\'t award quest owner rewards to all participants', async () => {
let questAwardQuest = questScrolls.vice2;
await party.finishQuest(questAwardQuest);
let updatedParticipatingMember = await User.findById(participatingMember._id);
expect(updatedParticipatingMember.items.quests.vice3).to.eql(1);
expect(updatedParticipatingMember.items.quests.vice3).to.not.exist;
});
it('awards pets', async () => {
@@ -1171,13 +1312,15 @@ describe('Group Model', () => {
context('Party quests', () => {
it('updates participating members with rewards', async () => {
sandbox.spy(User, 'update');
await party.finishQuest(quest);
expect(User.update).to.be.calledOnce;
expect(User.update).to.be.calledTwice;
expect(User.update).to.be.calledWithMatch({
_id: {
$in: [questLeader._id, participatingMember._id],
},
_id: questLeader._id,
});
expect(User.update).to.be.calledWithMatch({
_id: participatingMember._id,
});
});
@@ -1204,6 +1347,7 @@ describe('Group Model', () => {
});
it('updates all users with rewards', async () => {
sandbox.spy(User, 'update');
await party.finishQuest(tavernQuest);
expect(User.update).to.be.calledOnce;
+129 -22
View File
@@ -2,7 +2,7 @@ import { model as Challenge } from '../../../../../website/server/models/challen
import { model as Group } from '../../../../../website/server/models/group';
import { model as User } from '../../../../../website/server/models/user';
import * as Tasks from '../../../../../website/server/models/task';
import { each, find } from 'lodash';
import { each, find, findIndex } from 'lodash';
describe('Group Task Methods', () => {
let guild, leader, challenge, task;
@@ -68,11 +68,29 @@ describe('Group Task Methods', () => {
task = new Tasks[`${taskType}`](Tasks.Task.sanitize(taskValue));
task.group.id = guild._id;
await task.save();
if (task.checklist) {
task.checklist.push({
text: 'Checklist Item 1',
completed: false,
});
}
});
it('syncs an assigned task to a user', async () => {
await guild.syncTask(task, leader);
let updatedLeader = await User.findOne({_id: leader._id});
let tagIndex = findIndex(updatedLeader.tags, {id: guild._id});
let newTag = updatedLeader.tags[tagIndex];
expect(newTag.id).to.equal(guild._id);
expect(newTag.name).to.equal(guild.name);
expect(newTag.group).to.equal(guild._id);
});
it('create tags for a user when task is synced', async () => {
await guild.syncTask(task, leader);
let updatedLeader = await User.findOne({_id: leader._id});
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
@@ -96,35 +114,124 @@ describe('Group Task Methods', () => {
expect(syncedTask.text).to.equal(task.text);
});
it('syncs updated info for assigned task to all users', async () => {
let newMember = new User({
guilds: [guild._id],
});
await newMember.save();
it('syncs checklist items to an assigned user', async () => {
await guild.syncTask(task, leader);
await guild.syncTask(task, newMember);
let updatedTaskName = 'Update Task name';
task.text = updatedTaskName;
await guild.updateTask(task);
let updatedLeader = await User.findOne({_id: leader._id});
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
let updatedMember = await User.findOne({_id: newMember._id});
let updatedMemberTasks = await Tasks.Task.find({_id: { $in: updatedMember.tasksOrder[`${taskType}s`]}});
let syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
if (task.type !== 'daily' && task.type !== 'todo') return;
expect(task.group.assignedUsers).to.contain(leader._id);
expect(syncedTask).to.exist;
expect(syncedTask.text).to.equal(task.text);
expect(syncedTask.checklist.length).to.equal(task.checklist.length);
expect(syncedTask.checklist[0].text).to.equal(task.checklist[0].text);
});
expect(task.group.assignedUsers).to.contain(newMember._id);
expect(syncedMemberTask).to.exist;
expect(syncedMemberTask.text).to.equal(task.text);
describe('syncs updated info', async() => {
let newMember;
beforeEach(async () => {
newMember = new User({
guilds: [guild._id],
});
await newMember.save();
await guild.syncTask(task, leader);
await guild.syncTask(task, newMember);
});
it('syncs updated info for assigned task to all users', async () => {
let updatedTaskName = 'Update Task name';
task.text = updatedTaskName;
task.group.approval.required = true;
await guild.updateTask(task);
let updatedLeader = await User.findOne({_id: leader._id});
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
let updatedMember = await User.findOne({_id: newMember._id});
let updatedMemberTasks = await Tasks.Task.find({_id: { $in: updatedMember.tasksOrder[`${taskType}s`]}});
let syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
expect(task.group.assignedUsers).to.contain(leader._id);
expect(syncedTask).to.exist;
expect(syncedTask.text).to.equal(task.text);
expect(syncedTask.group.approval.required).to.equal(true);
expect(task.group.assignedUsers).to.contain(newMember._id);
expect(syncedMemberTask).to.exist;
expect(syncedMemberTask.text).to.equal(task.text);
expect(syncedMemberTask.group.approval.required).to.equal(true);
});
it('syncs a new checklist item to all assigned users', async () => {
if (task.type !== 'daily' && task.type !== 'todo') return;
let newCheckListItem = {
text: 'Checklist Item 1',
completed: false,
};
task.checklist.push(newCheckListItem);
await guild.updateTask(task, {newCheckListItem});
let updatedLeader = await User.findOne({_id: leader._id});
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
let updatedMember = await User.findOne({_id: newMember._id});
let updatedMemberTasks = await Tasks.Task.find({_id: { $in: updatedMember.tasksOrder[`${taskType}s`]}});
let syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
expect(syncedTask.checklist.length).to.equal(task.checklist.length);
expect(syncedTask.checklist[1].text).to.equal(task.checklist[1].text);
expect(syncedMemberTask.checklist.length).to.equal(task.checklist.length);
expect(syncedMemberTask.checklist[1].text).to.equal(task.checklist[1].text);
});
it('syncs updated info for checklist in assigned task to all users when flag is passed', async () => {
if (task.type !== 'daily' && task.type !== 'todo') return;
let updateCheckListText = 'Updated checklist item';
if (task.checklist) {
task.checklist[0].text = updateCheckListText;
}
await guild.updateTask(task, {updateCheckListItems: [task.checklist[0]]});
let updatedLeader = await User.findOne({_id: leader._id});
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
let updatedMember = await User.findOne({_id: newMember._id});
let updatedMemberTasks = await Tasks.Task.find({_id: { $in: updatedMember.tasksOrder[`${taskType}s`]}});
let syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
expect(syncedTask.checklist.length).to.equal(task.checklist.length);
expect(syncedTask.checklist[0].text).to.equal(updateCheckListText);
expect(syncedMemberTask.checklist.length).to.equal(task.checklist.length);
expect(syncedMemberTask.checklist[0].text).to.equal(updateCheckListText);
});
it('removes a checklist item in assigned task to all users when flag is passed with checklist id', async () => {
if (task.type !== 'daily' && task.type !== 'todo') return;
await guild.updateTask(task, {removedCheckListItemId: task.checklist[0].id});
let updatedLeader = await User.findOne({_id: leader._id});
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
let updatedMember = await User.findOne({_id: newMember._id});
let updatedMemberTasks = await Tasks.Task.find({_id: { $in: updatedMember.tasksOrder[`${taskType}s`]}});
let syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
expect(syncedTask.checklist.length).to.equal(0);
expect(syncedMemberTask.checklist.length).to.equal(0);
});
});
it('removes an assigned task and unlinks assignees', async () => {
+1 -1
View File
@@ -81,7 +81,7 @@ describe('Task Model', () => {
user = new User();
await user.save();
taskWithAlias = new Tasks.todo({ // eslint-disable-line babel/new-cap
taskWithAlias = new Tasks.todo({ // eslint-disable-line new-cap
text: 'some text',
alias: 'short-name',
userId: user.id,
+77 -4
View File
@@ -1,5 +1,6 @@
import { model as User } from '../../../../../website/server/models/user';
import common from '../../../../../website/common';
import Bluebird from 'bluebird';
describe('User Model', () => {
it('keeps user._tmp when calling .toJSON', () => {
@@ -48,28 +49,100 @@ describe('User Model', () => {
});
context('notifications', () => {
it('can add notifications with data', () => {
it('can add notifications without data', () => {
let user = new User();
user.addNotification('CRON');
let userToJSON = user.toJSON();
expect(user.notifications.length).to.equal(1);
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type', 'createdAt']);
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type']);
expect(userToJSON.notifications[0].type).to.equal('CRON');
expect(userToJSON.notifications[0].data).to.eql({});
});
it('can add notifications without data', () => {
it('can add notifications with data', () => {
let user = new User();
user.addNotification('CRON', {field: 1});
let userToJSON = user.toJSON();
expect(user.notifications.length).to.equal(1);
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type', 'createdAt']);
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type']);
expect(userToJSON.notifications[0].type).to.equal('CRON');
expect(userToJSON.notifications[0].data).to.eql({field: 1});
});
context('static push method', () => {
it('adds notifications for a single member via static method', async() => {
let user = new User();
await user.save();
await User.pushNotification({_id: user._id}, 'CRON');
user = await User.findOne({_id: user._id}).exec();
let userToJSON = user.toJSON();
expect(user.notifications.length).to.equal(1);
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type']);
expect(userToJSON.notifications[0].type).to.equal('CRON');
expect(userToJSON.notifications[0].data).to.eql({});
});
it('validates notifications via static method', async() => {
let user = new User();
await user.save();
expect(User.pushNotification({_id: user._id}, 'BAD_TYPE')).to.eventually.be.rejected;
});
it('adds notifications without data for all given users via static method', async() => {
let user = new User();
let otherUser = new User();
await Bluebird.all([user.save(), otherUser.save()]);
await User.pushNotification({_id: {$in: [user._id, otherUser._id]}}, 'CRON');
user = await User.findOne({_id: user._id}).exec();
let userToJSON = user.toJSON();
expect(user.notifications.length).to.equal(1);
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type']);
expect(userToJSON.notifications[0].type).to.equal('CRON');
expect(userToJSON.notifications[0].data).to.eql({});
user = await User.findOne({_id: otherUser._id}).exec();
userToJSON = user.toJSON();
expect(user.notifications.length).to.equal(1);
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type']);
expect(userToJSON.notifications[0].type).to.equal('CRON');
expect(userToJSON.notifications[0].data).to.eql({});
});
it('adds notifications with data for all given users via static method', async() => {
let user = new User();
let otherUser = new User();
await Bluebird.all([user.save(), otherUser.save()]);
await User.pushNotification({_id: {$in: [user._id, otherUser._id]}}, 'CRON', {field: 1});
user = await User.findOne({_id: user._id}).exec();
let userToJSON = user.toJSON();
expect(user.notifications.length).to.equal(1);
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type']);
expect(userToJSON.notifications[0].type).to.equal('CRON');
expect(userToJSON.notifications[0].data).to.eql({field: 1});
user = await User.findOne({_id: otherUser._id}).exec();
userToJSON = user.toJSON();
expect(user.notifications.length).to.equal(1);
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type']);
expect(userToJSON.notifications[0].type).to.equal('CRON');
expect(userToJSON.notifications[0].data).to.eql({field: 1});
});
});
});
});
@@ -275,7 +275,8 @@ describe('Challenges Controller', function() {
describe('editTask', function() {
it('is Tasks.editTask', function() {
inject(function(Tasks) {
expect(scope.editTask).to.eql(Tasks.editTask);
// @TODO: Currently we override the task function in the challenge ctrl, but we should abstract it again
// expect(scope.editTask).to.eql(Tasks.editTask);
});
});
});
@@ -28,7 +28,10 @@ describe('Inventory Controller', function() {
},
preferences: {
suppressModals: {}
}
},
purchased: {
plan: {}
},
});
Shared.wrap(user);
@@ -450,4 +453,84 @@ describe('Inventory Controller', function() {
expect(scope.hasAllTimeTravelerItemsOfType('mounts')).to.eql(true);
}));
});
describe('Gear search filter', function() {
var wrap = function(text) {
return {'text': function() {return text;}};
}
var toText = function(list) {
return _.map(list, function(ele) { return ele.text(); });
}
var gearByClass, gearByType;
beforeEach(function() {
scope.$digest();
gearByClass = {'raw': [wrap('kale'), wrap('sashimi')],
'cooked': [wrap('chicken'), wrap('potato')]};
gearByType = {'veg': [wrap('kale'), wrap('potato')],
'not': [wrap('chicken'), wrap('sashimi')]};
scope.gearByClass = gearByClass;
scope.gearByType = gearByType;
scope.equipmentQuery.query = 'a';
});
it('filters nothing if equipmentQuery is nothing', function() {
scope.equipmentQuery.query = '';
scope.$digest();
expect(toText(scope.filteredGearByClass['raw'])).to.eql(['kale', 'sashimi']);
expect(toText(scope.filteredGearByClass['cooked'])).to.eql(['chicken', 'potato']);
expect(toText(scope.filteredGearByType['veg'])).to.eql(['kale', 'potato']);
expect(toText(scope.filteredGearByType['not'])).to.eql(['chicken', 'sashimi']);
});
it('filters out gear if class gear changes', function() {
scope.$digest();
expect(toText(scope.filteredGearByClass['raw'])).to.eql(['kale', 'sashimi']);
expect(toText(scope.filteredGearByClass['cooked'])).to.eql(['potato']);
scope.gearByClass['raw'].push(wrap('zucchini'));
scope.gearByClass['cooked'].push(wrap('pizza'));
scope.$digest();
expect(toText(scope.filteredGearByClass['raw'])).to.eql(['kale', 'sashimi']);
expect(toText(scope.filteredGearByClass['cooked'])).to.eql(['potato', 'pizza']);
});
it('filters out gear if typed gear changes', function() {
scope.$digest();
expect(toText(scope.filteredGearByType['veg'])).to.eql(['kale', 'potato']);
expect(toText(scope.filteredGearByType['not'])).to.eql(['sashimi']);
scope.gearByType['veg'].push(wrap('zucchini'));
scope.gearByType['not'].push(wrap('pizza'));
scope.$digest();
expect(toText(scope.filteredGearByType['veg'])).to.eql(['kale', 'potato']);
expect(toText(scope.filteredGearByType['not'])).to.eql(['sashimi', 'pizza']);
});
it('filters out gear if filter query changes', function() {
scope.equipmentQuery.query = 'c';
scope.$digest();
expect(toText(scope.filteredGearByClass['raw'])).to.eql([]);
expect(toText(scope.filteredGearByClass['cooked'])).to.eql(['chicken']);
expect(toText(scope.filteredGearByType['veg'])).to.eql([]);
expect(toText(scope.filteredGearByType['not'])).to.eql(['chicken']);
});
it('returns the right filtered gear', function() {
var equipment = [wrap('spicy tuna'), wrap('dragon'), wrap('rainbow'), wrap('caterpillar')];
expect(toText(scope.equipmentSearch(equipment, 'ra'))).to.eql(['dragon', 'rainbow']);
});
it('returns the right filtered gear if the source gear has unicode', function() {
// blue hat, red hat, red shield
var equipment = [wrap('藍色軟帽'), wrap('紅色軟帽'), wrap('紅色盾牌')];
// searching for 'red' gives red hat, red shield
expect(toText(scope.equipmentSearch(equipment, '紅色'))).to.eql(['紅色軟帽', '紅色盾牌']);
});
});
});
@@ -14,6 +14,7 @@ describe('Notification Controller', function() {
let User = {
user,
readNotification: function noop () {},
readNotifications: function noop () {},
sync: userSync
};
@@ -32,7 +32,8 @@ describe('Tasks Controller', function() {
describe('editTask', function() {
it('is Tasks.editTask', function() {
inject(function(Tasks) {
expect(scope.editTask).to.eql(Tasks.editTask);
// @TODO: Currently we override the task function in the challenge ctrl, but we should abstract it again
// expect(scope.editTask).to.eql(Tasks.editTask);
});
});
});
@@ -194,6 +194,7 @@ describe('Analytics Service', function () {
rewards: 1
};
expectedProperties.balance = 12;
expectedProperties.balanceGemAmount = 48;
beforeEach(function() {
user._id = 'unique-user-id';
@@ -243,7 +244,8 @@ describe('Analytics Service', function () {
habits: 1,
rewards: 1
},
balance: 12
balance: 12,
balanceGemAmount: 48
};
beforeEach(function() {
@@ -16,6 +16,15 @@ describe('Tasks Service', function() {
rootScope.charts = {};
tasks = Tasks;
});
rootScope.openModal = function() {
return {
result: {
then: function() {},
catch: function() {},
},
};
};
});
it('calls get user tasks endpoint', function() {
@@ -81,6 +90,14 @@ describe('Tasks Service', function() {
$httpBackend.flush();
});
it('calls group move task endpoint', function() {
var taskId = 1;
var position = 0;
$httpBackend.expectPOST('/api/v3/group-tasks/' + taskId + '/move/to/' + position).respond({});
tasks.moveGroupTask(taskId, position);
$httpBackend.flush();
});
it('calls add check list item endpoint', function() {
var taskId = 1;
$httpBackend.expectPOST(apiV3Prefix + '/' + taskId + '/checklist').respond({});
@@ -151,7 +168,6 @@ describe('Tasks Service', function() {
});
describe('editTask', function() {
var task;
beforeEach(function(){
+5 -1
View File
@@ -1,5 +1,9 @@
{
"presets": ["es2015"],
"plugins": ["transform-object-rest-spread"],
"plugins": [
"transform-object-rest-spread",
"syntax-async-functions",
"transform-regenerator",
],
"comments": false
}
+6
View File
@@ -0,0 +1,6 @@
{
"env": {
"node": true,
"browser": true,
}
}
@@ -7,7 +7,7 @@
// for how to write custom assertions see
// http://nightwatchjs.org/guide#writing-custom-assertions
exports.assertion = function (selector, count) {
this.message = 'Testing if element <' + selector + '> has count: ' + count;
this.message = `Testing if element <${selector}> has count: ${count}`;
this.expected = count;
this.pass = function (val) {
return val === this.expected;
@@ -16,11 +16,10 @@ exports.assertion = function (selector, count) {
return res.value;
};
this.command = function (cb) {
var self = this;
return this.api.execute(function (selector) {
return document.querySelectorAll(selector).length;
}, [selector], function (res) {
cb.call(self, res);
return this.api.execute((el) => {
return document.querySelectorAll(el).length;
}, [selector], (res) => {
cb.call(this, res);
});
};
};
+31 -28
View File
@@ -1,45 +1,48 @@
/* eslint-disable camelcase */
require('babel-register');
var config = require('../../../webpack/config');
const config = require('../../../webpack/config');
const chromeDriverPath = require('chromedriver').path;
// http://nightwatchjs.org/guide#settings-file
module.exports = {
'src_folders': ['test/client/e2e/specs'],
'output_folder': 'test/client/e2e/reports',
'custom_assertions_path': ['test/client/e2e/custom-assertions'],
src_folders: ['test/client/e2e/specs'],
output_folder: 'test/client/e2e/reports',
custom_assertions_path: ['test/client/e2e/custom-assertions'],
'selenium': {
'start_process': true,
'server_path': 'node_modules/selenium-server/lib/runner/selenium-server-standalone-2.53.0.jar',
'host': '127.0.0.1',
'port': 4444,
'cli_args': {
'webdriver.chrome.driver': require('chromedriver').path,
selenium: {
start_process: true,
server_path: 'node_modules/selenium-server/lib/runner/selenium-server-standalone-2.53.0.jar',
host: '127.0.0.1',
port: 4444,
cli_args: {
'webdriver.chrome.driver': chromeDriverPath,
},
},
'test_settings': {
'default': {
'selenium_port': 4444,
'selenium_host': 'localhost',
'silent': true,
'globals': {
'devServerURL': 'http://localhost:' + (process.env.PORT || config.dev.port),
test_settings: {
default: {
selenium_port: 4444,
selenium_host: 'localhost',
silent: true,
globals: {
devServerURL: `http://localhost:${process.env.PORT || config.dev.port}`, // eslint-disable-line no-process-env
},
},
'chrome': {
'desiredCapabilities': {
'browserName': 'chrome',
'javascriptEnabled': true,
'acceptSslCerts': true,
chrome: {
desiredCapabilities: {
browserName: 'chrome',
javascriptEnabled: true,
acceptSslCerts: true,
},
},
'firefox': {
'desiredCapabilities': {
'browserName': 'firefox',
'javascriptEnabled': true,
'acceptSslCerts': true,
firefox: {
desiredCapabilities: {
browserName: 'firefox',
javascriptEnabled: true,
acceptSslCerts: true,
},
},
},
+5 -5
View File
@@ -1,6 +1,6 @@
// 1. start the dev server using production config
process.env.NODE_ENV = 'testing';
var server = require('../../../webpack/dev-server.js');
process.env.NODE_ENV = 'testing'; // eslint-disable-line no-process-env
const server = require('../../../webpack/dev-server.js');
// 2. run the nightwatch test suite against it
// to run in additional browsers:
@@ -9,7 +9,7 @@ var server = require('../../../webpack/dev-server.js');
// or override the environment flag, for example: `npm run e2e -- --env chrome,firefox`
// For more information on Nightwatch's config file, see
// http://nightwatchjs.org/guide#settings-file
var opts = process.argv.slice(2);
let opts = process.argv.slice(2);
if (opts.indexOf('--config') === -1) {
opts = opts.concat(['--config', 'test/client/e2e/nightwatch.conf.js']);
}
@@ -17,8 +17,8 @@ if (opts.indexOf('--env') === -1) {
opts = opts.concat(['--env', 'chrome']);
}
var spawn = require('cross-spawn');
var runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' });
const spawn = require('cross-spawn');
const runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' });
runner.on('exit', function (code) {
server.close();
+2 -3
View File
@@ -2,12 +2,11 @@
// http://nightwatchjs.org/guide#usage
module.exports = {
'default e2e tests': function (browser) {
'default e2e tests' (browser) {
// automatically uses dev Server port from /config.index.js
// default: http://localhost:8080
// see nightwatch.conf.js
var devServer = browser.globals.devServerURL;
const devServer = browser.globals.devServerURL;
browser
.url(devServer)
+3 -3
View File
@@ -1,11 +1,11 @@
// TODO verify if it's needed, added because Vuex require Promise in the global scope
// TODO verify if it's needed, added because Axios require Promise in the global scope
// and babel-runtime doesn't affect external libraries
require('babel-polyfill');
// require all test files (files that ends with .spec.js)
var testsContext = require.context('./specs', true, /\.spec$/);
let testsContext = require.context('./specs', true, /\.spec$/);
testsContext.keys().forEach(testsContext);
// require all .vue and .js files except main.js for coverage.
var srcContext = require.context('../../../website/client', true, /^\.\/(?=(?!main(\.js)?$))(?=(.*\.(vue|js)$))/);
let srcContext = require.context('../../../website/client', true, /^\.\/(?=(?!main(\.js)?$))(?=(.*\.(vue|js)$))/);
srcContext.keys().forEach(srcContext);
+14 -10
View File
@@ -3,14 +3,15 @@
// we are also using it with karma-webpack
// https://github.com/webpack/karma-webpack
var path = require('path');
var merge = require('webpack-merge');
var baseConfig = require('../../../webpack/webpack.base.conf');
var utils = require('../../../webpack/utils');
var webpack = require('webpack');
var projectRoot = path.resolve(__dirname, '../../../');
const path = require('path');
const merge = require('webpack-merge');
const baseConfig = require('../../../webpack/webpack.base.conf');
const utils = require('../../../webpack/utils');
const webpack = require('webpack');
const projectRoot = path.resolve(__dirname, '../../../');
const testEnv = require('../../../webpack/config/test.env');
var webpackConfig = merge(baseConfig, {
const webpackConfig = merge(baseConfig, {
// use inline sourcemap for karma-sourcemap-loader
module: {
loaders: utils.styleLoaders(),
@@ -23,7 +24,7 @@ var webpackConfig = merge(baseConfig, {
},
plugins: [
new webpack.DefinePlugin({
'process.env': require('../../../webpack/config/test.env'),
'process.env': testEnv,
}),
],
});
@@ -36,11 +37,14 @@ webpackConfig.module.preLoaders = webpackConfig.module.preLoaders || [];
webpackConfig.module.preLoaders.unshift({
test: /\.js$/,
loader: 'isparta',
include: path.resolve(projectRoot, 'website/client'),
include: [
path.resolve(projectRoot, 'website/client'),
path.resolve(projectRoot, 'website/common'),
],
});
// only apply babel for test files when using isparta
webpackConfig.module.loaders.some(function (loader, i) {
webpackConfig.module.loaders.some((loader) => {
if (loader.loader === 'babel') {
loader.include = path.resolve(projectRoot, 'test/client/unit');
return true;
@@ -0,0 +1,8 @@
import floorFilter from 'client/filters/floor';
describe('floor filter', () => {
it('can floor a decimal number', () => {
expect(floorFilter(4.567)).to.equal(4.56);
expect(floorFilter(4.562)).to.equal(4.56);
});
});
@@ -0,0 +1,8 @@
import roundFilter from 'client/filters/round';
describe('round filter', () => {
it('can round a decimal number', () => {
expect(roundFilter(4.567)).to.equal(4.57);
expect(roundFilter(4.562)).to.equal(4.56);
});
});
@@ -0,0 +1,13 @@
import { gems as userGems } from 'client/store/getters/user';
describe('userGems getter', () => {
it('returns the user\'s gems', () => {
expect(userGems({
state: {
user: {
balance: 4.5,
},
},
})).to.equal(18);
});
});
@@ -0,0 +1,19 @@
import i18n from 'client/plugins/i18n';
import commoni18n from 'common/script/i18n';
import Vue from 'vue';
describe('i18n plugin', () => {
before(() => {
i18n.install(Vue);
});
it('adds $t to Vue.prototype', () => {
expect(Vue.prototype.$t).to.be.a.function;
});
it('$t is a proxy for common/i18n.t', () => {
const result = (new Vue()).$t('reportBug');
expect(result).to.equal(commoni18n.t('reportBug'));
expect(result).to.equal('Report a Bug');
});
});
+53 -5
View File
@@ -1,6 +1,7 @@
import Vue from 'vue';
import storeInjector from 'inject?-vue!client/store';
import { mapState, mapGetters, mapActions } from 'client/store';
import { flattenAndNamespace } from 'client/store/helpers/internals';
describe('Store', () => {
let injectedStore;
@@ -14,11 +15,25 @@ describe('Store', () => {
computedName ({ state }) {
return `${state.name} computed!`;
},
...flattenAndNamespace({
nested: {
computedName ({ state }) {
return `${state.name} computed!`;
},
},
}),
},
'./actions': {
getName ({ state }, ...args) {
return [state.name, ...args];
},
...flattenAndNamespace({
nested: {
getName ({ state }, ...args) {
return [state.name, ...args];
},
},
}),
},
}).default;
});
@@ -41,17 +56,29 @@ describe('Store', () => {
injectedStore.state.name = 'test updated';
});
it('supports getters', () => {
expect(injectedStore.getters.computedName).to.equal('test computed!');
injectedStore.state.name = 'test updated';
expect(injectedStore.getters.computedName).to.equal('test updated computed!');
describe('getters', () => {
it('supports getters', () => {
expect(injectedStore.getters.computedName).to.equal('test computed!');
injectedStore.state.name = 'test updated';
expect(injectedStore.getters.computedName).to.equal('test updated computed!');
});
it('supports nested getters', () => {
expect(injectedStore.getters['nested:computedName']).to.equal('test computed!');
injectedStore.state.name = 'test updated';
expect(injectedStore.getters['nested:computedName']).to.equal('test updated computed!');
});
});
describe('actions', () => {
it('can be dispatched', () => {
it('can dispatch an action', () => {
expect(injectedStore.dispatch('getName', 1, 2, 3)).to.deep.equal(['test', 1, 2, 3]);
});
it('can dispatch a nested action', () => {
expect(injectedStore.dispatch('nested:getName', 1, 2, 3)).to.deep.equal(['test', 1, 2, 3]);
});
it('throws an error is the action doesn\'t exists', () => {
expect(() => injectedStore.dispatched('wrong')).to.throw;
});
@@ -116,5 +143,26 @@ describe('Store', () => {
},
});
});
it('flattenAndNamespace', () => {
let result = flattenAndNamespace({
nested: {
computed ({ state }, ...args) {
return [state.name, ...args];
},
getName ({ state }, ...args) {
return [state.name, ...args];
},
},
nested2: {
getName ({ state }, ...args) {
return [state.name, ...args];
},
},
});
expect(Object.keys(result).length).to.equal(3);
expect(Object.keys(result).sort()).to.deep.equal(['nested2:getName', 'nested:computed', 'nested:getName']);
});
});
});
+365
View File
@@ -0,0 +1,365 @@
import shared from '../../../website/common';
import {
generateUser,
} from '../../helpers/common.helper';
describe('achievements', () => {
describe('general well-formedness', () => {
let user = generateUser();
let achievements = shared.achievements.getAchievementsForProfile(user);
it('each category has \'label\' and \'achievements\' fields', () => {
_.each(achievements, (category) => {
expect(category).to.have.property('label')
.that.is.a('string');
expect(category).to.have.property('achievements')
.that.is.a('object');
});
});
it('each achievement has all required fields of correct types', () => {
_.each(achievements, (category) => {
_.each(category.achievements, (achiev) => {
// May have additional fields (such as 'value' and 'optionalCount').
expect(achiev).to.contain.all.keys(['title', 'text', 'icon', 'earned', 'index']);
expect(achiev.title).to.be.a('string');
expect(achiev.text).to.be.a('string');
expect(achiev.icon).to.be.a('string');
expect(achiev.earned).to.be.a('boolean');
expect(achiev.index).to.be.a('number');
});
});
});
it('categories have unique labels', () => {
let achievementsArray = _.values(achievements).map(cat => cat.label);
let labels = _.uniq(achievementsArray);
expect(labels.length).to.be.greaterThan(0);
expect(labels.length).to.eql(_.size(achievements));
});
it('achievements have unique keys', () => {
let keysSoFar = {};
_.each(achievements, (category) => {
_.keys(category.achievements).forEach((key) => {
expect(keysSoFar[key]).to.be.undefined;
keysSoFar[key] = key;
});
});
});
it('achievements have unique indices', () => {
let indicesSoFar = {};
_.each(achievements, (category) => {
_.each(category.achievements, (achiev) => {
let i = achiev.index;
expect(indicesSoFar[i]).to.be.undefined;
indicesSoFar[i] = i;
});
});
});
it('all categories have at least 1 achievement', () => {
_.each(achievements, (category) => {
expect(_.size(category.achievements)).to.be.greaterThan(0);
});
});
});
describe('unearned basic achievements', () => {
let user = generateUser();
let basicAchievs = shared.achievements.getAchievementsForProfile(user).basic.achievements;
it('streak and perfect day achievements exist with counts', () => {
let streak = basicAchievs.streak;
let perfect = basicAchievs.perfect;
expect(streak).to.exist;
expect(streak).to.have.property('optionalCount')
.that.is.a('number');
expect(perfect).to.exist;
expect(perfect).to.have.property('optionalCount')
.that.is.a('number');
});
it('party up/on achievements exist with no counts', () => {
let partyUp = basicAchievs.partyUp;
let partyOn = basicAchievs.partyOn;
expect(partyUp).to.exist;
expect(partyUp.optionalCount).to.be.undefined;
expect(partyOn).to.exist;
expect(partyOn.optionalCount).to.be.undefined;
});
it('pet/mount master and triad bingo achievements exist with counts', () => {
let beastMaster = basicAchievs.beastMaster;
let mountMaster = basicAchievs.mountMaster;
let triadBingo = basicAchievs.triadBingo;
expect(beastMaster).to.exist;
expect(beastMaster).to.have.property('optionalCount')
.that.is.a('number');
expect(mountMaster).to.exist;
expect(mountMaster).to.have.property('optionalCount')
.that.is.a('number');
expect(triadBingo).to.exist;
expect(triadBingo).to.have.property('optionalCount')
.that.is.a('number');
});
it('ultimate gear achievements exist with no counts', () => {
let gearTypes = ['healer', 'rogue', 'warrior', 'mage'];
gearTypes.forEach((gear) => {
let gearAchiev = basicAchievs[`${gear}UltimateGear`];
expect(gearAchiev).to.exist;
expect(gearAchiev.optionalCount).to.be.undefined;
});
});
it('rebirth achievement exists with no count', () => {
let rebirth = basicAchievs.rebirth;
expect(rebirth).to.exist;
expect(rebirth.optionalCount).to.be.undefined;
});
});
describe('unearned seasonal achievements', () => {
let user = generateUser();
let seasonalAchievs = shared.achievements.getAchievementsForProfile(user).seasonal.achievements;
it('habiticaDays and habitBirthdays achievements exist with counts', () => {
let habiticaDays = seasonalAchievs.habiticaDays;
let habitBirthdays = seasonalAchievs.habitBirthdays;
expect(habiticaDays).to.exist;
expect(habiticaDays).to.have.property('optionalCount')
.that.is.a('number');
expect(habitBirthdays).to.exist;
expect(habitBirthdays).to.have.property('optionalCount')
.that.is.a('number');
});
it('spell achievements exist with counts', () => {
let spellTypes = ['snowball', 'spookySparkles', 'shinySeed', 'seafoam'];
spellTypes.forEach((spell) => {
let spellAchiev = seasonalAchievs[spell];
expect(spellAchiev).to.exist;
expect(spellAchiev).to.have.property('optionalCount')
.that.is.a('number');
});
});
it('quest achievements do not exist', () => {
let quests = ['dilatory', 'stressbeast', 'burnout', 'bewilder'];
quests.forEach((quest) => {
let questAchiev = seasonalAchievs[`${quest}Quest`];
expect(questAchiev).to.not.exist;
});
});
it('costumeContests achievement exists with count', () => {
let costumeContests = seasonalAchievs.costumeContests;
expect(costumeContests).to.exist;
expect(costumeContests).to.have.property('optionalCount')
.that.is.a('number');
});
it('card achievements exist with counts', () => {
let cardTypes = ['greeting', 'thankyou', 'nye', 'valentine', 'birthday'];
cardTypes.forEach((card) => {
let cardAchiev = seasonalAchievs[`${card}Cards`];
expect(cardAchiev).to.exist;
expect(cardAchiev).to.have.property('optionalCount')
.that.is.a('number');
});
});
});
describe('unearned special achievements', () => {
let user = generateUser();
let specialAchievs = shared.achievements.getAchievementsForProfile(user).special.achievements;
it('habitSurveys achievement exists with count', () => {
let habitSurveys = specialAchievs.habitSurveys;
expect(habitSurveys).to.exist;
expect(habitSurveys).to.have.property('optionalCount')
.that.is.a('number');
});
it('contributor achievement exists with value and no count', () => {
let contributor = specialAchievs.contributor;
expect(contributor).to.exist;
expect(contributor).to.have.property('value')
.that.is.a('number');
expect(contributor.optionalCount).to.be.undefined;
});
it('npc achievement is hidden if unachieved', () => {
let npc = specialAchievs.npc;
expect(npc).to.not.exist;
});
it('kickstarter achievement is hidden if unachieved', () => {
let kickstarter = specialAchievs.kickstarter;
expect(kickstarter).to.not.exist;
});
it('veteran achievement is hidden if unachieved', () => {
let veteran = specialAchievs.veteran;
expect(veteran).to.not.exist;
});
it('originalUser achievement is hidden if unachieved', () => {
let originalUser = specialAchievs.originalUser;
expect(originalUser).to.not.exist;
});
});
describe('earned seasonal achievements', () => {
let user = generateUser();
let quests = ['dilatory', 'stressbeast', 'burnout', 'bewilder'];
quests.forEach((quest) => {
user.achievements.quests[quest] = 1;
});
let seasonalAchievs = shared.achievements.getAchievementsForProfile(user).seasonal.achievements;
it('quest achievements exist', () => {
quests.forEach((quest) => {
let questAchiev = seasonalAchievs[`${quest}Quest`];
expect(questAchiev).to.exist;
expect(questAchiev.optionalCount).to.be.undefined;
});
});
});
describe('earned special achievements', () => {
let user = generateUser({
achievements: {
habitSurveys: 2,
veteran: true,
originalUser: true,
},
backer: {tier: 3},
contributor: {level: 1},
});
let specialAchievs = shared.achievements.getAchievementsForProfile(user).special.achievements;
it('habitSurveys achievement is earned with correct value', () => {
let habitSurveys = specialAchievs.habitSurveys;
expect(habitSurveys).to.exist;
expect(habitSurveys.earned).to.eql(true);
expect(habitSurveys.value).to.eql(2);
});
it('contributor achievement is earned with correct value', () => {
let contributor = specialAchievs.contributor;
expect(contributor).to.exist;
expect(contributor.earned).to.eql(true);
expect(contributor.value).to.eql(1);
});
it('npc achievement is earned with correct value', () => {
let npcUser = generateUser({
backer: {npc: 'test'},
});
let npc = shared.achievements.getAchievementsForProfile(npcUser).special.achievements.npc;
expect(npc).to.exist;
expect(npc.earned).to.eql(true);
expect(npc.value).to.eql('test');
});
it('kickstarter achievement is earned with correct value', () => {
let kickstarter = specialAchievs.kickstarter;
expect(kickstarter).to.exist;
expect(kickstarter.earned).to.eql(true);
expect(kickstarter.value).to.eql(3);
});
it('veteran achievement is earned', () => {
let veteran = specialAchievs.veteran;
expect(veteran).to.exist;
expect(veteran.earned).to.eql(true);
});
it('originalUser achievement is earned', () => {
let originalUser = specialAchievs.originalUser;
expect(originalUser).to.exist;
expect(originalUser.earned).to.eql(true);
});
});
describe('mountMaster, beastMaster, and triadBingo achievements', () => {
it('master and triad bingo achievements do not include *Text2 strings if no keys have been used', () => {
let user = generateUser();
let basicAchievs = shared.achievements.getAchievementsForProfile(user).basic.achievements;
let beastMaster = basicAchievs.beastMaster;
let mountMaster = basicAchievs.mountMaster;
let triadBingo = basicAchievs.triadBingo;
expect(beastMaster.text).to.not.match(/released/);
expect(beastMaster.text).to.not.match(/0 time\(s\)/);
expect(mountMaster.text).to.not.match(/released/);
expect(mountMaster.text).to.not.match(/0 time\(s\)/);
expect(triadBingo.text).to.not.match(/released/);
expect(triadBingo.text).to.not.match(/0 time\(s\)/);
});
it('master and triad bingo achievements includes *Text2 strings if keys have been used', () => {
let user = generateUser({
achievements: {
beastMasterCount: 1,
mountMasterCount: 2,
triadBingoCount: 3,
},
});
let basicAchievs = shared.achievements.getAchievementsForProfile(user).basic.achievements;
let beastMaster = basicAchievs.beastMaster;
let mountMaster = basicAchievs.mountMaster;
let triadBingo = basicAchievs.triadBingo;
expect(beastMaster.text).to.match(/released/);
expect(beastMaster.text).to.match(/1 time\(s\)/);
expect(mountMaster.text).to.match(/released/);
expect(mountMaster.text).to.match(/2 time\(s\)/);
expect(triadBingo.text).to.match(/released/);
expect(triadBingo.text).to.match(/3 time\(s\)/);
});
});
describe('ultimateGear achievements', () => {
it('title and text contain localized class info', () => {
let user = generateUser();
let basicAchievs = shared.achievements.getAchievementsForProfile(user).basic.achievements;
let gearTypes = ['healer', 'rogue', 'warrior', 'mage'];
gearTypes.forEach((gear) => {
let gearAchiev = basicAchievs[`${gear}UltimateGear`];
let classNameRegex = new RegExp(gear.charAt(0).toUpperCase() + gear.slice(1));
expect(gearAchiev.title).to.match(classNameRegex);
expect(gearAchiev.text).to.match(classNameRegex);
});
});
});
});
+1 -1
View File
@@ -18,7 +18,7 @@ describe('shared.ops.blockUser', () => {
it('validates uuid', (done) => {
try {
blockUser(user, { params: { uuid: 1 } });
blockUser(user, { params: { uuid: '1' } });
} catch (error) {
expect(error.message).to.eql(i18n.t('invalidUUID'));
done();
+10 -1
View File
@@ -11,7 +11,7 @@ import {
} from '../../helpers/common.helper';
describe('shared.ops.purchase', () => {
const SEASONAL_FOOD = 'Candy_Base';
const SEASONAL_FOOD = 'Meat';
let user;
let goldPoints = 40;
let gemsBought = 40;
@@ -147,6 +147,15 @@ describe('shared.ops.purchase', () => {
expect(user.stats.gp).to.equal(goldPoints - planGemLimits.convRate);
});
it('purchases gems with a different language than the default', () => {
let [, message] = purchase(user, {params: {type: 'gems', key: 'gem'}, language: 'de'});
expect(message).to.equal(i18n.t('plusOneGem', 'de'));
expect(user.balance).to.equal(userGemAmount + 0.5);
expect(user.purchased.plan.gemsBought).to.equal(2);
expect(user.stats.gp).to.equal(goldPoints - planGemLimits.convRate * 2);
});
it('purchases eggs', () => {
let type = 'eggs';
let key = 'Wolf';
+65 -6
View File
@@ -1,3 +1,5 @@
/* eslint-disable camelcase */
import revive from '../../../website/common/script/ops/revive';
import i18n from '../../../website/common/script/i18n';
import {
@@ -53,7 +55,21 @@ describe('shared.ops.revive', () => {
expect(user.stats.str).to.equal(1);
});
it('TODO: test actual ways stats are affected');
it('it decreases a random stat from str, con, per, int by one', () => {
let stats = ['str', 'con', 'per', 'int'];
_.each(stats, (s) => {
user.stats[s] = 1;
});
revive(user);
let statSum = _.reduce(stats, (m, k) => {
return m + user.stats[k];
}, 0);
expect(statSum).to.equal(3);
});
it('removes a random item from user gear owned', () => {
let weaponKey = 'weapon_warrior_0';
@@ -65,15 +81,58 @@ describe('shared.ops.revive', () => {
expect(user.items.gear.owned[weaponKey]).to.be.false;
});
it('does not remove 0 value items');
it('does not remove 0 value items', () => {
user.items.gear.owned = {
eyewear_special_yellowTopFrame: true,
};
it('allows removing warrior sword (0 value item)');
revive(user);
it('does not remove items of a different class');
expect(user.items.gear.owned.eyewear_special_yellowTopFrame).to.be.true;
});
it('removes "special" items');
it('allows removing warrior sword (0 value item)', () => {
user.items.gear.owned = {
weapon_warrior_0: true,
};
it('removes "armoire" items');
let weaponKey = 'weapon_warrior_0';
let [, message] = revive(user);
expect(message).to.equal(i18n.t('messageLostItem', { itemText: content.gear.flat[weaponKey].text()}));
expect(user.items.gear.owned[weaponKey]).to.be.false;
});
it('does not remove items of a different class', () => {
let weaponKey = 'weapon_wizard_1';
user.items.gear.owned[weaponKey] = true;
let [, message] = revive(user);
expect(message).to.equal('');
expect(user.items.gear.owned[weaponKey]).to.be.true;
});
it('removes "special" items', () => {
let weaponKey = 'weapon_special_1';
user.items.gear.owned[weaponKey] = true;
let [, message] = revive(user);
expect(message).to.equal(i18n.t('messageLostItem', { itemText: content.gear.flat[weaponKey].text()}));
expect(user.items.gear.owned[weaponKey]).to.be.false;
});
it('removes "armoire" items', () => {
let weaponKey = 'armor_armoire_goldenToga';
user.items.gear.owned[weaponKey] = true;
let [, message] = revive(user);
expect(message).to.equal(i18n.t('messageLostItem', { itemText: content.gear.flat[weaponKey].text()}));
expect(user.items.gear.owned[weaponKey]).to.be.false;
});
it('dequips lost item from user if user had it equipped', () => {
let weaponKey = 'weapon_warrior_0';
+10
View File
@@ -157,6 +157,16 @@ describe('shared.ops.scoreTask', () => {
expect(ref.afterUser._tmp.quest.progressDelta).to.eql(secondTaskDelta);
});
it('does not modify stats when task need approval', () => {
todo.group.approval.required = true;
options = { user: ref.afterUser, task: todo, direction: 'up', times: 5, cron: false };
scoreTask(options);
expect(ref.afterUser.stats.hp).to.eql(50);
expect(ref.afterUser.stats.exp).to.equal(ref.beforeUser.stats.exp);
expect(ref.afterUser.stats.gp).to.equal(ref.beforeUser.stats.gp);
});
context('habits', () => {
it('up', () => {
options = { user: ref.afterUser, task: habit, direction: 'up', times: 5, cron: false };
+29
View File
@@ -0,0 +1,29 @@
import _ from 'lodash';
import {
generateUser,
} from '../helpers/common.helper';
import timeTravelers from '../../website/common/script/content/time-travelers'
describe('time-travelers store', () => {
let user;
beforeEach(() => {
user = generateUser();
});
it('removes owned sets from the time travelers store', () => {
user.items.gear.owned['head_mystery_201602'] = true;
expect(timeTravelers.timeTravelerStore(user)['201602']).to.not.exist;
expect(timeTravelers.timeTravelerStore(user)['201603']).to.exist;
});
it('removes unopened mystery item sets from the time travelers store', () => {
user.purchased = {
plan: {
mysteryItems: ['head_mystery_201602'],
},
};
expect(timeTravelers.timeTravelerStore(user)['201602']).to.not.exist;
expect(timeTravelers.timeTravelerStore(user)['201603']).to.exist;
});
});
+2 -2
View File
@@ -86,7 +86,7 @@ export function generateTodo (user) {
completed: false,
};
let task = new Tasks.todo(Tasks.Task.sanitize(todo)); // eslint-disable-line babel/new-cap
let task = new Tasks.todo(Tasks.Task.sanitize(todo)); // eslint-disable-line new-cap
task.userId = user._id;
task.save();
@@ -101,7 +101,7 @@ export function generateDaily (user) {
completed: false,
};
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line babel/new-cap
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line new-cap
task.userId = user._id;
task.save();
+2
View File
@@ -13,5 +13,7 @@ chai.use(require('sinon-chai'));
chai.use(require('chai-as-promised'));
global.expect = chai.expect;
global.sinon = require('sinon');
let sinonStubPromise = require('sinon-stub-promise');
sinonStubPromise(global.sinon);
global.sandbox = sinon.sandbox.create();
global.Promise = Bluebird;

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