Compare commits

...

235 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
Sabe Jones ddec458364 4.33.0 2018-03-22 19:58:55 +00:00
Sabe Jones bfe74a8dcb chore(i18n): update locales 2018-03-22 19:57:57 +00:00
SabreCat 13d9da404d chore(sprites): compile 2018-03-22 19:51:21 +00:00
SabreCat 85e0af0c0e feat(content): Mystery Items 2018/03 2018-03-22 19:50:47 +00:00
SabreCat a2e5548b1c Merge branch 'develop' into release 2018-03-22 18:53:40 +00:00
negue 36dabad2c9 fix seasonal shop (#10164) 2018-03-22 13:50:58 -05:00
Keith Holliday 25b9e6f330 Added memwatch for memory leak detection (#10166) 2018-03-22 12:44:45 -05:00
Keith Holliday d0a786554c Removed balance check test (#10159)
* Removed balance check test

* Removed balance check in common

* Removed gem logic and added achievement to tests
2018-03-21 11:53:47 -05:00
Alys de79e0e3c3 add swear word - TRIGGER / CONTENT WARNING: assault, slurs, swearwords, etc 2018-03-21 19:19:13 +10:00
Sabe Jones 0e74d25ede Merge branch 'release' into develop 2018-03-20 23:12:23 +00:00
Sabe Jones bde8b76da7 4.32.1 2018-03-20 23:11:49 +00:00
Sabe Jones 4235be4b43 chore(i18n): update locales 2018-03-20 23:11:37 +00:00
Sabe Jones 3af7f89d10 Merge branch 'release' into develop 2018-03-20 22:57:52 +00:00
Sabe Jones 396362d27e 4.32.0 2018-03-20 22:57:28 +00:00
SabreCat 972efb1878 fix(seasonal-shop): remove quest
it was breaking the API. Wut? Just use canBuy logic and Quest Shop for now
2018-03-20 22:49:54 +00:00
SabreCat 6c18d19d95 fix(test): extra curly 2018-03-20 21:56:55 +00:00
SabreCat 9b8bdb90d8 chore(sprites): compile 2018-03-20 21:31:04 +00:00
SabreCat a84ea8b1b7 feat(event): Spring Fling 2018 2018-03-20 21:30:43 +00:00
Matteo Pagliazzi 480a839bc5 update package-lock 2018-03-18 16:36:36 +01:00
greenkeeper[bot] df9c54fe20 chore(package): update http-proxy-middleware to version 0.18.0 (#10131) 2018-03-18 16:33:33 +01:00
greenkeeper[bot] 8dbab1a976 chore(package): update sinon-chai to version 3.0.0 (#10096) 2018-03-18 16:33:19 +01:00
greenkeeper[bot] df6088bc6d fix(package): update url-loader to version 1.0.0 (#10088) 2018-03-18 16:31:03 +01:00
greenkeeper[bot] f0ff3a4eb6 fix(package): update html-webpack-plugin to version 3.0.0 (#10070) 2018-03-18 16:29:25 +01:00
greenkeeper[bot] 786e4419da chore(package): update eslint-loader to version 2.0.0 (#10056) 2018-03-18 16:28:20 +01:00
Chris Wang fff4ea3ad3 Added popover stats to costume equipment on profile stats - fixes #9280 (#10136)
* Added popover stats to costume equipment on the stats page of a profile (#9280)

* Changed popover position to bottom for equip and costume items.

* Fixed indent on line 160

* Changed escaped double quotes to single quotes for readability
2018-03-18 16:24:41 +01:00
kartik adur e18e89bc10 Task page - task filters v2 (#10053)
* update column.vue, getters/task.js, getters/user.js and add unittests and helpers

* add vue-test-utils pkg + unit test for column.vue

* add unit test column.vue

* update unit tests

* fix linting errors
2018-03-18 16:23:58 +01:00
Matteo Pagliazzi 68ea28305d fix linting issues 2018-03-17 22:36:17 +01:00
Travis 242b3508a1 Update Staff list in the tavern to make staff names clickable to pull up staff profiles. (#10107)
* Update Staff list in the tavern to make staff names clickable to pull up staff profiles.

fixes #9290

* Addressing PR comments.
2018-03-17 22:29:52 +01:00
Travis 8c316d939f Updated get challenges api to return challenges sorted by which challenges include the habitica_official category (#10079)
and removed sorting on the official flag of the challenges object.

fixes #9955
2018-03-17 22:26:24 +01:00
Travis 45eb19e992 Update the API to prevent the user from leaving a group if they are the only member and have a quest active. (#10091)
* Update the API to prevent the user from leaving a group if they are the only member and have a quest active.

fixes #10068

* fixing api doc
2018-03-17 22:26:07 +01:00
Travis 3ad0ffcaec Escaping regex characters from user input before searching for groups. (#10092)
fixes #9953
2018-03-17 22:25:50 +01:00
Travis fef8929dd9 Fixes start date updating the repeat on day for dailies repeated monthly (#10095)
fixes # 9200
2018-03-17 22:25:34 +01:00
Travis 911f894750 Fixing edit profile page so blurb can be editted after reload. (#10097)
fixes #10032
2018-03-17 22:25:17 +01:00
Travis d4e2f5ac9e fix: Fixing xml data export 500 error. (#10114)
* fix: Fixing xml data export 500 error.

fixes #10100

* Removing outdated comment.

* Making xml data export test pass consistently.
2018-03-17 22:24:40 +01:00
Mahendran Nadesan e43749bed1 Stats over 3 digits overlap (fixes #10033) (#10133)
* Formatted stats

* small changes

* Fixed issues as per comments
2018-03-17 22:23:55 +01:00
Sarah Boo 8be29e27d0 Hide "Delete Completed" To-Dos button on Challenge or Group Task Board - fixes #9935 (#10128)
* only show delete to-dos for user's task page

* remove stray spaces
2018-03-17 22:22:12 +01:00
Mark Kuba 301668fe22 fix: you are already in group message - fixes #10119 (#10122)
* add youAreAlreadyInGroup message

* add test for youAreAlreadyInGroup message

* update youAreAlreadyInGroup message
2018-03-17 22:21:16 +01:00
Gene Vityugov 767f3ebe12 Add space in profile modal to make it consistent with other text (#10103) 2018-03-17 22:19:58 +01:00
EthanEFung 25e6f8e26e Add Completion Text to Older Quests #10066 (#10081) 2018-03-17 22:18:54 +01:00
Dmitry Torba 1c6608d071 fix flower avatar in chat #10015 (#10040) 2018-03-17 22:17:47 +01:00
Toivo Mattila d2b160438c Added margin to buttons on Settings-page (#10034)
* Added margin to buttons on Settings-page

* Added bottom margin for 'Enable Class System' button
2018-03-17 22:16:55 +01:00
Александр 37d70f089c Removes video from presskit.zip (#10013)
* presskit without video

* Boss/ update pics

* Logo/ change broken pics

* updated broken files

* Delete Level Up.PNG

* Add files via upload

* update outdated icons

* update party

* upload updated presskit
2018-03-17 22:15:40 +01:00
Julius Jung 04b4912d59 Fix password reset when querying for emails with upcase characters (fixes #9059) (#9707)
* downcase updating an email to be consistent with creating

* add tests to ensure downcase of email for create/update

* create migration to downcase existing User objects

* delete 'only'

* change gmail to example

* add trailing comma from lint error

* search for emails with at least one capital letter

* fix query in order to search for any email with at least one capital letter

* batch process effected users with at least one capital in email

* update script for batch process effected users
2018-03-17 22:13:54 +01:00
Zack Sunderland b9a6d9ceec Allow Un-subscribed Users to Claim Mystery Items (#9270)
Remove restriction on mystery/special inventory items that prevents
non-subscribed users from seeing the items. Organize the imports on the
page.

Resolves: Issue #9228
2018-03-17 22:13:31 +01:00
negue 2a97915477 Purchase API Refactoring: Market Gear (#10010)
* convert buyGear to buyMarketGearOperation + tests

* move NotImplementedError
2018-03-17 21:56:19 +01:00
Alys 29a9deaeb8 prevent lint indent warning on multi-line reduce function (#10145) 2018-03-17 21:13:55 +01:00
negue 0fcc1f2080 correct position of pets in the profile overview - fixes #10104 (#10144) 2018-03-17 21:13:35 +01:00
negue 9287098e70 fix #10116 (#10143) 2018-03-17 21:13:12 +01:00
negue 1812f63812 raise coverage for user api calls (#10099)
* user integration tests

* fix lint
2018-03-17 21:12:53 +01:00
negue 3d4107db3e checkout of the Inn - banner (#9835)
* show resting banner

* resume damage

* replace colors by variables

* remove indentation
2018-03-17 21:12:25 +01:00
Sabe Jones 67e750a81c 4.31.1 2018-03-15 21:19:44 +00:00
Keith Holliday 7fe62a731b Fixed gem amount on master key (#10140)
* Fixed gem amount on master key

* fix(news): update Bailey text
2018-03-15 16:19:11 -05:00
Sabe Jones 3c224fe353 Merge branch 'release' into develop 2018-03-15 19:10:56 +00:00
Sabe Jones bb6f465ac8 4.31.0 2018-03-15 19:10:37 +00:00
Sabe Jones a2a79e4607 chore(i18n): update locales 2018-03-15 19:07:38 +00:00
SabreCat 5125cc5f59 chore(news): Bailey 2018-03-15 19:04:00 +00:00
Matteo Pagliazzi cb42a31c43 Node 8 (WIP) (#9946)
* start upgrade to node 8

* upgrade travis

* improve travis

* Remove bluebird, babel (except for modules) from server (WIP) (#9947)

* remove bluebird, babel from server (except for modules)

* fixes

* fix path

* fix path

* fix export

* fix export

* fix test

* fix tests

* remove plugin for transform-object-rest-spread since it is supported in node8

* babel: correct syntax rest spread

* remove bluebird

* update migrations archive readme

* fix package-lock.json

* fix typo

* add package-loc
2018-03-15 19:59:36 +01:00
Alys b1b1e512f5 adjust word order in mod slack flag message (#10137) 2018-03-15 19:27:59 +01:00
Alys 5ba09c45df adjust comments and word lists in slurs and banned words. TRIGGER / CONTENT WARNING: assault, slurs, swearwords, etc 2018-03-15 20:53:48 +10:00
Matteo Pagliazzi 70dec611e3 Make Kafka dep optional (#10129)
* Made kafka optional

* make kafka optional
2018-03-14 18:21:39 +01:00
Keith Holliday 2195464772 Flag comment (#9991)
* Added initial chat flag comment form

* Added user comment to slack message

* Updated copy and styles

* Fixed comma

* Updated admin messaging
2018-03-14 07:41:13 -05:00
Sabe Jones b662f8bdff Merge branch 'release' into develop 2018-03-13 21:00:00 +00:00
Sabe Jones 0e6d6336c6 4.30.0 2018-03-13 20:59:13 +00:00
Sabe Jones d8078adacd chore(i18n): update locales 2018-03-13 20:58:43 +00:00
SabreCat a88800df78 chore(sprites): compile 2018-03-13 20:54:54 +00:00
SabreCat 42e0095bbd feat(content): spring Hatching Potions 2018-03-13 20:54:27 +00:00
Sabe Jones ee1aa653f0 fix(groups): Guild badge fencepost errors 2018-03-12 14:43:39 -05:00
SabreCat 8d1b1ff794 chore(npm): update package lock 2018-03-09 21:22:25 +00:00
SabreCat 70500d7c98 Merge branch 'release' into develop 2018-03-09 21:18:56 +00:00
Travis 35849ebdd7 Fixing items sort by number in the market. (#10108)
fixes #10093
2018-03-09 15:17:26 -06:00
Matteo Pagliazzi 1332fd68b0 Mongoose 5 (#9870)
* mongoose 5: remove unused autoinc, remove promise option (it uses native promises now)

* remove mongodb package

* remove mongoskin

* migrate migration away from mongoskin

* fix mongoose hooks

* fix _updateUserWithRetries

* try without next

* remove init

* update sinon

* fix some integration tests

* fix remaining tests

* fix error message

* fix error message

* fix error message

* another fix

* fix mongoose options

* remove greenkeeper exception
2018-03-09 15:13:58 -06:00
Travis f9a47b1420 Update user pinned items on purchases. (#10085)
fixes #10049
2018-03-09 15:11:42 -06:00
Sabe Jones 090c571f3e 4.29.8 2018-03-09 21:04:14 +00:00
Dexx Mandele 07b3824c4c Stop market background from covering sign (#10067) 2018-03-09 14:59:45 -06:00
Sabe Jones 1bf3736198 chore(i18n): update locales 2018-03-09 20:58:12 +00:00
Travis 92fa6805eb fix: Quest collected items displayed multiple times. (#10111)
fixes #10065
2018-03-09 14:56:59 -06:00
SabreCat d5efa7b2b0 fix(event): clean up more Valentine's content 2018-03-09 20:45:12 +00:00
Dexx Mandele f64b34318f Set warrior as default for the choose class pop-up (#10084) 2018-03-09 14:40:53 -06:00
Sabe Jones 5e13a9c503 4.29.7 2018-03-09 03:31:53 +00:00
Sabe Jones b27aec855c chore(event): end Cupid Potions 2018-03-09 03:31:39 +00:00
Sabe Jones fe61c0f29e Merge branch 'release' into develop 2018-03-08 21:43:35 +00:00
Sabe Jones 2db3ac7bd3 4.29.6 2018-03-08 21:42:52 +00:00
SabreCat 201ec0e865 fix(world-boss): add achievement badge, correct mount positioning 2018-03-08 21:37:17 +00:00
Sabe Jones a0253cf289 chore(i18n): update locales 2018-03-08 19:41:22 +00:00
SabreCat 5256569bac fix(world-boss): remove notification 2018-03-08 19:37:27 +00:00
Sabe Jones 5f0b957dc2 fix(logging): only start Stackimpact in prod (#10112) 2018-03-08 13:12:45 -06:00
Sabe Jones 37650ca674 Merge branch 'release' into develop 2018-03-08 00:30:37 +00:00
Sabe Jones b4dab2e13c Merge branch 'release' into develop 2018-03-07 22:53:09 +00:00
Sabe Jones b827b17481 Don't include seasonal class gear in classless category (#10047)
* fix(market): don't include seasonal class gear in classless category

* refactor(shops): use standard indexOf check
2018-03-06 11:03:49 -06:00
Sabe Jones 27ef187e66 Merge branch 'release' into develop 2018-03-05 20:59:17 +00:00
Keith Holliday 9c9b67aa9d Keys kennel fixes (#9848)
* Show keys to pets immediately

* Ensured keys to pets dissapear after use

* Added resdirect to stable after purchase

* Added mount check and updated keys to mounts and to both

* Added api calls

* Added check for beastmaster progress

* Added mount check for release mounts. Added pets and mount check to release both

* Added actions

* Added catch to common tests

* Added beast count and reload

* Removed extra console log
2018-03-02 15:30:11 -06:00
Keith Holliday 7cff331800 Removed uri for client side oauth (#10058)
* Removed uri for client side oauth

* Fixed lint
2018-03-02 14:07:15 -07:00
Keith Holliday e7bc505b88 Modal popup fixes (#10080)
* Added validation for modal stack

* Lint fixes
2018-03-02 12:43:22 -07:00
1092 changed files with 45077 additions and 36786 deletions
+2 -6
View File
@@ -1,10 +1,6 @@
{
"presets": ["es2015"],
"plugins": [
"transform-object-rest-spread",
["transform-async-to-module-method", {
"module": "bluebird",
"method": "coroutine"
}]
"transform-es2015-modules-commonjs",
"syntax-object-rest-spread",
]
}
+1
View File
@@ -4,6 +4,7 @@ node_modules/**
.bower-registry/**
website/client-old/**
website/client/**
website/client/store/**
website/views/**
website/build/**
dist/**
+1 -1
View File
@@ -1 +1 @@
6
8
+1 -3
View File
@@ -1,6 +1,6 @@
language: node_js
node_js:
- '6'
- '8'
services:
- mongodb
cache:
@@ -8,8 +8,6 @@ cache:
- 'node_modules'
addons:
chrome: stable
before_install:
- npm install -g npm@5
before_script:
- npm run test:build
- cp config.json.example config.json
+1 -4
View File
@@ -1,8 +1,5 @@
FROM node:boron
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
+2 -5
View File
@@ -1,4 +1,4 @@
FROM node:boron
FROM node:8
ENV ADMIN_EMAIL admin@habitica.com
ENV AMAZON_PAYMENTS_CLIENT_ID amzn1.application-oa2-client.68ed9e6904ef438fbc1bf86bf494056e
@@ -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.29.2 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
+4 -3
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",
@@ -112,5 +112,6 @@
"CLOUDKARAFKA_USERNAME": "",
"CLOUDKARAFKA_PASSWORD": "",
"CLOUDKARAFKA_TOPIC_PREFIX": ""
}
},
"STACK_IMPACT_KEY": "aaaabbbbccccddddeeeeffffgggg111100002222"
}
-1
View File
@@ -26,7 +26,6 @@ let improveRepl = (context) => {
const mongooseOptions = !isProd ? {} : {
keepAlive: 1,
connectTimeoutMS: 30000,
useMongoClient: true,
};
mongoose.connect(
nconf.get('NODE_DB_URI'),
+1 -2
View File
@@ -2,7 +2,6 @@ import { exec } from 'child_process';
import psTree from 'ps-tree';
import nconf from 'nconf';
import net from 'net';
import Bluebird from 'bluebird';
import { post } from 'superagent';
import { sync as glob } from 'glob';
import Mocha from 'mocha';
@@ -45,7 +44,7 @@ export function kill (proc) {
* before failing.
*/
export function awaitPort (port, max = 60) {
return new Bluebird((rej, res) => {
return new Promise((rej, res) => {
let socket;
let timeout;
let interval;
@@ -16,7 +16,6 @@
const authorName = 'Blade';
const authorUuid = '75f270e8-c5db-4722-a5e6-a83f1b23f76b';
global.Promise = require('bluebird');
const MongoClient = require('mongodb').MongoClient;
const TaskQueue = require('cwait').TaskQueue;
const logger = require('./utils/logger');
@@ -11,7 +11,6 @@
* pm'ed each user asking if they would like their tasks reset to the previous day
***************************************/
global.Promise = require('bluebird');
const MongoClient = require('mongodb').MongoClient;
const TaskQueue = require('cwait').TaskQueue;
const logger = require('./utils/logger');
@@ -12,7 +12,6 @@
* from an object to a number, hence this migration.
***************************************/
global.Promise = require('bluebird');
const TaskQueue = require('cwait').TaskQueue;
const logger = require('./utils/logger');
const Timer = require('./utils/timer');
@@ -9,7 +9,6 @@
* and transfers a group's progress to it
***************************************/
global.Promise = require('bluebird');
const TaskQueue = require('cwait').TaskQueue;
const logger = require('./utils/logger');
const Timer = require('./utils/timer');
@@ -12,7 +12,6 @@
* <userid>@example.com
***************************************/
global.Promise = require('bluebird');
const TaskQueue = require('cwait').TaskQueue;
const logger = require('./utils/logger');
const Timer = require('./utils/timer');
@@ -9,7 +9,6 @@
* they support a type and options and label
* ***************************************/
global.Promise = require('bluebird');
const TaskQueue = require('cwait').TaskQueue;
const logger = require('./utils/logger');
const Timer = require('./utils/timer');
@@ -12,7 +12,6 @@
* message into the chat for affected parties.
***************************************/
global.Promise = require('bluebird');
const uuid = require('uuid');
const TaskQueue = require('cwait').TaskQueue;
const logger = require('./utils/logger');
@@ -0,0 +1,88 @@
var migrationName = '20171211_sanitize_emails.js';
var authorName = 'Julius'; // in case script author needs to know when their ...
var authorUuid = 'dd16c270-1d6d-44bd-b4f9-737342e79be6'; //... own data is done
/*
User creation saves email as lowercase, but updating an email did not.
Run this script to ensure all lowercased emails in db AFTER fix for updating emails is implemented.
This will fix inconsistent querying for an email when attempting to password reset.
*/
var monk = require('monk');
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
var dbUsers = monk(connectionString).get('users', { castIds: false });
function processUsers(lastId) {
var query = {
'auth.local.email': /[A-Z]/
};
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)
'auth.local.email'
],
})
.then(updateUsers)
.catch(function (err) {
console.log(err);
return exiting(1, 'ERROR! ' + err);
});
}
var progressCount = 1000;
var count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
displayData();
return;
}
var userPromises = users.map(updateUser);
var lastUser = users[users.length - 1];
return Promise.all(userPromises)
.then(function () {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
var push;
var set = {
'auth.local.email': user.auth.local.email.toLowerCase()
};
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;
+3 -1
View File
@@ -1,4 +1,6 @@
If you need to use a migration from this folder, move it to /migrations.
Note that /migrations files (excluding /archive) are linted, so to pass test you'll have to make sure
that the file is written correctly.
that the file is written correctly.
They might also be using some old deps that we don't use anymore like Bluebird, mongoskin, ...
+2 -4
View File
@@ -1,5 +1,3 @@
import Bluebird from 'Bluebird';
import { model as Challenges } from '../../website/server/models/challenge';
import { model as User } from '../../website/server/models/user';
@@ -17,10 +15,10 @@ async function syncChallengeToMembers (challenges) {
promises.push(user.save());
});
return Bluebird.all(promises);
return Promise.all(promises);
});
return await Bluebird.all(challengSyncPromises);
return await Promise.all(challengSyncPromises);
}
async function syncChallenges (lastChallengeDate) {
+1 -3
View File
@@ -1,5 +1,3 @@
import Bluebird from 'bluebird';
import { model as Group } from '../../website/server/models/group';
import { model as User } from '../../website/server/models/user';
@@ -16,7 +14,7 @@ async function createGroup (name, privacy, type, leaderId) {
group.leader = user._id;
user.guilds.push(group._id);
return Bluebird.all([group.save(), user.save()]);
return Promise.all([group.save(), user.save()]);
}
module.exports = async function groupCreator () {
+1 -2
View File
@@ -3,7 +3,6 @@
/*
* This migration will find users with unlimited subscriptions who are also eligible for Jackalope mounts, and award them
*/
import Bluebird from 'bluebird';
import { model as Group } from '../../website/server/models/group';
import { model as User } from '../../website/server/models/user';
@@ -38,7 +37,7 @@ async function handOutJackalopes () {
cursor.on('close', async () => {
console.log('done');
return await Bluebird.all(promises);
return await Promise.all(promises);
});
}
+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;
@@ -9,8 +9,6 @@ let authorUuid = ''; // ... own data is done
* subscription to all members
*/
import Bluebird from 'bluebird';
import { model as Group } from '../../website/server/models/group';
import * as payments from '../../website/server/libs/payments';
@@ -28,7 +26,7 @@ async function updateGroupsWithGroupPlans () {
});
cursor.on('close', async () => {
return await Bluebird.all(promises);
return await Promise.all(promises);
});
}
+3 -3
View File
@@ -1,14 +1,14 @@
require('babel-register');
require('babel-polyfill');
// This file must use ES5, everything required can be in ES6
function setUpServer () {
const nconf = require('nconf'); // eslint-disable-line global-require, no-unused-vars
const mongoose = require('mongoose'); // eslint-disable-line global-require, no-unused-vars
const Bluebird = require('bluebird'); // eslint-disable-line global-require, no-unused-vars
const setupNconf = require('../website/server/libs/setupNconf'); // eslint-disable-line global-require
setupNconf();
// We require src/server and npt src/index because
// 1. nconf is already setup
// 2. we don't need clustering
@@ -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();
+1 -2
View File
@@ -1,4 +1,3 @@
let Bluebird = require('bluebird');
let request = require('superagent');
let last = require('lodash/last');
let AWS = require('aws-sdk');
@@ -74,7 +73,7 @@ function uploadToS3 (start, end, filesUrls) {
});
console.log(promises.length);
return Bluebird.all(promises)
return Promise.all(promises)
.then(() => {
currentIndex += 50;
uploadToS3(currentIndex, currentIndex + 50, filesUrls);
+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;
+1 -1
View File
@@ -5,7 +5,7 @@ const authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is do
/*
* Award this month's mystery items to subscribers
*/
const MYSTERY_ITEMS = ['armor_mystery_201802', 'head_mystery_201802', 'shield_mystery_201802'];
const MYSTERY_ITEMS = ['back_mystery_201803', 'head_mystery_201803'];
const connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
let monk = require('monk');
+4301 -3777
View File
File diff suppressed because it is too large Load Diff
+61 -62
View File
@@ -1,29 +1,24 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "4.29.5",
"version": "4.39.0",
"main": "./website/server/index.js",
"greenkeeper": {
"ignore": [
"mongoose"
]
},
"dependencies": {
"@slack/client": "^3.8.1",
"accepts": "^1.3.2",
"accepts": "^1.3.5",
"amazon-payments": "^0.2.6",
"amplitude": "^3.5.0",
"apidoc": "^0.17.5",
"autoprefixer": "^8.0.0",
"aws-sdk": "^2.200.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",
"babel-eslint": "^8.2.2",
"babel-loader": "^7.1.2",
"babel-loader": "^7.1.4",
"babel-plugin-syntax-async-functions": "^6.13.0",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-plugin-transform-async-to-module-method": "^6.8.0",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
"babel-plugin-transform-object-rest-spread": "^6.16.0",
"babel-plugin-transform-regenerator": "^6.16.1",
"babel-polyfill": "^6.6.1",
@@ -31,24 +26,23 @@
"babel-register": "^6.6.0",
"babel-runtime": "^6.11.6",
"bcrypt": "^1.0.2",
"bluebird": "^3.3.5",
"body-parser": "^1.15.0",
"bootstrap": "^4.0.0",
"bootstrap-vue": "^2.0.0-rc.1",
"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.3",
"css-loader": "^0.28.0",
"csv-stringify": "^2.0.4",
"cross-env": "^5.1.4",
"css-loader": "^0.28.11",
"csv-stringify": "^2.1.0",
"cwait": "^1.1.1",
"domain-middleware": "~0.1.0",
"express": "^4.16.2",
"express": "^4.16.3",
"express-basic-auth": "^1.1.4",
"express-validator": "^5.0.1",
"express-validator": "^5.1.2",
"extract-text-webpack-plugin": "^3.0.2",
"glob": "^7.1.2",
"got": "^8.2.0",
"got": "^8.3.0",
"gulp": "^4.0.0",
"gulp-babel": "^7.0.1",
"gulp-imagemin": "^4.1.0",
@@ -56,24 +50,24 @@
"gulp.spritesmith": "^6.9.0",
"habitica-markdown": "^1.3.0",
"hellojs": "^1.15.1",
"html-webpack-plugin": "^2.8.1",
"html-webpack-plugin": "^3.2.0",
"image-size": "^0.6.2",
"in-app-purchase": "^1.1.6",
"in-app-purchase": "^1.9.0",
"intro.js": "^2.6.0",
"jquery": ">=3.0.0",
"js2xmlparser": "^3.0.0",
"lodash": "^4.17.4",
"memwatch-next": "^0.3.0",
"merge-stream": "^1.0.0",
"method-override": "^2.3.5",
"moment": "^2.13.0",
"moment": "^2.22.0",
"moment-recur": "^1.0.7",
"mongoose": "^4.13.11",
"mongoose": "^5.0.14",
"morgan": "^1.7.0",
"nconf": "^0.10.0",
"node-gcm": "^0.14.4",
"node-rdkafka": "^2.2.3",
"node-sass": "^4.5.0",
"nodemailer": "^4.5.0",
"node-sass": "^4.8.3",
"nodemailer": "^4.6.4",
"ora": "^2.0.0",
"pageres": "^4.1.1",
"passport": "^0.4.0",
@@ -81,46 +75,46 @@
"passport-google-oauth20": "1.0.0",
"paypal-ipn": "3.0.0",
"paypal-rest-sdk": "^1.8.1",
"popper.js": "^1.13.0",
"popper.js": "^1.14.3",
"postcss-easy-import": "^3.0.0",
"ps-tree": "^1.0.0",
"pug": "^2.0.0-rc.4",
"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.2",
"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.0.2",
"svgo": "^1.0.4",
"svg-url-loader": "^2.3.2",
"svgo": "^1.0.5",
"svgo-loader": "^2.1.0",
"universal-analytics": "^0.4.16",
"url-loader": "^0.6.2",
"url-loader": "^1.0.0",
"useragent": "^2.1.9",
"uuid": "^3.0.1",
"validator": "^9.4.1",
"vinyl-buffer": "^1.0.1",
"vue": "^2.5.2",
"vue-loader": "^14.1.1",
"vue": "^2.5.16",
"vue-loader": "^14.2.2",
"vue-mugen-scroll": "^0.2.1",
"vue-router": "^3.0.0",
"vue-style-loader": "^4.0.2",
"vue-template-compiler": "^2.5.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",
"webpack": "^3.11.0",
"webpack-merge": "^4.0.0",
"winston": "^2.1.0",
"winston": "^2.4.1",
"winston-loggly-bulk": "^2.0.2",
"xml2js": "^0.4.4"
},
"private": true,
"engines": {
"node": "^6.9.1",
"npm": "^5.0.0"
"node": "^8.9.4",
"npm": "^5.6.0"
},
"scripts": {
"lint": "eslint --ext .js,.vue .",
@@ -147,23 +141,25 @@
"apidoc": "gulp apidoc"
},
"devDependencies": {
"babel-plugin-istanbul": "^4.0.0",
"@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.1",
"chromedriver": "^2.27.2",
"chalk": "^2.3.2",
"chromedriver": "^2.37.0",
"connect-history-api-fallback": "^1.1.0",
"coveralls": "^3.0.0",
"cross-spawn": "^6.0.4",
"eslint": "^4.18.1",
"cross-spawn": "^6.0.5",
"eslint": "^4.19.1",
"eslint-config-habitrpg": "^4.0.0",
"eslint-friendly-formatter": "^3.0.0",
"eslint-loader": "^1.3.0",
"eslint-plugin-html": "^4.0.2",
"eslint-plugin-mocha": "^4.7.0",
"eslint-friendly-formatter": "^4.0.1",
"eslint-loader": "^2.0.0",
"eslint-plugin-html": "^4.0.3",
"eslint-plugin-mocha": "^5.0.0",
"eventsource-polyfill": "^0.9.6",
"expect.js": "^0.3.1",
"http-proxy-middleware": "^0.17.0",
"http-proxy-middleware": "^0.18.0",
"istanbul": "^1.1.0-alpha.1",
"karma": "^2.0.0",
"karma-babel-preprocessor": "^7.0.0",
@@ -172,23 +168,26 @@
"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.2",
"karma-webpack": "^3.0.0",
"lcov-result-merger": "^2.0.0",
"mocha": "^5.0.1",
"mocha": "^5.0.5",
"monk": "^6.0.5",
"nightwatch": "^0.9.12",
"puppeteer": "^1.1.0",
"nightwatch": "^0.9.20",
"puppeteer": "^1.3.0",
"require-again": "^2.0.0",
"selenium-server": "^3.9.1",
"sinon": "^4.3.0",
"sinon-chai": "^2.8.0",
"selenium-server": "^3.11.0",
"sinon": "^4.5.0",
"sinon-chai": "^3.0.0",
"sinon-stub-promise": "^4.0.0",
"webpack-bundle-analyzer": "^2.2.1",
"webpack-bundle-analyzer": "^2.11.1",
"webpack-dev-middleware": "^2.0.5",
"webpack-hot-middleware": "^2.6.1"
"webpack-hot-middleware": "^2.22.0"
},
"optionalDependencies": {
"node-rdkafka": "^2.3.0"
}
}
-1
View File
@@ -1,5 +1,4 @@
require('babel-register');
require('babel-polyfill');
// This file is used for creating paypal billing plans. PayPal doesn't have a web interface for setting up recurring
// payment plan definitions, instead you have to create it via their REST SDK and keep it updated the same way. So this
@@ -151,7 +151,10 @@ describe('GET challenges/groups/:groupId', () => {
});
officialChallenge = await generateChallenge(user, group, {
official: true,
categories: [{
name: 'habitica_official',
slug: 'habitica_official',
}],
});
challenge = await generateChallenge(user, group);
@@ -193,7 +193,10 @@ describe('GET challenges/user', () => {
});
officialChallenge = await generateChallenge(user, group, {
official: true,
categories: [{
name: 'habitica_official',
slug: 'habitica_official',
}],
});
challenge = await generateChallenge(user, group);
@@ -224,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 () => {
@@ -2,13 +2,12 @@ import {
generateUser,
} from '../../../../helpers/api-v3-integration.helper';
import xml2js from 'xml2js';
import Bluebird from 'bluebird';
import util from 'util';
let parseStringAsync = Bluebird.promisify(xml2js.parseString, {context: xml2js});
let parseStringAsync = util.promisify(xml2js.parseString).bind(xml2js);
describe('GET /export/userdata.xml', () => {
// TODO disabled because it randomly causes the build to fail
xit('should return a valid XML file with user data', async () => {
it('should return a valid XML file with user data', async () => {
let user = await generateUser();
let tasks = await user.post('/tasks/user', [
{type: 'habit', text: 'habit 1'},
@@ -31,13 +30,21 @@ describe('GET /export/userdata.xml', () => {
expect(res).to.contain.all.keys(['tasks', 'flags', 'tasksOrder', 'auth']);
expect(res.auth.local).not.to.have.keys(['salt', 'hashed_password']);
expect(res.tasks).to.have.all.keys(['dailys', 'habits', 'todos', 'rewards']);
expect(res.tasks.habits.length).to.equal(2);
expect(res.tasks.habits[0]._id).to.equal(tasks[0]._id);
let habitIds = _.map(res.tasks.habits, '_id');
expect(habitIds).to.have.deep.members([tasks[0]._id, tasks[4]._id]);
expect(res.tasks.dailys.length).to.equal(2);
expect(res.tasks.dailys[0]._id).to.equal(tasks[1]._id);
let dailyIds = _.map(res.tasks.dailys, '_id');
expect(dailyIds).to.have.deep.members([tasks[1]._id, tasks[5]._id]);
expect(res.tasks.rewards.length).to.equal(2);
expect(res.tasks.rewards[0]._id).to.equal(tasks[2]._id);
let rewardIds = _.map(res.tasks.rewards, '_id');
expect(rewardIds).to.have.deep.members([tasks[2]._id, tasks[6]._id]);
expect(res.tasks.todos.length).to.equal(3);
expect(res.tasks.todos[1]._id).to.equal(tasks[3]._id);
let todoIds = _.map(res.tasks.todos, '_id');
expect(todoIds).to.deep.include.members([tasks[3]._id, tasks[7]._id]);
});
});
@@ -201,8 +201,8 @@ describe('GET /groups', () => {
await expect(user.get('/groups?type=publicGuilds&paginate=true&page=1'))
.to.eventually.have.a.lengthOf(GUILD_PER_PAGE);
let page2 = await expect(user.get('/groups?type=publicGuilds&paginate=true&page=2'))
.to.eventually.have.a.lengthOf(1 + 3); // 1 created now, 3 by other tests
expect(page2[3].name).to.equal('guild with less members');
.to.eventually.have.a.lengthOf(1 + 4); // 1 created now, 4 by other tests
expect(page2[4].name).to.equal('guild with less members');
});
});
@@ -220,4 +220,18 @@ describe('GET /groups', () => {
await expect(user.get('/groups?type=privateGuilds,publicGuilds,party,tavern'))
.to.eventually.have.lengthOf(NUMBER_OF_GROUPS_USER_CAN_VIEW);
});
it('returns a list of groups user has access to', async () => {
let group = await generateGroup(user, {
name: 'c++ coders',
type: 'guild',
privacy: 'public',
});
// search for 'c++ coders'
await expect(user.get('/groups?type=publicGuilds&paginate=true&page=0&search=c%2B%2B+coders'))
.to.eventually.have.lengthOf(1)
.and.to.have.nested.property('[0]')
.and.to.have.property('_id', group._id);
});
});
@@ -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 });
@@ -44,12 +44,12 @@ describe('POST /group/:groupId/join', () => {
expect(res.leader.profile.name).to.eql(user.profile.name);
});
it('returns an error is user was already a member', async () => {
it('returns an error if user was already a member', async () => {
await joiningUser.post(`/groups/${publicGuild._id}/join`);
await expect(joiningUser.post(`/groups/${publicGuild._id}/join`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('userAlreadyInGroup'),
message: t('youAreAlreadyInGroup'),
});
});
@@ -262,6 +262,30 @@ describe('POST /group/:groupId/join', () => {
await expect(checkExistence('groups', oldParty._id)).to.eventually.equal(false);
});
it('does not allow user to leave a party if a quest was active and they were the only member', async () => {
let userToInvite = await generateUser();
let oldParty = await userToInvite.post('/groups', { // add user to a party
name: 'Another Test Party',
type: 'party',
});
await userToInvite.update({
[`items.quests.${PET_QUEST}`]: 1,
});
await userToInvite.post(`/groups/${oldParty._id}/quests/invite/${PET_QUEST}`);
await expect(checkExistence('groups', oldParty._id)).to.eventually.equal(true);
await user.post(`/groups/${party._id}/invite`, {
uuids: [userToInvite._id],
});
await expect(userToInvite.post(`/groups/${party._id}/join`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageCannotLeaveWhileQuesting'),
});
});
it('invites joining member to active quest', async () => {
await user.update({
[`items.quests.${PET_QUEST}`]: 1,
@@ -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';
@@ -2,8 +2,9 @@ import {
createAndPopulateGroup,
translate as t,
generateUser,
sleep,
} from '../../../../helpers/api-v3-integration.helper';
import Bluebird from 'bluebird';
import { model as Chat } from '../../../../../website/server/models/chat';
describe('POST /groups/:groupId/quests/accept', () => {
const PET_QUEST = 'whale';
@@ -140,7 +141,7 @@ describe('POST /groups/:groupId/quests/accept', () => {
// quest will start after everyone has accepted
await partyMembers[1].post(`/groups/${questingGroup._id}/quests/accept`);
await Bluebird.delay(500);
await sleep(0.5);
await rejectingMember.sync();
@@ -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;
@@ -2,8 +2,9 @@ import {
createAndPopulateGroup,
translate as t,
generateUser,
sleep,
} from '../../../../helpers/api-v3-integration.helper';
import Bluebird from 'bluebird';
import { model as Chat } from '../../../../../website/server/models/chat';
describe('POST /groups/:groupId/quests/force-start', () => {
const PET_QUEST = 'whale';
@@ -135,7 +136,7 @@ describe('POST /groups/:groupId/quests/force-start', () => {
await leader.post(`/groups/${questingGroup._id}/quests/force-start`);
await Bluebird.delay(500);
await sleep(0.5);
await Promise.all([
partyMemberThatRejects.sync(),
@@ -161,7 +162,7 @@ describe('POST /groups/:groupId/quests/force-start', () => {
await leader.post(`/groups/${questingGroup._id}/quests/force-start`);
await Bluebird.delay(500);
await sleep(0.5);
await questingGroup.sync();
@@ -184,7 +185,7 @@ describe('POST /groups/:groupId/quests/force-start', () => {
await leader.post(`/groups/${questingGroup._id}/quests/force-start`);
await Bluebird.delay(500);
await sleep(0.5);
await questingGroup.sync();
@@ -201,7 +202,7 @@ describe('POST /groups/:groupId/quests/force-start', () => {
await leader.post(`/groups/${questingGroup._id}/quests/force-start`);
await Bluebird.delay(500);
await sleep(0.5);
await questingGroup.sync();
@@ -222,7 +223,7 @@ describe('POST /groups/:groupId/quests/force-start', () => {
await leader.post(`/groups/${questingGroup._id}/quests/force-start`);
await Bluebird.delay(500);
await sleep(0.5);
await questingGroup.sync();
@@ -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([
@@ -2,9 +2,10 @@ import {
createAndPopulateGroup,
translate as t,
generateUser,
sleep,
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
import Bluebird from 'bluebird';
import { model as Chat } from '../../../../../website/server/models/chat';
describe('POST /groups/:groupId/quests/reject', () => {
let questingGroup;
@@ -168,7 +169,7 @@ describe('POST /groups/:groupId/quests/reject', () => {
// quest will start after everyone has accepted or rejected
await rejectingMember.post(`/groups/${questingGroup._id}/quests/reject`);
await Bluebird.delay(500);
await sleep(0.5);
await questingGroup.sync();
@@ -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;
@@ -2,7 +2,6 @@ import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
import Bluebird from 'bluebird';
describe('GET /shops/market', () => {
let user;
@@ -42,13 +41,13 @@ describe('GET /shops/market', () => {
return array;
}, []);
let results = await Bluebird.each(items, (item) => {
let results = await Promise.all(items.map((item) => {
let { purchaseType, key } = item;
return user.post(`/user/purchase/${purchaseType}/${key}`);
});
}));
expect(results.length).to.be.greaterThan(0);
results.forEach((item) => {
items.forEach((item) => {
expect(item).to.include.keys('key', 'text', 'notes', 'class', 'value', 'currency');
});
});
@@ -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', () => {
@@ -2,8 +2,8 @@ import {
generateUser,
generateGroup,
generateChallenge,
sleep,
} from '../../../../../helpers/api-integration/v3';
import Bluebird from 'bluebird';
import { find } from 'lodash';
describe('POST /tasks/:id/score/:direction', () => {
@@ -27,7 +27,7 @@ describe('POST /tasks/:id/score/:direction', () => {
text: 'test habit',
type: 'habit',
});
await Bluebird.delay(1000);
await sleep(1);
let updatedUser = await user.get('/user');
usersChallengeTaskId = updatedUser.tasksOrder.habits[0];
});
@@ -65,7 +65,7 @@ describe('POST /tasks/:id/score/:direction', () => {
text: 'test daily',
type: 'daily',
});
await Bluebird.delay(1000);
await sleep(1);
let updatedUser = await user.get('/user');
usersChallengeTaskId = updatedUser.tasksOrder.dailys[0];
});
@@ -109,7 +109,7 @@ describe('POST /tasks/:id/score/:direction', () => {
text: 'test todo',
type: 'todo',
});
await Bluebird.delay(1000);
await sleep(1);
let updatedUser = await user.get('/user');
usersChallengeTaskId = updatedUser.tasksOrder.todos[0];
});
@@ -134,7 +134,7 @@ describe('POST /tasks/:id/score/:direction', () => {
text: 'test reward',
type: 'reward',
});
await Bluebird.delay(1000);
await sleep(1);
let updatedUser = await user.get('/user');
usersChallengeTaskId = updatedUser.tasksOrder.todos[0];
});
@@ -11,7 +11,6 @@ import {
each,
map,
} from 'lodash';
import Bluebird from 'bluebird';
import {
sha1MakeSalt,
sha1Encrypt as sha1EncryptPassword,
@@ -104,7 +103,7 @@ describe('DELETE /user', () => {
password,
});
await Bluebird.all(map(ids, id => {
await Promise.all(map(ids, id => {
return expect(checkExistence('tasks', id)).to.eventually.eql(false);
}));
});
@@ -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,24 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('GET /user/in-app-rewards', () => {
let user;
before(async () => {
user = await generateUser();
});
it('returns the reward items available for purchase', async () => {
let buyList = await user.get('/user/in-app-rewards');
expect(_.find(buyList, item => {
return item.text === t('armorWarrior1Text');
})).to.exist;
expect(_.find(buyList, item => {
return item.text === t('armorWarrior2Text');
})).to.not.exist;
});
});
@@ -0,0 +1,27 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('GET /user/toggle-pinned-item', () => {
let user;
before(async () => {
user = await generateUser();
});
it('cannot unpin potion', async () => {
await expect(user.get('/user/toggle-pinned-item/potion/potion'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('cannotUnpinArmoirPotion'),
});
});
it('can pin shield_rogue_5', async () => {
let result = await user.get('/user/toggle-pinned-item/marketGear/gear.flat.shield_rogue_5');
expect(result.pinnedItems.length).to.be.eql(user.pinnedItems.length + 1);
});
});
@@ -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');
@@ -258,11 +289,31 @@ describe('POST /user/class/cast/:spellId', () => {
expect(user.achievements.birthday).to.equal(1);
});
it('passes correct target to spell when targetType === \'task\'', async () => {
await user.update({'stats.class': 'wizard', 'stats.lvl': 11});
let task = await user.post('/tasks/user', {
text: 'test habit',
type: 'habit',
});
let result = await user.post(`/user/class/cast/fireball?targetId=${task._id}`);
expect(result.task._id).to.equal(task._id);
});
it('passes correct target to spell when targetType === \'self\'', async () => {
await user.update({'stats.class': 'wizard', 'stats.lvl': 14, 'stats.mp': 50});
let result = await user.post('/user/class/cast/frost');
expect(result.user.stats.mp).to.equal(10);
});
// TODO find a way to have sinon working in integration tests
// it doesn't work when tests are running separately from server
it('passes correct target to spell when targetType === \'task\'');
it('passes correct target to spell when targetType === \'tasks\'');
it('passes correct target to spell when targetType === \'self\'');
it('passes correct target to spell when targetType === \'party\'');
it('passes correct target to spell when targetType === \'user\'');
it('passes correct target to spell when targetType === \'party\' and user is not in a party');
@@ -30,10 +30,12 @@ describe('POST /user/release-both', () => {
'items.currentPet': animal,
'items.pets': loadPets(),
'items.mounts': loadMounts(),
'achievements.triadBingo': true,
});
});
it('returns an error when user balance is too low and user does not have triadBingo', async () => {
// @TODO: Traid is now free. Add this back if we need
xit('returns an error when user balance is too low and user does not have triadBingo', async () => {
await expect(user.post('/user/release-both'))
.to.eventually.be.rejected.and.to.eql({
code: 401,
@@ -45,9 +47,7 @@ describe('POST /user/release-both', () => {
// More tests in common code unit tests
it('grants triad bingo with gems', async () => {
await user.update({
balance: 1.5,
});
await user.update();
let response = await user.post('/user/release-both');
await user.sync();
@@ -27,6 +27,33 @@ describe('PUT /user', () => {
expect(user.stats.hp).to.eql(14);
});
it('tags must be an array', async () => {
await expect(user.put('/user', {
tags: {
tag: true,
},
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'mustBeArray',
});
});
it('update tags', async () => {
let userTags = user.tags;
await user.put('/user', {
tags: [...user.tags, {
name: 'new tag',
}],
});
await user.sync();
expect(user.tags.length).to.be.eql(userTags.length + 1);
});
it('profile.name cannot be an empty string or null', async () => {
await expect(user.put('/user', {
'profile.name': ' ', // string should be trimmed
@@ -357,6 +357,21 @@ describe('POST /user/auth/local/register', () => {
});
});
it('sanitizes email params to a lowercase string before creating the user', async () => {
let username = generateRandomUserName();
let email = 'ISANEmAiL@ExAmPle.coM';
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.auth.local.email).to.equal(email.toLowerCase());
});
it('fails on a habitica.com email', async () => {
let username = generateRandomUserName();
let email = `${username}@habitica.com`;
@@ -13,7 +13,7 @@ import nconf from 'nconf';
const ENDPOINT = '/user/auth/update-email';
describe('PUT /user/auth/update-email', () => {
let newEmail = 'some-new-email_2@example.net';
let newEmail = 'SOmE-nEw-emAIl_2@example.net';
let oldPassword = 'password'; // from habitrpg/test/helpers/api-integration/v3/object-generators.js
context('Local Authenticaion User', async () => {
@@ -53,14 +53,15 @@ describe('PUT /user/auth/update-email', () => {
});
it('changes email if new email and existing password are provided', async () => {
let lowerCaseNewEmail = newEmail.toLowerCase();
let response = await user.put(ENDPOINT, {
newEmail,
password: oldPassword,
});
expect(response).to.eql({ email: 'some-new-email_2@example.net' });
expect(response.email).to.eql(lowerCaseNewEmail);
await user.sync();
expect(user.auth.local.email).to.eql(newEmail);
expect(user.auth.local.email).to.eql(lowerCaseNewEmail);
});
it('rejects if email is already taken', async () => {
@@ -32,4 +32,11 @@ describe('GET /world-state', () => {
},
});
});
it('returns a string representing the current season for NPC sprites', async () => {
const res = await requester().get('/world-state');
expect(res).to.have.nested.property('npcImageSuffix');
expect(res.npcImageSuffix).to.be.a('string');
});
});
+466 -38
View File
@@ -1,7 +1,6 @@
/* eslint-disable global-require */
import moment from 'moment';
import nconf from 'nconf';
import Bluebird from 'bluebird';
import requireAgain from 'require-again';
import { recoverCron, cron } from '../../../../../website/server/libs/cron';
import { model as User } from '../../../../../website/server/models/user';
@@ -24,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
},
@@ -83,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;
@@ -118,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();
@@ -144,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;
@@ -185,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', () => {
@@ -1349,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
},
@@ -1363,7 +1791,7 @@ describe('recoverCron', () => {
});
it('throws an error if user cannot be found', async () => {
execStub.returns(Bluebird.resolve(null));
execStub.returns(Promise.resolve(null));
try {
await recoverCron(status, locals);
@@ -1374,8 +1802,8 @@ describe('recoverCron', () => {
});
it('increases status.times count and reruns up to 4 times', async () => {
execStub.returns(Bluebird.resolve({_cronSignature: 'RUNNING_CRON'}));
execStub.onCall(4).returns(Bluebird.resolve({_cronSignature: 'NOT_RUNNING'}));
execStub.returns(Promise.resolve({_cronSignature: 'RUNNING_CRON'}));
execStub.onCall(4).returns(Promise.resolve({_cronSignature: 'NOT_RUNNING'}));
await recoverCron(status, locals);
@@ -1384,7 +1812,7 @@ describe('recoverCron', () => {
});
it('throws an error if recoverCron runs 5 times', async () => {
execStub.returns(Bluebird.resolve({_cronSignature: 'RUNNING_CRON'}));
execStub.returns(Promise.resolve({_cronSignature: 'RUNNING_CRON'}));
try {
await recoverCron(status, locals);
@@ -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;

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