Compare commits

..

205 Commits

Author SHA1 Message Date
Sabe Jones 3606b58a1d 4.44.1 2018-05-31 20:04:47 +00:00
Sabe Jones 202db599ae chore(i18n): update locales 2018-05-31 20:03:06 +00:00
Sabe Jones 3aca0343e8 Merge branch 'release' into develop 2018-05-29 22:54:31 +00:00
Sabe Jones 97b99c0550 fix(tests): correct for new content, fix lint 2018-05-29 22:54:00 +00:00
Sabe Jones 0e63f68ed6 Merge branch 'release' into develop 2018-05-29 22:24:41 +00:00
Sabe Jones fa142e929f 4.44.0 2018-05-29 22:17:33 +00:00
Sabe Jones c4867f1e8e chore(i18n): update locales 2018-05-29 22:16:30 +00:00
SabreCat 5f58fe66de chore(sprites): compile + add news 2018-05-29 22:11:02 +00:00
SabreCat a0e2d6a05e feat(customize): earrings and headbands 2018-05-29 21:02:42 +00:00
Matteo Pagliazzi b67522e92b fix(contact form): add it back, fixes #10401 2018-05-29 19:28:28 +02:00
Matteo Pagliazzi 0e3496395c fix(challenges): fix display issues, fixes #10397 2018-05-29 19:25:43 +02:00
Matteo Pagliazzi 6e7b9f1f93 fix(due date): update value correctly, fixes #10405 2018-05-28 13:54:13 +02:00
Matteo Pagliazzi e6cf7564b8 fix(i18n): pass path to wrongItemPath string, fixes #10403 2018-05-28 13:40:49 +02:00
Matteo Pagliazzi bf424573a4 Members: user .lean() to improve performances (#10399)
* perf(members): use lean where possible

* fix unit tests

* fix unit tests and update calls to old function

* simplify code and add tests
2018-05-28 13:38:59 +02:00
Brian Fenton ac90a40be5 Api quest restrictions - no purchase/start without fulfilling eligibility requirements (#10387)
* removing duplicate translation key

* fixing typos

* extracting quest prerequisite check. adding check for previous quest completion, if required

* fixing (undoing) static change, adding tests

* more typos

* correcting test failures

* honoring quest prerequisites in quest invite API call. updating format of il8n string replacement arg

* no longer using apiError, use translate method instead (msg key was not defined)

* adding @apiError to docblock as requested in issue

* removing checks on quest invite method. small window of opportunity/low risk
2018-05-27 16:41:56 +02:00
Matteo Pagliazzi 821f84dbe8 fix(facebook): include email 2018-05-25 18:54:50 +02:00
Matteo Pagliazzi 8fb67e7944 only store necessary data for social login (continuation of 10352) (#10395)
* feat(gdpr) only store necessary data for social login

* feat(gdpr) also store email for social users

* fix(social auth): store emails array instead of single email

* fix(emails): do not get name from old facebook info

* add migration to remove extra data from social profiles

* update migration description

* fix tests

* fix typo in migration file
2018-05-25 18:16:30 +02:00
Matteo Pagliazzi e81e458e9b Merge branch 'pengfluf-PM_opt-in-out' into develop 2018-05-25 12:44:53 +02:00
Matteo Pagliazzi aec23d32f3 Merge branch 'PM_opt-in-out' of https://github.com/pengfluf/habitica into pengfluf-PM_opt-in-out 2018-05-25 12:44:44 +02:00
aszlig 4f2d066d66 client: Fix display of class bonus for other users (#10376)
Whenever one is hovering an item from another user, the bonuses of these
items are shown for the own user. So for example if you're a mage and
view a Royal Magus Robe of another mage, the class bonus is 6.

However if you're a warrior, the class bonus displays as 0 because the
attributes grid is always using the stats for the own user even if
viewing equipment of a different user.

I've fixed this by moving the user object to the properties in
attributesGrid and passing the current user from every other Vue file
that's using attributesGrid.

Not sure whether this is the right approach, as I'm no expert in Vue.js
but some testing with the client now shows the correct values.

Signed-off-by: aszlig <aszlig@nix.build>
2018-05-25 12:38:02 +02:00
Matteo Pagliazzi eaf0c62e16 fix(errors): snackbars for 502 and notification not found errors should have timeout (#10394) 2018-05-25 12:30:43 +02:00
Matteo Pagliazzi fc62db147f fix(emails): make sure quest invitations are sent to users that signed up with google, fixes #10389 (#10393) 2018-05-25 12:13:02 +02:00
Keith Holliday 6c9ff3e8ed Ensure leader is set (#10390) 2018-05-25 12:04:07 +02:00
Matteo Pagliazzi 6ef45a7fd2 Fix 9248: challenge creator should not automatically join their own challenge (#10383)
* fix(challenges): creator should not join challenge automatically

* change behavior on the client side as well

* update tests and fix membercount

* update tests

* fix tests
2018-05-25 12:03:39 +02:00
Sabe Jones 557212b549 4.43.1 2018-05-24 18:55:14 +00:00
Sabe Jones f8bd116e54 chore(npm): update package lock 2018-05-24 18:54:59 +00:00
Sabe Jones 9194e8226d 4.43.0 2018-05-24 18:34:13 +00:00
Sabe Jones e0140f67be chore(i18n): update locales 2018-05-24 18:33:40 +00:00
SabreCat 1e2fc14db9 Merge branch 'develop' into release 2018-05-24 18:26:21 +00:00
SabreCat 30082a3929 chore(sprites): compile 2018-05-24 18:25:56 +00:00
SabreCat 42d7744d12 feat(content): May Subscriber Items 2018-05-24 18:25:36 +00:00
Matteo Pagliazzi 2cbc41d02f fix(challenges); update category labels when filtering, fixes #10382 2018-05-21 20:58:01 +02:00
Alys 01ce7712e3 change "Advanced Options" to "Advanced Settings" in Settings screen to match change in wording on tasks page 2018-05-21 20:20:06 +10:00
Keith Holliday c52e4a07d4 Removed redirect uri (#10380)
* Removed redirect uri

* Fixed lint
2018-05-20 13:02:37 -05:00
Matteo Pagliazzi 5212ac6394 fix(tests): do not use arrow function when using this 2018-05-19 21:10:36 +02:00
Matteo Pagliazzi 7b5d6b508d fix(tests): longer timeout for invites 2018-05-19 20:45:56 +02:00
Matteo Pagliazzi c5a497ef91 fix(settings): when changing language, reload page entirely, fixes #9904 2018-05-19 20:22:30 +02:00
Matteo Pagliazzi 54bee67e03 fix(settings): language can be changed in firefox, fixes #9514 2018-05-19 20:17:48 +02:00
Matteo Pagliazzi 86ec68bedb Merge branch 'develop' of github.com:HabitRPG/habitica into develop 2018-05-19 20:03:36 +02:00
Matteo Pagliazzi 8223563e76 fix(audio): rename todo audio files with wrong casing, fixes #10294 2018-05-19 20:03:19 +02:00
Keith Holliday 37ab257f5b Added responsive fixes to home page (#10381) 2018-05-19 11:43:19 -05:00
Keith Holliday 04d7ff13de Refactored stripe checkout (#10345)
* Refactored stripe checkout

* Fixed dependency injection cache
2018-05-19 10:31:26 -05:00
Sabe Jones 25d07ac0ce fix(snackbars): don't timeout server error snacks (#10372)
Fixes #10031 and #9249.
2018-05-18 14:41:15 -05:00
SabreCat 724e1240a3 fix(content): update potion end date 2018-05-18 18:40:04 +00:00
Sabe Jones 026e1a5bca Merge branch 'release' into develop 2018-05-18 17:15:16 +00:00
Sabe Jones 6443918440 4.42.6 2018-05-18 17:14:39 +00:00
Matteo Pagliazzi ac973ee753 fix(tests): do not error when test chat messages miss the timestamp attribute 2018-05-18 18:04:32 +02:00
Matteo Pagliazzi c39b9dc320 fix(challenges): format summary with markdown and do not split words, fixes #10371 2018-05-18 17:33:38 +02:00
Matteo Pagliazzi 2132a3a242 fix(menu): correct padding 2018-05-18 17:30:15 +02:00
Dexx Mandele ba52a90d93 Make nav drop-down cleaner/scrollable (#10138)
* Make nav drop-down cleaner/scrollable

* Stop overriding bootstrap navbar

* Move user menu to top bar in mobile

* Restructure/style first drop-down item

* Add ALL the pretty colors

* Apply menu drop-down re-structure to all menu drop-downs

* Replace curly brace lost during rebase
2018-05-18 17:11:25 +02:00
Brian Fenton daa4994382 Change reward popunder (#10358)
* making add multiple tip reflect the task type

* removing duplicated key
2018-05-18 17:11:04 +02:00
Mateus Etto 12034161b7 Remove Ethereal Surge notification (#10368) 2018-05-18 17:09:00 +02:00
Ian Oxley 8438cf0578 Fix drawer text overlapping at smaller screen resolutions (#10360)
* Replace divs with semantic markup

Replace `<div>` tags with `<nav>`, `<aside>`, and a list for the nav items.

* Use grid layout

Replace flexbox with CSS grid layout. The right-hand side item is now in its own
grid cell, so the text wraps inside its cell at smaller screen widths.

Undo `<nav>` tag.

* Sort CSS

Sort the remaining CSS property declarations.

* Fix right alignment issue in Safari

Remove `justify-self: end` to fix the right alignment issue in Safari.

* Fix vertical alignment in Edge

Add `align-self: center` but only for MS Edge.

Also removed `position: relative` on the wrapper element for the tabs.
As the help item isn't using absolute positioning anymore we don't need
to set relative positioning on the parent element.
2018-05-18 17:07:42 +02:00
jerellmendoodoo 614848d60b added try catch, added snackbar notification for errors returned (#10370)
* added try catch, added snackbar notification for errors returned

* moved lines into try block
2018-05-18 17:04:18 +02:00
aszlig 79087b27d3 redirects: Fix parsing BASE_URL with port number (#10350)
The parsing in the redirects module was simply determining the base host
via trimming off everything up to //, so a BASE_URL like
"http://localhost:3000" will result in the host name "localhost:3000",
which isn't a valid host name.

So the problem here is that BASE_URL_HOST is used for determining
whether the client should be redirected and it's comparing the hostname
of the request object with BASE_URL_HOST.

For example if we have the aforementioned BASE_URL, we get to the
following comparison:

req.hostname !== BASE_URL_HOST

Which expands to:

"localhost" !== "localhost:3000"

So in order to get rid of the port number, we now use url.parse() to get
the right host name.

Signed-off-by: aszlig <aszlig@nix.build>
2018-05-18 17:02:36 +02:00
aszlig 5167f847d0 tests: Increase timeouts instead of disabling them (#10367)
Some tests were disabled in ba799c67f9 and
10567d81e2, because they tend to
frequently time out after 8 seconds.

Instead of disabling the tests (which IMHO is bad, because tests are
there for a reason), we're now increasing the timeout to 30 seconds just
for these tests.

As requested by @paglias, I've marked the timeout functions with a @TODO
comment, so that the slow tests or the functionality they're testing are
eventually refactored.

I also needed to change the arrow notation for the test cases to use the
function keyword, because otherwise we don't have this.timeout()
available.

Signed-off-by: aszlig <aszlig@nix.build>
Cc: @paglias
2018-05-18 17:02:20 +02:00
aszlig d3a0348ac7 Avoid using media element with empty src attribute (#10364)
Whenever the client starts up, the following is emitted in the Firefox
console:

Invalid URI. Load of media resource  failed.
All candidate resources failed to load. Media load paused.

This happens because the <source/> tags are preinitialized with a src
attribute of "".

So what we're doing instead is initialize the <audio/> element without
any children and add the children as soon as the first audio file needs
to be played. This also has the advantage that we can determine at
runtime whether the browser supports Ogg/Vorbis or whether we should
fall back to MPEG layer 3 so only one source element is needed.

Signed-off-by: aszlig <aszlig@nix.build>
2018-05-18 17:01:27 +02:00
negue de9883c3ac extract chat (#10362)
* extract chatTextarea from group/tavern - extract staffList array

* fix lint / rewrite condition

* clean up - part 1

* rename chatTextarea to chat

* refactor timestamp check
2018-05-18 17:01:05 +02:00
negue 3d39718048 Purchase API Refactoring: Spells [Gold] (#10305)
* convert buySpell operation

* remove purchaseWithSpell - change purchaseType 'special' to 'spells' - fix lint

* fix tests

* rollback 'spells' to 'special'
2018-05-18 17:00:39 +02:00
Matteo Pagliazzi a0c51ee4ca fix(loading bar): always above other elements 2018-05-18 13:42:44 +02:00
Sabe Jones b2edd1d932 4.42.5 2018-05-17 20:58:16 +00:00
Sabe Jones 6b5f46c5e1 chore(i18n): update locales 2018-05-17 20:57:57 +00:00
Alys ad191c2c5c change apidoc to explain that the equip route also unequips 2018-05-16 20:45:35 +10:00
Sabe Jones 4a55d36831 Merge branch 'release' into develop 2018-05-15 21:11:54 +00:00
Sabe Jones 959adb05cf 4.42.4 2018-05-15 21:11:35 +00:00
Sabe Jones d114b858fd chore(i18n): update locales 2018-05-15 21:06:11 +00:00
SabreCat ae9db7aee3 feat(content): enable Fairy Potions 2018-05-15 21:00:49 +00:00
Matteo Pagliazzi 10567d81e2 fix(tests): remove tests that timeout 2018-05-15 18:03:00 +02:00
Matteo Pagliazzi ba799c67f9 fix(tests): remove tests that timeout 2018-05-15 17:42:27 +02:00
Matteo Pagliazzi 37b890f282 fix(market): fixes #10316 2018-05-15 17:21:15 +02:00
Matteo Pagliazzi 196e5f5b95 upgrade deps 2018-05-15 17:00:17 +02:00
Matteo Pagliazzi 6db412f7e6 fix tags checkbox in bootstrap 4.1.1 2018-05-15 16:55:35 +02:00
Keith Holliday fa60c9a232 Reset stats after allocation (#10363) 2018-05-14 22:18:23 -05:00
Matteo Pagliazzi 2c3d268a63 Merge branch 'develop' of github.com:HabitRPG/habitica into develop 2018-05-13 16:30:47 +02:00
Matteo Pagliazzi d4a80a8561 Merge branch 'marvinrabe-fix-challenge-layout' into develop 2018-05-13 16:30:28 +02:00
Matteo Pagliazzi 388492e1e7 fix conflicts and remove extra dependency 2018-05-13 16:30:17 +02:00
aszlig 8cd695c397 locales/groups: Don't wrap task text in code block (#10349)
So far if a task contained Markdown, a system message like this would
have been posted to the group chat:

foo has claimed "Some [link](http://example.org/)"

Also, if the Markdown contained backticked code fragments, the whole
text would be displayed in red except the code part.

The reason for this is because the system message is already in Markdown
and a backticked task text would result in the following Markdown:

`foo has claimed "Foo `bar`"`

Here there are two code blocks, one with `foo has claimed "Foo ` and
another which only has `"`.

This is fixed by simply changing the userIsClamingTask translation
string to not wrap the task text inside a code block, as per @Alys
suggestion.

Signed-off-by: aszlig <aszlig@nix.build>
2018-05-13 16:14:17 +02:00
Brian Fenton 355f0fedfb disabling checking off a subtask if not assigned to a user (#10357) 2018-05-13 16:12:26 +02:00
Doğu Deniz Uğur 38d78de4b3 New method added to displaying locked quest popover message in shop (#10346)
isBuyingDependentOnPrevious () method checks if item.key of quest is in a list of quests whose unlock condition is not dependent on the completition of previous quest.
2018-05-13 16:07:20 +02:00
pengfluf 6c64a1cd8c Beard and mustache facial hairs now can be bought as a full set for 5 gems (#10338)
* Purchasing All Facial Hairs Fixed

* Notifications z-index fixed

* Notifications z-index fixed x2

* Z-indexes fixed, facial hairs buying corrected

* isPurchaseAllNeeded refactored

* isPurchaseAllNeeded is more generic now

* Linting Passed
2018-05-13 16:04:43 +02:00
Matteo Pagliazzi 128ec5a1b1 Update pull request template to mention issue number instead of url 2018-05-13 15:42:44 +02:00
siege918 d4d668f640 Prevent accidental submission of Tavern/Guild posts after pasting (#10226)
* Temporarily disable ctrl-enter to send Guild messages after paste

Disable Ctrl-Enter after pasting, because some users are experiencing issues with accidentally sending their messages after pasting.

* Code style fixes for "Temporarily disable ctrl-enter to send Guild messages after paste"

* Fix issues with variable location

* Fix variables for accidental chat submission features

Moving vatiables for the chat submit timeout to their own variable so they won't be overwritten

* Fix code formatting issues with accidental chat submission code

* Remove leading space from variables to fix lint issues
2018-05-11 15:18:41 -05:00
Marvin Rabe 41ccd58f8e Fixed tavern chat button. (#10342) 2018-05-11 15:15:50 -05:00
Sabe Jones a33299a341 4.42.3 2018-05-11 20:12:54 +00:00
Sabe Jones 9129e22433 fix(event): disable seasonal potions 2018-05-11 20:12:41 +00:00
Sabe Jones 86d1bdaff1 4.42.2 2018-05-11 01:38:33 +00:00
SabreCat 206ed1f155 fix(tags): downgrade Bootstrap to restore tag checkboxes 2018-05-11 01:34:31 +00:00
Sabe Jones eb66e9ec2e 4.42.1 2018-05-10 18:52:17 +00:00
Sabe Jones 8db99be017 chore(i18n): update locales 2018-05-10 18:51:37 +00:00
SabreCat c62386e2e5 chore(news): Bailey 2018-05-10 18:42:44 +00:00
Matteo Pagliazzi 042ac6ac73 Fix notifications in user pre save hook (#10348) 2018-05-09 19:19:08 +02:00
Matteo Pagliazzi a8655d923a Fix level up webhook (#10347)
* use user._tmp for level up webhook

* use post save hook to send webhook
2018-05-09 19:04:29 +02:00
Sabe Jones bbbd1f9f73 Merge branch 'release' into develop 2018-05-08 18:41:13 +00:00
Sabe Jones 8fee5a9ba0 4.42.0 2018-05-08 18:40:48 +00:00
Sabe Jones 8df2b1e8c2 chore(i18n): update locales 2018-05-08 18:38:43 +00:00
SabreCat 24cceb1c91 feat(content): Cuddle Bundle 2018-05-08 18:28:33 +00:00
Keith Holliday 21eac3cc94 Fixed stat allocation issues (#10344) 2018-05-08 08:54:50 -05:00
Keith Holliday e9ce968f88 4.41.8 2018-05-07 16:34:54 -05:00
Keith Holliday 8c283fdbe0 Removed hook updates (#10341)
* Removed hook updates

* Fixed lint error
2018-05-07 16:30:34 -05:00
Sabe Jones 69a782a1db Party header sort WIP (#10330)
* WIP(groups): improved sorting WIP

* WIP(groups): split sort option and direction

* WIP(party): header sort cont'd

* feat(party): header sorting
2018-05-07 16:19:00 -05:00
Keith Holliday ccaf629228 4.41.7 2018-05-07 12:50:13 -05:00
Keith Holliday 147f2bb28e 4.41.6 2018-05-07 12:48:48 -05:00
Keith Holliday 54a4bba228 Removed update stats notification (#10339)
* Removed update stats notification

* Removed level up hook
2018-05-07 12:45:36 -05:00
Marvin Rabe 6ee21dcfa9 Added category tags tests. 2018-05-07 18:29:05 +02:00
Marvin Rabe 68353fb874 Added sidebar section test. 2018-05-07 18:21:32 +02:00
Marvin Rabe 68526c07ae Added missing comma. 2018-05-07 16:43:07 +02:00
Marvin Rabe 5f319ca4f6 My Challenges should include all Owned Challenges (#9286) 2018-05-07 16:23:49 +02:00
Marvin Rabe 0d84643961 Joined and Owned Challenges should still appear in Discover, but annotated with status (#9956) 2018-05-07 16:04:40 +02:00
Alys 8470f16f4f allow subscribers to buy their final monthly gem (#10331) 2018-05-07 15:20:28 +02:00
Marvin Rabe 1896a8fab0 Challenge task numbers get created from computed array. 2018-05-07 14:18:05 +02:00
Marvin Rabe 891b5566a9 Created reusable category tags component. 2018-05-07 13:56:54 +02:00
Marvin Rabe c83499545c Added Tavern case 2018-05-07 13:35:53 +02:00
Marvin Rabe 4c837acf88 Use computed attribute for boss health bar width. 2018-05-07 13:30:33 +02:00
Marvin Rabe 11b223a81e Challenge item shadow and border radius matches guild item style. 2018-05-07 13:25:52 +02:00
Marvin Rabe 17001743e1 Changed last prop to :last-of-type 2018-05-07 13:24:48 +02:00
Keith Holliday ac451bdb9b Removed spell queue (#10337) 2018-05-06 17:53:49 -05:00
Keith Holliday 6af50c9f2f Payment refactor (#10325)
* Rarranged payment index functions

* Moved gem function

* Increased buy gems test coverage

* Reduced length of functions. Reduced cognitive complexity
2018-05-06 15:12:00 -05:00
Marvin Rabe 5231cb03a8 Fixed columns when translation is too long. (#10315) 2018-05-04 16:10:02 -05:00
Marvin Rabe f8739b6f37 Fixed learn more in user dropdown. (#10314) 2018-05-04 16:09:38 -05:00
Corey Gray e31f62a818 Add responsive margins to pets and mounts. (#10311)
* Add responsive margins to pets and mounts.

* Move all margins to margin-right to make left edges flush.
2018-05-04 16:06:58 -05:00
pengfluf d5d06c1d2d Header stuck naming fixed (#10309) 2018-05-04 16:05:38 -05:00
pengfluf 4fa2ef045d 10256 - The placeholder and the message row are fixed (#10307) 2018-05-04 16:04:06 -05:00
Matteo Pagliazzi 570a8bf0d5 do not load inbox in tasks routes (#10302) 2018-05-04 16:00:57 -05:00
Matteo Pagliazzi b7dfe41e15 do not load inbox in some user routes (#10301) 2018-05-04 16:00:40 -05:00
negue c26696a9eb moving developer-only strings to api/common messages (#10258)
* 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

* Split api- and commonMessages

* fix test

* fix sanity

* merge messages to an object, rename commonMessage to errorMessage

* apiMessages -> apiError, commonMessages -> errorMessage, extract messages to separate objects

* fix test

* module.exports
2018-05-04 16:00:19 -05:00
Matteo Pagliazzi f226b5da07 Revert #10324 and #10323 (#10329) 2018-05-04 20:57:18 +02:00
Keith Holliday 63cf5b6be7 4.41.5 2018-05-04 10:03:32 -05:00
Matteo Pagliazzi f3a947339c fix quest completion modal: send only one request (#10327) 2018-05-04 17:00:11 +02:00
Keith Holliday 1bb8acad5d 4.41.4 2018-05-04 08:19:37 -05:00
Keith Holliday 8e04d6e284 Removed update stats notification (#10324) 2018-05-04 08:17:22 -05:00
Sabe Jones f7415df6ba 4.41.3 2018-05-03 20:45:21 +00:00
Matteo Pagliazzi f85e1c2dc4 Hotfix for webhooks bus in models/group (#10323)
* remove new webhooks code from group model

* disable chat webhooks as well
2018-05-03 22:40:42 +02:00
Sabe Jones 33628a0a6a 4.41.2 2018-05-03 18:02:49 +00:00
Keith Holliday 5e6541faa6 Logged users out if they were logged in with facebook (#10322) 2018-05-03 13:00:50 -05:00
Sabe Jones c1ed02d383 4.41.1 2018-05-03 17:44:38 +00:00
Sabe Jones 3793e92b80 chore(i18n): update locales 2018-05-03 17:41:59 +00:00
Matteo Pagliazzi e3ce1c5322 possible fix for facebook auth bug 2018-05-03 18:06:01 +02:00
Alys 84b16f28c2 remove statement about deletion feedback being anonymous 2018-05-03 21:31:06 +10:00
user 9c702505a9 Locales Changing, PM Disabled Caption Added 2018-05-02 19:08:43 +03:00
Sabe Jones 27c73e028a Merge branch 'release' into develop 2018-05-01 21:32:29 +00:00
Sabe Jones d125b8d2f8 4.41.0 2018-05-01 21:32:06 +00:00
Sabe Jones 451e08ce1c chore(i18n): update locales 2018-05-01 21:31:34 +00:00
SabreCat 16b5b8b8c7 chore(sprites): compile 2018-05-01 21:25:58 +00:00
SabreCat 30a717148e chore(event): end Spring Fling 2018-05-01 21:25:45 +00:00
SabreCat bcf9670dbe feat(content): Armoire and backgrounds May 2018 2018-05-01 20:55:16 +00:00
Marvin Rabe 129fccf646 Guild category tags and challenge category tags have now the same styling. 2018-05-01 21:36:23 +02:00
Marvin Rabe 9d755c5d5f Merge branch 'fix-german-translations' into fix-challenge-layout 2018-05-01 20:17:29 +02:00
Marvin Rabe 05c43d1f9d Use sidebar section component in tavern. 2018-05-01 20:13:46 +02:00
Marvin Rabe 45df73e4be Fixed challenges on 'Tavern' 2018-05-01 19:53:31 +02:00
Marvin Rabe eaa00598d0 Improvements to Challenge Layout (#9619) 2018-05-01 19:47:04 +02:00
Marvin Rabe 88b14592c5 Make Challenge Owner's Name Clickable (#9283) 2018-05-01 17:23:02 +02:00
Marvin Rabe 85136675e9 Fixed group sidebar. 2018-05-01 16:13:16 +02:00
Marvin Rabe 4e4181a394 Improved challenge layout. 2018-05-01 16:10:57 +02:00
Marvin Rabe f93822b0b3 Several hard coded strings fixed. 2018-05-01 14:34:58 +02:00
Matteo Pagliazzi a864e69042 make unhandled promise rejections easier to find among logs 2018-05-01 12:09:30 +02:00
Matteo Pagliazzi 2ccd9eaa1e uprade deps 2018-05-01 12:01:47 +02:00
Alys 5faf00d489 replace loading screen tip about buff arrow
The buff arrow no longer appears on your avatar.
2018-05-01 18:32:44 +10:00
Alys f211610f5d add swear word - TRIGGER / CONTENT WARNING: assault, slurs, swearwords, etc 2018-05-01 15:29:01 +10:00
Alys 332f285ea2 exempt The Rhyme Commando guild from the swearword blocker
This allows people to quote passes from literature containing words
that would otherwise be banned as religious oaths.
2018-05-01 13:31:56 +10:00
Sabe Jones 006159cc9c Merge branch 'release' into develop 2018-04-30 20:46:03 +00:00
Sabe Jones 3722452b51 4.40.1 2018-04-30 20:45:38 +00:00
Sabe Jones d6b5d275da chore(i18n): update locales 2018-04-30 20:45:27 +00:00
SabreCat 72073386ec chore(news): Bailey 2018-04-30 20:41:31 +00:00
Matteo Pagliazzi d34ec62901 Remove inbox from more routes (#10303)
* remove inbox from some auth routes

* remove inbox from quests routes

* remove inbox from groups routes
2018-04-30 20:36:31 +02:00
Matteo Pagliazzi ca73b9af41 remove stackimpact 2018-04-30 19:07:46 +02:00
Matteo Pagliazzi 8b9bf88fa0 Remove inbox from more routes (#10300)
* remove inbox from user/stats routes

* remove inbox from news routes

* change signature for authWithHeaders

* do not load inbox in coupons routes

* do not load inbox in challenge routes

* do not load inbox in some members routes

* do not load inbox in chat routes
2018-04-30 17:36:41 +02:00
user 9133250a42 Useless CSS rule for the caption has deleted 2018-04-30 16:12:53 +03:00
user e60177f14a Sending messages is allowed; PM related texts moved 2018-04-30 00:58:08 +03:00
Matteo Pagliazzi 5f0ef2d8f0 Webhooks v2 (and other fixes) (#10265)
* begin implementing global webhooks

* add checklist item scored webhook

* add pet hatched and mount raised webhooks (no tests)

* fix typo

* add lvl up webhooks, remove corrupt notifications and reorganize pre-save hook

* fix typo

* add some tests, globalActivity webhook

* fix bug in global activiy webhook and add more tests

* add tests and fix typo for petHatched and mountRaised webhooks

* fix errors and add tests for level up webhook

* wip: add default data to all webhooks, change signature for WebhookSender.send (missing tests)

* remove unused code

* fix unit tests

* fix chat webhooks

* remove console

* fix lint

* add and fix webhook tests

* add questStarted webhook and questActivity type

* add unit tests

* add finial tests and features
2018-04-29 20:07:14 +02:00
user 770285f10d Toggle-switch aligned with Messages Title 2018-04-29 19:08:09 +03:00
user 495dd2736c Locale Small Update 2018-04-29 17:03:51 +03:00
user 4467da980c POST request toggling opt deleted, changed to PUT /user 2018-04-29 16:48:10 +03:00
user 082539b982 Toggle-switch changed to the local one; 'en' locale edited 2018-04-29 16:31:23 +03:00
user ef7719f91d PM opt-in opt-out intert internationalization 2018-04-29 04:32:33 +03:00
user f98efd4eb9 PM opt-in opt-out is ready to use 2018-04-29 04:05:31 +03:00
user 4a0856c919 Client: opt-in / opt-out functionalitonality is ready 2018-04-29 03:07:03 +03:00
user 2adc5c13e4 Server: /toggle-private-messages-opt 2018-04-28 23:44:17 +03:00
Shadi Moustafa cf274310a8 Changed Member List number in Guilds (#10268)
* Updated README.md

Added Team Name and Collaborators

* Updated README.md

* Changed Member List number in Guilds

Changed Member List number in Guilds

* remove habitica2.bat

* Updated README.md
2018-04-28 17:43:59 +02:00
Philip Karpiak a2ee73a2e2 Use consistent elements in footer links (fix add-on/forum link colors) (#10208)
* Fix html element rendering of some footer links

* Unscope footer.expanded + children styles

Fixes link color cascading
2018-04-28 17:43:40 +02:00
Brian Fenton c6c9503e22 Hiding popunder if challenge data is incomplete (#10284)
* removing file that only contained a reference to a missing folder

* fixing typo

* using full dates to avoid moment warning in tests

* more typos

* sending an empty string to vue bootstrap tooltip (disabling it) if no challenge short name is set
2018-04-28 17:38:38 +02:00
Asher Dale 403ac1ab7e Remove experience notification when leveling up (#10285) 2018-04-28 17:37:58 +02:00
Brian Fenton 63598f497b pinning mongodb container to recommended, supported version (#10270) 2018-04-28 17:37:19 +02:00
Asher Dale 9fcc953b18 Fix API challenges export CSV bug (Fixes #8350) (#10266)
* Fix challenges export CSV error by checking that users still belong to challenge

* Add test for challenge csv export fix

* Update fix for challenge export CSV bug

* Update tests for challenge export CSV to be more complete

* Refactor a test: change some 'let' variables to 'const'
2018-04-28 17:36:12 +02:00
Christos Maris 17408d01a9 Fix markdown (#10263)
The README.md file in the website/client/ directory had a flaw.
2018-04-28 17:35:14 +02:00
Tyler Nychka ae786f28a2 Fix locked class-specific gear after death fixes #10025 (#10212)
* Fix locked class-specific gear after death fixes #10025

* Update to allow items next in tier but not owned

* Updated logic

* Added tests
2018-04-28 17:34:08 +02:00
Matteo Pagliazzi 1effa16b5b load memwatch-next only if installed 2018-04-27 20:52:33 +02:00
Matteo Pagliazzi 6b7333927a make memwatch-next optional, fixes #10291 2018-04-27 19:48:04 +02:00
Matteo Pagliazzi 31b439129d update deps 2018-04-27 19:38:42 +02:00
greenkeeper[bot] 2de85b937f fix(package): update bcrypt to version 2.0.0 (#10233) 2018-04-27 19:30:09 +02:00
negue 4f963e99dc Purchase API Refactoring: Gems [Gold] (#10271)
* remove `keyRequired` - change to `missingKeyParam` - i18n-string

* extract & convert buyGemsOperation

* fix lint
2018-04-27 19:29:26 +02:00
Keith Holliday 58ce3a9a42 Added bulk allocation (#10283) 2018-04-27 11:07:41 -05:00
Alys e45d0c9b80 add website/raw_sprites/** to list of files ignored by nodemon (#10274) 2018-04-26 10:46:49 +02:00
Alys 84a20ef4f4 increase user count on home page from 2.5 to 3 million (#10257)
Uses a variable for the number instead of hard-coding it in the locales files.

Removes some old, unused locales strongs and an associated variable
from when we had a million users.
2018-04-25 10:48:04 -05:00
Alys 59a22805b9 changed message shown to muted users (after discussion with mods)
Adjusted apidocs comment to match.
Corrected the error type for that comment.
2018-04-25 20:40:21 +10:00
Alys 95865f5ec8 add a missing quote mark to the end of the Golden Knight's speech 2018-04-25 19:38:29 +10:00
Alys 79903d242f edit apidocs comment: CreateChallenge takes the parameter group not groupId 2018-04-25 16:23:07 +10:00
Sabe Jones 90959c18cd Merge branch 'release' into develop 2018-04-24 18:46:20 +00:00
Sabe Jones 8b2019c292 4.40.0 2018-04-24 18:45:51 +00:00
Sabe Jones 9ab70ca276 chore(i18n): update locales 2018-04-24 18:44:30 +00:00
SabreCat d51aa25470 chore(sprites): compile 2018-04-24 18:38:56 +00:00
SabreCat 0b2c1e6d2e fix(pixels): Better alignment for Squirrel mounts
Also (1) updates an artist credit to a new usename, and (2) corrects a typo in migration comments
2018-04-24 18:37:33 +00:00
SabreCat 58ee6e9703 feat(content): Subscriber Mystery Items 2018/04 2018-04-24 18:36:19 +00:00
Keith Holliday 0044778497 4.39.2 2018-04-24 08:16:35 -05:00
Matteo Pagliazzi 46d6590fec chat: use _id instead of id when finding doc (#10278) 2018-04-24 15:10:20 +02:00
Keith Holliday 8d25a5d140 Added bulk spell queue (#10241)
* Added bulk spell queue

* Removed extra comment

* Moved queue to store
2018-04-23 20:30:55 -05:00
885 changed files with 34585 additions and 31722 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
[//]: # (Note: See http://habitica.wikia.com/wiki/Using_Your_Local_Install_to_Modify_Habitica%27s_Website_and_API for more info)
[//]: # (Put Issue # or URL here, if applicable. This will automatically close the issue if your PR is merged in)
Fixes put_issue_url_here
[//]: # (Put Issue # here, if applicable. This will automatically close the issue if your PR is merged in)
Fixes put_#_and_issue_numer_here
### Changes
[//]: # (Describe the changes that were made in detail here. Include pictures if necessary)
+1
View File
@@ -17,3 +17,4 @@ CHANGELOG.md
newrelic_agent.log
*.swp
*.swx
website/raw_sprites/**
+1 -1
View File
@@ -17,7 +17,7 @@ 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.37.2 https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
RUN git clone --branch v4.44.0 https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
RUN npm install
RUN gulp build:prod --force
-1
View File
@@ -10,4 +10,3 @@ We need more programmers! Your assistance will be greatly appreciated.
For an introduction to the technologies used and how the software is organized, refer to [Guidance for Blacksmiths](http://habitica.wikia.com/wiki/Guidance_for_Blacksmiths).
To set up a local install of Habitica for development and testing on various platforms, see [Setting up Habitica Locally](http://habitica.wikia.com/wiki/Setting_up_Habitica_Locally).
+1 -2
View File
@@ -112,6 +112,5 @@
"CLOUDKARAFKA_USERNAME": "",
"CLOUDKARAFKA_PASSWORD": "",
"CLOUDKARAFKA_TOPIC_PREFIX": ""
},
"STACK_IMPACT_KEY": "aaaabbbbccccddddeeeeffffgggg111100002222"
}
}
+1 -1
View File
@@ -25,7 +25,7 @@ services:
- mongo
mongo:
image: mongo
image: mongo:3.4
ports:
- "27017:27017"
networks:
+1 -1
View File
@@ -167,7 +167,7 @@ gulp.task('test:content:safe', gulp.series('test:prepare:build', (cb) => {
gulp.task('test:api-v3:unit', (done) => {
let runner = exec(
testBin('node_modules/.bin/istanbul cover --dir coverage/api-v3-unit --report lcovonly node_modules/mocha/bin/_mocha -- test/api/v3/unit --recursive --require ./test/helpers/start-server'),
testBin('node_modules/.bin/istanbul cover --dir coverage/api-v3-unit node_modules/mocha/bin/_mocha -- test/api/v3/unit --recursive --require ./test/helpers/start-server'),
(err) => {
if (err) {
process.exit(1);
+1 -1
View File
@@ -4,7 +4,7 @@
/*
* This migration move ass chat off of groups and into their own model
* This migration moves chat off of groups and into their own model
*/
import { model as Group } from '../../website/server/models/group';
+1 -1
View File
@@ -17,5 +17,5 @@ function setUpServer () {
setUpServer();
// Replace this with your migration
const processUsers = require('./groups/migrate-chat.js');
const processUsers = require('./users/mystery-items.js');
processUsers();
+3 -3
View File
@@ -1,12 +1,12 @@
const migrationName = 'mystery-items-201802.js'; // Update per month
const migrationName = 'mystery-items-201805.js'; // Update per month
const authorName = 'Sabe'; // in case script author needs to know when their ...
const authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
/*
* Award this month's mystery items to subscribers
*/
const MYSTERY_ITEMS = ['back_mystery_201803', 'head_mystery_201803'];
const connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
const MYSTERY_ITEMS = ['back_mystery_201805', 'head_mystery_201805'];
const connectionString = 'mongodb://sabrecat:mT1wp9Nf5xUDmV5@ds013393-a0.mlab.com:13393/habitica?auto_reconnect=true';
let monk = require('monk');
let dbUsers = monk(connectionString).get('users', { castIds: false });
@@ -0,0 +1,109 @@
const migrationName = 'remove-social-users-extra-data.js';
const authorName = 'paglias'; // in case script author needs to know when their ...
const authorUuid = 'ed4c688c-6652-4a92-9d03-a5a79844174a'; // ... own data is done
/*
* Remove not needed data from social profiles
*/
const connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
const monk = require('monk');
const dbUsers = monk(connectionString).get('users', { castIds: false });
function processUsers (lastId) {
// specify a query to limit the affected users (empty for all users):
let query = {
migration: {$ne: migrationName},
$or: [
{ 'auth.facebook.id': { $exists: true } },
{ 'auth.google.id': { $exists: true } },
],
};
if (lastId) {
query._id = {
$gt: lastId,
};
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 250,
})
.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++;
const isFacebook = user.auth.facebook && user.auth.facebook.id;
const isGoogle = user.auth.google && user.auth.google.id;
const update = { $set: {} };
if (isFacebook) {
update.$set['auth.facebook'] = {
id: user.auth.facebook.id,
emails: user.auth.facebook.emails,
};
}
if (isGoogle) {
update.$set['auth.google'] = {
id: user.auth.google.id,
emails: user.auth.google.emails,
};
}
dbUsers.update({
_id: user._id,
}, update);
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;
+2635 -3551
View File
File diff suppressed because it is too large Load Diff
+42 -43
View File
@@ -1,48 +1,48 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "4.39.1",
"version": "4.44.1",
"main": "./website/server/index.js",
"dependencies": {
"@slack/client": "^3.8.1",
"accepts": "^1.3.5",
"amazon-payments": "^0.2.6",
"amazon-payments": "^0.2.7",
"amplitude": "^3.5.0",
"apidoc": "^0.17.5",
"autoprefixer": "^8.2.0",
"aws-sdk": "^2.224.1",
"autoprefixer": "^8.5.0",
"aws-sdk": "^2.239.1",
"axios": "^0.18.0",
"axios-progress-bar": "^1.1.8",
"babel-core": "^6.0.0",
"babel-eslint": "^8.2.2",
"axios-progress-bar": "^1.2.0",
"babel-core": "^6.26.3",
"babel-eslint": "^8.2.3",
"babel-loader": "^7.1.4",
"babel-plugin-syntax-async-functions": "^6.13.0",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
"babel-plugin-transform-object-rest-spread": "^6.16.0",
"babel-plugin-transform-regenerator": "^6.16.1",
"babel-polyfill": "^6.6.1",
"babel-preset-es2015": "^6.6.0",
"babel-register": "^6.6.0",
"babel-runtime": "^6.11.6",
"bcrypt": "^1.0.2",
"body-parser": "^1.15.0",
"bootstrap": "^4.1.0",
"bootstrap-vue": "^2.0.0-rc.6",
"bcrypt": "^2.0.0",
"body-parser": "^1.18.3",
"bootstrap": "^4.1.1",
"bootstrap-vue": "^2.0.0-rc.9",
"compression": "^1.7.2",
"cookie-session": "^1.2.0",
"coupon-code": "^0.4.5",
"cross-env": "^5.1.4",
"cross-env": "^5.1.5",
"css-loader": "^0.28.11",
"csv-stringify": "^2.1.0",
"cwait": "^1.1.1",
"domain-middleware": "~0.1.0",
"express": "^4.16.3",
"express-basic-auth": "^1.1.4",
"express-validator": "^5.1.2",
"express-basic-auth": "^1.1.5",
"express-validator": "^5.2.0",
"extract-text-webpack-plugin": "^3.0.2",
"glob": "^7.1.2",
"got": "^8.3.0",
"got": "^8.3.1",
"gulp": "^4.0.0",
"gulp-babel": "^7.0.1",
"gulp-imagemin": "^4.1.0",
@@ -52,23 +52,22 @@
"hellojs": "^1.15.1",
"html-webpack-plugin": "^3.2.0",
"image-size": "^0.6.2",
"in-app-purchase": "^1.9.0",
"intro.js": "^2.6.0",
"in-app-purchase": "^1.9.4",
"intro.js": "^2.9.3",
"jquery": ">=3.0.0",
"js2xmlparser": "^3.0.0",
"lodash": "^4.17.4",
"memwatch-next": "^0.3.0",
"lodash": "^4.17.10",
"merge-stream": "^1.0.0",
"method-override": "^2.3.5",
"moment": "^2.22.0",
"moment": "^2.22.1",
"moment-recur": "^1.0.7",
"mongoose": "^5.0.14",
"mongoose": "^5.1.2",
"morgan": "^1.7.0",
"nconf": "^0.10.0",
"node-gcm": "^0.14.4",
"node-sass": "^4.8.3",
"node-sass": "^4.9.0",
"nodemailer": "^4.6.4",
"ora": "^2.0.0",
"ora": "^2.1.0",
"pageres": "^4.1.1",
"passport": "^0.4.0",
"passport-facebook": "^2.0.0",
@@ -83,10 +82,9 @@
"pusher": "^1.3.0",
"rimraf": "^2.4.3",
"sass-loader": "^7.0.0",
"shelljs": "^0.8.1",
"stackimpact": "^1.3.0",
"stripe": "^5.8.0",
"superagent": "^3.4.3",
"shelljs": "^0.8.2",
"stripe": "^5.9.0",
"superagent": "^3.8.3",
"svg-inline-loader": "^0.8.0",
"svg-url-loader": "^2.3.2",
"svgo": "^1.0.5",
@@ -105,9 +103,9 @@
"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": "^3.12.0",
"webpack-merge": "^4.0.0",
"winston": "^2.4.1",
"winston": "^2.4.2",
"winston-loggly-bulk": "^2.0.2",
"xml2js": "^0.4.4"
},
@@ -141,15 +139,15 @@
"apidoc": "gulp apidoc"
},
"devDependencies": {
"@vue/test-utils": "^1.0.0-beta.13",
"@vue/test-utils": "^1.0.0-beta.16",
"babel-plugin-istanbul": "^4.1.6",
"babel-plugin-syntax-object-rest-spread": "^6.13.0",
"chai": "^4.1.2",
"chai-as-promised": "^7.1.1",
"chalk": "^2.3.2",
"chromedriver": "^2.37.0",
"chalk": "^2.4.1",
"chromedriver": "^2.38.3",
"connect-history-api-fallback": "^1.1.0",
"coveralls": "^3.0.0",
"coveralls": "^3.0.1",
"cross-spawn": "^6.0.5",
"eslint": "^4.19.1",
"eslint-config-habitrpg": "^4.0.0",
@@ -161,11 +159,11 @@
"expect.js": "^0.3.1",
"http-proxy-middleware": "^0.18.0",
"istanbul": "^1.1.0-alpha.1",
"karma": "^2.0.0",
"karma": "^2.0.2",
"karma-babel-preprocessor": "^7.0.0",
"karma-chai-plugins": "^0.9.0",
"karma-chrome-launcher": "^2.2.0",
"karma-coverage": "^1.1.1",
"karma-coverage": "^1.1.2",
"karma-mocha": "^1.3.0",
"karma-mocha-reporter": "^2.2.5",
"karma-sinon-chai": "^1.3.4",
@@ -174,20 +172,21 @@
"karma-spec-reporter": "0.0.32",
"karma-webpack": "^3.0.0",
"lcov-result-merger": "^2.0.0",
"mocha": "^5.0.5",
"monk": "^6.0.5",
"nightwatch": "^0.9.20",
"puppeteer": "^1.3.0",
"mocha": "^5.1.1",
"monk": "^6.0.6",
"nightwatch": "^0.9.21",
"puppeteer": "^1.4.0",
"require-again": "^2.0.0",
"selenium-server": "^3.11.0",
"selenium-server": "^3.12.0",
"sinon": "^4.5.0",
"sinon-chai": "^3.0.0",
"sinon-stub-promise": "^4.0.0",
"webpack-bundle-analyzer": "^2.11.1",
"webpack-bundle-analyzer": "^2.12.0",
"webpack-dev-middleware": "^2.0.5",
"webpack-hot-middleware": "^2.22.0"
"webpack-hot-middleware": "^2.22.2"
},
"optionalDependencies": {
"memwatch-next": "^0.3.0",
"node-rdkafka": "^2.3.0"
}
}
@@ -41,6 +41,7 @@ describe('DELETE /challenges/:challengeId', () => {
group = populatedGroup.group;
challenge = await generateChallenge(groupLeader, group);
await groupLeader.post(`/challenges/${challenge._id}/join`);
await groupLeader.post(`/tasks/challenge/${challenge._id}`, [
{type: 'habit', text: taskText},
@@ -33,9 +33,11 @@ describe('GET /challenges/:challengeId', () => {
group = populatedGroup.group;
challenge = await generateChallenge(groupLeader, group);
await groupLeader.post(`/challenges/${challenge._id}/join`);
});
it('should return challenge data', async () => {
await challenge.sync();
let chal = await user.get(`/challenges/${challenge._id}`);
expect(chal.memberCount).to.equal(challenge.memberCount);
expect(chal.name).to.equal(challenge.name);
@@ -80,6 +82,7 @@ describe('GET /challenges/:challengeId', () => {
challenge = await generateChallenge(groupLeader, group);
await members[0].post(`/challenges/${challenge._id}/join`);
await groupLeader.post(`/challenges/${challenge._id}/join`);
});
it('fails if user doesn\'t have access to the challenge', async () => {
@@ -134,6 +137,7 @@ describe('GET /challenges/:challengeId', () => {
challenge = await generateChallenge(groupLeader, group);
await members[0].post(`/challenges/${challenge._id}/join`);
await groupLeader.post(`/challenges/${challenge._id}/join`);
});
it('fails if user doesn\'t have access to the challenge', async () => {
@@ -24,6 +24,7 @@ describe('GET /challenges/:challengeId/export/csv', () => {
members = populatedGroup.members;
challenge = await generateChallenge(groupLeader, group);
await groupLeader.post(`/challenges/${challenge._id}/join`);
await members[0].post(`/challenges/${challenge._id}/join`);
await members[1].post(`/challenges/${challenge._id}/join`);
await members[2].post(`/challenges/${challenge._id}/join`);
@@ -60,9 +61,9 @@ describe('GET /challenges/:challengeId/export/csv', () => {
});
it('should return a valid CSV file with export data', async () => {
let res = await members[0].get(`/challenges/${challenge._id}/export/csv`);
let sortedMembers = _.sortBy([members[0], members[1], members[2], groupLeader], '_id');
let splitRes = res.split('\n');
const res = await members[0].get(`/challenges/${challenge._id}/export/csv`);
const sortedMembers = _.sortBy([members[0], members[1], members[2], groupLeader], '_id');
const splitRes = res.split('\n');
expect(splitRes[0]).to.equal('UUID,name,Task,Value,Notes,Streak,Task,Value,Notes,Streak');
expect(splitRes[1]).to.equal(`${sortedMembers[0]._id},${sortedMembers[0].profile.name},habit:Task 1,0,,0,todo:Task 2,0,,0`);
@@ -71,4 +72,16 @@ describe('GET /challenges/:challengeId/export/csv', () => {
expect(splitRes[4]).to.equal(`${sortedMembers[3]._id},${sortedMembers[3].profile.name},habit:Task 1,0,,0,todo:Task 2,0,,0`);
expect(splitRes[5]).to.equal('');
});
it('should successfully return when it contains erroneous residue user data', async () => {
await members[0].update({challenges: []});
const res = await members[1].get(`/challenges/${challenge._id}/export/csv`);
const sortedMembers = _.sortBy([members[1], members[2], groupLeader], '_id');
const splitRes = res.split('\n');
expect(splitRes[0]).to.equal('UUID,name,Task,Value,Notes,Streak,Task,Value,Notes,Streak');
expect(splitRes[1]).to.equal(`${sortedMembers[0]._id},${sortedMembers[0].profile.name},habit:Task 1,0,,0,todo:Task 2,0,,0`);
expect(splitRes[2]).to.equal(`${sortedMembers[1]._id},${sortedMembers[1].profile.name},habit:Task 1,0,,0,todo:Task 2,0,,0`);
expect(splitRes[3]).to.equal(`${sortedMembers[2]._id},${sortedMembers[2].profile.name},habit:Task 1,0,,0,todo:Task 2,0,,0`);
expect(splitRes[4]).to.equal('');
});
});
@@ -45,6 +45,7 @@ describe('GET /challenges/:challengeId/members', () => {
let leader = await generateUser({balance: 4});
let group = await generateGroup(leader, {type: 'guild', privacy: 'public', name: generateUUID()});
let challenge = await generateChallenge(leader, group);
await leader.post(`/challenges/${challenge._id}/join`);
let res = await user.get(`/challenges/${challenge._id}/members`);
expect(res[0]).to.eql({
_id: leader._id,
@@ -59,6 +60,7 @@ describe('GET /challenges/:challengeId/members', () => {
let anotherUser = await generateUser({balance: 3});
let group = await generateGroup(anotherUser, {type: 'guild', privacy: 'public', name: generateUUID()});
let challenge = await generateChallenge(anotherUser, group);
await anotherUser.post(`/challenges/${challenge._id}/join`);
let res = await user.get(`/challenges/${challenge._id}/members`);
expect(res[0]).to.eql({
_id: anotherUser._id,
@@ -72,6 +74,7 @@ describe('GET /challenges/:challengeId/members', () => {
it('returns only first 30 members if req.query.includeAllMembers is not true', async () => {
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
let challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
let usersToGenerate = [];
for (let i = 0; i < 31; i++) {
@@ -90,6 +93,7 @@ describe('GET /challenges/:challengeId/members', () => {
it('returns only first 30 members if req.query.includeAllMembers is not defined', async () => {
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
let challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
let usersToGenerate = [];
for (let i = 0; i < 31; i++) {
@@ -108,6 +112,7 @@ describe('GET /challenges/:challengeId/members', () => {
it('returns all members if req.query.includeAllMembers is true', async () => {
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
let challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
let usersToGenerate = [];
for (let i = 0; i < 31; i++) {
@@ -123,9 +128,11 @@ describe('GET /challenges/:challengeId/members', () => {
});
});
it('supports using req.query.lastId to get more members', async () => {
it('supports using req.query.lastId to get more members', async function () {
this.timeout(30000); // @TODO: times out after 8 seconds
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
let challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
let usersToGenerate = [];
for (let i = 0; i < 57; i++) {
@@ -146,6 +153,7 @@ describe('GET /challenges/:challengeId/members', () => {
it('supports using req.query.search to get search members', async () => {
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
let challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
let usersToGenerate = [];
for (let i = 0; i < 3; i++) {
@@ -50,6 +50,7 @@ describe('GET /challenges/:challengeId/members/:memberId', () => {
it('fails if user doesn\'t have access to the challenge', async () => {
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
let challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
let anotherUser = await generateUser();
let member = await generateUser();
await expect(anotherUser.get(`/challenges/${challenge._id}/members/${member._id}`)).to.eventually.be.rejected.and.eql({
@@ -62,6 +63,7 @@ describe('GET /challenges/:challengeId/members/:memberId', () => {
it('fails if member is not part of the challenge', async () => {
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
let challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
let member = await generateUser();
await expect(user.get(`/challenges/${challenge._id}/members/${member._id}`)).to.eventually.be.rejected.and.eql({
code: 404,
@@ -74,6 +76,7 @@ describe('GET /challenges/:challengeId/members/:memberId', () => {
let groupLeader = await generateUser({balance: 4});
let group = await generateGroup(groupLeader, {type: 'guild', privacy: 'public', name: generateUUID()});
let challenge = await generateChallenge(groupLeader, group);
await groupLeader.post(`/challenges/${challenge._id}/join`);
let taskText = 'Test Text';
await groupLeader.post(`/tasks/challenge/${challenge._id}`, [{type: 'habit', text: taskText}]);
@@ -86,6 +89,7 @@ describe('GET /challenges/:challengeId/members/:memberId', () => {
it('returns the member tasks for the challenges', async () => {
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
let challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
await user.post(`/tasks/challenge/${challenge._id}`, [{type: 'habit', text: 'Test Text'}]);
let memberProgress = await user.get(`/challenges/${challenge._id}/members/${user._id}`);
@@ -98,6 +102,7 @@ describe('GET /challenges/:challengeId/members/:memberId', () => {
it('returns the tasks without the tags and checklist', async () => {
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
let challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
let taskText = 'Test Text';
await user.post(`/tasks/challenge/${challenge._id}`, [{
type: 'todo',
@@ -25,7 +25,9 @@ describe('GET challenges/groups/:groupId', () => {
nonMember = await generateUser();
challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
challenge2 = await generateChallenge(user, group);
await user.post(`/challenges/${challenge2._id}/join`);
});
it('should return group challenges for non member with populated leader', async () => {
@@ -73,6 +75,7 @@ describe('GET challenges/groups/:groupId', () => {
expect(foundChallengeIndex).to.eql(0);
let newChallenge = await generateChallenge(user, publicGuild);
await user.post(`/challenges/${newChallenge._id}/join`);
challenges = await user.get(`/challenges/groups/${publicGuild._id}`);
@@ -99,7 +102,9 @@ describe('GET challenges/groups/:groupId', () => {
nonMember = await generateUser();
challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
challenge2 = await generateChallenge(user, group);
await user.post(`/challenges/${challenge2._id}/join`);
});
it('should prevent non-member from seeing challenges', async () => {
@@ -156,9 +161,12 @@ describe('GET challenges/groups/:groupId', () => {
slug: 'habitica_official',
}],
});
await user.post(`/challenges/${officialChallenge._id}/join`);
challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
challenge2 = await generateChallenge(user, group);
await user.post(`/challenges/${challenge2._id}/join`);
});
it('should return official challenges first', async () => {
@@ -178,6 +186,7 @@ describe('GET challenges/groups/:groupId', () => {
expect(foundChallengeIndex).to.eql(1);
let newChallenge = await generateChallenge(user, publicGuild);
await user.post(`/challenges/${newChallenge._id}/join`);
challenges = await user.get(`/challenges/groups/${publicGuild._id}`);
@@ -203,7 +212,9 @@ describe('GET challenges/groups/:groupId', () => {
nonMember = await generateUser();
challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
challenge2 = await generateChallenge(user, group);
await user.post(`/challenges/${challenge2._id}/join`);
});
it('should prevent non-member from seeing challenges', async () => {
@@ -263,7 +274,9 @@ describe('GET challenges/groups/:groupId', () => {
tavern = await user.get(`/groups/${TAVERN_ID}`);
challenge = await generateChallenge(user, tavern, {prize: 1});
await user.post(`/challenges/${challenge._id}/join`);
challenge2 = await generateChallenge(user, tavern, {prize: 1});
await user.post(`/challenges/${challenge2._id}/join`);
});
it('should return tavern challenges with populated leader', async () => {
@@ -24,7 +24,9 @@ describe('GET challenges/user', () => {
nonMember = await generateUser();
challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
challenge2 = await generateChallenge(user, group);
await user.post(`/challenges/${challenge2._id}/join`);
});
it('should return challenges user has joined', async () => {
@@ -146,6 +148,7 @@ describe('GET challenges/user', () => {
expect(foundChallengeIndex).to.eql(0);
let newChallenge = await generateChallenge(user, publicGuild);
await user.post(`/challenges/${newChallenge._id}/join`);
challenges = await user.get('/challenges/user');
@@ -164,6 +167,7 @@ describe('GET challenges/user', () => {
});
let privateChallenge = await generateChallenge(groupLeader, group);
await groupLeader.post(`/challenges/${privateChallenge._id}/join`);
let challenges = await nonMember.get('/challenges/user');
@@ -198,9 +202,12 @@ describe('GET challenges/user', () => {
slug: 'habitica_official',
}],
});
await user.post(`/challenges/${officialChallenge._id}/join`);
challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
challenge2 = await generateChallenge(user, group);
await user.post(`/challenges/${challenge2._id}/join`);
});
it('should return official challenges first', async () => {
@@ -220,6 +227,7 @@ describe('GET challenges/user', () => {
expect(foundChallengeIndex).to.eql(1);
let newChallenge = await generateChallenge(user, publicGuild);
await user.post(`/challenges/${newChallenge._id}/join`);
challenges = await user.get('/challenges/user');
@@ -252,12 +260,14 @@ describe('GET challenges/user', () => {
await user.update({balance: 20});
for (let i = 0; i < 11; i += 1) {
await generateChallenge(user, group); // eslint-disable-line
let challenge = await generateChallenge(user, group); // eslint-disable-line
await user.post(`/challenges/${challenge._id}/join`); // eslint-disable-line
}
});
it('returns public guilds filtered by category', async () => {
const categoryChallenge = await generateChallenge(user, guild, {categories});
await user.post(`/challenges/${categoryChallenge._id}/join`);
const challenges = await user.get(`/challenges/user?categories=${categories[0].slug}`);
expect(challenges[0]._id).to.eql(categoryChallenge._id);
@@ -242,7 +242,6 @@ describe('POST /challenges', () => {
it('returns an error when challenge validation fails; doesn\'s save user or group', async () => {
let oldChallengeCount = group.challengeCount;
let oldUserBalance = groupLeader.balance;
let oldUserChallenges = groupLeader.challenges;
let oldGroupBalance = group.balance;
await expect(groupLeader.post('/challenges', {
@@ -260,7 +259,6 @@ describe('POST /challenges', () => {
expect(group.challengeCount).to.eql(oldChallengeCount);
expect(group.balance).to.eql(oldGroupBalance);
expect(groupLeader.balance).to.eql(oldUserBalance);
expect(groupLeader.challenges).to.eql(oldUserChallenges);
});
it('sets all properites of the challenge as passed', async () => {
@@ -291,18 +289,19 @@ describe('POST /challenges', () => {
name: group.name,
type: group.type,
});
expect(challenge.memberCount).to.eql(1);
expect(challenge.memberCount).to.eql(0);
expect(challenge.prize).to.eql(prize);
});
it('adds challenge to creator\'s challenges', async () => {
let challenge = await groupLeader.post('/challenges', {
it('does not add challenge to creator\'s challenges', async () => {
await groupLeader.post('/challenges', {
group: group._id,
name: 'Test Challenge',
shortName: 'TC Label',
});
await expect(groupLeader.sync()).to.eventually.have.property('challenges').to.include(challenge._id);
await groupLeader.sync();
expect(groupLeader.challenges.length).to.equal(0);
});
it('awards achievement if this is creator\'s first challenge', async () => {
@@ -43,6 +43,7 @@ describe('POST /challenges/:challengeId/join', () => {
authorizedUser = populatedGroup.members[0];
challenge = await generateChallenge(groupLeader, group);
await groupLeader.post(`/challenges/${challenge._id}/join`);
});
it('returns an error when user doesn\'t have permissions to access the challenge', async () => {
@@ -91,6 +92,7 @@ describe('POST /challenges/:challengeId/join', () => {
});
it('increases memberCount of challenge', async () => {
await challenge.sync();
let oldMemberCount = challenge.memberCount;
await authorizedUser.post(`/challenges/${challenge._id}/join`);
@@ -48,6 +48,7 @@ describe('POST /challenges/:challengeId/leave', () => {
notInGroupLeavingUser = populatedGroup.members[2];
challenge = await generateChallenge(groupLeader, group);
await groupLeader.post(`/challenges/${challenge._id}/join`);
taskText = 'A challenge task text';
@@ -58,6 +58,7 @@ describe('POST /challenges/:challengeId/winner/:winnerId', () => {
challenge = await generateChallenge(groupLeader, group, {
prize: 1,
});
await groupLeader.post(`/challenges/${challenge._id}/join`);
await groupLeader.post(`/tasks/challenge/${challenge._id}`, [
{type: 'habit', text: taskText},
@@ -25,6 +25,7 @@ describe('PUT /challenges/:challengeId', () => {
member = members[0];
challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
await member.post(`/challenges/${challenge._id}/join`);
});
@@ -2,7 +2,7 @@ import {
generateUser,
resetHabiticaDB,
} from '../../../../helpers/api-v3-integration.helper';
import apiMessages from '../../../../../website/server/libs/apiMessages';
import apiError from '../../../../../website/server/libs/apiError';
describe('GET /coupons/', () => {
let user;
@@ -19,7 +19,7 @@ describe('GET /coupons/', () => {
await expect(user.get('/coupons')).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: apiMessages('noSudoAccess'),
message: apiError('noSudoAccess'),
});
});
@@ -4,7 +4,7 @@ import {
resetHabiticaDB,
} from '../../../../helpers/api-v3-integration.helper';
import couponCode from 'coupon-code';
import apiMessages from '../../../../../website/server/libs/apiMessages';
import apiError from '../../../../../website/server/libs/apiError';
describe('POST /coupons/generate/:event', () => {
let user;
@@ -26,7 +26,7 @@ describe('POST /coupons/generate/:event', () => {
await expect(user.post('/coupons/generate/aaa')).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: apiMessages('noSudoAccess'),
message: apiError('noSudoAccess'),
});
});
@@ -7,7 +7,7 @@ import {
import {
TAVERN_ID,
} from '../../../../../website/server/models/group';
import apiMessages from '../../../../../website/server/libs/apiMessages';
import apiError from '../../../../../website/server/libs/apiError';
describe('GET /groups', () => {
let user;
@@ -167,7 +167,7 @@ describe('GET /groups', () => {
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: apiMessages('guildsOnlyPaginate'),
message: apiError('guildsOnlyPaginate'),
});
});
@@ -81,7 +81,8 @@ describe('GET /groups/:groupId/invites', () => {
});
});
it('supports using req.query.lastId to get more invites', async () => {
it('supports using req.query.lastId to get more invites', async function () {
this.timeout(30000); // @TODO: times out after 8 seconds
let leader = await generateUser({balance: 4});
let group = await generateGroup(leader, {type: 'guild', privacy: 'public', name: generateUUID()});
@@ -142,7 +142,8 @@ describe('GET /groups/:groupId/members', () => {
});
});
it('supports using req.query.lastId to get more members', async () => {
it('supports using req.query.lastId to get more members', async function () {
this.timeout(30000); // @TODO: times out after 8 seconds
let leader = await generateUser({balance: 4});
let group = await generateGroup(leader, {type: 'guild', privacy: 'public', name: generateUUID()});
@@ -93,6 +93,7 @@ describe('POST /groups/:groupId/leave', () => {
beforeEach(async () => {
challenge = await generateChallenge(leader, groupToLeave);
await leader.post(`/challenges/${challenge._id}/join`);
await leader.post(`/tasks/challenge/${challenge._id}`, {
text: 'test habit',
@@ -1,8 +1,8 @@
import {
generateUser,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
import apiError from '../../../../../../website/server/libs/apiError';
describe('payments : paypal #checkoutSuccess', () => {
let endpoint = '/paypal/checkout/success';
@@ -17,7 +17,7 @@ describe('payments : paypal #checkoutSuccess', () => {
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingPaymentId'),
message: apiError('missingPaymentId'),
});
});
@@ -26,7 +26,7 @@ describe('payments : paypal #checkoutSuccess', () => {
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingCustomerId'),
message: apiError('missingCustomerId'),
});
});
@@ -1,9 +1,9 @@
import {
generateUser,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
import shared from '../../../../../../website/common';
import apiError from '../../../../../../website/server/libs/apiError';
describe('payments : paypal #subscribe', () => {
let endpoint = '/paypal/subscribe';
@@ -17,7 +17,7 @@ describe('payments : paypal #subscribe', () => {
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingSubKey'),
message: apiError('missingSubKey'),
});
});
@@ -1,7 +1,7 @@
import {
generateUser,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import apiError from '../../../../../../website/server/libs/apiError';
import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
describe('payments : paypal #subscribeSuccess', () => {
@@ -16,7 +16,7 @@ describe('payments : paypal #subscribeSuccess', () => {
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingPaypalBlock'),
message: apiError('missingPaypalBlock'),
});
});
@@ -6,6 +6,7 @@ import {
import { v4 as generateUUID } from 'uuid';
import { quests as questScrolls } from '../../../../../website/common/script/content';
import { model as Chat } from '../../../../../website/server/models/chat';
import apiError from '../../../../../website/server/libs/apiError';
describe('POST /groups/:groupId/quests/invite/:questKey', () => {
let questingGroup;
@@ -69,7 +70,7 @@ describe('POST /groups/:groupId/quests/invite/:questKey', () => {
await expect(leader.post(`/groups/${questingGroup._id}/quests/invite/${FAKE_QUEST}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('questNotFound', {key: FAKE_QUEST}),
message: apiError('questNotFound', {key: FAKE_QUEST}),
});
});
@@ -92,6 +92,7 @@ describe('DELETE /tasks/:id', () => {
});
let guild = await generateGroup(user);
let challenge = await generateChallenge(user, guild);
await user.post(`/challenges/${challenge._id}/join`);
await user.post('/user/webhook', {
url: `http://localhost:${server.port}/webhooks/${uuid}`,
@@ -40,6 +40,7 @@ describe('GET /tasks/:taskId', () => {
user = await generateUser();
guild = await generateGroup(user);
challenge = await generateChallenge(user, guild);
await user.post(`/challenges/${challenge._id}/join`);
});
it('returns error when incorrect id is passed', async () => {
@@ -9,6 +9,7 @@ describe('POST /tasks/clearCompletedTodos', () => {
let user = await generateUser({balance: 1});
let guild = await generateGroup(user);
let challenge = await generateChallenge(user, guild);
await user.post(`/challenges/${challenge._id}/join`);
let initialTodoCount = user.tasksOrder.todos.length;
await user.post('/tasks/user', [
@@ -81,6 +81,49 @@ describe('POST /tasks/:id/score/:direction', () => {
expect(body.direction).to.eql('up');
expect(body.delta).to.be.greaterThan(0);
});
context('sending user activity webhooks', () => {
before(async () => {
await server.start();
});
after(async () => {
await server.close();
});
it('sends user activity webhook when the user levels up', async () => {
let uuid = generateUUID();
await user.post('/user/webhook', {
url: `http://localhost:${server.port}/webhooks/${uuid}`,
type: 'userActivity',
enabled: true,
options: {
leveledUp: true,
},
});
const initialLvl = user.stats.lvl;
await user.update({
'stats.exp': 3000,
});
let task = await user.post('/tasks/user', {
text: 'test habit',
type: 'habit',
});
await user.post(`/tasks/${task.id}/score/up`);
await user.sync();
await sleep();
let body = server.getWebhookData(uuid);
expect(body.type).to.eql('leveledUp');
expect(body.initialLvl).to.eql(initialLvl);
expect(body.finalLvl).to.eql(user.stats.lvl);
});
});
});
context('todos', () => {
@@ -37,6 +37,7 @@ describe('POST /tasks/unlink-all/:challengeId', () => {
user = await generateUser();
guild = await generateGroup(user);
challenge = await generateChallenge(user, guild);
await user.post(`/challenges/${challenge._id}/join`);
});
it('fails if no keep query', async () => {
@@ -38,6 +38,7 @@ describe('POST /tasks/unlink-one/:taskId', () => {
user = await generateUser();
guild = await generateGroup(user);
challenge = await generateChallenge(user, guild);
await user.post(`/challenges/${challenge._id}/join`);
});
it('fails if no keep query', async () => {
@@ -65,6 +65,7 @@ describe('PUT /tasks/:id', () => {
fields for challenge tasks owned by a user`, async () => {
let guild = await generateGroup(user);
let challenge = await generateChallenge(user, guild);
await user.post(`/challenges/${challenge._id}/join`);
let challengeTask = await user.post(`/tasks/challenge/${challenge._id}`, {
type: 'daily',
@@ -198,6 +199,7 @@ describe('PUT /tasks/:id', () => {
});
let guild = await generateGroup(user);
let challenge = await generateChallenge(user, guild);
await user.post(`/challenges/${challenge._id}/join`);
await user.post('/user/webhook', {
url: `http://localhost:${server.port}/webhooks/${uuid}`,
@@ -15,6 +15,7 @@ describe('DELETE /tasks/:taskId/checklist/:itemId', () => {
user = await generateUser();
guild = await generateGroup(user);
challenge = await generateChallenge(user, guild);
await user.post(`/challenges/${challenge._id}/join`);
});
it('fails on task not found', async () => {
@@ -17,6 +17,7 @@ describe('DELETE /tasks/:id', () => {
user = await generateUser();
guild = await generateGroup(user);
challenge = await generateChallenge(user, guild);
await user.post(`/challenges/${challenge._id}/join`);
});
beforeEach(async () => {
@@ -42,6 +42,7 @@ describe('GET /tasks/challenge/:challengeId', () => {
user = await generateUser();
guild = await generateGroup(user);
challenge = await generateChallenge(user, guild);
await user.post(`/challenges/${challenge._id}/join`);
});
it('returns error when challenge is not found', async () => {
@@ -15,6 +15,7 @@ describe('POST /tasks/:taskId/checklist/', () => {
user = await generateUser();
guild = await generateGroup(user);
challenge = await generateChallenge(user, guild);
await user.post(`/challenges/${challenge._id}/join`);
});
it('fails on task not found', async () => {
@@ -20,6 +20,7 @@ describe('POST /tasks/challenge/:challengeId', () => {
user = await generateUser({balance: 1});
guild = await generateGroup(user);
challenge = await generateChallenge(user, guild);
await user.post(`/challenges/${challenge._id}/join`);
});
it('returns error when challenge is not found', async () => {
@@ -15,6 +15,7 @@ describe('POST /tasks/:id/score/:direction', () => {
user = await generateUser();
guild = await generateGroup(user);
challenge = await generateChallenge(user, guild);
await user.post(`/challenges/${challenge._id}/join`);
});
context('habits', () => {
@@ -15,6 +15,7 @@ describe('PUT /tasks/:id', () => {
user = await generateUser();
guild = await generateGroup(user);
challenge = await generateChallenge(user, guild);
await user.post(`/challenges/${challenge._id}/join`);
});
context('errors', () => {
@@ -15,6 +15,7 @@ describe('PUT /tasks/:taskId/checklist/:itemId', () => {
user = await generateUser();
guild = await generateGroup(user);
challenge = await generateChallenge(user, guild);
await user.post(`/challenges/${challenge._id}/join`);
});
it('fails on task not found', async () => {
@@ -1,6 +1,8 @@
import {
generateUser,
translate as t,
server,
sleep,
} from '../../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
@@ -94,4 +96,49 @@ describe('POST /tasks/:taskId/checklist/:itemId/score', () => {
message: t('checklistItemNotFound'),
});
});
context('sending task activity webhooks', () => {
before(async () => {
await server.start();
});
after(async () => {
await server.close();
});
it('sends task activity webhooks', async () => {
let uuid = generateUUID();
await user.post('/user/webhook', {
url: `http://localhost:${server.port}/webhooks/${uuid}`,
type: 'taskActivity',
enabled: true,
options: {
checklistScored: true,
updated: false,
},
});
let task = await user.post('/tasks/user', {
text: 'test daily',
type: 'daily',
});
let updatedTask = await user.post(`/tasks/${task.id}/checklist`, {
text: 'checklist item text',
});
let checklistItem = updatedTask.checklist[0];
let scoredItemTask = await user.post(`/tasks/${task.id}/checklist/${checklistItem.id}/score`);
await sleep();
let body = server.getWebhookData(uuid);
expect(body.type).to.eql('checklistScored');
expect(body.task).to.eql(scoredItemTask);
expect(body.item).to.eql(scoredItemTask.checklist[0]);
});
});
});
@@ -117,6 +117,7 @@ describe('DELETE /user', () => {
let authorizedUser = populatedGroup.members[1];
let challenge = await generateChallenge(populatedGroup.groupLeader, group);
await populatedGroup.groupLeader.post(`/challenges/${challenge._id}/join`);
await authorizedUser.post(`/challenges/${challenge._id}/join`);
await challenge.sync();
@@ -9,6 +9,7 @@ import {
import { v4 as generateUUID } from 'uuid';
import { find } from 'lodash';
import apiError from '../../../../../website/server/libs/apiError';
describe('POST /user/class/cast/:spellId', () => {
let user;
@@ -24,7 +25,7 @@ describe('POST /user/class/cast/:spellId', () => {
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('spellNotFound', {spellId}),
message: apiError('spellNotFound', {spellId}),
});
});
@@ -34,7 +35,7 @@ describe('POST /user/class/cast/:spellId', () => {
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('spellNotFound', {spellId}),
message: apiError('spellNotFound', {spellId}),
});
});
@@ -108,6 +109,7 @@ describe('POST /user/class/cast/:spellId', () => {
it('returns an error if a challenge task was targeted', async () => {
let {group, groupLeader} = await createAndPopulateGroup();
let challenge = await generateChallenge(groupLeader, group);
await groupLeader.post(`/challenges/${challenge._id}/join`);
await groupLeader.post(`/tasks/challenge/${challenge._id}`, [
{type: 'habit', text: 'task text'},
]);
@@ -237,6 +239,7 @@ describe('POST /user/class/cast/:spellId', () => {
it('searing brightness does not affect challenge or group tasks', async () => {
let guild = await generateGroup(user);
let challenge = await generateChallenge(user, guild);
await user.post(`/challenges/${challenge._id}/join`);
await user.post(`/tasks/challenge/${challenge._id}`, {
text: 'test challenge habit',
type: 'habit',
@@ -3,8 +3,11 @@
import {
generateUser,
translate as t,
server,
sleep,
} from '../../../../helpers/api-integration/v3';
import content from '../../../../../website/common/script/content';
import { v4 as generateUUID } from 'uuid';
describe('POST /user/feed/:pet/:food', () => {
let user;
@@ -37,4 +40,41 @@ describe('POST /user/feed/:pet/:food', () => {
expect(user.items.food.Milk).to.equal(1);
expect(user.items.pets['Wolf-Base']).to.equal(7);
});
context('sending user activity webhooks', () => {
before(async () => {
await server.start();
});
after(async () => {
await server.close();
});
it('sends user activity webhook when a new mount is raised', async () => {
let uuid = generateUUID();
await user.post('/user/webhook', {
url: `http://localhost:${server.port}/webhooks/${uuid}`,
type: 'userActivity',
enabled: true,
options: {
mountRaised: true,
},
});
await user.update({
'items.pets.Wolf-Base': 49,
'items.food.Milk': 2,
});
let res = await user.post('/user/feed/Wolf-Base/Milk');
await sleep();
let body = server.getWebhookData(uuid);
expect(body.type).to.eql('mountRaised');
expect(body.pet).to.eql('Wolf-Base');
expect(body.message).to.eql(res.message);
});
});
});
@@ -1,7 +1,10 @@
import {
generateUser,
translate as t,
server,
sleep,
} from '../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
describe('POST /user/hatch/:egg/:hatchingPotion', () => {
let user;
@@ -28,4 +31,41 @@ describe('POST /user/hatch/:egg/:hatchingPotion', () => {
data: JSON.parse(JSON.stringify(user.items)),
});
});
context('sending user activity webhooks', () => {
before(async () => {
await server.start();
});
after(async () => {
await server.close();
});
it('sends user activity webhook when a new pet is hatched', async () => {
let uuid = generateUUID();
await user.post('/user/webhook', {
url: `http://localhost:${server.port}/webhooks/${uuid}`,
type: 'userActivity',
enabled: true,
options: {
petHatched: true,
},
});
await user.update({
'items.eggs.Wolf': 1,
'items.hatchingPotions.Base': 1,
});
let res = await user.post('/user/hatch/Wolf/Base');
await sleep();
let body = server.getWebhookData(uuid);
expect(body.type).to.eql('petHatched');
expect(body.pet).to.eql('Wolf-Base');
expect(body.message).to.eql(res.message);
});
});
});
@@ -90,6 +90,7 @@ describe('POST /user/reset', () => {
it('does not delete challenge or group tasks', async () => {
let guild = await generateGroup(user);
let challenge = await generateChallenge(user, guild);
await user.post(`/challenges/${challenge._id}/join`);
await user.post(`/tasks/challenge/${challenge._id}`, {
text: 'test challenge habit',
type: 'habit',
@@ -5,6 +5,7 @@ import {
translate as t,
} from '../../../../../helpers/api-integration/v3';
import shared from '../../../../../../website/common/script';
import apiError from '../../../../../../website/server/libs/apiError';
let content = shared.content;
@@ -24,7 +25,7 @@ describe('POST /user/buy/:key', () => {
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('itemNotFound', {key: 'notExisting'}),
message: apiError('itemNotFound', {key: 'notExisting'}),
});
});
@@ -2,8 +2,8 @@
import {
generateUser,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import apiError from '../../../../../../website/server/libs/apiError';
describe('POST /user/buy-gear/:key', () => {
let user;
@@ -21,7 +21,7 @@ describe('POST /user/buy-gear/:key', () => {
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('itemNotFound', {key: 'notExisting'}),
message: apiError('itemNotFound', {key: 'notExisting'}),
});
});
@@ -3,6 +3,7 @@ import {
translate as t,
} from '../../../../../helpers/api-integration/v3';
import shared from '../../../../../../website/common/script';
import apiError from '../../../../../../website/server/libs/apiError';
let content = shared.content;
@@ -20,7 +21,7 @@ describe('POST /user/buy-quest/:key', () => {
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('questNotFound', {key: 'notExisting'}),
message: apiError('questNotFound', {key: 'notExisting'}),
});
});
@@ -37,4 +38,32 @@ describe('POST /user/buy-quest/:key', () => {
itemText: item.text(),
}));
});
it('returns an error if quest prerequisites are not met', async () => {
let key = 'dilatoryDistress2';
await expect(user.post(`/user/buy-quest/${key}`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('mustComplete', {quest: 'dilatoryDistress1'}),
});
});
it('allows purchase of a quest if prerequisites are met', async () => {
const prerequisite = 'dilatoryDistress1';
const key = 'dilatoryDistress2';
const item = content.quests[key];
const achievementName = `achievements.quests.${prerequisite}`;
await user.update({[achievementName]: true, 'stats.gp': 9999});
let res = await user.post(`/user/buy-quest/${key}`);
await user.sync();
expect(res.data).to.eql(user.items.quests);
expect(res.message).to.equal(t('messageBought', {
itemText: item.text(),
}));
});
});
@@ -3,6 +3,7 @@ import {
translate as t,
} from '../../../../../helpers/api-integration/v3';
import shared from '../../../../../../website/common/script';
import apiError from '../../../../../../website/server/libs/apiError';
let content = shared.content;
@@ -20,7 +21,7 @@ describe('POST /user/buy-special-spell/:key', () => {
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('spellNotFound', {spellId: 'notExisting'}),
message: apiError('spellNotFound', {spellId: 'notExisting'}),
});
});
@@ -2,6 +2,7 @@ import {
generateUser,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import apiError from '../../../../../../website/server/libs/apiError';
describe('POST /user/allocate', () => {
let user;
@@ -17,7 +18,7 @@ describe('POST /user/allocate', () => {
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidAttribute', {attr: 'invalid'}),
message: apiError('invalidAttribute', {attr: 'invalid'}),
});
});
@@ -3,6 +3,7 @@ import {
translate as t,
} from '../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
import apiError from '../../../../../website/server/libs/apiError';
describe('POST /user/webhook', () => {
let user, body;
@@ -116,6 +117,7 @@ describe('POST /user/webhook', () => {
let webhook = await user.post('/user/webhook', body);
expect(webhook.options).to.eql({
checklistScored: false,
created: false,
updated: false,
deleted: false,
@@ -126,6 +128,7 @@ describe('POST /user/webhook', () => {
it('can set taskActivity options', async () => {
body.type = 'taskActivity';
body.options = {
checklistScored: true,
created: true,
updated: true,
deleted: true,
@@ -135,6 +138,7 @@ describe('POST /user/webhook', () => {
let webhook = await user.post('/user/webhook', body);
expect(webhook.options).to.eql({
checklistScored: true,
created: true,
updated: true,
deleted: true,
@@ -145,6 +149,7 @@ describe('POST /user/webhook', () => {
it('discards extra properties in taskActivity options', async () => {
body.type = 'taskActivity';
body.options = {
checklistScored: false,
created: true,
updated: true,
deleted: true,
@@ -156,6 +161,7 @@ describe('POST /user/webhook', () => {
expect(webhook.options.foo).to.not.exist;
expect(webhook.options).to.eql({
checklistScored: false,
created: true,
updated: true,
deleted: true,
@@ -200,7 +206,7 @@ describe('POST /user/webhook', () => {
await expect(user.post('/user/webhook', body)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('groupIdRequired'),
message: apiError('groupIdRequired'),
});
});
@@ -218,4 +224,16 @@ describe('POST /user/webhook', () => {
groupId: body.options.groupId,
});
});
it('discards extra properties in globalActivity options', async () => {
body.type = 'globalActivity';
body.options = {
foo: 'bar',
};
let webhook = await user.post('/user/webhook', body);
expect(webhook.options.foo).to.not.exist;
expect(webhook.options).to.eql({});
});
});
@@ -3,6 +3,7 @@ import {
translate as t,
} from '../../../../helpers/api-integration/v3';
import { v4 as generateUUID} from 'uuid';
import apiError from '../../../../../website/server/libs/apiError';
describe('PUT /user/webhook/:id', () => {
let user, webhookToUpdate;
@@ -95,6 +96,7 @@ describe('PUT /user/webhook/:id', () => {
let webhook = await user.put(`/user/webhook/${webhookToUpdate.id}`, {type, options});
expect(webhook.options).to.eql({
checklistScored: false, // starting value
created: true, // starting value
updated: false,
deleted: true,
@@ -126,7 +128,7 @@ describe('PUT /user/webhook/:id', () => {
await expect(user.put(`/user/webhook/${webhookToUpdate.id}`, {type, options})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('groupIdRequired'),
message: apiError('groupIdRequired'),
});
});
});
@@ -1,19 +1,19 @@
import apiMessages from '../../../../../website/server/libs/apiMessages';
import apiError from '../../../../../website/server/libs/apiError';
describe('API Messages', () => {
const message = 'Only public guilds support pagination.';
it('returns an API message', () => {
expect(apiMessages('guildsOnlyPaginate')).to.equal(message);
expect(apiError('guildsOnlyPaginate')).to.equal(message);
});
it('throws if the API message does not exist', () => {
expect(() => apiMessages('iDoNotExist')).to.throw;
expect(() => apiError('iDoNotExist')).to.throw;
});
it('clones the passed variables', () => {
let vars = {a: 1};
sandbox.stub(_, 'clone').returns({});
apiMessages('guildsOnlyPaginate', vars);
apiError('guildsOnlyPaginate', vars);
expect(_.clone).to.have.been.calledOnce;
expect(_.clone).to.have.been.calledWith(vars);
});
@@ -22,7 +22,7 @@ describe('API Messages', () => {
let vars = {a: 1};
let stub = sinon.stub().returns('string');
sandbox.stub(_, 'template').returns(stub);
apiMessages('guildsOnlyPaginate', vars);
apiError('guildsOnlyPaginate', vars);
expect(_.template).to.have.been.calledOnce;
expect(_.template).to.have.been.calledWith(message);
expect(stub).to.have.been.calledOnce;
+2 -4
View File
@@ -19,7 +19,6 @@ function getUser () {
emails: [{
value: 'email@facebook',
}],
displayName: 'fb display name',
},
},
profile: {
@@ -100,7 +99,7 @@ describe('emails', () => {
let data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
expect(data).to.have.property('name', user.auth.facebook.displayName);
expect(data).to.have.property('name', user.profile.name);
expect(data).to.have.property('email', user.auth.facebook.emails[0].value);
expect(data).to.have.property('_id', user._id);
expect(data).to.have.property('canSend', true);
@@ -110,13 +109,12 @@ describe('emails', () => {
let attachEmail = requireAgain(pathToEmailLib);
let getUserInfo = attachEmail.getUserInfo;
let user = getUser();
delete user.profile.name;
delete user.auth.local.email;
delete user.auth.facebook;
let data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
expect(data).to.have.property('name', user.auth.local.username);
expect(data).to.have.property('name', user.profile.name);
expect(data).not.to.have.property('email');
expect(data).to.have.property('_id', user._id);
expect(data).to.have.property('canSend', true);
@@ -443,8 +443,7 @@ describe('Purchasing a group plan for group', () => {
await api.createSubscription(data);
let updatedUser = await User.findById(recipient._id).exec();
const updatedUser = await User.findById(recipient._id).exec();
expect(updatedUser.purchased.plan.extraMonths).to.within(2, 3);
});
@@ -632,6 +632,15 @@ describe('payments/index', () => {
expect(user.sendMessage).to.be.calledWith(recipient, { receiverMsg: msg, senderMsg: msg });
});
it('sends a message from purchaser to recipient wtih custom message', async () => {
data.gift.message = 'giftmessage';
await api.buyGems(data);
const msg = `\`Hello recipient, sender has sent you 4 gems!\` ${data.gift.message}`;
expect(user.sendMessage).to.be.calledWith(recipient, { receiverMsg: msg, senderMsg: msg });
});
it('sends a push notification if user did not gift to self', async () => {
await api.buyGems(data);
expect(notifications.sendNotification).to.be.calledOnce;
@@ -37,6 +37,22 @@ describe('checkout', () => {
payments.createSubscription.restore();
});
it('should error if there is no token', async () => {
await expect(stripePayments.checkout({
user,
gift,
groupId,
email,
headers,
coupon,
}, stripe))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
message: 'Missing req.body.id',
name: 'BadRequest',
});
});
it('should error if gem amount is too low', async () => {
let receivingUser = new User();
receivingUser.save();
@@ -64,7 +80,6 @@ describe('checkout', () => {
});
});
it('should error if user cannot get gems', async () => {
gift = undefined;
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
+295 -33
View File
@@ -4,11 +4,19 @@ import {
taskScoredWebhook,
groupChatReceivedWebhook,
taskActivityWebhook,
questActivityWebhook,
userActivityWebhook,
} from '../../../../../website/server/libs/webhook';
import {
model as User,
} from '../../../../../website/server/models/user';
import {
generateUser,
} from '../../../../helpers/api-unit.helper.js';
import { defer } from '../../../../helpers/api-unit.helper';
describe('webhooks', () => {
let webhooks;
let webhooks, user;
beforeEach(() => {
sandbox.stub(got, 'post').returns(defer().promise);
@@ -23,6 +31,26 @@ describe('webhooks', () => {
updated: true,
deleted: true,
scored: true,
checklistScored: true,
},
}, {
id: 'questActivity',
url: 'http://quest-activity.com',
enabled: true,
type: 'questActivity',
options: {
questStarted: true,
questFinised: true,
},
}, {
id: 'userActivity',
url: 'http://user-activity.com',
enabled: true,
type: 'userActivity',
options: {
petHatched: true,
mountRaised: true,
leveledUp: true,
},
}, {
id: 'groupChatReceived',
@@ -33,6 +61,9 @@ describe('webhooks', () => {
groupId: 'group-id',
},
}];
user = generateUser();
user.webhooks = webhooks;
});
afterEach(() => {
@@ -57,7 +88,8 @@ describe('webhooks', () => {
let body = { foo: 'bar' };
sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}], body);
user.webhooks = [{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}];
sendWebhook.send(user, body);
expect(WebhookSender.defaultTransformData).to.be.calledOnce;
expect(got.post).to.be.calledOnce;
@@ -67,6 +99,30 @@ describe('webhooks', () => {
});
});
it('adds default data (user and webhookType) to the body', () => {
let sendWebhook = new WebhookSender({
type: 'custom',
});
sandbox.spy(sendWebhook, 'attachDefaultData');
let body = { foo: 'bar' };
user.webhooks = [{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}];
sendWebhook.send(user, body);
expect(sendWebhook.attachDefaultData).to.be.calledOnce;
expect(got.post).to.be.calledOnce;
expect(got.post).to.be.calledWithMatch('http://custom-url.com', {
json: true,
});
expect(body).to.eql({
foo: 'bar',
user: {_id: user._id},
webhookType: 'custom',
});
});
it('can pass in a data transformation function', () => {
sandbox.spy(WebhookSender, 'defaultTransformData');
let sendWebhook = new WebhookSender({
@@ -80,7 +136,8 @@ describe('webhooks', () => {
let body = { foo: 'bar' };
sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}], body);
user.webhooks = [{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}];
sendWebhook.send(user, body);
expect(WebhookSender.defaultTransformData).to.not.be.called;
expect(got.post).to.be.calledOnce;
@@ -93,7 +150,7 @@ describe('webhooks', () => {
});
});
it('provieds a default filter function', () => {
it('provides a default filter function', () => {
sandbox.spy(WebhookSender, 'defaultWebhookFilter');
let sendWebhook = new WebhookSender({
type: 'custom',
@@ -101,7 +158,8 @@ describe('webhooks', () => {
let body = { foo: 'bar' };
sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}], body);
user.webhooks = [{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}];
sendWebhook.send(user, body);
expect(WebhookSender.defaultWebhookFilter).to.be.calledOnce;
});
@@ -117,7 +175,8 @@ describe('webhooks', () => {
let body = { foo: 'bar' };
sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}], body);
user.webhooks = [{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}];
sendWebhook.send(user, body);
expect(WebhookSender.defaultWebhookFilter).to.not.be.called;
expect(got.post).to.not.be.called;
@@ -134,10 +193,11 @@ describe('webhooks', () => {
let body = { foo: 'bar' };
sendWebhook.send([
user.webhooks = [
{ id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom', options: { foo: 'bar' }},
{ id: 'other-custom-webhook', url: 'http://other-custom-url.com', enabled: true, type: 'custom', options: { foo: 'foo' }},
], body);
];
sendWebhook.send(user, body);
expect(got.post).to.be.calledOnce;
expect(got.post).to.be.calledWithMatch('http://custom-url.com');
@@ -150,7 +210,8 @@ describe('webhooks', () => {
let body = { foo: 'bar' };
sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: false, type: 'custom'}], body);
user.webhooks = [{id: 'custom-webhook', url: 'http://custom-url.com', enabled: false, type: 'custom'}];
sendWebhook.send(user, body);
expect(got.post).to.not.be.called;
});
@@ -162,7 +223,8 @@ describe('webhooks', () => {
let body = { foo: 'bar' };
sendWebhook.send([{id: 'custom-webhook', url: 'httxp://custom-url!!', enabled: true, type: 'custom'}], body);
user.webhooks = [{id: 'custom-webhook', url: 'httxp://custom-url!!!', enabled: true, type: 'custom'}];
sendWebhook.send(user, body);
expect(got.post).to.not.be.called;
});
@@ -174,10 +236,30 @@ describe('webhooks', () => {
let body = { foo: 'bar' };
sendWebhook.send([
user.webhooks = [
{ id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'},
{ id: 'other-webhook', url: 'http://other-url.com', enabled: true, type: 'other'},
], body);
];
sendWebhook.send(user, body);
expect(got.post).to.be.calledOnce;
expect(got.post).to.be.calledWithMatch('http://custom-url.com', {
body,
json: true,
});
});
it('sends every type of activity to global webhooks', () => {
let sendWebhook = new WebhookSender({
type: 'custom',
});
let body = { foo: 'bar' };
user.webhooks = [
{ id: 'global-webhook', url: 'http://custom-url.com', enabled: true, type: 'globalActivity'},
];
sendWebhook.send(user, body);
expect(got.post).to.be.calledOnce;
expect(got.post).to.be.calledWithMatch('http://custom-url.com', {
@@ -193,10 +275,11 @@ describe('webhooks', () => {
let body = { foo: 'bar' };
sendWebhook.send([
user.webhooks = [
{ id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'},
{ id: 'other-custom-webhook', url: 'http://other-url.com', enabled: true, type: 'custom'},
], body);
];
sendWebhook.send(user, body);
expect(got.post).to.be.calledTwice;
expect(got.post).to.be.calledWithMatch('http://custom-url.com', {
@@ -216,7 +299,6 @@ describe('webhooks', () => {
beforeEach(() => {
data = {
user: {
_id: 'user-id',
_tmp: {foo: 'bar'},
stats: {
lvl: 5,
@@ -227,17 +309,6 @@ describe('webhooks', () => {
return this;
},
},
addComputedStatsToJSONObj () {
let mockStats = Object.assign({
maxHealth: 50,
maxMP: 103,
toNextLevel: 40,
}, this.stats);
delete mockStats.toJSON;
return mockStats;
},
},
task: {
text: 'text',
@@ -245,18 +316,66 @@ describe('webhooks', () => {
direction: 'up',
delta: 176,
};
let mockStats = Object.assign({
maxHealth: 50,
maxMP: 103,
toNextLevel: 40,
}, data.user.stats);
delete mockStats.toJSON;
sandbox.stub(User, 'addComputedStatsToJSONObj').returns(mockStats);
});
it('sends task and stats data', () => {
taskScoredWebhook.send(webhooks, data);
taskScoredWebhook.send(user, data);
expect(got.post).to.be.calledOnce;
expect(got.post).to.be.calledWithMatch(webhooks[0].url, {
json: true,
body: {
type: 'scored',
webhookType: 'taskActivity',
user: {
_id: 'user-id',
_id: user._id,
_tmp: {foo: 'bar'},
stats: {
lvl: 5,
int: 10,
str: 5,
exp: 423,
toNextLevel: 40,
maxHealth: 50,
maxMP: 103,
},
},
task: {
text: 'text',
},
direction: 'up',
delta: 176,
},
});
});
it('sends task and stats data to globalActivity webhookd', () => {
user.webhooks = [{
id: 'globalActivity',
url: 'http://global-activity.com',
enabled: true,
type: 'globalActivity',
}];
taskScoredWebhook.send(user, data);
expect(got.post).to.be.calledOnce;
expect(got.post).to.be.calledWithMatch('http://global-activity.com', {
json: true,
body: {
type: 'scored',
webhookType: 'taskActivity',
user: {
_id: user._id,
_tmp: {foo: 'bar'},
stats: {
lvl: 5,
@@ -280,7 +399,7 @@ describe('webhooks', () => {
it('does not send task scored data if scored option is not true', () => {
webhooks[0].options.scored = false;
taskScoredWebhook.send(webhooks, data);
taskScoredWebhook.send(user, data);
expect(got.post).to.not.be.called;
});
@@ -301,13 +420,17 @@ describe('webhooks', () => {
it(`sends ${type} tasks`, () => {
data.type = type;
taskActivityWebhook.send(webhooks, data);
taskActivityWebhook.send(user, data);
expect(got.post).to.be.calledOnce;
expect(got.post).to.be.calledWithMatch(webhooks[0].url, {
json: true,
body: {
type,
webhookType: 'taskActivity',
user: {
_id: user._id,
},
task: data.task,
},
});
@@ -317,7 +440,142 @@ describe('webhooks', () => {
data.type = type;
webhooks[0].options[type] = false;
taskActivityWebhook.send(webhooks, data);
taskActivityWebhook.send(user, data);
expect(got.post).to.not.be.called;
});
});
describe('checklistScored', () => {
beforeEach(() => {
data = {
task: {
text: 'text',
},
item: {
text: 'item-text',
},
};
});
it('sends \'checklistScored\' tasks', () => {
data.type = 'checklistScored';
taskActivityWebhook.send(user, data);
expect(got.post).to.be.calledOnce;
expect(got.post).to.be.calledWithMatch(webhooks[0].url, {
json: true,
body: {
webhookType: 'taskActivity',
user: {
_id: user._id,
},
type: data.type,
task: data.task,
item: data.item,
},
});
});
it('does not send task \'checklistScored\' data if \'checklistScored\' option is not true', () => {
data.type = 'checklistScored';
webhooks[0].options.checklistScored = false;
taskActivityWebhook.send(user, data);
expect(got.post).to.not.be.called;
});
});
});
describe('userActivityWebhook', () => {
let data;
beforeEach(() => {
data = {
something: true,
};
});
['petHatched', 'mountRaised', 'leveledUp'].forEach((type) => {
it(`sends ${type} webhooks`, () => {
data.type = type;
userActivityWebhook.send(user, data);
expect(got.post).to.be.calledOnce;
expect(got.post).to.be.calledWithMatch(webhooks[2].url, {
json: true,
body: {
type,
webhookType: 'userActivity',
user: {
_id: user._id,
},
something: true,
},
});
});
it(`does not send webhook ${type} data if ${type} option is not true`, () => {
data.type = type;
webhooks[2].options[type] = false;
userActivityWebhook.send(user, data);
expect(got.post).to.not.be.called;
});
});
});
describe('questActivityWebhook', () => {
let data;
beforeEach(() => {
data = {
group: {
id: 'group-id',
name: 'some group',
otherData: 'foo',
},
quest: {
key: 'some-key',
},
};
});
['questStarted', 'questFinised'].forEach((type) => {
it(`sends ${type} webhooks`, () => {
data.type = type;
questActivityWebhook.send(user, data);
expect(got.post).to.be.calledOnce;
expect(got.post).to.be.calledWithMatch(webhooks[1].url, {
json: true,
body: {
type,
webhookType: 'questActivity',
user: {
_id: user._id,
},
group: {
id: 'group-id',
name: 'some group',
},
quest: {
key: 'some-key',
},
},
});
});
it(`does not send webhook ${type} data if ${type} option is not true`, () => {
data.type = type;
webhooks[1].options[type] = false;
userActivityWebhook.send(user, data);
expect(got.post).to.not.be.called;
});
@@ -338,12 +596,16 @@ describe('webhooks', () => {
},
};
groupChatReceivedWebhook.send(webhooks, data);
groupChatReceivedWebhook.send(user, data);
expect(got.post).to.be.calledOnce;
expect(got.post).to.be.calledWithMatch(webhooks[webhooks.length - 1].url, {
json: true,
body: {
webhookType: 'groupChatReceived',
user: {
_id: user._id,
},
group: {
id: 'group-id',
name: 'some group',
@@ -369,7 +631,7 @@ describe('webhooks', () => {
},
};
groupChatReceivedWebhook.send(webhooks, data);
groupChatReceivedWebhook.send(user, data);
expect(got.post).to.not.be.called;
});
+1 -1
View File
@@ -15,7 +15,7 @@ describe('auth middleware', () => {
describe('auth with headers', () => {
it('allows to specify a list of user field that we do not want to load', (done) => {
const authWithHeaders = authWithHeadersFactory(false, {
const authWithHeaders = authWithHeadersFactory({
userFieldsToExclude: ['items', 'flags', 'auth.timestamps'],
});
@@ -7,7 +7,7 @@ import {
import i18n from '../../../../../website/common/script/i18n';
import { ensureAdmin, ensureSudo } from '../../../../../website/server/middlewares/ensureAccessRight';
import { NotAuthorized } from '../../../../../website/server/libs/errors';
import apiMessages from '../../../../../website/server/libs/apiMessages';
import apiError from '../../../../../website/server/libs/apiError';
describe('ensure access middlewares', () => {
let res, req, next;
@@ -46,7 +46,7 @@ describe('ensure access middlewares', () => {
ensureSudo(req, res, next);
const calledWith = next.getCall(0).args;
expect(calledWith[0].message).to.equal(apiMessages('noSudoAccess'));
expect(calledWith[0].message).to.equal(apiError('noSudoAccess'));
expect(calledWith[0] instanceof NotAuthorized).to.equal(true);
});
+86 -5
View File
@@ -11,7 +11,10 @@ import {
} from '../../../../../website/server/models/group';
import { model as User } from '../../../../../website/server/models/user';
import { quests as questScrolls } from '../../../../../website/common/script/content';
import { groupChatReceivedWebhook } from '../../../../../website/server/libs/webhook';
import {
groupChatReceivedWebhook,
questActivityWebhook,
} from '../../../../../website/server/libs/webhook';
import * as email from '../../../../../website/server/libs/email';
import { TAVERN_ID } from '../../../../../website/common/script/';
import shared from '../../../../../website/common';
@@ -21,6 +24,7 @@ describe('Group Model', () => {
beforeEach(async () => {
sandbox.stub(email, 'sendTxn');
sandbox.stub(questActivityWebhook, 'send');
party = new Group({
name: 'test party',
@@ -1189,6 +1193,47 @@ describe('Group Model', () => {
expect(typeOfEmail).to.eql('quest-started');
});
it('sends webhook to participating members that quest has started', async () => {
// should receive webhook
participatingMember.webhooks = [{
type: 'questActivity',
url: 'http://someurl.com',
options: {
questStarted: true,
},
}];
questLeader.webhooks = [{
type: 'questActivity',
url: 'http://someurl.com',
options: {
questStarted: true,
},
}];
await Promise.all([participatingMember.save(), questLeader.save()]);
await party.startQuest(nonParticipatingMember);
await sleep(0.5);
expect(questActivityWebhook.send).to.be.calledTwice; // for 2 participating members
let args = questActivityWebhook.send.args[0];
let webhooks = args[0].webhooks;
let webhookOwner = args[0]._id;
let options = args[1];
expect(webhooks).to.have.a.lengthOf(1);
if (webhookOwner === questLeader._id) {
expect(webhooks[0].id).to.eql(questLeader.webhooks[0].id);
} else {
expect(webhooks[0].id).to.eql(participatingMember.webhooks[0].id);
}
expect(webhooks[0].type).to.eql('questActivity');
expect(options.group).to.eql(party);
expect(options.quest.key).to.eql('whale');
});
it('sends email only to members who have not opted out', async () => {
participatingMember.preferences.emailNotifications.questStarted = false;
questLeader.preferences.emailNotifications.questStarted = true;
@@ -1570,6 +1615,42 @@ describe('Group Model', () => {
});
});
it('sends webhook to participating members that quest has finished', async () => {
// should receive webhook
participatingMember.webhooks = [{
type: 'questActivity',
url: 'http://someurl.com',
options: {
questFinished: true,
},
}];
questLeader.webhooks = [{
type: 'questActivity',
url: 'http://someurl.com',
options: {
questStarted: true, // will not receive the webhook
},
}];
await Promise.all([participatingMember.save(), questLeader.save()]);
await party.finishQuest(quest);
await sleep(0.5);
expect(questActivityWebhook.send).to.be.calledOnce;
let args = questActivityWebhook.send.args[0];
let webhooks = args[0].webhooks;
let options = args[1];
expect(webhooks).to.have.a.lengthOf(1);
expect(webhooks[0].id).to.eql(participatingMember.webhooks[0].id);
expect(webhooks[0].type).to.eql('questActivity');
expect(options.group).to.eql(party);
expect(options.quest.key).to.eql(quest.key);
});
context('World quests in Tavern', () => {
let tavernQuest;
@@ -1685,7 +1766,7 @@ describe('Group Model', () => {
expect(groupChatReceivedWebhook.send).to.be.calledOnce;
let args = groupChatReceivedWebhook.send.args[0];
let webhooks = args[0];
let webhooks = args[0].webhooks;
let options = args[1];
expect(webhooks).to.have.a.lengthOf(1);
@@ -1749,9 +1830,9 @@ describe('Group Model', () => {
expect(groupChatReceivedWebhook.send).to.be.calledThrice;
let args = groupChatReceivedWebhook.send.args;
expect(args.find(arg => arg[0][0].id === memberWithWebhook.webhooks[0].id)).to.be.exist;
expect(args.find(arg => arg[0][0].id === memberWithWebhook2.webhooks[0].id)).to.be.exist;
expect(args.find(arg => arg[0][0].id === memberWithWebhook3.webhooks[0].id)).to.be.exist;
expect(args.find(arg => arg[0].webhooks[0].id === memberWithWebhook.webhooks[0].id)).to.be.exist;
expect(args.find(arg => arg[0].webhooks[0].id === memberWithWebhook2.webhooks[0].id)).to.be.exist;
expect(args.find(arg => arg[0].webhooks[0].id === memberWithWebhook3.webhooks[0].id)).to.be.exist;
});
});
+36 -1
View File
@@ -42,13 +42,48 @@ describe('User Model', () => {
expect(userToJSON.stats.maxHealth).to.not.exist;
expect(userToJSON.stats.toNextLevel).to.not.exist;
user.addComputedStatsToJSONObj(userToJSON.stats);
User.addComputedStatsToJSONObj(userToJSON.stats, userToJSON);
expect(userToJSON.stats.maxMP).to.exist;
expect(userToJSON.stats.maxHealth).to.equal(common.maxHealth);
expect(userToJSON.stats.toNextLevel).to.equal(common.tnl(user.stats.lvl));
});
it('can transform user object without mongoose helpers', async () => {
let user = new User();
await user.save();
let userToJSON = await User.findById(user._id).lean().exec();
expect(userToJSON.stats.maxMP).to.not.exist;
expect(userToJSON.stats.maxHealth).to.not.exist;
expect(userToJSON.stats.toNextLevel).to.not.exist;
expect(userToJSON.id).to.not.exist;
User.transformJSONUser(userToJSON);
expect(userToJSON.id).to.equal(userToJSON._id);
expect(userToJSON.stats.maxMP).to.not.exist;
expect(userToJSON.stats.maxHealth).to.not.exist;
expect(userToJSON.stats.toNextLevel).to.not.exist;
});
it('can transform user object without mongoose helpers (including computed stats)', async () => {
let user = new User();
await user.save();
let userToJSON = await User.findById(user._id).lean().exec();
expect(userToJSON.stats.maxMP).to.not.exist;
expect(userToJSON.stats.maxHealth).to.not.exist;
expect(userToJSON.stats.toNextLevel).to.not.exist;
User.transformJSONUser(userToJSON, true);
expect(userToJSON.id).to.equal(userToJSON._id);
expect(userToJSON.stats.maxMP).to.exist;
expect(userToJSON.stats.maxHealth).to.equal(common.maxHealth);
expect(userToJSON.stats.toNextLevel).to.equal(common.tnl(user.stats.lvl));
});
context('notifications', () => {
it('can add notifications without data', () => {
let user = new User();
+180 -3
View File
@@ -1,6 +1,7 @@
import { model as Webhook } from '../../../../../website/server/models/webhook';
import { BadRequest } from '../../../../../website/server/libs/errors';
import { v4 as generateUUID } from 'uuid';
import apiError from '../../../../../website/server/libs/apiError';
describe('Webhook Model', () => {
context('Instance Methods', () => {
@@ -24,6 +25,7 @@ describe('Webhook Model', () => {
updated: true,
deleted: true,
scored: true,
checklistScored: true,
},
};
});
@@ -36,6 +38,7 @@ describe('Webhook Model', () => {
wh.formatOptions(res);
expect(wh.options).to.eql({
checklistScored: false,
created: false,
updated: false,
deleted: false,
@@ -51,6 +54,7 @@ describe('Webhook Model', () => {
wh.formatOptions(res);
expect(wh.options).to.eql({
checklistScored: true,
created: false,
updated: true,
deleted: true,
@@ -67,6 +71,7 @@ describe('Webhook Model', () => {
expect(wh.options.foo).to.not.exist;
expect(wh.options).to.eql({
checklistScored: true,
created: true,
updated: true,
deleted: true,
@@ -74,7 +79,155 @@ describe('Webhook Model', () => {
});
});
['created', 'updated', 'deleted', 'scored'].forEach((option) => {
['created', 'updated', 'deleted', 'scored', 'checklistScored'].forEach((option) => {
it(`validates that ${option} is a boolean`, (done) => {
config.options[option] = 'not a boolean';
try {
let wh = new Webhook(config);
wh.formatOptions(res);
} catch (err) {
expect(err).to.be.an.instanceOf(BadRequest);
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('webhookBooleanOption', { option });
done();
}
});
});
});
context('type is userActivity', () => {
let config;
beforeEach(() => {
config = {
type: 'userActivity',
url: 'https//exmaple.com/endpoint',
options: {
petHatched: true,
mountRaised: true,
leveledUp: true,
},
};
});
it('it provides default values for options', () => {
delete config.options;
let wh = new Webhook(config);
wh.formatOptions(res);
expect(wh.options).to.eql({
petHatched: false,
mountRaised: false,
leveledUp: false,
});
});
it('provides missing user options', () => {
delete config.options.petHatched;
let wh = new Webhook(config);
wh.formatOptions(res);
expect(wh.options).to.eql({
petHatched: false,
mountRaised: true,
leveledUp: true,
});
});
it('discards additional options', () => {
config.options.foo = 'another option';
let wh = new Webhook(config);
wh.formatOptions(res);
expect(wh.options.foo).to.not.exist;
expect(wh.options).to.eql({
petHatched: true,
mountRaised: true,
leveledUp: true,
});
});
['petHatched', 'petHatched', 'leveledUp'].forEach((option) => {
it(`validates that ${option} is a boolean`, (done) => {
config.options[option] = 'not a boolean';
try {
let wh = new Webhook(config);
wh.formatOptions(res);
} catch (err) {
expect(err).to.be.an.instanceOf(BadRequest);
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('webhookBooleanOption', { option });
done();
}
});
});
});
context('type is questActivity', () => {
let config;
beforeEach(() => {
config = {
type: 'questActivity',
url: 'https//exmaple.com/endpoint',
options: {
questStarted: true,
questFinished: true,
},
};
});
it('it provides default values for options', () => {
delete config.options;
let wh = new Webhook(config);
wh.formatOptions(res);
expect(wh.options).to.eql({
questStarted: false,
questFinished: false,
});
});
it('provides missing user options', () => {
delete config.options.questStarted;
let wh = new Webhook(config);
wh.formatOptions(res);
expect(wh.options).to.eql({
questStarted: false,
questFinished: true,
});
});
it('discards additional options', () => {
config.options.foo = 'another option';
let wh = new Webhook(config);
wh.formatOptions(res);
expect(wh.options.foo).to.not.exist;
expect(wh.options).to.eql({
questStarted: true,
questFinished: true,
});
});
['questStarted', 'questFinished'].forEach((option) => {
it(`validates that ${option} is a boolean`, (done) => {
config.options[option] = 'not a boolean';
@@ -135,12 +288,36 @@ describe('Webhook Model', () => {
wh.formatOptions(res);
} catch (err) {
expect(err).to.be.an.instanceOf(BadRequest);
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('groupIdRequired');
expect(err.message).to.eql(apiError('groupIdRequired'));
done();
}
});
});
context('type is globalActivity', () => {
let config;
beforeEach(() => {
config = {
type: 'globalActivity',
url: 'https//exmaple.com/endpoint',
options: { },
};
});
it('discards additional objects', () => {
config.options.foo = 'another thing';
let wh = new Webhook(config);
wh.formatOptions(res);
expect(wh.options.foo).to.not.exist;
expect(wh.options).to.eql({});
});
});
});
});
});
-3
View File
@@ -1,3 +0,0 @@
This folder contains the test files for the new client side that is being developed.
The old client side tests can be found in /test/client-old.
@@ -0,0 +1,65 @@
import {shallow} from '@vue/test-utils';
import CategoryTags from 'client/components/categories/categoryTags.vue';
describe('Category Tags', () => {
let wrapper;
beforeEach(function () {
wrapper = shallow(CategoryTags, {
propsData: {
categories: [],
},
slots: {
default: '<p>This is a slot.</p>',
},
mocks: {
$t: (string) => string,
},
});
});
it('displays a category', () => {
wrapper.setProps({
categories: [
{
name: 'test',
},
],
});
expect(wrapper.contains('.category-label')).to.eq(true);
expect(wrapper.find('.category-label').text()).to.eq('test');
});
it('displays a habitica official in purple', () => {
wrapper.setProps({
categories: [
{
name: 'habitica_official',
},
],
});
expect(wrapper.contains('.category-label-purple')).to.eq(true);
expect(wrapper.find('.category-label').text()).to.eq('habitica_official');
});
it('displays owner label', () => {
wrapper.setProps({
owner: true,
});
expect(wrapper.contains('.category-label-blue')).to.eq(true);
expect(wrapper.find('.category-label').text()).to.eq('owned');
});
it('displays member label', () => {
wrapper.setProps({
member: true,
});
expect(wrapper.contains('.category-label-green')).to.eq(true);
expect(wrapper.find('.category-label').text()).to.eq('joined');
});
it('displays additional content at the end', () => {
expect(wrapper.find('p').text()).to.eq('This is a slot.');
});
});
@@ -0,0 +1,54 @@
import {shallow} from '@vue/test-utils';
import SidebarSection from 'client/components/sidebarSection.vue';
describe('Sidebar Section', () => {
let wrapper;
beforeEach(function () {
wrapper = shallow(SidebarSection, {
propsData: {
title: 'Hello World',
},
slots: {
default: '<p>This is a test.</p>',
},
});
});
it('displays title', () => {
expect(wrapper.find('h3').text()).to.eq('Hello World');
});
it('displays contents', () => {
expect(wrapper.find('.section-body').find('p').text()).to.eq('This is a test.');
});
it('displays tooltip icon', () => {
expect(wrapper.contains('.section-info')).to.eq(false);
wrapper.setProps({tooltip: 'This is a test'});
expect(wrapper.contains('.section-info')).to.eq(true);
});
it('hides contents', () => {
expect(wrapper.find('.section-body').element.style.display).to.not.eq('none');
wrapper.find('.section-toggle').trigger('click');
expect(wrapper.find('.section-body').element.style.display).to.eq('none');
wrapper.find('.section-toggle').trigger('click');
expect(wrapper.find('.section-body').element.style.display).to.not.eq('none');
});
it('can hide contents by default', () => {
wrapper = shallow(SidebarSection, {
propsData: {
title: 'Hello World',
show: false,
},
slots: {
default: '<p>This is a test.</p>',
},
});
expect(wrapper.find('.section-body').element.style.display).to.eq('none');
});
});
+11
View File
@@ -117,6 +117,17 @@ describe('common.fns.updateStats', () => {
expect(user.addNotification).to.be.calledWith('DROPS_ENABLED');
});
it('add user notification when the user levels up', () => {
const initialLvl = user.stats.lvl;
updateStats(user, {
exp: 3000,
});
expect(user._tmp.leveledUp).to.eql([{
initialLvl,
newLvl: user.stats.lvl,
}]);
});
it('add user notification when rebirth is enabled', () => {
user.stats.lvl = 51;
updateStats(user, { });
+72
View File
@@ -59,6 +59,78 @@ describe('shops', () => {
expect(specialCategory.items.find((item) => item.key === 'weapon_special_critical'));
expect(specialCategory.items.find((item) => item.key === 'weapon_armoire_basicCrossbow'));// eslint-disable-line camelcase
});
it('does not show gear when it is all owned', () => {
let userWithItems = generateUser({
stats: {
class: 'wizard',
},
items: {
gear: {
owned: {
weapon_wizard_0: true, // eslint-disable-line camelcase
weapon_wizard_1: true, // eslint-disable-line camelcase
weapon_wizard_2: true, // eslint-disable-line camelcase
weapon_wizard_3: true, // eslint-disable-line camelcase
weapon_wizard_4: true, // eslint-disable-line camelcase
weapon_wizard_5: true, // eslint-disable-line camelcase
weapon_wizard_6: true, // eslint-disable-line camelcase
armor_wizard_1: true, // eslint-disable-line camelcase
armor_wizard_2: true, // eslint-disable-line camelcase
armor_wizard_3: true, // eslint-disable-line camelcase
armor_wizard_4: true, // eslint-disable-line camelcase
armor_wizard_5: true, // eslint-disable-line camelcase
head_wizard_1: true, // eslint-disable-line camelcase
head_wizard_2: true, // eslint-disable-line camelcase
head_wizard_3: true, // eslint-disable-line camelcase
head_wizard_4: true, // eslint-disable-line camelcase
head_wizard_5: true, // eslint-disable-line camelcase
},
},
},
});
let shopWizardItems = shared.shops.getMarketGearCategories(userWithItems).find(x => x.identifier === 'wizard').items.filter(x => x.klass === 'wizard' && (x.owned === false || x.owned === undefined));
expect(shopWizardItems.length).to.eql(0);
});
it('shows available gear not yet purchased and previously owned', () => {
let userWithItems = generateUser({
stats: {
class: 'wizard',
},
items: {
gear: {
owned: {
weapon_wizard_0: true, // eslint-disable-line camelcase
weapon_wizard_1: true, // eslint-disable-line camelcase
weapon_wizard_2: true, // eslint-disable-line camelcase
weapon_wizard_3: true, // eslint-disable-line camelcase
weapon_wizard_4: true, // eslint-disable-line camelcase
armor_wizard_1: true, // eslint-disable-line camelcase
armor_wizard_2: true, // eslint-disable-line camelcase
armor_wizard_3: false, // eslint-disable-line camelcase
armor_wizard_4: false, // eslint-disable-line camelcase
head_wizard_1: true, // eslint-disable-line camelcase
head_wizard_2: false, // eslint-disable-line camelcase
head_wizard_3: true, // eslint-disable-line camelcase
head_wizard_4: false, // eslint-disable-line camelcase
head_wizard_5: true, // eslint-disable-line camelcase
},
},
},
});
let shopWizardItems = shared.shops.getMarketGearCategories(userWithItems).find(x => x.identifier === 'wizard').items.filter(x => x.klass === 'wizard' && (x.owned === false || x.owned === undefined));
expect(shopWizardItems.find(item => item.key === 'weapon_wizard_5').locked).to.eql(false);
expect(shopWizardItems.find(item => item.key === 'weapon_wizard_6').locked).to.eql(true);
expect(shopWizardItems.find(item => item.key === 'armor_wizard_3').locked).to.eql(false);
expect(shopWizardItems.find(item => item.key === 'armor_wizard_4').locked).to.eql(true);
expect(shopWizardItems.find(item => item.key === 'head_wizard_2').locked).to.eql(false);
expect(shopWizardItems.find(item => item.key === 'head_wizard_4').locked).to.eql(true);
});
});
describe('questShop', () => {
+9 -1
View File
@@ -8,6 +8,7 @@ import {
} from '../../../../website/common/script/libs/errors';
import i18n from '../../../../website/common/script/i18n';
import content from '../../../../website/common/script/content/index';
import errorMessage from '../../../../website/common/script/libs/errorMessage';
describe('shared.ops.buy', () => {
let user;
@@ -40,7 +41,7 @@ describe('shared.ops.buy', () => {
buy(user);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('missingKeyParam'));
expect(err.message).to.equal(errorMessage('missingKeyParam'));
done();
}
});
@@ -68,6 +69,13 @@ describe('shared.ops.buy', () => {
eyewear_special_redTopFrame: true,
eyewear_special_whiteTopFrame: true,
eyewear_special_yellowTopFrame: true,
headAccessory_special_blackHeadband: true,
headAccessory_special_blueHeadband: true,
headAccessory_special_greenHeadband: true,
headAccessory_special_pinkHeadband: true,
headAccessory_special_redHeadband: true,
headAccessory_special_whiteHeadband: true,
headAccessory_special_yellowHeadband: true,
});
});
+141
View File
@@ -0,0 +1,141 @@
/* eslint-disable camelcase */
import sinon from 'sinon'; // eslint-disable-line no-shadow
import {
generateUser,
} from '../../../helpers/common.helper';
import {
BadRequest, NotAuthorized,
} from '../../../../website/common/script/libs/errors';
import i18n from '../../../../website/common/script/i18n';
import {BuyGemOperation} from '../../../../website/common/script/ops/buy/buyGem';
import planGemLimits from '../../../../website/common/script/libs/planGemLimits';
function buyGem (user, req, analytics) {
let buyOp = new BuyGemOperation(user, req, analytics);
return buyOp.purchase();
}
describe('shared.ops.buyGem', () => {
let user;
let analytics = {track () {}};
let goldPoints = 40;
let gemsBought = 40;
let userGemAmount = 10;
beforeEach(() => {
user = generateUser({
stats: { gp: goldPoints },
balance: userGemAmount,
purchased: {
plan: {
gemsBought: 0,
customerId: 'costumer-id',
},
},
});
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
});
context('Gems', () => {
it('purchases gems', () => {
let [, message] = buyGem(user, {params: {type: 'gems', key: 'gem'}}, analytics);
expect(message).to.equal(i18n.t('plusGem', {count: 1}));
expect(user.balance).to.equal(userGemAmount + 0.25);
expect(user.purchased.plan.gemsBought).to.equal(1);
expect(user.stats.gp).to.equal(goldPoints - planGemLimits.convRate);
expect(analytics.track).to.be.calledOnce;
});
it('purchases gems with a different language than the default', () => {
let [, message] = buyGem(user, {params: {type: 'gems', key: 'gem'}, language: 'de'});
expect(message).to.equal(i18n.t('plusGem', {count: 1}, 'de'));
expect(user.balance).to.equal(userGemAmount + 0.25);
expect(user.purchased.plan.gemsBought).to.equal(1);
expect(user.stats.gp).to.equal(goldPoints - planGemLimits.convRate);
});
it('makes bulk purchases of gems', () => {
let [, message] = buyGem(user, {
params: {type: 'gems', key: 'gem'},
quantity: 2,
});
expect(message).to.equal(i18n.t('plusGem', {count: 2}));
expect(user.balance).to.equal(userGemAmount + 0.50);
expect(user.purchased.plan.gemsBought).to.equal(2);
expect(user.stats.gp).to.equal(goldPoints - planGemLimits.convRate * 2);
});
context('Failure conditions', () => {
it('returns an error when key is not provided', (done) => {
try {
buyGem(user, {params: {type: 'gems'}});
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('missingKeyParam'));
done();
}
});
it('prevents unsubscribed user from buying gems', (done) => {
delete user.purchased.plan.customerId;
try {
buyGem(user, {params: {type: 'gems', key: 'gem'}});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('mustSubscribeToPurchaseGems'));
done();
}
});
it('prevents user with not enough gold from buying gems', (done) => {
user.stats.gp = 15;
try {
buyGem(user, {params: {type: 'gems', key: 'gem'}});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('messageNotEnoughGold'));
done();
}
});
it('prevents user that have reached the conversion cap from buying gems', (done) => {
user.stats.gp = goldPoints;
user.purchased.plan.gemsBought = gemsBought;
try {
buyGem(user, {params: {type: 'gems', key: 'gem'}});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('reachedGoldToGemCap', {convCap: planGemLimits.convCap}));
done();
}
});
it('prevents user from buying an invalid quantity', (done) => {
user.stats.gp = goldPoints;
user.purchased.plan.gemsBought = gemsBought;
try {
buyGem(user, {params: {type: 'gems', key: 'gem'}, quantity: 'a'});
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity'));
done();
}
});
});
});
});
+10 -2
View File
@@ -10,6 +10,7 @@ import {
BadRequest, NotAuthorized, NotFound,
} from '../../../../website/common/script/libs/errors';
import i18n from '../../../../website/common/script/i18n';
import errorMessage from '../../../../website/common/script/libs/errorMessage';
function buyGear (user, req, analytics) {
let buyOp = new BuyMarketGearOperation(user, req, analytics);
@@ -63,6 +64,13 @@ describe('shared.ops.buyMarketGear', () => {
eyewear_special_redTopFrame: true,
eyewear_special_whiteTopFrame: true,
eyewear_special_yellowTopFrame: true,
headAccessory_special_blackHeadband: true,
headAccessory_special_blueHeadband: true,
headAccessory_special_greenHeadband: true,
headAccessory_special_pinkHeadband: true,
headAccessory_special_redHeadband: true,
headAccessory_special_whiteHeadband: true,
headAccessory_special_yellowHeadband: true,
});
expect(analytics.track).to.be.calledOnce;
});
@@ -190,7 +198,7 @@ describe('shared.ops.buyMarketGear', () => {
buyGear(user);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('missingKeyParam'));
expect(err.message).to.equal(errorMessage('missingKeyParam'));
done();
}
});
@@ -202,7 +210,7 @@ describe('shared.ops.buyMarketGear', () => {
buyGear(user, {params});
} catch (err) {
expect(err).to.be.an.instanceof(NotFound);
expect(err.message).to.equal(i18n.t('itemNotFound', params));
expect(err.message).to.equal(errorMessage('itemNotFound', params));
done();
}
});
+2 -1
View File
@@ -10,6 +10,7 @@ import {
NotFound,
} from '../../../../website/common/script/libs/errors';
import i18n from '../../../../website/common/script/i18n';
import errorMessage from '../../../../website/common/script/libs/errorMessage';
describe('shared.ops.buyMysterySet', () => {
let user;
@@ -70,7 +71,7 @@ describe('shared.ops.buyMysterySet', () => {
buyMysterySet(user);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('missingKeyParam'));
expect(err.message).to.equal(errorMessage('missingKeyParam'));
done();
}
});
+18 -2
View File
@@ -8,6 +8,7 @@ import {
NotFound,
} from '../../../../website/common/script/libs/errors';
import i18n from '../../../../website/common/script/i18n';
import errorMessage from '../../../../website/common/script/libs/errorMessage';
describe('shared.ops.buyQuest', () => {
let user;
@@ -106,7 +107,7 @@ describe('shared.ops.buyQuest', () => {
});
} catch (err) {
expect(err).to.be.an.instanceof(NotFound);
expect(err.message).to.equal(i18n.t('questNotFound', {key: 'snarfblatter'}));
expect(err.message).to.equal(errorMessage('questNotFound', {key: 'snarfblatter'}));
expect(user.items.quests).to.eql({});
expect(user.stats.gp).to.equal(9999);
done();
@@ -151,7 +152,22 @@ describe('shared.ops.buyQuest', () => {
buyQuest(user);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('missingKeyParam'));
expect(err.message).to.equal(errorMessage('missingKeyParam'));
done();
}
});
it('does not buy a quest without completing previous quests', (done) => {
try {
buyQuest(user, {
params: {
key: 'dilatoryDistress3',
},
});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('mustComplete', {quest: 'dilatoryDistress2'}));
expect(user.items.quests).to.eql({});
done();
}
});
@@ -1,4 +1,4 @@
import buySpecialSpell from '../../../../website/common/script/ops/buy/buySpecialSpell';
import {BuySpellOperation} from '../../../../website/common/script/ops/buy/buySpell';
import {
BadRequest,
NotFound,
@@ -9,11 +9,17 @@ import {
generateUser,
} from '../../../helpers/common.helper';
import content from '../../../../website/common/script/content/index';
import errorMessage from '../../../../website/common/script/libs/errorMessage';
describe('shared.ops.buySpecialSpell', () => {
let user;
let analytics = {track () {}};
function buySpecialSpell (_user, _req, _analytics) {
const buyOp = new BuySpellOperation(_user, _req, _analytics);
return buyOp.purchase();
}
beforeEach(() => {
user = generateUser();
sinon.stub(analytics, 'track');
@@ -28,7 +34,7 @@ describe('shared.ops.buySpecialSpell', () => {
buySpecialSpell(user);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('missingKeyParam'));
expect(err.message).to.equal(errorMessage('missingKeyParam'));
done();
}
});
@@ -42,7 +48,7 @@ describe('shared.ops.buySpecialSpell', () => {
});
} catch (err) {
expect(err).to.be.an.instanceof(NotFound);
expect(err.message).to.equal(i18n.t('spellNotFound', {spellId: 'notExisting'}));
expect(err.message).to.equal(errorMessage('spellNotFound', {spellId: 'notExisting'}));
done();
}
});
+3 -2
View File
@@ -8,6 +8,7 @@ import content from '../../../../website/common/script/content/index';
import {
generateUser,
} from '../../../helpers/common.helper';
import errorMessage from '../../../../website/common/script/libs/errorMessage';
describe('common.ops.hourglassPurchase', () => {
let user;
@@ -28,7 +29,7 @@ describe('common.ops.hourglassPurchase', () => {
hourglassPurchase(user, {params: {}});
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.eql(i18n.t('missingKeyParam'));
expect(err.message).to.eql(errorMessage('missingKeyParam'));
done();
}
});
@@ -38,7 +39,7 @@ describe('common.ops.hourglassPurchase', () => {
hourglassPurchase(user, {params: {key: 'Base'}});
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.eql(i18n.t('missingTypeParam'));
expect(err.message).to.eql(errorMessage('missingTypeParam'));
done();
}
});
-90
View File
@@ -1,6 +1,5 @@
import purchase from '../../../../website/common/script/ops/buy/purchase';
import pinnedGearUtils from '../../../../website/common/script/ops/pinnedGearUtils';
import planGemLimits from '../../../../website/common/script/libs/planGemLimits';
import {
BadRequest,
NotAuthorized,
@@ -17,7 +16,6 @@ describe('shared.ops.purchase', () => {
const SEASONAL_FOOD = 'Meat';
let user;
let goldPoints = 40;
let gemsBought = 40;
let analytics = {track () {}};
before(() => {
@@ -45,63 +43,6 @@ describe('shared.ops.purchase', () => {
}
});
it('returns an error when key is not provided', (done) => {
try {
purchase(user, {params: {type: 'gems'}});
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('keyRequired'));
done();
}
});
it('prevents unsubscribed user from buying gems', (done) => {
try {
purchase(user, {params: {type: 'gems', key: 'gem'}});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('mustSubscribeToPurchaseGems'));
done();
}
});
it('prevents user with not enough gold from buying gems', (done) => {
user.purchased.plan.customerId = 'customer-id';
try {
purchase(user, {params: {type: 'gems', key: 'gem'}});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('messageNotEnoughGold'));
done();
}
});
it('prevents user that have reached the conversion cap from buying gems', (done) => {
user.stats.gp = goldPoints;
user.purchased.plan.gemsBought = gemsBought;
try {
purchase(user, {params: {type: 'gems', key: 'gem'}});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('reachedGoldToGemCap', {convCap: planGemLimits.convCap}));
done();
}
});
it('prevents user from buying an invalid quantity', (done) => {
user.stats.gp = goldPoints;
user.purchased.plan.gemsBought = gemsBought;
try {
purchase(user, {params: {type: 'gems', key: 'gem'}, quantity: 'a'});
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity'));
done();
}
});
it('returns error when unknown type is provided', (done) => {
try {
@@ -185,25 +126,6 @@ describe('shared.ops.purchase', () => {
user.pinnedItems.push({type: 'bundles', key: 'featheredFriends'});
});
it('purchases gems', () => {
let [, message] = purchase(user, {params: {type: 'gems', key: 'gem'}}, analytics);
expect(message).to.equal(i18n.t('plusOneGem'));
expect(user.balance).to.equal(userGemAmount + 0.25);
expect(user.purchased.plan.gemsBought).to.equal(1);
expect(user.stats.gp).to.equal(goldPoints - planGemLimits.convRate);
expect(analytics.track).to.be.calledOnce;
});
it('purchases gems with a different language than the default', () => {
let [, message] = purchase(user, {params: {type: 'gems', key: 'gem'}, language: 'de'});
expect(message).to.equal(i18n.t('plusOneGem', 'de'));
expect(user.balance).to.equal(userGemAmount + 0.5);
expect(user.purchased.plan.gemsBought).to.equal(2);
expect(user.stats.gp).to.equal(goldPoints - planGemLimits.convRate * 2);
});
it('purchases eggs', () => {
let type = 'eggs';
let key = 'Wolf';
@@ -307,18 +229,6 @@ describe('shared.ops.purchase', () => {
}
});
it('makes bulk purchases of gems', () => {
let [, message] = purchase(user, {
params: {type: 'gems', key: 'gem'},
quantity: 2,
});
expect(message).to.equal(i18n.t('plusOneGem'));
expect(user.balance).to.equal(userGemAmount + 0.50);
expect(user.purchased.plan.gemsBought).to.equal(2);
expect(user.stats.gp).to.equal(goldPoints - planGemLimits.convRate * 2);
});
it('makes bulk purchases of eggs', () => {
let type = 'eggs';
let key = 'TigerCub';
+4 -3
View File
@@ -9,6 +9,7 @@ import i18n from '../../../website/common/script/i18n';
import {
generateUser,
} from '../../helpers/common.helper';
import errorMessage from '../../../website/common/script/libs/errorMessage';
describe('shared.ops.feed', () => {
let user;
@@ -23,7 +24,7 @@ describe('shared.ops.feed', () => {
feed(user);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('missingPetFoodFeed'));
expect(err.message).to.equal(errorMessage('missingPetFoodFeed'));
done();
}
});
@@ -33,7 +34,7 @@ describe('shared.ops.feed', () => {
feed(user, {params: {pet: 'invalid', food: 'food'}});
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidPetName'));
expect(err.message).to.equal(errorMessage('invalidPetName'));
done();
}
});
@@ -43,7 +44,7 @@ describe('shared.ops.feed', () => {
feed(user, {params: {pet: 'Wolf-Red', food: 'invalid food name'}});
} catch (err) {
expect(err).to.be.an.instanceof(NotFound);
expect(err.message).to.equal(i18n.t('messageFoodNotFound'));
expect(err.message).to.equal(errorMessage('invalidFoodName'));
done();
}
});
+2 -1
View File
@@ -8,6 +8,7 @@ import i18n from '../../../website/common/script/i18n';
import {
generateUser,
} from '../../helpers/common.helper';
import errorMessage from '../../../website/common/script/libs/errorMessage';
describe('shared.ops.hatch', () => {
let user;
@@ -24,7 +25,7 @@ describe('shared.ops.hatch', () => {
hatch(user);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('missingEggHatchingPotionHatch'));
expect(err.message).to.equal(errorMessage('missingEggHatchingPotion'));
expect(user.items.pets).to.be.empty;
}
});
+1 -1
View File
@@ -36,7 +36,7 @@ describe('shared.ops.sell', () => {
sell(user, {params: { type } });
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('keyRequired'));
expect(err.message).to.equal(i18n.t('missingKeyParam'));
done();
}
});
+2 -1
View File
@@ -7,6 +7,7 @@ import i18n from '../../../../website/common/script/i18n';
import {
generateUser,
} from '../../../helpers/common.helper';
import errorMessage from '../../../../website/common/script/libs/errorMessage';
describe('shared.ops.allocate', () => {
let user;
@@ -22,7 +23,7 @@ describe('shared.ops.allocate', () => {
});
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidAttribute', {attr: 'notValid'}));
expect(err.message).to.equal(errorMessage('invalidAttribute', {attr: 'notValid'}));
done();
}
});
+3 -2
View File
@@ -7,6 +7,7 @@ import i18n from '../../../../website/common/script/i18n';
import {
generateUser,
} from '../../../helpers/common.helper';
import errorMessage from '../../../../website/common/script/libs/errorMessage';
describe('shared.ops.allocateBulk', () => {
let user;
@@ -27,7 +28,7 @@ describe('shared.ops.allocateBulk', () => {
});
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidAttribute', {attr: 'invalid'}));
expect(err.message).to.equal(errorMessage('invalidAttribute', {attr: 'invalid'}));
done();
}
});
@@ -37,7 +38,7 @@ describe('shared.ops.allocateBulk', () => {
allocateBulk(user);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('statsObjectRequired'));
expect(err.message).to.equal(errorMessage('statsObjectRequired'));
done();
}
});
+2 -2
View File
@@ -966,7 +966,7 @@ describe('shouldDo', () => {
m: false,
};
let today = moment('2017-01-27');
let today = moment('2017-01-27:00:00.000-00:00');
let week = today.monthWeek();
let dayOfWeek = today.day();
dailyTask.startDate = today.toDate();
@@ -974,7 +974,7 @@ describe('shouldDo', () => {
dailyTask.repeat[DAY_MAPPING[dayOfWeek]] = true;
dailyTask.everyX = 1;
dailyTask.frequency = 'monthly';
day = moment('2017-02-24');
day = moment('2017-02-24:00:00.000-00:00');
expect(shouldDo(day, dailyTask, options)).to.equal(true);
});
@@ -10,7 +10,7 @@ import * as Tasks from '../../../../website/server/models/task';
// If you need the user to have specific requirements,
// such as a balance > 0, just pass in the adjustment
// to the update object. If you want to adjust a nested
// paramter, such as the number of wolf eggs the user has,
// parameter, such as the number of wolf eggs the user has,
// , you can do so by passing in the full path as a string:
// { 'items.eggs.Wolf': 10 }
export async function generateUser (update = {}) {
+1 -1
View File
@@ -12,7 +12,7 @@ if (process.env.LOAD_SERVER === '0') { // when the server is in a different proc
nconf.set('NODE_DB_URI', nconf.get('TEST_DB_URI'));
nconf.set('NODE_ENV', 'test');
nconf.set('IS_TEST', true);
// We require src/server and npt src/index because
// We require src/server and not src/index because
// 1. nconf is already setup
// 2. we don't need clustering
require('../../website/server/server'); // eslint-disable-line global-require
+5 -4
View File
@@ -1,8 +1,7 @@
#Running
- Open a terminal and type `npm run client:dev`
- Open a second terminal and type `npm start`
# Running
For information about installing Habitica locally, see [Setting up Habitica Locally](http://habitica.wikia.com/wiki/Setting_up_Habitica_Locally) and for information about running the local client, refer to the ["Run Habitica" section](http://habitica.wikia.com/wiki/Setting_up_Habitica_Locally#Run_Habitica) in that page.
#Preparation Reading
# Preparation Reading
- Vue 2 (https://vuejs.org)
- Webpack (https://webpack.github.io/) is the build system and it includes plugins for code transformation, right now we have: BabelJS for ES6 transpilation, eslint for code style, less and postcss for css compilation. The code comes from https://github.com/vuejs-templates/webpack which is a Webpack template for Vue, with some small modifications to adapt it to our use case. Docs http://vuejs-templates.github.io/webpack/
@@ -18,3 +17,5 @@ The API is almost the same except that we dont use mutations but only actions
The project is developed directly in the `develop` branch as long as well be able to avoid splitting it into a different branch.
So far most of the work has been on the template, so theres no complex logic to understand. The only thing I would suggest you to read about is Vuex for data management: its basically a Flux implementation: theres a central store that hold the data for the entire app, and every change to the data must happen through an action, the data cannot be mutated directly.
For further resources, see [Guidance for Blacksmiths](http://habitica.wikia.com/wiki/Guidance_for_Blacksmiths), and in particular the ["Website Technology Stack" section](http://habitica.wikia.com/wiki/Guidance_for_Blacksmiths#Website_Technology_Stack).
+38 -20
View File
@@ -44,8 +44,6 @@ div
router-view
app-footer
audio#sound(autoplay, ref="sound")
source#oggSource(type="audio/ogg", :src="sound.oggSource")
source#mp3Source(type="audio/mp3", :src="sound.mp3Source")
</template>
<style lang='scss' scoped>
@@ -118,7 +116,7 @@ div
/* Push progress bar above modals */
#nprogress .bar {
z-index: 1043 !important; /* Must stay above nav bar */
z-index: 1090 !important; /* Must stay above nav bar */
}
.restingInn {
@@ -127,7 +125,7 @@ div
}
#app-header {
margin-top: 96px !important;
margin-top: 40px !important;
}
}
@@ -220,10 +218,9 @@ export default {
selectedItemToBuy: null,
selectedSpellToBuy: null,
sound: {
oggSource: '',
mp3Source: '',
},
audioSource: null,
audioSuffix: null,
loading: true,
currentTipNumber: 0,
bannerHidden: false,
@@ -259,11 +256,22 @@ export default {
return;
}
let file = `/static/audio/${theme}/${sound}`;
this.sound = {
oggSource: `${file}.ogg`,
mp3Source: `${file}.mp3`,
};
let file = `/static/audio/${theme}/${sound}`;
if (this.audioSuffix === null) {
this.audioSource = document.createElement('source');
if (this.$refs.sound.canPlayType('audio/ogg')) {
this.audioSuffix = '.ogg';
this.audioSource.type = 'audio/ogg';
} else {
this.audioSuffix = '.mp3';
this.audioSource.type = 'audio/mp3';
}
this.audioSource.src = file + this.audioSuffix;
this.$refs.sound.appendChild(this.audioSource);
} else {
this.audioSource.src = file + this.audioSuffix;
}
this.$refs.sound.load();
});
@@ -295,12 +303,6 @@ export default {
if (error.response.status >= 400) {
this.checkForBannedUser(error);
// Check for conditions to reset the user auth
const invalidUserMessage = [this.$t('invalidCredentials'), 'Missing authentication headers.'];
if (invalidUserMessage.indexOf(error.response.data) !== -1) {
this.$store.dispatch('auth:logout');
}
// Don't show errors from getting user details. These users have delete their account,
// but their chat message still exists.
let configExists = Boolean(error.response) && Boolean(error.response.config);
@@ -313,11 +315,27 @@ export default {
const errorData = error.response.data;
const errorMessage = errorData.message || errorData;
// Check for conditions to reset the user auth
const invalidUserMessage = [this.$t('invalidCredentials'), 'Missing authentication headers.'];
if (invalidUserMessage.indexOf(errorMessage) !== -1) {
this.$store.dispatch('auth:logout');
}
// Most server errors should return is click to dismiss errors, with some exceptions
let snackbarTimeout = false;
if (error.response.status === 502) snackbarTimeout = true;
const notificationNotFoundMessage = [
this.$t('messageNotificationNotFound'),
this.$t('messageNotificationNotFound', 'en'),
];
if (notificationNotFoundMessage.indexOf(errorMessage) !== -1) snackbarTimeout = true;
this.$store.dispatch('snackbars:add', {
title: 'Habitica',
text: errorMessage,
type: 'error',
timeout: true,
timeout: snackbarTimeout,
});
}
@@ -1,66 +1,54 @@
.promo_armoire_background_201804 {
.promo_armoire_backgrounds_201805 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -142px -587px;
background-position: -878px 0px;
width: 141px;
height: 441px;
}
.promo_bundle_cuddleBuddies {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -534px;
width: 141px;
height: 441px;
}
.promo_earrings_headbands {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -565px -223px;
width: 297px;
height: 147px;
}
.promo_fairy_potions {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px 0px;
width: 564px;
height: 196px;
}
.promo_ios {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -532px 0px;
background-position: 0px -197px;
width: 325px;
height: 336px;
}
.promo_mystery_201803 {
.promo_mystery_201805 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -695px -337px;
background-position: -878px -442px;
width: 114px;
height: 90px;
}
.promo_rainbow_potions {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -284px -587px;
width: 141px;
height: 441px;
}
.promo_seasonalshop_spring {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -532px -337px;
width: 162px;
height: 138px;
}
.promo_shimmer_pastel {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -426px -735px;
width: 354px;
height: 147px;
}
.promo_shiny_seeds {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -426px -587px;
width: 360px;
height: 147px;
}
.promo_spring_fling_2018 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -587px;
width: 141px;
height: 588px;
}
.promo_take_this {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -532px -476px;
background-position: -716px -371px;
width: 114px;
height: 87px;
}
.scene_positivity {
.scene_casting_spells {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px 0px;
width: 531px;
height: 243px;
background-position: -565px 0px;
width: 312px;
height: 222px;
}
.scene_video_games {
.scene_meditation {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -244px;
width: 339px;
height: 342px;
background-position: -565px -371px;
width: 150px;
height: 150px;
}
@@ -354,7 +354,7 @@
}
.background_aurora {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -284px -444px;
background-position: -568px -444px;
width: 141px;
height: 147px;
}
@@ -366,19 +366,19 @@
}
.background_back_of_giant_beast {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -426px -444px;
background-position: -710px 0px;
width: 141px;
height: 147px;
}
.background_bamboo_forest {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -568px -444px;
background-position: -710px -148px;
width: 141px;
height: 147px;
}
.background_beach {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -710px 0px;
background-position: -710px -296px;
width: 141px;
height: 147px;
}
@@ -396,7 +396,7 @@
}
.background_beside_well {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -710px -148px;
background-position: -710px -444px;
width: 141px;
height: 147px;
}
@@ -426,355 +426,367 @@
}
.background_buried_treasure {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -710px -296px;
background-position: 0px 0px;
width: 141px;
height: 147px;
}
.background_cherry_trees {
.background_champions_colosseum {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1277px 0px;
width: 140px;
height: 147px;
}
.background_cherry_trees {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1277px -148px;
width: 140px;
height: 147px;
}
.background_chessboard_land {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -710px -444px;
width: 141px;
height: 147px;
}
.background_clouds {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1277px -296px;
width: 140px;
height: 147px;
}
.background_coral_reef {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1277px -444px;
width: 140px;
height: 147px;
}
.background_cornfields {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1277px -592px;
width: 140px;
height: 147px;
}
.background_cozy_library {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: 0px 0px;
width: 141px;
height: 147px;
}
.background_crosscountry_ski_trail {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -142px -592px;
width: 141px;
height: 147px;
}
.background_crystal_cave {
.background_clouds {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1277px -1036px;
background-position: -1277px -444px;
width: 140px;
height: 147px;
}
.background_deep_mine {
.background_coral_reef {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: 0px -1184px;
background-position: -1277px -592px;
width: 140px;
height: 147px;
}
.background_deep_sea {
.background_cornfields {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -141px -1184px;
background-position: -1277px -740px;
width: 140px;
height: 147px;
}
.background_desert_dunes {
.background_cozy_library {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -284px -592px;
width: 141px;
height: 147px;
}
.background_dilatory_castle {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -423px -1184px;
width: 140px;
height: 147px;
}
.background_dilatory_ruins {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -564px -1184px;
width: 140px;
height: 147px;
}
.background_distant_castle {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -705px -1184px;
width: 140px;
height: 147px;
}
.background_drifting_raft {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -846px -1184px;
width: 140px;
height: 147px;
}
.background_driving_a_coach {
.background_crosscountry_ski_trail {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -426px -592px;
width: 141px;
height: 147px;
}
.background_driving_a_sleigh {
.background_crystal_cave {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: 0px -1184px;
width: 140px;
height: 147px;
}
.background_deep_mine {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -141px -1184px;
width: 140px;
height: 147px;
}
.background_deep_sea {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -282px -1184px;
width: 140px;
height: 147px;
}
.background_desert_dunes {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -568px -592px;
width: 141px;
height: 147px;
}
.background_dusty_canyons {
.background_dilatory_castle {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1269px -1184px;
background-position: -564px -1184px;
width: 140px;
height: 147px;
}
.background_elegant_balcony {
.background_dilatory_ruins {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -705px -1184px;
width: 140px;
height: 147px;
}
.background_distant_castle {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -846px -1184px;
width: 140px;
height: 147px;
}
.background_drifting_raft {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -987px -1184px;
width: 140px;
height: 147px;
}
.background_driving_a_coach {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -710px -592px;
width: 141px;
height: 147px;
}
.background_fairy_ring {
.background_driving_a_sleigh {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1418px -148px;
background-position: -852px 0px;
width: 141px;
height: 147px;
}
.background_dusty_canyons {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1418px 0px;
width: 140px;
height: 147px;
}
.background_farmhouse {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1418px -1184px;
width: 140px;
height: 147px;
}
.background_fiber_arts_room {
.background_elegant_balcony {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -852px -148px;
width: 141px;
height: 147px;
}
.background_floating_islands {
.background_fairy_ring {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -994px -740px;
width: 140px;
height: 147px;
}
.background_fantastical_shoe_store {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1418px -444px;
width: 140px;
height: 147px;
}
.background_farmhouse {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1418px -592px;
width: 140px;
height: 147px;
}
.background_floral_meadow {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1418px -740px;
width: 140px;
height: 147px;
}
.background_flying_over_a_field_of_wildflowers {
.background_fiber_arts_room {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -852px -296px;
width: 141px;
height: 147px;
}
.customize-option.background_flying_over_a_field_of_wildflowers {
.background_floating_islands {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -877px -311px;
width: 60px;
height: 60px;
background-position: -1418px -888px;
width: 140px;
height: 147px;
}
.background_flying_over_an_ancient_forest {
.background_floral_meadow {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1418px -1036px;
width: 140px;
height: 147px;
}
.background_flying_over_a_field_of_wildflowers {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -852px -444px;
width: 141px;
height: 147px;
}
.background_flying_over_icy_steppes {
.customize-option.background_flying_over_a_field_of_wildflowers {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -877px -459px;
width: 60px;
height: 60px;
}
.background_flying_over_an_ancient_forest {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -852px -592px;
width: 141px;
height: 147px;
}
.background_forest {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: 0px -1332px;
width: 140px;
height: 147px;
}
.background_frigid_peak {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -141px -1332px;
width: 140px;
height: 147px;
}
.background_frozen_lake {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -282px -1332px;
width: 140px;
height: 147px;
}
.background_garden_shed {
.background_flying_over_icy_steppes {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: 0px -740px;
width: 141px;
height: 147px;
}
.background_gazebo {
.background_forest {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -282px -1332px;
width: 140px;
height: 147px;
}
.background_frigid_peak {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -423px -1332px;
width: 140px;
height: 147px;
}
.background_frozen_lake {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -564px -1332px;
width: 140px;
height: 147px;
}
.background_giant_birdhouse {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -705px -1332px;
width: 140px;
height: 147px;
}
.background_giant_florals {
.background_garden_shed {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -142px -740px;
width: 141px;
height: 147px;
}
.background_giant_seashell {
.background_gazebo {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -846px -1332px;
width: 140px;
height: 147px;
}
.background_giant_birdhouse {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -987px -1332px;
width: 140px;
height: 147px;
}
.background_giant_florals {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -284px -740px;
width: 141px;
height: 147px;
}
.background_giant_wave {
.background_giant_seashell {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -426px -740px;
width: 141px;
height: 147px;
}
.background_gorgeous_greenhouse {
.background_giant_wave {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -568px -740px;
width: 141px;
height: 147px;
}
.background_grand_staircase {
.background_gorgeous_greenhouse {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -710px -740px;
width: 141px;
height: 147px;
}
.background_graveyard {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1559px 0px;
width: 140px;
height: 147px;
}
.background_green {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1559px -148px;
width: 140px;
height: 147px;
}
.background_guardian_statues {
.background_grand_staircase {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -852px -740px;
width: 141px;
height: 147px;
}
.background_gumdrop_land {
.background_graveyard {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1559px -296px;
width: 140px;
height: 147px;
}
.background_green {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1559px -444px;
width: 140px;
height: 147px;
}
.background_habit_city_streets {
.background_guardian_statues {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -994px 0px;
width: 141px;
height: 147px;
}
.background_harvest_feast {
.background_gumdrop_land {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1559px -740px;
width: 140px;
height: 147px;
}
.background_harvest_fields {
.background_habit_city_streets {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -994px -148px;
width: 141px;
height: 147px;
}
.background_harvest_moon {
.background_harvest_feast {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1559px -1036px;
width: 140px;
height: 147px;
}
.background_harvest_fields {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -994px -296px;
width: 141px;
height: 147px;
}
.background_haunted_house {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1559px -1184px;
width: 140px;
height: 147px;
}
.background_ice_cave {
.background_harvest_moon {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -994px -444px;
width: 141px;
height: 147px;
}
.background_iceberg {
.background_haunted_house {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: 0px -1480px;
width: 140px;
height: 147px;
}
.background_idyllic_cabin {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -141px -1480px;
width: 140px;
height: 147px;
}
.background_island_waterfalls {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -282px -1480px;
width: 140px;
height: 147px;
}
.background_kelp_forest {
.background_ice_cave {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -994px -592px;
width: 141px;
height: 147px;
}
.background_lighthouse_shore {
.background_iceberg {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -282px -1480px;
width: 140px;
height: 147px;
}
.background_idyllic_cabin {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -423px -1480px;
width: 140px;
height: 147px;
}
.background_island_waterfalls {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -564px -1480px;
width: 140px;
height: 147px;
}
.background_lilypad {
.background_kelp_forest {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -705px -1480px;
width: 140px;
background-position: -142px 0px;
width: 141px;
height: 147px;
}
.background_magic_beanstalk {
.background_lighthouse_shore {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -846px -1480px;
width: 140px;
height: 147px;
}
.background_lilypad {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -987px -1480px;
width: 140px;
height: 147px;
}
.background_magic_beanstalk {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1418px -296px;
width: 140px;
height: 147px;
}
.background_magical_candles {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -994px -740px;
background-position: -426px -444px;
width: 141px;
height: 147px;
}
.background_magical_museum {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -852px 0px;
background-position: -284px -444px;
width: 141px;
height: 147px;
}
@@ -786,13 +798,13 @@
}
.background_market {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -846px -888px;
background-position: -564px -888px;
width: 140px;
height: 147px;
}
.background_meandering_cave {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -705px -888px;
background-position: -423px -888px;
width: 140px;
height: 147px;
}
@@ -804,7 +816,7 @@
}
.background_midnight_clouds {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -423px -888px;
background-position: -141px -888px;
width: 140px;
height: 147px;
}
@@ -816,31 +828,31 @@
}
.background_mist_shrouded_mountain {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -141px -888px;
background-position: -705px -1480px;
width: 140px;
height: 147px;
}
.background_mistiflying_circus {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: 0px -888px;
background-position: -141px -1480px;
width: 140px;
height: 147px;
}
.background_mountain_lake {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -987px -1480px;
background-position: -1559px -1332px;
width: 140px;
height: 147px;
}
.background_mountain_pyramid {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -423px -1480px;
background-position: -1559px -1184px;
width: 140px;
height: 147px;
}
.background_night_dunes {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1559px -1332px;
background-position: -1559px -888px;
width: 140px;
height: 147px;
}
@@ -864,13 +876,13 @@
}
.background_orchard {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1559px -296px;
background-position: -1410px -1332px;
width: 140px;
height: 147px;
}
.background_pagodas {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1410px -1332px;
background-position: -1269px -1332px;
width: 140px;
height: 147px;
}
@@ -882,13 +894,13 @@
}
.background_pumpkin_patch {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1128px -1332px;
background-position: -705px -1332px;
width: 140px;
height: 147px;
}
.background_purple {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -987px -1332px;
background-position: -141px -1332px;
width: 140px;
height: 147px;
}
@@ -912,13 +924,13 @@
}
.background_rainy_city {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1418px -1036px;
background-position: -1418px -148px;
width: 140px;
height: 147px;
}
.background_red {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1418px -888px;
background-position: -1269px -1184px;
width: 140px;
height: 147px;
}
@@ -942,61 +954,61 @@
}
.background_seafarer_ship {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1128px -1184px;
background-position: -1277px -888px;
width: 140px;
height: 147px;
}
.background_shimmering_ice_prism {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -987px -1184px;
background-position: -1277px -296px;
width: 140px;
height: 147px;
}
.background_shimmery_bubbles {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -282px -1184px;
background-position: -1128px -1036px;
width: 140px;
height: 147px;
}
.background_slimy_swamp {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1277px -888px;
background-position: -423px -1036px;
width: 140px;
height: 147px;
}
.background_snowman_army {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1277px -740px;
background-position: 0px -1036px;
width: 140px;
height: 147px;
}
.background_snowy_pines {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1277px -148px;
background-position: -1136px -888px;
width: 140px;
height: 147px;
}
.background_snowy_sunrise {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1128px -1036px;
background-position: -1136px -740px;
width: 140px;
height: 147px;
}
.background_south_pole {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -423px -1036px;
background-position: -1136px -444px;
width: 140px;
height: 147px;
}
.background_sparkling_snowflake {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: 0px -1036px;
background-position: -987px -888px;
width: 140px;
height: 147px;
}
.background_spider_web {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1136px -888px;
background-position: -846px -888px;
width: 140px;
height: 147px;
}
@@ -1008,25 +1020,25 @@
}
.background_spring_rain {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1136px -444px;
background-position: -282px -888px;
width: 140px;
height: 147px;
}
.background_stable {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -987px -888px;
background-position: 0px -888px;
width: 140px;
height: 147px;
}
.background_stained_glass {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -564px -888px;
background-position: -1559px -592px;
width: 140px;
height: 147px;
}
.background_starry_skies {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -282px -888px;
background-position: -1559px -148px;
width: 140px;
height: 147px;
}
@@ -1038,31 +1050,31 @@
}
.background_stoikalm_volcanoes {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1559px -888px;
background-position: -1128px -1332px;
width: 140px;
height: 147px;
}
.background_stone_circle {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1559px -592px;
background-position: 0px -1332px;
width: 140px;
height: 147px;
}
.background_stormy_rooftops {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1269px -1332px;
background-position: -1418px -1184px;
width: 140px;
height: 147px;
}
.background_stormy_ship {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -846px -1332px;
background-position: -1418px -740px;
width: 140px;
height: 147px;
}
.background_strange_sewers {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -423px -1332px;
background-position: -1128px -1184px;
width: 140px;
height: 147px;
}
@@ -1074,37 +1086,25 @@
}
.background_sunken_ship {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1418px -444px;
background-position: -1277px -1036px;
width: 140px;
height: 147px;
}
.background_sunset_meadow {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1418px -296px;
background-position: -705px -888px;
width: 140px;
height: 147px;
}
.background_sunset_oasis {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1418px 0px;
background-position: -1559px 0px;
width: 140px;
height: 147px;
}
.background_sunset_savannah {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1136px -740px;
background-position: -423px -1184px;
width: 140px;
height: 147px;
}
.background_swarming_darkness {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -1559px -1036px;
width: 140px;
height: 147px;
}
.background_tar_pits {
background-image: url('~assets/images/sprites/spritesmith-main-0.png');
background-position: -142px 0px;
width: 141px;
height: 147px;
}
File diff suppressed because it is too large Load Diff

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