Compare commits

..

142 Commits

Author SHA1 Message Date
Sabe Jones 3b35a0a203 4.39.0 2018-04-23 17:21:12 +00:00
Sabe Jones d787ad43d3 chore(i18n): update locales 2018-04-23 17:20:54 +00:00
Keith Holliday 7d7fe6047c Move Chat to Model (#9703)
* Began moving group chat to separate model

* Fixed lint issue

* Updated delete chat with new model

* Updated flag chat to support model

* Updated like chat to use model

* Fixed duplicate code and chat messages

* Added note about concat chat

* Updated clear flags to user new model

* Updated more chat checks when loading get group

* Fixed spell test and back save

* Moved get chat to json method

* Updated flagging with new chat model

* Added missing await

* Fixed chat user styles. Fixed spell group test

* Added new model to quest chat and group plan chat

* Removed extra timestamps. Added limit check for group plans

* Updated tests

* Synced id fields

* Fixed id creation

* Add meta and fixed tests

* Fixed group quest accept test

* Updated puppeteer

* Added migration

* Export vars

* Updated comments
2018-04-23 12:17:16 -05:00
Sabe Jones 0ec1a91774 4.38.0 2018-04-19 19:29:33 +00:00
Sabe Jones adf3281bef chore(i18n): update locales 2018-04-19 19:28:43 +00:00
SabreCat ea86b35833 chore(news): Bailey 2018-04-19 19:25:45 +00:00
Alys ade14edcd7 add partial documentation for dueDate parameter in /api/v3/tasks/user and related code 2018-04-18 23:22:11 +10:00
Sabe Jones 3a1888739a Merge branch 'release' into develop 2018-04-17 20:07:12 +00:00
Sabe Jones 3b54ce4949 4.37.2 2018-04-17 20:06:46 +00:00
Sabe Jones 4a8aaf7389 chore(i18n): update locales 2018-04-17 19:55:10 +00:00
SabreCat 45eec47b7f chore(news): Bailey
Also disable some costly analytics
2018-04-17 19:52:36 +00:00
Keith Holliday 4b9af8aa86 Added analytics to front. Fixed group plan tracking (#10262) 2018-04-17 12:43:52 -05:00
SabreCat 631bbcb786 Merge branch 'fix-hippocrite' into develop 2018-04-17 01:37:20 +00:00
Matteo Pagliazzi 76a10d6cf9 start removing inbox from some routes (#10259) 2018-04-16 18:43:09 +02:00
Keith Holliday a1c9ebd661 Prevent dropdown from closing when clicking search (#10252) 2018-04-15 19:18:40 -05:00
SabreCat 9f06d78db6 Revert "moving developer-only strings to api messages (#10188)"
This reverts commit a42cb0e3ab. Testing hypothesis that this was causing Staging to break.
2018-04-15 17:09:15 +00:00
Alys ac98aa9271 replace Lemoness's email address with admin in sample config file
This is for consistency with the production server and to ensure
that contributors' screenshots in PRs match what will be seen
in production.
2018-04-15 13:34:42 +10:00
negue 455f7ac59b round priority on update too (#10186)
* round priority on update too

* move the fix to Task sanitizeTransform

* refactor the task.priority parsing
2018-04-14 16:16:25 +02:00
negue a42cb0e3ab moving developer-only strings to api messages (#10188)
* move translatable string to apiMessages

* use apiMessages instead of res.t for groupIdRequired / keepOrRemove

* move pageMustBeNumber to apiMessages

* change apimessages

* move missingKeyParam to apiMessages

* move more strings to apiMessages

* fix lint

* revert lodash imports to fix tests

* fix webhook test

* fix test

* rollback key change of `keepOrRemove`

* remove unneeded `req.language` param

*  extract more messages from i18n

* add missing `missingTypeParam` message
2018-04-14 16:13:13 +02:00
Alys d05d2fb9d7 removed a slur that has legit uses - TRIGGER / CONTENT WARNING: slurs, swearwords, assault, etc 2018-04-14 21:51:06 +10:00
negue 6c4c5b4697 always check for the quantity not (#10251) 2018-04-13 21:04:08 +02:00
Keith Holliday 5da87640e4 Apple pay tests (#10248)
* Added more tests for verifyGemPurchase

* Added more tests for subscribe

* Added user is subscribed check

* Reverted gulp task

* Added existence check
2018-04-13 12:41:41 -05:00
Kip Raske fa044ffb44 Feature/sortable reward area (#9930)
* Client POC

We need to wrap each draggable region it its own div or else the
"draggable" element will conflict with each other. This screws up the
styling but that is totally fixable

* Ah that ref was being used after all, changing back

* Scaffold out a new callback for when we drag these things

Next is going to be the hard part: I need to save the sort order for
these to the database. I don't even know if there is a schema but hey
this is the best place to start

* Firefox caching is the problem: don't actually need the wrapper div

So I guess I should try this in chrome and see how it works then come
back to firefox and figure out what the heck is going on

* Scaffolding out our API call to save the sort order

The endpoint doesn't exist yet so we will need to add that

* Ok we are now calling our API endpoint to reorder these things

Of course it doesn't exist yet so you get a 404 when you try, but that
is ok

* Defining api endpoint, a work in progress

In particular I really had ought to use _id for these too, it appears
that the primary way we detect order doesn't even use "key" at all.

* Switching to using the pinned item UUID

This has much better results, but of course the server and client logic
don't match now. Will have to keep working on my splice to make sure
that they are the same

* I thought this would fix our server/client mismatch but it is not it

Something is really wrong with my logic somewhere, maybe I need to
update the db step?

* Moving this logic to the "user" rather than "tasks" and key off path

Path is unique and is less finiky than dealing with string comparisons
with ids. Unfortunately everything is still not working... I suppose
user.update() doesn't care about the position?

* This client code caused quite a lot of problems if you dragged fast

We don't really need it it seems, so off it goes

* Updating markup and CSS so it actually looks good.

Everything is working horray!!

I did just notice the following bug: the popover text sometimes makes it
very annoying to drag because you can't drop over it@

* Cleaning up my comments in the API section user.js

I had a lot of TODOS that are mostly done now

* Fixing a spacing code standards thing

* Turns out we never use type, so we should remove this from the API call

* Adding pinnedItemsOrder into the user schema

And disabling my call in the frontend before I do any more damage

* Halfway to using pinnedItemsOrder

This isn't working yet but it is not going to break it horribly like it
was before.

* Hooking up inAppRewards to always produce sorted information

It is suspicially working right now even though I have not added the
seasonal stuff logic yet...

* Updating the comments in user.js in movedPinnedItem

It turns out that my bandaid fix to just get the ball rolling perfectly
does what I need it to do when we have a length discrepancy. So we are
getting much closer to the final product, just need lots of testing

* Cleaning up code standards kinds of things

* Yay, this fixes the popover issue

I hope this is the right "vue" way to do things, because I tried a bunch
of other things that definately were not the right way to do it. And
this appears to work too

* ** Partial Work ** Starting tests on api call for draggable items

Doesn't work, doesn't compile so don't include in PR!

* Test failing still...

This is worth a save. The api call grabs the seasonal items too, so we
can't get away from using the common functions and calls here to get the
actual list of items

* Okay have the first test passing

Need to clean up my linter problems though

* Planning out the next two tests and fixing my format problems

* 2nd Test case written, this time with the "more" odd case

* Making sure that we didn't mess with pinned items

* Huh... this test doesn't give me the expected result

Drat, I guess I found a bug

* Throw an error when we put garbage in our api call.

Well, before we got user.pinnedItemsOrder filled with a bunch of "null"
entries which is not ideal. it still worked, but isn't this confusing
enough already?

* Cleaning up the multitude of linting problems thanks gulp :)

* Writing tests for inAppRewards.js, but something is wrong

* Fixing my linting errors in inAppRewards tests

These tests still do not run though, so they may fail and I would not
know

* Applying Negue's fixes to inAppRewards.js test

It never occured to me that we shouldn't try to reach the database while
in the common tests. Well, we shouldn't do that, we should use the
common.helpers instead. Thanks!
2018-04-13 15:22:06 +02:00
Tyler Nychka 5449652bd2 pinned items fixes #10012 (#10216)
* Don't unpin non-gear items

Assumes that multiple of bundles, quests, eggs, potions can be bought

* Added tests

* Changed type checking and made variables global

* Lint fix
2018-04-13 15:19:44 +02:00
Philip Karpiak c12ae9ea25 Fix #10202 - Send DELETE request when detaching social auth (#10207) 2018-04-13 15:16:49 +02:00
greenkeeper[bot] 734a300b92 fix(package): update sass-loader to version 7.0.0 (#10250) 2018-04-13 15:15:08 +02:00
negue 1109ae308d convert buyQuest (gold) to the purchase refactoring / check quantity to be a number (#10244) 2018-04-13 15:14:51 +02:00
negue 8f1d241e83 if a pet is still hatchable show the hatchable - icon instead of the "you already own the mount"-icon (#10243) 2018-04-13 15:13:42 +02:00
Matteo Pagliazzi acbca4d1dc upgrade deps 2018-04-12 21:55:24 +02:00
Matteo Pagliazzi 1ea9be8aa2 Preparatory Work for Smaller user doc (WIP) (#10245)
* protect all paths in user.pre(save using this.isDirectSelected to see if a field is available

* fix linting

* authWithHeaders: specify user fields to exclude instead of the ones to include, add comments, doc and improve test

* add more options to unit helper generateReq and add tests for excluding fields in authWithHeaders
2018-04-12 21:17:47 +02:00
Sabe Jones ace02893e5 4.37.1 2018-04-12 18:38:18 +00:00
Sabe Jones 1c3e043fac chore(i18n): update locales 2018-04-12 18:37:36 +00:00
Matteo Pagliazzi 71c9e7a685 Tasks Modal: add setter for repeatsOn (#10247)
* fix for 10236, add setter to repeatsOn

* remove console.log
2018-04-12 13:30:56 -05:00
Sabe Jones fa945c7689 Merge branch 'release' into develop 2018-04-11 01:38:10 +00:00
Sabe Jones c54ce96033 4.37.0 2018-04-11 01:37:46 +00:00
Sabe Jones 85c4e93763 chore(i18n): update locales 2018-04-11 01:37:27 +00:00
SabreCat 25e5e78373 chore(sprites): compile 2018-04-11 01:33:15 +00:00
SabreCat 06181d0a1a feat(content): Squirrel Pet Quest 2018-04-11 01:32:49 +00:00
Matteo Pagliazzi d5a8259fdb fix members modals (#10240) 2018-04-10 13:27:06 +02:00
Isaac Lim 9db7141853 Added meta image for social media sharing (#10193)
* Add meta image for social media sharing

* Meta Image in Images

* Update index.html
2018-04-09 08:34:12 +02:00
Brian Fenton ec2a1927a0 adding name attribute to radio inputs so browser inforces selecting a single item from the named set (#10236) 2018-04-09 08:31:33 +02:00
Matteo Pagliazzi 1c1b0f00ad reorganize payments files (#10235) 2018-04-08 16:27:03 +02:00
Alys fb4d3e44d3 improve code and tests for banned words and slurs (#10211)
* remove removePunctuationFromString function from test code

It's not needed now that the test banned words don't contain underscores.

* prevent tests accidentally throwing messageGroupChatSpam

This commit makes the user for most tests have contributor tiers so
that the user can't trigger the messageGroupChatSpam error message
(for posting messages too quickly).

This is useful when some of the tests fail due to broken code
because that makes more messages be posted than expected. If the user
doesn't have tiers, the messageGroupChatSpam error message would be
triggered, which gives misleading information about the test failure.

* add tests for banned swear and slur words posted in mixed case

* allow banned word error message to show bad words in the same case the user typed them

* stop using randomly-chosen real banned words in tests

The test modified in this commit had been using real banned words,
which meant that those words were being displayed to the contributors
when the test failed.

NB the 'check all banned words are matched' test also uses the real
banned words but the test failure messages don't show the words.

* improve translatability of bannedWordUsed error message
2018-04-08 15:31:37 +02:00
Alys 37fd062cf9 increase Hourglasses and gemCapExtra promptly when multi-month subscription renews - fixes #4819 (#10147)
* allow Hourglasses and gemCapExtra to increase promptly after a multi-month subscription has renewed

* fix existing Hourglass and Gem Cap tests that were wrong

The scenario originally used for these two tests was a six-month recurring
subscription (you can tell that from the starting offset having a non-zero value).
For recurring subscriptions, we do NOT want to increase the consecutive month
benefits as soon as the sixth month starts because the user has already been
given a full six months' benefits in advance and they might cancel the
subscription before it renews later in the sixth month.
Therefore we want to give the extra benefits at the beginning of the seventh
month (ideally we'd give them mid-month in the sixth month when the renewal
happens but we don't have support for tracking renewal dates).
So, the two changed tests were actually not correct for the case
where the offset started as non-zero.

These tests are correct for one-month recurring subscriptions (when the offset
is never set to anything above zero). The user isn't meant to get any consecutive
month benefits until a multiple of 3 months has been reached.

* add tests for one-month recurring subscription before 3x months are reached

* add tests for 3-, 6-, and 12-month recurring subscriptions

The 3-month tests are the most thorough, stepping through the
expected start and end values of consecutive data for a 7-month
range.

The 6-month tests are a bit less thorough since the same code is
used for all multi-month periods.
The discount Google subscription code is used to ensure we keep
support for it.

The 12-month tests are less thorough still, since again the same
code is used.

I'm about to try some more tests with `useFakeTimers`, which should
be a better way to test the code since they won't rely on me having
set the initial values correctly for each test. :) But I wanted to
work through these cases manually first to ensure my understanding
of how the values should change does actually match the code.

* add tests for 1-, 3-, 6-, and 12-month recurring subscriptions using clock changes to simulate passing months

Also fixed the clock call in an unrelated test because it was forming
the date incorrectly (`unix()` can't be used to create a date).

Also changed email@email.email to email@example.com because
email@email.email is potentially a real email address.

* add tests for 3-month gift subscriptions - no extra consecutive benefits given

* add tests for consecutive benefits for 6-month recurring subscription that has incorrect consecutive month data because it started before issue #4819 was fixed

* fix lint errors

* remove outdated subscription tests
2018-04-08 15:26:25 +02:00
Matteo Pagliazzi 485c3c5c46 disable failing test 2018-04-08 14:58:51 +02:00
negue 5007393f24 enable hair style edit during intro (#10227) 2018-04-08 14:52:26 +02:00
Alys e111ac730c enable translated pet names in hatching success message (#10231) 2018-04-08 14:50:36 +02:00
Philip Karpiak e7c78eabce Wrap creator icon + text in @click event (#10221)
Perviously only clicking the icon would activate tabs in the creator, which was confusing
2018-04-06 12:56:33 -05:00
Philip Karpiak 5da7699548 Add tooltip to character buff icon (#10156)
* Add tooltip to character buff icon

* Add tooltips for task streak, challenge and broken challenge

* Add tooltips for task menu and due date

* Challenge icon tooltip displays the challenge short name
2018-04-06 12:53:39 -05:00
Keith Holliday f42955a0ba Added initial account banned modal (#9868)
* Added initial account banned modal

* Fixed check for non logged in user
2018-04-06 08:33:38 -05:00
Sabe Jones 4d67df4da6 4.36.0 2018-04-05 21:09:10 +00:00
Sabe Jones ab7459f4f3 chore(i18n): update locales 2018-04-05 21:08:53 +00:00
SabreCat 469db7c0e2 Merge branch 'develop' into release 2018-04-05 21:04:11 +00:00
SabreCat 952e813b30 feat(event): avatar customizations 2018-04-05 21:03:57 +00:00
Matteo Pagliazzi f04d05fee1 fix likes appearing immediately in chat 2018-04-05 20:54:20 +02:00
SabreCat 6d9aa43c07 fix(purchasing): typo "substract" 2018-04-03 20:30:54 +00:00
Sabe Jones f527221079 Merge branch 'release' into develop 2018-04-03 18:49:37 +00:00
Sabe Jones d9b852e1ea 4.35.1 2018-04-03 18:49:14 +00:00
Sabe Jones a1207c1d8d chore(i18n): update locales 2018-04-03 18:48:56 +00:00
SabreCat f4fb90013d feat(event): enable Shiny Seeds
Plus Bailey news and fix for bulk purchasing transformation items
2018-04-03 18:42:24 +00:00
negue 73a7c0eebc antidote display and functionality (#10199)
* update antidote display and functionality - fixes #9758 and #10160

update antidote display and functionality - fixes #9758 and #10160

update antidote display and functionality - fixes #9758 and #10160

update antidote display and functionality - fixes #9758 and #10160

* clean up / refactor

* prevent unpin of all items which don't have a pinType

* remove the double boolean casting / fix lint
2018-04-03 17:35:56 +00:00
SabreCat 1819398f41 Revert "feat(event): April Foolery 2018"
This reverts commit f7b9ca124d.
2018-04-03 17:19:14 +00:00
Keith Holliday ab14312368 Group plan landing page (#10222)
* Updated task page styles

* Added initial page styles

* Added login and payments

* Updated more styles

* Added white header

* Added group plan overview modal

* Updated copy

* Fixed location

* Style updates

* Added analytics

* More style updates

* Added locales

* Removed duplicate key
2018-04-03 11:26:08 -05:00
Sabe Jones 690d3e3fd2 Merge branch 'release' into develop 2018-04-03 00:55:28 +00:00
Sabe Jones 36f9a4918f 4.35.0 2018-04-03 00:55:06 +00:00
Sabe Jones a4b5e27614 chore(i18n): update locales 2018-04-03 00:45:59 +00:00
SabreCat 0abfe86296 chore(sprites): compile 2018-04-03 00:41:49 +00:00
SabreCat e11c777325 feat(content): Armoire and Backgrounds 4/18 2018-04-03 00:41:22 +00:00
Keith Holliday 63a04f36c9 Added guilds to allowed banned words 2018-04-02 17:36:57 -05:00
Keith Holliday e58af6e3ea Replaced siena with admin 2018-04-02 17:35:26 -05:00
Keith Holliday 6ba28b5757 Added fix for chat revoke creation test (#10218) 2018-04-02 19:11:30 +02:00
Keith Holliday ed607d2bae Fixed challenge count check (#10215) 2018-04-01 16:54:32 -05:00
Keith Holliday 1f7fc594e5 Added supression support for pet/mount modals and added mount modal (#9882)
* Added supression support for pet/mount modals and added mount modal

* Moved create animal

* Fixed raise pet logic

* Added suppresion for stable hatch modal

* Fixed click paw and added growl

* Fixed confirm. Fixed mount name

* Suppresed confirmation
2018-04-01 14:43:18 -05:00
Alys 45d0a4fac2 add a second test swear word and slur - TRIGGER / CONTENT WARNING: assault, slurs, swearwords, etc 2018-04-01 19:58:23 +10:00
Sabe Jones e50bc189aa Merge branch 'release' into develop 2018-04-01 03:55:40 +00:00
Sabe Jones 95f8867bf8 4.34.2 2018-04-01 03:01:04 +00:00
SabreCat 4968b291f7 chore(news): Bailey 2018-04-01 02:59:54 +00:00
SabreCat f7b9ca124d feat(event): April Foolery 2018 2018-04-01 02:55:04 +00:00
Philip Karpiak 4623bcd877 Force menu links to have white color on :hover (#10200)
This is mostly to fix the contact form link under the Help menu inheriting the wrong color on hover due to missing href attribute
2018-03-31 13:48:41 +02:00
Alys 4a368a1128 supply the correct type and path for featured Magic Hatching Potions - fixes #10135 (#10204) 2018-03-31 13:46:51 +02:00
Alys bec8cb01e0 prevents a user who has had their chat privileges revoked from creating a public guild (#10205) 2018-03-31 13:46:14 +02:00
negue f3c041a561 antidote display and functionality (#10199)
* update antidote display and functionality - fixes #9758 and #10160

update antidote display and functionality - fixes #9758 and #10160

update antidote display and functionality - fixes #9758 and #10160

update antidote display and functionality - fixes #9758 and #10160

* clean up / refactor

* prevent unpin of all items which don't have a pinType

* remove the double boolean casting / fix lint
2018-03-31 13:39:26 +02:00
Mateus Etto c21726ec61 No Ethereal Surge on Mages (#10121)
* problem location identified (breaks code)

* problem identification notes

* Add class checking to ES (does not yet notify user)

* Add error message

* Add .gitattributes

Attempting to fix line ending disaster

* package stuff

so I can see what's broken

* add reminder and hopefully fix gitattributes

* Fix lint errors

* Redo surge fail notifs

* exterminate rogue comment, fix gitattributes

* Remove unused import 

As per @paglias' request.

* fix(lint): remove extraneous expression

* Delete .gitattributes

* Fix skill key surge -> mpheal

* Show notification only when there are mages in party

* Fix notification being too big and appearing outside the notification div

* Remove unused code

* Only show the notification on parties with 2 or more mages

The caster is a mage, so certainly at least 1 mage will be counted.

* Automated test: mpheal does not heal other mages

* Fix lint error

* Fix typo in test description

* Increase performance of test

* Using target instead of requestion partyMembers again

* Rename variable 'party' to 'partyMembers'

* Update strings in English

* spell -> Skill
2018-03-31 13:34:39 +02:00
Alys df69208caa prevent a user with no chat privileges from inviting any player to a guild or party (#10194)
This is because they could use private group chat messages to bypass
the restriction on talking to other players.
2018-03-31 13:29:08 +02:00
negue 08d07cdd67 split profile and profileStats (#10185) 2018-03-31 13:22:17 +02:00
Philip Karpiak a309e48183 Remove Like action from inbox chat messages (#10181)
There is no API endpoint for this action and seems rather useless for private messages anyway
2018-03-31 13:20:41 +02:00
Clay Smith 70c539cc81 (fixes #9978) Remove Leader dropdown from groupFormModal (#10176) 2018-03-31 13:17:10 +02:00
negue 11f136ac89 Purchase API Refactoring: Armoire (#10153)
* convert armoire

* fix armoire tests

* fix lint
2018-03-31 13:10:37 +02:00
negue 567d5f74ba Purchase API Refactoring: Health Potion (#10152)
* convert buyHealthPotion

* fix health potion tests

* fix lint
2018-03-31 13:09:16 +02:00
Alys 338781f57b improves rounding for boss hp and player pending damage - partial fix for #8368 (#9749)
* improves rounding for boss hp and player pending damage

* use floor filter function for user's pending damage for party page and tavern boss
2018-03-31 12:57:37 +02:00
Matteo Pagliazzi bd07f3cd38 disable test failing on travis 2018-03-31 12:20:11 +02:00
Alys 0b735abd44 remove underscores from test swear words and slurs - TRIGGER / CONTENT WARNING: assault, slurs, swearwords, etc
This is because real words don't contain them and the test words should mimic real words.
2018-03-31 18:06:04 +10:00
Alys a88cdaf1fc Revert "Move notification snackbars when resting bar is shown (#10177)" (#10203)
This reverts commit 64e86bad91
because the console was showing errors like this:

[Vue warn]: Error in render: "TypeError: this.user is null"
found in
---> <App> at website/client/app.vue
       <Root>
2018-03-31 15:15:13 +10:00
Neel Mehta 7cae5f1a37 fix featured quests label under butterfly (#10197) 2018-03-30 15:39:33 -05:00
Neel Mehta e453330535 Make modal overlay transparent, not solid purple (#10191) 2018-03-30 15:38:22 -05:00
Philip Karpiak b1e5fcdeaf Focus title input of task modal when shown (#10182) 2018-03-30 15:35:59 -05:00
Philip Karpiak 10e0848a5c Use margin offsets for group plan header (#10180)
So there are no whitespaces around the header
2018-03-30 15:34:46 -05:00
Philip Karpiak 64e86bad91 Move notification snackbars when resting bar is shown (#10177)
Allows top-most notification to still be shown
2018-03-30 15:34:06 -05:00
Gabriel Siedler 21cf5d2321 fixes #10173 - Task page search bar only searches task titles, not No… (#10175)
* fixes #10173 - Task page search bar only searches task titles, not Notes (#10173)

* Fixing unit test: Test expect task to have note, but it has notes.
2018-03-30 15:30:15 -05:00
Matteo Pagliazzi a6106a801b try fix for test 2018-03-30 19:31:35 +02:00
Matteo Pagliazzi 769405ff34 google subs: make sure the subs can be cancelled if they return a 410 from google (#10201) 2018-03-30 19:24:51 +02:00
Sabe Jones a0803796b2 Merge branch 'release' into develop 2018-03-30 17:03:39 +00:00
Sabe Jones cb418882f3 4.34.1 2018-03-30 17:03:13 +00:00
Sabe Jones b17a09ac17 chore(i18n): update locales 2018-03-30 16:58:55 +00:00
Sabe Jones 88bb4f6a72 Revert "Revert "Adding the ability for admins to revoke/reinstate chat privileges and block/unblock users from the profile page. (#10082)""
This reverts commit fed2d3fb19.
2018-03-30 16:57:21 +00:00
SabreCat ae0c440846 chore(news): Bailey 2018-03-30 16:55:51 +00:00
Keith Holliday 2f69f4039e Made challenge paging optional 2018-03-30 11:34:00 -05:00
Matteo Pagliazzi 10370ea1dc fix wording on admin tools 2018-03-30 18:27:20 +02:00
Sabe Jones 0d65e5219e 4.34.0 2018-03-30 01:20:25 +00:00
Sabe Jones fed2d3fb19 Revert "Adding the ability for admins to revoke/reinstate chat privileges and block/unblock users from the profile page. (#10082)"
This reverts commit e4b13eecd1.
2018-03-30 01:20:18 +00:00
Sabe Jones 6ec50ed0c1 chore(i18n): update locales 2018-03-30 01:18:27 +00:00
SabreCat 6f1a551d76 chore(news): Bailey 2018-03-30 01:12:38 +00:00
SabreCat bed97f0610 feat(community): update Community Guidelines 2018-03-30 00:26:07 +00:00
Sabe Jones f86f98f4a6 Merge branch 'release' into develop 2018-03-28 21:33:04 +00:00
Sabe Jones 0e442a0076 4.33.2 2018-03-28 21:32:03 +00:00
Sabe Jones 89f047b15b chore(i18n): update locales 2018-03-28 21:30:38 +00:00
Travis Husman e64bc2e39a Fixing issue with pull request when task is changed to monthly but that start date isn't updated. 2018-03-28 21:00:17 +00:00
SabreCat 3b3fcbdfce fix(sound): correct element nesting 2018-03-28 20:47:31 +00:00
Neel Mehta 558dd2e4bf fix hippo-crite scroll image size 2018-03-27 23:04:34 -04:00
SabreCat 7914a959b3 fix(test): add flags to expected public fields 2018-03-27 21:50:55 +00:00
SabreCat 8a27524fa0 fix(members): include classSelected flag in public fields 2018-03-27 21:21:11 +00:00
Matteo Pagliazzi a64fed97ac try to fix failing tests 2018-03-27 15:54:00 +02:00
Keith Holliday e937d1722e Contact form links (#10187)
* Added links to contact form

* Fixed encoding link. Added link to tavern. Updated copy

* Converted link to cookie link

* Updated domain format

* Updated links

* Added apikey to cookie
2018-03-26 14:02:32 -05:00
greenkeeper[bot] f537e8142f chore(package): update eslint-plugin-mocha to version 5.0.0 (#10174) 2018-03-24 18:34:32 +01:00
Travis e4b13eecd1 Adding the ability for admins to revoke/reinstate chat privileges and block/unblock users from the profile page. (#10082)
* Adding the ability for admins to revoke/reinstate chat privileges and block/unblock users from the profile page.

fixes #10073

* Updating fix to dynamically load user blocked and chat revoked state for admins.

* Fixing pr according to comments.
2018-03-24 11:18:52 -05:00
Travis b5872a9577 Give MasterClasser Acheivement on completion of any of the series quests, not just the final. (#10086)
* Adding check to give master classer acheivement on any master classer series quest completion

fixes #9461

* Fixing concat bug by assigning the variable after concatenation.

* Fixing retry query.
2018-03-24 11:18:33 -05:00
Travis 48fa78bef2 Quest completion modal acknowledge on close (#10089)
* Change quest completion modal to only close on user acknowledge of clicking ok or 'x'
and update 'x' to acknowledge the quest completion.

* Removing check on header-close.

* Removing unused variable.
2018-03-24 11:18:13 -05:00
Travis 781256c917 fix: fix quest shop to not use string addition when buying quests (#10120)
* fix: fix quest shop to not use string addition when buying quests

fixes #10115

* Fixing quest purchase quantity interpretted as a string on the server side.

* Adjusting pull-request according to comments.

* Updating according to PR comments.
2018-03-24 11:17:23 -05:00
Mark Kuba dcd680c293 Fix/mana bar unselected class - fix: #10026 (#10126)
* Update getClass() for users who have not yet selected a class

* Added tests for members.getClass()

* fix linter errors

* Update test

* Update import in test to point to correct module

* use hasClass() getter where appropriate

* Fix linter error
2018-03-24 11:15:40 -05:00
Philip Karpiak ec6f53bb1b Wrap attribute cell value text (#10161)
When they become floating point or have more than 3 charcters they wrap to the next line. This change prevents that.
2018-03-24 11:12:26 -05:00
jack 9834afee4a fixes #9880 - uneven sizes and spacing in the action buttons of the user modal. (#10163) 2018-03-24 11:09:56 -05:00
Dexx Mandele 9b279563ea Prettify background overview (#10078)
* Bring buy bg set button in line with title

* Make background sets pretty

* Add checkbox to filter backgrounds

* Review: replace background filter checkbox with toggle

* Remove dashed line from toggle-switch label

* Test: add comma to make es-lint happy

* Move toggle tooltip into toggle-switch template

* Make toggle-switch label optional

* Review: Remove toggle-switch margin
2018-03-24 11:07:37 -05:00
Sabe Jones 347fe69667 Merge branch 'release' into develop 2018-03-24 02:51:25 +00:00
Sabe Jones 552cf70abd 4.33.1 2018-03-24 02:51:01 +00:00
Sabe Jones 01c8ef9382 chore(i18n): update locales 2018-03-24 02:48:23 +00:00
Keith Holliday 298a6a743c Added paging (#10150)
* Added paging

* Escaped regex

* Fixed challenge side effect tests
2018-03-23 14:13:08 -05:00
Matteo Pagliazzi fa17ab7c17 avoid errors when trying to call createTasks with no tasks (#10170) 2018-03-23 17:15:50 +01:00
Matteo Pagliazzi 0f339d8d3e Docker: yarn is not necessary anymore 2018-03-23 16:20:00 +01:00
Keith Holliday 99852fcd89 Stringified mem report (#10167)
* Stringified mem report

* Passed info
2018-03-23 10:19:24 -05:00
Matteo Pagliazzi ca3d044aa1 upgrade deps 2018-03-23 16:01:51 +01:00
greenkeeper[bot] b338e65dc9 chore(package): update karma-webpack to version 3.0.0 (#10151) 2018-03-23 16:00:18 +01:00
greenkeeper[bot] 1475c93962 chore(package): update eslint-friendly-formatter to version 4.0.0 (#10168) 2018-03-23 15:58:40 +01:00
778 changed files with 32909 additions and 29094 deletions
-3
View File
@@ -1,8 +1,5 @@
FROM node:8
# Upgrade NPM to v5 (Yarn is needed because of this bug https://github.com/npm/npm/issues/16807)
# The used solution is suggested here https://github.com/npm/npm/issues/16807#issuecomment-313591975
RUN yarn global add npm@5
# Install global packages
RUN npm install -g gulp-cli mocha
+1 -4
View File
@@ -11,16 +11,13 @@ ENV GOOGLE_CLIENT_ID 1035232791481-32vtplgnjnd1aufv3mcu1lthf31795fq.apps.googleu
ENV NODE_ENV production
ENV STRIPE_PUB_KEY pk_85fQ0yMECHNfHTSsZoxZXlPSwSNfA
# Upgrade NPM to v5 (Yarn is needed because of this bug https://github.com/npm/npm/issues/16807)
# The used solution is suggested here https://github.com/npm/npm/issues/16807#issuecomment-313591975
RUN yarn global add npm@5
# Install global packages
RUN npm install -g gulp-cli mocha
# Clone Habitica repo and install dependencies
RUN mkdir -p /usr/src/habitrpg
WORKDIR /usr/src/habitrpg
RUN git clone --branch v4.32.0 https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
RUN git clone --branch v4.37.2 https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
RUN npm install
RUN gulp build:prod --force
+2 -2
View File
@@ -98,9 +98,9 @@
},
"ITUNES_SHARED_SECRET": "aaaabbbbccccddddeeeeffff00001111",
"EMAILS" : {
"COMMUNITY_MANAGER_EMAIL" : "leslie@habitica.com",
"COMMUNITY_MANAGER_EMAIL" : "admin@habitica.com",
"TECH_ASSISTANCE_EMAIL" : "admin@habitica.com",
"PRESS_ENQUIRY_EMAIL" : "leslie@habitica.com"
"PRESS_ENQUIRY_EMAIL" : "admin@habitica.com"
},
"LOGGLY" : {
"TOKEN" : "example-token",
+52
View File
@@ -0,0 +1,52 @@
// @migrationName = 'MigrateGroupChat';
// @authorName = 'TheHollidayInn'; // in case script author needs to know when their ...
// @authorUuid = ''; // ... own data is done
/*
* This migration move ass chat off of groups and into their own model
*/
import { model as Group } from '../../website/server/models/group';
import { model as Chat } from '../../website/server/models/chat';
async function moveGroupChatToModel (skip = 0) {
const groups = await Group.find({})
.limit(50)
.skip(skip)
.sort({ _id: -1 })
.exec();
if (groups.length === 0) {
console.log('End of groups');
process.exit();
}
const promises = groups.map(group => {
const chatpromises = group.chat.map(message => {
const newChat = new Chat();
Object.assign(newChat, message);
newChat._id = message.id;
newChat.groupId = group._id;
return newChat.save();
});
group.chat = [];
chatpromises.push(group.save());
return chatpromises;
});
const reducedPromises = promises.reduce((acc, curr) => {
acc = acc.concat(curr);
return acc;
}, []);
console.log(reducedPromises);
await Promise.all(reducedPromises);
moveGroupChatToModel(skip + 50);
}
module.exports = moveGroupChatToModel;
+1 -1
View File
@@ -17,5 +17,5 @@ function setUpServer () {
setUpServer();
// Replace this with your migration
const processUsers = require('./20180125_clean_new_notifications.js');
const processUsers = require('./groups/migrate-chat.js');
processUsers();
+117
View File
@@ -0,0 +1,117 @@
import each from 'lodash/each';
import keys from 'lodash/keys';
import content from '../../website/common/script/content/index';
const migrationName = 'full-stable.js';
const authorName = 'Sabe'; // in case script author needs to know when their ...
const authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
/*
* Award users every extant pet and mount
*/
const connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
let monk = require('monk');
let dbUsers = monk(connectionString).get('users', { castIds: false });
function processUsers (lastId) {
// specify a query to limit the affected users (empty for all users):
let query = {
'profile.name': 'SabreCat',
};
if (lastId) {
query._id = {
$gt: lastId,
};
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 250,
fields: [
], // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
})
.then(updateUsers)
.catch((err) => {
console.log(err);
return exiting(1, `ERROR! ${ err}`);
});
}
let progressCount = 1000;
let count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
displayData();
return;
}
let userPromises = users.map(updateUser);
let lastUser = users[users.length - 1];
return Promise.all(userPromises)
.then(() => {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
let set = {
migration: migrationName,
};
each(keys(content.pets), (pet) => {
set[`items.pets.${pet}`] = 5;
});
each(keys(content.premiumPets), (pet) => {
set[`items.pets.${pet}`] = 5;
});
each(keys(content.questPets), (pet) => {
set[`items.pets.${pet}`] = 5;
});
each(keys(content.specialPets), (pet) => {
set[`items.pets.${pet}`] = 5;
});
each(keys(content.mounts), (mount) => {
set[`items.mounts.${mount}`] = true;
});
each(keys(content.premiumMounts), (mount) => {
set[`items.mounts.${mount}`] = true;
});
each(keys(content.questMounts), (mount) => {
set[`items.mounts.${mount}`] = true;
});
each(keys(content.specialMounts), (mount) => {
set[`items.mounts.${mount}`] = true;
});
dbUsers.update({_id: user._id}, {$set: set});
if (count % progressCount === 0) console.warn(`${count } ${ user._id}`);
if (user._id === authorUuid) console.warn(`${authorName } processed`);
}
function displayData () {
console.warn(`\n${ count } users processed\n`);
return exiting(0);
}
function exiting (code, msg) {
code = code || 0; // 0 = success
if (code && !msg) {
msg = 'ERROR!';
}
if (msg) {
if (code) {
console.error(msg);
} else {
console.log(msg);
}
}
process.exit(code);
}
module.exports = processUsers;
+295 -314
View File
File diff suppressed because it is too large Load Diff
+32 -32
View File
@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "4.33.0",
"version": "4.39.0",
"main": "./website/server/index.js",
"dependencies": {
"@slack/client": "^3.8.1",
@@ -9,8 +9,8 @@
"amazon-payments": "^0.2.6",
"amplitude": "^3.5.0",
"apidoc": "^0.17.5",
"autoprefixer": "^8.1.0",
"aws-sdk": "^2.211.0",
"autoprefixer": "^8.2.0",
"aws-sdk": "^2.224.1",
"axios": "^0.18.0",
"axios-progress-bar": "^1.1.8",
"babel-core": "^6.0.0",
@@ -27,19 +27,19 @@
"babel-runtime": "^6.11.6",
"bcrypt": "^1.0.2",
"body-parser": "^1.15.0",
"bootstrap": "^4.0.0",
"bootstrap-vue": "^2.0.0-rc.2",
"bootstrap": "^4.1.0",
"bootstrap-vue": "^2.0.0-rc.6",
"compression": "^1.7.2",
"cookie-session": "^1.2.0",
"coupon-code": "^0.4.5",
"cross-env": "^5.1.4",
"css-loader": "^0.28.11",
"csv-stringify": "^2.0.4",
"csv-stringify": "^2.1.0",
"cwait": "^1.1.1",
"domain-middleware": "~0.1.0",
"express": "^4.16.3",
"express-basic-auth": "^1.1.4",
"express-validator": "^5.0.3",
"express-validator": "^5.1.2",
"extract-text-webpack-plugin": "^3.0.2",
"glob": "^7.1.2",
"got": "^8.3.0",
@@ -50,9 +50,9 @@
"gulp.spritesmith": "^6.9.0",
"habitica-markdown": "^1.3.0",
"hellojs": "^1.15.1",
"html-webpack-plugin": "^3.0.0",
"html-webpack-plugin": "^3.2.0",
"image-size": "^0.6.2",
"in-app-purchase": "^1.8.9",
"in-app-purchase": "^1.9.0",
"intro.js": "^2.6.0",
"jquery": ">=3.0.0",
"js2xmlparser": "^3.0.0",
@@ -60,14 +60,14 @@
"memwatch-next": "^0.3.0",
"merge-stream": "^1.0.0",
"method-override": "^2.3.5",
"moment": "^2.21.0",
"moment": "^2.22.0",
"moment-recur": "^1.0.7",
"mongoose": "^5.0.10",
"mongoose": "^5.0.14",
"morgan": "^1.7.0",
"nconf": "^0.10.0",
"node-gcm": "^0.14.4",
"node-sass": "^4.8.2",
"nodemailer": "^4.6.3",
"node-sass": "^4.8.3",
"nodemailer": "^4.6.4",
"ora": "^2.0.0",
"pageres": "^4.1.1",
"passport": "^0.4.0",
@@ -75,17 +75,17 @@
"passport-google-oauth20": "1.0.0",
"paypal-ipn": "3.0.0",
"paypal-rest-sdk": "^1.8.1",
"popper.js": "^1.14.1",
"popper.js": "^1.14.3",
"postcss-easy-import": "^3.0.0",
"ps-tree": "^1.0.0",
"pug": "^2.0.1",
"pug": "^2.0.3",
"push-notify": "git://github.com/habitrpg/push-notify.git#6bc2b5fdb1bdc9649b9ec1964d79ca50187fc8a9",
"pusher": "^1.3.0",
"rimraf": "^2.4.3",
"sass-loader": "^6.0.7",
"sass-loader": "^7.0.0",
"shelljs": "^0.8.1",
"stackimpact": "^1.2.1",
"stripe": "^5.5.0",
"stackimpact": "^1.3.0",
"stripe": "^5.8.0",
"superagent": "^3.4.3",
"svg-inline-loader": "^0.8.0",
"svg-url-loader": "^2.3.2",
@@ -98,10 +98,10 @@
"validator": "^9.4.1",
"vinyl-buffer": "^1.0.1",
"vue": "^2.5.16",
"vue-loader": "^14.2.1",
"vue-loader": "^14.2.2",
"vue-mugen-scroll": "^0.2.1",
"vue-router": "^3.0.0",
"vue-style-loader": "^4.0.2",
"vue-style-loader": "^4.1.0",
"vue-template-compiler": "^2.5.16",
"vuedraggable": "^2.15.0",
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#5d237615463a84a23dd6f3f77c6ab577d68593ec",
@@ -141,22 +141,22 @@
"apidoc": "gulp apidoc"
},
"devDependencies": {
"@vue/test-utils": "^1.0.0-beta.12",
"@vue/test-utils": "^1.0.0-beta.13",
"babel-plugin-istanbul": "^4.1.6",
"babel-plugin-syntax-object-rest-spread": "^6.13.0",
"chai": "^4.1.2",
"chai-as-promised": "^7.1.1",
"chalk": "^2.3.2",
"chromedriver": "^2.36.0",
"chromedriver": "^2.37.0",
"connect-history-api-fallback": "^1.1.0",
"coveralls": "^3.0.0",
"cross-spawn": "^6.0.5",
"eslint": "^4.19.0",
"eslint": "^4.19.1",
"eslint-config-habitrpg": "^4.0.0",
"eslint-friendly-formatter": "^3.0.0",
"eslint-friendly-formatter": "^4.0.1",
"eslint-loader": "^2.0.0",
"eslint-plugin-html": "^4.0.2",
"eslint-plugin-mocha": "^4.12.1",
"eslint-plugin-html": "^4.0.3",
"eslint-plugin-mocha": "^5.0.0",
"eventsource-polyfill": "^0.9.6",
"expect.js": "^0.3.1",
"http-proxy-middleware": "^0.18.0",
@@ -168,24 +168,24 @@
"karma-coverage": "^1.1.1",
"karma-mocha": "^1.3.0",
"karma-mocha-reporter": "^2.2.5",
"karma-sinon-chai": "^1.3.3",
"karma-sinon-chai": "^1.3.4",
"karma-sinon-stub-promise": "^1.0.0",
"karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "0.0.32",
"karma-webpack": "^2.0.13",
"karma-webpack": "^3.0.0",
"lcov-result-merger": "^2.0.0",
"mocha": "^5.0.4",
"mocha": "^5.0.5",
"monk": "^6.0.5",
"nightwatch": "^0.9.20",
"puppeteer": "^1.2.0",
"puppeteer": "^1.3.0",
"require-again": "^2.0.0",
"selenium-server": "^3.11.0",
"sinon": "^4.4.5",
"sinon": "^4.5.0",
"sinon-chai": "^3.0.0",
"sinon-stub-promise": "^4.0.0",
"webpack-bundle-analyzer": "^2.11.1",
"webpack-dev-middleware": "^2.0.5",
"webpack-hot-middleware": "^2.21.2"
"webpack-hot-middleware": "^2.22.0"
},
"optionalDependencies": {
"node-rdkafka": "^2.3.0"
@@ -227,4 +227,61 @@ describe('GET challenges/user', () => {
expect(foundChallengeIndex).to.eql(1);
});
});
context('filters and paging', () => {
let user, guild, member;
const categories = [{
slug: 'newCat',
name: 'New Category',
}];
before(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: 'TestGuild',
type: 'guild',
privacy: 'public',
},
members: 1,
});
user = groupLeader;
guild = group;
member = members[0];
await user.update({balance: 20});
for (let i = 0; i < 11; i += 1) {
await generateChallenge(user, group); // eslint-disable-line
}
});
it('returns public guilds filtered by category', async () => {
const categoryChallenge = await generateChallenge(user, guild, {categories});
const challenges = await user.get(`/challenges/user?categories=${categories[0].slug}`);
expect(challenges[0]._id).to.eql(categoryChallenge._id);
expect(challenges.length).to.eql(1);
});
it('does not page challenges if page parameter is absent', async () => {
const challenges = await user.get('/challenges/user');
expect(challenges.length).to.be.above(11);
});
it('paginates challenges', async () => {
const challenges = await user.get('/challenges/user?page=0');
const challengesPaged = await user.get('/challenges/user?page=1&owned=owned');
expect(challenges.length).to.eql(10);
expect(challengesPaged.length).to.eql(2);
});
it('filters by owned', async () => {
const challenges = await member.get('/challenges/user?owned=owned');
expect(challenges.length).to.eql(0);
});
});
});
@@ -53,16 +53,26 @@ describe('DELETE /groups/:groupId/chat/:chatId', () => {
it('allows creator to delete a their message', async () => {
await user.del(`/groups/${groupWithChat._id}/chat/${nextMessage.id}`);
let messages = await user.get(`/groups/${groupWithChat._id}/chat/`);
expect(messages).is.an('array');
expect(messages).to.not.include(nextMessage);
const returnedMessages = await user.get(`/groups/${groupWithChat._id}/chat/`);
const messageFromUser = returnedMessages.find(returnedMessage => {
return returnedMessage.id === nextMessage.id;
});
expect(returnedMessages).is.an('array');
expect(messageFromUser).to.not.exist;
});
it('allows admin to delete another user\'s message', async () => {
await admin.del(`/groups/${groupWithChat._id}/chat/${nextMessage.id}`);
let messages = await user.get(`/groups/${groupWithChat._id}/chat/`);
expect(messages).is.an('array');
expect(messages).to.not.include(nextMessage);
const returnedMessages = await user.get(`/groups/${groupWithChat._id}/chat/`);
const messageFromUser = returnedMessages.find(returnedMessage => {
return returnedMessage.id === nextMessage.id;
});
expect(returnedMessages).is.an('array');
expect(messageFromUser).to.not.exist;
});
it('returns empty when previous message parameter is passed and the last message was deleted', async () => {
@@ -71,9 +81,9 @@ describe('DELETE /groups/:groupId/chat/:chatId', () => {
});
it('returns the update chat when previous message parameter is passed and the chat is updated', async () => {
let deleteResult = await user.del(`/groups/${groupWithChat._id}/chat/${nextMessage.id}?previousMsg=${message.id}`);
const updatedChat = await user.del(`/groups/${groupWithChat._id}/chat/${nextMessage.id}?previousMsg=${message.id}`);
expect(deleteResult[0].id).to.eql(message.id);
expect(updatedChat[0].id).to.eql(message.id);
});
});
});
@@ -23,14 +23,14 @@ describe('GET /groups/:groupId/chat', () => {
privacy: 'public',
}, {
chat: [
{text: 'Hello', flags: {}},
{text: 'Welcome to the Guild', flags: {}},
{text: 'Hello', flags: {}, id: 1},
{text: 'Welcome to the Guild', flags: {}, id: 2},
],
});
});
it('returns Guild chat', async () => {
let chat = await user.get(`/groups/${group._id}/chat`);
const chat = await user.get(`/groups/${group._id}/chat`);
expect(chat).to.eql(group.chat);
});
+35 -11
View File
@@ -11,7 +11,7 @@ import {
TAVERN_ID,
} from '../../../../../website/server/models/group';
import { v4 as generateUUID } from 'uuid';
import { getMatchesByWordArray, removePunctuationFromString } from '../../../../../website/server/libs/stringUtils';
import { getMatchesByWordArray } from '../../../../../website/server/libs/stringUtils';
import bannedWords from '../../../../../website/server/libs/bannedWords';
import guildsAllowingBannedWords from '../../../../../website/server/libs/guildsAllowingBannedWords';
import * as email from '../../../../../website/server/libs/email';
@@ -23,11 +23,11 @@ const BASE_URL = nconf.get('BASE_URL');
describe('POST /chat', () => {
let user, groupWithChat, member, additionalMember;
let testMessage = 'Test Message';
let testBannedWordMessage = 'TEST_PLACEHOLDER_SWEAR_WORD_HERE';
let testSlurMessage = 'message with TEST_PLACEHOLDER_SLUR_WORD_HERE';
let bannedWordErrorMessage = t('bannedWordUsed').split('.');
bannedWordErrorMessage[0] += ` (${removePunctuationFromString(testBannedWordMessage.toLowerCase())})`;
bannedWordErrorMessage = bannedWordErrorMessage.join('.');
let testBannedWordMessage = 'TESTPLACEHOLDERSWEARWORDHERE';
let testBannedWordMessage1 = 'TESTPLACEHOLDERSWEARWORDHERE1';
let testSlurMessage = 'message with TESTPLACEHOLDERSLURWORDHERE';
let testSlurMessage1 = 'TESTPLACEHOLDERSLURWORDHERE1';
let bannedWordErrorMessage = t('bannedWordUsed', {swearWordsUsed: testBannedWordMessage});
before(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
@@ -39,6 +39,7 @@ describe('POST /chat', () => {
members: 2,
});
user = groupLeader;
await user.update({'contributor.level': SPAM_MIN_EXEMPT_CONTRIB_LEVEL}); // prevent tests accidentally throwing messageGroupChatSpam
groupWithChat = group;
member = members[0];
additionalMember = members[1];
@@ -136,9 +137,19 @@ describe('POST /chat', () => {
});
});
it('checks error message has the banned words used', async () => {
let randIndex = Math.floor(Math.random() * (bannedWords.length + 1));
let testBannedWords = bannedWords.slice(randIndex, randIndex + 2).map((w) => w.replace(/\\/g, ''));
it('errors when word is typed in mixed case', async () => {
let substrLength = Math.floor(testBannedWordMessage.length / 2);
let chatMessage = testBannedWordMessage.substring(0, substrLength).toLowerCase() + testBannedWordMessage.substring(substrLength).toUpperCase();
await expect(user.post('/groups/habitrpg/chat', { message: chatMessage }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedWordUsed', {swearWordsUsed: chatMessage}),
});
});
it('checks error message has all the banned words used, regardless of case', async () => {
let testBannedWords = [testBannedWordMessage.toUpperCase(), testBannedWordMessage1.toLowerCase()];
let chatMessage = `Mixing ${testBannedWords[0]} and ${testBannedWords[1]} is bad for you.`;
await expect(user.post('/groups/habitrpg/chat', { message: chatMessage}))
.to.eventually.be.rejected
@@ -320,6 +331,17 @@ describe('POST /chat', () => {
members[0].flags.chatRevoked = false;
await members[0].update({'flags.chatRevoked': false});
});
it('errors when slur is typed in mixed case', async () => {
let substrLength = Math.floor(testSlurMessage1.length / 2);
let chatMessage = testSlurMessage1.substring(0, substrLength).toLowerCase() + testSlurMessage1.substring(substrLength).toUpperCase();
await expect(user.post('/groups/habitrpg/chat', { message: chatMessage }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedSlurUsed'),
});
});
});
it('does not error when sending a message to a private guild with a user with revoked chat', async () => {
@@ -359,9 +381,11 @@ describe('POST /chat', () => {
});
it('creates a chat', async () => {
let message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage});
const newMessage = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage});
const groupMessages = await user.get(`/groups/${groupWithChat._id}/chat`);
expect(message.message.id).to.exist;
expect(newMessage.message.id).to.exist;
expect(groupMessages[0].id).to.exist;
});
it('creates a chat with user styles', async () => {
@@ -72,7 +72,7 @@ describe('GET /groups/:groupId/members', () => {
expect(memberRes).to.have.all.keys([ // works as: object has all and only these keys
'_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party',
'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives',
'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives', 'flags',
]);
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
expect(Object.keys(memberRes.preferences).sort()).to.eql([
@@ -93,7 +93,7 @@ describe('GET /groups/:groupId/members', () => {
expect(memberRes).to.have.all.keys([ // works as: object has all and only these keys
'_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party',
'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives',
'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives', 'flags',
]);
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
expect(Object.keys(memberRes.preferences).sort()).to.eql([
@@ -136,6 +136,22 @@ describe('POST /group', () => {
},
});
});
it('returns an error when a user with no chat privileges attempts to create a public guild', async () => {
await user.update({ 'flags.chatRevoked': true });
await expect(
user.post('/groups', {
name: 'Test Public Guild',
type: 'guild',
privacy: 'public',
})
).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('cannotCreatePublicGuildWhenMuted'),
});
});
});
context('private guild', () => {
@@ -163,6 +179,17 @@ describe('POST /group', () => {
});
});
it('creates a private guild when the user has no chat privileges', async () => {
await user.update({ 'flags.chatRevoked': true });
let privateGuild = await user.post('/groups', {
name: groupName,
type: groupType,
privacy: groupPrivacy,
});
expect(privateGuild._id).to.exist;
});
it('deducts gems from user and adds them to guild bank', async () => {
let privateGuild = await user.post('/groups', {
name: groupName,
@@ -201,6 +228,16 @@ describe('POST /group', () => {
});
});
it('creates a party when the user has no chat privileges', async () => {
await user.update({ 'flags.chatRevoked': true });
let party = await user.post('/groups', {
name: partyName,
type: partyType,
});
expect(party._id).to.exist;
});
it('does not require gems to create a party', async () => {
await user.update({ balance: 0 });
@@ -11,7 +11,7 @@ import {
each,
} from 'lodash';
import { model as User } from '../../../../../website/server/models/user';
import * as payments from '../../../../../website/server/libs/payments';
import * as payments from '../../../../../website/server/libs/payments/payments';
describe('POST /groups/:groupId/leave', () => {
let typesOfGroups = {
@@ -24,6 +24,19 @@ describe('Post /groups/:groupId/invite', () => {
});
describe('user id invites', () => {
it('returns an error when inviter has no chat privileges', async () => {
let inviterMuted = await inviter.update({'flags.chatRevoked': true});
let userToInvite = await generateUser();
await expect(inviterMuted.post(`/groups/${group._id}/invite`, {
uuids: [userToInvite._id],
}))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('cannotInviteWhenMuted'),
});
});
it('returns an error when invited user is not found', async () => {
let fakeID = generateUUID();
@@ -160,6 +173,19 @@ describe('Post /groups/:groupId/invite', () => {
describe('email invites', () => {
let testInvite = {name: 'test', email: 'test@habitica.com'};
it('returns an error when inviter has no chat privileges', async () => {
let inviterMuted = await inviter.update({'flags.chatRevoked': true});
await expect(inviterMuted.post(`/groups/${group._id}/invite`, {
emails: [testInvite],
inviter: 'inviter name',
}))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('cannotInviteWhenMuted'),
});
});
it('returns an error when invite is missing an email', async () => {
await expect(inviter.post(`/groups/${group._id}/invite`, {
emails: [{name: 'test'}],
@@ -321,6 +347,19 @@ describe('Post /groups/:groupId/invite', () => {
});
describe('guild invites', () => {
it('returns an error when inviter has no chat privileges', async () => {
let inviterMuted = await inviter.update({'flags.chatRevoked': true});
let userToInvite = await generateUser();
await expect(inviterMuted.post(`/groups/${group._id}/invite`, {
uuids: [userToInvite._id],
}))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('cannotInviteWhenMuted'),
});
});
it('returns an error when invited user is already invited to the group', async () => {
let userToInvite = await generateUser();
await inviter.post(`/groups/${group._id}/invite`, {
@@ -398,6 +437,19 @@ describe('Post /groups/:groupId/invite', () => {
});
});
it('returns an error when inviter has no chat privileges', async () => {
let inviterMuted = await inviter.update({'flags.chatRevoked': true});
let userToInvite = await generateUser();
await expect(inviterMuted.post(`/groups/${party._id}/invite`, {
uuids: [userToInvite._id],
}))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('cannotInviteWhenMuted'),
});
});
it('returns an error when invited user has a pending invitation to the party', async () => {
let userToInvite = await generateUser();
await inviter.post(`/groups/${party._id}/invite`, {
@@ -32,7 +32,7 @@ describe('GET /members/:memberId', () => {
let memberRes = await user.get(`/members/${member._id}`);
expect(memberRes).to.have.all.keys([ // works as: object has all and only these keys
'_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party',
'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives',
'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives', 'flags',
]);
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
expect(Object.keys(memberRes.preferences).sort()).to.eql([
@@ -3,7 +3,7 @@ import {
generateGroup,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import amzLib from '../../../../../../website/server/libs/amazonPayments';
import amzLib from '../../../../../../website/server/libs/payments/amazon';
describe('payments : amazon #subscribeCancel', () => {
let endpoint = '/amazon/subscribe/cancel?noRedirect=true';
@@ -1,7 +1,7 @@
import {
generateUser,
} from '../../../../../helpers/api-integration/v3';
import amzLib from '../../../../../../website/server/libs/amazonPayments';
import amzLib from '../../../../../../website/server/libs/payments/amazon';
describe('payments - amazon - #checkout', () => {
let endpoint = '/amazon/checkout';
@@ -3,7 +3,7 @@ import {
generateGroup,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import amzLib from '../../../../../../website/server/libs/amazonPayments';
import amzLib from '../../../../../../website/server/libs/payments/amazon';
describe('payments - amazon - #subscribe', () => {
let endpoint = '/amazon/subscribe';
@@ -1,5 +1,5 @@
import {generateUser} from '../../../../../helpers/api-integration/v3';
import applePayments from '../../../../../../website/server/libs/applePayments';
import applePayments from '../../../../../../website/server/libs/payments/apple';
describe('payments : apple #cancelSubscribe', () => {
let endpoint = '/iap/ios/subscribe/cancel?noRedirect=true';
@@ -1,5 +1,5 @@
import {generateUser} from '../../../../../helpers/api-integration/v3';
import applePayments from '../../../../../../website/server/libs/applePayments';
import applePayments from '../../../../../../website/server/libs/payments/apple';
describe('payments : apple #verify', () => {
let endpoint = '/iap/ios/verify';
@@ -1,5 +1,5 @@
import {generateUser, translate as t} from '../../../../../helpers/api-integration/v3';
import applePayments from '../../../../../../website/server/libs/applePayments';
import applePayments from '../../../../../../website/server/libs/payments/apple';
describe('payments : apple #subscribe', () => {
let endpoint = '/iap/ios/subscribe';
@@ -1,5 +1,5 @@
import {generateUser} from '../../../../../helpers/api-integration/v3';
import googlePayments from '../../../../../../website/server/libs/googlePayments';
import googlePayments from '../../../../../../website/server/libs/payments/google';
describe('payments : google #cancelSubscribe', () => {
let endpoint = '/iap/android/subscribe/cancel?noRedirect=true';
@@ -1,5 +1,5 @@
import {generateUser, translate as t} from '../../../../../helpers/api-integration/v3';
import googlePayments from '../../../../../../website/server/libs/googlePayments';
import googlePayments from '../../../../../../website/server/libs/payments/google';
describe('payments : google #subscribe', () => {
let endpoint = '/iap/android/subscribe';
@@ -1,5 +1,5 @@
import {generateUser} from '../../../../../helpers/api-integration/v3';
import googlePayments from '../../../../../../website/server/libs/googlePayments';
import googlePayments from '../../../../../../website/server/libs/payments/google';
describe('payments : google #verify', () => {
let endpoint = '/iap/android/verify';
@@ -1,7 +1,7 @@
import {
generateUser,
} from '../../../../../helpers/api-integration/v3';
import paypalPayments from '../../../../../../website/server/libs/paypalPayments';
import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
describe('payments : paypal #checkout', () => {
let endpoint = '/paypal/checkout';
@@ -2,7 +2,7 @@ import {
generateUser,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import paypalPayments from '../../../../../../website/server/libs/paypalPayments';
import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
describe('payments : paypal #checkoutSuccess', () => {
let endpoint = '/paypal/checkout/success';
@@ -2,7 +2,7 @@ import {
generateUser,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import paypalPayments from '../../../../../../website/server/libs/paypalPayments';
import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
import shared from '../../../../../../website/common';
describe('payments : paypal #subscribe', () => {
@@ -2,7 +2,7 @@ import {
generateUser,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import paypalPayments from '../../../../../../website/server/libs/paypalPayments';
import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
describe('payments : paypal #subscribeCancel', () => {
let endpoint = '/paypal/subscribe/cancel';
@@ -2,7 +2,7 @@ import {
generateUser,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import paypalPayments from '../../../../../../website/server/libs/paypalPayments';
import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
describe('payments : paypal #subscribeSuccess', () => {
let endpoint = '/paypal/subscribe/success';
@@ -1,7 +1,7 @@
import {
generateUser,
} from '../../../../../helpers/api-integration/v3';
import paypalPayments from '../../../../../../website/server/libs/paypalPayments';
import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
describe('payments - paypal - #ipn', () => {
let endpoint = '/paypal/ipn';
@@ -3,7 +3,7 @@ import {
generateGroup,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import stripePayments from '../../../../../../website/server/libs/stripePayments';
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
describe('payments - stripe - #subscribeCancel', () => {
let endpoint = '/stripe/subscribe/cancel?redirect=none';
@@ -2,7 +2,7 @@ import {
generateUser,
generateGroup,
} from '../../../../../helpers/api-integration/v3';
import stripePayments from '../../../../../../website/server/libs/stripePayments';
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
describe('payments - stripe - #checkout', () => {
let endpoint = '/stripe/checkout';
@@ -3,7 +3,7 @@ import {
generateGroup,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import stripePayments from '../../../../../../website/server/libs/stripePayments';
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
describe('payments - stripe - #subscribeEdit', () => {
let endpoint = '/stripe/subscribe/edit';
@@ -4,6 +4,7 @@ import {
generateUser,
sleep,
} from '../../../../helpers/api-v3-integration.helper';
import { model as Chat } from '../../../../../website/server/models/chat';
describe('POST /groups/:groupId/quests/accept', () => {
const PET_QUEST = 'whale';
@@ -155,10 +156,11 @@ describe('POST /groups/:groupId/quests/accept', () => {
// quest will start after everyone has accepted
await partyMembers[1].post(`/groups/${questingGroup._id}/quests/accept`);
await questingGroup.sync();
expect(questingGroup.chat[0].text).to.exist;
expect(questingGroup.chat[0]._meta).to.exist;
expect(questingGroup.chat[0]._meta).to.have.all.keys(['participatingMembers']);
const groupChat = await Chat.find({ groupId: questingGroup._id }).exec();
expect(groupChat[0].text).to.exist;
expect(groupChat[0]._meta).to.exist;
expect(groupChat[0]._meta).to.have.all.keys(['participatingMembers']);
let returnedGroup = await leader.get(`/groups/${questingGroup._id}`);
expect(returnedGroup.chat[0]._meta).to.be.undefined;
@@ -4,6 +4,7 @@ import {
generateUser,
sleep,
} from '../../../../helpers/api-v3-integration.helper';
import { model as Chat } from '../../../../../website/server/models/chat';
describe('POST /groups/:groupId/quests/force-start', () => {
const PET_QUEST = 'whale';
@@ -241,11 +242,13 @@ describe('POST /groups/:groupId/quests/force-start', () => {
await questingGroup.sync();
expect(questingGroup.chat[0].text).to.exist;
expect(questingGroup.chat[0]._meta).to.exist;
expect(questingGroup.chat[0]._meta).to.have.all.keys(['participatingMembers']);
const groupChat = await Chat.find({ groupId: questingGroup._id }).exec();
let returnedGroup = await leader.get(`/groups/${questingGroup._id}`);
expect(groupChat[0].text).to.exist;
expect(groupChat[0]._meta).to.exist;
expect(groupChat[0]._meta).to.have.all.keys(['participatingMembers']);
const returnedGroup = await leader.get(`/groups/${questingGroup._id}`);
expect(returnedGroup.chat[0]._meta).to.be.undefined;
});
});
@@ -5,6 +5,7 @@ import {
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
import { quests as questScrolls } from '../../../../../website/common/script/content';
import { model as Chat } from '../../../../../website/server/models/chat';
describe('POST /groups/:groupId/quests/invite/:questKey', () => {
let questingGroup;
@@ -199,11 +200,11 @@ describe('POST /groups/:groupId/quests/invite/:questKey', () => {
await groupLeader.post(`/groups/${group._id}/quests/invite/${PET_QUEST}`);
await group.sync();
const groupChat = await Chat.find({ groupId: group._id }).exec();
expect(group.chat[0].text).to.exist;
expect(group.chat[0]._meta).to.exist;
expect(group.chat[0]._meta).to.have.all.keys(['participatingMembers']);
expect(groupChat[0].text).to.exist;
expect(groupChat[0]._meta).to.exist;
expect(groupChat[0]._meta).to.have.all.keys(['participatingMembers']);
let returnedGroup = await groupLeader.get(`/groups/${group._id}`);
expect(returnedGroup.chat[0]._meta).to.be.undefined;
@@ -90,7 +90,7 @@ describe('POST /groups/:groupId/quests/abort', () => {
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
await partyMembers[1].post(`/groups/${questingGroup._id}/quests/accept`);
let stub = sandbox.stub(Group.prototype, 'sendChat');
let stub = sandbox.spy(Group.prototype, 'sendChat');
let res = await leader.post(`/groups/${questingGroup._id}/quests/abort`);
await Promise.all([
@@ -5,6 +5,7 @@ import {
sleep,
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
import { model as Chat } from '../../../../../website/server/models/chat';
describe('POST /groups/:groupId/quests/reject', () => {
let questingGroup;
@@ -185,11 +186,12 @@ describe('POST /groups/:groupId/quests/reject', () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
await partyMembers[1].post(`/groups/${questingGroup._id}/quests/reject`);
await questingGroup.sync();
expect(questingGroup.chat[0].text).to.exist;
expect(questingGroup.chat[0]._meta).to.exist;
expect(questingGroup.chat[0]._meta).to.have.all.keys(['participatingMembers']);
const groupChat = await Chat.find({ groupId: questingGroup._id }).exec();
expect(groupChat[0].text).to.exist;
expect(groupChat[0]._meta).to.exist;
expect(groupChat[0]._meta).to.have.all.keys(['participatingMembers']);
let returnedGroup = await leader.get(`/groups/${questingGroup._id}`);
expect(returnedGroup.chat[0]._meta).to.be.undefined;
@@ -296,6 +296,16 @@ describe('PUT /tasks/:id', () => {
expect(fetchedDaily.text).to.eql('saved');
});
// This is a special case for iOS requests
it('will round a priority (difficulty)', async () => {
daily = await user.put(`/tasks/${daily._id}`, {
alias: 'alias',
priority: 0.10000000000005,
});
expect(daily.priority).to.eql(0.1);
});
});
context('habits', () => {
@@ -34,6 +34,8 @@ describe('GET /user', () => {
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;
});
});
@@ -0,0 +1,148 @@
import {
generateUser,
} from '../../../../helpers/api-integration/v3';
import getOfficialPinnedItems from '../../../../../website/common/script/libs/getOfficialPinnedItems.js';
describe('POST /user/move-pinned-item/:path/move/to/:position', () => {
let user;
let officialPinnedItems;
let officialPinnedItemPaths;
beforeEach(async () => {
user = await generateUser();
officialPinnedItems = getOfficialPinnedItems(user);
officialPinnedItemPaths = [];
// officialPinnedItems are returned in { type: ..., path:... } format but we just need the paths for testPinnedItemsOrder
if (officialPinnedItems.length > 0) {
officialPinnedItemPaths = officialPinnedItems.map(item => item.path);
}
});
it('adjusts the order of pinned items with no order mismatch', async () => {
let testPinnedItems = [
{ type: 'armoire', path: 'armoire' },
{ type: 'potion', path: 'potion' },
{ type: 'marketGear', path: 'gear.flat.weapon_warrior_1' },
{ type: 'marketGear', path: 'gear.flat.head_warrior_1' },
{ type: 'marketGear', path: 'gear.flat.armor_warrior_1' },
{ type: 'hatchingPotions', path: 'hatchingPotions.Golden' },
{ type: 'marketGear', path: 'gear.flat.shield_warrior_1' },
{ type: 'card', path: 'cardTypes.greeting' },
{ type: 'potion', path: 'hatchingPotions.Golden' },
{ type: 'card', path: 'cardTypes.thankyou' },
{ type: 'food', path: 'food.Saddle' },
];
let testPinnedItemsOrder = [
'hatchingPotions.Golden',
'cardTypes.greeting',
'armoire',
'gear.flat.weapon_warrior_1',
'gear.flat.head_warrior_1',
'cardTypes.thankyou',
'gear.flat.armor_warrior_1',
'food.Saddle',
'gear.flat.shield_warrior_1',
'potion',
];
// For this test put seasonal items at the end so they stay out of the way
testPinnedItemsOrder = testPinnedItemsOrder.concat(officialPinnedItemPaths);
await user.update({
pinnedItems: testPinnedItems,
pinnedItemsOrder: testPinnedItemsOrder,
});
let res = await user.post('/user/move-pinned-item/armoire/move/to/5');
await user.sync();
expect(user.pinnedItemsOrder[5]).to.equal('armoire');
expect(user.pinnedItemsOrder[2]).to.equal('gear.flat.weapon_warrior_1');
// We have done nothing to change pinnedItems!
expect(user.pinnedItems).to.deep.equal(testPinnedItems);
let expectedResponse = [
'hatchingPotions.Golden',
'cardTypes.greeting',
'gear.flat.weapon_warrior_1',
'gear.flat.head_warrior_1',
'cardTypes.thankyou',
'armoire',
'gear.flat.armor_warrior_1',
'food.Saddle',
'gear.flat.shield_warrior_1',
'potion',
];
expectedResponse = expectedResponse.concat(officialPinnedItemPaths);
expect(res).to.eql(expectedResponse);
});
it('adjusts the order of pinned items with order mismatch', async () => {
let testPinnedItems = [
{ type: 'card', path: 'cardTypes.thankyou' },
{ type: 'card', path: 'cardTypes.greeting' },
{ type: 'potion', path: 'potion' },
{ type: 'armoire', path: 'armoire' },
];
let testPinnedItemsOrder = [
'armoire',
'potion',
];
await user.update({
pinnedItems: testPinnedItems,
pinnedItemsOrder: testPinnedItemsOrder,
});
let res = await user.post('/user/move-pinned-item/armoire/move/to/1');
await user.sync();
// The basic test
expect(user.pinnedItemsOrder[1]).to.equal('armoire');
// potion is now the last item because the 2 unacounted for cards show up
// at the beginning of the order
expect(user.pinnedItemsOrder[user.pinnedItemsOrder.length - 1]).to.equal('potion');
let expectedResponse = [
'cardTypes.thankyou',
'cardTypes.greeting',
'potion',
];
// inAppRewards is used here and will by default put these seasonal items in the front like this:
expectedResponse = officialPinnedItemPaths.concat(expectedResponse);
// now put "armoire" in where we moved it:
expectedResponse.splice(1, 0, 'armoire');
expect(res).to.eql(expectedResponse);
});
it('cannot move pinned item that you do not have pinned', async () => {
let testPinnedItems = [
{ type: 'potion', path: 'potion' },
{ type: 'armoire', path: 'armoire' },
];
let testPinnedItemsOrder = [
'armoire',
'potion',
];
await user.update({
pinnedItems: testPinnedItems,
pinnedItemsOrder: testPinnedItemsOrder,
});
try {
await user.post('/user/move-pinned-item/cardTypes.thankyou/move/to/1');
} catch (err) {
expect(err).to.exist;
}
});
});
@@ -180,11 +180,42 @@ describe('POST /user/class/cast/:spellId', () => {
members: 1,
});
await groupLeader.update({'stats.mp': 200, 'stats.class': 'wizard', 'stats.lvl': 13});
await groupLeader.post('/user/class/cast/earth');
await sleep(1);
await group.sync();
expect(group.chat[0]).to.exist;
expect(group.chat[0].uuid).to.equal('system');
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 () => {
@@ -197,7 +228,7 @@ describe('POST /user/class/cast/:spellId', () => {
await groupLeader.post('/user/class/cast/earth', {quantity: 2});
await sleep(1);
await group.sync();
group = await groupLeader.get(`/groups/${group._id}`);
expect(group.chat[0]).to.exist;
expect(group.chat[0].uuid).to.equal('system');
+462 -33
View File
@@ -23,7 +23,7 @@ describe('cron', () => {
local: {
username: 'username',
lowerCaseUsername: 'username',
email: 'email@email.email',
email: 'email@example.com',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},
@@ -82,7 +82,7 @@ describe('cron', () => {
});
it('does not reset plan.gemsBought within the month', () => {
let clock = sinon.useFakeTimers(moment().startOf('month').add(2, 'days').unix());
let clock = sinon.useFakeTimers(moment().startOf('month').add(2, 'days').toDate());
user.purchased.plan.dateUpdated = moment().startOf('month').toDate();
user.purchased.plan.gemsBought = 10;
@@ -117,21 +117,6 @@ describe('cron', () => {
expect(user.purchased.plan.consecutive.offset).to.equal(1);
});
it('increments plan.consecutive.trinkets when user has reached a month that is a multiple of 3', () => {
user.purchased.plan.consecutive.count = 5;
user.purchased.plan.consecutive.offset = 1;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user.purchased.plan.consecutive.offset).to.equal(0);
});
it('increments plan.consecutive.trinkets multiple times if user has been absent with continuous subscription', () => {
user.purchased.plan.dateUpdated = moment().subtract(6, 'months').toDate();
user.purchased.plan.consecutive.count = 5;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.trinkets).to.equal(2);
});
it('does not award unearned plan.consecutive.trinkets if subscription ended during an absence', () => {
user.purchased.plan.dateUpdated = moment().subtract(6, 'months').toDate();
user.purchased.plan.dateTerminated = moment().subtract(3, 'months').toDate();
@@ -143,21 +128,6 @@ describe('cron', () => {
expect(user.purchased.plan.consecutive.trinkets).to.equal(1);
});
it('increments plan.consecutive.gemCapExtra when user has reached a month that is a multiple of 3', () => {
user.purchased.plan.consecutive.count = 5;
user.purchased.plan.consecutive.offset = 1;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(5);
expect(user.purchased.plan.consecutive.offset).to.equal(0);
});
it('increments plan.consecutive.gemCapExtra multiple times if user has been absent with continuous subscription', () => {
user.purchased.plan.dateUpdated = moment().subtract(6, 'months').toDate();
user.purchased.plan.consecutive.count = 5;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(10);
});
it('does not increment plan.consecutive.gemCapExtra when user has reached the gemCap limit', () => {
user.purchased.plan.consecutive.gemCapExtra = 25;
user.purchased.plan.consecutive.count = 5;
@@ -184,6 +154,465 @@ describe('cron', () => {
expect(user.purchased.plan.consecutive.count).to.equal(0);
expect(user.purchased.plan.consecutive.offset).to.equal(0);
});
describe('for a 1-month recurring subscription', () => {
let clock;
// create a user that will be used for all of these tests without a reset before each
let user1 = new User({
auth: {
local: {
username: 'username1',
lowerCaseUsername: 'username1',
email: 'email1@example.com',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},
},
});
// user1 has a 1-month recurring subscription starting today
user1.purchased.plan.customerId = 'subscribedId';
user1.purchased.plan.dateUpdated = moment().toDate();
user1.purchased.plan.planId = 'basic';
user1.purchased.plan.consecutive.count = 0;
user1.purchased.plan.consecutive.offset = 0;
user1.purchased.plan.consecutive.trinkets = 0;
user1.purchased.plan.consecutive.gemCapExtra = 0;
it('does not increment consecutive benefits after the first month', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months').add(2, 'days').toDate());
// Add 1 month to simulate what happens a month after the subscription was created.
// Add 2 days so that we're sure we're not affected by any start-of-month effects e.g., from time zone oddness.
cron({user: user1, tasksByType, daysMissed, analytics});
expect(user1.purchased.plan.consecutive.count).to.equal(1);
expect(user1.purchased.plan.consecutive.offset).to.equal(0);
expect(user1.purchased.plan.consecutive.trinkets).to.equal(0);
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(0);
clock.restore();
});
it('does not increment consecutive benefits after the second month', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months').add(2, 'days').toDate());
// Add 1 month to simulate what happens a month after the subscription was created.
// Add 2 days so that we're sure we're not affected by any start-of-month effects e.g., from time zone oddness.
cron({user: user1, tasksByType, daysMissed, analytics});
expect(user1.purchased.plan.consecutive.count).to.equal(2);
expect(user1.purchased.plan.consecutive.offset).to.equal(0);
expect(user1.purchased.plan.consecutive.trinkets).to.equal(0);
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(0);
clock.restore();
});
it('increments consecutive benefits after the third month', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months').add(2, 'days').toDate());
// Add 1 month to simulate what happens a month after the subscription was created.
// Add 2 days so that we're sure we're not affected by any start-of-month effects e.g., from time zone oddness.
cron({user: user1, tasksByType, daysMissed, analytics});
expect(user1.purchased.plan.consecutive.count).to.equal(3);
expect(user1.purchased.plan.consecutive.offset).to.equal(0);
expect(user1.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(5);
clock.restore();
});
it('does not increment consecutive benefits after the fourth month', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(4, 'months').add(2, 'days').toDate());
// Add 1 month to simulate what happens a month after the subscription was created.
// Add 2 days so that we're sure we're not affected by any start-of-month effects e.g., from time zone oddness.
cron({user: user1, tasksByType, daysMissed, analytics});
expect(user1.purchased.plan.consecutive.count).to.equal(4);
expect(user1.purchased.plan.consecutive.offset).to.equal(0);
expect(user1.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(5);
clock.restore();
});
it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(10, 'months').add(2, 'days').toDate());
cron({user: user1, tasksByType, daysMissed, analytics});
expect(user1.purchased.plan.consecutive.count).to.equal(10);
expect(user1.purchased.plan.consecutive.offset).to.equal(0);
expect(user1.purchased.plan.consecutive.trinkets).to.equal(3);
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(15);
clock.restore();
});
});
describe('for a 3-month recurring subscription', () => {
let clock;
let user3 = new User({
auth: {
local: {
username: 'username3',
lowerCaseUsername: 'username3',
email: 'email3@example.com',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},
},
});
// user3 has a 3-month recurring subscription starting today
user3.purchased.plan.customerId = 'subscribedId';
user3.purchased.plan.dateUpdated = moment().toDate();
user3.purchased.plan.planId = 'basic_3mo';
user3.purchased.plan.consecutive.count = 0;
user3.purchased.plan.consecutive.offset = 3;
user3.purchased.plan.consecutive.trinkets = 1;
user3.purchased.plan.consecutive.gemCapExtra = 5;
it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months').add(2, 'days').toDate());
cron({user: user3, tasksByType, daysMissed, analytics});
expect(user3.purchased.plan.consecutive.count).to.equal(1);
expect(user3.purchased.plan.consecutive.offset).to.equal(2);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(5);
clock.restore();
});
it('does not increment consecutive benefits in the middle of the period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months').add(2, 'days').toDate());
cron({user: user3, tasksByType, daysMissed, analytics});
expect(user3.purchased.plan.consecutive.count).to.equal(2);
expect(user3.purchased.plan.consecutive.offset).to.equal(1);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(5);
clock.restore();
});
it('does not increment consecutive benefits in the final month of the period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months').add(2, 'days').toDate());
cron({user: user3, tasksByType, daysMissed, analytics});
expect(user3.purchased.plan.consecutive.count).to.equal(3);
expect(user3.purchased.plan.consecutive.offset).to.equal(0);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(5);
clock.restore();
});
it('increments consecutive benefits the month after the second paid period has started', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(4, 'months').add(2, 'days').toDate());
cron({user: user3, tasksByType, daysMissed, analytics});
expect(user3.purchased.plan.consecutive.count).to.equal(4);
expect(user3.purchased.plan.consecutive.offset).to.equal(2);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(2);
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(10);
clock.restore();
});
it('does not increment consecutive benefits in the second month of the second period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(5, 'months').add(2, 'days').toDate());
cron({user: user3, tasksByType, daysMissed, analytics});
expect(user3.purchased.plan.consecutive.count).to.equal(5);
expect(user3.purchased.plan.consecutive.offset).to.equal(1);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(2);
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(10);
clock.restore();
});
it('does not increment consecutive benefits in the final month of the second period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(6, 'months').add(2, 'days').toDate());
cron({user: user3, tasksByType, daysMissed, analytics});
expect(user3.purchased.plan.consecutive.count).to.equal(6);
expect(user3.purchased.plan.consecutive.offset).to.equal(0);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(2);
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(10);
clock.restore();
});
it('increments consecutive benefits the month after the third paid period has started', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(7, 'months').add(2, 'days').toDate());
cron({user: user3, tasksByType, daysMissed, analytics});
expect(user3.purchased.plan.consecutive.count).to.equal(7);
expect(user3.purchased.plan.consecutive.offset).to.equal(2);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(3);
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(15);
clock.restore();
});
it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(10, 'months').add(2, 'days').toDate());
cron({user: user3, tasksByType, daysMissed, analytics});
expect(user3.purchased.plan.consecutive.count).to.equal(10);
expect(user3.purchased.plan.consecutive.offset).to.equal(2);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(4);
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(20);
clock.restore();
});
});
describe('for a 6-month recurring subscription', () => {
let clock;
let user6 = new User({
auth: {
local: {
username: 'username6',
lowerCaseUsername: 'username6',
email: 'email6@example.com',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},
},
});
// user6 has a 6-month recurring subscription starting today
user6.purchased.plan.customerId = 'subscribedId';
user6.purchased.plan.dateUpdated = moment().toDate();
user6.purchased.plan.planId = 'google_6mo';
user6.purchased.plan.consecutive.count = 0;
user6.purchased.plan.consecutive.offset = 6;
user6.purchased.plan.consecutive.trinkets = 2;
user6.purchased.plan.consecutive.gemCapExtra = 10;
it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months').add(2, 'days').toDate());
cron({user: user6, tasksByType, daysMissed, analytics});
expect(user6.purchased.plan.consecutive.count).to.equal(1);
expect(user6.purchased.plan.consecutive.offset).to.equal(5);
expect(user6.purchased.plan.consecutive.trinkets).to.equal(2);
expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(10);
clock.restore();
});
it('does not increment consecutive benefits in the final month of the period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(6, 'months').add(2, 'days').toDate());
cron({user: user6, tasksByType, daysMissed, analytics});
expect(user6.purchased.plan.consecutive.count).to.equal(6);
expect(user6.purchased.plan.consecutive.offset).to.equal(0);
expect(user6.purchased.plan.consecutive.trinkets).to.equal(2);
expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(10);
clock.restore();
});
it('increments consecutive benefits the month after the second paid period has started', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(7, 'months').add(2, 'days').toDate());
cron({user: user6, tasksByType, daysMissed, analytics});
expect(user6.purchased.plan.consecutive.count).to.equal(7);
expect(user6.purchased.plan.consecutive.offset).to.equal(5);
expect(user6.purchased.plan.consecutive.trinkets).to.equal(4);
expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(20);
clock.restore();
});
it('increments consecutive benefits the month after the third paid period has started', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(13, 'months').add(2, 'days').toDate());
cron({user: user6, tasksByType, daysMissed, analytics});
expect(user6.purchased.plan.consecutive.count).to.equal(13);
expect(user6.purchased.plan.consecutive.offset).to.equal(5);
expect(user6.purchased.plan.consecutive.trinkets).to.equal(6);
expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(25);
clock.restore();
});
it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(19, 'months').add(2, 'days').toDate());
cron({user: user6, tasksByType, daysMissed, analytics});
expect(user6.purchased.plan.consecutive.count).to.equal(19);
expect(user6.purchased.plan.consecutive.offset).to.equal(5);
expect(user6.purchased.plan.consecutive.trinkets).to.equal(8);
expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(25);
clock.restore();
});
});
describe('for a 12-month recurring subscription', () => {
let clock;
let user12 = new User({
auth: {
local: {
username: 'username12',
lowerCaseUsername: 'username12',
email: 'email12@example.com',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},
},
});
// user12 has a 12-month recurring subscription starting today
user12.purchased.plan.customerId = 'subscribedId';
user12.purchased.plan.dateUpdated = moment().toDate();
user12.purchased.plan.planId = 'basic_12mo';
user12.purchased.plan.consecutive.count = 0;
user12.purchased.plan.consecutive.offset = 12;
user12.purchased.plan.consecutive.trinkets = 4;
user12.purchased.plan.consecutive.gemCapExtra = 20;
it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months').add(2, 'days').toDate());
cron({user: user12, tasksByType, daysMissed, analytics});
expect(user12.purchased.plan.consecutive.count).to.equal(1);
expect(user12.purchased.plan.consecutive.offset).to.equal(11);
expect(user12.purchased.plan.consecutive.trinkets).to.equal(4);
expect(user12.purchased.plan.consecutive.gemCapExtra).to.equal(20);
clock.restore();
});
it('does not increment consecutive benefits in the final month of the period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(12, 'months').add(2, 'days').toDate());
cron({user: user12, tasksByType, daysMissed, analytics});
expect(user12.purchased.plan.consecutive.count).to.equal(12);
expect(user12.purchased.plan.consecutive.offset).to.equal(0);
expect(user12.purchased.plan.consecutive.trinkets).to.equal(4);
expect(user12.purchased.plan.consecutive.gemCapExtra).to.equal(20);
clock.restore();
});
it('increments consecutive benefits the month after the second paid period has started', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(13, 'months').add(2, 'days').toDate());
cron({user: user12, tasksByType, daysMissed, analytics});
expect(user12.purchased.plan.consecutive.count).to.equal(13);
expect(user12.purchased.plan.consecutive.offset).to.equal(11);
expect(user12.purchased.plan.consecutive.trinkets).to.equal(8);
expect(user12.purchased.plan.consecutive.gemCapExtra).to.equal(25);
clock.restore();
});
it('increments consecutive benefits the month after the third paid period has started', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(25, 'months').add(2, 'days').toDate());
cron({user: user12, tasksByType, daysMissed, analytics});
expect(user12.purchased.plan.consecutive.count).to.equal(25);
expect(user12.purchased.plan.consecutive.offset).to.equal(11);
expect(user12.purchased.plan.consecutive.trinkets).to.equal(12);
expect(user12.purchased.plan.consecutive.gemCapExtra).to.equal(25);
clock.restore();
});
it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(37, 'months').add(2, 'days').toDate());
cron({user: user12, tasksByType, daysMissed, analytics});
expect(user12.purchased.plan.consecutive.count).to.equal(37);
expect(user12.purchased.plan.consecutive.offset).to.equal(11);
expect(user12.purchased.plan.consecutive.trinkets).to.equal(16);
expect(user12.purchased.plan.consecutive.gemCapExtra).to.equal(25);
clock.restore();
});
});
describe('for a 3-month gift subscription (non-recurring)', () => {
let clock;
let user3g = new User({
auth: {
local: {
username: 'username3g',
lowerCaseUsername: 'username3g',
email: 'email3g@example.com',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},
},
});
// user3g has a 3-month gift subscription starting today
user3g.purchased.plan.customerId = 'Gift';
user3g.purchased.plan.dateUpdated = moment().toDate();
user3g.purchased.plan.dateTerminated = moment().add(3, 'months').toDate();
user3g.purchased.plan.planId = null;
user3g.purchased.plan.consecutive.count = 0;
user3g.purchased.plan.consecutive.offset = 3;
user3g.purchased.plan.consecutive.trinkets = 1;
user3g.purchased.plan.consecutive.gemCapExtra = 5;
it('does not increment consecutive benefits in the first month of the gift subscription', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months').add(2, 'days').toDate());
cron({user: user3g, tasksByType, daysMissed, analytics});
expect(user3g.purchased.plan.consecutive.count).to.equal(1);
expect(user3g.purchased.plan.consecutive.offset).to.equal(2);
expect(user3g.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user3g.purchased.plan.consecutive.gemCapExtra).to.equal(5);
clock.restore();
});
it('does not increment consecutive benefits in the second month of the gift subscription', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months').add(2, 'days').toDate());
cron({user: user3g, tasksByType, daysMissed, analytics});
expect(user3g.purchased.plan.consecutive.count).to.equal(2);
expect(user3g.purchased.plan.consecutive.offset).to.equal(1);
expect(user3g.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user3g.purchased.plan.consecutive.gemCapExtra).to.equal(5);
clock.restore();
});
it('does not increment consecutive benefits in the third month of the gift subscription', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months').add(2, 'days').toDate());
cron({user: user3g, tasksByType, daysMissed, analytics});
expect(user3g.purchased.plan.consecutive.count).to.equal(3);
expect(user3g.purchased.plan.consecutive.offset).to.equal(0);
expect(user3g.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user3g.purchased.plan.consecutive.gemCapExtra).to.equal(5);
clock.restore();
});
it('does not increment consecutive benefits in the month after the gift subscription has ended', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(4, 'months').add(2, 'days').toDate());
cron({user: user3g, tasksByType, daysMissed, analytics});
expect(user3g.purchased.plan.consecutive.count).to.equal(0); // subscription has been erased by now
expect(user3g.purchased.plan.consecutive.offset).to.equal(0);
expect(user3g.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user3g.purchased.plan.consecutive.gemCapExtra).to.equal(0); // erased
clock.restore();
});
});
describe('for a 6-month recurring subscription where the user has incorrect consecutive month data from prior bugs', () => {
let clock;
let user6x = new User({
auth: {
local: {
username: 'username6x',
lowerCaseUsername: 'username6x',
email: 'email6x@example.com',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},
},
});
// user6x has a 6-month recurring subscription starting 8 months in the past before issue #4819 was fixed
user6x.purchased.plan.customerId = 'subscribedId';
user6x.purchased.plan.dateUpdated = moment().toDate();
user6x.purchased.plan.planId = 'basic_6mo';
user6x.purchased.plan.consecutive.count = 8;
user6x.purchased.plan.consecutive.offset = 0;
user6x.purchased.plan.consecutive.trinkets = 3;
user6x.purchased.plan.consecutive.gemCapExtra = 15;
it('increments consecutive benefits in the first month since the fix for #4819 goes live', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months').add(2, 'days').toDate());
cron({user: user6x, tasksByType, daysMissed, analytics});
expect(user6x.purchased.plan.consecutive.count).to.equal(9);
expect(user6x.purchased.plan.consecutive.offset).to.equal(5);
expect(user6x.purchased.plan.consecutive.trinkets).to.equal(5);
expect(user6x.purchased.plan.consecutive.gemCapExtra).to.equal(25);
clock.restore();
});
it('does not increment consecutive benefits in the second month after the fix goes live', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months').add(2, 'days').toDate());
cron({user: user6x, tasksByType, daysMissed, analytics});
expect(user6x.purchased.plan.consecutive.count).to.equal(10);
expect(user6x.purchased.plan.consecutive.offset).to.equal(4);
expect(user6x.purchased.plan.consecutive.trinkets).to.equal(5);
expect(user6x.purchased.plan.consecutive.gemCapExtra).to.equal(25);
clock.restore();
});
it('does not increment consecutive benefits in the third month after the fix goes live', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months').add(2, 'days').toDate());
cron({user: user6x, tasksByType, daysMissed, analytics});
expect(user6x.purchased.plan.consecutive.count).to.equal(11);
expect(user6x.purchased.plan.consecutive.offset).to.equal(3);
expect(user6x.purchased.plan.consecutive.trinkets).to.equal(5);
expect(user6x.purchased.plan.consecutive.gemCapExtra).to.equal(25);
clock.restore();
});
it('increments consecutive benefits in the seventh month after the fix goes live', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(7, 'months').add(2, 'days').toDate());
cron({user: user6x, tasksByType, daysMissed, analytics});
expect(user6x.purchased.plan.consecutive.count).to.equal(15);
expect(user6x.purchased.plan.consecutive.offset).to.equal(5);
expect(user6x.purchased.plan.consecutive.trinkets).to.equal(7);
expect(user6x.purchased.plan.consecutive.gemCapExtra).to.equal(25);
clock.restore();
});
});
});
describe('end of the month perks when user is not subscribed', () => {
@@ -1348,7 +1777,7 @@ describe('recoverCron', () => {
local: {
username: 'username',
lowerCaseUsername: 'username',
email: 'email@email.email',
email: 'email@example.com',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},
@@ -4,8 +4,8 @@ import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
import amzLib from '../../../../../../../website/server/libs/amazonPayments';
import payments from '../../../../../../../website/server/libs/payments';
import amzLib from '../../../../../../../website/server/libs/payments/amazon';
import payments from '../../../../../../../website/server/libs/payments/payments';
import common from '../../../../../../../website/common';
import { createNonLeaderGroupMember } from '../paymentHelpers';
@@ -1,6 +1,6 @@
import { model as User } from '../../../../../../../website/server/models/user';
import amzLib from '../../../../../../../website/server/libs/amazonPayments';
import payments from '../../../../../../../website/server/libs/payments';
import amzLib from '../../../../../../../website/server/libs/payments/amazon';
import payments from '../../../../../../../website/server/libs/payments/payments';
import common from '../../../../../../../website/common';
const i18n = common.i18n;
@@ -5,8 +5,8 @@ import {
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
import { model as Coupon } from '../../../../../../../website/server/models/coupon';
import amzLib from '../../../../../../../website/server/libs/amazonPayments';
import payments from '../../../../../../../website/server/libs/payments';
import amzLib from '../../../../../../../website/server/libs/payments/amazon';
import payments from '../../../../../../../website/server/libs/payments/payments';
import common from '../../../../../../../website/common';
const i18n = common.i18n;
@@ -5,8 +5,8 @@ import {
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
import { model as Group } from '../../../../../../../website/server/models/group';
import amzLib from '../../../../../../../website/server/libs/amazonPayments';
import payments from '../../../../../../../website/server/libs/payments';
import amzLib from '../../../../../../../website/server/libs/payments/amazon';
import payments from '../../../../../../../website/server/libs/payments/payments';
describe('#upgradeGroupPlan', () => {
let spy, data, user, group, uuidString;
@@ -1,10 +1,10 @@
/* eslint-disable camelcase */
import iapModule from '../../../../../website/server/libs/inAppPurchases';
import payments from '../../../../../website/server/libs/payments';
import applePayments from '../../../../../website/server/libs/applePayments';
import iap from '../../../../../website/server/libs/inAppPurchases';
import {model as User} from '../../../../../website/server/models/user';
import common from '../../../../../website/common';
import iapModule from '../../../../../../website/server/libs/inAppPurchases';
import payments from '../../../../../../website/server/libs/payments/payments';
import applePayments from '../../../../../../website/server/libs/payments/apple';
import iap from '../../../../../../website/server/libs/inAppPurchases';
import {model as User} from '../../../../../../website/server/models/user';
import common from '../../../../../../website/common';
import moment from 'moment';
const i18n = common.i18n;
@@ -57,6 +57,18 @@ describe('Apple Payments', () => {
});
});
it('should throw an error if getPurchaseData is invalid', async () => {
iapGetPurchaseDataStub.restore();
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData').returns([]);
await expect(applePayments.verifyGemPurchase(user, receipt, headers))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: applePayments.constants.RESPONSE_NO_ITEM_PURCHASED,
});
});
it('errors if the user cannot purchase gems', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
await expect(applePayments.verifyGemPurchase(user, receipt, headers))
@@ -69,27 +81,76 @@ describe('Apple Payments', () => {
user.canGetGems.restore();
});
it('purchases gems', async () => {
it('errors if amount does not exist', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
await applePayments.verifyGemPurchase(user, receipt, headers);
iapGetPurchaseDataStub.restore();
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
.returns([{productId: 'badProduct',
transactionId: token,
}]);
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt);
expect(iapIsValidatedStub).to.be.calledOnce;
expect(iapIsValidatedStub).to.be.calledWith({});
expect(iapGetPurchaseDataStub).to.be.calledOnce;
await expect(applePayments.verifyGemPurchase(user, receipt, headers))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: applePayments.constants.RESPONSE_INVALID_ITEM,
});
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
amount: 5.25,
headers,
});
expect(user.canGetGems).to.be.calledOnce;
user.canGetGems.restore();
});
const gemsCanPurchase = [
{
productId: 'com.habitrpg.ios.Habitica.4gems',
amount: 1,
},
{
productId: 'com.habitrpg.ios.Habitica.20gems',
amount: 5.25,
},
{
productId: 'com.habitrpg.ios.Habitica.21gems',
amount: 5.25,
},
{
productId: 'com.habitrpg.ios.Habitica.42gems',
amount: 10.5,
},
{
productId: 'com.habitrpg.ios.Habitica.84gems',
amount: 21,
},
];
gemsCanPurchase.forEach(gemTest => {
it(`purchases ${gemTest.productId} gems`, async () => {
iapGetPurchaseDataStub.restore();
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
.returns([{productId: gemTest.productId,
transactionId: token,
}]);
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
await applePayments.verifyGemPurchase(user, receipt, headers);
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt);
expect(iapIsValidatedStub).to.be.calledOnce;
expect(iapIsValidatedStub).to.be.calledWith({});
expect(iapGetPurchaseDataStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
amount: gemTest.amount,
headers,
});
expect(user.canGetGems).to.be.calledOnce;
user.canGetGems.restore();
});
});
});
describe('subscribe', () => {
@@ -133,7 +194,16 @@ describe('Apple Payments', () => {
iapModule.validate.restore();
iapModule.isValidated.restore();
iapModule.getPurchaseData.restore();
payments.createSubscription.restore();
if (payments.createSubscription.restore) payments.createSubscription.restore();
});
it('should throw an error if sku is empty', async () => {
await expect(applePayments.subscribe('', user, receipt, headers, nextPaymentProcessing))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
message: i18n.t('missingSubscriptionCode'),
});
});
it('should throw an error if receipt is invalid', async () => {
@@ -149,26 +219,69 @@ describe('Apple Payments', () => {
});
});
it('creates a user subscription', async () => {
const subOptions = [
{
sku: 'subscription1month',
subKey: 'basic_earned',
},
{
sku: 'com.habitrpg.ios.habitica.subscription.3month',
subKey: 'basic_3mo',
},
{
sku: 'com.habitrpg.ios.habitica.subscription.6month',
subKey: 'basic_6mo',
},
{
sku: 'com.habitrpg.ios.habitica.subscription.12month',
subKey: 'basic_12mo',
},
];
subOptions.forEach(option => {
it(`creates a user subscription for ${option.sku}`, async () => {
iapModule.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().add({day: 1}).toDate(),
productId: option.sku,
transactionId: token,
}]);
sub = common.content.subscriptionBlocks[option.subKey];
await applePayments.subscribe(option.sku, user, receipt, headers, nextPaymentProcessing);
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt);
expect(iapIsValidatedStub).to.be.calledOnce;
expect(iapIsValidatedStub).to.be.calledWith({});
expect(iapGetPurchaseDataStub).to.be.calledOnce;
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
expect(paymentsCreateSubscritionStub).to.be.calledWith({
user,
customerId: token,
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
sub,
headers,
additionalData: receipt,
nextPaymentProcessing,
});
});
});
it('errors when a user is already subscribed', async () => {
payments.createSubscription.restore();
user = new User();
await applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing);
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt);
expect(iapIsValidatedStub).to.be.calledOnce;
expect(iapIsValidatedStub).to.be.calledWith({});
expect(iapGetPurchaseDataStub).to.be.calledOnce;
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
expect(paymentsCreateSubscritionStub).to.be.calledWith({
user,
customerId: token,
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
sub,
headers,
additionalData: receipt,
nextPaymentProcessing,
});
await expect(applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: applePayments.constants.RESPONSE_ALREADY_USED,
});
});
});
@@ -1,10 +1,10 @@
/* eslint-disable camelcase */
import iapModule from '../../../../../website/server/libs/inAppPurchases';
import payments from '../../../../../website/server/libs/payments';
import googlePayments from '../../../../../website/server/libs/googlePayments';
import iap from '../../../../../website/server/libs/inAppPurchases';
import {model as User} from '../../../../../website/server/models/user';
import common from '../../../../../website/common';
import iapModule from '../../../../../../website/server/libs/inAppPurchases';
import payments from '../../../../../../website/server/libs/payments/payments';
import googlePayments from '../../../../../../website/server/libs/payments/google';
import iap from '../../../../../../website/server/libs/inAppPurchases';
import {model as User} from '../../../../../../website/server/models/user';
import common from '../../../../../../website/common';
import moment from 'moment';
const i18n = common.i18n;
@@ -1,7 +1,7 @@
import moment from 'moment';
import * as sender from '../../../../../../../website/server/libs/email';
import * as api from '../../../../../../../website/server/libs/payments';
import * as api from '../../../../../../../website/server/libs/payments/payments';
import { model as User } from '../../../../../../../website/server/models/user';
import { model as Group } from '../../../../../../../website/server/models/group';
import {
@@ -3,10 +3,10 @@ import stripeModule from 'stripe';
import nconf from 'nconf';
import * as sender from '../../../../../../../website/server/libs/email';
import * as api from '../../../../../../../website/server/libs/payments';
import amzLib from '../../../../../../../website/server/libs/amazonPayments';
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
import * as api from '../../../../../../../website/server/libs/payments/payments';
import amzLib from '../../../../../../../website/server/libs/payments/amazon';
import paypalPayments from '../../../../../../../website/server/libs/payments/paypal';
import stripePayments from '../../../../../../../website/server/libs/payments/stripe';
import { model as User } from '../../../../../../../website/server/models/user';
import { model as Group } from '../../../../../../../website/server/models/group';
import {
@@ -1,14 +1,14 @@
import moment from 'moment';
import * as sender from '../../../../../website/server/libs/email';
import * as api from '../../../../../website/server/libs/payments';
import analytics from '../../../../../website/server/libs/analyticsService';
import notifications from '../../../../../website/server/libs/pushNotifications';
import { model as User } from '../../../../../website/server/models/user';
import { translate as t } from '../../../../helpers/api-v3-integration.helper';
import * as sender from '../../../../../../website/server/libs/email';
import * as api from '../../../../../../website/server/libs/payments/payments';
import analytics from '../../../../../../website/server/libs/analyticsService';
import notifications from '../../../../../../website/server/libs/pushNotifications';
import { model as User } from '../../../../../../website/server/models/user';
import { translate as t } from '../../../../../helpers/api-v3-integration.helper';
import {
generateGroup,
} from '../../../../helpers/api-unit.helper.js';
} from '../../../../../helpers/api-unit.helper.js';
describe('payments/index', () => {
let user, group, data, plan;
@@ -1,6 +1,6 @@
/* eslint-disable camelcase */
import payments from '../../../../../../../website/server/libs/payments';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
import paypalPayments from '../../../../../../../website/server/libs/payments/paypal';
import payments from '../../../../../../../website/server/libs/payments/payments';
import { model as User } from '../../../../../../../website/server/models/user';
describe('checkout success', () => {
@@ -1,7 +1,7 @@
/* eslint-disable camelcase */
import nconf from 'nconf';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
import paypalPayments from '../../../../../../../website/server/libs/payments/paypal';
import { model as User } from '../../../../../../../website/server/models/user';
import common from '../../../../../../../website/common';
@@ -1,6 +1,6 @@
/* eslint-disable camelcase */
import payments from '../../../../../../../website/server/libs/payments';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
import paypalPayments from '../../../../../../../website/server/libs/payments/paypal';
import payments from '../../../../../../../website/server/libs/payments/payments';
import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
@@ -1,6 +1,6 @@
/* eslint-disable camelcase */
import payments from '../../../../../../../website/server/libs/payments';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
import paypalPayments from '../../../../../../../website/server/libs/payments/paypal';
import payments from '../../../../../../../website/server/libs/payments/payments';
import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
@@ -1,6 +1,6 @@
/* eslint-disable camelcase */
import payments from '../../../../../../../website/server/libs/payments';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
import paypalPayments from '../../../../../../../website/server/libs/payments/paypal';
import payments from '../../../../../../../website/server/libs/payments/payments';
import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
@@ -2,7 +2,7 @@
import moment from 'moment';
import cc from 'coupon-code';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
import paypalPayments from '../../../../../../../website/server/libs/payments/paypal';
import { model as Coupon } from '../../../../../../../website/server/models/coupon';
import common from '../../../../../../../website/common';
@@ -4,8 +4,8 @@ import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
import payments from '../../../../../../../website/server/libs/payments';
import stripePayments from '../../../../../../../website/server/libs/payments/stripe';
import payments from '../../../../../../../website/server/libs/payments/payments';
import common from '../../../../../../../website/common';
const i18n = common.i18n;
@@ -6,8 +6,8 @@ import {
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
import { model as Coupon } from '../../../../../../../website/server/models/coupon';
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
import payments from '../../../../../../../website/server/libs/payments';
import stripePayments from '../../../../../../../website/server/libs/payments/stripe';
import payments from '../../../../../../../website/server/libs/payments/payments';
import common from '../../../../../../../website/common';
const i18n = common.i18n;
@@ -1,8 +1,8 @@
import stripeModule from 'stripe';
import { model as User } from '../../../../../../../website/server/models/user';
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
import payments from '../../../../../../../website/server/libs/payments';
import stripePayments from '../../../../../../../website/server/libs/payments/stripe';
import payments from '../../../../../../../website/server/libs/payments/payments';
import common from '../../../../../../../website/common';
const i18n = common.i18n;
@@ -4,7 +4,7 @@ import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
import stripePayments from '../../../../../../../website/server/libs/payments/stripe';
import common from '../../../../../../../website/common';
const i18n = common.i18n;
@@ -4,8 +4,8 @@ import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
import payments from '../../../../../../../website/server/libs/payments';
import stripePayments from '../../../../../../../website/server/libs/payments/stripe';
import payments from '../../../../../../../website/server/libs/payments/payments';
import common from '../../../../../../../website/common';
import logger from '../../../../../../../website/server/libs/logger';
import { v4 as uuid } from 'uuid';
@@ -5,8 +5,8 @@ import {
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
import { model as Group } from '../../../../../../../website/server/models/group';
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
import payments from '../../../../../../../website/server/libs/payments';
import stripePayments from '../../../../../../../website/server/libs/payments/stripe';
import payments from '../../../../../../../website/server/libs/payments/payments';
describe('Stripe - Upgrade Group Plan', () => {
const stripe = stripeModule('test');
+40
View File
@@ -0,0 +1,40 @@
import {
generateRes,
generateReq,
} from '../../../../helpers/api-unit.helper';
import { authWithHeaders as authWithHeadersFactory } from '../../../../../website/server/middlewares/auth';
describe('auth middleware', () => {
let res, req, user;
beforeEach(async () => {
res = generateRes();
req = generateReq();
user = await res.locals.user.save();
});
describe('auth with headers', () => {
it('allows to specify a list of user field that we do not want to load', (done) => {
const authWithHeaders = authWithHeadersFactory(false, {
userFieldsToExclude: ['items', 'flags', 'auth.timestamps'],
});
req.headers['x-api-user'] = user._id;
req.headers['x-api-key'] = user.apiToken;
authWithHeaders(req, res, (err) => {
if (err) return done(err);
const userToJSON = res.locals.user.toJSON();
expect(userToJSON.items).to.not.exist;
expect(userToJSON.flags).to.not.exist;
expect(userToJSON.auth.timestamps).to.not.exist;
expect(userToJSON.auth).to.exist;
expect(userToJSON.notifications).to.exist;
expect(userToJSON.preferences).to.exist;
done();
});
});
});
});
+48 -25
View File
@@ -182,7 +182,7 @@ describe('Group Model', () => {
await party.startQuest(questLeader);
await party.save();
sendChatStub = sandbox.stub(Group.prototype, 'sendChat');
sendChatStub = sandbox.spy(Group.prototype, 'sendChat');
});
afterEach(() => sendChatStub.restore());
@@ -378,7 +378,7 @@ describe('Group Model', () => {
await party.startQuest(questLeader);
await party.save();
sendChatStub = sandbox.stub(Group.prototype, 'sendChat');
sendChatStub = sandbox.spy(Group.prototype, 'sendChat');
});
afterEach(() => sendChatStub.restore());
@@ -918,21 +918,8 @@ describe('Group Model', () => {
sandbox.spy(User, 'update');
});
it('puts message at top of chat array', () => {
let oldMessage = {
text: 'a message',
};
party.chat.push(oldMessage, oldMessage, oldMessage);
party.sendChat('a new message', {_id: 'user-id', profile: { name: 'user name' }});
expect(party.chat).to.have.a.lengthOf(4);
expect(party.chat[0].text).to.eql('a new message');
expect(party.chat[0].uuid).to.eql('user-id');
});
it('formats message', () => {
party.sendChat('a new message', {
const chatMessage = party.sendChat('a new message', {
_id: 'user-id',
profile: { name: 'user name' },
contributor: {
@@ -947,11 +934,11 @@ describe('Group Model', () => {
},
});
let chat = party.chat[0];
const chat = chatMessage;
expect(chat.text).to.eql('a new message');
expect(validator.isUUID(chat.id)).to.eql(true);
expect(chat.timestamp).to.be.a('number');
expect(chat.timestamp).to.be.a('date');
expect(chat.likes).to.eql({});
expect(chat.flags).to.eql({});
expect(chat.flagCount).to.eql(0);
@@ -962,13 +949,11 @@ describe('Group Model', () => {
});
it('formats message as system if no user is passed in', () => {
party.sendChat('a system message');
let chat = party.chat[0];
const chat = party.sendChat('a system message');
expect(chat.text).to.eql('a system message');
expect(validator.isUUID(chat.id)).to.eql(true);
expect(chat.timestamp).to.be.a('number');
expect(chat.timestamp).to.be.a('date');
expect(chat.likes).to.eql({});
expect(chat.flags).to.eql({});
expect(chat.flagCount).to.eql(0);
@@ -1375,7 +1360,8 @@ describe('Group Model', () => {
expect(updatedParticipatingMember.achievements.quests[quest.key]).to.eql(1);
});
it('gives out super awesome Masterclasser achievement to the deserving', async () => {
// Disable test, it fails on TravisCI, but only there
xit('gives out super awesome Masterclasser achievement to the deserving', async () => {
quest = questScrolls.lostMasterclasser4;
party.quest.key = quest.key;
@@ -1403,8 +1389,45 @@ describe('Group Model', () => {
updatedLeader,
updatedParticipatingMember,
] = await Promise.all([
User.findById(questLeader._id),
User.findById(participatingMember._id),
User.findById(questLeader._id).exec(),
User.findById(participatingMember._id).exec(),
]);
expect(updatedLeader.achievements.lostMasterclasser).to.eql(true);
expect(updatedParticipatingMember.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 () => {
quest = questScrolls.lostMasterclasser1;
party.quest.key = quest.key;
questLeader.achievements.quests = {
mayhemMistiflying1: 1,
mayhemMistiflying2: 1,
mayhemMistiflying3: 1,
stoikalmCalamity1: 1,
stoikalmCalamity2: 1,
stoikalmCalamity3: 1,
taskwoodsTerror1: 1,
taskwoodsTerror2: 1,
taskwoodsTerror3: 1,
dilatoryDistress1: 1,
dilatoryDistress2: 1,
dilatoryDistress3: 1,
lostMasterclasser2: 1,
lostMasterclasser3: 1,
lostMasterclasser4: 1,
};
await questLeader.save();
await party.finishQuest(quest);
let [
updatedLeader,
updatedParticipatingMember,
] = await Promise.all([
User.findById(questLeader._id).exec(),
User.findById(participatingMember._id).exec(),
]);
expect(updatedLeader.achievements.lostMasterclasser).to.eql(true);
@@ -145,17 +145,17 @@ describe('Task Column', () => {
tasks = [
{
text: 'Hello world 1',
note: '',
notes: '',
checklist: [],
},
{
text: 'Hello world 2',
note: '',
notes: '',
checklist: [],
},
{
text: 'Generic Task Title',
note: '',
notes: '',
checklist: [
{ text: 'Check 1' },
{ text: 'Check 2' },
@@ -164,7 +164,7 @@ describe('Task Column', () => {
},
{
text: 'Hello world 3',
note: 'Generic Task Note',
notes: 'Generic Task Note',
checklist: [
{ text: 'Checkitem 1' },
{ text: 'Checkitem 2' },
@@ -0,0 +1,63 @@
import { hasClass } from 'client/store/getters/members';
describe('hasClass getter', () => {
it('returns false if level < 10', () => {
const member = {
stats: {
lvl: 5,
},
preferences: {
disableClasses: false,
},
flags: {
classSelected: true,
},
};
expect(hasClass()(member)).to.equal(false);
});
it('returns false if member has disabled classes', () => {
const member = {
stats: {
lvl: 10,
},
preferences: {
disableClasses: true,
},
flags: {
classSelected: true,
},
};
expect(hasClass()(member)).to.equal(false);
});
it('returns false if member has not yet selected a class', () => {
const member = {
stats: {
lvl: 10,
},
preferences: {
disableClasses: false,
},
flags: {
classSelected: false,
},
};
expect(hasClass()(member)).to.equal(false);
});
it('returns true when all conditions are met', () => {
const member = {
stats: {
lvl: 10,
},
preferences: {
disableClasses: false,
},
flags: {
classSelected: true,
},
};
expect(hasClass()(member)).to.equal(true);
});
});
+84
View File
@@ -0,0 +1,84 @@
import {
generateUser,
} from '../../helpers/common.helper';
import getOfficialPinnedItems from '../../../website/common/script/libs/getOfficialPinnedItems.js';
import inAppRewards from '../../../website/common/script/libs/inAppRewards';
describe('inAppRewards', () => {
let user;
let officialPinnedItems;
let officialPinnedItemPaths;
let testPinnedItems;
let testPinnedItemsOrder;
beforeEach(() => {
user = generateUser();
officialPinnedItems = getOfficialPinnedItems(user);
officialPinnedItemPaths = [];
// officialPinnedItems are returned in { type: ..., path:... } format but we just need the paths for testPinnedItemsOrder
if (officialPinnedItems.length > 0) {
officialPinnedItemPaths = officialPinnedItems.map(item => item.path);
}
testPinnedItems = [
{ type: 'armoire', path: 'armoire' },
{ type: 'potion', path: 'potion' },
{ type: 'marketGear', path: 'gear.flat.weapon_warrior_1' },
{ type: 'marketGear', path: 'gear.flat.head_warrior_1' },
{ type: 'marketGear', path: 'gear.flat.armor_warrior_1' },
{ type: 'hatchingPotions', path: 'hatchingPotions.Golden' },
{ type: 'marketGear', path: 'gear.flat.shield_warrior_1' },
{ type: 'card', path: 'cardTypes.greeting' },
{ type: 'potion', path: 'hatchingPotions.Golden' },
{ type: 'card', path: 'cardTypes.thankyou' },
{ type: 'food', path: 'food.Saddle' },
];
testPinnedItemsOrder = [
'hatchingPotions.Golden',
'cardTypes.greeting',
'armoire',
'gear.flat.weapon_warrior_1',
'gear.flat.head_warrior_1',
'cardTypes.thankyou',
'gear.flat.armor_warrior_1',
'food.Saddle',
'gear.flat.shield_warrior_1',
'potion',
];
// For this test put seasonal items at the end so they stay out of the way
testPinnedItemsOrder = testPinnedItemsOrder.concat(officialPinnedItemPaths);
});
it('returns the pinned items in the correct order', () => {
user.pinnedItems = testPinnedItems;
user.pinnedItemsOrder = testPinnedItemsOrder;
let result = inAppRewards(user);
expect(result[2].path).to.eql('armoire');
expect(result[9].path).to.eql('potion');
});
it('does not return seasonal items which have been unpinned', () => {
if (officialPinnedItems.length === 0) {
return; // if no seasonal items, this test is not applicable
}
let testUnpinnedItem = officialPinnedItems[0];
let testUnpinnedPath = testUnpinnedItem.path;
let testUnpinnedItems = [
{ type: testUnpinnedItem.type, path: testUnpinnedPath},
];
user.pinnedItems = testPinnedItems;
user.pinnedItemsOrder = testPinnedItemsOrder;
user.unpinnedItems = testUnpinnedItems;
let result = inAppRewards(user);
let itemPaths = result.map(item => item.path);
expect(itemPaths).to.not.include(testUnpinnedPath);
});
});
+7 -1
View File
@@ -4,7 +4,7 @@ import {
generateUser,
} from '../../../helpers/common.helper';
import count from '../../../../website/common/script/count';
import buyArmoire from '../../../../website/common/script/ops/buy/buyArmoire';
import {BuyArmoireOperation} from '../../../../website/common/script/ops/buy/buyArmoire';
import randomVal from '../../../../website/common/script/libs/randomVal';
import content from '../../../../website/common/script/content/index';
import {
@@ -33,6 +33,12 @@ describe('shared.ops.buyArmoire', () => {
let YIELD_EXP = 0.9;
let analytics = {track () {}};
function buyArmoire (_user, _req, _analytics) {
const buyOp = new BuyArmoireOperation(_user, _req, _analytics);
return buyOp.purchase();
}
beforeEach(() => {
user = generateUser({
stats: { gp: 200 },
+7 -1
View File
@@ -2,7 +2,7 @@
import {
generateUser,
} from '../../../helpers/common.helper';
import buyHealthPotion from '../../../../website/common/script/ops/buy/buyHealthPotion';
import { BuyHealthPotionOperation } from '../../../../website/common/script/ops/buy/buyHealthPotion';
import {
NotAuthorized,
} from '../../../../website/common/script/libs/errors';
@@ -12,6 +12,12 @@ describe('shared.ops.buyHealthPotion', () => {
let user;
let analytics = {track () {}};
function buyHealthPotion (_user, _req, _analytics) {
const buyOp = new BuyHealthPotionOperation(_user, _req, _analytics);
return buyOp.purchase();
}
beforeEach(() => {
user = generateUser({
items: {
+44 -1
View File
@@ -1,7 +1,7 @@
import {
generateUser,
} from '../../../helpers/common.helper';
import buyQuest from '../../../../website/common/script/ops/buy/buyQuest';
import {BuyQuestWithGoldOperation} from '../../../../website/common/script/ops/buy/buyQuest';
import {
BadRequest,
NotAuthorized,
@@ -13,6 +13,12 @@ describe('shared.ops.buyQuest', () => {
let user;
let analytics = {track () {}};
function buyQuest (_user, _req, _analytics) {
const buyOp = new BuyQuestWithGoldOperation(_user, _req, _analytics);
return buyOp.purchase();
}
beforeEach(() => {
user = generateUser();
sinon.stub(analytics, 'track');
@@ -36,6 +42,43 @@ describe('shared.ops.buyQuest', () => {
expect(analytics.track).to.be.calledOnce;
});
it('buys a Quest scroll with the right quantity if a string is passed for quantity', () => {
user.stats.gp = 1000;
buyQuest(user, {
params: {
key: 'dilatoryDistress1',
},
}, analytics);
buyQuest(user, {
params: {
key: 'dilatoryDistress1',
},
quantity: '3',
}, analytics);
expect(user.items.quests).to.eql({
dilatoryDistress1: 4,
});
});
it('does not buy a Quest scroll when an invalid quantity is passed', (done) => {
user.stats.gp = 1000;
try {
buyQuest(user, {
params: {
key: 'dilatoryDistress1',
},
quantity: 'a',
}, analytics);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity'));
expect(user.items.quests).to.eql({});
expect(user.stats.gp).to.equal(1000);
done();
}
});
it('does not buy Quests without enough Gold', (done) => {
user.stats.gp = 1;
try {
+28
View File
@@ -1,4 +1,5 @@
import purchase from '../../../../website/common/script/ops/buy/purchase';
import pinnedGearUtils from '../../../../website/common/script/ops/pinnedGearUtils';
import planGemLimits from '../../../../website/common/script/libs/planGemLimits';
import {
BadRequest,
@@ -25,10 +26,12 @@ describe('shared.ops.purchase', () => {
beforeEach(() => {
sinon.stub(analytics, 'track');
sinon.spy(pinnedGearUtils, 'removeItemByPath');
});
afterEach(() => {
analytics.track.restore();
pinnedGearUtils.removeItemByPath.restore();
});
context('failure conditions', () => {
@@ -87,6 +90,19 @@ describe('shared.ops.purchase', () => {
}
});
it('prevents user from buying an invalid quantity', (done) => {
user.stats.gp = goldPoints;
user.purchased.plan.gemsBought = gemsBought;
try {
purchase(user, {params: {type: 'gems', key: 'gem'}, quantity: 'a'});
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity'));
done();
}
});
it('returns error when unknown type is provided', (done) => {
try {
purchase(user, {params: {type: 'randomType', key: 'gem'}});
@@ -161,6 +177,12 @@ describe('shared.ops.purchase', () => {
user.stats.gp = goldPoints;
user.purchased.plan.gemsBought = 0;
user.purchased.plan.customerId = 'customer-id';
user.pinnedItems.push({type: 'eggs', key: 'Wolf'});
user.pinnedItems.push({type: 'hatchingPotions', key: 'Base'});
user.pinnedItems.push({type: 'food', key: SEASONAL_FOOD});
user.pinnedItems.push({type: 'quests', key: 'gryphon'});
user.pinnedItems.push({type: 'gear', key: 'headAccessory_special_tigerEars'});
user.pinnedItems.push({type: 'bundles', key: 'featheredFriends'});
});
it('purchases gems', () => {
@@ -189,6 +211,7 @@ describe('shared.ops.purchase', () => {
purchase(user, {params: {type, key}}, analytics);
expect(user.items[type][key]).to.equal(1);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
expect(analytics.track).to.be.calledOnce;
});
@@ -199,6 +222,7 @@ describe('shared.ops.purchase', () => {
purchase(user, {params: {type, key}});
expect(user.items[type][key]).to.equal(1);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
});
it('purchases food', () => {
@@ -208,6 +232,7 @@ describe('shared.ops.purchase', () => {
purchase(user, {params: {type, key}});
expect(user.items[type][key]).to.equal(1);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
});
it('purchases quests', () => {
@@ -217,6 +242,7 @@ describe('shared.ops.purchase', () => {
purchase(user, {params: {type, key}});
expect(user.items[type][key]).to.equal(1);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
});
it('purchases gear', () => {
@@ -226,6 +252,7 @@ describe('shared.ops.purchase', () => {
purchase(user, {params: {type, key}});
expect(user.items.gear.owned[key]).to.be.true;
expect(pinnedGearUtils.removeItemByPath.calledOnce).to.equal(true);
});
it('purchases quest bundles', () => {
@@ -248,6 +275,7 @@ describe('shared.ops.purchase', () => {
expect(user.balance).to.equal(startingBalance - price);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
clock.restore();
});
});
+7 -2
View File
@@ -54,10 +54,15 @@ export function generateReq (options = {}) {
body: {},
query: {},
headers: {},
header: sandbox.stub().returns(null),
header (header) {
return this.headers[header];
},
session: {},
};
return defaultsDeep(options, defaultReq);
const req = defaultsDeep(options, defaultReq);
return req;
}
export function generateNext (func) {
+43 -8
View File
@@ -9,7 +9,8 @@ div
h2 {{$t('tipTitle', {tipNumber: currentTipNumber})}}
p {{currentTip}}
#app(:class='{"casting-spell": castingSpell}')
amazon-payments-modal
banned-account-modal
amazon-payments-modal(v-if='!isStaticPage')
snackbars
router-view(v-if="!isUserLoggedIn || isStaticPage")
template(v-else)
@@ -42,9 +43,9 @@ div
div(:class='{sticky: user.preferences.stickyHeader}')
router-view
app-footer
audio#sound(autoplay, ref="sound")
source#oggSource(type="audio/ogg", :src="sound.oggSource")
source#mp3Source(type="audio/mp3", :src="sound.mp3Source")
audio#sound(autoplay, ref="sound")
source#oggSource(type="audio/ogg", :src="sound.oggSource")
source#mp3Source(type="audio/mp3", :src="sound.mp3Source")
</template>
<style lang='scss' scoped>
@@ -111,7 +112,7 @@ div
}
.modal-backdrop.show {
opacity: 1 !important;
opacity: .9 !important;
background-color: $purple-100 !important;
}
@@ -193,6 +194,9 @@ import amazonPaymentsModal from 'client/components/payments/amazonModal';
import spellsMixin from 'client/mixins/spells';
import svgClose from 'assets/svg/close.svg';
import bannedAccountModal from 'client/components/bannedAccountModal';
const COMMUNITY_MANAGER_EMAIL = process.env.EMAILS.COMMUNITY_MANAGER_EMAIL; // eslint-disable-line
export default {
mixins: [notifications, spellsMixin],
@@ -206,6 +210,7 @@ export default {
BuyModal,
SelectMembersModal,
amazonPaymentsModal,
bannedAccountModal,
},
data () {
return {
@@ -250,8 +255,9 @@ export default {
this.$root.$on('playSound', (sound) => {
let theme = this.user.preferences.sound;
if (!theme || theme === 'off')
if (!theme || theme === 'off') {
return;
}
let file = `/static/audio/${theme}/${sound}`;
this.sound = {
@@ -287,6 +293,8 @@ export default {
return response;
}, (error) => {
if (error.response.status >= 400) {
this.checkForBannedUser(error);
// Check for conditions to reset the user auth
const invalidUserMessage = [this.$t('invalidCredentials'), 'Missing authentication headers.'];
if (invalidUserMessage.indexOf(error.response.data) !== -1) {
@@ -365,6 +373,11 @@ export default {
document.title = title;
});
this.$nextTick(() => {
// Load external scripts after the app has been rendered
Analytics.load();
});
if (this.isUserLoggedIn && !this.isStaticPage) {
// Load the user and the user tasks
Promise.all([
@@ -387,7 +400,6 @@ export default {
this.$nextTick(() => {
// Load external scripts after the app has been rendered
setupPayments();
Analytics.load();
});
}).catch((err) => {
console.error('Impossible to fetch user. Clean up localStorage and refresh.', err); // eslint-disable-line no-console
@@ -411,6 +423,25 @@ export default {
if (loadingScreen) document.body.removeChild(loadingScreen);
},
methods: {
checkForBannedUser (error) {
const AUTH_SETTINGS = localStorage.getItem('habit-mobile-settings');
const parseSettings = JSON.parse(AUTH_SETTINGS);
const errorMessage = error.response.data.message;
// Case where user is not logged in
if (!parseSettings) {
return;
}
const bannedMessage = this.$t('accountSuspended', {
communityManagerEmail: COMMUNITY_MANAGER_EMAIL,
userId: parseSettings.auth.apiId,
});
if (errorMessage !== bannedMessage) return;
this.$root.$emit('bv::show::modal', 'banned-account');
},
initializeModalStack () {
// Manage modals
this.$root.$on('bv::show::modal', (modalId, data = {}) => {
@@ -499,7 +530,7 @@ export default {
if (!item)
return false;
if (item.purchaseType === 'card')
if (['card', 'debuffPotion'].includes(item.purchaseType))
return false;
return true;
@@ -520,6 +551,10 @@ export default {
this.$root.$emit('bv::show::modal', 'select-member-modal');
}
if (item.purchaseType === 'debuffPotion') {
this.castStart(item, this.user);
}
},
async memberSelected (member) {
await this.castStart(this.selectedSpellToBuy, member);
@@ -1,78 +1,66 @@
.promo_armoire_background_201803 {
.promo_armoire_background_201804 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -142px -327px;
background-position: -142px -587px;
width: 141px;
height: 441px;
}
.promo_egg_hunt {
.promo_ios {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -814px 0px;
width: 354px;
height: 147px;
}
.promo_hugabug_bundle {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -441px -408px;
width: 141px;
height: 441px;
}
.promo_mystery_201802 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -441px 0px;
width: 372px;
height: 196px;
background-position: -532px 0px;
width: 325px;
height: 336px;
}
.promo_mystery_201803 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -977px -148px;
background-position: -695px -337px;
width: 114px;
height: 90px;
}
.promo_rainbow_potions {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -583px -408px;
background-position: -284px -587px;
width: 141px;
height: 441px;
}
.promo_seasonalshop_spring {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -814px -148px;
background-position: -532px -337px;
width: 162px;
height: 138px;
}
.promo_shimmer_pastel {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -426px -735px;
width: 354px;
height: 147px;
}
.promo_shiny_seeds {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -426px -587px;
width: 360px;
height: 147px;
}
.promo_spring_fling_2018 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -327px;
background-position: 0px -587px;
width: 141px;
height: 588px;
}
.promo_take_this {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -956px -287px;
background-position: -532px -476px;
width: 114px;
height: 87px;
}
.scene_achievement {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -441px -197px;
width: 339px;
height: 210px;
}
.scene_podcast {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -814px -287px;
width: 141px;
height: 141px;
}
.scene_sweeping {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -814px -429px;
width: 138px;
height: 144px;
}
.scene_tavern {
.scene_positivity {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px 0px;
width: 440px;
height: 326px;
width: 531px;
height: 243px;
}
.scene_video_games {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -244px;
width: 339px;
height: 342px;
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,24 +1,42 @@
.phobia_dysheartener {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: 0px -1510px;
width: 201px;
height: 195px;
}
.quest_armadillo {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1320px -440px;
width: 219px;
height: 219px;
}
.quest_atom1 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1376px -1332px;
width: 250px;
height: 150px;
}
.quest_atom2 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -642px -1519px;
background-position: -633px -1510px;
width: 207px;
height: 138px;
}
.quest_atom3 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -211px -1519px;
background-position: -202px -1510px;
width: 216px;
height: 180px;
}
.quest_axolotl {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -440px 0px;
background-position: -220px -232px;
width: 219px;
height: 219px;
}
.quest_badger {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: 0px -232px;
background-position: -440px -232px;
width: 219px;
height: 219px;
}
@@ -30,25 +48,25 @@
}
.quest_beetle {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1322px -1112px;
background-position: -1540px -1079px;
width: 204px;
height: 201px;
}
.quest_bunny {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: 0px -1519px;
background-position: -1322px -1112px;
width: 210px;
height: 186px;
}
.quest_butterfly {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -660px -220px;
background-position: -220px -452px;
width: 219px;
height: 219px;
}
.quest_cheetah {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: 0px -452px;
background-position: -440px -452px;
width: 219px;
height: 219px;
}
@@ -60,13 +78,13 @@
}
.quest_dilatory {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -660px -452px;
background-position: -880px -220px;
width: 219px;
height: 219px;
}
.quest_dilatoryDistress1 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1540px -1085px;
background-position: -1540px -868px;
width: 210px;
height: 210px;
}
@@ -78,19 +96,19 @@
}
.quest_dilatoryDistress3 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -880px -440px;
background-position: -220px -672px;
width: 219px;
height: 219px;
}
.quest_dilatory_derby {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -440px -452px;
background-position: -880px 0px;
width: 219px;
height: 219px;
}
.quest_dustbunnies {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: 0px -672px;
background-position: -440px -672px;
width: 219px;
height: 219px;
}
@@ -102,49 +120,49 @@
}
.quest_evilsanta {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1757px -1026px;
background-position: -1757px -875px;
width: 118px;
height: 131px;
}
.quest_evilsanta2 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -660px -672px;
background-position: -1100px 0px;
width: 219px;
height: 219px;
}
.quest_falcon {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -880px -672px;
background-position: -1100px -220px;
width: 219px;
height: 219px;
}
.quest_ferret {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1100px 0px;
background-position: -1100px -440px;
width: 219px;
height: 219px;
}
.quest_frog {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -880px -1112px;
background-position: -660px -1112px;
width: 221px;
height: 213px;
}
.quest_ghost_stag {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1100px -440px;
background-position: -220px 0px;
width: 219px;
height: 219px;
}
.quest_goldenknight1 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1100px -660px;
background-position: -220px -892px;
width: 219px;
height: 219px;
}
.quest_goldenknight2 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1094px -1332px;
background-position: -874px -1332px;
width: 250px;
height: 150px;
}
@@ -156,145 +174,145 @@
}
.quest_gryphon {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -660px -1332px;
background-position: -657px -1332px;
width: 216px;
height: 177px;
}
.quest_guineapig {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -660px -892px;
background-position: -1100px -892px;
width: 219px;
height: 219px;
}
.quest_harpy {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -880px -892px;
background-position: -1320px 0px;
width: 219px;
height: 219px;
}
.quest_hedgehog {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: 0px -1332px;
background-position: -1102px -1112px;
width: 219px;
height: 186px;
}
.quest_hippo {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1320px 0px;
background-position: -660px -892px;
width: 219px;
height: 219px;
}
.quest_horse {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1320px -220px;
background-position: -1320px -660px;
width: 219px;
height: 219px;
}
.quest_kraken {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -443px -1332px;
background-position: -223px -1332px;
width: 216px;
height: 177px;
}
.quest_lostMasterclasser1 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1320px -660px;
background-position: 0px -1112px;
width: 219px;
height: 219px;
}
.quest_lostMasterclasser2 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -220px 0px;
background-position: -220px -1112px;
width: 219px;
height: 219px;
}
.quest_lostMasterclasser3 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: 0px -1112px;
background-position: -1320px -880px;
width: 219px;
height: 219px;
}
.quest_mayhemMistiflying1 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1757px -875px;
background-position: -1757px -422px;
width: 150px;
height: 150px;
}
.quest_mayhemMistiflying2 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -440px -1112px;
background-position: -1320px -220px;
width: 219px;
height: 219px;
}
.quest_mayhemMistiflying3 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -660px -1112px;
background-position: -440px -892px;
width: 219px;
height: 219px;
}
.quest_monkey {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -220px -1112px;
background-position: -1100px -660px;
width: 219px;
height: 219px;
}
.quest_moon1 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1540px -217px;
background-position: -1540px -434px;
width: 216px;
height: 216px;
}
.quest_moon2 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1320px -880px;
background-position: 0px -672px;
width: 219px;
height: 219px;
}
.quest_moon3 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1320px -440px;
background-position: -880px -440px;
width: 219px;
height: 219px;
}
.quest_moonstone1 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1100px -892px;
background-position: -660px -220px;
width: 219px;
height: 219px;
}
.quest_moonstone2 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -440px -892px;
background-position: -440px 0px;
width: 219px;
height: 219px;
}
.quest_moonstone3 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: 0px -892px;
background-position: -440px -1112px;
width: 219px;
height: 219px;
}
.quest_nudibranch {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1540px -868px;
background-position: -1540px -651px;
width: 216px;
height: 216px;
}
.quest_octopus {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -220px -1332px;
background-position: 0px -1332px;
width: 222px;
height: 177px;
}
.quest_owl {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1100px -220px;
background-position: -880px -892px;
width: 219px;
height: 219px;
}
.quest_peacock {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1540px -434px;
background-position: -1540px -217px;
width: 216px;
height: 216px;
}
@@ -306,103 +324,79 @@
}
.quest_pterodactyl {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -440px -672px;
background-position: -880px -672px;
width: 219px;
height: 219px;
}
.quest_rat {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -220px -672px;
background-position: -660px -672px;
width: 219px;
height: 219px;
}
.quest_rock {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1540px -651px;
background-position: -1540px 0px;
width: 216px;
height: 216px;
}
.quest_rooster {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -428px -1519px;
background-position: -419px -1510px;
width: 213px;
height: 174px;
}
.quest_sabretooth {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -880px -220px;
background-position: -660px -452px;
width: 219px;
height: 219px;
}
.quest_sheep {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -880px 0px;
background-position: 0px -452px;
width: 219px;
height: 219px;
}
.quest_slime {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -220px -452px;
background-position: -660px 0px;
width: 219px;
height: 219px;
}
.quest_sloth {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -660px 0px;
background-position: 0px -232px;
width: 219px;
height: 219px;
}
.quest_snail {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1102px -1112px;
background-position: -882px -1112px;
width: 219px;
height: 213px;
}
.quest_snake {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -877px -1332px;
background-position: -440px -1332px;
width: 216px;
height: 177px;
}
.quest_spider {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1345px -1332px;
background-position: -1125px -1332px;
width: 250px;
height: 150px;
}
.quest_squirrel {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: 0px -892px;
width: 219px;
height: 219px;
}
.quest_stoikalmCalamity1 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1757px -422px;
width: 150px;
height: 150px;
}
.quest_stoikalmCalamity2 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -440px -232px;
width: 219px;
height: 219px;
}
.quest_stoikalmCalamity3 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -220px -232px;
width: 219px;
height: 219px;
}
.quest_taskwoodsTerror1 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1757px -724px;
width: 150px;
height: 150px;
}
.quest_taskwoodsTerror2 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -1540px 0px;
width: 216px;
height: 216px;
}
.quest_taskwoodsTerror3 {
background-image: url('~assets/images/sprites/spritesmith-main-10.png');
background-position: -220px -892px;
width: 219px;
height: 219px;
}
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
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

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