Compare commits

..

174 Commits

Author SHA1 Message Date
Sabe Jones
539f0e33e2 4.64.1 2018-10-05 19:55:23 +00:00
Sabe Jones
405e053377 chore(i18n): update locales 2018-10-05 19:54:21 +00:00
Phillip Thelen
52fbb8f899 Verify username as valid if user is re-checking their current name (#10737)
* Verify username as valid if user is re-checking their current name

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

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

* Implement API call to verify username without setting it

* Improve coding style

* Apply username verification to registration

* Update error messages

* Validate display names.

* Fix API early Stat Point allocation (#10680)

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

* Check that user has selected class before allocating stat points

* chore(event): end Ember Hatching Potions

* chore(analytics): reenable navigation tracking

* update bcrypt

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

* Animal ears after death (#10691)

* Animal Ears purchasable with Gold if lost in Death

* remove ears from pinned items when set is bought

* standardise css and error handling for gems and coins

* revert accidental new line

* fix client tests

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

* chore(i18n): update locales

* 4.61.1

* feat(content): Subscriber Items and Magic Potions

* chore(sprites): compile

* chore(i18n): update locales

* 4.62.0

* Display notification for users to confirm their username

* fix typo

* WIP(usernames): Changes to address #10694

* WIP(usernames): Further changes for #10694

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

* Change verify username notification to new version

* Improve feedback for invalid usernames

* Allow user to set their username again to confirm it

* Improve validation display for usernames

* Temporarily move display name validation outside of schema

* Improve rendering banner about sleeping in the inn

See #10695

* Display settings in one column

* Position inn banner when window is resized

* Update inn banner handling

* Fix banner offset on initial load

* Fix minor issues.

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

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

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

* chore(news): Bailey announcements

* chore(i18n): update locales

* 4.62.1

* adjust wiki link for usernameInfo string

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

* raise coverage for tasks api calls (#10029)

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

* add expect to test the new checklist length

* - moves tasks to a specified position out of length

* remove unused line

* website getter tasks tests

* re-add sanitizeUserChallengeTask

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

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

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

* Change update username API call

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

* feat(content): Subscriber Items and Magic Potions

* Re-add register call

* Fix merge issue

* Fix issue with setting username

* Implement new alert style

* Display username confirmation status in settings

* Add disclaimer to change username field

* validate username in settings

* Allow specific fields to be focused when opening site settings

* Implement requested changes.

* Fix merge issue

* Fix failing tests

* verify username when users register with username and password

* Set ID for change username notification

* Disable submit button if username is invalid

* Improve username confirmation handling

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

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

This reverts commit 9b6609ad64.

* Social user username (#10620)

* Refactored private functions to library

* Refactored social login code

* Added username to social registration

* Changed id library

* Added new local auth check

* Fixed export error. Fixed password check error

* fix(settings): password not available on client

* refactor(settings): more sensible placement of methods

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

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

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

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

* fix(migration): unused var

* fix(usernames): only generate 20 characters

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

* add expect to test the new checklist length

* - moves tasks to a specified position out of length

* remove unused line

* website getter tasks tests

* re-add sanitizeUserChallengeTask

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

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

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

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

* remove ears from pinned items when set is bought

* standardise css and error handling for gems and coins

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

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

* disable inbox schema

* inbox: use separate model

* remove old code that used group.chat

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

* remove inbox exclusions when loading user

* add GET /api/v3/inbox/messages

* add comment

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

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

* implement DELETE /api/v4/inbox/clear

* fix url

* fix doc

* update /export/inbox.html

* update other data exports

* add back messages in user schema

* add user.toJSONWithInbox

* add compativility until migration is done

* more compatibility

* fix tojson called twice

* add compatibility methods

* fix common tests

* fix v4 integration tests

* v3 get user -> with inbox

* start to fix tests

* fix v3 integration tests

* wip

* wip, client use new route

* update tests for members/send-private-message

* tests for get user in v4

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

* add tests for DELETE /inbox/clear in v4

* update docs

* fix tests

* initial migration

* fix migration

* fix migration

* migration fixes

* migrate api.enterCouponCode

* migrate api.castSpell

* migrate reset, reroll, rebirth

* add routes to v4 version

* fix tests

* fixes

* api.updateUser

* remove .only

* get user -> userLib

* refactor inbox.vue to work with new data model

* fix return message when messaging yourself

* wip fix bug with new conversation

* wip

* fix remaining ui issues

* move api.registerLocal, fixes

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

* fix client unit test

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

* show scrollbar only if needed

* extract featuredItemsHeader / pinUtils

* extract pageLayout

* extract layoutSection / filterDropdown - fix sortByNumber

* rollback sortByNumber order-fix

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

* refactor sellModal

* extract checkbox

* extract equipment section

* extract category row

* revert scroll - remove sellModal item template

* fix(lint): commas and semis

* Created category item component (#10613)

* extract filter sidebar

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

* fixes #10659

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

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

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

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

* add isLeader function for challenges

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

* delete duplicate test

* clarify title of existing tests

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

* fix lint errors

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

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

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

* Added Contributor Titles to Names on Hover

* added contributor title text on hover

* added contributor titles on hover in chat

* added contributor titles to text on hover

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

* add tests for sendPrivateMessage returning the message

* update DELETE user message tests

* port v3 GET-inbox_messages

* use v4 delete message route

* sendPrivateMessage: return sent message

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

* improve comment

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

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

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

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

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

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

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

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

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

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

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

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

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

* Fixed toFixed rounding
2018-08-18 21:08:56 -05:00
Keith Holliday
d6514bce8b Fixed inbox id after add (#10609) 2018-08-18 21:08:32 -05:00
Alys
603fc8c4dd combine cron's Resting in the Inn code with non-sleeping code - fixes #5232 etc (#10577)
* remove commented-out code for purging PMs - no longer needed

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

* adjust comments

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

* rename tests to use consistent terminology for sleeping

* add tests for cron when user is sleeping

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

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

* replace a broken test with new tests

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

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

* remove old test code
2018-08-18 12:47:07 +02:00
Sabe Jones
3c602351f9 Merge branch 'release' into develop 2018-08-18 03:32:39 +00:00
Keith Holliday
6aa204c3f5 Fixed concurrency issues with push devices (#10598)
* Fixed concurrency issues with push devices

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

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

* adding chair assets and wiring them to customize screen

* adding customization data for new wheelchair types

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

* explicitly re-enabilng linting rule

* adding button-sized chair assets

* updating assets to new resolution

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

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

Adding test for heal cast if user has full health

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

* check for 404

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

* update isDue for new tasks

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

* Removed forced and prevent overlay click
2018-08-16 16:53:37 -05:00
Matteo Pagliazzi
696121fb24 remove auth with url 2018-08-15 10:40:25 +02:00
827 changed files with 43673 additions and 33319 deletions

View File

@@ -17,7 +17,7 @@
"NODE_DB_URI":"mongodb://localhost/habitrpg",
"TEST_DB_URI":"mongodb://localhost/habitrpg_test",
"NODE_ENV":"development",
"ENABLE_CONSOLE_LOGS_IN_TEST": false,
"ENABLE_CONSOLE_LOGS_IN_TEST": "false",
"CRON_SAFE_MODE":"false",
"CRON_SEMI_SAFE_MODE":"false",
"MAINTENANCE_MODE": "false",

View File

@@ -0,0 +1,123 @@
const migrationName = '20180811_inboxOutsideUser.js';
const authorName = 'paglias'; // in case script author needs to know when their ...
const authorUuid = 'ed4c688c-6652-4a92-9d03-a5a79844174a'; // ... own data is done
/*
* Move inbox messages from the user model to their own collection
*/
const monk = require('monk');
const nconf = require('nconf');
const Inbox = require('../website/server/models/message').inboxModel;
const connectionString = nconf.get('MIGRATION_CONNECT_STRING'); // FOR TEST DATABASE
const dbInboxes = monk(connectionString).get('inboxes', { castIds: false });
const dbUsers = monk(connectionString).get('users', { castIds: false });
function processUsers (lastId) {
let query = {
migration: {$ne: migrationName},
};
if (lastId) {
query._id = {
$gt: lastId,
};
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 1000,
fields: ['_id', 'inbox'],
})
.then(updateUsers)
.catch((err) => {
console.log(err);
return exiting(1, `ERROR! ${ err}`);
});
}
let progressCount = 1000;
let count = 0;
let msgCount = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users and their tasks found and modified.');
displayData();
return;
}
let usersPromises = users.map(updateUser);
let lastUser = users[users.length - 1];
return Promise.all(usersPromises)
.then(() => {
return processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
if (count % progressCount === 0) console.warn(`${count } ${ user._id}`);
if (msgCount % progressCount === 0) console.warn(`${msgCount } messages processed`);
if (user._id === authorUuid) console.warn(`${authorName } being processed`);
const oldInboxMessages = user.inbox.messages || {};
const oldInboxMessagesIds = Object.keys(oldInboxMessages);
msgCount += oldInboxMessagesIds.length;
const newInboxMessages = oldInboxMessagesIds.map(msgId => {
const msg = oldInboxMessages[msgId];
if (!msg || (!msg.id && !msg._id)) { // eslint-disable-line no-extra-parens
console.log('missing message or message _id and id', msg);
throw new Error('error!');
}
if (msg.id && !msg._id) msg._id = msg.id;
if (msg._id && !msg.id) msg.id = msg._id;
const newMsg = new Inbox(msg);
newMsg.ownerId = user._id;
return newMsg.toJSON();
});
return dbInboxes.insert(newInboxMessages)
.then(() => {
return dbUsers.update({_id: user._id}, {
$set: {
migration: migrationName,
'inbox.messages': {},
},
});
})
.catch((err) => {
console.log(err);
return exiting(1, `ERROR! ${ err}`);
});
}
function displayData () {
console.warn(`\n${ count } users processed\n`);
console.warn(`\n${ msgCount } messages processed\n`);
return exiting(0);
}
function exiting (code, msg) {
code = code || 0; // 0 = success
if (code && !msg) {
msg = 'ERROR!';
}
if (msg) {
if (code) {
console.error(msg);
} else {
console.log(msg);
}
}
process.exit(code);
}
module.exports = processUsers;

View File

@@ -0,0 +1,99 @@
let authorName = 'Sabe'; // in case script author needs to know when their ...
let authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
/*
* Generate usernames for users who lack them
*/
import monk from 'monk';
import nconf from 'nconf';
import { generateUsername } from '../../website/server/libs/auth/utils';
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING'); // FOR TEST DATABASE
let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false });
function processUsers (lastId) {
// specify a query to limit the affected users (empty for all users):
let query = {
'auth.local.username': {$exists: false},
'auth.timestamps.loggedin': {$gt: new Date('2018-04-01')}, // Initial coverage for users active within last 6 months
};
if (lastId) {
query._id = {
$gt: lastId,
};
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 250,
fields: [
'auth',
], // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
})
.then(updateUsers)
.catch((err) => {
console.log(err);
return exiting(1, `ERROR! ${ err}`);
});
}
let progressCount = 1000;
let count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
displayData();
return;
}
let userPromises = users.map(updateUser);
let lastUser = users[users.length - 1];
return Promise.all(userPromises)
.then(() => {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
if (!user.auth.local.username) {
const newName = generateUsername();
dbUsers.update(
{_id: user._id},
{$set:
{
'auth.local.username': newName,
'auth.local.lowerCaseUsername': newName,
},
}
);
}
if (count % progressCount === 0) console.warn(`${count } ${ user._id}`);
if (user._id === authorUuid) console.warn(`${authorName } processed`);
}
function displayData () {
console.warn(`\n${ count } users processed\n`);
return exiting(0);
}
function exiting (code, msg) {
code = code || 0; // 0 = success
if (code && !msg) {
msg = 'ERROR!';
}
if (msg) {
if (code) {
console.error(msg);
} else {
console.log(msg);
}
}
process.exit(code);
}
module.exports = processUsers;

View File

@@ -1,14 +1,14 @@
import monk from 'monk';
import nconf from 'nconf';
const migrationName = 'mystery-items-201807.js'; // Update per month
const migrationName = 'mystery-items-201808.js'; // Update per month
const authorName = 'Sabe'; // in case script author needs to know when their ...
const authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
/*
* Award this month's mystery items to subscribers
*/
const MYSTERY_ITEMS = ['armor_mystery_201807', 'head_mystery_201807'];
const MYSTERY_ITEMS = ['armor_mystery_201809', 'head_mystery_201809'];
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING');
let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false });

View File

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

4441
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "4.57.1",
"version": "4.64.1",
"main": "./website/server/index.js",
"dependencies": {
"@slack/client": "^3.8.1",
@@ -9,9 +9,9 @@
"amazon-payments": "^0.2.7",
"amplitude": "^3.5.0",
"apidoc": "^0.17.5",
"apn": "^2.2.0",
"autoprefixer": "^8.5.0",
"aws-sdk": "^2.239.1",
"apn": "^2.2.0",
"axios": "^0.18.0",
"axios-progress-bar": "^1.2.0",
"babel-core": "^6.26.3",
@@ -26,7 +26,7 @@
"babel-preset-es2015": "^6.6.0",
"babel-register": "^6.6.0",
"babel-runtime": "^6.11.6",
"bcrypt": "^3.0.0",
"bcrypt": "^3.0.1",
"body-parser": "^1.18.3",
"bootstrap": "^4.1.1",
"bootstrap-vue": "^2.0.0-rc.9",
@@ -62,7 +62,7 @@
"method-override": "^2.3.5",
"moment": "^2.22.1",
"moment-recur": "^1.0.7",
"mongoose": "^5.1.2",
"mongoose": "^5.1.3",
"morgan": "^1.7.0",
"nconf": "^0.10.0",
"node-gcm": "^0.14.4",
@@ -83,6 +83,8 @@
"rimraf": "^2.4.3",
"sass-loader": "^7.0.0",
"shelljs": "^0.8.2",
"short-uuid": "^3.0.0",
"smartbanner.js": "^1.9.1",
"stripe": "^5.9.0",
"superagent": "^3.8.3",
"svg-inline-loader": "^0.8.0",

View File

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

View File

@@ -107,6 +107,25 @@ describe('Password Utilities', () => {
}
});
it('defaults to SHA1 encryption if salt is provided', async () => {
let textPassword = 'mySecretPassword';
let salt = sha1MakeSalt();
let hashedPassword = sha1EncryptPassword(textPassword, salt);
let user = {
auth: {
local: {
hashed_password: hashedPassword,
salt,
passwordHashMethod: '',
},
},
};
let isValidPassword = await compare(user, textPassword);
expect(isValidPassword).to.eql(true);
});
it('throws an error if an invalid hashing method is used', async () => {
try {
await compare({

View File

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

View File

@@ -20,7 +20,7 @@ import { TAVERN_ID } from '../../../../website/common/script/';
import shared from '../../../../website/common';
describe('Group Model', () => {
let party, questLeader, participatingMember, nonParticipatingMember, undecidedMember;
let party, questLeader, participatingMember, sleepingParticipatingMember, nonParticipatingMember, undecidedMember;
beforeEach(async () => {
sandbox.stub(email, 'sendTxn');
@@ -48,6 +48,11 @@ describe('Group Model', () => {
party: { _id: party._id },
profile: { name: 'Participating Member' },
});
sleepingParticipatingMember = new User({
party: { _id: party._id },
profile: { name: 'Sleeping Participating Member' },
preferences: { sleep: true },
});
nonParticipatingMember = new User({
party: { _id: party._id },
profile: { name: 'Non-Participating Member' },
@@ -61,6 +66,7 @@ describe('Group Model', () => {
party.save(),
questLeader.save(),
participatingMember.save(),
sleepingParticipatingMember.save(),
nonParticipatingMember.save(),
undecidedMember.save(),
]);
@@ -80,6 +86,7 @@ describe('Group Model', () => {
party.quest.members = {
[questLeader._id]: true,
[participatingMember._id]: true,
[sleepingParticipatingMember._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
};
@@ -175,6 +182,34 @@ describe('Group Model', () => {
expect(party._processBossQuest).to.not.be.called;
expect(Group.prototype._processCollectionQuest).to.be.calledOnce;
});
it('does not call _processBossQuest when user is resting in the inn', async () => {
party.quest.key = 'whale';
await party.startQuest(questLeader);
await party.save();
await Group.processQuestProgress(sleepingParticipatingMember, progress);
party = await Group.findOne({_id: party._id});
expect(party._processBossQuest).to.not.be.called;
expect(party._processCollectionQuest).to.not.be.called;
});
it('does not call _processCollectionQuest when user is resting in the inn', async () => {
party.quest.key = 'evilsanta2';
await party.startQuest(questLeader);
await party.save();
await Group.processQuestProgress(sleepingParticipatingMember, progress);
party = await Group.findOne({_id: party._id});
expect(party._processBossQuest).to.not.be.called;
expect(party._processCollectionQuest).to.not.be.called;
});
});
context('Boss Quests', () => {
@@ -216,17 +251,20 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
updatedNonParticipatingMember,
updatedUndecidedMember,
] = await Promise.all([
User.findById(questLeader._id),
User.findById(participatingMember._id),
User.findById(sleepingParticipatingMember._id),
User.findById(nonParticipatingMember._id),
User.findById(undecidedMember._id),
]);
expect(updatedLeader.stats.hp).to.eql(42.5);
expect(updatedParticipatingMember.stats.hp).to.eql(42.5);
expect(updatedSleepingParticipatingMember.stats.hp).to.eql(42.5);
expect(updatedNonParticipatingMember.stats.hp).to.eql(50);
expect(updatedUndecidedMember.stats.hp).to.eql(50);
});
@@ -236,6 +274,7 @@ describe('Group Model', () => {
party.quest.members = {
[questLeader._id]: true,
[participatingMember._id]: true,
[sleepingParticipatingMember._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
};
@@ -248,17 +287,20 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
updatedNonParticipatingMember,
updatedUndecidedMember,
] = await Promise.all([
User.findById(questLeader._id),
User.findById(participatingMember._id),
User.findById(sleepingParticipatingMember._id),
User.findById(nonParticipatingMember._id),
User.findById(undecidedMember._id),
]);
expect(updatedLeader.stats.hp).to.eql(42.5);
expect(updatedParticipatingMember.stats.hp).to.eql(42.5);
expect(updatedSleepingParticipatingMember.stats.hp).to.eql(42.5);
expect(updatedNonParticipatingMember.stats.hp).to.eql(50);
expect(updatedUndecidedMember.stats.hp).to.eql(50);
});
@@ -497,9 +539,11 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
] = await Promise.all([
User.findById(questLeader._id),
User.findById(participatingMember._id),
User.findById(sleepingParticipatingMember._id),
]);
expect(updatedLeader.achievements.quests[party.quest.key]).to.eql(1);
@@ -508,6 +552,9 @@ describe('Group Model', () => {
expect(updatedParticipatingMember.achievements.quests[party.quest.key]).to.eql(1);
expect(updatedParticipatingMember.stats.exp).to.be.greaterThan(0);
expect(updatedParticipatingMember.stats.gp).to.be.greaterThan(0);
expect(updatedSleepingParticipatingMember.achievements.quests[party.quest.key]).to.eql(1);
expect(updatedSleepingParticipatingMember.stats.exp).to.be.greaterThan(0);
expect(updatedSleepingParticipatingMember.stats.gp).to.be.greaterThan(0);
});
});
});
@@ -647,6 +694,7 @@ describe('Group Model', () => {
it('returns an array of members whose quest status set to true', () => {
party.quest.members = {
[participatingMember._id]: true,
[sleepingParticipatingMember._id]: true,
[questLeader._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
@@ -654,6 +702,7 @@ describe('Group Model', () => {
expect(party.getParticipatingQuestMembers()).to.eql([
participatingMember._id,
sleepingParticipatingMember._id,
questLeader._id,
]);
});
@@ -756,11 +805,12 @@ describe('Group Model', () => {
it('removes user from group quest', async () => {
party.quest.members = {
[participatingMember._id]: true,
[sleepingParticipatingMember._id]: true,
[questLeader._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
};
party.memberCount = 4;
party.memberCount = 5;
await party.save();
await party.leave(participatingMember);
@@ -768,6 +818,7 @@ describe('Group Model', () => {
party = await Group.findOne({_id: party._id});
expect(party.quest.members).to.eql({
[questLeader._id]: true,
[sleepingParticipatingMember._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
});
@@ -775,6 +826,7 @@ describe('Group Model', () => {
it('deletes a private party when the last member leaves', async () => {
await party.leave(participatingMember);
await party.leave(sleepingParticipatingMember);
await party.leave(questLeader);
await party.leave(nonParticipatingMember);
await party.leave(undecidedMember);
@@ -846,6 +898,7 @@ describe('Group Model', () => {
party.privacy = 'public';
await party.leave(participatingMember);
await party.leave(sleepingParticipatingMember);
await party.leave(questLeader);
await party.leave(nonParticipatingMember);
await party.leave(undecidedMember);
@@ -967,32 +1020,6 @@ describe('Group Model', () => {
expect(chat.user).to.not.exist;
});
it('cuts down chat to 200 messages', () => {
for (let i = 0; i < 220; i++) {
party.chat.push({ text: 'a message' });
}
expect(party.chat).to.have.a.lengthOf(220);
party.sendChat('message');
expect(party.chat).to.have.a.lengthOf(200);
});
it('cuts down chat to 400 messages when group is subcribed', () => {
party.purchased.plan.customerId = 'test-customer-id';
for (let i = 0; i < 420; i++) {
party.chat.push({ text: 'a message' });
}
expect(party.chat).to.have.a.lengthOf(420);
party.sendChat('message');
expect(party.chat).to.have.a.lengthOf(400);
});
it('updates users about new messages in party', () => {
party.sendChat('message');
@@ -1074,6 +1101,7 @@ describe('Group Model', () => {
party.quest.members = {
[questLeader._id]: true,
[participatingMember._id]: true,
[sleepingParticipatingMember._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
};
@@ -1130,6 +1158,7 @@ describe('Group Model', () => {
let expectedQuestMembers = {};
expectedQuestMembers[questLeader._id] = true;
expectedQuestMembers[participatingMember._id] = true;
expectedQuestMembers[sleepingParticipatingMember._id] = true;
expect(party.quest.members).to.eql(expectedQuestMembers);
});
@@ -1148,12 +1177,18 @@ describe('Group Model', () => {
questLeader = await User.findById(questLeader._id);
participatingMember = await User.findById(participatingMember._id);
sleepingParticipatingMember = await User.findById(sleepingParticipatingMember._id);
expect(participatingMember.party.quest.key).to.eql('whale');
expect(participatingMember.party.quest.progress.down).to.eql(0);
expect(participatingMember.party.quest.progress.collectedItems).to.eql(0);
expect(participatingMember.party.quest.completed).to.eql(null);
expect(sleepingParticipatingMember.party.quest.key).to.eql('whale');
expect(sleepingParticipatingMember.party.quest.progress.down).to.eql(0);
expect(sleepingParticipatingMember.party.quest.progress.collectedItems).to.eql(0);
expect(sleepingParticipatingMember.party.quest.completed).to.eql(null);
expect(questLeader.party.quest.key).to.eql('whale');
expect(questLeader.party.quest.progress.down).to.eql(0);
expect(questLeader.party.quest.progress.collectedItems).to.eql(0);
@@ -1172,9 +1207,11 @@ describe('Group Model', () => {
it('sends email to participating members that quest has started', async () => {
participatingMember.preferences.emailNotifications.questStarted = true;
sleepingParticipatingMember.preferences.emailNotifications.questStarted = true;
questLeader.preferences.emailNotifications.questStarted = true;
await Promise.all([
participatingMember.save(),
sleepingParticipatingMember.save(),
questLeader.save(),
]);
@@ -1187,8 +1224,9 @@ describe('Group Model', () => {
let memberIds = _.map(email.sendTxn.args[0][0], '_id');
let typeOfEmail = email.sendTxn.args[0][1];
expect(memberIds).to.have.a.lengthOf(2);
expect(memberIds).to.have.a.lengthOf(3);
expect(memberIds).to.include(participatingMember._id);
expect(memberIds).to.include(sleepingParticipatingMember._id);
expect(memberIds).to.include(questLeader._id);
expect(typeOfEmail).to.eql('quest-started');
});
@@ -1202,6 +1240,13 @@ describe('Group Model', () => {
questStarted: true,
},
}];
sleepingParticipatingMember.webhooks = [{
type: 'questActivity',
url: 'http://someurl.com',
options: {
questStarted: true,
},
}];
questLeader.webhooks = [{
type: 'questActivity',
url: 'http://someurl.com',
@@ -1210,13 +1255,13 @@ describe('Group Model', () => {
},
}];
await Promise.all([participatingMember.save(), questLeader.save()]);
await Promise.all([participatingMember.save(), sleepingParticipatingMember.save(), questLeader.save()]);
await party.startQuest(nonParticipatingMember);
await sleep(0.5);
expect(questActivityWebhook.send).to.be.calledTwice; // for 2 participating members
expect(questActivityWebhook.send).to.be.calledThrice; // for 3 participating members
let args = questActivityWebhook.send.args[0];
let webhooks = args[0].webhooks;
@@ -1226,6 +1271,8 @@ describe('Group Model', () => {
expect(webhooks).to.have.a.lengthOf(1);
if (webhookOwner === questLeader._id) {
expect(webhooks[0].id).to.eql(questLeader.webhooks[0].id);
} else if (webhookOwner === sleepingParticipatingMember._id) {
expect(webhooks[0].id).to.eql(sleepingParticipatingMember.webhooks[0].id);
} else {
expect(webhooks[0].id).to.eql(participatingMember.webhooks[0].id);
}
@@ -1236,9 +1283,11 @@ describe('Group Model', () => {
it('sends email only to members who have not opted out', async () => {
participatingMember.preferences.emailNotifications.questStarted = false;
sleepingParticipatingMember.preferences.emailNotifications.questStarted = false;
questLeader.preferences.emailNotifications.questStarted = true;
await Promise.all([
participatingMember.save(),
sleepingParticipatingMember.save(),
questLeader.save(),
]);
@@ -1252,14 +1301,17 @@ describe('Group Model', () => {
expect(memberIds).to.have.a.lengthOf(1);
expect(memberIds).to.not.include(participatingMember._id);
expect(memberIds).to.not.include(sleepingParticipatingMember._id);
expect(memberIds).to.include(questLeader._id);
});
it('does not send email to initiating member', async () => {
participatingMember.preferences.emailNotifications.questStarted = true;
sleepingParticipatingMember.preferences.emailNotifications.questStarted = true;
questLeader.preferences.emailNotifications.questStarted = true;
await Promise.all([
participatingMember.save(),
sleepingParticipatingMember.save(),
questLeader.save(),
]);
@@ -1271,8 +1323,9 @@ describe('Group Model', () => {
let memberIds = _.map(email.sendTxn.args[0][0], '_id');
expect(memberIds).to.have.a.lengthOf(1);
expect(memberIds).to.have.a.lengthOf(2);
expect(memberIds).to.not.include(participatingMember._id);
expect(memberIds).to.include(sleepingParticipatingMember._id);
expect(memberIds).to.include(questLeader._id);
});
@@ -1281,7 +1334,7 @@ describe('Group Model', () => {
await party.startQuest(nonParticipatingMember);
let members = [questLeader._id, participatingMember._id];
let members = [questLeader._id, participatingMember._id, sleepingParticipatingMember._id];
expect(User.update).to.be.calledWith(
{ _id: { $in: members } },
@@ -1346,6 +1399,7 @@ describe('Group Model', () => {
party.quest.members = {
[questLeader._id]: true,
[participatingMember._id]: true,
[sleepingParticipatingMember._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
};
@@ -1368,7 +1422,7 @@ describe('Group Model', () => {
await party.finishQuest(quest);
expect(User.update).to.be.calledTwice;
expect(User.update).to.be.calledThrice;
});
it('stops retrying when a successful update has occurred', async () => {
@@ -1378,7 +1432,7 @@ describe('Group Model', () => {
await party.finishQuest(quest);
expect(User.update).to.be.calledThrice;
expect(User.update.callCount).to.equal(4);
});
it('retries failed updates at most five times per user', async () => {
@@ -1386,7 +1440,7 @@ describe('Group Model', () => {
await expect(party.finishQuest(quest)).to.eventually.be.rejected;
expect(User.update.callCount).to.eql(10);
expect(User.update.callCount).to.eql(15); // for 3 users
});
});
@@ -1396,17 +1450,19 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
] = await Promise.all([
User.findById(questLeader._id),
User.findById(participatingMember._id),
User.findById(sleepingParticipatingMember._id),
]);
expect(updatedLeader.achievements.quests[quest.key]).to.eql(1);
expect(updatedParticipatingMember.achievements.quests[quest.key]).to.eql(1);
expect(updatedSleepingParticipatingMember.achievements.quests[quest.key]).to.eql(1);
});
// Disable test, it fails on TravisCI, but only there
xit('gives out super awesome Masterclasser achievement to the deserving', async () => {
it('gives out super awesome Masterclasser achievement to the deserving', async () => {
quest = questScrolls.lostMasterclasser4;
party.quest.key = quest.key;
@@ -1433,17 +1489,19 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
] = await Promise.all([
User.findById(questLeader._id).exec(),
User.findById(participatingMember._id).exec(),
User.findById(sleepingParticipatingMember._id).exec(),
]);
expect(updatedLeader.achievements.lostMasterclasser).to.eql(true);
expect(updatedParticipatingMember.achievements.lostMasterclasser).to.not.eql(true);
expect(updatedSleepingParticipatingMember.achievements.lostMasterclasser).to.not.eql(true);
});
// Disable test, it fails on TravisCI, but only there
xit('gives out super awesome Masterclasser achievement when quests done out of order', async () => {
it('gives out super awesome Masterclasser achievement when quests done out of order', async () => {
quest = questScrolls.lostMasterclasser1;
party.quest.key = quest.key;
@@ -1470,13 +1528,16 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
] = await Promise.all([
User.findById(questLeader._id).exec(),
User.findById(participatingMember._id).exec(),
User.findById(sleepingParticipatingMember._id).exec(),
]);
expect(updatedLeader.achievements.lostMasterclasser).to.eql(true);
expect(updatedParticipatingMember.achievements.lostMasterclasser).to.not.eql(true);
expect(updatedSleepingParticipatingMember.achievements.lostMasterclasser).to.not.eql(true);
});
it('gives xp and gold', async () => {
@@ -1485,15 +1546,19 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
] = await Promise.all([
User.findById(questLeader._id),
User.findById(participatingMember._id),
User.findById(sleepingParticipatingMember._id),
]);
expect(updatedLeader.stats.exp).to.eql(quest.drop.exp);
expect(updatedLeader.stats.gp).to.eql(quest.drop.gp);
expect(updatedParticipatingMember.stats.exp).to.eql(quest.drop.exp);
expect(updatedParticipatingMember.stats.gp).to.eql(quest.drop.gp);
expect(updatedSleepingParticipatingMember.stats.exp).to.eql(quest.drop.exp);
expect(updatedSleepingParticipatingMember.stats.gp).to.eql(quest.drop.gp);
});
context('drops', () => {
@@ -1593,13 +1658,16 @@ describe('Group Model', () => {
sandbox.spy(User, 'update');
await party.finishQuest(quest);
expect(User.update).to.be.calledTwice;
expect(User.update).to.be.calledThrice;
expect(User.update).to.be.calledWithMatch({
_id: questLeader._id,
});
expect(User.update).to.be.calledWithMatch({
_id: participatingMember._id,
});
expect(User.update).to.be.calledWithMatch({
_id: sleepingParticipatingMember._id,
});
});
it('sets user quest object to a clean state', async () => {
@@ -1632,7 +1700,7 @@ describe('Group Model', () => {
},
}];
await Promise.all([participatingMember.save(), questLeader.save()]);
await Promise.all([participatingMember.save(), sleepingParticipatingMember.save(), questLeader.save()]);
await party.finishQuest(quest);

View File

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

View File

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

View File

@@ -94,16 +94,6 @@ describe('POST /challenges', () => {
});
});
it('returns an error when non-leader member creates a challenge in leaderOnly group', async () => {
await expect(groupMember.post('/challenges', {
group: group._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlyGroupLeaderChal'),
});
});
it('allows non-leader member to create a challenge', async () => {
let populatedGroup = await createAndPopulateGroup({
members: 1,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ import {
} from '../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
import { quests as questScrolls } from '../../../../../website/common/script/content';
import { model as Chat } from '../../../../../website/server/models/chat';
import { chatModel as Chat } from '../../../../../website/server/models/message';
import apiError from '../../../../../website/server/libs/apiError';
describe('POST /groups/:groupId/quests/invite/:questKey', () => {

View File

@@ -5,7 +5,7 @@ import {
sleep,
} from '../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
import { model as Chat } from '../../../../../website/server/models/chat';
import { chatModel as Chat } from '../../../../../website/server/models/message';
describe('POST /groups/:groupId/quests/reject', () => {
let questingGroup;

View File

@@ -1,7 +1,7 @@
import {
createAndPopulateGroup,
createAndPopulateGroup, translate as t,
} from '../../../../../helpers/api-integration/v3';
import { find } from 'lodash';
import {find} from 'lodash';
describe('PUT /tasks/:id', () => {
let user, guild, member, member2, task;
@@ -38,16 +38,64 @@ describe('PUT /tasks/:id', () => {
it('updates a group task', async () => {
let savedHabit = await user.put(`/tasks/${task._id}`, {
text: 'some new text',
up: false,
down: false,
notes: 'some new notes',
});
expect(savedHabit.text).to.eql('some new text');
expect(savedHabit.notes).to.eql('some new notes');
expect(savedHabit.up).to.eql(false);
expect(savedHabit.down).to.eql(false);
});
it('updates a group task - approval is required', async () => {
// allow to manage
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member._id,
});
// change the todo
task = await member.put(`/tasks/${task._id}`, {
text: 'new text!',
requiresApproval: true,
});
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, (memberTask) => memberTask.group.taskId === task._id);
// score up to trigger approval
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
});
it('updates a group task with checklist', async () => {
// add a new todo
task = await user.post(`/tasks/group/${guild._id}`, {
text: 'todo',
type: 'todo',
checklist: [
{
text: 'checklist 1',
},
],
});
await user.post(`/tasks/${task._id}/assign/${member._id}`);
// change the checklist text
task = await user.put(`/tasks/${task._id}`, {
checklist: [
{
id: task.checklist[0].id,
text: 'checklist 1 - edit',
},
{
text: 'checklist 2 - edit',
},
],
});
expect(task.checklist.length).to.eql(2);
});
it('updates the linked tasks', async () => {

View File

@@ -3,25 +3,41 @@ import {
} from '../../../../helpers/api-integration/v3';
describe('DELETE user message', () => {
let user;
let user, messagesId, otherUser;
beforeEach(async () => {
user = await generateUser({ inbox: { messages: { first: 'message', second: 'message' } } });
expect(user.inbox.messages.first).to.eql('message');
expect(user.inbox.messages.second).to.eql('message');
before(async () => {
[user, otherUser] = await Promise.all([generateUser(), generateUser()]);
await user.post('/members/send-private-message', {
toUserId: otherUser.id,
message: 'first',
});
await user.post('/members/send-private-message', {
toUserId: otherUser.id,
message: 'second',
});
let userRes = await user.get('/user');
messagesId = Object.keys(userRes.inbox.messages);
expect(messagesId.length).to.eql(2);
expect(userRes.inbox.messages[messagesId[0]].text).to.eql('first');
expect(userRes.inbox.messages[messagesId[1]].text).to.eql('second');
});
it('one message', async () => {
let result = await user.del('/user/messages/first');
await user.sync();
expect(result).to.eql({ second: 'message' });
expect(user.inbox.messages).to.eql({ second: 'message' });
let result = await user.del(`/user/messages/${messagesId[0]}`);
messagesId = Object.keys(result);
expect(messagesId.length).to.eql(1);
let userRes = await user.get('/user');
expect(Object.keys(userRes.inbox.messages).length).to.eql(1);
expect(userRes.inbox.messages[messagesId[0]].text).to.eql('second');
});
it('clear all', async () => {
let result = await user.del('/user/messages');
await user.sync();
expect(user.inbox.messages).to.eql({});
let userRes = await user.get('/user');
expect(userRes.inbox.messages).to.eql({});
expect(result).to.eql({});
});
});

View File

@@ -58,6 +58,21 @@ describe('POST /user/class/cast/:spellId', () => {
});
});
it('returns an error if use Healing Light spell with full health', async () => {
await user.update({
'stats.class': 'healer',
'stats.lvl': 11,
'stats.hp': 50,
'stats.mp': 200,
});
await expect(user.post('/user/class/cast/heal'))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageHealthAlreadyMax'),
});
});
it('returns an error if spell.lvl > user.level', async () => {
await user.update({'stats.mp': 200, 'stats.class': 'wizard'});
await expect(user.post('/user/class/cast/earth'))

View File

@@ -50,11 +50,24 @@ describe('POST /user/push-devices', () => {
});
it('adds a push device to the user', async () => {
let response = await user.post('/user/push-devices', {type, regId});
const response = await user.post('/user/push-devices', {type, regId});
await user.sync();
expect(response.message).to.equal(t('pushDeviceAdded'));
expect(response.data[0].type).to.equal(type);
expect(response.data[0].regId).to.equal(regId);
expect(user.pushDevices[0].type).to.equal(type);
expect(user.pushDevices[0].regId).to.equal(regId);
});
it('removes a push device to the user', async () => {
await user.post('/user/push-devices', {type, regId});
const response = await user.del(`/user/push-devices/${regId}`);
await user.sync();
expect(response.message).to.equal(t('pushDeviceRemoved'));
expect(response.data[0]).to.not.exist;
expect(user.pushDevices[0]).to.not.exist;
});
});

View File

@@ -54,7 +54,7 @@ describe('PUT /user', () => {
});
it('profile.name cannot be an empty string or null', async () => {
it('validates profile.name', async () => {
await expect(user.put('/user', {
'profile.name': ' ', // string should be trimmed
})).to.eventually.be.rejected.and.eql({
@@ -76,7 +76,23 @@ describe('PUT /user', () => {
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'User validation failed',
message: t('invalidReqParams'),
});
await expect(user.put('/user', {
'profile.name': 'this is a very long display name that will not be allowed due to length',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('displaynameIssueLength'),
});
await expect(user.put('/user', {
'profile.name': 'TESTPLACEHOLDERSLURWORDHERE',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('displaynameIssueSlur'),
});
});
});

View File

@@ -41,6 +41,23 @@ describe('POST /user/auth/local/register', () => {
expect(user.newUser).to.eql(true);
});
it('registers a new user and sets verifiedUsername to true', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user._id).to.exist;
expect(user.apiToken).to.exist;
expect(user.flags.verifiedUsername).to.eql(true);
});
xit('remove spaces from username', async () => {
// TODO can probably delete this test now
let username = ' usernamewithspaces ';

View File

@@ -39,7 +39,7 @@ describe('POST /user/auth/social', () => {
});
it('registers a new user', async () => {
let response = await api.post(endpoint, {
const response = await api.post(endpoint, {
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
network,
});
@@ -47,7 +47,10 @@ describe('POST /user/auth/social', () => {
expect(response.apiToken).to.exist;
expect(response.id).to.exist;
expect(response.newUser).to.be.true;
expect(response.username).to.exist;
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a facebook user');
await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.exist;
});
it('logs an existing user in', async () => {

View File

@@ -8,7 +8,11 @@ describe('POST /user/allocate', () => {
let user;
beforeEach(async () => {
user = await generateUser();
user = await generateUser({
'stats.lvl': 10,
'flags.classSelected': true,
'preferences.disableClasses': false,
});
});
// More tests in common code unit tests
@@ -31,6 +35,16 @@ describe('POST /user/allocate', () => {
});
});
it('returns an error if the user hasn\'t selected class', async () => {
await user.update({'flags.classSelected': false});
await expect(user.post('/user/allocate'))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('classNotSelected'),
});
});
it('allocates attribute points', async () => {
await user.update({'stats.points': 1});
let res = await user.post('/user/allocate?stat=con');

View File

@@ -13,7 +13,11 @@ describe('POST /user/allocate-bulk', () => {
};
beforeEach(async () => {
user = await generateUser();
user = await generateUser({
'stats.lvl': 10,
'flags.classSelected': true,
'preferences.disableClasses': false,
});
});
// More tests in common code unit tests
@@ -27,6 +31,16 @@ describe('POST /user/allocate-bulk', () => {
});
});
it('returns an error if user has not selected class', async () => {
await user.update({'flags.classSelected': false});
await expect(user.post('/user/allocate-bulk', statsUpdate))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('classNotSelected'),
});
});
it('allocates attribute points', async () => {
await user.update({'stats.points': 3});

View File

@@ -0,0 +1,62 @@
import {
generateUser,
translate as t,
resetHabiticaDB,
} from '../../../helpers/api-integration/v4';
describe('POST /coupons/enter/:code', () => {
let user;
let sudoUser;
before(async () => {
await resetHabiticaDB();
});
beforeEach(async () => {
user = await generateUser();
sudoUser = await generateUser({
'contributor.sudo': true,
});
});
it('returns an error if code is missing', async () => {
await expect(user.post('/coupons/enter')).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: 'Not found.',
});
});
it('returns an error if code is invalid', async () => {
await expect(user.post('/coupons/enter/notValid')).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidCoupon'),
});
});
it('returns an error if coupon has been used', async () => {
let [coupon] = await sudoUser.post('/coupons/generate/wondercon?count=1');
await user.post(`/coupons/enter/${coupon._id}`); // use coupon
await expect(user.post(`/coupons/enter/${coupon._id}`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('couponUsed'),
});
});
it('should apply the coupon to the user', async () => {
let [coupon] = await sudoUser.post('/coupons/generate/wondercon?count=1');
let userRes = await user.post(`/coupons/enter/${coupon._id}`);
expect(userRes._id).to.equal(user._id);
expect(userRes.items.gear.owned.eyewear_special_wondercon_red).to.be.true;
expect(userRes.items.gear.owned.eyewear_special_wondercon_black).to.be.true;
expect(userRes.items.gear.owned.back_special_wondercon_black).to.be.true;
expect(userRes.items.gear.owned.back_special_wondercon_red).to.be.true;
expect(userRes.items.gear.owned.body_special_wondercon_red).to.be.true;
expect(userRes.items.gear.owned.body_special_wondercon_black).to.be.true;
expect(userRes.items.gear.owned.body_special_wondercon_gold).to.be.true;
expect(userRes.extra).to.eql({signupEvent: 'wondercon'});
});
});

View File

@@ -0,0 +1,30 @@
import {
generateUser,
} from '../../../helpers/api-integration/v4';
describe('DELETE /inbox/clear', () => {
it('removes all inbox messages for the user', async () => {
const [user, otherUser] = await Promise.all([generateUser(), generateUser()]);
await otherUser.post('/members/send-private-message', {
toUserId: user.id,
message: 'first',
});
await user.post('/members/send-private-message', {
toUserId: otherUser.id,
message: 'second',
});
await otherUser.post('/members/send-private-message', {
toUserId: user.id,
message: 'third',
});
let messages = await user.get('/inbox/messages');
expect(messages.length).to.equal(3);
await user.del('/inbox/clear/');
messages = await user.get('/inbox/messages');
expect(messages.length).to.equal(0);
});
});

View File

@@ -0,0 +1,62 @@
import {
generateUser,
translate as t,
} from '../../../helpers/api-integration/v4';
import { v4 as generateUUID } from 'uuid';
describe('DELETE /inbox/messages/:messageId', () => {
let user;
let otherUser;
before(async () => {
[user, otherUser] = await Promise.all([generateUser(), generateUser()]);
await otherUser.post('/members/send-private-message', {
toUserId: user.id,
message: 'first',
});
await user.post('/members/send-private-message', {
toUserId: otherUser.id,
message: 'second',
});
await otherUser.post('/members/send-private-message', {
toUserId: user.id,
message: 'third',
});
});
it('returns an error if the messageId parameter is not an UUID', async () => {
await expect(user.del('/inbox/messages/123'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Invalid request parameters.',
});
});
it('returns an error if the message does not exist', async () => {
await expect(user.del(`/inbox/messages/${generateUUID()}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('messageGroupChatNotFound'),
});
});
it('deletes one message', async () => {
const messages = await user.get('/inbox/messages');
expect(messages.length).to.equal(3);
expect(messages[0].text).to.equal('third');
expect(messages[1].text).to.equal('second');
expect(messages[2].text).to.equal('first');
await user.del(`/inbox/messages/${messages[1]._id}`);
const updatedMessages = await user.get('/inbox/messages');
expect(updatedMessages.length).to.equal(2);
expect(updatedMessages[0].text).to.equal('third');
expect(updatedMessages[1].text).to.equal('first');
});
});

View File

@@ -0,0 +1,58 @@
import {
generateUser,
} from '../../../helpers/api-integration/v4';
import common from '../../../../website/common';
describe('GET /user', () => {
let user;
before(async () => {
user = await generateUser();
});
it('returns the authenticated user with computed stats', async () => {
let returnedUser = await user.get('/user');
expect(returnedUser._id).to.equal(user._id);
expect(returnedUser.stats.maxMP).to.exist;
expect(returnedUser.stats.maxHealth).to.equal(common.maxHealth);
expect(returnedUser.stats.toNextLevel).to.equal(common.tnl(returnedUser.stats.lvl));
});
it('does not return private paths (and apiToken)', async () => {
let returnedUser = await user.get('/user');
expect(returnedUser.auth.local.hashed_password).to.not.exist;
expect(returnedUser.auth.local.passwordHashMethod).to.not.exist;
expect(returnedUser.auth.local.salt).to.not.exist;
expect(returnedUser.apiToken).to.not.exist;
});
it('returns only user properties requested', async () => {
let returnedUser = await user.get('/user?userFields=achievements,items.mounts');
expect(returnedUser._id).to.equal(user._id);
expect(returnedUser.achievements).to.exist;
expect(returnedUser.items.mounts).to.exist;
// Notifications are always returned
expect(returnedUser.notifications).to.exist;
expect(returnedUser.stats).to.not.exist;
});
it('does not return new inbox messages', async () => {
const otherUser = await generateUser();
await otherUser.post('/members/send-private-message', {
toUserId: user.id,
message: 'first',
});
await otherUser.post('/members/send-private-message', {
toUserId: user.id,
message: 'second',
});
let returnedUser = await user.get('/user');
expect(returnedUser._id).to.equal(user._id);
expect(returnedUser.inbox.messages).to.be.empty;
});
});

View File

@@ -0,0 +1,324 @@
import {
generateUser,
translate as t,
createAndPopulateGroup,
generateGroup,
generateChallenge,
sleep,
} from '../../../helpers/api-integration/v4';
import { v4 as generateUUID } from 'uuid';
import { find } from 'lodash';
import apiError from '../../../../website/server/libs/apiError';
describe('POST /user/class/cast/:spellId', () => {
let user;
beforeEach(async () => {
user = await generateUser();
});
it('returns an error if spell does not exist', async () => {
await user.update({'stats.class': 'rogue'});
let spellId = 'invalidSpell';
await expect(user.post(`/user/class/cast/${spellId}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: apiError('spellNotFound', {spellId}),
});
});
it('returns an error if spell does not exist in user\'s class', async () => {
let spellId = 'pickPocket';
await expect(user.post(`/user/class/cast/${spellId}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: apiError('spellNotFound', {spellId}),
});
});
it('returns an error if spell.mana > user.mana', async () => {
await user.update({'stats.class': 'rogue'});
await expect(user.post('/user/class/cast/backStab'))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('notEnoughMana'),
});
});
it('returns an error if spell.value > user.gold', async () => {
await expect(user.post('/user/class/cast/birthday'))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageNotEnoughGold'),
});
});
it('returns an error if spell.lvl > user.level', async () => {
await user.update({'stats.mp': 200, 'stats.class': 'wizard'});
await expect(user.post('/user/class/cast/earth'))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('spellLevelTooHigh', {level: 13}),
});
});
it('returns an error if user doesn\'t own the spell', async () => {
await expect(user.post('/user/class/cast/snowball'))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('spellNotOwned'),
});
});
it('returns an error if targetId is not an UUID', async () => {
await expect(user.post('/user/class/cast/spellId?targetId=notAnUUID'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('returns an error if targetId is required but missing', async () => {
await user.update({'stats.class': 'rogue', 'stats.lvl': 11});
await expect(user.post('/user/class/cast/pickPocket'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('targetIdUUID'),
});
});
it('returns an error if targeted task doesn\'t exist', async () => {
await user.update({'stats.class': 'rogue', 'stats.lvl': 11});
await expect(user.post(`/user/class/cast/pickPocket?targetId=${generateUUID()}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('taskNotFound'),
});
});
it('returns an error if a challenge task was targeted', async () => {
let {group, groupLeader} = await createAndPopulateGroup();
let challenge = await generateChallenge(groupLeader, group);
await groupLeader.post(`/challenges/${challenge._id}/join`);
await groupLeader.post(`/tasks/challenge/${challenge._id}`, [
{type: 'habit', text: 'task text'},
]);
await groupLeader.update({'stats.class': 'rogue', 'stats.lvl': 11});
await sleep(0.5);
await groupLeader.sync();
await expect(groupLeader.post(`/user/class/cast/pickPocket?targetId=${groupLeader.tasksOrder.habits[0]}`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('challengeTasksNoCast'),
});
});
it('returns an error if a group task was targeted', async () => {
let {group, groupLeader} = await createAndPopulateGroup();
let groupTask = await groupLeader.post(`/tasks/group/${group._id}`, {
text: 'todo group',
type: 'todo',
});
await groupLeader.post(`/tasks/${groupTask._id}/assign/${groupLeader._id}`);
let memberTasks = await groupLeader.get('/tasks/user');
let syncedGroupTask = find(memberTasks, function findAssignedTask (memberTask) {
return memberTask.group.id === group._id;
});
await groupLeader.update({'stats.class': 'rogue', 'stats.lvl': 11});
await sleep(0.5);
await groupLeader.sync();
await expect(groupLeader.post(`/user/class/cast/pickPocket?targetId=${syncedGroupTask._id}`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('groupTasksNoCast'),
});
});
it('returns an error if targeted party member doesn\'t exist', async () => {
let {groupLeader} = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
members: 1,
});
await groupLeader.update({'items.special.snowball': 3});
let target = generateUUID();
await expect(groupLeader.post(`/user/class/cast/snowball?targetId=${target}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('userWithIDNotFound', {userId: target}),
});
});
it('returns an error if party does not exists', async () => {
await user.update({'items.special.snowball': 3});
await expect(user.post(`/user/class/cast/snowball?targetId=${generateUUID()}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('partyNotFound'),
});
});
it('send message in party chat if party && !spell.silent', async () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
members: 1,
});
await groupLeader.update({'stats.mp': 200, 'stats.class': 'wizard', 'stats.lvl': 13});
await groupLeader.post('/user/class/cast/earth');
await sleep(1);
const groupMessages = await groupLeader.get(`/groups/${group._id}/chat`);
expect(groupMessages[0]).to.exist;
expect(groupMessages[0].uuid).to.equal('system');
});
it('Ethereal Surge does not recover mp of other mages', async () => {
let group = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
members: 4,
});
let promises = [];
promises.push(group.groupLeader.update({'stats.mp': 200, 'stats.class': 'wizard', 'stats.lvl': 20}));
promises.push(group.members[0].update({'stats.mp': 0, 'stats.class': 'warrior', 'stats.lvl': 20}));
promises.push(group.members[1].update({'stats.mp': 0, 'stats.class': 'wizard', 'stats.lvl': 20}));
promises.push(group.members[2].update({'stats.mp': 0, 'stats.class': 'rogue', 'stats.lvl': 20}));
promises.push(group.members[3].update({'stats.mp': 0, 'stats.class': 'healer', 'stats.lvl': 20}));
await Promise.all(promises);
await group.groupLeader.post('/user/class/cast/mpheal');
promises = [];
promises.push(group.members[0].sync());
promises.push(group.members[1].sync());
promises.push(group.members[2].sync());
promises.push(group.members[3].sync());
await Promise.all(promises);
expect(group.members[0].stats.mp).to.be.greaterThan(0); // warrior
expect(group.members[1].stats.mp).to.equal(0); // wizard
expect(group.members[2].stats.mp).to.be.greaterThan(0); // rogue
expect(group.members[3].stats.mp).to.be.greaterThan(0); // healer
});
it('cast bulk', async () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
members: 1,
});
await groupLeader.update({'stats.mp': 200, 'stats.class': 'wizard', 'stats.lvl': 13});
await groupLeader.post('/user/class/cast/earth', {quantity: 2});
await sleep(1);
group = await groupLeader.get(`/groups/${group._id}`);
expect(group.chat[0]).to.exist;
expect(group.chat[0].uuid).to.equal('system');
});
it('searing brightness does not affect challenge or group tasks', async () => {
let guild = await generateGroup(user);
let challenge = await generateChallenge(user, guild);
await user.post(`/challenges/${challenge._id}/join`);
await user.post(`/tasks/challenge/${challenge._id}`, {
text: 'test challenge habit',
type: 'habit',
});
let groupTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'todo group',
type: 'todo',
});
await user.update({'stats.class': 'healer', 'stats.mp': 200, 'stats.lvl': 15});
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
await user.post('/user/class/cast/brightness');
await user.sync();
let memberTasks = await user.get('/tasks/user');
let syncedGroupTask = find(memberTasks, function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
});
let userChallengeTask = find(memberTasks, function findAssignedTask (memberTask) {
return memberTask.challenge.id === challenge._id;
});
expect(userChallengeTask).to.exist;
expect(syncedGroupTask).to.exist;
expect(userChallengeTask.value).to.equal(0);
expect(syncedGroupTask.value).to.equal(0);
});
it('increases both user\'s achievement values', async () => {
let party = await createAndPopulateGroup({
members: 1,
});
let leader = party.groupLeader;
let recipient = party.members[0];
await leader.update({'stats.gp': 10});
await leader.post(`/user/class/cast/birthday?targetId=${recipient._id}`);
await leader.sync();
await recipient.sync();
expect(leader.achievements.birthday).to.equal(1);
expect(recipient.achievements.birthday).to.equal(1);
});
it('only increases user\'s achievement one if target == caster', async () => {
await user.update({'stats.gp': 10});
await user.post(`/user/class/cast/birthday?targetId=${user._id}`);
await user.sync();
expect(user.achievements.birthday).to.equal(1);
});
it('passes correct target to spell when targetType === \'task\'', async () => {
await user.update({'stats.class': 'wizard', 'stats.lvl': 11});
let task = await user.post('/tasks/user', {
text: 'test habit',
type: 'habit',
});
let result = await user.post(`/user/class/cast/fireball?targetId=${task._id}`);
expect(result.task._id).to.equal(task._id);
});
it('passes correct target to spell when targetType === \'self\'', async () => {
await user.update({'stats.class': 'wizard', 'stats.lvl': 14, 'stats.mp': 50});
let result = await user.post('/user/class/cast/frost');
expect(result.user.stats.mp).to.equal(10);
});
// TODO find a way to have sinon working in integration tests
// it doesn't work when tests are running separately from server
it('passes correct target to spell when targetType === \'tasks\'');
it('passes correct target to spell when targetType === \'party\'');
it('passes correct target to spell when targetType === \'user\'');
it('passes correct target to spell when targetType === \'party\' and user is not in a party');
it('passes correct target to spell when targetType === \'user\' and user is not in a party');
});

View File

@@ -0,0 +1,60 @@
import {
generateUser,
generateDaily,
generateReward,
translate as t,
} from '../../../helpers/api-integration/v4';
describe('POST /user/rebirth', () => {
let user;
beforeEach(async () => {
user = await generateUser();
});
it('returns an error when user balance is too low', async () => {
await expect(user.post('/user/rebirth'))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('notEnoughGems'),
});
});
// More tests in common code unit tests
it('resets user\'s tasks', async () => {
await user.update({
balance: 1.5,
});
let daily = await generateDaily({
text: 'test habit',
type: 'daily',
value: 1,
streak: 1,
userId: user._id,
});
let reward = await generateReward({
text: 'test reward',
type: 'reward',
value: 1,
userId: user._id,
});
let response = await user.post('/user/rebirth');
await user.sync();
expect(user.notifications.length).to.equal(1);
expect(user.notifications[0].type).to.equal('REBIRTH_ACHIEVEMENT');
let updatedDaily = await user.get(`/tasks/${daily._id}`);
let updatedReward = await user.get(`/tasks/${reward._id}`);
expect(response.message).to.equal(t('rebirthComplete'));
expect(updatedDaily.streak).to.equal(0);
expect(updatedDaily.value).to.equal(0);
expect(updatedReward.value).to.equal(1);
});
});

View File

@@ -0,0 +1,54 @@
import {
generateUser,
generateDaily,
generateReward,
translate as t,
} from '../../../helpers/api-integration/v4';
describe('POST /user/reroll', () => {
let user;
beforeEach(async () => {
user = await generateUser();
});
it('returns an error when user balance is too low', async () => {
await expect(user.post('/user/reroll'))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('notEnoughGems'),
});
});
// More tests in common code unit tests
it('resets user\'s tasks', async () => {
await user.update({
balance: 2,
});
let daily = await generateDaily({
text: 'test habit',
type: 'daily',
userId: user._id,
});
let reward = await generateReward({
text: 'test reward',
type: 'reward',
value: 1,
userId: user._id,
});
let response = await user.post('/user/reroll');
await user.sync();
let updatedDaily = await user.get(`/tasks/${daily._id}`);
let updatedReward = await user.get(`/tasks/${reward._id}`);
expect(response.message).to.equal(t('fortifyComplete'));
expect(updatedDaily.value).to.equal(0);
expect(updatedReward.value).to.equal(1);
});
});

View File

@@ -0,0 +1,121 @@
import {
generateUser,
generateGroup,
generateChallenge,
translate as t,
} from '../../../helpers/api-integration/v4';
import { find } from 'lodash';
describe('POST /user/reset', () => {
let user;
beforeEach(async () => {
user = await generateUser();
});
// More tests in common code unit tests
it('resets user\'s habits', async () => {
let task = await user.post('/tasks/user', {
text: 'test habit',
type: 'habit',
});
await user.post('/user/reset');
await user.sync();
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('taskNotFound'),
});
expect(user.tasksOrder.habits).to.be.empty;
});
it('resets user\'s dailys', async () => {
let task = await user.post('/tasks/user', {
text: 'test daily',
type: 'daily',
});
await user.post('/user/reset');
await user.sync();
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('taskNotFound'),
});
expect(user.tasksOrder.dailys).to.be.empty;
});
it('resets user\'s todos', async () => {
let task = await user.post('/tasks/user', {
text: 'test todo',
type: 'todo',
});
await user.post('/user/reset');
await user.sync();
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('taskNotFound'),
});
expect(user.tasksOrder.todos).to.be.empty;
});
it('resets user\'s rewards', async () => {
let task = await user.post('/tasks/user', {
text: 'test reward',
type: 'reward',
});
await user.post('/user/reset');
await user.sync();
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('taskNotFound'),
});
expect(user.tasksOrder.rewards).to.be.empty;
});
it('does not delete challenge or group tasks', async () => {
let guild = await generateGroup(user);
let challenge = await generateChallenge(user, guild);
await user.post(`/challenges/${challenge._id}/join`);
await user.post(`/tasks/challenge/${challenge._id}`, {
text: 'test challenge habit',
type: 'habit',
});
let groupTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'todo group',
type: 'todo',
});
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
await user.post('/user/reset');
await user.sync();
let memberTasks = await user.get('/tasks/user');
let syncedGroupTask = find(memberTasks, function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
});
let userChallengeTask = find(memberTasks, function findAssignedTask (memberTask) {
return memberTask.challenge.id === challenge._id;
});
expect(userChallengeTask).to.exist;
expect(syncedGroupTask).to.exist;
});
});

View File

@@ -0,0 +1,256 @@
import {
generateUser,
translate as t,
} from '../../../helpers/api-integration/v4';
import { each, get } from 'lodash';
describe('PUT /user', () => {
let user;
beforeEach(async () => {
user = await generateUser();
});
context('Allowed Operations', () => {
it('updates the user', async () => {
await user.put('/user', {
'profile.name': 'Frodo',
'preferences.costume': true,
'stats.hp': 14,
});
await user.sync();
expect(user.profile.name).to.eql('Frodo');
expect(user.preferences.costume).to.eql(true);
expect(user.stats.hp).to.eql(14);
});
it('tags must be an array', async () => {
await expect(user.put('/user', {
tags: {
tag: true,
},
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'mustBeArray',
});
});
it('update tags', async () => {
let userTags = user.tags;
await user.put('/user', {
tags: [...user.tags, {
name: 'new tag',
}],
});
await user.sync();
expect(user.tags.length).to.be.eql(userTags.length + 1);
});
it('profile.name cannot be an empty string or null', async () => {
await expect(user.put('/user', {
'profile.name': ' ', // string should be trimmed
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'User validation failed',
});
await expect(user.put('/user', {
'profile.name': '',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'User validation failed',
});
await expect(user.put('/user', {
'profile.name': null,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
});
context('Top Level Protected Operations', () => {
let protectedOperations = {
'gem balance': {balance: 100},
auth: {'auth.blocked': true, 'auth.timestamps.created': new Date()},
contributor: {'contributor.level': 9, 'contributor.admin': true, 'contributor.text': 'some text'},
backer: {'backer.tier': 10, 'backer.npc': 'Bilbo'},
subscriptions: {'purchased.plan.extraMonths': 500, 'purchased.plan.consecutive.trinkets': 1000},
'customization gem purchases': {'purchased.background.tavern': true, 'purchased.skin.bear': true},
notifications: [{type: 123}],
webhooks: {webhooks: [{url: 'https://foobar.com'}]},
};
each(protectedOperations, (data, testName) => {
it(`does not allow updating ${testName}`, async () => {
let errorText = t('messageUserOperationProtected', { operation: Object.keys(data)[0] });
await expect(user.put('/user', data)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: errorText,
});
});
});
});
context('Sub-Level Protected Operations', () => {
let protectedOperations = {
'class stat': {'stats.class': 'wizard'},
'flags unless whitelisted': {'flags.dropsEnabled': true},
webhooks: {'preferences.webhooks': [1, 2, 3]},
sleep: {'preferences.sleep': true},
'disable classes': {'preferences.disableClasses': true},
};
each(protectedOperations, (data, testName) => {
it(`does not allow updating ${testName}`, async () => {
let errorText = t('messageUserOperationProtected', { operation: Object.keys(data)[0] });
await expect(user.put('/user', data)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: errorText,
});
});
});
});
context('Default Appearance Preferences', () => {
let testCases = {
shirt: 'yellow',
skin: 'ddc994',
'hair.color': 'blond',
'hair.bangs': 2,
'hair.base': 1,
'hair.flower': 4,
size: 'broad',
};
each(testCases, (item, type) => {
const update = {};
update[`preferences.${type}`] = item;
it(`updates user with ${type} that is a default`, async () => {
let dbUpdate = {};
dbUpdate[`purchased.${type}.${item}`] = true;
await user.update(dbUpdate);
// Sanity checks to make sure user is not already equipped with item
expect(get(user.preferences, type)).to.not.eql(item);
let updatedUser = await user.put('/user', update);
expect(get(updatedUser.preferences, type)).to.eql(item);
});
});
it('returns an error if user tries to update body size with invalid type', async () => {
await expect(user.put('/user', {
'preferences.size': 'round',
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('mustPurchaseToSet', { val: 'round', key: 'preferences.size' }),
});
});
it('can set beard to default', async () => {
await user.update({
'purchased.hair.beard': 3,
'preferences.hair.beard': 3,
});
let updatedUser = await user.put('/user', {
'preferences.hair.beard': 0,
});
expect(updatedUser.preferences.hair.beard).to.eql(0);
});
it('can set mustache to default', async () => {
await user.update({
'purchased.hair.mustache': 2,
'preferences.hair.mustache': 2,
});
let updatedUser = await user.put('/user', {
'preferences.hair.mustache': 0,
});
expect(updatedUser.preferences.hair.mustache).to.eql(0);
});
});
context('Purchasable Appearance Preferences', () => {
let testCases = {
background: 'volcano',
shirt: 'convict',
skin: 'cactus',
'hair.base': 7,
'hair.beard': 2,
'hair.color': 'rainbow',
'hair.mustache': 2,
};
each(testCases, (item, type) => {
const update = {};
update[`preferences.${type}`] = item;
it(`returns an error if user tries to update ${type} with ${type} the user does not own`, async () => {
await expect(user.put('/user', update)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('mustPurchaseToSet', {val: item, key: `preferences.${type}`}),
});
});
it(`updates user with ${type} user does own`, async () => {
let dbUpdate = {};
dbUpdate[`purchased.${type}.${item}`] = true;
await user.update(dbUpdate);
// Sanity check to make sure user is not already equipped with item
expect(get(user.preferences, type)).to.not.eql(item);
let updatedUser = await user.put('/user', update);
expect(get(updatedUser.preferences, type)).to.eql(item);
});
});
});
context('Improvement Categories', () => {
it('sets valid categories', async () => {
await user.put('/user', {
'preferences.improvementCategories': ['work', 'school'],
});
await user.sync();
expect(user.preferences.improvementCategories).to.eql(['work', 'school']);
});
it('discards invalid categories', async () => {
await expect(user.put('/user', {
'preferences.improvementCategories': ['work', 'procrastination', 'school'],
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'User validation failed',
});
});
});
});

View File

@@ -0,0 +1,739 @@
import {
generateUser,
requester,
translate as t,
createAndPopulateGroup,
getProperty,
} from '../../../../helpers/api-integration/v4';
import { ApiUser } from '../../../../helpers/api-integration/api-classes';
import { v4 as uuid } from 'uuid';
import { each } from 'lodash';
import { encrypt } from '../../../../../website/server/libs/encryption';
function generateRandomUserName () {
return (Date.now() + uuid()).substring(0, 20);
}
describe('POST /user/auth/local/register', () => {
context('username and email are free', () => {
let api;
beforeEach(async () => {
api = requester();
});
it('registers a new user', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user._id).to.exist;
expect(user.apiToken).to.exist;
expect(user.auth.local.username).to.eql(username);
expect(user.profile.name).to.eql(username);
expect(user.newUser).to.eql(true);
});
xit('remove spaces from username', async () => {
// TODO can probably delete this test now
let username = ' usernamewithspaces ';
let email = 'test@example.com';
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.auth.local.username).to.eql(username.trim());
expect(user.profile.name).to.eql(username.trim());
});
context('validates username', () => {
const email = 'test@example.com';
const password = 'password';
it('requires to username to be less than 20', async () => {
const username = (Date.now() + uuid()).substring(0, 21);
await expect(api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Invalid request parameters.',
});
});
it('rejects chracters not in [-_a-zA-Z0-9]', async () => {
const username = 'a-zA_Z09*';
await expect(api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Invalid request parameters.',
});
});
it('allows only [-_a-zA-Z0-9] characters', async () => {
const username = 'a-zA_Z09';
const user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.auth.local.username).to.eql(username);
});
});
context('provides default tags and tasks', async () => {
it('for a generic API consumer', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
let requests = new ApiUser(user);
let habits = await requests.get('/tasks/user?type=habits');
let dailys = await requests.get('/tasks/user?type=dailys');
let todos = await requests.get('/tasks/user?type=todos');
let rewards = await requests.get('/tasks/user?type=rewards');
let tags = await requests.get('/tags');
expect(habits).to.have.a.lengthOf(0);
expect(dailys).to.have.a.lengthOf(0);
expect(todos).to.have.a.lengthOf(1);
expect(rewards).to.have.a.lengthOf(0);
expect(tags).to.have.a.lengthOf(7);
expect(tags[0].name).to.eql(t('defaultTag1'));
expect(tags[1].name).to.eql(t('defaultTag2'));
expect(tags[2].name).to.eql(t('defaultTag3'));
expect(tags[3].name).to.eql(t('defaultTag4'));
expect(tags[4].name).to.eql(t('defaultTag5'));
expect(tags[5].name).to.eql(t('defaultTag6'));
expect(tags[6].name).to.eql(t('defaultTag7'));
});
xit('for Web', async () => {
api = requester(
null,
{'x-client': 'habitica-web'},
);
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
let requests = new ApiUser(user);
let habits = await requests.get('/tasks/user?type=habits');
let dailys = await requests.get('/tasks/user?type=dailys');
let todos = await requests.get('/tasks/user?type=todos');
let rewards = await requests.get('/tasks/user?type=rewards');
let tags = await requests.get('/tags');
expect(habits).to.have.a.lengthOf(3);
expect(habits[0].text).to.eql(t('defaultHabit1Text'));
expect(habits[0].notes).to.eql('');
expect(habits[1].text).to.eql(t('defaultHabit2Text'));
expect(habits[1].notes).to.eql('');
expect(habits[2].text).to.eql(t('defaultHabit3Text'));
expect(habits[2].notes).to.eql('');
expect(dailys).to.have.a.lengthOf(0);
expect(todos).to.have.a.lengthOf(1);
expect(todos[0].text).to.eql(t('defaultTodo1Text'));
expect(todos[0].notes).to.eql(t('defaultTodoNotes'));
expect(rewards).to.have.a.lengthOf(1);
expect(rewards[0].text).to.eql(t('defaultReward1Text'));
expect(rewards[0].notes).to.eql('');
expect(tags).to.have.a.lengthOf(7);
expect(tags[0].name).to.eql(t('defaultTag1'));
expect(tags[1].name).to.eql(t('defaultTag2'));
expect(tags[2].name).to.eql(t('defaultTag3'));
expect(tags[3].name).to.eql(t('defaultTag4'));
expect(tags[4].name).to.eql(t('defaultTag5'));
expect(tags[5].name).to.eql(t('defaultTag6'));
expect(tags[6].name).to.eql(t('defaultTag7'));
});
});
context('does not provide default tags and tasks', async () => {
it('for Android', async () => {
api = requester(
null,
{'x-client': 'habitica-android'},
);
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
let requests = new ApiUser(user);
let habits = await requests.get('/tasks/user?type=habits');
let dailys = await requests.get('/tasks/user?type=dailys');
let todos = await requests.get('/tasks/user?type=todos');
let rewards = await requests.get('/tasks/user?type=rewards');
let tags = await requests.get('/tags');
expect(habits).to.have.a.lengthOf(0);
expect(dailys).to.have.a.lengthOf(0);
expect(todos).to.have.a.lengthOf(0);
expect(rewards).to.have.a.lengthOf(0);
expect(tags).to.have.a.lengthOf(0);
});
it('for iOS', async () => {
api = requester(
null,
{'x-client': 'habitica-ios'},
);
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
let requests = new ApiUser(user);
let habits = await requests.get('/tasks/user?type=habits');
let dailys = await requests.get('/tasks/user?type=dailys');
let todos = await requests.get('/tasks/user?type=todos');
let rewards = await requests.get('/tasks/user?type=rewards');
let tags = await requests.get('/tags');
expect(habits).to.have.a.lengthOf(0);
expect(dailys).to.have.a.lengthOf(0);
expect(todos).to.have.a.lengthOf(0);
expect(rewards).to.have.a.lengthOf(0);
expect(tags).to.have.a.lengthOf(0);
});
});
it('enrolls new users in an A/B test', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
});
it('includes items awarded by default when creating a new user', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.items.quests.dustbunnies).to.equal(1);
expect(user.purchased.background.violet).to.be.ok;
expect(user.preferences.background).to.equal('violet');
});
it('requires password and confirmPassword to match', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let confirmPassword = 'not password';
await expect(api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('requires a username', async () => {
let email = `${generateRandomUserName()}@example.com`;
let password = 'password';
let confirmPassword = 'password';
await expect(api.post('/user/auth/local/register', {
email,
password,
confirmPassword,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('requires an email', async () => {
let username = generateRandomUserName();
let password = 'password';
await expect(api.post('/user/auth/local/register', {
username,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('requires a valid email', async () => {
let username = generateRandomUserName();
let email = 'notanemail@sdf';
let password = 'password';
await expect(api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('sanitizes email params to a lowercase string before creating the user', async () => {
let username = generateRandomUserName();
let email = 'ISANEmAiL@ExAmPle.coM';
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.auth.local.email).to.equal(email.toLowerCase());
});
it('fails on a habitica.com email', async () => {
let username = generateRandomUserName();
let email = `${username}@habitica.com`;
let password = 'password';
await expect(api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'User validation failed',
});
});
it('fails on a habitrpg.com email', async () => {
let username = generateRandomUserName();
let email = `${username}@habitrpg.com`;
let password = 'password';
await expect(api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'User validation failed',
});
});
it('requires a password', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
let confirmPassword = 'password';
await expect(api.post('/user/auth/local/register', {
username,
email,
confirmPassword,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
});
context('attach to facebook user', () => {
let user;
let email = 'some@email.net';
let username = 'some-username';
let password = 'some-password';
beforeEach(async () => {
user = await generateUser();
});
it('checks onlySocialAttachLocal', async () => {
await expect(user.post('/user/auth/local/register', {
email,
username,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlySocialAttachLocal'),
});
});
it('succeeds', async () => {
await user.update({ 'auth.facebook.id': 'some-fb-id', 'auth.local': { ok: true } });
await user.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
await user.sync();
expect(user.auth.local.username).to.eql(username);
expect(user.auth.local.email).to.eql(email);
});
});
context('login is already taken', () => {
let username, email, api;
beforeEach(async () => {
api = requester();
username = generateRandomUserName();
email = `${username}@example.com`;
return generateUser({
'auth.local.username': username,
'auth.local.lowerCaseUsername': username,
'auth.local.email': email,
});
});
it('rejects if username is already taken', async () => {
let uniqueEmail = `${generateRandomUserName()}@example.com`;
let password = 'password';
await expect(api.post('/user/auth/local/register', {
username,
email: uniqueEmail,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('usernameTaken'),
});
});
it('rejects if email is already taken', async () => {
let uniqueUsername = generateRandomUserName();
let password = 'password';
await expect(api.post('/user/auth/local/register', {
username: uniqueUsername,
email,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('emailTaken'),
});
});
});
context('req.query.groupInvite', () => {
let api, username, email, password;
beforeEach(() => {
api = requester();
username = generateRandomUserName();
email = `${username}@example.com`;
password = 'password';
});
it('does not crash the signup process when it\'s invalid', async () => {
let user = await api.post('/user/auth/local/register?groupInvite=aaaaInvalid', {
username,
email,
password,
confirmPassword: password,
});
expect(user._id).to.be.a('string');
});
it('supports invite using req.query.groupInvite', async () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
});
let invite = encrypt(JSON.stringify({
id: group._id,
inviter: groupLeader._id,
sentAt: Date.now(), // so we can let it expire
}));
let user = await api.post(`/user/auth/local/register?groupInvite=${invite}`, {
username,
email,
password,
confirmPassword: password,
});
expect(user.invitations.parties[0].id).to.eql(group._id);
expect(user.invitations.parties[0].name).to.eql(group.name);
expect(user.invitations.parties[0].inviter).to.eql(groupLeader._id);
});
it('awards achievement to inviter', async () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
});
let invite = encrypt(JSON.stringify({
id: group._id,
inviter: groupLeader._id,
sentAt: Date.now(),
}));
await api.post(`/user/auth/local/register?groupInvite=${invite}`, {
username,
email,
password,
confirmPassword: password,
});
await groupLeader.sync();
expect(groupLeader.achievements.invitedFriend).to.be.true;
});
it('user not added to a party on expired invite', async () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
});
let invite = encrypt(JSON.stringify({
id: group._id,
inviter: groupLeader._id,
sentAt: Date.now() - 6.912e8, // 8 days old
}));
let user = await api.post(`/user/auth/local/register?groupInvite=${invite}`, {
username,
email,
password,
confirmPassword: password,
});
expect(user.invitations.party).to.eql({});
});
it('adds a user to a guild on an invite of type other than party', async () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
});
let invite = encrypt(JSON.stringify({
id: group._id,
inviter: groupLeader._id,
sentAt: Date.now(),
}));
let user = await api.post(`/user/auth/local/register?groupInvite=${invite}`, {
username,
email,
password,
confirmPassword: password,
});
expect(user.invitations.guilds[0]).to.eql({
id: group._id,
name: group.name,
inviter: groupLeader._id,
});
});
});
context('successful login via api', () => {
let api, username, email, password;
beforeEach(() => {
api = requester();
username = generateRandomUserName();
email = `${username}@example.com`;
password = 'password';
});
it('sets all site tour values to -2 (already seen)', async () => {
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.flags.tour).to.not.be.empty;
each(user.flags.tour, (value) => {
expect(value).to.eql(-2);
});
});
it('populates user with default todos, not no other task types', async () => {
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.tasksOrder.todos).to.not.be.empty;
expect(user.tasksOrder.dailys).to.be.empty;
expect(user.tasksOrder.habits).to.be.empty;
expect(user.tasksOrder.rewards).to.be.empty;
});
it('populates user with default tags', async () => {
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.tags).to.not.be.empty;
});
});
context('successful login with habitica-web header', () => {
let api, username, email, password;
beforeEach(() => {
api = requester({}, {'x-client': 'habitica-web'});
username = generateRandomUserName();
email = `${username}@example.com`;
password = 'password';
});
it('sets all common tutorial flags to true', async () => {
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.flags.tour).to.not.be.empty;
each(user.flags.tutorial.common, (value) => {
expect(value).to.eql(true);
});
});
it('populates user with default todos, habits, and rewards', async () => {
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.tasksOrder.todos).to.be.empty;
expect(user.tasksOrder.dailys).to.be.empty;
expect(user.tasksOrder.habits).to.be.empty;
expect(user.tasksOrder.rewards).to.be.empty;
});
it('populates user with default tags', async () => {
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.tags).to.not.be.empty;
});
it('adds the correct tags to the correct tasks', async () => {
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
let requests = new ApiUser(user);
let habits = await requests.get('/tasks/user?type=habits');
let todos = await requests.get('/tasks/user?type=todos');
expect(habits).to.have.a.lengthOf(0);
expect(todos).to.have.a.lengthOf(0);
});
});
});

View File

@@ -0,0 +1,89 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v4';
const ENDPOINT = '/user/auth/verify-username';
describe('POST /user/auth/verify-username', async () => {
let user;
beforeEach(async () => {
user = await generateUser();
});
it('successfully verifies username', async () => {
let newUsername = 'new-username';
let response = await user.post(ENDPOINT, {
username: newUsername,
});
expect(response).to.eql({ isUsable: true });
});
it('successfully verifies username with allowed characters', async () => {
let newUsername = 'new-username_123';
let response = await user.post(ENDPOINT, {
username: newUsername,
});
expect(response).to.eql({ isUsable: true });
});
context('errors', async () => {
it('errors if username is not provided', async () => {
await expect(user.post(ENDPOINT, {
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('errors if username is a slur', async () => {
await expect(user.post(ENDPOINT, {
username: 'TESTPLACEHOLDERSLURWORDHERE',
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueLength'), t('usernameIssueSlur')] });
});
it('errors if username contains a slur', async () => {
await expect(user.post(ENDPOINT, {
username: 'TESTPLACEHOLDERSLURWORDHERE_otherword',
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueLength'), t('usernameIssueSlur')] });
await expect(user.post(ENDPOINT, {
username: 'something_TESTPLACEHOLDERSLURWORDHERE',
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueLength'), t('usernameIssueSlur')] });
await expect(user.post(ENDPOINT, {
username: 'somethingTESTPLACEHOLDERSLURWORDHEREotherword',
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueLength'), t('usernameIssueSlur')] });
});
it('errors if username is not allowed', async () => {
await expect(user.post(ENDPOINT, {
username: 'support',
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueForbidden')] });
});
it('errors if username is not allowed regardless of casing', async () => {
await expect(user.post(ENDPOINT, {
username: 'SUppORT',
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueForbidden')] });
});
it('errors if username has incorrect length', async () => {
await expect(user.post(ENDPOINT, {
username: 'thisisaverylongusernameover20characters',
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueLength')] });
});
it('errors if username contains invalid characters', async () => {
await expect(user.post(ENDPOINT, {
username: 'Eichhörnchen',
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueInvalidCharacters')] });
await expect(user.post(ENDPOINT, {
username: 'test.name',
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueInvalidCharacters')] });
await expect(user.post(ENDPOINT, {
username: '🤬',
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueInvalidCharacters')] });
});
});
});

View File

@@ -0,0 +1,224 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v4';
import {
bcryptCompare,
sha1MakeSalt,
sha1Encrypt as sha1EncryptPassword,
} from '../../../../../website/server/libs/password';
const ENDPOINT = '/user/auth/update-username';
describe('PUT /user/auth/update-username', async () => {
let user;
let password = 'password'; // from habitrpg/test/helpers/api-integration/v4/object-generators.js
beforeEach(async () => {
user = await generateUser();
});
it('successfully changes username with password', async () => {
let newUsername = 'new-username';
let response = await user.put(ENDPOINT, {
username: newUsername,
password,
});
expect(response).to.eql({ username: newUsername });
await user.sync();
expect(user.auth.local.username).to.eql(newUsername);
});
it('successfully changes username without password', async () => {
let newUsername = 'new-username-nopw';
let response = await user.put(ENDPOINT, {
username: newUsername,
});
expect(response).to.eql({ username: newUsername });
await user.sync();
expect(user.auth.local.username).to.eql(newUsername);
});
it('successfully changes username containing number and underscore', async () => {
let newUsername = 'new_username9';
let response = await user.put(ENDPOINT, {
username: newUsername,
});
expect(response).to.eql({ username: newUsername });
await user.sync();
expect(user.auth.local.username).to.eql(newUsername);
});
it('sets verifiedUsername when changing username', async () => {
user.flags.verifiedUsername = false;
await user.sync();
let newUsername = 'new-username-verify';
let response = await user.put(ENDPOINT, {
username: newUsername,
});
expect(response).to.eql({ username: newUsername });
await user.sync();
expect(user.flags.verifiedUsername).to.eql(true);
});
it('converts user with SHA1 encrypted password to bcrypt encryption', async () => {
let myNewUsername = 'my-new-username';
let textPassword = 'mySecretPassword';
let salt = sha1MakeSalt();
let sha1HashedPassword = sha1EncryptPassword(textPassword, salt);
await user.update({
'auth.local.hashed_password': sha1HashedPassword,
'auth.local.passwordHashMethod': 'sha1',
'auth.local.salt': salt,
});
await user.sync();
expect(user.auth.local.passwordHashMethod).to.equal('sha1');
expect(user.auth.local.salt).to.equal(salt);
expect(user.auth.local.hashed_password).to.equal(sha1HashedPassword);
// update email
let response = await user.put(ENDPOINT, {
username: myNewUsername,
password: textPassword,
});
expect(response).to.eql({ username: myNewUsername });
await user.sync();
expect(user.auth.local.username).to.eql(myNewUsername);
expect(user.auth.local.passwordHashMethod).to.equal('bcrypt');
expect(user.auth.local.salt).to.be.undefined;
expect(user.auth.local.hashed_password).not.to.equal(sha1HashedPassword);
let isValidPassword = await bcryptCompare(textPassword, user.auth.local.hashed_password);
expect(isValidPassword).to.equal(true);
});
context('errors', async () => {
it('prevents username update if new username is already taken', async () => {
let existingUsername = 'existing-username';
await generateUser({'auth.local.username': existingUsername, 'auth.local.lowerCaseUsername': existingUsername });
await expect(user.put(ENDPOINT, {
username: existingUsername,
password,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('usernameTaken'),
});
});
it('errors if password is wrong', async () => {
let newUsername = 'new-username';
await expect(user.put(ENDPOINT, {
username: newUsername,
password: 'wrong-password',
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('wrongPassword'),
});
});
it('errors if new username is not provided', async () => {
await expect(user.put(ENDPOINT, {
password,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('errors if new username is a slur', async () => {
await expect(user.put(ENDPOINT, {
username: 'TESTPLACEHOLDERSLURWORDHERE',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: [t('usernameIssueLength'), t('usernameIssueSlur')].join(' '),
});
});
it('errors if new username contains a slur', async () => {
await expect(user.put(ENDPOINT, {
username: 'TESTPLACEHOLDERSLURWORDHERE_otherword',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: [t('usernameIssueLength'), t('usernameIssueSlur')].join(' '),
});
await expect(user.put(ENDPOINT, {
username: 'something_TESTPLACEHOLDERSLURWORDHERE',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: [t('usernameIssueLength'), t('usernameIssueSlur')].join(' '),
});
await expect(user.put(ENDPOINT, {
username: 'somethingTESTPLACEHOLDERSLURWORDHEREotherword',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: [t('usernameIssueLength'), t('usernameIssueSlur')].join(' '),
});
});
it('errors if new username is not allowed', async () => {
await expect(user.put(ENDPOINT, {
username: 'support',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('usernameIssueForbidden'),
});
});
it('errors if new username is not allowed regardless of casing', async () => {
await expect(user.put(ENDPOINT, {
username: 'SUppORT',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('usernameIssueForbidden'),
});
});
it('errors if username has incorrect length', async () => {
await expect(user.put(ENDPOINT, {
username: 'thisisaverylongusernameover20characters',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('usernameIssueLength'),
});
});
it('errors if new username contains invalid characters', async () => {
await expect(user.put(ENDPOINT, {
username: 'Eichhörnchen',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('usernameIssueInvalidCharacters'),
});
await expect(user.put(ENDPOINT, {
username: 'test.name',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('usernameIssueInvalidCharacters'),
});
await expect(user.put(ENDPOINT, {
username: '🤬',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('usernameIssueInvalidCharacters'),
});
});
});
});

View File

@@ -0,0 +1,50 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import NotificationsComponent from 'client/components/notifications.vue';
import Store from 'client/libs/store';
import { hasClass } from 'client/store/getters/members';
const localVue = createLocalVue();
localVue.use(Store);
describe('Notifications', () => {
let store;
beforeEach(() => {
store = new Store({
state: {
user: {
data: {
stats: {
lvl: 0,
},
flags: {},
preferences: {},
party: {
quest: {
},
},
},
},
},
actions: {
'user:fetch': () => {},
'tasks:fetchUserTasks': () => {},
},
getters: {
'members:hasClass': hasClass,
},
});
});
it('set user has class computed prop', () => {
const wrapper = shallowMount(NotificationsComponent, { store, localVue });
expect(wrapper.vm.userHasClass).to.be.false;
store.state.user.data.stats.lvl = 10;
store.state.user.data.flags.classSelected = true;
store.state.user.data.preferences.disableClasses = false;
expect(wrapper.vm.userHasClass).to.be.true;
});
});

View File

@@ -1,4 +1,4 @@
import {shallow} from '@vue/test-utils';
import { shallow } from '@vue/test-utils';
import SidebarSection from 'client/components/sidebarSection.vue';
@@ -51,4 +51,4 @@ describe('Sidebar Section', () => {
expect(wrapper.find('.section-body').element.style.display).to.eq('none');
});
});
});

View File

@@ -88,7 +88,7 @@ describe('Task Column', () => {
expect(el).to.eq(taskListOverride[i]);
});
wrapper.setProps({ isUser: false, taskListOverride });
wrapper.setProps({ isUser: false });
wrapper.vm.taskList.forEach((el, i) => {
expect(el).to.eq(taskListOverride[i]);

View File

@@ -0,0 +1,19 @@
import generateStore from 'client/store';
describe('canDelete getter', () => {
it('cannot delete active challenge task', () => {
const store = generateStore();
const task = {userId: 1, challenge: {id: 2}};
expect(store.getters['tasks:canDelete'](task)).to.equal(false);
});
it('can delete broken challenge task', () => {
const store = generateStore();
const task = {userId: 1, challenge: {id: 2, broken: true}};
expect(store.getters['tasks:canDelete'](task)).to.equal(true);
});
});

View File

@@ -0,0 +1,141 @@
import generateStore from 'client/store';
describe('getTaskClasses getter', () => {
let store, getTaskClasses;
beforeEach(() => {
store = generateStore();
store.state.user.data = {
preferences: {
},
};
getTaskClasses = store.getters['tasks:getTaskClasses'];
});
it('returns reward edit-modal-bg class', () => {
const task = {type: 'reward'};
expect(getTaskClasses(task, 'edit-modal-bg')).to.equal('task-purple-modal-bg');
});
it('returns worst task edit-modal-bg class', () => {
const task = {type: 'todo', value: -21};
expect(getTaskClasses(task, 'edit-modal-bg')).to.equal('task-worst-modal-bg');
});
it('returns worse task edit-modal-bg class', () => {
const task = {type: 'todo', value: -11};
expect(getTaskClasses(task, 'edit-modal-bg')).to.equal('task-worse-modal-bg');
});
it('returns bad task edit-modal-bg class', () => {
const task = {type: 'todo', value: -6};
expect(getTaskClasses(task, 'edit-modal-bg')).to.equal('task-bad-modal-bg');
});
it('returns neutral task edit-modal-bg class', () => {
const task = {type: 'todo', value: 0};
expect(getTaskClasses(task, 'edit-modal-bg')).to.equal('task-neutral-modal-bg');
});
it('returns good task edit-modal-bg class', () => {
const task = {type: 'todo', value: 2};
expect(getTaskClasses(task, 'edit-modal-bg')).to.equal('task-good-modal-bg');
});
it('returns better task edit-modal-bg class', () => {
const task = {type: 'todo', value: 6};
expect(getTaskClasses(task, 'edit-modal-bg')).to.equal('task-better-modal-bg');
});
it('returns best task edit-modal-bg class', () => {
const task = {type: 'todo', value: 12};
expect(getTaskClasses(task, 'edit-modal-bg')).to.equal('task-best-modal-bg');
});
it('returns best task edit-modal-text class', () => {
const task = {type: 'todo', value: 12};
expect(getTaskClasses(task, 'edit-modal-text')).to.equal('task-best-modal-text');
});
it('returns best task edit-modal-icon class', () => {
const task = {type: 'todo', value: 12};
expect(getTaskClasses(task, 'edit-modal-icon')).to.equal('task-best-modal-icon');
});
it('returns best task edit-modal-option-disabled class', () => {
const task = {type: 'todo', value: 12};
expect(getTaskClasses(task, 'edit-modal-option-disabled')).to.equal('task-best-modal-option-disabled');
});
it('returns best task edit-modal-control-disabled class', () => {
const task = {type: 'todo', value: 12};
expect(getTaskClasses(task, 'edit-modal-habit-control-disabled')).to.equal('task-best-modal-habit-control-disabled');
});
it('returns create-modal-bg class', () => {
const task = {type: 'todo'};
expect(getTaskClasses(task, 'create-modal-bg')).to.equal('task-purple-modal-bg');
});
it('returns create-modal-text class', () => {
const task = {type: 'todo'};
expect(getTaskClasses(task, 'create-modal-text')).to.equal('task-purple-modal-text');
});
it('returns create-modal-icon class', () => {
const task = {type: 'todo'};
expect(getTaskClasses(task, 'create-modal-icon')).to.equal('task-purple-modal-icon');
});
it('returns create-modal-option-disabled class', () => {
const task = {type: 'todo'};
expect(getTaskClasses(task, 'create-modal-option-disabled')).to.equal('task-purple-modal-option-disabled');
});
it('returns create-modal-habit-control-disabled class', () => {
const task = {type: 'todo'};
expect(getTaskClasses(task, 'create-modal-habit-control-disabled')).to.equal('task-purple-modal-habit-control-disabled');
});
it('returns completed todo classes', () => {
const task = {type: 'todo', value: 2, completed: true};
expect(getTaskClasses(task, 'control')).to.deep.equal({
bg: 'task-disabled-daily-todo-control-bg',
checkbox: 'task-disabled-daily-todo-control-checkbox',
inner: 'task-disabled-daily-todo-control-inner',
content: 'task-disabled-daily-todo-control-content',
});
});
xit('returns good todo classes', () => {
const task = {type: 'todo', value: 2};
expect(getTaskClasses(task, 'control')).to.deep.equal({
bg: 'task-good-control-bg',
checkbox: 'task-good-control-checkbox',
inner: 'task-good-control-inner-daily-todo`',
});
});
it('returns reward classes', () => {
const task = {type: 'reward'};
expect(getTaskClasses(task, 'control')).to.deep.equal({
bg: 'task-reward-control-bg',
});
});
it('returns habit up classes', () => {
const task = {type: 'habit', value: 2, up: true};
expect(getTaskClasses(task, 'control')).to.deep.equal({
up: {
bg: 'task-good-control-bg',
inner: 'task-good-control-inner-habit',
},
down: {
bg: 'task-disabled-habit-control-bg',
inner: 'task-disabled-habit-control-inner',
},
});
});
});

View File

@@ -0,0 +1,52 @@
import hasClass from '../../../website/common/script/libs/hasClass';
import { generateUser } from '../../helpers/common.helper';
describe('hasClass', () => {
it('returns false for user with level below 10', () => {
let userLvl9 = generateUser({
'stats.lvl': 9,
'flags.classSelected': true,
'preferences.disableClasses': false,
});
let result = hasClass(userLvl9);
expect(result).to.eql(false);
});
it('returns false for user with class not selected', () => {
let userClassNotSelected = generateUser({
'stats.lvl': 10,
'flags.classSelected': false,
'preferences.disableClasses': false,
});
let result = hasClass(userClassNotSelected);
expect(result).to.eql(false);
});
it('returns false for user with classes disabled', () => {
let userClassesDisabled = generateUser({
'stats.lvl': 10,
'flags.classSelected': true,
'preferences.disableClasses': true,
});
let result = hasClass(userClassesDisabled);
expect(result).to.eql(false);
});
it('returns true for user with class', () => {
let userClassSelected = generateUser({
'stats.lvl': 10,
'flags.classSelected': true,
'preferences.disableClasses': false,
});
let result = hasClass(userClassSelected);
expect(result).to.eql(true);
});
});

View File

@@ -1,20 +0,0 @@
import clearPMs from '../../../website/common/script/ops/clearPMs';
import {
generateUser,
} from '../../helpers/common.helper';
describe('shared.ops.clearPMs', () => {
let user;
beforeEach(() => {
user = generateUser();
user.inbox.messages = { first: 'message', second: 'message' };
});
it('clears messages', () => {
expect(user.inbox.messages).to.not.eql({});
let [result] = clearPMs(user);
expect(user.inbox.messages).to.eql({});
expect(result).to.eql({});
});
});

View File

@@ -1,20 +0,0 @@
import deletePM from '../../../website/common/script/ops/deletePM';
import {
generateUser,
} from '../../helpers/common.helper';
describe('shared.ops.deletePM', () => {
let user;
beforeEach(() => {
user = generateUser();
user.inbox.messages = { first: 'message', second: 'message' };
});
it('delete message', () => {
expect(user.inbox.messages).to.not.eql({ second: 'message' });
let [response] = deletePM(user, { params: { id: 'first' } });
expect(user.inbox.messages).to.eql({ second: 'message' });
expect(response).to.eql({ second: 'message' });
});
});

38
test/common/ops/spells.js Normal file
View File

@@ -0,0 +1,38 @@
import {
generateUser,
} from '../../helpers/common.helper';
import spells from '../../../website/common/script/content/spells';
import {
NotAuthorized,
} from '../../../website/common/script/libs/errors';
import i18n from '../../../website/common/script/i18n';
// TODO complete the test suite...
describe('shared.ops.spells', () => {
let user;
beforeEach(() => {
user = generateUser();
});
it('returns an error when healer tries to cast Healing Light with full health', (done) => {
user.stats.class = 'healer';
user.stats.lvl = 11;
user.stats.hp = 50;
user.stats.mp = 200;
let spell = spells.healer.heal;
try {
spell.cast(user);
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('messageHealthAlreadyMax'));
expect(user.stats.hp).to.eql(50);
expect(user.stats.mp).to.eql(200);
done();
}
});
});

View File

@@ -13,7 +13,11 @@ describe('shared.ops.allocate', () => {
let user;
beforeEach(() => {
user = generateUser();
user = generateUser({
'stats.lvl': 10,
'flags.classSelected': true,
'preferences.disableClasses': false,
});
});
it('throws an error if an invalid attribute is supplied', (done) => {
@@ -28,6 +32,39 @@ describe('shared.ops.allocate', () => {
}
});
it('throws an error if the user is below lvl 10', (done) => {
user.stats.lvl = 9;
try {
allocate(user);
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('classNotSelected'));
done();
}
});
it('throws an error if the user hasn\'t selected class', (done) => {
user.flags.classSelected = false;
try {
allocate(user);
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('classNotSelected'));
done();
}
});
it('throws an error if the user has disabled classes', (done) => {
user.preferences.disableClasses = true;
try {
allocate(user);
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('classNotSelected'));
done();
}
});
it('throws an error if the user doesn\'t have attribute points', (done) => {
try {
allocate(user);

View File

@@ -13,7 +13,11 @@ describe('shared.ops.allocateBulk', () => {
let user;
beforeEach(() => {
user = generateUser();
user = generateUser({
'stats.lvl': 10,
'flags.classSelected': true,
'preferences.disableClasses': false,
});
});
it('throws an error if an invalid attribute is supplied', (done) => {
@@ -43,6 +47,60 @@ describe('shared.ops.allocateBulk', () => {
}
});
it('throws an error if the user is below lvl 10', (done) => {
user.stats.lvl = 9;
try {
allocateBulk(user, {
body: {
stats: {
int: 1,
str: 2,
},
},
});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('classNotSelected'));
done();
}
});
it('throws an error if the user hasn\'t selected class', (done) => {
user.flags.classSelected = false;
try {
allocateBulk(user, {
body: {
stats: {
int: 1,
str: 2,
},
},
});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('classNotSelected'));
done();
}
});
it('throws an error if the user has disabled classes', (done) => {
user.preferences.disableClasses = true;
try {
allocateBulk(user, {
body: {
stats: {
int: 1,
str: 2,
},
},
});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('classNotSelected'));
done();
}
});
it('throws an error if the user doesn\'t have attribute points', (done) => {
try {
allocateBulk(user, {

View File

@@ -15,21 +15,21 @@ div
router-view(v-if="!isUserLoggedIn || isStaticPage")
template(v-else)
template(v-if="isUserLoaded")
div.resting-banner(v-if="showRestingBanner")
div.resting-banner(v-show="showRestingBanner", ref="restingBanner")
span.content
span.label {{ $t('innCheckOutBanner') }}
span.separator |
span.label.d-inline.d-sm-none {{ $t('innCheckOutBannerShort') }}
span.label.d-none.d-sm-inline {{ $t('innCheckOutBanner') }}
span.separator |
span.resume(@click="resumeDamage()") {{ $t('resumeDamage') }}
div.closepadding(@click="hideBanner()")
span.svg-icon.inline.icon-10(aria-hidden="true", v-html="icons.close")
notifications-display
app-menu(:class='{"restingInn": showRestingBanner}')
app-menu(:class='{"restingInn": showRestingBanner}' :style="{ marginTop: bannerHeight + 'px' }")
.container-fluid
app-header(:class='{"restingInn": showRestingBanner}')
buyModal(
:item="selectedItemToBuy || {}",
:withPin="true",
@change="resetItemToBuy($event)",
@buyPressed="customPurchase($event)",
:genericPurchase="genericPurchase(selectedItemToBuy)",
@@ -119,20 +119,9 @@ div
z-index: 1600 !important; /* Must stay above nav bar */
}
.restingInn {
.navbar {
top: 40px;
}
#app-header {
margin-top: 40px !important;
}
}
.resting-banner {
width: 100%;
height: 40px;
min-height: 40px;
background-color: $blue-10;
position: fixed;
top: 0;
@@ -140,11 +129,10 @@ div
display: flex;
.content {
height: 24px;
line-height: 1.71;
text-align: center;
color: $white;
padding: 8px 38px 8px 8px;
margin: auto;
}
@@ -161,6 +149,13 @@ div
}
}
@media only screen and (max-width: 768px) {
.content {
font-size: 12px;
line-height: 1.4;
}
}
.separator {
color: $blue-100;
margin: 0px 15px;
@@ -169,6 +164,7 @@ div
.resume {
font-weight: bold;
cursor: pointer;
white-space:nowrap;
}
}
</style>
@@ -224,6 +220,7 @@ export default {
loading: true,
currentTipNumber: 0,
bannerHidden: false,
bannerHeight: 0,
};
},
computed: {
@@ -414,7 +411,6 @@ export default {
this.$store.watch(state => state.title, (title) => {
document.title = title;
});
this.$nextTick(() => {
// Load external scripts after the app has been rendered
Analytics.load();
@@ -432,6 +428,14 @@ export default {
this.hideLoadingScreen();
window.addEventListener('resize', this.setBannerOffset);
// Adjust the positioning of the header banners
this.$watch('showRestingBanner', () => {
this.$nextTick(() => {
this.setBannerOffset();
});
}, {immediate: true});
// Adjust the timezone offset
if (this.user.preferences.timezoneOffset !== this.browserTimezoneOffset) {
this.$store.dispatch('user:set', {
@@ -458,6 +462,7 @@ export default {
this.$root.$off('bv::show::modal');
this.$root.$off('buyModal::showItem');
this.$root.$off('selectMembersModal::showItem');
window.removeEventListener('resize', this.setBannerOffset);
},
mounted () {
// Remove the index.html loading screen and now show the inapp loading
@@ -567,13 +572,6 @@ export default {
});
}
},
resetItemToBuy ($event) {
// @TODO: Do we need this? I think selecting a new item
// overwrites. @negue might know
if (!$event && this.selectedItemToBuy.purchaseType !== 'card') {
this.selectedItemToBuy = null;
}
},
itemSelected (item) {
this.selectedItemToBuy = item;
},
@@ -623,10 +621,22 @@ export default {
},
hideBanner () {
this.bannerHidden = true;
this.setBannerOffset();
},
resumeDamage () {
this.$store.dispatch('user:sleep');
},
setBannerOffset () {
let contentPlacement = 0;
if (this.showRestingBanner && this.$refs.restingBanner !== undefined) {
contentPlacement = this.$refs.restingBanner.clientHeight;
}
this.bannerHeight = contentPlacement;
let smartBanner = document.getElementsByClassName('smartbanner')[0];
if (smartBanner !== undefined) {
smartBanner.style.top = `${contentPlacement}px`;
}
},
},
};
</script>
@@ -659,3 +669,4 @@ export default {
<style src="assets/css/sprites/spritesmith-main-21.css"></style>
<style src="assets/css/sprites/spritesmith-main-22.css"></style>
<style src="assets/css/sprites.css"></style>
<style src="smartbanner.js/dist/smartbanner.min.css"></style>

View File

@@ -1,42 +1,84 @@
.promo_armoire_backgrounds_201808 {
.achievement-costumeContest6x {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -817px 0px;
background-position: -1076px -214px;
width: 144px;
height: 156px;
}
.promo_animal_tails {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -934px -214px;
width: 141px;
height: 441px;
}
.promo_armoire_backgrounds_201810 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -910px;
width: 423px;
height: 147px;
}
.promo_fall_festival_2017 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -699px;
width: 414px;
height: 210px;
}
.promo_fall_festival_2018 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -934px 0px;
width: 393px;
height: 213px;
}
.promo_forest_friends_bundle {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -421px -495px;
width: 423px;
height: 147px;
}
.promo_ghost_potions {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -415px -699px;
width: 423px;
height: 147px;
}
.promo_ios {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -441px 0px;
background-position: 0px -337px;
width: 375px;
height: 361px;
}
.promo_kangaroo {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -327px;
background-position: 0px 0px;
width: 420px;
height: 336px;
}
.promo_mystery_201807 {
.promo_mystery_201809 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -817px -442px;
width: 114px;
height: 120px;
background-position: -934px -656px;
width: 306px;
height: 147px;
}
.promo_seasonal_shop {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1076px -371px;
width: 162px;
height: 138px;
}
.promo_take_this {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -817px -563px;
background-position: -1221px -214px;
width: 96px;
height: 69px;
}
.promo_unconventional_armor {
.scene_nametag {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -441px -362px;
width: 180px;
height: 180px;
background-position: -421px 0px;
width: 512px;
height: 208px;
}
.scene_tavern {
.scene_tools {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px 0px;
width: 440px;
height: 326px;
background-position: -421px -209px;
width: 366px;
height: 285px;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,114 @@
.Pet_HatchingPotion_Floral {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -69px 0px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Ghost {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -207px 0px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Glass {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: 0px -69px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Glow {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -69px -69px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Golden {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -138px 0px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Holly {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -138px -69px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Peppermint {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: 0px -138px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Purple {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -69px -138px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Rainbow {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -138px -138px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Red {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: 0px 0px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_RoyalPurple {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -207px -69px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Shade {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -207px -138px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Shimmer {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: 0px -207px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Skeleton {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -69px -207px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Spooky {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -138px -207px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_StarryNight {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -207px -207px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Thunderstorm {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -276px 0px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_White {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -276px -69px;
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Zombie {
background-image: url('~assets/images/sprites/spritesmith-main-23.png');
background-position: -276px -138px;
width: 68px;
height: 68px;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 549 KiB

After

Width:  |  Height:  |  Size: 554 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 449 KiB

After

Width:  |  Height:  |  Size: 483 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 315 KiB

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 343 KiB

After

Width:  |  Height:  |  Size: 419 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 79 KiB

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