Compare commits

...

175 Commits

Author SHA1 Message Date
Sabe Jones
7f38c61c70 3.52.0 2016-10-31 19:02:24 +00:00
Sabe Jones
1c018cedb1 chore(event): sprites and news 2016-10-31 18:45:39 +00:00
Sabe Jones
80892bd6a8 feat(event): JackOLantern ladder (#8174) 2016-10-31 08:14:06 -05:00
Blade Barringer
f5ba636579 chore(i18n): update locales 2016-10-27 22:03:58 -05:00
Sabe Jones
4dd7e49552 3.51.1 2016-10-27 20:44:17 +00:00
Sabe Jones
d2f673ef1e chore(news): Blog Bailey 2016-10-27 19:58:43 +00:00
Sabe Jones
e198dd551a feat(content): strings for BGs/Armoire 2016-11 2016-10-26 20:28:44 +00:00
Travis
0bfc9d9516 fix: allows user to save an alias and checklistCollapsed properties of a challenge task. fixes #7875 (#8170) 2016-10-25 21:47:49 -05:00
Sabe Jones
d4e20ee4aa 3.51.0 2016-10-25 21:56:09 +00:00
Sabe Jones
a751a367fc chore(sprites): compile 2016-10-25 21:55:40 +00:00
Sabe Jones
d323be19c6 Mystery Items 2016/10 (#8169)
* feat(content): mystery items 2016-10

* chore(news): Bailey 2016-10-25
Also ends the Enchanted Armoire A/B test.

* fix(armoire): failing tests from A/B conclusion
2016-10-25 16:16:00 -05:00
AccioBooks
be3f61a94b Remove cookies on clearing browser data (#8135)
* remove cookies

* update cookie removal

* Remove + and add link

* Fix tests

* Add condition

* update strings
2016-10-25 19:53:56 +10:00
Alys
f1bb2db73b fix wrong variable name in Polish questDamage string
The translators have been notified that it needs to be fixed in Transifex before the next migration of strings back to GitHub.
2016-10-23 09:54:18 +10:00
Matteo Pagliazzi
a622344d44 3.50.0 2016-10-22 18:12:09 +02:00
Blade Barringer
e279a3550b chore(travis): start API tests earlier 2016-10-22 08:14:10 -05:00
Blade Barringer
70aab3059c fix(client): bump version of ngInfinitScroll
v1.0.0 could not be found in bower registry
2016-10-22 08:14:09 -05:00
Blade Barringer
c264e37182 chore(travis): pend grunt build task
chore(travis): Move test prep to gulpfile
2016-10-22 08:14:07 -05:00
Blade Barringer
b31bc15493 chore(travis): Add grunt-cli pkg 2016-10-22 08:14:06 -05:00
Blade Barringer
ba19c00617 Setup up non-API tests to not need server and mongo running
chore(travis): Build files before running tests

chore(travis): require server for api tests
2016-10-22 08:14:00 -05:00
Blade Barringer
93aa92de7c chore(travis): Split up build tasks 2016-10-20 22:14:37 -05:00
Blade Barringer
d021680945 chore(travis): Remove grunt and mocha install step 2016-10-20 22:05:21 -05:00
Blade Barringer
f9595af8a5 Re-enable armoire tests 2016-10-20 22:04:24 -05:00
Alyssa Batula
d2756278c3 Only unequip Gen 1 pets/mounts when releasing pets/mounts, fixes #5366 (#8119)
* Only unequip Gen 1 pets/mounts when releasing pets/mounts

* Changed mount declaration to match releasePets

* Check if a pet/mount is a drop type instead of checking for its name in the list of pets

* Changed references to pet and mount to petInfo and mountInfo for consistency with releasePets and releaseMounts

* Test that releasePets, releaseMounts, and releaseBoth do not unequip quest pets

* Fixed test names, and tests verify that a pet/mount is/is not a drop pet/mount on release

* Removed unneeded comments
2016-10-20 22:00:15 -05:00
Blade Barringer
2e2dc179c4 chore: pend armoire test 2016-10-20 19:30:22 -05:00
Alys
acf7b811ab fix wrong variable name in French questTaskDamage string
The translators have been notified that it needs to be fixed in Transifex.
2016-10-21 08:24:36 +10:00
Blade Barringer
d5170251c0 fix: remove unneeded Math.random test 2016-10-20 17:11:28 -05:00
Sabe Jones
c9ba9054e3 chore(npm): shrinkwrap 2016-10-20 03:44:13 +00:00
MathWhiz
d4aac1ee4b Documentation - coupon
closes #8109
2016-10-19 21:31:07 -05:00
Blade Barringer
9615a332a5 fix(client): Allow member hp to be clickable
fixes #8016
closes #8155
2016-10-19 21:01:35 -05:00
Blade Barringer
417455e5ef Merge branch 'snyk-community-snyk-community-patch-1' into develop 2016-10-19 17:42:20 -05:00
Blade Barringer
136502a110 chore: update express 2016-10-19 17:41:58 -05:00
Blade Barringer
425887c1e4 chore(i18n): update locales 2016-10-19 17:40:26 -05:00
Sabe Jones
cfa8a5190f 3.49.0 2016-10-19 19:43:47 +00:00
Sabe Jones
df5be81706 chore(sprites): compile 2016-10-19 19:10:39 +00:00
Sabe Jones
08b3491047 Taskwoods Quest Line (#8156)
* feat(content): Gold Quest 2016-10

* chore(news): Bailey
2016-10-19 14:04:34 -05:00
Snyk Community
e73c3147c1 Fix for the ReDOS vulnerability
habitica is currently affected by the high-severity [ReDOS vulnerability](https://snyk.io/vuln/npm:tough-cookie:20160722). 

Vulnerable module: `tough-cookie`
Introduced through: ` request`

This PR fixes the ReDOS vulnerability by upgrading ` request` to version 2.74.0

Check out the [Snyk test report](https://snyk.io/test/github/HabitRPG/habitica) to review other vulnerabilities that affect this repo. 

[Watch the repo](https://snyk.io/add) to 
* get alerts if newly disclosed vulnerabilities affect this repo in the future. 
* generate pull requests with the fixes you want, or let us do the work: when a newly disclosed vulnerability affects you, we'll submit a fix to you right away. 

Stay secure, 
The Snyk team
2016-10-19 17:50:16 +03:00
Alys
a43254000e change Indulgence Armadillo to Indulgent Armadillo
reference for Habitica admins: https://habitica.slack.com/archives/general/p1476655925000002
2016-10-18 17:39:52 +10:00
Blade Barringer
4e3c984baf chore(i18n): update locales 2016-10-17 17:14:59 -05:00
Sabe Jones
c112e923f1 feat(content): Strings October 2016 2016-10-17 20:32:50 +00:00
Blade Barringer
540353f024 fix(client): Correct broken image on "how it works" page 2016-10-17 07:33:02 -05:00
AccioBooks
2b9b5e369e /static/features TLC (#8021)
* Fix grammatical errors / stylistical changes

* Apps and Extentions

* and

* Sections -> Sectors

* Grammatical / Stylistic Changes

* remove extraneous .row

* add breaks in final marketing para

* revert features.jade

* Move period
2016-10-17 07:32:25 -05:00
Thomas Gamble
cb38475765 delete unread messages when a user leaves a group
closes #7955
closes #7965
2016-10-16 22:01:34 -05:00
Kees Cook
8bb92577b0 quest progress reporting whitespace fixes (#8106)
Notifications of other things (HP, GP, etc) have a regular format of
"+/- NUM THING". For example:

  function gp(val, bonus) {
    _notify(_sign(val) + " " + coins(val - bonus), 'gp');
  }

However, the recent quest collection/damage notifications do not. This
attempts to regularize the reporting by adding in the "missing" space.

Signed-off-by: Kees Cook <kees@outflux.net>
2016-10-16 21:16:42 -05:00
Blade Barringer
fb26cbd26d Merge pull request #8110 from Hus274/7814
Removing links to outdated tutorials
2016-10-16 21:02:29 -05:00
Blade Barringer
a0de5cd8f8 Merge pull request #8139 from bcpletcher/develop
Cleaned up some CSS
2016-10-16 20:59:22 -05:00
Blade Barringer
9fe10b1818 Merge pull request #8143 from dumindux/Issue-8115
changed gemCost to include the amount of gems
2016-10-16 20:58:48 -05:00
Dumindu Karunathilaka
d8dd39422a changed gemCost to include the amount of gems 2016-10-15 18:10:22 +05:30
Benjamin Pletcher
3f9b710773 Cleaned up some CSS 2016-10-13 21:51:55 -04:00
Sabe Jones
8a8bab4be1 chore(npm): update shrinkwrap 2016-10-13 23:30:10 +00:00
Sabe Jones
2a0747ed72 3.48.0 2016-10-13 23:23:34 +00:00
Sabe Jones
a5196e94f6 chore(news): Bailey 2016-10-13 2016-10-13 23:04:32 +00:00
Sabe Jones
009ab26711 Add special spells to Seasonal Shop API (#8138)
* WIP(shops): add spells to Seasonal API

* refactor(shops): remove superfluous if

* feat(shops): handle spell purchasing

* fix(test): proper required fields check
Also corrects a linting error.

* refactor(shops): use constants
2016-10-13 17:53:02 -05:00
Blade Barringer
3fabf3391f chore(docs): Remove uneeded links in data export docs 2016-10-12 22:43:23 -05:00
Blade Barringer
8020990264 chore(i18n): update locales 2016-10-12 20:28:32 -05:00
Blade Barringer
a2cfeafc02 fix(client): ctrl-enter can be used to send chat
fixes #8122
2016-10-12 20:24:48 -05:00
Matteo Pagliazzi
d04a4fb1ed amazon: fix cancelling subscription: use correct path 2016-10-12 19:33:14 +02:00
Matteo Pagliazzi
aeb86db306 3.47.2 2016-10-12 18:45:00 +02:00
Matteo Pagliazzi
49960c0e32 amazon: fix cancelling subscription 2016-10-12 18:44:06 +02:00
Blade Barringer
932cb5cf6a 3.47.1 2016-10-12 08:07:47 -05:00
MathWhiz
74d6e77504 chore(docs): refine dataexport docs
closes #8120
2016-10-12 08:06:54 -05:00
Blade Barringer
8400f1786b Merge pull request #8125 from DrStrangepork/travis-ci
Changed travis-ci URL to https://travis-ci.org/HabitRPG/habitica
2016-10-12 07:37:11 -05:00
Blade Barringer
d7bd5dd9f8 chore(i18n): update locales 2016-10-12 07:36:50 -05:00
Rick Kasten
3288b0de33 Changed travis-ci URL to https://travis-ci.org/HabitRPG/habitica 2016-10-12 04:52:50 -04:00
Phillip Thelen
c025ffbd10 Fix wrong identifier for old android IAP (#8121) 2016-10-12 09:12:58 +02:00
Blade Barringer
afb5b473a3 chore(docs): Add global definitions for param types 2016-10-11 21:35:58 -05:00
Blade Barringer
aeee29f5fa chore(i18n): update locales 2016-10-11 18:04:25 -05:00
Sabe Jones
0cca2a07a2 fix(news): typo 2016-10-11 22:58:10 +00:00
Sabe Jones
55d94c129a 3.47.0 2016-10-11 21:19:41 +00:00
Sabe Jones
358e1aed22 chore(sprites): compile 2016-10-11 21:00:55 +00:00
Sabe Jones
36241f061f chore(news): Bailey 2016-10-11 2016-10-11 20:59:02 +00:00
Matteo Pagliazzi
b6201a3b75 amplitude: only log generic error message 2016-10-11 22:49:54 +02:00
Matteo Pagliazzi
005f74d918 Merge branch 'vIiRuS-iap' into develop 2016-10-11 21:29:52 +02:00
Matteo Pagliazzi
926e188017 fix eslint errors 2016-10-11 21:29:35 +02:00
Matteo Pagliazzi
94da808279 Merge branch 'iap' of https://github.com/vIiRuS/habitrpg into vIiRuS-iap 2016-10-11 21:28:37 +02:00
Phillip Thelen
7568dd52e9 Fix wrong if statements 2016-10-11 20:49:46 +02:00
Phillip Thelen
c6e2b78982 Make requested syntax changes 2016-10-11 20:47:01 +02:00
Matteo Pagliazzi
b6104c3ef3 remove dup dependency 2016-10-11 18:52:50 +02:00
Sabe Jones
56b5c960f0 feat(content): Beetle Pet Quest 2016-10-11 16:40:27 +00:00
Matteo Pagliazzi
528abf77af amazon: directly cancel subscription when already closed by amazon 2016-10-11 15:54:48 +02:00
Blade Barringer
8db6b7c6cb fix(api): Allow x-client to be set in cors middleware (#8117)
* fix(api): Allow x-client to be set in cors middleware

* chore: update cors middlware tests
2016-10-10 17:35:00 -05:00
Sabe Jones
578dee59bd feat(content): pet quest strings 2016-10-10 19:43:24 +00:00
Sabe Jones
d40c923e6e refactor(test): less clunky timestamp conv 2016-10-10 16:02:08 +00:00
Sabe Jones
3c4c64b023 fix(subscriptions): don't reset Gems midmonth 2016-10-10 15:52:33 +00:00
Phillip Thelen
c84d6ba141 fix linter errors 2016-10-10 14:27:51 +02:00
Phillip Thelen
5f3b147d2a refactor IAP handling 2016-10-10 10:07:10 +02:00
Keith Holliday
ff08e8b586 [WIP] Group tasks claim (#8099)
* Added initial group tasks ui

* Changed group compnent directory

* Added group task checklist support

* Added checklist support to ui

* Fixed delete tags route

* Added checklist routes to support new group tasks

* Added assign user tag input

* Added new group members autocomplete directive

* Linked assign ui to api

* Added styles

* Limited tag use

* Fixed line endings

* Updated to new file structure

* Fixed failing task tests

* Updatd with new checklist logic and fixed columns

* Updated add task function

* Added userid check back to tag routes

* Added back routes accidently deleted

* Added locale strings

* Moved common task function to task service

* Removed files from manifest

* Added initial group tasks ui

* Changed group compnent directory

* Added checklist support to ui

* Added assign user tag input

* Added assign user tag input

* Added new group members autocomplete directive

* Added new group members autocomplete directive

* Removed group get tasks until live

* Linked assign ui to api

* Added styles

* Added server code for claiming a task

* ADded group task meta and claim button

* Adjusted styles, added local, and added confirm

* Updated claim with new file structures

* Fixed merge issue

* Removed extra file

* Removed duplicate functions

* Removed extra directive

* Removed dev items
2016-10-09 19:23:34 +02:00
Phillip Thelen
cb2acbfefd add additional IAP price tiers 2016-10-09 15:20:45 +02:00
Travis Husman
b16da35585 chore(cleanup): removing links to outdated tutorials
closes #7814
2016-10-07 17:17:29 -07:00
Sabe Jones
826d7b85d7 Subscriptions Fixes (#8105)
* fix(subscriptions): round up months

* fix(subscriptions): resub improvements
Don't allow negative extraMonths; flatten new Dates to YYYYMMDD

* fix(subscriptions): remove resub Gems exploit
Also standardizes some uses of new Date() to remove potential race condition oddities.

* fix(subscriptions): bump consecutive months...
...even if the user didn't log in then, if subscription has been continuous through that period

* test(subscriptions): cover fix cases
Also refactor: use constant for YYYY-MM format

* refactor(subscriptions): don't stringify moments
2016-10-07 15:08:30 -05:00
Travis
6bcc6a15e2 Hitting enter no longer sends a chat message, instead inserts a new line (#8096)
* changing behavior so hitting enter in a chat box only now inserts a newline instead of submitting the form. closes #8066

* Adding a tooltip message
2016-10-06 21:55:00 -05:00
MathWhiz
b600eceb49 /v3/content documentation
closes #8098
2016-10-06 21:45:37 -05:00
Blade Barringer
b83ef872c9 Merge branch 'JTorr-develop' into develop 2016-10-06 20:54:25 -05:00
Blade Barringer
4ebc2e2175 chore(docs): Adjust invite route docs 2016-10-06 20:54:04 -05:00
Sabe Jones
2f4b8c569a 3.46.2 2016-10-06 23:20:55 +00:00
Sabe Jones
85b5b5a62d chore(event): enable & announce Spooky Sparkles 2016-10-06 22:49:56 +00:00
Julie Torres
e271e57f63 Improve API Docs for Invite to Group, Iss#8087 2016-10-06 14:23:07 -04:00
Blade Barringer
558fb145b5 chore: remove references to debug-scripts 2016-10-04 20:48:36 -05:00
Blade Barringer
fc30456b53 chore: remove unused debug scripts 2016-10-04 20:38:40 -05:00
Sabe Jones
68b2d19b04 3.46.1 2016-10-04 23:32:02 +00:00
Blade Barringer
6d33acccf4 fix(api) Allow revoked chat ussers to post in private guilds 2016-10-04 17:49:19 -05:00
Sabe Jones
acee4bad80 fix(sprites): add new spritesheet 2016-10-04 17:04:09 +00:00
Sabe Jones
30fe5088b8 3.46.0 2016-10-04 15:55:41 +00:00
Sabe Jones
69602f93e9 chore(sprites): compile 2016-10-04 15:54:55 +00:00
Sabe Jones
0109aa4250 feat(content): Armoire and BGs data (#8095) 2016-10-04 09:57:28 -05:00
Blade Barringer
2dc0958678 chore(docs): Define resource not found errors and permissions 2016-10-03 21:35:53 -05:00
Blade Barringer
52f4e5f37d chore(docs): Update webhook documentation 2016-10-03 17:20:11 -05:00
Blade Barringer
c014da297c chore(docs): remove unneeded apiVersion param 2016-10-03 17:11:59 -05:00
Keith Holliday
285041cdee Group tasks ui picked (#7996)
* Added initial group tasks ui

* Changed group compnent directory

* Added group task checklist support

* Added checklist support to ui

* Fixed delete tags route

* Added checklist routes to support new group tasks

* Added assign user tag input

* Added new group members autocomplete directive

* Linked assign ui to api

* Added styles

* Limited tag use

* Fixed line endings

* Updated to new file structure

* Fixed failing task tests

* Updatd with new checklist logic and fixed columns

* Added purchased info to group and prevented non purchased group from seeing new group tasks

* Updated add task function

* Added userid check back to tag routes

* Marked tag tests as pending

* Added comments to pending tests

* Added back routes accidently deleted

* Added locale strings

* Other clarity fixes

* Moved common task function to task service

* Removed files from manifest

* Fixed naming collision and remove logic

* Removed group get tasks until live

* Fixed test to check update task. Removed extra removeTask call. Synced updated checklists. Added purchased to noset

* Fixed delete group task
2016-10-03 22:12:20 +02:00
Sabe Jones
6a82206f81 feat(content): Armoire and BG sprites 2016-10-03 19:23:05 +00:00
Blade Barringer
8b6052a3ca fix(api): Prevent webhooks from having duplicate ids 2016-10-03 08:13:33 -05:00
Alys
04fd907a45 remove incorrect space from an Indonesian locales variable
The mis-formatting of the variable was causing an error when when a user tried to use the "forgot password" feature.

The Linguists have been informed of the need to fix the string in Transifex.
2016-10-03 07:56:30 +10:00
Blade Barringer
70343079f1 Merge branch 'develop' of github.com:HabitRPG/habitrpg into develop 2016-10-02 12:59:09 -05:00
Sabe Jones
df952eece5 chore(news): Take This Bailey 2016-10-02 16:03:50 +00:00
Blade Barringer
e3a619c7ff 3.45.0 2016-10-02 09:53:54 -05:00
Sabe Jones
23f531372b chore(event): Sept-Oct Take This migration 2016-10-02 14:38:16 +00:00
Blade Barringer
97b15006fd chore: adjust webhook migration to sort webhooks properly 2016-10-02 09:31:28 -05:00
Blade Barringer
35b92f13a3 Webhook improvements (#7879)
* refactor: Move translate test utility to helpers directory

* Add kind property to webhooks

* feat: Add options to create webhook route

* refactor: Move webhook ops into single file

* refactor: Create webhook objects for specific webhook behavior

* chore(tests): Add default sleep helper value of 1 second

* feat(api): Add method for groups to send out webhook

* feat(api): Add taskCreated webhook task creation

* feat(api): Send chat webhooks after a chat is sent

* refactor: Move webhook routes to own controller

* lint: Correct linting errors

* fix(api): Correct taskCreated webhook method

* fix(api): Fix webhook logging to only log when there is an error

* fix: Update groupChatRecieved webhook creation

* chore: Add integration tests for webhooks

* fix: Set webhook creation response to 201

* fix: Correct how task scored webhook data is sent

* Revert group chat recieved webhook to only support one group

* Remove quest activity option for webhooks

* feat: Send webhook for each task created

* feat: Allow webhooks without a type to default to taskScored

* feat: Add logic for adding ids to webhook

* feat: optimize webhook url check by shortcircuiting if no url is passed

* refactor: Use full name for webhook variable

* feat: Add missing params to client webhook

* lint: Add missing semicolon

* chore(tests): Fix inccorect webhook tests

* chore: Add migration to update task scored webhooks

* feat: Allow default value of webhook add route to be enabled

* chore: Update webhook documentation

* chore: Remove special handling for v2

* refactor: adjust addComputedStatsToJSONObject to work for webhooks

* refactor: combine taskScored and taskActivity webhooks

* feat(api): Add task activity to task update and delete routes

* chore: Change references to taskScored to taskActivity

* fix: Correct stats object being passed in for transform

* chore: Remove extra line break

* fix: Pass in the language to use for the translations

* refactor(api): Move webhooks from user.preferences.webhooks to user.webhooks

* chore: Update migration to set webhook array

* lint: Correct brace spacing

* chore: convert webhook lib to use user.webhooks

* refactor(api): Consolidate filters

* chore: clarify migration instructions

* fix(test): Correct user creation in user anonymized tests

* chore: add test that webhooks cannot be updated via PUT /user

* refactor: Simplify default webhook id value

* refactor(client): Push newly created webhook instead of doing a sync

* chore(test): Add test file for webhook model

* refactor: Remove webhook validation

* refactor: Remove need for watch on webhooks

* refactor(client): Update webhooks object without syncing

* chore: update webhook documentation

* Fix migrations issues

* chore: remove v2 test helper

* fix(api): Provide webhook type in task scored webhook

* fix(client): Fix webhook deletion appearing to delete all webhooks

* feat(api): add optional label field for webhooks

* feat: provide empty string as default for webhook label

* chore: Update webhook migration

* chore: update webhook migration name
2016-10-02 09:16:22 -05:00
Alys
556a7e5229 add new loading screen tip for The Bulletin Board guild, as discusssed in Aspiring Socialites 2016-10-02 16:23:17 +10:00
Alys
378625b4af clarify and correct instructions for changing login name and profile name 2016-10-02 16:00:22 +10:00
Blade Barringer
ee15e29ba4 3.44.5 2016-09-30 13:01:36 -05:00
Dumindu Buddhika
ed880a665a added balance to analytics (#8086)
* added balance to analytics

* removed if check
2016-09-30 11:52:14 -05:00
Blade Barringer
3c7f71d214 chore(i18n): update locales 2016-09-30 11:42:34 -05:00
Blade Barringer
edac06b0d1 chore(docs): Update group invite docs 2016-09-30 11:27:08 -05:00
Blade Barringer
24562f8d60 refactor: move total invitation errors to group invite validation method 2016-09-30 11:27:08 -05:00
Blade Barringer
97840ed732 chore: add apidoc watch command 2016-09-30 11:27:08 -05:00
Blade Barringer
76499412ed refactor(api): Move invitation validation to group static method 2016-09-30 11:27:07 -05:00
Julie Torres
9b10f348cc Prevent submission of blank invitation, fixes #7807 (#8080) 2016-09-30 11:25:57 -05:00
Blade Barringer
17b0329c43 chore(i18n): update locales 2016-09-30 08:54:34 -05:00
Blade Barringer
cda84a6d68 chore: move randomVal test to correct folder 2016-09-30 08:39:30 -05:00
Blade Barringer
306505ebab fix(api,client): Pass in predictable random to revive randomVal calls
closes #8085
2016-09-30 08:39:30 -05:00
Blade Barringer
2476cdd873 chore: Add test shells for revive 2016-09-30 08:16:04 -05:00
Blade Barringer
8465dd69be chore: Send author's email when sending flag notification to slack 2016-09-30 07:43:15 -05:00
Matteo Pagliazzi
461e7445c2 remove old server_side tests 2016-09-30 12:33:20 +02:00
Matteo Pagliazzi
24df8d8f2f pusher: sync user when reconnecting 2016-09-29 23:30:11 +02:00
Matteo Pagliazzi
2bca92b4d5 3.44.4 2016-09-29 23:22:30 +02:00
Matteo Pagliazzi
c3843cae80 client: fix bug that prevented drop notifications from showing up 2016-09-29 23:19:46 +02:00
Sabe Jones
816e4a2f19 3.44.3 2016-09-29 18:12:31 +00:00
Sabe Jones
d0d4927e59 fix(login): uncomment Google auth 2016-09-29 18:11:47 +00:00
Sabe Jones
023ff5789d 3.44.2 2016-09-29 17:13:16 +00:00
Blade Barringer
cc9be6f4a1 chore(i18n): update locales 2016-09-29 11:53:29 -05:00
Sabe Jones
145bcb6f7c 3.44.1 2016-09-29 16:46:26 +00:00
Sabe Jones
d7db599f88 chore(npm): update shrinkwrap 2016-09-29 16:22:44 +00:00
Sabe Jones
ca935670f7 chore(event): GaymerX armor & news 2016-09-29 16:16:43 +00:00
AccioBooks
c2eb113672 Make Wikia Links Translatable (#7036)
* Wikia Links

* Update contrib.json (Added comma)

* Implement @Alys's Suggestions

* conLearnLink -> conLearnURL

* add link to plans.jade

* Appended "URL" to locale strings with URLs

* Add 's'
2016-09-29 21:57:44 +10:00
Matteo Pagliazzi
257e932bc3 Vue Store (#8071)
* vue: use our own store in place of vuex

* vue store: add getters, watcher and use internal vue instance

* vue store: better state getter and credits to Vuex

* vue store: $watch -> watch

* vuex store: pass store to getters and fix typos

* add comments to store, start writing tests

* fix unit tests and add missing ones

* cleanup components, add less folder, fetch tassks

* use Vuex helpers

* pin vuex version

* move semantic-ui theme to assets/less, keep website/build empty but in git

* import helpers from vuex
2016-09-29 13:32:36 +02:00
Sabe Jones
50e2731811 chore(assets): remove unused promos 2016-09-28 18:47:04 +00:00
Matteo Pagliazzi
d67b9e5688 do not send welcome email if user already exists 2016-09-28 19:23:07 +02:00
Blade Barringer
bfc7b9d3e8 fix(client): allow admins to open flag modal regardless of flag status
fixes #8078
2016-09-28 08:16:06 -05:00
Blade Barringer
eb0e234afa chore(i18n): update locales 2016-09-28 07:44:30 -05:00
Matteo Pagliazzi
177f78cbb0 3.44.0 2016-09-28 12:49:30 +02:00
Phillip Thelen
e3b484b29a Add Google Signin (#7969)
* Start adding google login

* fix local js issue

* implement syntax suggestions

* fix delete social tests

* Add service for authentication alerts

* fix social login tests

* make suggested google sign in changes

* fix accidentally deleted code

* refactor social network sign in

* fix incorrect find

* implement suggested google sign in changes

* fix(tests): Inject fake Auth module for auth controller

* fix(test): prevent social service from causing page reload

* fix loading user info

* Use lodash's implimentation of find for IE compatibility

* chore: increase test coverage around deletion route

* chore: clean up social auth test

* chore: Fix social login tests

* remove profile from login scope

* fix(api): Allow social accounts to deregister as user has auth backup

* temporarily disable google login button
2016-09-28 12:11:10 +02:00
Sabe Jones
941000d737 3.43.2 2016-09-28 03:12:35 +00:00
Sabe Jones
63ce7c6034 chore(news): misc Bailey 2016-09-28 02:40:24 +00:00
Sabe Jones
921f9a65a3 feat(content): BG and Armoire strings 201610 2016-09-27 21:15:54 +00:00
Sabe Jones
d6bf30eff8 3.43.1 2016-09-27 14:43:34 +00:00
Matteo Pagliazzi
faed0dff20 pusher: remove 1 tab limit, better disconnection 2016-09-27 15:25:07 +02:00
Matteo Pagliazzi
7bb2f4a3fa fix semantic-ui site path 2016-09-27 10:09:46 +02:00
Matteo Pagliazzi
e3bcea4077 Add semantic-ui (#8076)
* client: add semantic-ui

* add README, disable google fonts

* karma: limit coverage to .vue and .js files

* add missing deps

* semantic-ui in assets folder
2016-09-27 09:34:45 +02:00
Sabe Jones
51ffe2c8c2 A/B Testing, Round 2 (#8077)
* feat(analytics): A/B test 2016-09-26

* feat(tutorial): A/B variant text
2016-09-26 17:10:43 -05:00
Blade Barringer
efc0469bef fix(docs): correct email route url in api docs 2016-09-26 14:56:53 -05:00
Sabe Jones
bda0617a23 chore(npm): update shrinkwrap 2016-09-26 17:32:33 +00:00
Blade Barringer
913cb16638 refactor: move randomVal to a lib 2016-09-26 11:55:07 -05:00
Blade Barringer
331993c1df refactor: remove seeding from randomVal 2016-09-26 11:55:07 -05:00
Blade Barringer
136e2de125 refactor: adjust randomDrop to use wrapped random function 2016-09-26 11:55:07 -05:00
Blade Barringer
966a50431f refactor: Move user argument to options in randomVal function 2016-09-26 11:55:07 -05:00
Blade Barringer
4df1601718 fix(api): Armoire actually works randomly 2016-09-26 11:55:07 -05:00
Thomas Gamble
4d5b6992be drops are randomly selected, not based on user values fixes #7929 2016-09-26 11:55:07 -05:00
Tom Gamble
b54441a637 Shows quest progress notification when completing task #7922 (#7951) 2016-09-23 20:41:31 -05:00
Matteo Pagliazzi
bccdf4e989 client: load user based on localStorage (only for testing) (#8055) 2016-09-23 22:39:06 +02:00
Matteo Pagliazzi
633da7ff73 Delete App.vue 2016-09-23 22:18:13 +02:00
Matteo Pagliazzi
d3371e323e Vuex Structure (#8054)
* wip: vuex structure

* add missing files

* client: do not fail dev build on eslint error

* eslint does not block compilation, mount app when data is ready

* eslintrc.js -> eslintrc
2016-09-23 19:49:11 +02:00
Sabe Jones
5480157977 fix(shops): 2h weapon pricing 2016-09-23 15:43:45 +00:00
Blade Barringer
c5888e3d21 fix(client): Allow backgrounds to be changed 2016-09-23 07:04:11 -05:00
Blade Barringer
2ca185474f fix(client): Advanced options not using preference when creating task
closes #8033
2016-09-22 21:41:42 -05:00
Kaitlin Hipkin
5f0c1687b5 Remove unused v2 code from /website/common/script (#8034)
* remove apiv2 behavior from ops

* remove apiv2 behavior from fns
2016-09-22 21:23:46 -05:00
896 changed files with 28396 additions and 22666 deletions

View File

@@ -10,7 +10,6 @@ dist-client/
# Not linted
migrations/*
website/client-old/
debug-scripts/*
scripts/*
test/server_side/**/*
test/client-old/spec/**/*
@@ -23,4 +22,6 @@ Gruntfile.js
gulpfile.js
gulp
webpack
test/client
test/client/e2e
test/client/unit/index.js
test/client/unit/karma.conf.js

1
.gitignore vendored
View File

@@ -14,7 +14,6 @@ npm-debug.log*
lib
website/client-old/bower_components
website/client-old/new-stuff.html
website/build
newrelic_agent.log
.bower-tmp
.bower-registry

View File

@@ -2,16 +2,21 @@ language: node_js
node_js:
- '4.3.1'
before_install:
- "npm install -g npm@3"
- "npm install -g gulp"
- "sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10"
- "echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list"
- "sudo apt-get update"
- "sudo apt-get install mongodb-org-server"
- npm install -g npm@3
- if [ $REQUIRES_SERVER ]; then sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10; echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list; sudo apt-get update; sudo apt-get install mongodb-org-server; fi
before_script:
- 'npm install -g grunt-cli mocha'
- npm run test:build
- cp config.json.example config.json
- "until nc -z localhost 27017; do echo Waiting for MongoDB; sleep 1; done"
- "export DISPLAY=:99"
- if [ $REQUIRES_SERVER ]; then until nc -z localhost 27017; do echo Waiting for MongoDB; sleep 1; done; export DISPLAY=:99; fi
after_script:
- "./node_modules/.bin/lcov-result-merger 'coverage/**/*.info' | ./node_modules/coveralls/bin/coveralls.js"
- ./node_modules/.bin/lcov-result-merger 'coverage/**/*.info' | ./node_modules/coveralls/bin/coveralls.js
script: npm run $TEST
env:
matrix:
- TEST="lint"
- TEST="test:api-v3" REQUIRES_SERVER=true
- TEST="test:sanity"
- TEST="test:content"
- TEST="test:common"
- TEST="test:karma"
- TEST="client:unit"

View File

@@ -126,15 +126,7 @@ module.exports = function(grunt) {
// Register tasks.
grunt.registerTask('build:prod', ['loadManifestFiles', 'clean:build', 'uglify', 'stylus', 'cssmin', 'copy:build', 'hashres']);
grunt.registerTask('build:dev', ['cssmin', 'stylus']);
grunt.registerTask('build:test', ['test:prepare:translations', 'build:dev']);
grunt.registerTask('test:prepare:translations', function() {
var i18n = require('./website/server/libs/i18n'),
fs = require('fs');
fs.writeFileSync('test/client-old/spec/mocks/translations.js',
"if(!window.env) window.env = {};\n" +
"window.env.translations = " + JSON.stringify(i18n.translations['en']) + ';');
});
grunt.registerTask('build:test', ['build:dev']);
// Load tasks
grunt.loadNpmTasks('grunt-contrib-uglify');

View File

@@ -1,4 +1,4 @@
Habitica [![Build Status](https://travis-ci.org/HabitRPG/habitrpg.svg?branch=develop)](https://travis-ci.org/HabitRPG/habitrpg) [![Code Climate](https://codeclimate.com/github/HabitRPG/habitrpg.svg)](https://codeclimate.com/github/HabitRPG/habitrpg) [![Coverage Status](https://coveralls.io/repos/HabitRPG/habitrpg/badge.svg?branch=develop)](https://coveralls.io/r/HabitRPG/habitrpg?branch=develop) [![Bountysource](https://api.bountysource.com/badge/tracker?tracker_id=68393)](https://www.bountysource.com/trackers/68393-habitrpg?utm_source=68393&utm_medium=shield&utm_campaign=TRACKER_BADGE)
Habitica [![Build Status](https://travis-ci.org/HabitRPG/habitica.svg?branch=develop)](https://travis-ci.org/HabitRPG/habitica) [![Code Climate](https://codeclimate.com/github/HabitRPG/habitrpg.svg)](https://codeclimate.com/github/HabitRPG/habitrpg) [![Coverage Status](https://coveralls.io/repos/HabitRPG/habitrpg/badge.svg?branch=develop)](https://coveralls.io/r/HabitRPG/habitrpg?branch=develop) [![Bountysource](https://api.bountysource.com/badge/tracker?tracker_id=68393)](https://www.bountysource.com/trackers/68393-habitrpg?utm_source=68393&utm_medium=shield&utm_campaign=TRACKER_BADGE)
===============
[Habitica](https://habitica.com) is an open source habit building program which treats your life like a Role Playing Game. Level up as you succeed, lose HP as you fail, earn money to buy weapons and armor.
@@ -10,21 +10,3 @@ For an introduction to the technologies used and how the software is organized,
To set up a local install of Habitica for development and testing, see [Setting up Habitica Locally](http://habitica.wikia.com/wiki/Setting_up_Habitica_Locally), which contains instructions for Windows, *nix / Mac OS, and Vagrant.
Then read [Guidance for Blacksmiths](http://habitica.wikia.com/wiki/Guidance_for_Blacksmiths) for additional instructions and useful tips.
## Debug Scripts
In the `./debug-scripts/` folder, there are a few files. Here's a sample:
```bash
grant-all-equipment.js
grant-all-mounts.js
grant-all-pets.js
```
You can run them by doing:
```bash
node debug-scripts/name-of-script.js
```
If there are more arguments required to make the script work, it will print out the usage and an explanation of what the script does.

View File

@@ -36,14 +36,15 @@
"jquery-ui": "1.10.3",
"jquery.cookie": "1.4.0",
"js-emoji": "snicker/js-emoji#f25d8a303f",
"ngInfiniteScroll": "1.0.0",
"ngInfiniteScroll": "1.1.0",
"pnotify": "1.3.1",
"sticky": "1.0.3",
"swagger-ui": "wordnik/swagger-ui#v2.0.24",
"smart-app-banner": "78ef9c0679723b25be1a0ae04f7b4aef7cbced4f",
"habitica-markdown": "1.2.2",
"pusher-js-auth": "^2.0.0",
"pusher-websocket-iso": "pusher#^3.1.0"
"pusher-websocket-iso": "pusher#^3.2.0",
"taggle": "^1.11.1"
},
"devDependencies": {
"angular-mocks": "1.3.9"

View File

@@ -7,6 +7,8 @@
"FACEBOOK_ANALYTICS":"1234567890123456",
"FACEBOOK_KEY":"123456789012345",
"FACEBOOK_SECRET":"aaaabbbbccccddddeeeeffff00001111",
"GOOGLE_CLIENT_ID":"123456789012345",
"GOOGLE_CLIENT_SECRET":"aaaabbbbccccddddeeeeffff00001111",
"NODE_DB_URI":"mongodb://localhost/habitrpg",
"TEST_DB_URI":"mongodb://localhost/habitrpg_test",
"NODE_ENV":"development",

View File

@@ -1,19 +0,0 @@
import { MongoClient as mongo } from 'mongodb';
import config from '../config';
module.exports.updateUser = (_id, path, value) => {
mongo.connect(config.NODE_DB_URI, (err, db) => {
if (err) throw err;
let collection = db.collection('users');
collection.updateOne(
{ _id },
{ $set: { [`${path}`]: value } },
(updateErr, result) => {
if (updateErr) throw updateErr;
console.log('done updating', _id);
db.close();
}
);
});
}

View File

@@ -1,24 +0,0 @@
'use strict';
require('babel-register');
let _ = require('lodash');
let updateUser = require('./_helper').updateUser;
let userId = process.argv[2];
if (!userId) {
console.error('USAGE: node debug-scripts/grant-all-equipment.js <user_id>');
console.error('EFFECT: Adds all gear to specified user');
return;
}
let gearFlat = require('../common').content.gear.flat;
let userGear = {};
_.each(gearFlat, (piece, key) => {
userGear[key] = true;
});
updateUser(userId, 'items.gear.owned', userGear);

View File

@@ -1,28 +0,0 @@
'use strict';
require('babel-register');
let _ = require('lodash');
let updateUser = require('./_helper').updateUser;
let userId = process.argv[2];
if (!userId) {
console.error('USAGE: node debug-scripts/grant-all-mounts.js <user_id>');
console.error('EFFECT: Adds all mounts to specified user');
return;
}
let dropMounts = require('../common').content.mounts;
let questMounts = require('../common').content.questMounts;
let specialMounts = require('../common').content.specialMounts;
let premiumMounts = require('../common').content.premiumPets; // premium mounts isn't exposed on the content object
let userMounts = {};
_.each([ dropMounts, questMounts, specialMounts, premiumMounts ], (set) => {
_.each(set, (pet, key) => {
userMounts[key] = true;
});
})
updateUser(userId, 'items.mounts', userMounts);

View File

@@ -1,28 +0,0 @@
'use strict';
require('babel-register');
let _ = require('lodash');
let updateUser = require('./_helper').updateUser;
let userId = process.argv[2];
if (!userId) {
console.error('USAGE: node debug-scripts/grant-all-pets.js <user_id>');
console.error('EFFECT: Adds all pets to specified user');
return;
}
let dropPets = require('../common').content.pets;
let questPets = require('../common').content.questPets;
let specialPets = require('../common').content.specialPets;
let premiumPets = require('../common').content.premiumPets;
let userPets = {};
_.each([ dropPets, questPets, specialPets, premiumPets ], (set) => {
_.each(set, (pet, key) => {
userPets[key] = 95;
});
})
updateUser(userId, 'items.pets', userPets);

View File

@@ -20,3 +20,7 @@ gulp.task('apidoc', ['apidoc:clean'], (done) => {
done();
}
});
gulp.task('apidoc:watch', ['apidoc'], () => {
return gulp.watch(APIDOC_SRC_PATH + '/**/*.js', ['apidoc']);
});

View File

@@ -25,7 +25,7 @@ gulp.task('build:common', () => {
gulp.task('build:server', ['build:src', 'build:common']);
gulp.task('build:dev', ['browserify', 'prepare:staticNewStuff'], (done) => {
gulp.task('build:dev', ['browserify', 'prepare:staticNewStuff', 'semantic-ui'], (done) => {
gulp.start('grunt-build:dev', done);
});
@@ -33,7 +33,7 @@ gulp.task('build:dev:watch', ['build:dev'], () => {
gulp.watch(['website/client-old/**/*.styl', 'website/common/script/*']);
});
gulp.task('build:prod', ['browserify', 'build:server', 'prepare:staticNewStuff'], (done) => {
gulp.task('build:prod', ['browserify', 'build:server', 'prepare:staticNewStuff', 'semantic-ui'], (done) => {
runSequence(
'grunt-build:prod',
'apidoc',

43
gulp/gulp-semanticui.js Normal file
View File

@@ -0,0 +1,43 @@
import gulp from 'gulp';
import fs from 'fs';
// Make semantic-ui-less work with a theme in a different folder
// Code taken from https://www.artembutusov.com/webpack-semantic-ui/
// Relative to node_modules/semantic-ui-less
const SEMANTIC_THEME_PATH = '../../website/client/assets/less/semantic-ui/theme.config';
// fix well known bug with default distribution
function fixFontPath (filename) {
return new Promise((resolve, reject) => {
fs.readFile(filename, 'utf8', (err, content) => {
if (err) return reject(err);
let newContent = content.replace(
'@fontPath : \'../../themes/',
'@fontPath : \'../../../themes/'
);
fs.writeFile(filename, newContent, 'utf8', (err1) => {
if (err) return reject(err1);
resolve();
});
});
});
}
gulp.task('semantic-ui', (done) => {
// relocate default config
fs.writeFile(
'node_modules/semantic-ui-less/theme.config',
`@import '${SEMANTIC_THEME_PATH}';\n`,
'utf8',
(err) => {
if (err) return done(err);
fixFontPath('node_modules/semantic-ui-less/themes/default/globals/site.variables')
.then(() => done())
.catch(done);
}
);
});

View File

@@ -13,6 +13,9 @@ import Bluebird from 'bluebird';
import runSequence from 'run-sequence';
import os from 'os';
import nconf from 'nconf';
import fs from 'fs';
const i18n = require('../website/server/libs/i18n');
// TODO rewrite
@@ -72,10 +75,17 @@ gulp.task('test:prepare:server', ['test:prepare:mongo'], () => {
}
});
gulp.task('test:prepare:build', ['build'], (cb) => {
exec(testBin('grunt build:test'), cb);
gulp.task('test:prepare:translations', (cb) => {
fs.writeFile(
'test/client-old/spec/mocks/translations.js',
`if(!window.env) window.env = {};
window.env.translations = ${JSON.stringify(i18n.translations['en'])};`, cb);
});
gulp.task('test:prepare:build', ['build', 'test:prepare:translations']);
// exec(testBin('grunt build:test'), cb);
gulp.task('test:prepare:webdriver', (cb) => {
exec('npm run test:prepare:webdriver', cb);
});
@@ -175,32 +185,6 @@ gulp.task('test:content:safe', ['test:prepare:build'], (cb) => {
pipe(runner);
});
gulp.task('test:server_side', ['test:prepare:build'], (cb) => {
let runner = exec(
testBin(SERVER_SIDE_TEST_COMMAND),
(err, stdout, stderr) => {
cb(err);
}
);
pipe(runner);
});
gulp.task('test:server_side:safe', ['test:prepare:build'], (cb) => {
let runner = exec(
testBin(SERVER_SIDE_TEST_COMMAND),
(err, stdout, stderr) => {
testResults.push({
suite: 'Server Side Specs',
pass: testCount(stdout, /(\d+) passing/),
fail: testCount(stdout, /(\d+) failing/),
pend: testCount(stdout, /(\d+) pending/),
});
cb();
}
);
pipe(runner);
});
gulp.task('test:karma', ['test:prepare:build'], (cb) => {
let runner = exec(
testBin(KARMA_TEST_COMMAND),
@@ -296,7 +280,7 @@ gulp.task('test:e2e:safe', ['test:prepare', 'test:prepare:server'], (cb) => {
gulp.task('test:api-v3:unit', (done) => {
let runner = exec(
testBin('mocha test/api/v3/unit --recursive'),
testBin('mocha test/api/v3/unit --recursive --require ./test/helpers/start-server'),
(err, stdout, stderr) => {
if (err) {
process.exit(1);
@@ -314,7 +298,7 @@ gulp.task('test:api-v3:unit:watch', () => {
gulp.task('test:api-v3:integration', (done) => {
let runner = exec(
testBin('mocha test/api/v3/integration --recursive'),
testBin('mocha test/api/v3/integration --recursive --require ./test/helpers/start-server'),
{maxBuffer: 500 * 1024},
(err, stdout, stderr) => {
if (err) {

View File

@@ -9,6 +9,7 @@
require('babel-register');
if (process.env.NODE_ENV === 'production') {
require('./gulp/gulp-semanticui');
require('./gulp/gulp-apidoc');
require('./gulp/gulp-newstuff');
require('./gulp/gulp-build');

View File

@@ -0,0 +1,116 @@
'use strict';
/****************************************
* Author: Blade Barringer @crookedneighbor
*
* Reason: Webhooks have been moved from
* being an object on preferences.webhooks
* to being an array on webhooks. In addition
* they support a type and options and label
* ***************************************/
global.Promise = require('bluebird');
const TaskQueue = require('cwait').TaskQueue;
const logger = require('./utils/logger');
const Timer = require('./utils/timer');
const connectToDb = require('./utils/connect').connectToDb;
const closeDb = require('./utils/connect').closeDb;
const validator = require('validator');
const timer = new Timer();
const MIGRATION_NAME = '20161002_add_missing_webhook_type.js';
// const DB_URI = 'mongodb://username:password@dsXXXXXX-a0.mlab.com:XXXXX,dsXXXXXX-a1.mlab.com:XXXXX/habitica?replicaSet=rs-dsXXXXXX';
const DB_URI = 'mongodb://localhost/prod-copy-1';
const LOGGEDIN_DATE_RANGE = {
$gte: new Date("2016-09-30T00:00:00.000Z"),
// $lte: new Date("2016-09-25T00:00:00.000Z"),
};
let Users;
connectToDb(DB_URI).then((db) => {
Users = db.collection('users');
})
.then(findUsersWithWebhooks)
.then(correctWebhooks)
.then(() => {
timer.stop();
closeDb();
}).catch(reportError);
function reportError (err) {
logger.error('Uh oh, an error occurred');
logger.error(err);
closeDb();
timer.stop();
}
// Cached ids of users that need updating
const USER_IDS = require('../../ids_of_webhooks_to_update.json');
function findUsersWithWebhooks () {
logger.warn('Fetching users with webhooks...');
return Users.find({'_id': {$in: USER_IDS}}, ['preferences.webhooks']).toArray().then((docs) => {
// return Users.find({'preferences.webhooks': {$ne: {} }}, ['preferences.webhooks']).toArray().then((docs) => {
// TODO: Run this after the initial migration to catch any webhooks that may have been aded since the prod backup download
// return Users.find({'preferences.webhooks': {$ne: {} }, 'auth.timestamps.loggedin': LOGGEDIN_DATE_RANGE}, ['preferences.webhooks']).toArray().then((docs) => {
let updates = docs.map((user) => {
let oldWebhooks = user.preferences.webhooks;
let webhooks = Object.keys(oldWebhooks).map((id) => {
let webhook = oldWebhooks[id]
webhook.type = 'taskActivity';
webhook.label = '';
webhook.options = {
created: false,
updated: false,
deleted: false,
scored: true,
};
return webhook;
}).sort((a, b) => {
return a.sort - b.sort;
});
return {
webhooks,
id: user._id,
}
});
return Promise.resolve(updates);
});
}
function updateUserById (user) {
let userId = user.id;
let webhooks = user.webhooks;
return Users.findOneAndUpdate({
_id: userId},
{$set: {webhooks: webhooks, migration: MIGRATION_NAME}
}, {returnOriginal: false})
}
function correctWebhooks (users) {
let queue = new TaskQueue(Promise, 300);
logger.warn('About to update', users.length, 'users...');
return Promise.map(users, queue.wrap(updateUserById)).then((result) => {
let updates = result.filter(res => res.lastErrorObject && res.lastErrorObject.updatedExisting)
let failures = result.filter(res => !(res.lastErrorObject && res.lastErrorObject.updatedExisting));
logger.warn(updates.length, 'users have been fixed');
if (failures.length > 0) {
logger.error(failures.length, 'users could not be found');
}
return Promise.resolve();
});
}

View File

@@ -0,0 +1,73 @@
var migrationName = '20161002_takeThis.js';
var authorName = 'Sabe'; // in case script author needs to know when their ...
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
/*
* Award Take This ladder items to participants in this month's challenge
*/
var mongo = require('mongoskin');
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
var dbUsers = mongo.db(connectionString).collection('users');
// specify a query to limit the affected users (empty for all users):
var query = {
'migration':{$ne:migrationName},
'challenges':{$in:['4bbf63b5-10bc-49f9-8e95-5bd2ac99cd1c']}
};
// specify fields we are interested in to limit retrieved data (empty if we're not reading data):
var fields = {
'items.gear.owned': 1
};
console.warn('Updating users...');
var progressCount = 1000;
var count = 0;
dbUsers.findEach(query, fields, {batchSize:250}, function(err, user) {
if (err) { return exiting(1, 'ERROR! ' + err); }
if (!user) {
console.warn('All appropriate users found and modified.');
setTimeout(displayData, 300000);
return;
}
count++;
// specify user data to change:
var set = {};
if (typeof user.items.gear.owned.armor_special_takeThis !== 'undefined') {
set = {'migration':migrationName, 'items.gear.owned.head_special_takeThis':false};
} else if (typeof user.items.gear.owned.weapon_special_takeThis !== 'undefined') {
set = {'migration':migrationName, 'items.gear.owned.armor_special_takeThis':false};
} else if (typeof user.items.gear.owned.shield_special_takeThis !== 'undefined') {
set = {'migration':migrationName, 'items.gear.owned.weapon_special_takeThis':false};
} else {
set = {'migration':migrationName, 'items.gear.owned.shield_special_takeThis':false};
}
dbUsers.update({_id:user._id}, {$set:set});
if (count%progressCount == 0) console.warn(count + ' ' + user._id);
if (user._id == authorUuid) console.warn(authorName + ' processed');
});
function displayData() {
console.warn('\n' + count + ' users processed\n');
return exiting(0);
}
function exiting(code, msg) {
code = code || 0; // 0 = success
if (code && !msg) { msg = 'ERROR!'; }
if (msg) {
if (code) { console.error(msg); }
else { console.log( msg); }
}
process.exit(code);
}

View File

@@ -0,0 +1,86 @@
var migrationName = '20161030-jackolanterns.js';
var authorName = 'Sabe'; // in case script author needs to know when their ...
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
/*
* set the newStuff flag in all user accounts so they see a Bailey message
*/
var mongo = require('mongoskin');
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
var dbUsers = mongo.db(connectionString).collection('users');
// specify a query to limit the affected users (empty for all users):
var query = {
'auth.timestamps.loggedin':{$gt:new Date('2016-10-01')} // remove when running migration a second time
};
// specify fields we are interested in to limit retrieved data (empty if we're not reading data):
var fields = {
'migration': 1,
'items.pets.JackOLantern-Base': 1,
'items.mounts.JackOLantern-Base': 1,
};
console.warn('Updating users...');
var progressCount = 1000;
var count = 0;
dbUsers.findEach(query, fields, {batchSize:250}, function(err, user) {
if (err) { return exiting(1, 'ERROR! ' + err); }
if (!user) {
console.warn('All appropriate users found and modified.');
setTimeout(displayData, 300000);
return;
}
count++;
// specify user data to change:
var set = {};
var inc = {};
if (user.migration !== migrationName) {
if (user.items.mounts['JackOLantern-Base']) {
set = {'migration':migrationName, 'items.pets.JackOLantern-Ghost':5};
} else if (user.items.pets['JackOLantern-Base']) {
set = {'migration':migrationName, 'items.mounts.JackOLantern-Base':true};
} else {
set = {'migration':migrationName, 'items.pets.JackOLantern-Base':5};
}
inc = {
'items.food.Candy_Base': 1,
'items.food.Candy_CottonCandyBlue': 1,
'items.food.Candy_CottonCandyPink': 1,
'items.food.Candy_Desert': 1,
'items.food.Candy_Golden': 1,
'items.food.Candy_Red': 1,
'items.food.Candy_Shade': 1,
'items.food.Candy_Skeleton': 1,
'items.food.Candy_White': 1,
'items.food.Candy_Zombie': 1,
}
}
dbUsers.update({_id:user._id}, {$set:set, $inc:inc});
if (count%progressCount == 0) console.warn(count + ' ' + user._id);
if (user._id == authorUuid) console.warn(authorName + ' processed');
});
function displayData() {
console.warn('\n' + count + ' users processed\n');
return exiting(0);
}
function exiting(code, msg) {
code = code || 0; // 0 = success
if (code && !msg) { msg = 'ERROR!'; }
if (msg) {
if (code) { console.error(msg); }
else { console.log( msg); }
}
process.exit(code);
}

View File

@@ -2,7 +2,7 @@ var _id = '';
var update = {
$addToSet: {
'purchased.plan.mysteryItems':{
$each:['head_mystery_201609','armor_mystery_201609']
$each:['head_mystery_201610','armor_mystery_201610']
}
}
};

View File

@@ -25,7 +25,7 @@ function connectToDb (dbUri) {
function closeDb () {
if (db) db.close();
logger.success('CLosed connection to the database');
logger.success('Closed connection to the database');
return Promise.resolve();
}

1046
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "3.43.0",
"version": "3.52.0",
"main": "./website/server/index.js",
"dependencies": {
"@slack/client": "3.6.0",
@@ -13,9 +13,10 @@
"async": "^1.5.0",
"autoprefixer": "^6.4.0",
"aws-sdk": "^2.0.25",
"babel-loader": "^6.0.0",
"babel-core": "^6.0.0",
"babel-loader": "^6.0.0",
"babel-plugin-transform-async-to-module-method": "^6.8.0",
"babel-plugin-transform-object-rest-spread": "^6.16.0",
"babel-polyfill": "^6.6.1",
"babel-preset-es2015": "^6.6.0",
"babel-register": "^6.6.0",
@@ -34,7 +35,7 @@
"cwait": "^1.0.0",
"domain-middleware": "~0.1.0",
"estraverse": "^4.1.1",
"express": "~4.13.3",
"express": "~4.14.0",
"express-csv": "~0.6.0",
"express-validator": "^2.18.0",
"extract-text-webpack-plugin": "^1.0.1",
@@ -65,8 +66,11 @@
"jade": "~1.11.0",
"js2xmlparser": "~1.0.0",
"json-loader": "^0.5.4",
"less": "^2.7.1",
"less-loader": "^2.2.3",
"lodash": "^3.10.1",
"lodash.setwith": "^4.2.0",
"lodash.pickby": "^4.2.0",
"merge-stream": "^1.0.0",
"method-override": "^2.3.5",
"moment": "^2.13.0",
@@ -83,6 +87,7 @@
"pageres": "^4.1.1",
"passport": "~0.2.1",
"passport-facebook": "2.0.0",
"passport-google-oauth20": "1.0.0",
"paypal-ipn": "3.0.0",
"paypal-rest-sdk": "^1.2.1",
"pretty-data": "^0.40.0",
@@ -90,10 +95,11 @@
"pug": "^2.0.0-beta6",
"push-notify": "habitrpg/push-notify#v1.2.0",
"pusher": "^1.3.0",
"request": "~2.72.0",
"request": "~2.74.0",
"rimraf": "^2.4.3",
"run-sequence": "^1.1.4",
"s3-upload-stream": "^1.0.6",
"semantic-ui-less": "~2.2.4",
"serve-favicon": "^2.3.0",
"shelljs": "^0.6.0",
"stripe": "^4.2.0",
@@ -108,9 +114,8 @@
"vue": "^2.0.0-rc.6",
"vue-hot-reload-api": "^1.2.0",
"vue-loader": "^9.4.0",
"vue-resource": "^1.0.2",
"vue-router": "^2.0.0-rc.5",
"vuex": "^2.0.0-rc.5",
"vuex-router-sync": "^3.0.0",
"webpack": "^1.12.2",
"webpack-merge": "^0.8.3",
"winston": "^2.1.0",
@@ -124,6 +129,7 @@
"scripts": {
"lint": "eslint --ext .js,.vue .",
"test": "npm run lint && gulp test && npm run client:unit",
"test:build": "gulp test:prepare:build",
"test:api-v3": "gulp test:api-v3",
"test:api-v3:unit": "gulp test:api-v3:unit",
"test:api-v3:integration": "gulp test:api-v3:integration",
@@ -142,6 +148,7 @@
"client:dev": "node webpack/dev-server.js",
"client:build": "node webpack/build.js",
"client:unit": "karma start test/client/unit/karma.conf.js --single-run",
"client:unit:watch": "karma start test/client/unit/karma.conf.js",
"client:e2e": "node test/client/e2e/runner.js",
"client:test": "npm run client:unit && npm run client:e2e",
"start": "gulp run:dev",
@@ -198,7 +205,6 @@
"sinon": "^1.17.2",
"sinon-chai": "^2.8.0",
"superagent-defaults": "^0.1.13",
"vinyl-source-stream": "^1.0.0",
"vinyl-transform": "^1.0.0",
"webpack-dev-middleware": "^1.4.0",
"webpack-hot-middleware": "^2.6.0"

View File

@@ -1,7 +1,10 @@
import {
createAndPopulateGroup,
translate as t,
sleep,
server,
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
describe('POST /chat', () => {
let user, groupWithChat, userWithChatRevoked, member;
@@ -40,7 +43,7 @@ describe('POST /chat', () => {
});
});
it('Returns an error when chat privileges are revoked', async () => {
it('returns an error when chat privileges are revoked when sending a message to a public guild', async () => {
await expect(userWithChatRevoked.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage})).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
@@ -48,12 +51,86 @@ describe('POST /chat', () => {
});
});
it('does not error when sending a message to a private guild with a user with revoked chat', async () => {
let { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'Private Guild',
type: 'guild',
privacy: 'private',
},
members: 1,
});
let privateGuildMemberWithChatsRevoked = members[0];
await privateGuildMemberWithChatsRevoked.update({'flags.chatRevoked': true});
let message = await privateGuildMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage});
expect(message.message.id).to.exist;
});
it('does not error when sending a message to a party with a user with revoked chat', async () => {
let { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'Party',
type: 'party',
privacy: 'private',
},
members: 1,
});
let privatePartyMemberWithChatsRevoked = members[0];
await privatePartyMemberWithChatsRevoked.update({'flags.chatRevoked': true});
let message = await privatePartyMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage});
expect(message.message.id).to.exist;
});
it('creates a chat', async () => {
let message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage});
expect(message.message.id).to.exist;
});
it('sends group chat received webhooks', async () => {
let userUuid = generateUUID();
let memberUuid = generateUUID();
await server.start();
await user.post('/user/webhook', {
url: `http://localhost:${server.port}/webhooks/${userUuid}`,
type: 'groupChatReceived',
enabled: true,
options: {
groupId: groupWithChat.id,
},
});
await member.post('/user/webhook', {
url: `http://localhost:${server.port}/webhooks/${memberUuid}`,
type: 'groupChatReceived',
enabled: true,
options: {
groupId: groupWithChat.id,
},
});
let message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
await sleep();
await server.close();
let userBody = server.getWebhookData(userUuid);
let memberBody = server.getWebhookData(memberUuid);
[userBody, memberBody].forEach((body) => {
expect(body.group.id).to.eql(groupWithChat._id);
expect(body.group.name).to.eql(groupWithChat.name);
expect(body.chat).to.eql(message.message);
});
});
it('notifies other users of new messages for a guild', async () => {
let message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage});
let memberWithNotification = await member.get('/user');

View File

@@ -29,14 +29,6 @@ describe('POST /coupons/generate/:event', () => {
});
});
it('returns an error if event is missing', async () => {
await expect(user.post('/coupons/generate')).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: 'Not found.',
});
});
it('returns an error if event is invalid', async () => {
await expect(user.post('/coupons/generate/notValid?count=1')).to.eventually.be.rejected.and.eql({
code: 400,

View File

@@ -65,6 +65,19 @@ describe('POST /groups/:groupId/leave', () => {
expect(groupToLeave.leader).to.equal(member._id);
});
it('removes new messages for that group from user', async () => {
await member.post(`/groups/${groupToLeave._id}/chat`, { message: 'Some message' });
await leader.sync();
expect(leader.newMessages[groupToLeave._id]).to.not.be.empty;
await leader.post(`/groups/${groupToLeave._id}/leave`);
await leader.sync();
expect(leader.newMessages[groupToLeave._id]).to.be.empty;
});
context('With challenges', () => {
let challenge;
@@ -122,6 +135,8 @@ describe('POST /groups/:groupId/leave', () => {
privateGuild = group;
leader = groupLeader;
invitedUser = invitees[0];
await leader.post(`/groups/${group._id}/chat`, { message: 'Some message' });
});
it('removes a group when the last member leaves', async () => {

View File

@@ -87,6 +87,7 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
let partyLeader;
let partyInvitedUser;
let partyMember;
let removedMember;
beforeEach(async () => {
let { group, groupLeader, invitees, members } = await createAndPopulateGroup({
@@ -96,13 +97,14 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
privacy: 'private',
},
invites: 1,
members: 1,
members: 2,
});
party = group;
partyLeader = groupLeader;
partyInvitedUser = invitees[0];
partyMember = members[0];
removedMember = members[1];
});
it('can remove other members', async () => {
@@ -129,6 +131,18 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
expect(invitedUserWithoutInvite.invitations.party).to.be.empty;
});
it('removes new messages from a member who is removed', async () => {
await partyLeader.post(`/groups/${party._id}/chat`, { message: 'Some message' });
await removedMember.sync();
expect(removedMember.newMessages[party._id]).to.not.be.empty;
await partyLeader.post(`/groups/${party._id}/removeMember/${removedMember._id}`);
await removedMember.sync();
expect(removedMember.newMessages[party._id]).to.be.empty;
});
it('removes user from quest when removing user from party after quest starts', async () => {
let petQuest = 'whale';
await partyLeader.update({

View File

@@ -57,11 +57,27 @@ describe('Post /groups/:groupId/invite', () => {
});
});
it('returns empty when uuids is empty', async () => {
it('returns an error when uuids and emails are empty', async () => {
await expect(inviter.post(`/groups/${group._id}/invite`, {
emails: [],
uuids: [],
}))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('inviteMustNotBeEmpty'),
});
});
it('returns an error when uuids is empty and emails is not passed', async () => {
await expect(inviter.post(`/groups/${group._id}/invite`, {
uuids: [],
}))
.to.eventually.be.empty;
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('inviteMissingUuid'),
});
});
it('returns an error when there are more than INVITES_LIMIT uuids', async () => {
@@ -159,11 +175,15 @@ describe('Post /groups/:groupId/invite', () => {
});
});
it('returns empty when emails is an empty array', async () => {
it('returns an error when emails is empty and uuids is not passed', async () => {
await expect(inviter.post(`/groups/${group._id}/invite`, {
emails: [],
}))
.to.eventually.be.empty;
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('inviteMissingEmail'),
});
});
it('returns an error when there are more than INVITES_LIMIT emails', async () => {

View File

@@ -1,7 +1,12 @@
import {
generateUser,
translate as t,
generateGroup,
sleep,
generateChallenge,
server,
} from '../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
describe('DELETE /tasks/:id', () => {
let user;
@@ -42,6 +47,77 @@ describe('DELETE /tasks/:id', () => {
});
});
context('sending task activity webhooks', () => {
before(async () => {
await server.start();
});
after(async () => {
await server.close();
});
it('sends task activity webhooks if task is user owned', async () => {
let uuid = generateUUID();
let task = await user.post('/tasks/user', {
text: 'test habit',
type: 'habit',
});
await user.post('/user/webhook', {
url: `http://localhost:${server.port}/webhooks/${uuid}`,
type: 'taskActivity',
enabled: true,
options: {
created: false,
deleted: true,
},
});
await user.del(`/tasks/${task.id}`);
await sleep();
let body = server.getWebhookData(uuid);
expect(body.type).to.eql('deleted');
expect(body.task).to.eql(task);
});
it('does not send task activity webhooks if task is not user owned', async () => {
let uuid = generateUUID();
await user.update({
balance: 10,
});
let guild = await generateGroup(user);
let challenge = await generateChallenge(user, guild);
await user.post('/user/webhook', {
url: `http://localhost:${server.port}/webhooks/${uuid}`,
type: 'taskActivity',
enabled: true,
options: {
created: false,
deleted: true,
},
});
let challengeTask = await user.post(`/tasks/challenge/${challenge._id}`, {
text: 'test habit',
type: 'habit',
});
await user.del(`/tasks/${challengeTask.id}`);
await sleep();
let body = server.getWebhookData(uuid);
expect(body).to.not.exist;
});
});
context('task cannot be deleted', () => {
it('cannot delete a non-existant task', async () => {
await expect(user.del('/tasks/550e8400-e29b-41d4-a716-446655440000')).to.eventually.be.rejected.and.eql({

View File

@@ -1,6 +1,8 @@
import {
generateUser,
sleep,
translate as t,
server,
} from '../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
@@ -45,6 +47,40 @@ describe('POST /tasks/:id/score/:direction', () => {
message: t('invalidReqParams'),
});
});
it('sends task scored webhooks', async () => {
let uuid = generateUUID();
await server.start();
await user.post('/user/webhook', {
url: `http://localhost:${server.port}/webhooks/${uuid}`,
type: 'taskActivity',
enabled: true,
options: {
created: false,
scored: true,
},
});
let task = await user.post('/tasks/user', {
text: 'test habit',
type: 'habit',
});
await user.post(`/tasks/${task.id}/score/up`);
await sleep();
await server.close();
let body = server.getWebhookData(uuid);
expect(body.user).to.have.all.keys('_id', '_tmp', 'stats');
expect(body.user.stats).to.have.all.keys('hp', 'mp', 'exp', 'gp', 'lvl', 'class', 'points', 'str', 'con', 'int', 'per', 'buffs', 'training', 'maxHealth', 'maxMP', 'toNextLevel');
expect(body.task.id).to.eql(task.id);
expect(body.direction).to.eql('up');
expect(body.delta).to.be.greaterThan(0);
});
});
context('todos', () => {

View File

@@ -1,13 +1,15 @@
import {
generateUser,
sleep,
translate as t,
server,
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
describe('POST /tasks/user', () => {
let user;
before(async () => {
beforeEach(async () => {
user = await generateUser();
});
@@ -205,6 +207,71 @@ describe('POST /tasks/user', () => {
});
});
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: {
created: true,
},
});
let task = await user.post('/tasks/user', {
text: 'test habit',
type: 'habit',
});
await sleep();
let body = server.getWebhookData(uuid);
expect(body.task).to.eql(task);
});
it('sends a task activity webhook for each task', async () => {
let uuid = generateUUID();
await user.post('/user/webhook', {
url: `http://localhost:${server.port}/webhooks/${uuid}`,
type: 'taskActivity',
enabled: true,
options: {
created: true,
},
});
let tasks = await user.post('/tasks/user', [{
text: 'test habit',
type: 'habit',
}, {
text: 'test todo',
type: 'todo',
}]);
await sleep();
let taskBodies = [
server.getWebhookData(uuid),
server.getWebhookData(uuid),
];
expect(taskBodies.find(body => body.task.id === tasks[0].id)).to.exist;
expect(taskBodies.find(body => body.task.id === tasks[1].id)).to.exist;
});
});
context('all types', () => {
it('can create reminders', async () => {
let id1 = generateUUID();

View File

@@ -3,6 +3,7 @@ import {
generateGroup,
sleep,
generateChallenge,
server,
} from '../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
@@ -73,6 +74,7 @@ describe('PUT /tasks/:id', () => {
checklist: [
{text: 123, completed: false},
],
collapseChecklist: false,
});
await sleep(2);
@@ -110,6 +112,7 @@ describe('PUT /tasks/:id', () => {
{text: 123, completed: false},
{text: 456, completed: true},
],
collapseChecklist: true,
notes: 'new notes',
attribute: 'per',
tags: [challengeUserTaskId],
@@ -142,6 +145,83 @@ describe('PUT /tasks/:id', () => {
expect(savedChallengeUserTask.streak).to.equal(25);
expect(savedChallengeUserTask.reminders.length).to.equal(2);
expect(savedChallengeUserTask.checklist.length).to.equal(2);
expect(savedChallengeUserTask.alias).to.equal('a-short-task-name');
expect(savedChallengeUserTask.collapseChecklist).to.equal(true);
});
});
context('sending task activity webhooks', () => {
before(async () => {
await server.start();
});
after(async () => {
await server.close();
});
it('sends task activity webhooks if task is user owned', async () => {
let uuid = generateUUID();
await user.post('/user/webhook', {
url: `http://localhost:${server.port}/webhooks/${uuid}`,
type: 'taskActivity',
enabled: true,
options: {
created: false,
updated: true,
},
});
let task = await user.post('/tasks/user', {
text: 'test habit',
type: 'habit',
});
let updatedTask = await user.put(`/tasks/${task.id}`, {
text: 'updated text',
});
await sleep();
let body = server.getWebhookData(uuid);
expect(body.type).to.eql('updated');
expect(body.task).to.eql(updatedTask);
});
it('does not send task activity webhooks if task is not user owned', async () => {
let uuid = generateUUID();
await user.update({
balance: 10,
});
let guild = await generateGroup(user);
let challenge = await generateChallenge(user, guild);
await user.post('/user/webhook', {
url: `http://localhost:${server.port}/webhooks/${uuid}`,
type: 'taskActivity',
enabled: true,
options: {
created: false,
updated: true,
},
});
let task = await user.post(`/tasks/challenge/${challenge._id}`, {
text: 'test habit',
type: 'habit',
});
await user.put(`/tasks/${task.id}`, {
text: 'updated text',
});
await sleep();
let body = server.getWebhookData(uuid);
expect(body).to.not.exist;
});
});

View File

@@ -74,7 +74,7 @@ describe('POST /tasks/:taskId', () => {
});
it('returns error when non leader tries to create a task', async () => {
await expect(member.post(`/tasks/${task._id}/assign/${member._id}`))
await expect(member2.post(`/tasks/${task._id}/assign/${member._id}`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
@@ -82,6 +82,17 @@ describe('POST /tasks/:taskId', () => {
});
});
it('allows user to assign themselves', async () => {
await member.post(`/tasks/${task._id}/assign/${member._id}`);
let groupTask = await user.get(`/tasks/group/${guild._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
expect(groupTask[0].group.assignedUsers).to.contain(member._id);
expect(syncedTask).to.exist;
});
it('assigns a task to a user', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);

View File

@@ -0,0 +1,83 @@
import {
createAndPopulateGroup,
translate as t,
} from '../../../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
describe('DELETE group /tasks/:taskId/checklist/:itemId', () => {
let user, guild, task;
before(async () => {
let {group, groupLeader} = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
},
members: 2,
});
guild = group;
user = groupLeader;
});
it('deletes a checklist item', async () => {
task = await user.post(`/tasks/group/${guild._id}`, {
type: 'daily',
text: 'Daily with checklist',
});
let savedTask = await user.post(`/tasks/${task._id}/checklist`, {text: 'Checklist Item 1', completed: false});
await user.del(`/tasks/${task._id}/checklist/${savedTask.checklist[0].id}`);
savedTask = await user.get(`/tasks/group/${guild._id}`);
expect(savedTask[0].checklist.length).to.equal(0);
});
it('does not work with habits', async () => {
let habit = await user.post(`/tasks/group/${guild._id}`, {
type: 'habit',
text: 'habit with checklist',
});
await expect(user.del(`/tasks/${habit._id}/checklist/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('checklistOnlyDailyTodo'),
});
});
it('does not work with rewards', async () => {
let reward = await user.post(`/tasks/group/${guild._id}`, {
type: 'reward',
text: 'reward with checklist',
});
await expect(user.del(`/tasks/${reward._id}/checklist/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('checklistOnlyDailyTodo'),
});
});
it('fails on task not found', async () => {
await expect(user.del(`/tasks/${generateUUID()}/checklist/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('taskNotFound'),
});
});
it('fails on checklist item not found', async () => {
let createdTask = await user.post(`/tasks/group/${guild._id}`, {
type: 'daily',
text: 'daily with checklist',
});
await expect(user.del(`/tasks/${createdTask._id}/checklist/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('checklistItemNotFound'),
});
});
});

View File

@@ -0,0 +1,85 @@
import {
createAndPopulateGroup,
translate as t,
} from '../../../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
describe('POST group /tasks/:taskId/checklist/', () => {
let user, guild, task;
before(async () => {
let {group, groupLeader} = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
},
members: 2,
});
guild = group;
user = groupLeader;
});
it('adds a checklist item to a task', async () => {
task = await user.post(`/tasks/group/${guild._id}`, {
type: 'daily',
text: 'Daily with checklist',
});
await user.post(`/tasks/${task._id}/checklist`, {
text: 'Checklist Item 1',
ignored: false,
_id: 123,
});
let updatedTasks = await user.get(`/tasks/group/${guild._id}`);
let updatedTask = updatedTasks[0];
expect(updatedTask.checklist.length).to.equal(1);
expect(updatedTask.checklist[0].text).to.equal('Checklist Item 1');
expect(updatedTask.checklist[0].completed).to.equal(false);
expect(updatedTask.checklist[0].id).to.be.a('string');
expect(updatedTask.checklist[0].id).to.not.equal('123');
expect(updatedTask.checklist[0].ignored).to.be.an('undefined');
});
it('does not add a checklist to habits', async () => {
let habit = await user.post(`/tasks/group/${guild._id}`, {
type: 'habit',
text: 'habit with checklist',
});
await expect(user.post(`/tasks/${habit._id}/checklist`, {
text: 'Checklist Item 1',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('checklistOnlyDailyTodo'),
});
});
it('does not add a checklist to rewards', async () => {
let reward = await user.post(`/tasks/group/${guild._id}`, {
type: 'reward',
text: 'reward with checklist',
});
await expect(user.post(`/tasks/${reward._id}/checklist`, {
text: 'Checklist Item 1',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('checklistOnlyDailyTodo'),
});
});
it('fails on task not found', async () => {
await expect(user.post(`/tasks/${generateUUID()}/checklist`, {
text: 'Checklist Item 1',
})).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('taskNotFound'),
});
});
});

View File

@@ -0,0 +1,92 @@
import {
createAndPopulateGroup,
translate as t,
} from '../../../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
describe('PUT group /tasks/:taskId/checklist/:itemId', () => {
let user, guild, task;
before(async () => {
let {group, groupLeader} = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
},
members: 2,
});
guild = group;
user = groupLeader;
});
it('updates a checklist item', async () => {
task = await user.post(`/tasks/group/${guild._id}`, {
type: 'daily',
text: 'Daily with checklist',
});
let savedTask = await user.post(`/tasks/${task._id}/checklist`, {
text: 'Checklist Item 1',
completed: false,
});
savedTask = await user.put(`/tasks/${task._id}/checklist/${savedTask.checklist[0].id}`, {
text: 'updated',
completed: true,
_id: 123, // ignored
});
expect(savedTask.checklist.length).to.equal(1);
expect(savedTask.checklist[0].text).to.equal('updated');
expect(savedTask.checklist[0].completed).to.equal(true);
expect(savedTask.checklist[0].id).to.not.equal('123');
});
it('fails on habits', async () => {
let habit = await user.post(`/tasks/group/${guild._id}`, {
type: 'habit',
text: 'habit with checklist',
});
await expect(user.put(`/tasks/${habit._id}/checklist/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('checklistOnlyDailyTodo'),
});
});
it('fails on rewards', async () => {
let reward = await user.post(`/tasks/group/${guild._id}`, {
type: 'reward',
text: 'reward with checklist',
});
await expect(user.put(`/tasks/${reward._id}/checklist/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('checklistOnlyDailyTodo'),
});
});
it('fails on task not found', async () => {
await expect(user.put(`/tasks/${generateUUID()}/checklist/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('taskNotFound'),
});
});
it('fails on checklist item not found', async () => {
let createdTask = await user.post(`/tasks/group/${guild._id}`, {
type: 'daily',
text: 'daily with checklist',
});
await expect(user.put(`/tasks/${createdTask._id}/checklist/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('checklistItemNotFound'),
});
});
});

View File

@@ -0,0 +1,51 @@
import {
createAndPopulateGroup,
translate as t,
} from '../../../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
// Currently we do not support adding tags to group original tasks, but if we do in the future, these tests will check
xdescribe('DELETE group /tasks/:taskId/tags/:tagId', () => {
let user, guild, task;
before(async () => {
let {group, groupLeader} = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
},
members: 2,
});
guild = group;
user = groupLeader;
});
it('removes a tag from a task', async () => {
task = await user.post(`/tasks/group/${guild._id}`, {
type: 'habit',
text: 'Task with tag',
});
let tag = await user.post('/tags', {name: 'Tag 1'});
await user.post(`/tasks/${task._id}/tags/${tag.id}`);
await user.del(`/tasks/${task._id}/tags/${tag.id}`);
let updatedTask = await user.get(`/tasks/group/${guild._id}`);
expect(updatedTask[0].tags.length).to.equal(0);
});
it('only deletes existing tags', async () => {
let createdTask = await user.post(`/tasks/group/${guild._id}`, {
type: 'habit',
text: 'Task with tag',
});
await expect(user.del(`/tasks/${createdTask._id}/tags/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('tagNotFound'),
});
});
});

View File

@@ -0,0 +1,64 @@
import {
createAndPopulateGroup,
translate as t,
} from '../../../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
// Currently we do not support adding tags to group original tasks, but if we do in the future, these tests will check
xdescribe('POST group /tasks/:taskId/tags/:tagId', () => {
let user, guild, task;
before(async () => {
let {group, groupLeader} = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
},
members: 2,
});
guild = group;
user = groupLeader;
});
it('adds a tag to a task', async () => {
task = await user.post(`/tasks/group/${guild._id}`, {
type: 'habit',
text: 'Task with tag',
});
let tag = await user.post('/tags', {name: 'Tag 1'});
let savedTask = await user.post(`/tasks/${task._id}/tags/${tag.id}`);
expect(savedTask.tags[0]).to.equal(tag.id);
});
it('does not add a tag to a task twice', async () => {
task = await user.post(`/tasks/group/${guild._id}`, {
type: 'habit',
text: 'Task with tag',
});
let tag = await user.post('/tags', {name: 'Tag 1'});
await user.post(`/tasks/${task._id}/tags/${tag.id}`);
await expect(user.post(`/tasks/${task._id}/tags/${tag.id}`)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('alreadyTagged'),
});
});
it('does not add a non existing tag to a task', async () => {
task = await user.post(`/tasks/group/${guild._id}`, {
type: 'habit',
text: 'Task with tag',
});
await expect(user.post(`/tasks/${task._id}/tags/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
});

View File

@@ -1,23 +0,0 @@
import {
generateUser,
} from '../../../../helpers/api-integration/v3';
let user;
let endpoint = '/user/webhook';
describe('DELETE /user/webhook', () => {
beforeEach(async () => {
user = await generateUser();
});
it('succeeds', async () => {
let id = 'some-id';
user.preferences.webhooks[id] = { url: 'http://some-url.com', enabled: true };
await user.sync();
expect(user.preferences.webhooks).to.eql({});
let response = await user.del(`${endpoint}/${id}`);
expect(response).to.eql({});
await user.sync();
expect(user.preferences.webhooks).to.eql({});
});
});

View File

@@ -13,12 +13,19 @@ describe('GET /user/anonymized', () => {
before(async () => {
user = await generateUser();
await user.update({ newMessages: ['some', 'new', 'messages'], 'profile.name': 'profile', 'purchased.plan': 'purchased plan',
contributor: 'contributor', invitations: 'invitations', 'items.special.nyeReceived': 'some', 'items.special.valentineReceived': 'some',
webhooks: 'some', 'achievements.challenges': 'some',
'inbox.messages': [{ text: 'some text' }],
tags: [{ name: 'some name', challenge: 'some challenge' }],
});
await user.update({
newMessages: ['some', 'new', 'messages'],
'profile.name': 'profile',
'purchased.plan': 'purchased plan',
contributor: 'contributor',
invitations: 'invitations',
'items.special.nyeReceived': 'some',
'items.special.valentineReceived': 'some',
webhooks: [{url: 'https://somurl.com'}],
'achievements.challenges': 'some',
'inbox.messages': [{ text: 'some text' }],
tags: [{ name: 'some name', challenge: 'some challenge' }],
});
await generateHabit({ userId: user._id });
await generateHabit({ userId: user._id, text: generateUUID() });

View File

@@ -1,29 +0,0 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
let user;
let endpoint = '/user/webhook';
describe('POST /user/webhook', () => {
beforeEach(async () => {
user = await generateUser();
});
it('validates', async () => {
await expect(user.post(endpoint, { enabled: true })).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidUrl'),
});
});
it('successfully adds the webhook', async () => {
expect(user.preferences.webhooks).to.eql({});
let response = await user.post(endpoint, { enabled: true, url: 'http://some-url.com'});
expect(response.id).to.exist;
await user.sync();
expect(user.preferences.webhooks).to.not.eql({});
});
});

View File

@@ -37,6 +37,7 @@ describe('PUT /user', () => {
subscriptions: {'purchased.plan.extraMonths': 500, 'purchased.plan.consecutive.trinkets': 1000},
'customization gem purchases': {'purchased.background.tavern': true, 'purchased.skin.bear': true},
notifications: [{type: 123}],
webhooks: {webhooks: [{url: 'https://foobar.com'}]},
};
each(protectedOperations, (data, testName) => {

View File

@@ -1,32 +0,0 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
let user;
let url = 'http://new-url.com';
let enabled = true;
describe('PUT /user/webhook/:id', () => {
beforeEach(async () => {
user = await generateUser();
});
it('validation fails', async () => {
await expect(user.put('/user/webhook/some-id'), { enabled: true }).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidUrl'),
});
});
it('succeeds', async () => {
let response = await user.post('/user/webhook', { enabled: true, url: 'http://some-url.com'});
await user.sync();
expect(user.preferences.webhooks[response.id].url).to.not.eql(url);
let response2 = await user.put(`/user/webhook/${response.id}`, {url, enabled});
expect(response2.url).to.eql(url);
await user.sync();
expect(user.preferences.webhooks[response.id].url).to.eql(url);
});
});

View File

@@ -5,36 +5,94 @@ import {
describe('DELETE social registration', () => {
let user;
let endpoint = '/user/auth/social/facebook';
beforeEach(async () => {
user = await generateUser();
await user.update({ 'auth.facebook.id': 'some-fb-id' });
expect(user.auth.local.username).to.not.be.empty;
expect(user.auth.facebook).to.not.be.empty;
});
context('of NOT-FACEBOOK', () => {
context('NOT-SUPPORTED', () => {
it('is not supported', async () => {
await expect(user.del('/user/auth/social/SOME-OTHER-NETWORK')).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlyFbSupported'),
code: 400,
error: 'BadRequest',
message: t('unsupportedNetwork'),
});
});
});
context('of facebook', () => {
it('fails if local registration does not exist for this user', async () => {
await user.update({ 'auth.local': { ok: true } });
await expect(user.del(endpoint)).to.eventually.be.rejected.and.eql({
context('Facebook', () => {
it('fails if user does not have an alternative registration method', async () => {
await user.update({
'auth.facebook.id': 'some-fb-id',
'auth.local': { ok: true },
});
await expect(user.del('/user/auth/social/facebook')).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('cantDetachFb'),
message: t('cantDetachSocial'),
});
});
it('succeeds', async () => {
let response = await user.del(endpoint);
it('succeeds if user has a local registration', async () => {
await user.update({
'auth.facebook.id': 'some-fb-id',
});
let response = await user.del('/user/auth/social/facebook');
expect(response).to.eql({});
await user.sync();
expect(user.auth.facebook).to.be.empty;
});
it('succeeds if user has a google registration', async () => {
await user.update({
'auth.facebook.id': 'some-fb-id',
'auth.google.id': 'some-google-id',
'auth.local': { ok: true },
});
let response = await user.del('/user/auth/social/facebook');
expect(response).to.eql({});
await user.sync();
expect(user.auth.facebook).to.be.empty;
});
});
context('Google', () => {
it('fails if user does not have an alternative registration method', async () => {
await user.update({
'auth.google.id': 'some-google-id',
'auth.local': { ok: true },
});
await expect(user.del('/user/auth/social/google')).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('cantDetachSocial'),
});
});
it('succeeds if user has a local registration', async () => {
await user.update({
'auth.google.id': 'some-google-id',
});
let response = await user.del('/user/auth/social/google');
expect(response).to.eql({});
await user.sync();
expect(user.auth.google).to.be.empty;
});
it('succeeds if user has a facebook registration', async () => {
await user.update({
'auth.google.id': 'some-google-id',
'auth.facebook.id': 'some-facebook-id',
'auth.local': { ok: true },
});
let response = await user.del('/user/auth/social/google');
expect(response).to.eql({});
await user.sync();
expect(user.auth.google).to.be.empty;
});
});
});

View File

@@ -12,58 +12,132 @@ describe('POST /user/auth/social', () => {
let endpoint = '/user/auth/social';
let randomAccessToken = '123456';
let facebookId = 'facebookId';
let network = 'facebook';
let googleId = 'googleId';
let network = 'NoNetwork';
before(async () => {
beforeEach(async () => {
api = requester();
user = await generateUser();
let expectedResult = {id: facebookId};
let passportFacebookProfile = sandbox.stub(passport._strategies.facebook, 'userProfile');
passportFacebookProfile.yields(null, expectedResult);
});
it('fails if network is not facebook', async () => {
it('fails if network is not supported', async () => {
await expect(api.post(endpoint, {
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
network: 'NotFacebook',
network,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlyFbSupported'),
code: 400,
error: 'BadRequest',
message: t('unsupportedNetwork'),
});
});
it('registers a new user', async () => {
let response = await api.post(endpoint, {
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
network,
describe('facebook', () => {
before(async () => {
let expectedResult = {id: facebookId};
sandbox.stub(passport._strategies.facebook, 'userProfile').yields(null, expectedResult);
network = 'facebook';
});
expect(response.apiToken).to.exist;
expect(response.id).to.exist;
expect(response.newUser).to.be.true;
it('registers a new user', async () => {
let response = await api.post(endpoint, {
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
network,
});
expect(response.apiToken).to.exist;
expect(response.id).to.exist;
expect(response.newUser).to.be.true;
});
it('logs an existing user in', async () => {
let registerResponse = await api.post(endpoint, {
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
network,
});
let response = await api.post(endpoint, {
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
network,
});
expect(response.apiToken).to.eql(registerResponse.apiToken);
expect(response.id).to.eql(registerResponse.id);
expect(response.newUser).to.be.false;
});
it('add social auth to an existing user', async () => {
let response = await user.post(endpoint, {
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
network,
});
expect(response.apiToken).to.exist;
expect(response.id).to.exist;
expect(response.newUser).to.be.false;
});
it('enrolls a new user in an A/B test', async () => {
await api.post(endpoint, {
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
network,
});
await expect(getProperty('users', user._id, '_ABtest')).to.eventually.be.a('string');
});
});
it('enrolls a new user in an A/B test', async () => {
await api.post(endpoint, {
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
network,
describe('google', () => {
before(async () => {
let expectedResult = {id: googleId};
sandbox.stub(passport._strategies.google, 'userProfile').yields(null, expectedResult);
network = 'google';
});
await expect(getProperty('users', user._id, '_ABtest')).to.eventually.be.a('string');
});
it('registers a new user', async () => {
let response = await api.post(endpoint, {
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
network,
});
it('logs an existing user in', async () => {
await user.update({ 'auth.facebook.id': facebookId });
let response = await api.post(endpoint, {
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
network,
expect(response.apiToken).to.exist;
expect(response.id).to.exist;
expect(response.newUser).to.be.true;
});
expect(response.apiToken).to.eql(user.apiToken);
expect(response.id).to.eql(user._id);
expect(response.newUser).to.be.false;
it('logs an existing user in', async () => {
let registerResponse = await api.post(endpoint, {
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
network,
});
let response = await api.post(endpoint, {
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
network,
});
expect(response.apiToken).to.eql(registerResponse.apiToken);
expect(response.id).to.eql(registerResponse.id);
expect(response.newUser).to.be.false;
});
it('add social auth to an existing user', async () => {
let response = await user.post(endpoint, {
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
network,
});
expect(response.apiToken).to.exist;
expect(response.id).to.exist;
expect(response.newUser).to.be.false;
});
it('enrolls a new user in an A/B test', async () => {
await api.post(endpoint, {
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
network,
});
await expect(getProperty('users', user._id, '_ABtest')).to.eventually.be.a('string');
});
});
});

View File

@@ -0,0 +1,54 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
let user, webhookToDelete;
let endpoint = '/user/webhook';
describe('DELETE /user/webhook', () => {
beforeEach(async () => {
user = await generateUser();
webhookToDelete = await user.post('/user/webhook', {
url: 'http://some-url.com',
enabled: true,
});
await user.post('/user/webhook', {
url: 'http://some-other-url.com',
enabled: false,
});
await user.sync();
});
it('deletes a webhook', async () => {
expect(user.webhooks).to.have.a.lengthOf(2);
await user.del(`${endpoint}/${webhookToDelete.id}`);
await user.sync();
expect(user.webhooks).to.have.a.lengthOf(1);
});
it('returns the remaining webhooks', async () => {
let [remainingWebhook] = await user.del(`${endpoint}/${webhookToDelete.id}`);
await user.sync();
let webhook = user.webhooks[0];
expect(remainingWebhook.id).to.eql(webhook.id);
expect(remainingWebhook.url).to.eql(webhook.url);
expect(remainingWebhook.type).to.eql(webhook.type);
expect(remainingWebhook.options).to.eql(webhook.options);
});
it('returns an error if webhook with id does not exist', async () => {
await expect(user.del(`${endpoint}/id-that-does-not-exist`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('noWebhookWithId', {id: 'id-that-does-not-exist'}),
});
});
});

View File

@@ -0,0 +1,221 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
describe('POST /user/webhook', () => {
let user, body;
beforeEach(async () => {
user = await generateUser();
body = {
id: generateUUID(),
url: 'https://example.com/endpoint',
type: 'taskActivity',
enabled: false,
};
});
it('requires a url', async () => {
delete body.url;
await expect(user.post('/user/webhook', body)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'User validation failed',
});
});
it('requires custom id to be a uuid', async () => {
body.id = 'not-a-uuid';
await expect(user.post('/user/webhook', body)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'User validation failed',
});
});
it('defaults id to a uuid', async () => {
delete body.id;
let webhook = await user.post('/user/webhook', body);
expect(webhook.id).to.exist;
});
it('requires type to be of an accetable type', async () => {
body.type = 'not a valid type';
await expect(user.post('/user/webhook', body)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'User validation failed',
});
});
it('defaults enabled to true', async () => {
delete body.enabled;
let webhook = await user.post('/user/webhook', body);
expect(webhook.enabled).to.be.true;
});
it('can pass a label', async () => {
body.label = 'Custom Label';
let webhook = await user.post('/user/webhook', body);
expect(webhook.label).to.equal('Custom Label');
});
it('defaults type to taskActivity', async () => {
delete body.type;
let webhook = await user.post('/user/webhook', body);
expect(webhook.type).to.eql('taskActivity');
});
it('successfully adds the webhook', async () => {
expect(user.webhooks).to.eql([]);
let response = await user.post('/user/webhook', body);
expect(response.id).to.eql(body.id);
expect(response.type).to.eql(body.type);
expect(response.url).to.eql(body.url);
expect(response.enabled).to.eql(body.enabled);
await user.sync();
expect(user.webhooks).to.not.eql([]);
let webhook = user.webhooks[0];
expect(webhook.enabled).to.be.false;
expect(webhook.type).to.eql('taskActivity');
expect(webhook.url).to.eql(body.url);
});
it('cannot use an id of a webhook that already exists', async () => {
await user.post('/user/webhook', body);
await expect(user.post('/user/webhook', body)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('webhookIdAlreadyTaken', { id: body.id }),
});
});
it('defaults taskActivity options', async () => {
body.type = 'taskActivity';
let webhook = await user.post('/user/webhook', body);
expect(webhook.options).to.eql({
created: false,
updated: false,
deleted: false,
scored: true,
});
});
it('can set taskActivity options', async () => {
body.type = 'taskActivity';
body.options = {
created: true,
updated: true,
deleted: true,
scored: false,
};
let webhook = await user.post('/user/webhook', body);
expect(webhook.options).to.eql({
created: true,
updated: true,
deleted: true,
scored: false,
});
});
it('discards extra properties in taskActivity options', async () => {
body.type = 'taskActivity';
body.options = {
created: true,
updated: true,
deleted: true,
scored: false,
foo: 'bar',
};
let webhook = await user.post('/user/webhook', body);
expect(webhook.options.foo).to.not.exist;
expect(webhook.options).to.eql({
created: true,
updated: true,
deleted: true,
scored: false,
});
});
['created', 'updated', 'deleted', 'scored'].forEach((option) => {
it(`requires taskActivity option ${option} to be a boolean`, async () => {
body.type = 'taskActivity';
body.options = {
[option]: 'not a boolean',
};
await expect(user.post('/user/webhook', body)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('webhookBooleanOption', { option }),
});
});
});
it('can set groupChatReceived options', async () => {
body.type = 'groupChatReceived';
body.options = {
groupId: generateUUID(),
};
let webhook = await user.post('/user/webhook', body);
expect(webhook.options).to.eql({
groupId: body.options.groupId,
});
});
it('groupChatReceived options requires a uuid for the groupId', async () => {
body.type = 'groupChatReceived';
body.options = {
groupId: 'not-a-uuid',
};
await expect(user.post('/user/webhook', body)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('groupIdRequired'),
});
});
it('discards extra properties in groupChatReceived options', async () => {
body.type = 'groupChatReceived';
body.options = {
groupId: generateUUID(),
foo: 'bar',
};
let webhook = await user.post('/user/webhook', body);
expect(webhook.options.foo).to.not.exist;
expect(webhook.options).to.eql({
groupId: body.options.groupId,
});
});
});

View File

@@ -0,0 +1,132 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
import { v4 as generateUUID} from 'uuid';
describe('PUT /user/webhook/:id', () => {
let user, webhookToUpdate;
beforeEach(async () => {
user = await generateUser();
webhookToUpdate = await user.post('/user/webhook', {
url: 'http://some-url.com',
label: 'Original Label',
enabled: true,
type: 'taskActivity',
options: { created: true, scored: true },
});
await user.post('/user/webhook', {
url: 'http://some-other-url.com',
enabled: false,
});
await user.sync();
});
it('returns an error if webhook with id does not exist', async () => {
await expect(user.put('/user/webhook/id-that-does-not-exist')).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('noWebhookWithId', {id: 'id-that-does-not-exist'}),
});
});
it('returns an error if validation fails', async () => {
await expect(user.put(`/user/webhook/${webhookToUpdate.id}`, { url: 'foo', enabled: true })).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'User validation failed',
});
});
it('updates a webhook', async () => {
let url = 'http://a-new-url.com';
let type = 'groupChatReceived';
let label = 'New Label';
let options = { groupId: generateUUID() };
await user.put(`/user/webhook/${webhookToUpdate.id}`, {url, type, options, label});
await user.sync();
let webhook = user.webhooks.find(hook => webhookToUpdate.id === hook.id);
expect(webhook.url).to.equal(url);
expect(webhook.label).to.equal(label);
expect(webhook.type).to.equal(type);
expect(webhook.options).to.eql(options);
});
it('returns the updated webhook', async () => {
let url = 'http://a-new-url.com';
let type = 'groupChatReceived';
let options = { groupId: generateUUID() };
let response = await user.put(`/user/webhook/${webhookToUpdate.id}`, {url, type, options});
expect(response.url).to.eql(url);
expect(response.type).to.eql(type);
expect(response.options).to.eql(options);
});
it('cannot update the id', async () => {
let id = generateUUID();
let url = 'http://a-new-url.com';
await user.put(`/user/webhook/${webhookToUpdate.id}`, {url, id});
await user.sync();
let webhook = user.webhooks.find(hook => webhookToUpdate.id === hook.id);
expect(webhook.id).to.eql(webhookToUpdate.id);
expect(webhook.url).to.eql(url);
});
it('can update taskActivity options', async () => {
let type = 'taskActivity';
let options = {
updated: false,
deleted: true,
};
let webhook = await user.put(`/user/webhook/${webhookToUpdate.id}`, {type, options});
expect(webhook.options).to.eql({
created: true, // starting value
updated: false,
deleted: true,
scored: true, // default value
});
});
it('errors if taskActivity option is not a boolean', async () => {
let type = 'taskActivity';
let options = {
created: 'not a boolean',
updated: false,
deleted: true,
};
await expect(user.put(`/user/webhook/${webhookToUpdate.id}`, {type, options})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('webhookBooleanOption', { option: 'created' }),
});
});
it('errors if groupChatRecieved groupId option is not a uuid', async () => {
let type = 'groupChatReceived';
let options = {
groupId: 'not-a-uuid',
};
await expect(user.put(`/user/webhook/${webhookToUpdate.id}`, {type, options})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('groupIdRequired'),
});
});
});

View File

@@ -11,6 +11,10 @@ describe('analyticsService', () => {
sandbox.stub(Visitor.prototype, 'transaction');
});
afterEach(() => {
sandbox.restore();
});
describe('#track', () => {
let eventType, data;
@@ -273,6 +277,7 @@ describe('analyticsService', () => {
dailys: [{_id: 'daily'}],
todos: [{_id: 'todo'}],
rewards: [{_id: 'reward'}],
balance: 12,
};
data.user = user;
@@ -296,6 +301,7 @@ describe('analyticsService', () => {
},
contributorLevel: 1,
subscription: 'foo-plan',
balance: 12,
},
});
});

View File

@@ -62,7 +62,7 @@ describe('cron', () => {
describe('end of the month perks', () => {
beforeEach(() => {
user.purchased.plan.customerId = 'subscribedId';
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
user.purchased.plan.dateUpdated = moment().subtract(1, 'months').toDate();
});
it('resets plan.gemsBought on a new month', () => {
@@ -71,10 +71,21 @@ describe('cron', () => {
expect(user.purchased.plan.gemsBought).to.equal(0);
});
it('resets plan.dateUpdated on a new month', () => {
let currentMonth = moment().format('MMYYYY');
it('does not reset plan.gemsBought within the month', () => {
let clock = sinon.useFakeTimers(moment().startOf('month').add(2, 'days').unix());
user.purchased.plan.dateUpdated = moment().startOf('month').toDate();
user.purchased.plan.gemsBought = 10;
cron({user, tasksByType, daysMissed, analytics});
expect(moment(user.purchased.plan.dateUpdated).format('MMYYYY')).to.equal(currentMonth);
expect(user.purchased.plan.gemsBought).to.equal(10);
clock.restore();
});
it('resets plan.dateUpdated on a new month', () => {
let currentMonth = moment().startOf('month');
cron({user, tasksByType, daysMissed, analytics});
expect(moment(user.purchased.plan.dateUpdated).startOf('month').isSame(currentMonth)).to.eql(true);
});
it('increments plan.consecutive.count', () => {
@@ -83,6 +94,13 @@ describe('cron', () => {
expect(user.purchased.plan.consecutive.count).to.equal(1);
});
it('increments plan.consecutive.count by more than 1 if user skipped months between logins', () => {
user.purchased.plan.dateUpdated = moment().subtract(2, 'months').toDate();
user.purchased.plan.consecutive.count = 0;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.count).to.equal(2);
});
it('decrements plan.consecutive.offset when offset is greater than 0', () => {
user.purchased.plan.consecutive.offset = 2;
cron({user, tasksByType, daysMissed, analytics});
@@ -97,6 +115,21 @@ describe('cron', () => {
expect(user.purchased.plan.consecutive.offset).to.equal(0);
});
it('increments plan.consecutive.trinkets multiple times if user has been absent with continuous subscription', () => {
user.purchased.plan.dateUpdated = moment().subtract(6, 'months').toDate();
user.purchased.plan.consecutive.count = 5;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.trinkets).to.equal(2);
});
it('does not award unearned plan.consecutive.trinkets if subscription ended during an absence', () => {
user.purchased.plan.dateUpdated = moment().subtract(6, 'months').toDate();
user.purchased.plan.dateTerminated = moment().subtract(3, 'months').toDate();
user.purchased.plan.consecutive.count = 5;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.trinkets).to.equal(1);
});
it('increments plan.consecutive.gemCapExtra when user has reached a month that is a multiple of 3', () => {
user.purchased.plan.consecutive.count = 5;
user.purchased.plan.consecutive.offset = 1;
@@ -105,6 +138,13 @@ describe('cron', () => {
expect(user.purchased.plan.consecutive.offset).to.equal(0);
});
it('increments plan.consecutive.gemCapExtra multiple times if user has been absent with continuous subscription', () => {
user.purchased.plan.dateUpdated = moment().subtract(6, 'months').toDate();
user.purchased.plan.consecutive.count = 5;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(10);
});
it('does not increment plan.consecutive.gemCapExtra when user has reached the gemCap limit', () => {
user.purchased.plan.consecutive.gemCapExtra = 25;
user.purchased.plan.consecutive.count = 5;
@@ -118,7 +158,7 @@ describe('cron', () => {
expect(user.purchased.plan.customerId).to.exist;
});
it('does reset plan stats until we are after the last day of the cancelled month', () => {
it('does reset plan stats if we are after the last day of the cancelled month', () => {
user.purchased.plan.dateTerminated = moment(new Date()).subtract({days: 1});
user.purchased.plan.consecutive.gemCapExtra = 20;
user.purchased.plan.consecutive.count = 5;
@@ -134,10 +174,25 @@ describe('cron', () => {
});
describe('end of the month perks when user is not subscribed', () => {
it('does not reset plan.gemsBought on a new month', () => {
beforeEach(() => {
user.purchased.plan.dateUpdated = moment().subtract(1, 'months').toDate();
});
it('resets plan.gemsBought on a new month', () => {
user.purchased.plan.gemsBought = 10;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.gemsBought).to.equal(0);
});
it('does not reset plan.gemsBought within the month', () => {
let clock = sinon.useFakeTimers(moment().startOf('month').add(2, 'days').unix());
user.purchased.plan.dateUpdated = moment().startOf('month').toDate();
user.purchased.plan.gemsBought = 10;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.gemsBought).to.equal(10);
clock.restore();
});
it('does not reset plan.dateUpdated on a new month', () => {

View File

@@ -53,31 +53,32 @@ describe('emails', () => {
let pathToEmailLib = '../../../../../website/server/libs/email';
describe('sendEmail', () => {
it('can send an email using the default transport', () => {
let sendMailSpy = sandbox.stub().returns(defer().promise);
let sendMailSpy;
beforeEach(() => {
sendMailSpy = sandbox.stub().returns(defer().promise);
sandbox.stub(nodemailer, 'createTransport').returns({
sendMail: sendMailSpy,
});
});
afterEach(() => {
sandbox.restore();
});
it('can send an email using the default transport', () => {
let attachEmail = requireAgain(pathToEmailLib);
attachEmail.send();
expect(sendMailSpy).to.be.calledOnce;
});
it('logs errors', (done) => {
let deferred = defer();
let sendMailSpy = sandbox.stub().returns(deferred.promise);
sandbox.stub(nodemailer, 'createTransport').returns({
sendMail: sendMailSpy,
});
sandbox.stub(logger, 'error');
let attachEmail = requireAgain(pathToEmailLib);
attachEmail.send();
expect(sendMailSpy).to.be.calledOnce;
deferred.reject();
defer().reject();
// wait for unhandledRejection event to fire
setTimeout(() => {

View File

@@ -80,6 +80,24 @@ describe('payments/index', () => {
expect(recipient.purchased.plan.extraMonths).to.eql(3);
});
it('does not set negative extraMonths if plan has past dateTerminated date', async () => {
let dateTerminated = moment().subtract(2, 'months').toDate();
recipient.purchased.plan.dateTerminated = dateTerminated;
await api.createSubscription(data);
expect(recipient.purchased.plan.extraMonths).to.eql(0);
});
it('does not reset Gold-to-Gems cap on an existing subscription', async () => {
recipient.purchased.plan = plan;
recipient.purchased.plan.gemsBought = 12;
await api.createSubscription(data);
expect(recipient.purchased.plan.gemsBought).to.eql(12);
});
it('adds to date terminated for an existing plan with a future terminated date', async () => {
let dateTerminated = moment().add(1, 'months').toDate();
recipient.purchased.plan = plan;
@@ -210,6 +228,25 @@ describe('payments/index', () => {
expect(user.purchased.plan.extraMonths).to.within(1.9, 2);
});
it('does not set negative extraMonths if plan has past dateTerminated date', async () => {
user.purchased.plan = plan;
user.purchased.plan.dateTerminated = moment(new Date()).subtract(2, 'months');
expect(user.purchased.plan.extraMonths).to.eql(0);
await api.createSubscription(data);
expect(user.purchased.plan.extraMonths).to.eql(0);
});
it('does not reset Gold-to-Gems cap on additional subscription', async () => {
user.purchased.plan = plan;
user.purchased.plan.gemsBought = 10;
await api.createSubscription(data);
expect(user.purchased.plan.gemsBought).to.eql(10);
});
it('sets lastBillingDate if payment method is "Amazon Payments"', async () => {
data.paymentMethod = 'Amazon Payments';
@@ -218,7 +255,7 @@ describe('payments/index', () => {
expect(user.purchased.plan.lastBillingDate).to.exist;
});
it('increases the user\'s transcation count', async () => {
it('increases the user\'s transaction count', async () => {
expect(user.purchased.txnCount).to.eql(0);
await api.createSubscription(data);

View File

@@ -8,27 +8,30 @@ import nconf from 'nconf';
describe('slack', () => {
describe('sendFlagNotification', () => {
let flagger, group, message;
let data;
beforeEach(() => {
sandbox.stub(IncomingWebhook.prototype, 'send');
flagger = {
id: 'flagger-id',
profile: {
name: 'flagger',
data = {
authorEmail: 'author@example.com',
flagger: {
id: 'flagger-id',
profile: {
name: 'flagger',
},
},
group: {
id: 'group-id',
privacy: 'private',
name: 'Some group',
type: 'guild',
},
message: {
id: 'chat-id',
user: 'Author',
uuid: 'author-id',
text: 'some text',
},
};
group = {
id: 'group-id',
privacy: 'private',
name: 'Some group',
type: 'guild',
};
message = {
id: 'chat-id',
user: 'Author',
uuid: 'author-id',
text: 'some text',
};
});
@@ -37,11 +40,7 @@ describe('slack', () => {
});
it('sends a slack webhook', () => {
slack.sendFlagNotification({
flagger,
group,
message,
});
slack.sendFlagNotification(data);
expect(IncomingWebhook.prototype.send).to.be.calledOnce;
expect(IncomingWebhook.prototype.send).to.be.calledWith({
@@ -49,7 +48,7 @@ describe('slack', () => {
attachments: [{
fallback: 'Flag Message',
color: 'danger',
author_name: 'Author - author-id',
author_name: 'Author - author@example.com - author-id',
title: 'Flag in Some group - (private guild)',
title_link: undefined,
text: 'some text',
@@ -62,13 +61,9 @@ describe('slack', () => {
});
it('includes a title link if guild is public', () => {
group.privacy = 'public';
data.group.privacy = 'public';
slack.sendFlagNotification({
flagger,
group,
message,
});
slack.sendFlagNotification(data);
expect(IncomingWebhook.prototype.send).to.be.calledWithMatch({
attachments: [sandbox.match({
@@ -79,15 +74,11 @@ describe('slack', () => {
});
it('links to tavern', () => {
group.privacy = 'public';
group.name = 'Tavern';
group.id = TAVERN_ID;
data.group.privacy = 'public';
data.group.name = 'Tavern';
data.group.id = TAVERN_ID;
slack.sendFlagNotification({
flagger,
group,
message,
});
slack.sendFlagNotification(data);
expect(IncomingWebhook.prototype.send).to.be.calledWithMatch({
attachments: [sandbox.match({
@@ -98,14 +89,10 @@ describe('slack', () => {
});
it('provides name for system message', () => {
message.uuid = 'system';
delete message.user;
data.message.uuid = 'system';
delete data.message.user;
slack.sendFlagNotification({
flagger,
group,
message,
});
slack.sendFlagNotification(data);
expect(IncomingWebhook.prototype.send).to.be.calledWithMatch({
attachments: [sandbox.match({
@@ -121,11 +108,7 @@ describe('slack', () => {
expect(logger.error).to.be.calledOnce;
reRequiredSlack.sendFlagNotification({
flagger,
group,
message,
});
reRequiredSlack.sendFlagNotification(data);
expect(IncomingWebhook.prototype.send).to.not.be.called;
});

View File

@@ -1,135 +1,376 @@
import request from 'request';
import { sendTaskWebhook } from '../../../../../website/server/libs/webhook';
import {
WebhookSender,
taskScoredWebhook,
groupChatReceivedWebhook,
taskActivityWebhook,
} from '../../../../../website/server/libs/webhook';
describe('webhooks', () => {
let webhooks;
beforeEach(() => {
sandbox.stub(request, 'post');
webhooks = [{
id: 'taskActivity',
url: 'http://task-scored.com',
enabled: true,
type: 'taskActivity',
options: {
created: true,
updated: true,
deleted: true,
scored: true,
},
}, {
id: 'groupChatReceived',
url: 'http://group-chat-received.com',
enabled: true,
type: 'groupChatReceived',
options: {
groupId: 'group-id',
},
}];
});
afterEach(() => {
sandbox.restore();
});
describe('sendTaskWebhook', () => {
let task = {
details: { _id: 'task-id' },
delta: 1.4,
direction: 'up',
};
describe('WebhookSender', () => {
it('creates a new WebhookSender object', () => {
let sendWebhook = new WebhookSender({
type: 'custom',
});
let data = {
task,
user: { _id: 'user-id' },
};
expect(sendWebhook.type).to.equal('custom');
expect(sendWebhook).to.respondTo('send');
});
it('does not send if no webhook endpoints exist', () => {
let webhooks = { };
it('provides default function for data transformation', () => {
sandbox.spy(WebhookSender, 'defaultTransformData');
let sendWebhook = new WebhookSender({
type: 'custom',
});
sendTaskWebhook(webhooks, data);
let body = { foo: 'bar' };
sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}], body);
expect(WebhookSender.defaultTransformData).to.be.calledOnce;
expect(request.post).to.be.calledOnce;
expect(request.post).to.be.calledWithMatch({
body,
});
});
it('can pass in a data transformation function', () => {
sandbox.spy(WebhookSender, 'defaultTransformData');
let sendWebhook = new WebhookSender({
type: 'custom',
transformData (data) {
let dataToSend = Object.assign({baz: 'biz'}, data);
return dataToSend;
},
});
let body = { foo: 'bar' };
sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}], body);
expect(WebhookSender.defaultTransformData).to.not.be.called;
expect(request.post).to.be.calledOnce;
expect(request.post).to.be.calledWithMatch({
body: {
foo: 'bar',
baz: 'biz',
},
});
});
it('provieds a default filter function', () => {
sandbox.spy(WebhookSender, 'defaultWebhookFilter');
let sendWebhook = new WebhookSender({
type: 'custom',
});
let body = { foo: 'bar' };
sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}], body);
expect(WebhookSender.defaultWebhookFilter).to.be.calledOnce;
});
it('can pass in a webhook filter function', () => {
sandbox.spy(WebhookSender, 'defaultWebhookFilter');
let sendWebhook = new WebhookSender({
type: 'custom',
webhookFilter (hook) {
return hook.url !== 'http://custom-url.com';
},
});
let body = { foo: 'bar' };
sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}], body);
expect(WebhookSender.defaultWebhookFilter).to.not.be.called;
expect(request.post).to.not.be.called;
});
it('does not send if no webhooks are enabled', () => {
let webhooks = {
'some-id': {
sort: 0,
id: 'some-id',
enabled: false,
url: 'http://example.org/endpoint',
it('can pass in a webhook filter function that filters on data', () => {
sandbox.spy(WebhookSender, 'defaultWebhookFilter');
let sendWebhook = new WebhookSender({
type: 'custom',
webhookFilter (hook, data) {
return hook.options.foo === data.foo;
},
};
});
sendTaskWebhook(webhooks, data);
let body = { foo: 'bar' };
expect(request.post).to.not.be.called;
});
it('does not send if webhook url is not valid', () => {
let webhooks = {
'some-id': {
sort: 0,
id: 'some-id',
enabled: true,
url: 'http://malformedurl/endpoint',
},
};
sendTaskWebhook(webhooks, data);
expect(request.post).to.not.be.called;
});
it('sends task direction, task, task delta, and abridged user data', () => {
let webhooks = {
'some-id': {
sort: 0,
id: 'some-id',
enabled: true,
url: 'http://example.org/endpoint',
},
};
sendTaskWebhook(webhooks, data);
sendWebhook.send([
{ 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);
expect(request.post).to.be.calledOnce;
expect(request.post).to.be.calledWith({
url: 'http://example.org/endpoint',
body: {
direction: 'up',
task: { _id: 'task-id' },
delta: 1.4,
user: {
_id: 'user-id',
},
},
expect(request.post).to.be.calledWithMatch({
url: 'http://custom-url.com',
});
});
it('ignores disabled webhooks', () => {
let sendWebhook = new WebhookSender({
type: 'custom',
});
let body = { foo: 'bar' };
sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: false, type: 'custom'}], body);
expect(request.post).to.not.be.called;
});
it('ignores webhooks with invalid urls', () => {
let sendWebhook = new WebhookSender({
type: 'custom',
});
let body = { foo: 'bar' };
sendWebhook.send([{id: 'custom-webhook', url: 'httxp://custom-url!!', enabled: true, type: 'custom'}], body);
expect(request.post).to.not.be.called;
});
it('ignores webhooks of other types', () => {
let sendWebhook = new WebhookSender({
type: 'custom',
});
let body = { foo: 'bar' };
sendWebhook.send([
{ 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);
expect(request.post).to.be.calledOnce;
expect(request.post).to.be.calledWithMatch({
url: 'http://custom-url.com',
body,
json: true,
});
});
it('sends a post request for each webhook endpoint', () => {
let webhooks = {
'some-id': {
sort: 0,
id: 'some-id',
enabled: true,
url: 'http://example.org/endpoint',
},
'second-webhook': {
sort: 1,
id: 'second-webhook',
enabled: true,
url: 'http://example.com/2/endpoint',
},
};
it('sends multiple webhooks of the same type', () => {
let sendWebhook = new WebhookSender({
type: 'custom',
});
sendTaskWebhook(webhooks, data);
let body = { foo: 'bar' };
sendWebhook.send([
{ 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);
expect(request.post).to.be.calledTwice;
expect(request.post).to.be.calledWith({
url: 'http://example.org/endpoint',
body: {
direction: 'up',
task: { _id: 'task-id' },
delta: 1.4,
user: {
_id: 'user-id',
},
},
expect(request.post).to.be.calledWithMatch({
url: 'http://custom-url.com',
body,
json: true,
});
expect(request.post).to.be.calledWith({
url: 'http://example.com/2/endpoint',
body: {
direction: 'up',
task: { _id: 'task-id' },
delta: 1.4,
user: {
_id: 'user-id',
},
},
expect(request.post).to.be.calledWithMatch({
url: 'http://other-url.com',
body,
json: true,
});
});
});
describe('taskScoredWebhook', () => {
let data;
beforeEach(() => {
data = {
user: {
_id: 'user-id',
_tmp: {foo: 'bar'},
stats: {
lvl: 5,
int: 10,
str: 5,
exp: 423,
toJSON () {
return this;
},
},
addComputedStatsToJSONObj () {
let mockStats = Object.assign({
maxHealth: 50,
maxMP: 103,
toNextLevel: 40,
}, this.stats);
delete mockStats.toJSON;
return mockStats;
},
},
task: {
text: 'text',
},
direction: 'up',
delta: 176,
};
});
it('sends task and stats data', () => {
taskScoredWebhook.send(webhooks, data);
expect(request.post).to.be.calledOnce;
expect(request.post).to.be.calledWithMatch({
body: {
type: 'scored',
user: {
_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('does not send task scored data if scored option is not true', () => {
webhooks[0].options.scored = false;
taskScoredWebhook.send(webhooks, data);
expect(request.post).to.not.be.called;
});
});
describe('taskActivityWebhook', () => {
let data;
beforeEach(() => {
data = {
task: {
text: 'text',
},
};
});
['created', 'updated', 'deleted'].forEach((type) => {
it(`sends ${type} tasks`, () => {
data.type = type;
taskActivityWebhook.send(webhooks, data);
expect(request.post).to.be.calledOnce;
expect(request.post).to.be.calledWithMatch({
body: {
type,
task: data.task,
},
});
});
it(`does not send task ${type} data if ${type} option is not true`, () => {
data.type = type;
webhooks[0].options[type] = false;
taskActivityWebhook.send(webhooks, data);
expect(request.post).to.not.be.called;
});
});
});
describe('groupChatReceivedWebhook', () => {
it('sends chat data', () => {
let data = {
group: {
id: 'group-id',
name: 'some group',
otherData: 'foo',
},
chat: {
id: 'some-id',
text: 'message',
},
};
groupChatReceivedWebhook.send(webhooks, data);
expect(request.post).to.be.calledOnce;
expect(request.post).to.be.calledWithMatch({
body: {
group: {
id: 'group-id',
name: 'some group',
},
chat: {
id: 'some-id',
text: 'message',
},
},
});
});
it('does not send chat data for group if not selected', () => {
let data = {
group: {
id: 'not-group-id',
name: 'some group',
otherData: 'foo',
},
chat: {
id: 'some-id',
text: 'message',
},
};
groupChatReceivedWebhook.send(webhooks, data);
expect(request.post).to.not.be.called;
});
});
});

View File

@@ -20,7 +20,7 @@ describe('cors middleware', () => {
expect(res.set).to.have.been.calledWith({
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'OPTIONS,GET,POST,PUT,HEAD,DELETE',
'Access-Control-Allow-Headers': 'Content-Type,Accept,Content-Encoding,X-Requested-With,x-api-user,x-api-key',
'Access-Control-Allow-Headers': 'Content-Type,Accept,Content-Encoding,X-Requested-With,x-api-user,x-api-key,x-client',
});
expect(res.sendStatus).to.not.have.been.called;
expect(next).to.have.been.called.once;
@@ -32,7 +32,7 @@ describe('cors middleware', () => {
expect(res.set).to.have.been.calledWith({
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'OPTIONS,GET,POST,PUT,HEAD,DELETE',
'Access-Control-Allow-Headers': 'Content-Type,Accept,Content-Encoding,X-Requested-With,x-api-user,x-api-key',
'Access-Control-Allow-Headers': 'Content-Type,Accept,Content-Encoding,X-Requested-With,x-api-user,x-api-key,x-client',
});
expect(res.sendStatus).to.have.been.calledWith(200);
expect(next).to.not.have.been.called;

View File

@@ -1,10 +1,13 @@
import { sleep } from '../../../../helpers/api-unit.helper';
import { model as Group } from '../../../../../website/server/models/group';
import { model as Group, INVITES_LIMIT } from '../../../../../website/server/models/group';
import { model as User } from '../../../../../website/server/models/user';
import { BadRequest } from '../../../../../website/server/libs/errors';
import { quests as questScrolls } from '../../../../../website/common/script/content';
import { groupChatReceivedWebhook } from '../../../../../website/server/libs/webhook';
import * as email from '../../../../../website/server/libs/email';
import validator from 'validator';
import { TAVERN_ID } from '../../../../../website/common/script/';
import { v4 as generateUUID } from 'uuid';
describe('Group Model', () => {
let party, questLeader, participatingMember, nonParticipatingMember, undecidedMember;
@@ -433,6 +436,158 @@ describe('Group Model', () => {
});
});
});
describe('validateInvitations', () => {
let res;
beforeEach(() => {
res = {
t: sandbox.spy(),
};
});
it('throws an error if no uuids or emails are passed in', (done) => {
try {
Group.validateInvitations(null, null, res);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('canOnlyInviteEmailUuid');
done();
}
});
it('throws an error if only uuids are passed in, but they are not an array', (done) => {
try {
Group.validateInvitations({ uuid: 'user-id'}, null, res);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('uuidsMustBeAnArray');
done();
}
});
it('throws an error if only emails are passed in, but they are not an array', (done) => {
try {
Group.validateInvitations(null, { emails: 'user@example.com'}, res);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('emailsMustBeAnArray');
done();
}
});
it('throws an error if emails are not passed in, and uuid array is empty', (done) => {
try {
Group.validateInvitations([], null, res);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('inviteMissingUuid');
done();
}
});
it('throws an error if uuids are not passed in, and email array is empty', (done) => {
try {
Group.validateInvitations(null, [], res);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('inviteMissingEmail');
done();
}
});
it('throws an error if uuids and emails are passed in as empty arrays', (done) => {
try {
Group.validateInvitations([], [], res);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('inviteMustNotBeEmpty');
done();
}
});
it('throws an error if total invites exceed max invite constant', (done) => {
let uuids = [];
let emails = [];
for (let i = 0; i < INVITES_LIMIT / 2; i++) {
uuids.push(`user-id-${i}`);
emails.push(`user-${i}@example.com`);
}
uuids.push('one-more-uuid'); // to put it over the limit
try {
Group.validateInvitations(uuids, emails, res);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('canOnlyInviteMaxInvites', {maxInvites: INVITES_LIMIT });
done();
}
});
it('does not throw error if number of invites matches max invite limit', () => {
let uuids = [];
let emails = [];
for (let i = 0; i < INVITES_LIMIT / 2; i++) {
uuids.push(`user-id-${i}`);
emails.push(`user-${i}@example.com`);
}
expect(function () {
Group.validateInvitations(uuids, emails, res);
}).to.not.throw();
});
it('does not throw an error if only user ids are passed in', () => {
expect(function () {
Group.validateInvitations(['user-id', 'user-id2'], null, res);
}).to.not.throw();
expect(res.t).to.not.be.called;
});
it('does not throw an error if only emails are passed in', () => {
expect(function () {
Group.validateInvitations(null, ['user1@example.com', 'user2@example.com'], res);
}).to.not.throw();
expect(res.t).to.not.be.called;
});
it('does not throw an error if both uuids and emails are passed in', () => {
expect(function () {
Group.validateInvitations(['user-id', 'user-id2'], ['user1@example.com', 'user2@example.com'], res);
}).to.not.throw();
expect(res.t).to.not.be.called;
});
it('does not throw an error if uuids are passed in and emails are an empty array', () => {
expect(function () {
Group.validateInvitations(['user-id', 'user-id2'], [], res);
}).to.not.throw();
expect(res.t).to.not.be.called;
});
it('does not throw an error if emails are passed in and uuids are an empty array', () => {
expect(function () {
Group.validateInvitations([], ['user1@example.com', 'user2@example.com'], res);
}).to.not.throw();
expect(res.t).to.not.be.called;
});
});
});
context('Instance Methods', () => {
@@ -1064,5 +1219,163 @@ describe('Group Model', () => {
});
});
});
describe('sendGroupChatReceivedWebhooks', () => {
beforeEach(() => {
sandbox.stub(groupChatReceivedWebhook, 'send');
});
it('looks for users in specified guild with webhooks', () => {
sandbox.spy(User, 'find');
let guild = new Group({
type: 'guild',
});
guild.sendGroupChatReceivedWebhooks({});
expect(User.find).to.be.calledWith({
webhooks: {
$elemMatch: {
type: 'groupChatReceived',
'options.groupId': guild._id,
},
},
guilds: guild._id,
});
});
it('looks for users in specified party with webhooks', () => {
sandbox.spy(User, 'find');
party.sendGroupChatReceivedWebhooks({});
expect(User.find).to.be.calledWith({
webhooks: {
$elemMatch: {
type: 'groupChatReceived',
'options.groupId': party._id,
},
},
'party._id': party._id,
});
});
it('sends webhooks for users with webhooks', async () => {
let guild = new Group({
name: 'some guild',
type: 'guild',
});
let chat = {message: 'text'};
let memberWithWebhook = new User({
guilds: [guild._id],
webhooks: [{
type: 'groupChatReceived',
url: 'http://someurl.com',
options: {
groupId: guild._id,
},
}],
});
let memberWithoutWebhook = new User({
guilds: [guild._id],
});
let nonMemberWithWebhooks = new User({
webhooks: [{
type: 'groupChatReceived',
url: 'http://a-different-url.com',
options: {
groupId: generateUUID(),
},
}],
});
await Promise.all([
memberWithWebhook.save(),
memberWithoutWebhook.save(),
nonMemberWithWebhooks.save(),
]);
guild.leader = memberWithWebhook._id;
await guild.save();
guild.sendGroupChatReceivedWebhooks(chat);
await sleep();
expect(groupChatReceivedWebhook.send).to.be.calledOnce;
let args = groupChatReceivedWebhook.send.args[0];
let webhooks = args[0];
let options = args[1];
expect(webhooks).to.have.a.lengthOf(1);
expect(webhooks[0].id).to.eql(memberWithWebhook.webhooks[0].id);
expect(options.group).to.eql(guild);
expect(options.chat).to.eql(chat);
});
it('sends webhooks for each user with webhooks in group', async () => {
let guild = new Group({
name: 'some guild',
type: 'guild',
});
let chat = {message: 'text'};
let memberWithWebhook = new User({
guilds: [guild._id],
webhooks: [{
type: 'groupChatReceived',
url: 'http://someurl.com',
options: {
groupId: guild._id,
},
}],
});
let memberWithWebhook2 = new User({
guilds: [guild._id],
webhooks: [{
type: 'groupChatReceived',
url: 'http://another-member.com',
options: {
groupId: guild._id,
},
}],
});
let memberWithWebhook3 = new User({
guilds: [guild._id],
webhooks: [{
type: 'groupChatReceived',
url: 'http://a-third-member.com',
options: {
groupId: guild._id,
},
}],
});
await Promise.all([
memberWithWebhook.save(),
memberWithWebhook2.save(),
memberWithWebhook3.save(),
]);
guild.leader = memberWithWebhook._id;
await guild.save();
guild.sendGroupChatReceivedWebhooks(chat);
await sleep();
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;
});
});
});
});

View File

@@ -40,7 +40,7 @@ describe('User Model', () => {
expect(userToJSON.stats.maxHealth).to.not.exist;
expect(userToJSON.stats.toNextLevel).to.not.exist;
user.addComputedStatsToJSONObj(userToJSON);
user.addComputedStatsToJSONObj(userToJSON.stats);
expect(userToJSON.stats.maxMP).to.exist;
expect(userToJSON.stats.maxHealth).to.equal(common.maxHealth);

View File

@@ -0,0 +1,146 @@
import { model as Webhook } from '../../../../../website/server/models/webhook';
import { BadRequest } from '../../../../../website/server/libs/errors';
import { v4 as generateUUID } from 'uuid';
describe('Webhook Model', () => {
context('Instance Methods', () => {
describe('#formatOptions', () => {
let res;
beforeEach(() => {
res = {
t: sandbox.spy(),
};
});
context('type is taskActivity', () => {
let config;
beforeEach(() => {
config = {
type: 'taskActivity',
url: 'https//exmaple.com/endpoint',
options: {
created: true,
updated: true,
deleted: true,
scored: true,
},
};
});
it('it provides default values for options', () => {
delete config.options;
let wh = new Webhook(config);
wh.formatOptions(res);
expect(wh.options).to.eql({
created: false,
updated: false,
deleted: false,
scored: true,
});
});
it('provides missing task options', () => {
delete config.options.created;
let wh = new Webhook(config);
wh.formatOptions(res);
expect(wh.options).to.eql({
created: false,
updated: true,
deleted: true,
scored: 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({
created: true,
updated: true,
deleted: true,
scored: true,
});
});
['created', 'updated', 'deleted', 'scored'].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 groupChatReceived', () => {
let config;
beforeEach(() => {
config = {
type: 'groupChatReceived',
url: 'https//exmaple.com/endpoint',
options: {
groupId: generateUUID(),
},
};
});
it('creates options', () => {
let wh = new Webhook(config);
wh.formatOptions(res);
expect(wh.options).to.eql(config.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({
groupId: config.options.groupId,
});
});
it('requires groupId option to be a uuid', (done) => {
config.options.groupId = 'not a uuid';
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('groupIdRequired');
done();
}
});
});
});
});
});

View File

@@ -1,12 +1,16 @@
'use strict';
describe('Auth Controller', function() {
var scope, ctrl, user, $httpBackend, $window, $modal;
var scope, ctrl, user, $httpBackend, $window, $modal, alert, Auth;
beforeEach(function(){
module(function($provide) {
Auth = {
runAuth: sandbox.spy(),
};
$provide.value('Analytics', analyticsMock);
$provide.value('Chat', { seenMessage: function() {} });
$provide.value('Auth', Auth);
});
inject(function(_$httpBackend_, $rootScope, $controller, _$modal_) {
@@ -17,27 +21,27 @@ describe('Auth Controller', function() {
$window = { location: { href: ""}, alert: sandbox.spy() };
$modal = _$modal_;
user = { user: {}, authenticate: sandbox.spy() };
alert = { authErrorAlert: sandbox.spy() };
ctrl = $controller('AuthCtrl', {$scope: scope, $window: $window, User: user});
ctrl = $controller('AuthCtrl', {$scope: scope, $window: $window, User: user, Alert: alert});
})
});
describe('logging in', function() {
it('should log in users with correct uname / pass', function() {
$httpBackend.expectPOST('/api/v3/user/auth/local/login').respond({data: {id: 'abc', apiToken: 'abc'}});
scope.auth();
$httpBackend.flush();
expect(user.authenticate).to.be.calledOnce;
expect($window.alert).to.not.be.called;
expect(Auth.runAuth).to.be.calledOnce;
expect(alert.authErrorAlert).to.not.be.called;
});
it('should not log in users with incorrect uname / pass', function() {
$httpBackend.expectPOST('/api/v3/user/auth/local/login').respond(404, '');
scope.auth();
$httpBackend.flush();
expect(user.authenticate).to.not.be.called;
expect($window.alert).to.be.calledOnce;
expect(Auth.runAuth).to.not.be.called;
expect(alert.authErrorAlert).to.be.calledOnce;
});
});

View File

@@ -13,7 +13,7 @@ describe('Footer Controller', function() {
user: user
};
scope = $rootScope.$new();
$controller('FooterCtrl', {$scope: scope, User: User});
$controller('FooterCtrl', {$scope: scope, User: User, Social: {}});
}));
context('Debug mode', function() {

View File

@@ -193,6 +193,7 @@ describe('Analytics Service', function () {
todos: 1,
rewards: 1
};
expectedProperties.balance = 12;
beforeEach(function() {
user._id = 'unique-user-id';
@@ -207,6 +208,7 @@ describe('Analytics Service', function () {
user.dailys = [{_id: 'daily'}];
user.todos = [{_id: 'todo'}];
user.rewards = [{_id: 'reward'}];
user.balance = 12;
analytics.updateUser(properties);
clock.tick();
@@ -240,7 +242,8 @@ describe('Analytics Service', function () {
dailys: 1,
habits: 1,
rewards: 1
}
},
balance: 12
};
beforeEach(function() {
@@ -258,6 +261,7 @@ describe('Analytics Service', function () {
user.dailys = [{_id: 'daily'}];
user.todos = [{_id: 'todo'}];
user.rewards = [{_id: 'reward'}];
user.balance = 12;
analytics.updateUser();
clock.tick();

5
test/client/.babelrc Normal file
View File

@@ -0,0 +1,5 @@
{
"presets": ["es2015"],
"plugins": ["transform-object-rest-spread"],
"comments": false
}

View File

@@ -6,6 +6,6 @@ require('babel-polyfill');
var testsContext = require.context('./specs', true, /\.spec$/);
testsContext.keys().forEach(testsContext);
// require all src files except main.js/ README.md / index.html for coverage.
var srcContext = require.context('../../../website/client', true, /^\.\/(?!(main(\.js)?)|(index(\.html)?)$)/);
// require all .vue and .js files except main.js for coverage.
var srcContext = require.context('../../../website/client', true, /^\.\/(?=(?!main(\.js)?$))(?=(.*\.(vue|js)$))/);
srcContext.keys().forEach(srcContext);

View File

@@ -1,16 +0,0 @@
import Vue from 'vue';
// import Hello from 'src/components/Hello';
describe('Hello.vue', () => {
xit('should render correct contents', () => {
const vm = new Vue({
el: document.createElement('div'),
render: (h) => h(Hello),
});
expect(vm.$el.querySelector('.hello h1').textContent).to.equal('Hello Vue!');
});
it('should make assertions', () => {
expect(true).to.equal(true);
});
});

View File

@@ -0,0 +1,120 @@
import Vue from 'vue';
import storeInjector from 'inject?-vue!client/store';
import { mapState, mapGetters, mapActions } from 'client/store';
describe('Store', () => {
let injectedStore;
beforeEach(() => {
injectedStore = storeInjector({ // eslint-disable-line babel/new-cap
'./state': {
name: 'test',
},
'./getters': {
computedName ({ state }) {
return `${state.name} computed!`;
},
},
'./actions': {
getName ({ state }, ...args) {
return [state.name, ...args];
},
},
}).default;
});
it('injects itself in all component', (done) => {
new Vue({ // eslint-disable-line no-new
created () {
expect(this.$store).to.equal(injectedStore);
done();
},
});
});
it('can watch a function on the state', (done) => {
injectedStore.watch(state => state.name, (newName) => {
expect(newName).to.equal('test updated');
done();
});
injectedStore.state.name = 'test updated';
});
it('supports getters', () => {
expect(injectedStore.getters.computedName).to.equal('test computed!');
injectedStore.state.name = 'test updated';
expect(injectedStore.getters.computedName).to.equal('test updated computed!');
});
describe('actions', () => {
it('can be dispatched', () => {
expect(injectedStore.dispatch('getName', 1, 2, 3)).to.deep.equal(['test', 1, 2, 3]);
});
it('throws an error is the action doesn\'t exists', () => {
expect(() => injectedStore.dispatched('wrong')).to.throw;
});
});
describe('helpers', () => {
it('mapState', (done) => {
new Vue({ // eslint-disable-line no-new
data: {
title: 'internal',
},
computed: {
...mapState(['name']),
...mapState({
nameComputed (state, getters) {
return `${this.title} ${getters.computedName} ${state.name}`;
},
}),
},
created () {
expect(this.name).to.equal('test');
expect(this.nameComputed).to.equal('internal test computed! test');
done();
},
});
});
it('mapGetters', (done) => {
new Vue({ // eslint-disable-line no-new
data: {
title: 'internal',
},
computed: {
...mapGetters(['computedName']),
...mapGetters({
nameComputedTwice: 'computedName',
}),
},
created () {
expect(this.computedName).to.equal('test computed!');
expect(this.nameComputedTwice).to.equal('test computed!');
done();
},
});
});
it('mapActions', (done) => {
new Vue({ // eslint-disable-line no-new
data: {
title: 'internal',
},
methods: {
...mapActions(['getName']),
...mapActions({
getNameRenamed: 'getName',
}),
},
created () {
expect(this.getName('123')).to.deep.equal(['test', '123']);
expect(this.getNameRenamed('123')).to.deep.equal(['test', '123']);
done();
},
});
});
});
});

View File

@@ -16,21 +16,20 @@ describe('common.fns.randomDrop', () => {
user = generateUser();
user._tmp = user._tmp ? user._tmp : {};
task = generateTodo({ userId: user._id });
predictableRandom = () => {
return 0.5;
};
predictableRandom = sandbox.stub().returns(0.5);
});
it('drops an item for the user.party.quest.progress', () => {
expect(user.party.quest.progress.collectedItems).to.eql(0);
user.party.quest.key = 'vice2';
predictableRandom = () => {
return 0.0001;
};
predictableRandom.returns(0.0001);
randomDrop(user, { task, predictableRandom });
expect(user.party.quest.progress.collectedItems).to.eql(1);
expect(user._tmp.quest.collection).to.eql(1);
randomDrop(user, { task, predictableRandom });
expect(user.party.quest.progress.collectedItems).to.eql(2);
expect(user._tmp.quest.collection).to.eql(1);
});
context('drops enabled', () => {
@@ -42,15 +41,14 @@ describe('common.fns.randomDrop', () => {
it('does nothing if user.items.lastDrop.count is exceeded', () => {
user.items.lastDrop.count = 100;
randomDrop(user, { task, predictableRandom });
expect(user._tmp).to.eql({});
expect(user._tmp.drop).to.be.undefined;
});
it('drops something when the task is a todo', () => {
expect(user._tmp).to.eql({});
user.flags.dropsEnabled = true;
predictableRandom = () => {
return 0.1;
};
predictableRandom.returns(0.1);
randomDrop(user, { task, predictableRandom });
expect(user._tmp).to.not.eql({});
});
@@ -59,9 +57,8 @@ describe('common.fns.randomDrop', () => {
task = generateHabit({ userId: user._id });
expect(user._tmp).to.eql({});
user.flags.dropsEnabled = true;
predictableRandom = () => {
return 0.1;
};
predictableRandom.returns(0.1);
randomDrop(user, { task, predictableRandom });
expect(user._tmp).to.not.eql({});
});
@@ -70,9 +67,8 @@ describe('common.fns.randomDrop', () => {
task = generateDaily({ userId: user._id });
expect(user._tmp).to.eql({});
user.flags.dropsEnabled = true;
predictableRandom = () => {
return 0.1;
};
predictableRandom.returns(0.1);
randomDrop(user, { task, predictableRandom });
expect(user._tmp).to.not.eql({});
});
@@ -81,34 +77,30 @@ describe('common.fns.randomDrop', () => {
task = generateReward({ userId: user._id });
expect(user._tmp).to.eql({});
user.flags.dropsEnabled = true;
predictableRandom = () => {
return 0.1;
};
predictableRandom.returns(0.1);
randomDrop(user, { task, predictableRandom });
expect(user._tmp).to.not.eql({});
});
it('drops food', () => {
predictableRandom = () => {
return 0.65;
};
predictableRandom.returns(0.65);
randomDrop(user, { task, predictableRandom });
expect(user._tmp.drop.type).to.eql('Food');
});
it('drops eggs', () => {
predictableRandom = () => {
return 0.35;
};
predictableRandom.returns(0.35);
randomDrop(user, { task, predictableRandom });
expect(user._tmp.drop.type).to.eql('Egg');
});
context('drops hatching potion', () => {
it('drops a very rare potion', () => {
predictableRandom = () => {
return 0.01;
};
predictableRandom.returns(0.01);
randomDrop(user, { task, predictableRandom });
expect(user._tmp.drop.type).to.eql('HatchingPotion');
expect(user._tmp.drop.value).to.eql(5);
@@ -116,9 +108,8 @@ describe('common.fns.randomDrop', () => {
});
it('drops a rare potion', () => {
predictableRandom = () => {
return 0.08;
};
predictableRandom.returns(0.08);
randomDrop(user, { task, predictableRandom });
expect(user._tmp.drop.type).to.eql('HatchingPotion');
expect(user._tmp.drop.value).to.eql(4);
@@ -127,9 +118,8 @@ describe('common.fns.randomDrop', () => {
});
it('drops an uncommon potion', () => {
predictableRandom = () => {
return 0.17;
};
predictableRandom.returns(0.17);
randomDrop(user, { task, predictableRandom });
expect(user._tmp.drop.type).to.eql('HatchingPotion');
expect(user._tmp.drop.value).to.eql(3);
@@ -138,9 +128,8 @@ describe('common.fns.randomDrop', () => {
});
it('drops a common potion', () => {
predictableRandom = () => {
return 0.20;
};
predictableRandom.returns(0.20);
randomDrop(user, { task, predictableRandom });
expect(user._tmp.drop.type).to.eql('HatchingPotion');
expect(user._tmp.drop.value).to.eql(2);

View File

@@ -1,119 +0,0 @@
import randomVal from '../../../website/common/script/fns/randomVal';
import {
generateUser,
} from '../../helpers/common.helper';
describe('shared.fns.randomVal', () => {
let user;
let obj = {
a: 1,
b: 2,
c: 3,
d: 4,
};
beforeEach(() => {
user = generateUser();
});
describe('returns a random property value from an object', () => {
it('returns the same value when the seed is the same', () => {
let val1 = randomVal(user, obj, {
seed: 222,
});
let val2 = randomVal(user, obj, {
seed: 222,
});
expect(val2).to.equal(val1);
});
it('returns the same value when user.stats is the same', () => {
user.stats.gp = 34;
let val1 = randomVal(user, obj);
let val2 = randomVal(user, obj);
expect(val2).to.equal(val1);
});
it('returns a different value when the seed is different', () => {
let val1 = randomVal(user, obj, {
seed: 222,
});
let val2 = randomVal(user, obj, {
seed: 333,
});
expect(val2).to.not.equal(val1);
});
it('returns a different value when user.stats is different', () => {
user.stats.gp = 34;
let val1 = randomVal(user, obj);
user.stats.gp = 343;
let val2 = randomVal(user, obj);
expect(val2).to.not.equal(val1);
});
});
describe('returns a random key from an object', () => {
it('returns the same key when the seed is the same', () => {
let key1 = randomVal(user, obj, {
key: true,
seed: 222,
});
let key2 = randomVal(user, obj, {
key: true,
seed: 222,
});
expect(key2).to.equal(key1);
});
it('returns the same key when user.stats is the same', () => {
user.stats.gp = 45;
let key1 = randomVal(user, obj, {
key: true,
});
let key2 = randomVal(user, obj, {
key: true,
});
expect(key2).to.equal(key1);
});
it('returns a different key when the seed is different', () => {
let key1 = randomVal(user, obj, {
key: true,
seed: 222,
});
let key2 = randomVal(user, obj, {
key: true,
seed: 333,
});
expect(key2).to.not.equal(key1);
});
it('returns a different key when user.stats is different', () => {
user.stats.gp = 45;
let key1 = randomVal(user, obj, {
key: true,
});
user.stats.gp = 43;
let key2 = randomVal(user, obj, {
key: true,
});
expect(key2).to.not.equal(key1);
});
});
});

View File

@@ -38,7 +38,8 @@ describe('shared.fns.ultimateGear', () => {
expect(user.addNotification).to.be.calledWith('ULTIMATE_GEAR_ACHIEVEMENT');
});
it('does not set armoirEnabled when gear is not owned', () => {
it('does not set armoireEnabled when gear is not owned', () => {
user.flags.armoireEnabled = false;
let items = {
gear: {
owned: {

View File

@@ -0,0 +1,37 @@
import randomVal from '../../../website/common/script/libs/randomVal';
import {times} from 'lodash';
describe('randomVal', () => {
let obj;
beforeEach(() => {
obj = {
a: 1,
b: 2,
c: 3,
d: 4,
};
});
afterEach(() => {
sandbox.restore();
});
it('returns a random value from an object', () => {
let result = randomVal(obj);
expect(result).to.be.oneOf([1, 2, 3, 4]);
});
it('can pass in a predictable random value', () => {
times(30, () => {
expect(randomVal(obj, {
predictableRandom: 0.3,
})).to.equal(2);
});
});
it('returns a random key when the key option is passed in', () => {
let result = randomVal(obj, { key: true });
expect(result).to.be.oneOf(['a', 'b', 'c', 'd']);
});
});

View File

@@ -22,7 +22,9 @@ describe('shops', () => {
it('items contain required fields', () => {
_.each(shopCategories, (category) => {
_.each(category.items, (item) => {
expect(item).to.have.all.keys(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'class']);
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'class'], (key) => {
expect(_.has(item, key)).to.eql(true);
});
});
});
});
@@ -46,7 +48,9 @@ describe('shops', () => {
it('items contain required fields', () => {
_.each(shopCategories, (category) => {
_.each(category.items, (item) => {
expect(item).to.have.all.keys('key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'boss', 'class', 'collect', 'drop', 'unlockCondition', 'lvl');
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'boss', 'class', 'collect', 'drop', 'unlockCondition', 'lvl'], (key) => {
expect(_.has(item, key)).to.eql(true);
});
});
});
});
@@ -70,7 +74,9 @@ describe('shops', () => {
it('items contain required fields', () => {
_.each(shopCategories, (category) => {
_.each(category.items, (item) => {
expect(item).to.have.all.keys('key', 'text', 'value', 'currency', 'locked', 'purchaseType', 'class', 'notes', 'class');
_.each(['key', 'text', 'value', 'currency', 'locked', 'purchaseType', 'class', 'notes', 'class'], (key) => {
expect(_.has(item, key)).to.eql(true);
});
});
});
});
@@ -94,7 +100,9 @@ describe('shops', () => {
it('items contain required fields', () => {
_.each(shopCategories, (category) => {
_.each(category.items, (item) => {
expect(item).to.have.all.keys('key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'specialClass', 'type');
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'type'], (key) => {
expect(_.has(item, key)).to.eql(true);
});
});
});
});

View File

@@ -1,57 +0,0 @@
import addWebhook from '../../../website/common/script/ops/addWebhook';
import {
BadRequest,
} from '../../../website/common/script/libs/errors';
import i18n from '../../../website/common/script/i18n';
import {
generateUser,
} from '../../helpers/common.helper';
describe('shared.ops.addWebhook', () => {
let user;
let req;
beforeEach(() => {
user = generateUser();
req = { body: {
enabled: true,
url: 'http://some-url.com',
} };
});
context('adds webhook', () => {
it('validates req.body.url', (done) => {
delete req.body.url;
try {
addWebhook(user, req);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidUrl'));
done();
}
});
it('validates req.body.enabled', (done) => {
delete req.body.enabled;
try {
addWebhook(user, req);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidEnabled'));
done();
}
});
it('calls marksModified()', () => {
user.markModified = sinon.spy();
addWebhook(user, req);
expect(user.markModified.called).to.eql(true);
});
it('succeeds', () => {
expect(user.preferences.webhooks).to.eql({});
addWebhook(user, req);
expect(user.preferences.webhooks).to.not.eql({});
});
});
});

View File

@@ -1,24 +1,18 @@
/* eslint-disable camelcase */
import sinon from 'sinon'; // eslint-disable-line no-shadow
import {
generateUser,
} from '../../helpers/common.helper';
import count from '../../../website/common/script/count';
import buyArmoire from '../../../website/common/script/ops/buyArmoire';
import shared from '../../../website/common/script';
import randomVal from '../../../website/common/script/libs/randomVal';
import content from '../../../website/common/script/content/index';
import {
NotAuthorized,
} from '../../../website/common/script/libs/errors';
import i18n from '../../../website/common/script/i18n';
describe('shared.ops.buyArmoire', () => {
let user;
let YIELD_EQUIPMENT = 0.5;
let YIELD_FOOD = 0.7;
let YIELD_EXP = 0.9;
function getFullArmoire () {
let fullArmoire = {};
_(content.gearTypes).each((type) => {
@@ -29,39 +23,36 @@ describe('shared.ops.buyArmoire', () => {
}).value();
}).value();
return fullArmoire;
}
describe('shared.ops.buyArmoire', () => {
let user;
let YIELD_EQUIPMENT = 0.5;
let YIELD_FOOD = 0.7;
let YIELD_EXP = 0.9;
beforeEach(() => {
user = generateUser({
items: {
gear: {
owned: {
weapon_warrior_0: true,
},
equipped: {
weapon_warrior_0: true,
},
},
},
stats: { gp: 200 },
});
user.items.gear.owned = {
weapon_warrior_0: true,
};
user.achievements.ultimateGearSets = { rogue: true };
user.flags.armoireOpened = true;
user.stats.exp = 0;
user.items.food = {};
sinon.stub(shared.fns, 'randomVal');
sinon.stub(shared.fns, 'predictableRandom');
sandbox.stub(randomVal, 'trueRandom');
});
afterEach(() => {
shared.fns.randomVal.restore();
shared.fns.predictableRandom.restore();
randomVal.trueRandom.restore();
});
context('failure conditions', () => {
it('does not open if user does not have enough gold', (done) => {
shared.fns.predictableRandom.returns(YIELD_EQUIPMENT);
user.stats.gp = 50;
try {
@@ -71,13 +62,6 @@ describe('shared.ops.buyArmoire', () => {
expect(err.message).to.equal(i18n.t('messageNotEnoughGold'));
expect(user.items.gear.owned).to.eql({
weapon_warrior_0: true,
eyewear_special_blackTopFrame: true,
eyewear_special_blueTopFrame: true,
eyewear_special_greenTopFrame: true,
eyewear_special_pinkTopFrame: true,
eyewear_special_redTopFrame: true,
eyewear_special_whiteTopFrame: true,
eyewear_special_yellowTopFrame: true,
});
expect(user.items.food).to.be.empty;
expect(user.stats.exp).to.eql(0);
@@ -86,7 +70,6 @@ describe('shared.ops.buyArmoire', () => {
});
it('does not open without Ultimate Gear achievement', (done) => {
shared.fns.predictableRandom.returns(YIELD_EQUIPMENT);
user.achievements.ultimateGearSets = {healer: false, wizard: false, rogue: false, warrior: false};
try {
@@ -96,13 +79,6 @@ describe('shared.ops.buyArmoire', () => {
expect(err.message).to.equal(i18n.t('cannotBuyItem'));
expect(user.items.gear.owned).to.eql({
weapon_warrior_0: true,
eyewear_special_blackTopFrame: true,
eyewear_special_blueTopFrame: true,
eyewear_special_greenTopFrame: true,
eyewear_special_pinkTopFrame: true,
eyewear_special_redTopFrame: true,
eyewear_special_whiteTopFrame: true,
eyewear_special_yellowTopFrame: true,
});
expect(user.items.food).to.be.empty;
expect(user.stats.exp).to.eql(0);
@@ -112,93 +88,83 @@ describe('shared.ops.buyArmoire', () => {
});
context('non-gear awards', () => {
// Skipped because can't stub predictableRandom correctly
xit('gives Experience', () => {
shared.fns.predictableRandom.returns(YIELD_EXP);
it('gives Experience', () => {
let previousExp = user.stats.exp;
randomVal.trueRandom.returns(YIELD_EXP);
buyArmoire(user);
expect(user.items.gear.owned).to.eql({weapon_warrior_0: true});
expect(user.items.food).to.be.empty;
expect(user.stats.exp).to.eql(46);
expect(user.stats.gp).to.eql(100);
expect(user.stats.exp).to.be.greaterThan(previousExp);
expect(user.stats.gp).to.equal(100);
});
// Skipped because can't stub predictableRandom correctly
xit('gives food', () => {
let honey = content.food.Honey;
it('gives food', () => {
let previousExp = user.stats.exp;
shared.fns.randomVal.returns(honey);
shared.fns.predictableRandom.returns(YIELD_FOOD);
randomVal.trueRandom.returns(YIELD_FOOD);
buyArmoire(user);
expect(user.items.gear.owned).to.eql({weapon_warrior_0: true});
expect(user.items.food).to.eql({Honey: 1});
expect(user.stats.exp).to.eql(0);
expect(user.stats.gp).to.eql(100);
expect(user.items.food).to.not.be.empty;
expect(user.stats.exp).to.equal(previousExp);
expect(user.stats.gp).to.equal(100);
});
// Skipped because can't stub predictableRandom correctly
xit('does not give equipment if all equipment has been found', () => {
shared.fns.predictableRandom.returns(YIELD_EQUIPMENT);
user.items.gear.owned = fullArmoire;
it('does not give equipment if all equipment has been found', () => {
randomVal.trueRandom.returns(YIELD_EQUIPMENT);
user.items.gear.owned = getFullArmoire();
user.stats.gp = 150;
buyArmoire(user);
expect(user.items.gear.owned).to.eql(fullArmoire);
expect(user.items.gear.owned).to.eql(getFullArmoire());
let armoireCount = count.remainingGearInSet(user.items.gear.owned, 'armoire');
expect(armoireCount).to.eql(0);
expect(user.stats.exp).to.eql(30);
expect(user.stats.gp).to.eql(50);
expect(user.stats.gp).to.equal(50);
});
});
context('gear awards', () => {
beforeEach(() => {
let shield = content.gear.tree.shield.armoire.gladiatorShield;
shared.fns.randomVal.returns(shield);
});
// Skipped because can't stub predictableRandom correctly
xit('always drops equipment the first time', () => {
it('always drops equipment the first time', () => {
delete user.flags.armoireOpened;
shared.fns.predictableRandom.returns(YIELD_EXP);
randomVal.trueRandom.returns(YIELD_EXP);
expect(_.size(user.items.gear.owned)).to.equal(1);
buyArmoire(user);
expect(user.items.gear.owned).to.eql({
weapon_warrior_0: true,
shield_armoire_gladiatorShield: true,
});
expect(_.size(user.items.gear.owned)).to.equal(2);
let armoireCount = count.remainingGearInSet(user.items.gear.owned, 'armoire');
expect(armoireCount).to.eql(_.size(fullArmoire) - 1);
expect(armoireCount).to.eql(_.size(getFullArmoire()) - 1);
expect(user.items.food).to.be.empty;
expect(user.stats.exp).to.eql(0);
expect(user.stats.gp).to.eql(100);
expect(user.stats.exp).to.equal(0);
expect(user.stats.gp).to.equal(100);
});
// Skipped because can't stub predictableRandom correctly
xit('gives more equipment', () => {
shared.fns.predictableRandom.returns(YIELD_EQUIPMENT);
it('gives more equipment', () => {
randomVal.trueRandom.returns(YIELD_EQUIPMENT);
user.items.gear.owned = {
weapon_warrior_0: true,
head_armoire_hornedIronHelm: true,
};
user.stats.gp = 200;
expect(_.size(user.items.gear.owned)).to.equal(2);
buyArmoire(user);
expect(user.items.gear.owned).to.eql({weapon_warrior_0: true, shield_armoire_gladiatorShield: true, head_armoire_hornedIronHelm: true});
expect(_.size(user.items.gear.owned)).to.equal(3);
let armoireCount = count.remainingGearInSet(user.items.gear.owned, 'armoire');
expect(armoireCount).to.eql(_.size(fullArmoire) - 2);
expect(armoireCount).to.eql(_.size(getFullArmoire()) - 2);
expect(user.stats.gp).to.eql(100);
});
});

View File

@@ -29,12 +29,12 @@ describe('shared.ops.buyGear', () => {
stats: { gp: 200 },
});
sinon.stub(shared.fns, 'randomVal');
sinon.stub(shared, 'randomVal');
sinon.stub(shared.fns, 'predictableRandom');
});
afterEach(() => {
shared.fns.randomVal.restore();
shared.randomVal.restore();
shared.fns.predictableRandom.restore();
});

View File

@@ -1,21 +0,0 @@
import deleteWebhook from '../../../website/common/script/ops/deleteWebhook';
import {
generateUser,
} from '../../helpers/common.helper';
describe('shared.ops.deleteWebhook', () => {
let user;
let req;
beforeEach(() => {
user = generateUser();
req = { params: { id: 'some-id' } };
});
it('succeeds', () => {
user.preferences.webhooks = { 'some-id': {}, 'another-id': {} };
let [data] = deleteWebhook(user, req);
expect(user.preferences.webhooks).to.eql({'another-id': {}});
expect(data).to.equal(user.preferences.webhooks);
});
});

View File

@@ -1,4 +1,5 @@
import releaseBoth from '../../../website/common/script/ops/releaseBoth';
import content from '../../../website/common/script/content/index';
import i18n from '../../../website/common/script/i18n';
import {
generateUser,
@@ -65,19 +66,41 @@ describe('shared.ops.releaseBoth', () => {
expect(user.items.mounts[animal]).to.equal(null);
});
it('removes currentPet', () => {
it('removes drop currentPet', () => {
let petInfo = content.petInfo[user.items.currentPet];
expect(petInfo.type).to.equal('drop');
releaseBoth(user);
expect(user.items.currentMount).to.be.empty;
expect(user.items.currentPet).to.be.empty;
});
it('removes currentMount', () => {
it('removes drop currentMount', () => {
let mountInfo = content.mountInfo[user.items.currentMount];
expect(mountInfo.type).to.equal('drop');
releaseBoth(user);
expect(user.items.currentMount).to.be.empty;
});
it('leaves non-drop pets and mounts equipped', () => {
let questAnimal = 'Gryphon-Base';
user.items.currentMount = questAnimal;
user.items.currentPet = questAnimal;
user.items.pets[questAnimal] = 5;
user.items.mounts[questAnimal] = true;
let petInfo = content.petInfo[user.items.currentPet];
expect(petInfo.type).to.not.equal('drop');
let mountInfo = content.mountInfo[user.items.currentMount];
expect(mountInfo.type).to.not.equal('drop');
releaseBoth(user);
expect(user.items.currentMount).to.equal(questAnimal);
expect(user.items.currentPet).to.equal(questAnimal);
});
it('decreases user\'s balance', () => {
releaseBoth(user);

View File

@@ -1,4 +1,5 @@
import releaseMounts from '../../../website/common/script/ops/releaseMounts';
import content from '../../../website/common/script/content/index';
import i18n from '../../../website/common/script/i18n';
import {
generateUser,
@@ -37,12 +38,26 @@ describe('shared.ops.releaseMounts', () => {
expect(user.items.mounts[animal]).to.equal(null);
});
it('removes currentMount', () => {
it('removes drop currentMount', () => {
let mountInfo = content.mountInfo[user.items.currentMount];
expect(mountInfo.type).to.equal('drop');
releaseMounts(user);
expect(user.items.currentMount).to.be.empty;
});
it('leaves non-drop mount equipped', () => {
let questAnimal = 'Gryphon-Base';
user.items.currentMount = questAnimal;
user.items.mounts[questAnimal] = true;
let mountInfo = content.mountInfo[user.items.currentMount];
expect(mountInfo.type).to.not.equal('drop');
releaseMounts(user);
expect(user.items.currentMount).to.equal(questAnimal);
});
it('increases mountMasterCount achievement', () => {
releaseMounts(user);

View File

@@ -1,4 +1,5 @@
import releasePets from '../../../website/common/script/ops/releasePets';
import content from '../../../website/common/script/content/index';
import i18n from '../../../website/common/script/i18n';
import {
generateUser,
@@ -37,12 +38,26 @@ describe('shared.ops.releasePets', () => {
expect(user.items.pets[animal]).to.equal(0);
});
it('removes currentPet', () => {
it('removes drop currentPet', () => {
let petInfo = content.petInfo[user.items.currentPet];
expect(petInfo.type).to.equal('drop');
releasePets(user);
expect(user.items.currentPet).to.be.empty;
});
it('leaves non-drop pets equipped', () => {
let questAnimal = 'Gryphon-Base';
user.items.currentPet = questAnimal;
user.items.pets[questAnimal] = 5;
let petInfo = content.petInfo[user.items.currentPet];
expect(petInfo.type).to.not.equal('drop');
releasePets(user);
expect(user.items.currentPet).to.equal(questAnimal);
});
it('decreases user\'s balance', () => {
releasePets(user);

View File

@@ -53,6 +53,8 @@ describe('shared.ops.revive', () => {
expect(user.stats.str).to.equal(1);
});
it('TODO: test actual ways stats are affected');
it('removes a random item from user gear owned', () => {
let weaponKey = 'weapon_warrior_0';
user.items.gear.owned[weaponKey] = true;
@@ -63,7 +65,17 @@ describe('shared.ops.revive', () => {
expect(user.items.gear.owned[weaponKey]).to.be.false;
});
it('removes a random item from user gear equipped', () => {
it('does not remove 0 value items');
it('allows removing warrior sword (0 value item)');
it('does not remove items of a different class');
it('removes "special" items');
it('removes "armoire" items');
it('dequips lost item from user if user had it equipped', () => {
let weaponKey = 'weapon_warrior_0';
let itemToLose = content.gear.flat[weaponKey];
@@ -76,7 +88,7 @@ describe('shared.ops.revive', () => {
expect(user.items.gear.equipped[itemToLose.type]).to.equal(`${itemToLose.type}_base_0`);
});
it('removes a random item from user gear costume', () => {
it('dequips lost item from user costume if user was using it in costume', () => {
let weaponKey = 'weapon_warrior_0';
let itemToLose = content.gear.flat[weaponKey];

View File

@@ -142,6 +142,21 @@ describe('shared.ops.scoreTask', () => {
expect(ref.beforeUser._id).to.eql(ref.afterUser._id);
});
it('and increments quest progress', () => {
expect(ref.afterUser.party.quest.progress.up).to.eql(0);
ref.afterUser.party.quest.key = 'gryphon';
scoreTask({ user: ref.afterUser, task: habit, direction: 'up', cron: false });
let firstTaskDelta = ref.afterUser.party.quest.progress.up;
expect(firstTaskDelta).to.be.greaterThan(0);
expect(ref.afterUser._tmp.quest.progressDelta).to.eql(firstTaskDelta);
scoreTask({ user: ref.afterUser, task: habit, direction: 'up', cron: false });
let secondTaskDelta = ref.afterUser.party.quest.progress.up - firstTaskDelta;
expect(secondTaskDelta).to.be.greaterThan(0);
expect(ref.afterUser._tmp.quest.progressDelta).to.eql(secondTaskDelta);
});
context('habits', () => {
it('up', () => {
options = { user: ref.afterUser, task: habit, direction: 'up', times: 5, cron: false };

View File

@@ -1,42 +0,0 @@
import updateWebhook from '../../../website/common/script/ops/updateWebhook';
import {
BadRequest,
} from '../../../website/common/script/libs/errors';
import i18n from '../../../website/common/script/i18n';
import {
generateUser,
} from '../../helpers/common.helper';
describe('shared.ops.updateWebhook', () => {
let user;
let req;
let newUrl = 'http://new-url.com';
beforeEach(() => {
user = generateUser();
req = { params: {
id: 'this-id',
}, body: {
url: newUrl,
enabled: true,
} };
});
it('validates body', (done) => {
delete req.body.url;
try {
updateWebhook(user, req);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidUrl'));
done();
}
});
it('succeeds', () => {
let url = 'http://existing-url.com';
user.preferences.webhooks = { 'this-id': { url } };
updateWebhook(user, req);
expect(user.preferences.webhooks['this-id'].url).to.eql(newUrl);
});
});

View File

@@ -0,0 +1,70 @@
'use strict';
let express = require('express');
let uuid = require('uuid');
let bodyParser = require('body-parser');
let app = express();
let server = require('http').createServer(app);
const PORT = process.env.TEST_WEBHOOK_APP_PORT || 3099; // eslint-disable-line no-process-env
let webhookData = {};
app.use(bodyParser.urlencoded({
extended: true,
}));
app.use(bodyParser.json());
app.post('/webhooks/:id', function (req, res) {
let id = req.params.id;
if (!webhookData[id]) {
webhookData[id] = [];
}
webhookData[id].push(req.body);
res.status(200);
});
// Helps close down server from within mocha test
// See http://stackoverflow.com/a/37054753/2601552
let sockets = {};
server.on('connection', (socket) => {
let id = uuid.v4();
sockets[id] = socket;
socket.once('close', () => {
delete sockets[id];
});
});
function start () {
return new Promise((resolve) => {
server.listen(PORT, resolve);
});
}
function close () {
return new Promise((resolve) => {
server.close(resolve);
Object.keys(sockets).forEach((socket) => {
sockets[socket].end();
});
});
}
function getWebhookData (id) {
if (!webhookData[id]) {
return null;
}
return webhookData[id].pop();
}
module.exports = {
start,
close,
getWebhookData,
port: PORT,
};

View File

@@ -1,11 +1,13 @@
/* eslint-disable no-use-before-define */
// Import requester function, set it up for v2, export it
// Import requester function, set it up for v3, export it
import { requester } from '../requester';
requester.setApiVersion('v3');
export { requester };
export { translate } from '../translate';
import server from './external-server';
export { server };
export { translate } from '../../translate';
export { checkExistence, getProperty, resetHabiticaDB } from '../../mongo';
export * from './object-generators';
export { sleep } from '../../sleep';

View File

@@ -8,6 +8,7 @@ import {
RewardSchema,
TodoSchema,
} from '../../website/server/models/task';
export {translate} from './translate';
export function generateUser (options = {}) {
let user = new User(options).toObject();

View File

@@ -15,25 +15,3 @@ global.expect = chai.expect;
global.sinon = require('sinon');
global.sandbox = sinon.sandbox.create();
global.Promise = Bluebird;
import nconf from 'nconf';
import mongoose from 'mongoose';
//------------------------------
// Load nconf for unit tests
//------------------------------
if (process.env.LOAD_SERVER === '0') { // when the server is in a different process we simply connect to mongoose
require('../../website/server/libs/setupNconf')('./config.json');
// Use Q promises instead of mpromise in mongoose
mongoose.Promise = Bluebird;
mongoose.connect(nconf.get('TEST_DB_URI'));
} else { // When running tests and the server in the same process
require('../../website/server/libs/setupNconf')('./config.json.example');
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
// 1. nconf is already setup
// 2. we don't need clustering
require('../../website/server/server');
}

View File

@@ -1,4 +1,4 @@
export async function sleep (seconds) {
export async function sleep (seconds = 1) {
let milliseconds = seconds * 1000;
return new Promise((resolve) => {

View File

@@ -0,0 +1,21 @@
/* eslint-disable no-process-env */
import nconf from 'nconf';
import mongoose from 'mongoose';
import Bluebird from 'bluebird';
import setupNconf from '../../website/server/libs/setupNconf';
if (process.env.LOAD_SERVER === '0') { // when the server is in a different process we simply connect to mongoose
setupNconf('./config.json');
// Use Q promises instead of mpromise in mongoose
mongoose.Promise = Bluebird;
mongoose.connect(nconf.get('TEST_DB_URI'));
} else { // When running tests and the server in the same process
setupNconf('./config.json.example');
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
// 1. nconf is already setup
// 2. we don't need clustering
require('../../website/server/server'); // eslint-disable-line global-require
}

View File

@@ -1,13 +1,13 @@
import i18n from '../../../website/common/script/i18n';
i18n.translations = require('../../../website/server/libs/i18n').translations;
import i18n from '../../website/common/script/i18n';
i18n.translations = require('../../website/server/libs/i18n').translations;
const STRING_ERROR_MSG = 'Error processing the string. Please see Help > Report a Bug.';
const STRING_DOES_NOT_EXIST_MSG = /^String '.*' not found.$/;
// Use this to verify error messages returned by the server
// That way, if the translated string changes, the test
// will not break. NOTE: it checks against errors with string as well.
export function translate (key, variables) {
const STRING_ERROR_MSG = 'Error processing the string. Please see Help > Report a Bug.';
const STRING_DOES_NOT_EXIST_MSG = /^String '.*' not found.$/;
let translatedString = i18n.t(key, variables);
expect(translatedString).to.not.be.empty;

View File

@@ -1,467 +0,0 @@
var sinon = require('sinon');
var chai = require("chai")
chai.use(require("sinon-chai"))
var expect = chai.expect
var rewire = require('rewire');
describe('analytics', function() {
// Mocks
var amplitudeMock = sinon.stub();
var googleAnalyticsMock = sinon.stub();
var amplitudeTrack = sinon.stub().returns({
catch: function () { return true; }
});
var googleEvent = sinon.stub().returns({
send: function() { }
});
var googleItem = sinon.stub().returns({
send: function() { }
});
var googleTransaction = sinon.stub().returns({
item: googleItem
});
afterEach(function(){
amplitudeMock.reset();
amplitudeTrack.reset();
googleEvent.reset();
googleTransaction.reset();
googleItem.reset();
});
describe('init', function() {
var analytics = rewire('../../website/server/libs/api-v2/analytics');
it('throws an error if no options are passed in', function() {
expect(analytics).to.throw('No options provided');
});
it('registers amplitude with token', function() {
analytics.__set__('Amplitude', amplitudeMock);
var options = {
amplitudeToken: 'token'
};
analytics(options);
expect(amplitudeMock).to.be.calledOnce;
expect(amplitudeMock).to.be.calledWith('token');
});
it('registers google analytics with token', function() {
analytics.__set__('googleAnalytics', googleAnalyticsMock);
var options = {
googleAnalytics: 'token'
};
analytics(options);
expect(googleAnalyticsMock).to.be.calledOnce;
expect(googleAnalyticsMock).to.be.calledWith('token');
});
});
describe('track', function() {
var analyticsData, event_type;
var analytics = rewire('../../website/server/libs/api-v2/analytics');
var initializedAnalytics;
beforeEach(function() {
analytics.__set__('Amplitude', amplitudeMock);
initializedAnalytics = analytics({amplitudeToken: 'token'});
analytics.__set__('amplitude.track', amplitudeTrack);
analytics.__set__('ga.event', googleEvent);
event_type = 'Cron';
analyticsData = {
category: 'behavior',
uuid: 'unique-user-id',
resting: true,
cronCount: 5
}
});
context('Amplitude', function() {
it('tracks event in amplitude', function() {
initializedAnalytics.track(event_type, analyticsData);
expect(amplitudeTrack).to.be.calledOnce;
expect(amplitudeTrack).to.be.calledWith({
event_type: 'Cron',
user_id: 'unique-user-id',
platform: 'server',
event_properties: {
category: 'behavior',
resting: true,
cronCount: 5
}
});
});
it('uses a dummy user id if none is provided', function() {
delete analyticsData.uuid;
initializedAnalytics.track(event_type, analyticsData);
expect(amplitudeTrack).to.be.calledOnce;
expect(amplitudeTrack).to.be.calledWith({
event_type: 'Cron',
user_id: 'no-user-id-was-provided',
platform: 'server',
event_properties: {
category: 'behavior',
resting: true,
cronCount: 5
}
});
});
it('sends english item name for gear if itemKey is provided', function() {
analyticsData.itemKey = 'headAccessory_special_foxEars'
initializedAnalytics.track(event_type, analyticsData);
expect(amplitudeTrack).to.be.calledOnce;
expect(amplitudeTrack).to.be.calledWith({
event_type: 'Cron',
user_id: 'unique-user-id',
platform: 'server',
event_properties: {
itemKey: 'headAccessory_special_foxEars',
itemName: 'Fox Ears',
category: 'behavior',
resting: true,
cronCount: 5
}
});
});
it('sends english item name for egg if itemKey is provided', function() {
analyticsData.itemKey = 'Wolf'
initializedAnalytics.track(event_type, analyticsData);
expect(amplitudeTrack).to.be.calledOnce;
expect(amplitudeTrack).to.be.calledWith({
event_type: 'Cron',
user_id: 'unique-user-id',
platform: 'server',
event_properties: {
itemKey: 'Wolf',
itemName: 'Wolf Egg',
category: 'behavior',
resting: true,
cronCount: 5
}
});
});
it('sends english item name for food if itemKey is provided', function() {
analyticsData.itemKey = 'Cake_Skeleton'
initializedAnalytics.track(event_type, analyticsData);
expect(amplitudeTrack).to.be.calledOnce;
expect(amplitudeTrack).to.be.calledWith({
event_type: 'Cron',
user_id: 'unique-user-id',
platform: 'server',
event_properties: {
itemKey: 'Cake_Skeleton',
itemName: 'Bare Bones Cake',
category: 'behavior',
resting: true,
cronCount: 5
}
});
});
it('sends english item name for hatching potion if itemKey is provided', function() {
analyticsData.itemKey = 'Golden'
initializedAnalytics.track(event_type, analyticsData);
expect(amplitudeTrack).to.be.calledOnce;
expect(amplitudeTrack).to.be.calledWith({
event_type: 'Cron',
user_id: 'unique-user-id',
platform: 'server',
event_properties: {
itemKey: 'Golden',
itemName: 'Golden Hatching Potion',
category: 'behavior',
resting: true,
cronCount: 5
}
});
});
it('sends english item name for quest if itemKey is provided', function() {
analyticsData.itemKey = 'atom1'
initializedAnalytics.track(event_type, analyticsData);
expect(amplitudeTrack).to.be.calledOnce;
expect(amplitudeTrack).to.be.calledWith({
event_type: 'Cron',
user_id: 'unique-user-id',
platform: 'server',
event_properties: {
itemKey: 'atom1',
itemName: 'Attack of the Mundane, Part 1: Dish Disaster!',
category: 'behavior',
resting: true,
cronCount: 5
}
});
});
it('sends english item name for purchased spell if itemKey is provided', function() {
analyticsData.itemKey = 'seafoam'
initializedAnalytics.track(event_type, analyticsData);
expect(amplitudeTrack).to.be.calledOnce;
expect(amplitudeTrack).to.be.calledWith({
event_type: 'Cron',
user_id: 'unique-user-id',
platform: 'server',
event_properties: {
itemKey: 'seafoam',
itemName: 'Seafoam',
category: 'behavior',
resting: true,
cronCount: 5
}
});
});
it('sends user data if provided', function() {
var stats = { class: 'wizard', exp: 5, gp: 23, hp: 10, lvl: 4, mp: 30 };
var user = {
stats: stats,
contributor: { level: 1 },
purchased: { plan: { planId: 'foo-plan' } },
flags: {tour: {intro: -2}},
habits: [{_id: 'habit'}],
dailys: [{_id: 'daily'}],
todos: [{_id: 'todo'}],
rewards: [{_id: 'reward'}]
};
analyticsData.user = user;
initializedAnalytics.track(event_type, analyticsData);
expect(amplitudeTrack).to.be.calledOnce;
expect(amplitudeTrack).to.be.calledWith({
event_type: 'Cron',
user_id: 'unique-user-id',
platform: 'server',
event_properties: {
category: 'behavior',
resting: true,
cronCount: 5
},
user_properties: {
Class: 'wizard',
Experience: 5,
Gold: 23,
Health: 10,
Level: 4,
Mana: 30,
contributorLevel: 1,
subscription: 'foo-plan',
tutorialComplete: true,
"Number Of Tasks": {
todos: 1,
dailys: 1,
habits: 1,
rewards: 1
}
}
});
});
});
context('Google Analytics', function() {
it('tracks event in google analytics', function() {
initializedAnalytics.track(event_type, analyticsData);
expect(googleEvent).to.be.calledOnce;
expect(googleEvent).to.be.calledWith({
ec: 'behavior',
ea: 'Cron'
});
});
it('if itemKey property is provided, use as label', function() {
analyticsData.itemKey = 'some item';
initializedAnalytics.track(event_type, analyticsData);
expect(googleEvent).to.be.calledOnce;
expect(googleEvent).to.be.calledWith({
ec: 'behavior',
ea: 'Cron',
el: 'some item'
});
});
it('if gaLabel property is provided, use as label (overrides itemKey)', function() {
analyticsData.value = 'some value';
analyticsData.itemKey = 'some item';
analyticsData.gaLabel = 'some label';
initializedAnalytics.track(event_type, analyticsData);
expect(googleEvent).to.be.calledOnce;
expect(googleEvent).to.be.calledWith({
ec: 'behavior',
ea: 'Cron',
el: 'some label'
});
});
it('if goldCost property is provided, use as value', function() {
analyticsData.goldCost = 5;
initializedAnalytics.track(event_type, analyticsData);
expect(googleEvent).to.be.calledOnce;
expect(googleEvent).to.be.calledWith({
ec: 'behavior',
ea: 'Cron',
ev: 5
});
});
it('if gemCost property is provided, use as value (overrides goldCost)', function() {
analyticsData.gemCost = 7;
analyticsData.goldCost = 5;
initializedAnalytics.track(event_type, analyticsData);
expect(googleEvent).to.be.calledOnce;
expect(googleEvent).to.be.calledWith({
ec: 'behavior',
ea: 'Cron',
ev: 7
});
});
it('if gaValue property is provided, use as value (overrides gemCost)', function() {
analyticsData.gemCost = 7;
analyticsData.gaValue = 5;
initializedAnalytics.track(event_type, analyticsData);
expect(googleEvent).to.be.calledOnce;
expect(googleEvent).to.be.calledWith({
ec: 'behavior',
ea: 'Cron',
ev: 5
});
});
});
});
describe('trackPurchase', function() {
var purchaseData;
var analytics = rewire('../../website/server/libs/api-v2/analytics');
var initializedAnalytics;
beforeEach(function() {
analytics.__set__('Amplitude', amplitudeMock);
initializedAnalytics = analytics({amplitudeToken: 'token', googleAnalytics: 'token'});
analytics.__set__('amplitude.track', amplitudeTrack);
analytics.__set__('ga.event', googleEvent);
analytics.__set__('ga.transaction', googleTransaction);
purchaseData = {
uuid: 'user-id',
sku: 'paypal-checkout',
paymentMethod: 'PayPal',
itemPurchased: 'Gems',
purchaseValue: 8,
purchaseType: 'checkout',
gift: false,
quantity: 1
}
});
context('Amplitude', function() {
it('calls amplitude.track', function() {
initializedAnalytics.trackPurchase(purchaseData);
expect(amplitudeTrack).to.be.calledOnce;
expect(amplitudeTrack).to.be.calledWith({
event_type: 'purchase',
user_id: 'user-id',
platform: 'server',
event_properties: {
paymentMethod: 'PayPal',
sku: 'paypal-checkout',
gift: false,
itemPurchased: 'Gems',
purchaseType: 'checkout',
quantity: 1
},
revenue: 8
});
});
});
context('Google Analytics', function() {
it('calls ga.event', function() {
initializedAnalytics.trackPurchase(purchaseData);
expect(googleEvent).to.be.calledOnce;
expect(googleEvent).to.be.calledWith({
ec: 'commerce',
ea: 'checkout',
el: 'PayPal',
ev: 8
});
});
it('calls ga.transaction', function() {
initializedAnalytics.trackPurchase(purchaseData);
expect(googleTransaction).to.be.calledOnce;
expect(googleTransaction).to.be.calledWith(
'user-id',
8
);
expect(googleItem).to.be.calledOnce;
expect(googleItem).to.be.calledWith(
8,
1,
'paypal-checkout',
'Gems',
'checkout'
);
});
it('appends gift to variation of ga.transaction.item if gift is true', function() {
purchaseData.gift = true;
initializedAnalytics.trackPurchase(purchaseData);
expect(googleItem).to.be.calledOnce;
expect(googleItem).to.be.calledWith(
8,
1,
'paypal-checkout',
'Gems',
'checkout - Gift'
);
});
});
});
});

View File

@@ -1,497 +0,0 @@
var sinon = require('sinon');
var chai = require("chai");
chai.use(require("sinon-chai"));
var expect = chai.expect;
var Bluebird = require('bluebird');
var Group = require('../../../website/server/models/group').model;
var groupsController = require('../../../website/server/controllers/api-v2/groups');
describe('Groups Controller', function() {
var utils = require('../../../website/server/libs/api-v2/utils');
describe('#invite', function() {
var res, req, user, group;
beforeEach(function() {
group = {
_id: 'group-id',
name: 'group-name',
type: 'party',
members: [
'user-id',
'another-user'
],
save: sinon.stub().yields(),
markModified: sinon.spy()
};
user = {
_id: 'user-id',
name: 'inviter',
email: 'inviter@example.com',
save: sinon.stub().yields(),
markModified: sinon.spy()
};
res = {
locals: {
group: group,
user: user
},
json: sinon.stub(),
sendStatus: sinon.stub()
};
req = {
body: {}
};
});
context('uuids', function() {
beforeEach(function() {
req.body.uuids = ['invited-user'];
});
it('returns 400 if user not found');
it('returns a 400 if user is already in the group');
it('retuns 400 if user was already invited to that group');
it('returns 400 if user is already pending an invitation');
it('returns 400 is user is already in another party');
it('emails invited user');
it('does not email invited user if email preference is set to false');
});
context('emails', function() {
var EmailUnsubscription = require('../../../website/server/models/emailUnsubscription').model;
var execStub, selectStub;
beforeEach(function() {
sinon.stub(utils, 'encrypt').returns('http://link.com');
sinon.stub(utils, 'getUserInfo').returns({
name: user.name,
email: user.email
});
execStub = sinon.stub();
selectStub = sinon.stub().returns({
exec: execStub
});
sinon.stub(User, 'findOne').returns({
select: selectStub
});
sinon.stub(EmailUnsubscription, 'findOne');
sinon.stub(utils, 'txnEmail');
req.body.emails = [{email: 'user@example.com', name: 'user'}];
});
afterEach(function() {
User.findOne.restore();
EmailUnsubscription.findOne.restore();
utils.encrypt.restore();
utils.getUserInfo.restore();
utils.txnEmail.restore();
});
it('emails user with invite', function() {
execStub.yields(null, null);
EmailUnsubscription.findOne.yields(null, null);
groupsController.invite(req, res);
expect(utils.txnEmail).to.be.calledOnce;
expect(utils.txnEmail).to.be.calledWith(
{ email: 'user@example.com', name: 'user' },
'invite-friend',
[
{ name: 'LINK', content: '?partyInvite=http://link.com' },
{ name: 'INVITER', content: 'inviter' }
]
);
});
it('does not email user if user is on unsubscribe list', function() {
EmailUnsubscription.findOne.yields(null, {_id: 'on-list'});
expect(utils.txnEmail).to.not.be.called;
});
it('checks if a user with provided email already exists');
});
context('others', function() {
it ('returns a 400 error', function() {
groupsController.invite(req, res);
expect(res.json).to.be.calledOnce;
expect(res.json).to.be.calledWith(
400,
{ err: 'Can invite only by email or uuid' }
);
});
});
});
describe('#leave', function() {
var res, req, user, group;
beforeEach(function() {
group = {
_id: 'group-id',
type: 'party',
members: [
'user-id',
'another-user'
],
save: sinon.stub().yields(),
leave: sinon.stub().yields(),
markModified: sinon.spy()
};
user = {
_id: 'user-id',
save: sinon.stub().yields(),
markModified: sinon.spy()
};
res = {
locals: {
group: group,
user: user
},
json: sinon.stub(),
sendStatus: sinon.stub()
};
req = {
query: { keep: 'keep' }
};
});
context('party', function() {
beforeEach(function() {
group.type = 'party';
});
it('prevents user from leaving party if quest is active and part of the active members list', function() {
group.quest = {
active: true,
members: {
another_user: true,
yet_another_user: null,
'user-id': true
}
};
groupsController.leave(req, res);
expect(group.leave).to.not.be.called;
expect(res.json).to.be.calledOnce;
expect(res.json).to.be.calledWith(403, 'You cannot leave party during an active quest. Please leave the quest first.');
});
it('prevents quest leader from leaving a party if they have started a quest', function() {
group.quest = {
active: false,
leader: 'user-id'
};
groupsController.leave(req, res);
expect(group.leave).to.not.be.called;
expect(res.json).to.be.calledOnce;
expect(res.json).to.be.calledWith(403, 'You cannot leave your party when you have started a quest. Abort the quest first.');
});
it('leaves party if quest is not active', function() {
group.quest = {
active: false,
members: {
another_user: true,
yet_another_user: null,
'user-id': null
}
};
groupsController.leave(req, res);
expect(group.leave).to.be.calledOnce;
expect(res.json).to.not.be.called;
});
it('leaves party if quest is active, but user is not part of quest', function() {
group.quest = {
active: true,
members: {
another_user: true,
yet_another_user: null,
'user-id': null
}
};
groupsController.leave(req, res);
expect(group.leave).to.be.calledOnce;
expect(res.json).to.not.be.called;
});
});
});
describe('#questLeave', function() {
var res, req, group, user, saveSpy;
beforeEach(function() {
sinon.stub(Q, 'all').returns({
done: sinon.stub().yields()
});
group = {
_id: 'group-id',
type: 'party',
quest: {
leader : 'another-user',
active: true,
members: {
'user-id': true,
'another-user': true
},
key : 'vice1',
progress : {
hp : 364,
collect : {}
}
},
save: sinon.stub().yields(),
markModified: sinon.spy()
};
user = {
_id: 'user-id',
party : {
quest : {
key : 'vice1',
progress : {
up : 50,
down : 0,
collectedItems : {}
},
completed : null,
RSVPNeeded : false
}
},
save: sinon.stub().yields(),
markModified: sinon.spy()
};
res = {
locals: {
group: group,
user: user
},
json: sinon.stub(),
sendStatus: sinon.stub()
};
req = { };
});
afterEach(function() {
Promise.all.restore();
});
context('error conditions', function() {
it('errors if quest is not active', function() {
group.quest.active = false;
groupsController.questLeave(req, res);
expect(res.json).to.be.calledOnce;
expect(res.json).to.be.calledWith(
404,
{ err: 'No active quest to leave' }
);
});
it('errors if user is not part of quest', function() {
delete group.quest.members[user._id];
groupsController.questLeave(req, res);
expect(res.json).to.be.calledOnce;
expect(res.json).to.be.calledWith(
403,
{ err: 'You are not part of the quest' }
);
});
it('does not allow quest leader to leave quest', function() {
group.quest.leader = 'user-id';
groupsController.questLeave(req, res);
expect(res.json).to.be.calledOnce;
expect(res.json).to.be.calledWith(
403,
{ err: 'Quest leader cannot leave quest' }
);
});
it('sends 500 if group cannot save', function() {
Promise.all.returns({
done: sinon.stub().callsArgWith(1, {err: 'save error'})
});
var nextSpy = sinon.spy();
groupsController.questLeave(req, res, nextSpy);
expect(res.json).to.not.be.called;
expect(nextSpy).to.be.calledOnce;
expect(nextSpy).to.be.calledWith({err: 'save error'});
});
});
context('success', function() {
it('removes user from quest', function() {
expect(group.quest.members[user._id]).to.exist;
groupsController.questLeave(req, res);
expect(group.quest.members[user._id]).to.not.exist;
});
it('scrubs quest data from user', function() {
user.party.quest.progress = {
up: 100,
down: 32,
collectedItems: 16,
collect: {
foo: 12,
bar: 4
}
};
groupsController.questLeave(req, res);
expect(user.party.quest.key).to.not.exist;
expect(user.party.quest.progress).to.eql({
up: 0,
down: 0,
collectedItems: 0,
});
});
it('sends back 204 on success', function() {
groupsController.questLeave(req, res);
expect(res.sendStatus).to.be.calledOnce;
expect(res.sendStatus).to.be.calledWith(204);
});
});
});
describe('#removeMember', function() {
var req, res, group, user;
beforeEach(function() {
user = { _id: 'user-id' };
group = {
_id: 'group-id',
leader: 'user-id',
members: ['user-id', 'member-to-boot', 'another-user']
}
res = {
locals: {
user: user,
group: group
},
sendStatus: sinon.stub()
};
req = {
query: {
uuid: 'member-to-boot'
}
};
sinon.stub(Group, 'update');
sinon.stub(User, 'update');
sinon.stub(User, 'findById');
});
afterEach(function() {
Group.update.restore();
User.update.restore();
User.findById.restore();
});
context('quest behavior', function() {
it('removes quest from party if booted member was quest leader', function() {
group.quest = {
leader: 'member-to-boot',
active: true,
members: {
'user-id': true,
'leader-id': true,
'member-to-boot': true
},
key: 'whale'
}
groupsController.removeMember(req, res);
expect(Group.update).to.be.calledOnce;
expect(Group.update).to.be.calledWith(
{ _id: 'group-id'},
{
'$inc': { memberCount: -1 },
'$pull': { members: 'member-to-boot' },
'$set': { quest: {key: null, leader: null} }
}
);
});
it('returns quest scroll to booted member if booted member was leader of quest', function() {
Group.update.yields();
var bootedMember = {
_id: 'member-to-boot',
apiToken: 'api',
preferences: {
emailNotifications: {
kickedGroup: false
}
}
};
User.findById.yields(null, bootedMember);
User.update.returns({
exec: sinon.stub()
});
group.quest = {
leader: 'member-to-boot',
active: true,
members: {
'user-id': true,
'leader-id': true,
'member-to-boot': true
},
key: 'whale'
}
groupsController.removeMember(req, res);
expect(User.update).to.be.calledOnce;
expect(User.update).to.be.calledWith(
{ _id: 'member-to-boot', apiToken: 'api' },
{
'$unset': { 'newMessages.group-id': ''},
'$inc': { 'items.quests.whale': 1 }
}
);
});
});
});
});

View File

@@ -1,617 +0,0 @@
var sinon = require('sinon');
var chai = require("chai")
chai.use(require("sinon-chai"))
var expect = chai.expect
var rewire = require('rewire');
var userController = rewire('../../../website/server/controllers/api-v2/user');
describe('User Controller', function() {
describe('score', function() {
var req, res, user;
beforeEach(function() {
user = {
_id: 'user-id',
_tmp: {
drop: true
},
_statsComputed: {
maxMP: 100
},
ops: {
score: sinon.stub(),
addTask: sinon.stub()
},
stats: {
lvl: 10,
hp: 43,
mp: 50
},
preferences: {
webhooks: {
'some-id': {
sort: 0,
id: 'some-id',
enabled: true,
url: 'http://example.org/endpoint'
}
}
},
save: sinon.stub(),
tasks: {
task_id: {
id: 'task_id',
type: 'todo'
}
}
};
req = {
language: 'en',
params: {
id: 'task_id',
direction: 'up'
}
};
res = {
locals: { user: user },
json: sinon.spy()
};
});
context('early return conditions', function() {
it('sends an error when no id is provided', function() {
delete req.params.id;
userController.score(req, res);
expect(res.json).to.be.calledOnce;
expect(res.json).to.be.calledWith(400, {err: ':id required'});
});
it('sends an error when no direction is provided', function() {
delete req.params.direction;
userController.score(req, res);
expect(res.json).to.be.calledOnce;
expect(res.json).to.be.calledWith(400, {err: ":direction must be 'up' or 'down'"});
});
it('calls next when direction is "unlink"', function() {
req.params.direction = 'unlink';
var nextSpy = sinon.spy();
userController.score(req, res, nextSpy);
expect(nextSpy).to.be.calledOnce;
});
it('calls next when direction is "sort"', function() {
req.params.direction = 'sort';
var nextSpy = sinon.spy();
userController.score(req, res, nextSpy);
expect(nextSpy).to.be.calledOnce;
});
});
context('task exists', function() {
it('sets todo to completed if direction is "up"', function() {
req.params.direction = 'up';
req.params.id = 'todo_id';
user.tasks.todo_id = {
_id: 'todo_id',
type: 'todo',
completed: false
};
userController.score(req, res);
expect(user.tasks.todo_id.completed).to.eql(true);
});
it('sets todo to not completed if direction is "down"', function() {
req.params.direction = 'down';
req.params.id = 'todo_id';
user.tasks.todo_id = {
_id: 'todo_id',
type: 'todo',
completed: true
};
userController.score(req, res);
expect(user.tasks.todo_id.completed).to.eql(false);
});
it('sets daily to completed if direction is "up"', function() {
req.params.direction = 'up';
req.params.id = 'daily_id';
user.tasks.daily_id = {
_id: 'daily_id',
type: 'daily',
completed: false
};
userController.score(req, res);
expect(user.tasks.daily_id.completed).to.eql(true);
});
it('sets daily to not completed if direction is "down"', function() {
req.params.direction = 'down';
req.params.id = 'daily_id';
user.tasks.daily_id = {
_id: 'daily_id',
type: 'daily',
completed: true
};
userController.score(req, res);
expect(user.tasks.daily_id.completed).to.eql(false);
});
});
context('task does not exist', function() {
it('creates the task', function() {
user.ops.addTask.returns({id: 'an-id-that-does-not-exist'});
req.params.id = 'an-id-that-does-not-exist-yet';
req.body = {
type: 'todo',
text: 'some todo',
notes: 'some notes'
}
userController.score(req, res);
expect(user.ops.addTask).to.be.calledOnce;
expect(user.ops.addTask).to.be.calledWith({
body: {
id: 'an-id-that-does-not-exist-yet',
completed: true,
type: 'todo',
text: 'some todo',
notes: 'some notes'
}
});
});
it('provides a default note if no note is provided', function() {
user.ops.addTask.returns({id: 'an-id-that-does-not-exist'});
req.params.id = 'an-id-that-does-not-exist-yet';
req.body = {
type: 'todo',
text: 'some todo'
}
userController.score(req, res);
expect(user.ops.addTask).to.be.calledOnce;
expect(user.ops.addTask).to.be.calledWith({
body: {
id: 'an-id-that-does-not-exist-yet',
completed: true,
type: 'todo',
text: 'some todo',
notes: "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task."
}
});
});
it('todo task is completed if direction is "up"', function() {
user.ops.addTask.returns({id: 'an-id-that-does-not-exist'});
req.params.direction = 'up';
req.params.id = 'an-id-that-does-not-exist-yet';
req.body = {
type: 'todo',
text: 'some todo',
notes: 'some notes'
}
userController.score(req, res);
expect(user.ops.addTask).to.be.calledOnce;
expect(user.ops.addTask).to.be.calledWith({
body: {
id: 'an-id-that-does-not-exist-yet',
completed: true,
type: 'todo',
text: 'some todo',
notes: 'some notes'
}
});
});
it('todo task is not completed if direction is "down"', function() {
user.ops.addTask.returns({id: 'an-id-that-does-not-exist'});
req.params.direction = 'down';
req.params.id = 'an-id-that-does-not-exist-yet';
req.body = {
type: 'todo',
text: 'some todo',
notes: 'some notes'
}
userController.score(req, res);
expect(user.ops.addTask).to.be.calledOnce;
expect(user.ops.addTask).to.be.calledWith({
body: {
id: 'an-id-that-does-not-exist-yet',
completed: false,
type: 'todo',
text: 'some todo',
notes: 'some notes'
}
});
});
it('daily task is completed if direction is "up"', function() {
user.ops.addTask.returns({id: 'an-id-that-does-not-exist'});
req.params.direction = 'up';
req.params.id = 'an-id-that-does-not-exist-yet';
req.body = {
type: 'daily',
text: 'some daily',
notes: 'some notes'
}
userController.score(req, res);
expect(user.ops.addTask).to.be.calledOnce;
expect(user.ops.addTask).to.be.calledWith({
body: {
id: 'an-id-that-does-not-exist-yet',
completed: true,
type: 'daily',
text: 'some daily',
notes: 'some notes'
}
});
});
it('daily task is not completed if direction is "down"', function() {
user.ops.addTask.returns({id: 'an-id-that-does-not-exist'});
req.params.direction = 'down';
req.params.id = 'an-id-that-does-not-exist-yet';
req.body = {
type: 'daily',
text: 'some daily',
notes: 'some notes'
}
userController.score(req, res);
expect(user.ops.addTask).to.be.calledOnce;
expect(user.ops.addTask).to.be.calledWith({
body: {
id: 'an-id-that-does-not-exist-yet',
completed: false,
type: 'daily',
text: 'some daily',
notes: 'some notes'
}
});
});
});
context('whether task exists or it does not exist', function() {
it('calls user.ops.score', function() {
userController.score(req, res);
expect(user.ops.score).to.be.calledOnce;
expect(user.ops.score).to.be.calledWith({
params: {id: 'task_id', direction: 'up'},
language: 'en'
});
});
it('saves user', function() {
userController.score(req, res);
expect(user.save).to.be.calledOnce;
});
});
context('user.save callback', function() {
var savedUser;
beforeEach(function() {
savedUser = {
stats: user.stats
}
user.save.yields(null, savedUser);
user.ops.score.returns(1.5);
});
it('calls next if saving yields an error', function() {
var nextSpy = sinon.spy();
user.save.yields('an error');
userController.score(req, res, nextSpy);
expect(nextSpy).to.be.calledOnce;
expect(nextSpy).to.be.calledWith('an error');
});
it('sends some user data with res.json', function() {
userController.score(req, res);
expect(res.json).to.be.calledOnce;
expect(res.json).to.be.calledWith(200, {
delta: 1.5,
_tmp: user._tmp,
lvl: 10,
hp: 43,
mp: 50
});
});
it('sends webhooks', function() {
var webhook = require('../../../website/server/libs/webhook');
sinon.spy(webhook, 'sendTaskWebhook');
userController.score(req, res);
expect(webhook.sendTaskWebhook).to.be.calledOnce;
expect(webhook.sendTaskWebhook).to.be.calledWith(
user.preferences.webhooks,
{
task: {
delta: 1.5,
details: { completed: true, id: "task_id", type: "todo" },
direction: "up"
},
user: {
_id: "user-id",
_tmp: { drop: true },
stats: { hp: 43, lvl: 10, maxHealth: 50, maxMP: 100, mp: 50, toNextLevel: 260 }
}
}
);
});
});
context('save callback dealing with non challenge tasks', function() {
var Challenge = require('../../../website/server/models/challenge').model;
beforeEach(function() {
user.save.yields(null, user);
sinon.stub(Challenge, 'findById');
req.params.id = 'non_active_challenge_task';
user.tasks.non_active_challenge_task = {
id: 'non_active_challenge_task',
challenge: { id: 'some-id' },
type: 'todo'
}
});
afterEach(function() {
Challenge.findById.restore();
});
it('returns early if not a challenge', function() {
delete user.tasks.non_active_challenge_task.challenge;
userController.score(req, res);
expect(Challenge.findById).to.not.be.called;
});
it('returns early if no challenge id', function() {
delete user.tasks.non_active_challenge_task.challenge.id;
userController.score(req, res);
expect(Challenge.findById).to.not.be.called;
});
it('returns early if challenge is broken', function() {
user.tasks.non_active_challenge_task.challenge.broken = true;
userController.score(req, res);
expect(Challenge.findById).to.not.be.called;
});
it('returns early if task is a reward', function() {
user.tasks.non_active_challenge_task.type = 'reward';
userController.score(req, res);
expect(Challenge.findById).to.not.be.called;
});
it('calls next if there is an error looking up challenge', function() {
Challenge.findById.yields('an error');
var nextSpy = sinon.spy();
userController.score(req, res, nextSpy);
expect(Challenge.findById).to.be.calledOnce;
expect(nextSpy).to.be.calledOnce;
expect(nextSpy).to.be.calledWith('an error');
});
});
context('save callback dealing with challenge tasks', function() {
var Challenge = require('../../../website/server/models/challenge').model;
var chal;
beforeEach(function() {
chal = {
id: 'id',
tasks: {
active_challenge_task: { id: 'active_challenge_task', value: 1 }
},
syncToUser: sinon.spy(),
save: sinon.spy()
};
user.save.yields(null, user);
user.ops.score.returns(1.4);
req.params.id = 'active_challenge_task';
user.tasks.active_challenge_task = {
id: 'active_challenge_task',
challenge: { id: 'challenge_id' },
type: 'todo'
};
sinon.stub(Challenge, 'findById');
});
afterEach(function() {
Challenge.findById.restore();
});
xit('sets challenge as broken if no challenge can be found', function() {
Challenge.findById.yields(null, null);
userController.score(req, res);
expect(Challenge.findById).to.be.calledOnce;
expect(user.tasks.active_challenge_task.challenge.broken).to.eql('CHALLENGE_DELETED');
});
it('notifies user if task has been deleted from challenge', function() {
delete chal.tasks.active_challenge_task;
Challenge.findById.yields(null, chal);
userController.score(req, res);
expect(Challenge.findById).to.be.calledOnce;
expect(chal.syncToUser).to.be.calledOnce;
});
it('changes task value by delta', function() {
Challenge.findById.yields(null, chal);
userController.score(req, res);
expect(Challenge.findById).to.be.calledOnce;
expect(chal.tasks.active_challenge_task.value).to.be.eql(2.4);
});
it('adds history if task is a habit', function() {
chal.tasks.active_challenge_task = {
id: 'active_challenge_task',
type: 'habit',
value: 1,
history: [{value: 1, date: 1234}]
};
Challenge.findById.yields(null, chal);
userController.score(req, res);
expect(Challenge.findById).to.be.calledOnce;
var historyEvent = chal.tasks.active_challenge_task.history[1];
expect(historyEvent.value).to.eql(2.4);
expect(historyEvent.date).to.be.closeTo(+new Date, 10);
});
it('adds history if task is a daily', function() {
chal.tasks.active_challenge_task = {
id: 'active_challenge_task',
type: 'daily',
value: 1,
history: [{value: 1, date: 1234}]
};
Challenge.findById.yields(null, chal);
userController.score(req, res);
expect(Challenge.findById).to.be.calledOnce;
var historyEvent = chal.tasks.active_challenge_task.history[1];
expect(historyEvent.value).to.eql(2.4);
expect(historyEvent.date).to.be.closeTo(+new Date, 10);
});
it('saves the challenge data', function() {
Challenge.findById.yields(null, chal);
userController.score(req, res);
expect(Challenge.findById).to.be.calledOnce;
expect(chal.save).to.be.calledOnce;
});
});
});
describe('#addTenGems', function() {
var req, res, user;
beforeEach(function() {
user = {
_id: 'user-id',
balance: 5,
save: sinon.stub().yields()
};
req = { };
res = {
locals: { user: user },
send: sinon.spy()
};
});
it('adds 2.5 to user balance', function() {
userController.addTenGems(req, res);
expect(user.balance).to.eql(7.5);
expect(user.save).to.be.calledOnce;
});
it('sends back 204', function() {
userController.addTenGems(req, res);
expect(res.sendStatus).to.be.calledOnce;
expect(res.sendStatus).to.be.calledWith(204);
});
});
describe('#addHourglass', function() {
var req, res, user;
beforeEach(function() {
user = {
_id: 'user-id',
purchased: { plan: { consecutive: { trinkets: 3 } } },
save: sinon.stub().yields()
};
req = { };
res = {
locals: { user: user },
send: sinon.spy()
};
});
it('adds an hourglass to user', function() {
userController.addHourglass(req, res);
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
expect(user.save).to.be.calledOnce;
});
it('sends back 204', function() {
userController.addHourglass(req, res);
expect(res.sendStatus).to.be.calledOnce;
expect(res.sendStatus).to.be.calledWith(204);
});
});
});

View File

@@ -1,139 +0,0 @@
var sinon = require('sinon');
var chai = require("chai")
chai.use(require("sinon-chai"))
var expect = chai.expect
var rewire = require('rewire');
var webhook = rewire('../../website/server/libs/api-v2/webhook');
describe('webhooks', function() {
var postSpy;
beforeEach(function() {
postSpy = sinon.stub();
webhook.__set__('request.post', postSpy);
});
describe('sendTaskWebhook', function() {
var task = {
details: { _id: 'task-id' },
delta: 1.4,
direction: 'up'
};
var data = {
task: task,
user: { _id: 'user-id' }
};
it('does not send if no webhook endpoints exist', function() {
var webhooks = { };
webhook.sendTaskWebhook(webhooks, data);
expect(postSpy).to.not.be.called;
});
it('does not send if no webhooks are enabled', function() {
var webhooks = {
'some-id': {
sort: 0,
id: 'some-id',
enabled: false,
url: 'http://example.org/endpoint'
}
};
webhook.sendTaskWebhook(webhooks, data);
expect(postSpy).to.not.be.called;
});
it('does not send if webhook url is not valid', function() {
var webhooks = {
'some-id': {
sort: 0,
id: 'some-id',
enabled: true,
url: 'http://malformedurl/endpoint'
}
};
webhook.sendTaskWebhook(webhooks, data);
expect(postSpy).to.not.be.called;
});
it('sends task direction, task, task delta, and abridged user data', function() {
var webhooks = {
'some-id': {
sort: 0,
id: 'some-id',
enabled: true,
url: 'http://example.org/endpoint'
}
};
webhook.sendTaskWebhook(webhooks, data);
expect(postSpy).to.be.calledOnce;
expect(postSpy).to.be.calledWith({
url: 'http://example.org/endpoint',
body: {
direction: 'up',
task: { _id: 'task-id' },
delta: 1.4,
user: {
_id: 'user-id'
}
},
json: true
});
});
it('sends a post request for each webhook endpoint', function() {
var webhooks = {
'some-id': {
sort: 0,
id: 'some-id',
enabled: true,
url: 'http://example.org/endpoint'
},
'second-webhook': {
sort: 1,
id: 'second-webhook',
enabled: true,
url: 'http://example.com/2/endpoint'
}
};
webhook.sendTaskWebhook(webhooks, data);
expect(postSpy).to.be.calledTwice;
expect(postSpy).to.be.calledWith({
url: 'http://example.org/endpoint',
body: {
direction: 'up',
task: { _id: 'task-id' },
delta: 1.4,
user: {
_id: 'user-id'
}
},
json: true
});
expect(postSpy).to.be.calledWith({
url: 'http://example.com/2/endpoint',
body: {
direction: 'up',
task: { _id: 'task-id' },
delta: 1.4,
user: {
_id: 'user-id'
}
},
json: true
});
});
});
});

View File

@@ -1,6 +1,6 @@
/* eslint-disable */
require('eventsource-polyfill')
var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true&overlay=false')
hotClient.subscribe(function (event) {
if (event.action === 'reload') {

View File

@@ -17,7 +17,7 @@ var baseConfig = {
extensions: ['', '.js', '.vue'],
fallback: [path.join(__dirname, '../node_modules')],
alias: {
src: path.resolve(__dirname, '../website/client'),
client: path.resolve(__dirname, '../website/client'),
assets: path.resolve(__dirname, '../website/client/assets'),
components: path.resolve(__dirname, '../website/client/components'),
},
@@ -86,6 +86,7 @@ var baseConfig = {
if (!IS_PROD) {
baseConfig.eslint = {
formatter: require('eslint-friendly-formatter'),
emitWarning: true,
};
}
module.exports = baseConfig;

View File

@@ -23,7 +23,6 @@ module.exports = merge(baseWebpackConfig, {
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
new webpack.optimize.OccurenceOrderPlugin(),
new webpack.HotModuleReplacementPlugin(),
new webpack.NoErrorsPlugin(),
// https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: 'index.html',

View File

@@ -36,8 +36,13 @@
background-color: #727272;
}
/* FIXME figure out how to handle customize menu!! */
/*.customize-menu .f_head_0 {width: 60px; height: 60px; background-position: -1917px -9px;}*/
/* FIXME figure out how to handle customize menu!!
.customize-menu .f_head_0 {
width: 60px;
height: 60px;
background-position: -1917px -9px;
}
*/
.achievement {
float:left;
@@ -51,7 +56,8 @@
padding-right: 0.5em;
}
[class*="Mount_Head_"], [class*="Mount_Body_"]{
[class*="Mount_Head_"],
[class*="Mount_Body_"] {
margin-top:18px; /* Sprite accommodates 105x123 box */
}

View File

@@ -1,54 +1,30 @@
.2014_Fall_HealerPROMO2 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1212px -1488px;
width: 90px;
height: 90px;
}
.2014_Fall_Mage_PROMO9 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -452px -884px;
width: 120px;
height: 90px;
}
.2014_Fall_RoguePROMO3 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -558px -1385px;
width: 105px;
height: 90px;
}
.2014_Fall_Warrior_PROMO {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -848px -1488px;
width: 90px;
height: 90px;
}
.promo_android {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -978px -573px;
background-position: -1416px -829px;
width: 175px;
height: 175px;
}
.promo_backgrounds_armoire_201602 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1417px -103px;
background-position: -1157px -573px;
width: 141px;
height: 294px;
}
.promo_backgrounds_armoire_201603 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1275px -103px;
background-position: -1015px -573px;
width: 141px;
height: 294px;
}
.promo_backgrounds_armoire_201604 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -452px -442px;
background-position: -452px 0px;
width: 140px;
height: 441px;
}
.promo_backgrounds_armoire_201605 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -593px 0px;
background-position: -452px -442px;
width: 140px;
height: 441px;
}
@@ -66,73 +42,85 @@
}
.promo_backgrounds_armoire_201608 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -593px -442px;
background-position: -734px -442px;
width: 140px;
height: 439px;
}
.promo_backgrounds_armoire_201609 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -734px 0px;
background-position: -875px 0px;
width: 139px;
height: 438px;
}
.promo_backgrounds_armoire_201610 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -593px -442px;
width: 140px;
height: 441px;
}
.promo_backtoschool {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -151px -1488px;
background-position: -1522px -1005px;
width: 150px;
height: 150px;
}
.promo_burnout {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -874px -151px;
background-position: -1015px -151px;
width: 219px;
height: 240px;
}
.promo_chairs_glasses {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -306px -220px;
background-position: -1299px -573px;
width: 51px;
height: 210px;
}
.promo_classes_fall_2014 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -874px -1013px;
background-position: -363px -1313px;
width: 321px;
height: 100px;
}
.promo_classes_fall_2015 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1275px -878px;
background-position: -430px -1210px;
width: 377px;
height: 99px;
}
.promo_classes_fall_2016 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -874px -573px;
background-position: -1416px -480px;
width: 103px;
height: 348px;
}
.promo_contrib_spotlight_beffymaroo {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1154px -573px;
background-position: -1696px -595px;
width: 114px;
height: 147px;
}
.promo_contrib_spotlight_cantras {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1592px -829px;
width: 87px;
height: 109px;
}
.promo_cow {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -281px -525px;
background-position: -734px 0px;
width: 140px;
height: 441px;
}
.promo_dilatoryDistress {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1316px -1385px;
background-position: -1177px -1527px;
width: 90px;
height: 90px;
}
.promo_egg_mounts {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1275px -398px;
background-position: -1015px -868px;
width: 280px;
height: 147px;
}
@@ -144,49 +132,49 @@
}
.promo_enchanted_armoire_201507 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -289px -1294px;
background-position: -1122px -1416px;
width: 217px;
height: 90px;
}
.promo_enchanted_armoire_201508 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -725px -1294px;
background-position: -1340px -1416px;
width: 180px;
height: 90px;
}
.promo_enchanted_armoire_201509 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -666px -1488px;
background-position: -722px -1527px;
width: 90px;
height: 90px;
}
.promo_enchanted_armoire_201511 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -734px -978px;
background-position: -1696px -1396px;
width: 122px;
height: 90px;
}
.promo_enchanted_armoire_201601 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -939px -1488px;
background-position: -593px -975px;
width: 90px;
height: 90px;
}
.promo_floral_potions {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1559px -103px;
background-position: -1416px -1005px;
width: 105px;
height: 273px;
}
.promo_ghost_potions {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -452px 0px;
background-position: -593px 0px;
width: 140px;
height: 441px;
}
.promo_habitica {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1094px -151px;
background-position: -1520px -480px;
width: 175px;
height: 175px;
}
@@ -196,297 +184,333 @@
width: 305px;
height: 304px;
}
.promo_habitoween_2016 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -281px -525px;
width: 140px;
height: 441px;
}
.promo_haunted_hair {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -734px -734px;
background-position: -1696px -1038px;
width: 100px;
height: 137px;
}
.promo_item_notif {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: 0px -1385px;
background-position: 0px -1527px;
width: 249px;
height: 102px;
}
.promo_mystery_201405 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1589px -1385px;
background-position: -1086px -1527px;
width: 90px;
height: 90px;
}
.promo_mystery_201406 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -358px -326px;
background-position: -875px -755px;
width: 90px;
height: 96px;
}
.promo_mystery_201407 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1064px -1114px;
background-position: -1796px -954px;
width: 42px;
height: 62px;
}
.promo_mystery_201408 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1196px -1013px;
background-position: -1351px -655px;
width: 60px;
height: 71px;
}
.promo_mystery_201409 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1030px -1488px;
background-position: -1268px -1527px;
width: 90px;
height: 90px;
}
.promo_mystery_201410 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1131px -840px;
background-position: -1592px -939px;
width: 72px;
height: 63px;
}
.promo_mystery_201411 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1225px -1385px;
background-position: -904px -1527px;
width: 90px;
height: 90px;
}
.promo_mystery_201412 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1021px -1114px;
background-position: -1799px -813px;
width: 42px;
height: 66px;
}
.promo_mystery_201501 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1653px -878px;
background-position: -1796px -890px;
width: 48px;
height: 63px;
}
.promo_mystery_201502 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -393px -1488px;
background-position: -540px -1527px;
width: 90px;
height: 90px;
}
.promo_mystery_201503 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -575px -1488px;
background-position: -449px -1527px;
width: 90px;
height: 90px;
}
.promo_mystery_201504 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -593px -1030px;
background-position: -875px -1034px;
width: 60px;
height: 69px;
}
.promo_mystery_201505 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -757px -1488px;
background-position: -452px -975px;
width: 90px;
height: 90px;
}
.promo_mystery_201506 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -654px -1030px;
background-position: -1799px -743px;
width: 42px;
height: 69px;
}
.promo_mystery_201507 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1601px -1081px;
background-position: -875px -543px;
width: 90px;
height: 105px;
}
.promo_mystery_201508 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -664px -1385px;
background-position: -875px -852px;
width: 93px;
height: 90px;
}
.promo_mystery_201509 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1121px -1488px;
background-position: -452px -884px;
width: 90px;
height: 90px;
}
.promo_mystery_201510 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -758px -1385px;
background-position: -593px -884px;
width: 93px;
height: 90px;
}
.promo_mystery_201511 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1134px -1385px;
background-position: -1359px -1527px;
width: 90px;
height: 90px;
}
.promo_mystery_201512 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1638px -978px;
background-position: -1351px -573px;
width: 60px;
height: 81px;
}
.promo_mystery_201601 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -452px -975px;
background-position: -1286px -392px;
width: 120px;
height: 90px;
}
.promo_mystery_201602 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1407px -1385px;
background-position: -995px -1527px;
width: 90px;
height: 90px;
}
.promo_mystery_201603 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1498px -1385px;
background-position: -1521px -1416px;
width: 90px;
height: 90px;
}
.promo_mystery_201604 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -852px -1385px;
background-position: -875px -943px;
width: 93px;
height: 90px;
}
.promo_mystery_201605 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -302px -1488px;
background-position: -813px -1527px;
width: 90px;
height: 90px;
}
.promo_mystery_201606 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -358px -220px;
background-position: -875px -649px;
width: 90px;
height: 105px;
}
.promo_mystery_201607 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -484px -1488px;
background-position: -631px -1527px;
width: 90px;
height: 90px;
}
.promo_mystery_201608 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1040px -1385px;
background-position: -734px -973px;
width: 93px;
height: 90px;
}
.promo_mystery_201609 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -946px -1385px;
background-position: -734px -882px;
width: 93px;
height: 90px;
}
.promo_mystery_201610 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1350px -302px;
width: 63px;
height: 84px;
}
.promo_mystery_3014 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -507px -1294px;
background-position: -904px -1416px;
width: 217px;
height: 90px;
}
.promo_orca {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -734px -872px;
background-position: -1696px -1290px;
width: 105px;
height: 105px;
}
.promo_partyhats {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1094px -327px;
background-position: -1696px -1563px;
width: 115px;
height: 47px;
}
.promo_pastel_skin {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -331px -1210px;
background-position: -808px -1210px;
width: 330px;
height: 83px;
}
.customize-option.promo_pastel_skin {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -356px -1225px;
background-position: -833px -1225px;
width: 60px;
height: 60px;
}
.promo_peppermint_flame {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -593px -882px;
background-position: -1696px -151px;
width: 140px;
height: 147px;
}
.promo_pet_skins {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1556px -398px;
background-position: -1696px -299px;
width: 140px;
height: 147px;
}
.customize-option.promo_pet_skins {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1581px -413px;
background-position: -1721px -314px;
width: 60px;
height: 60px;
}
.promo_pyromancer {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1696px -1176px;
width: 113px;
height: 113px;
}
.promo_rainbow_armor {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -875px -439px;
width: 92px;
height: 103px;
}
.promo_seasonal_shop_fall_2016 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1416px 0px;
width: 279px;
height: 147px;
}
.promo_shimmer_hair {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: 0px -1210px;
background-position: -685px -1313px;
width: 330px;
height: 83px;
}
.promo_splashyskins {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -250px -1385px;
background-position: -250px -1527px;
width: 198px;
height: 91px;
}
.customize-option.promo_splashyskins {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -275px -1400px;
background-position: -275px -1542px;
width: 60px;
height: 60px;
}
.promo_spooky_sparkles_fall_2016 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -306px -220px;
width: 140px;
height: 294px;
}
.promo_spring_classes_2016 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1275px -978px;
background-position: 0px -1313px;
width: 362px;
height: 102px;
}
.promo_springclasses2014 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: 0px -1294px;
background-position: -326px -1416px;
width: 288px;
height: 90px;
}
.promo_springclasses2015 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -978px -749px;
background-position: -615px -1416px;
width: 288px;
height: 90px;
}
.promo_staff_spotlight_Lemoness {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -734px -439px;
background-position: -1696px -743px;
width: 102px;
height: 146px;
}
.promo_staff_spotlight_Viirus {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1145px -392px;
background-position: -1696px -447px;
width: 119px;
height: 147px;
}
.promo_staff_spotlight_paglias {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -734px -586px;
background-position: -1696px -890px;
width: 99px;
height: 147px;
}
.promo_summer_classes_2014 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1275px 0px;
background-position: 0px -1210px;
width: 429px;
height: 102px;
}
@@ -498,55 +522,61 @@
}
.promo_summer_classes_2016 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -874px 0px;
background-position: -1015px 0px;
width: 400px;
height: 150px;
}
.promo_takeThis_gear {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -306px -431px;
background-position: -1235px -302px;
width: 114px;
height: 87px;
}
.promo_takethis_armor {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -906px -1294px;
background-position: -1286px -483px;
width: 114px;
height: 87px;
}
.promo_unconventional_armor {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1204px -840px;
background-position: -936px -1034px;
width: 60px;
height: 60px;
}
.promo_unconventional_armor2 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1299px -784px;
width: 70px;
height: 74px;
}
.promo_updos {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1522px -546px;
background-position: -1520px -656px;
width: 156px;
height: 147px;
}
.promo_veteran_pets {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -874px -1114px;
background-position: -1696px -1487px;
width: 146px;
height: 75px;
}
.promo_winter_classes_2016 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -874px -922px;
background-position: -1015px -1016px;
width: 360px;
height: 90px;
}
.promo_winterclasses2015 {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1275px -1081px;
background-position: 0px -1416px;
width: 325px;
height: 110px;
}
.promo_winteryhair {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -978px -840px;
background-position: -1522px -1156px;
width: 152px;
height: 75px;
}
@@ -558,7 +588,7 @@
}
.npc_viirus {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -449px -1385px;
background-position: -1296px -868px;
width: 108px;
height: 90px;
}
@@ -570,31 +600,31 @@
}
.scene_coding {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: 0px -1488px;
background-position: -1696px 0px;
width: 150px;
height: 150px;
}
.scene_phone_peek {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1522px -712px;
background-position: -1235px -151px;
width: 150px;
height: 150px;
}
.welcome_basic_avatars {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1275px -546px;
background-position: -1416px -148px;
width: 246px;
height: 165px;
}
.welcome_promo_party {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -874px -392px;
background-position: -1015px -392px;
width: 270px;
height: 180px;
}
.welcome_sample_tasks {
background-image: url(/spritesmith-largeSprites-0.png);
background-position: -1275px -712px;
background-position: -1416px -314px;
width: 246px;
height: 165px;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 692 KiB

After

Width:  |  Height:  |  Size: 776 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 418 KiB

After

Width:  |  Height:  |  Size: 438 KiB

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