Compare commits

..

223 Commits

Author SHA1 Message Date
Sabe Jones
f6c26fe869 3.56.0 2016-11-23 02:05:23 +00:00
Sabe Jones
80e9735b28 Turkey Day 2016 (#8231)
* feat(event): Turkey Day 2016

* fix(test): allow for free pet
2016-11-22 20:00:10 -06:00
Matteo Pagliazzi
aa6f188bd9 new client: remove comments 2016-11-22 13:49:35 +01:00
MathWhiz
e8b7660376 Add Costume Info to member modal (#7768)
* Add localization strings

* Change name of Equipment section

* Add costume section to member modal

* Add costume section to member modal

* Add current pet and current mount info

* Reorder Sections and Separate Active Mounts/Pets

* switch ng-show with ng-if

* Add `noActiveMount` to pets.json

* Breaking Stuff

* Add petservices.js to the manifest

* Remove Extra Parenthesis

* Progress towards backgrounds

* Add semicolons

* Add background information

* Add all methods in petServices to userCtrl and memberModalCtrl

* Add avatar settings

* Add semicolons

* Revert "Add avatar settings"

This reverts commit 6e8cca9736.

* Remove active-pet-and-mount

* Remove Content from memberModalCtrl

* Update costumeServices.js

* Make costumeservices.js more readable

* Update costumeServices.js

* Update costumeService logic

* Remove unused strings

* Fix include statements

* move service

* Update pet/mount logic

* fixes

* Fix background logic
2016-11-21 21:19:13 +10:00
Mich Elliott
7d76622410 Remove white backgrounds from mount sprites (#8217)
* Remove white background from single mount sprite

* Remove white background on ~40 mount sprites
2016-11-21 07:48:41 +10:00
MathWhiz
928e5f66c4 Add translation link to news (#8216)
* Add translation link to news

* Add newsArchive string

* Translate news archive link
2016-11-20 14:22:59 +10:00
MathWhiz
6a343535c0 Remove party joined option (#8212)
* Remove party joined option

* Make default sort sort by profile name

* Remove extraneous comment
2016-11-20 14:08:18 +10:00
Alys
f58f6acb44 correct apidoc comments for updating and deleting a tag 2016-11-19 12:37:21 +10:00
Matteo Pagliazzi
64754777ed New Client: working navigation (#8131)
* initial work

* new client: working navigation and tasks showing up

* finish header menu and add avatar component

* fix sprites in new client

* initial header version

* initial styling for top menu

* more progress on the header menu

* almost complete menu and avatar

* correctly apply active class for /social and /help

* fix header colors and simplify css

* switch from Roboto to native fonts

* remove small avatar and add viewport

* fixes

* fix user menu with and progress bars

* fix avatar rendeting

* move bars colors to theme

* add site overrides

* fix tests

* shrinkwrap

* fix sprites path

* another try at fixing the sprites path

* another try at fixing the sprites path
2016-11-18 19:20:25 +01:00
Sabe Jones
3b5e4b6d84 3.55.0 2016-11-17 22:26:26 +00:00
Sabe Jones
9383578cb8 chore(news): Bailey 2016-11-17 21:29:26 +00:00
Sabe Jones
474672ec64 Merge pull request #8225 from HabitRPG/sabrecat/hairstyles
New Hairstyles
2016-11-17 15:20:55 -06:00
Keith Holliday
25c6691793 Added party sync and request sync events (#8223)
* Added party sync and request sync events

* Changed party member sync to be handled locally

* Optimized assignment to only use member variables

* Removed party sync event
2016-11-17 20:10:33 +01:00
Keith Holliday
3ea7b72024 Passed language param to text functions (#8220) 2016-11-17 19:32:07 +01:00
Sabe Jones
2d6f05a9a4 fix(hairstyles): base layer above bangs 2016-11-17 17:49:35 +00:00
Sabe Jones
28637286d6 fix(sprites): remove base outlines 2016-11-17 17:49:35 +00:00
Sabe Jones
874887b790 fix(hair): exclusivity and canvas tweaks 2016-11-17 17:49:34 +00:00
Sabe Jones
c977e5ebb5 fix(sprites): adjust Y position 2016-11-17 17:49:34 +00:00
Sabe Jones
f040e668f3 chore(sprites): add 2016-11-17 17:49:34 +00:00
Sabe Jones
55a15f938c feat(customize): new hairstyles 2016-11-17 17:45:31 +00:00
shalott
8c4f35daf4 Fixing test failure
This test seems to occasionally start failing (another coder reported the same thing happening to them in the blacksmiths’ guild) because the order in which the tasks are created can sometimes not match the order in the array. So I have sorted the tasks array after creation by the task name to ensure a consistent ordering, and slightly reordered the expect statements to match.
2016-11-16 21:52:23 +01:00
Matteo Pagliazzi
8f38ce3424 do not give _id to purchased.plan 2016-11-16 21:44:47 +01:00
Arashi007
b8f57a74d0 Added Airu's Theme (#8204)
* Airu's Theme
* Delete Minus_Habit.ogg
* Delete Minus_Habit.mp3
* Add files via upload
2016-11-16 20:11:22 +10:00
Sabe Jones
7ed26c0dbe 3.54.0 2016-11-16 02:12:33 +00:00
Sabe Jones
e8f5b26d4d chore(sprites): compile
and Bailey
2016-11-16 02:04:10 +00:00
Sabe Jones
0273648b6b Merge pull request #8221 from HabitRPG/sabrecat/pets-201611
Ferret Pet
2016-11-15 19:48:33 -06:00
Sabe Jones
b6fdac8885 feat(quest): Ferret Pet 2016-11-15 22:14:12 +00:00
Sabe Jones
00e6389672 3.53.5 2016-11-15 04:21:46 +00:00
Blade Barringer
e02c669b61 Move hr to prevent UserID comment from showing (#8214) 2016-11-14 21:59:31 -06:00
Blade Barringer
f0cb7c6bf3 Comment out group task fetching 2016-11-14 21:42:23 -06:00
Sabe Jones
571ef0b309 fix(news): add date 2016-11-15 03:34:50 +00:00
Blade Barringer
74328d1bcc chore(i18n): update locales 2016-11-14 21:33:01 -06:00
Sabe Jones
d34a9d828c Removed task get approvals request (#8218) 2016-11-14 21:09:36 -06:00
Keith Holliday
2fd35b3a40 Removed task get approvals request 2016-11-14 21:05:54 -06:00
Sabe Jones
e27512f626 3.53.4 2016-11-15 00:46:15 +00:00
Sabe Jones
dbf9cb3b4e chore(news): misc Bailey 2016-11-15 00:27:40 +00:00
AccioBooks
34c1245519 Move hr to prevent UserID comment from showing 2016-11-14 10:58:03 -06:00
Keith Holliday
f602bfe438 Removed group subscription options (#8211) 2016-11-13 20:24:59 +01:00
Alys
9aa4b8aa64 add 'month' to gift subscription message - fixes https://github.com/HabitRPG/habitica/issues/7747
I haven't pluralised this by using "month(s)", because the phrase "a 3 month subscription" is acceptable in English. Translators may use pluralisation as desired, although note that the same wording will be used for 1 month subscriptions.
2016-11-13 20:40:01 +10:00
Alys
5a150ebc5b change '/group/' to '/groups/' in docs for /api/v3/challenges/groups/:groupId 2016-11-13 17:43:52 +10:00
Keith Holliday
cbe1892b50 Added note sync when user adds task to challenge, tests, and fixed challenge tests (#8200) 2016-11-12 23:48:22 +01:00
Keith Holliday
13df60e0dd Group approval ui (#8184)
* Added all ui components back

* Added group ui items back and initial group approval directive

* Added ability to mark tasks as requires approval. Added approvals ctrl. Added get approvals method to tasks service

* Added approval list view with approving functionality

* Added error to produce message when task requests approval

* Added notification display for group approvals

* Fixed notification read and adding task

* Fixed syncing with group approval required

* Added group id to notifications for redirect on client side

* Fixed approval request tests

* Fixed linting issues

* Removed expectation from beforeEach

* Moved string to locale

* Added eslint ignore

* Updated notification for group approved, added new icons, and updated styles

* Hid group plan ui
2016-11-12 23:47:45 +01:00
Blade Barringer
3ff7692528 chore(i18n): update locales 2016-11-11 08:08:46 -06:00
Sabe Jones
111bba84dc feat(content): 2016-11 pet quest strings 2016-11-10 23:12:42 +00:00
Keith Holliday
b0d2b72b88 Updated buy special item to use function call wrapper (#8203) 2016-11-10 21:36:49 +01:00
Sabe Jones
696317ea8a fix(quests): Basilist error with no party 2016-11-07 15:30:44 +00:00
Sabe Jones
593178a46a fix(sprites): copy corrected Ian to prod path
fixes #7867 (again)
2016-11-07 15:20:04 +00:00
MathWhiz
f8fe16482d Unsubscribe documentation
closes #8187
2016-11-06 21:41:12 -06:00
Romeeka Gayhart
5108480ec5 Get skipped/pending unit tests working for revive (#8193) 2016-11-06 21:17:52 -06:00
Sabe Jones
95968b1b1c 3.53.3 2016-11-06 22:05:28 +00:00
Sabe Jones
566569af98 fix(event): end Fall Fest f'real 2016-11-06 21:36:52 +00:00
Alys
6693e9fca9 replace candy food with normal food and enhance canBuy / canDrop code (#8194)
* change food to normal; add variables to choose type of food; add canBuy, canDrop to cake

* reinstate ability to control canBuy and canDrop separately
2016-11-06 15:33:19 -06:00
Romeeka Gayhart
431bde56d2 Convert test UUID to string to avoid test error (#8195) 2016-11-05 23:53:11 -04:00
Sabe Jones
7cf17c0e63 3.53.2 2016-11-04 21:15:10 +00:00
Sabe Jones
49561bfc8c fix(test): accommodate changing seasons 2016-11-04 20:38:29 +00:00
Sabe Jones
8cbbb58e78 chore(event): end Fall Fest 2016-11-04 20:20:53 +00:00
Sabe Jones
905549e379 3.53.1 2016-11-04 19:23:55 +00:00
Sabe Jones
5d45c7209a chore(news): blog Bailey 2016-11-04 19:02:29 +00:00
Rick Kasten
371cddfe17 Updated bossColl1, bossColl1Broken (#8148) 2016-11-04 19:38:12 +10:00
AccioBooks
fcfac30caa Api doc status (#8165)
* Add example

* Update example
2016-11-03 08:31:30 -05:00
Corinna Jaschek
b094fb1e52 added message for challenges that could not be found - fixes #5538
closes #8176
2016-11-03 07:52:43 -05:00
Keith Holliday
a2dd82b6db Hid nav bar (#8181) 2016-11-02 21:58:17 -05:00
Sabe Jones
e6071610e4 fix(migration): revert bogus connect info 2016-11-03 00:29:57 +00:00
Sabe Jones
bdd0e2bb79 3.53.0 2016-11-03 00:12:32 +00:00
Sabe Jones
054a9a6f2b chore(sprites): compile 2016-11-02 23:29:12 +00:00
Sabe Jones
35b9ed6273 backgrounds and Armoire 2016-11 (#8178)
* feat(content): backgrounds and Armoire 2016-11

* chore(event): November Take This migration

* chore(news): Bailey
2016-11-02 18:27:32 -05:00
Keith Holliday
e65277baa5 Added check to ensure config is defined (#8180) 2016-11-02 18:27:22 -05:00
Amanda Furrow
421bd8624c Add flagger language to flag message sent to slack
closes #8179
fixes #8140
2016-11-02 17:28:44 -05:00
Blade Barringer
4562c6422a chore(i18n): update locales 2016-11-02 17:20:46 -05:00
Matteo Pagliazzi
a5cd9f2473 Merge branch 'TheHollidayInn-group-tasks-approval2' into develop 2016-11-01 21:55:32 +01:00
Matteo Pagliazzi
18bbdfa84b Merge branch 'group-tasks-approval' of https://github.com/TheHollidayInn/habitrpg into TheHollidayInn-group-tasks-approval2 2016-11-01 21:55:18 +01:00
Keith Holliday
d8c37f6e2d Group plan subscription (#8153)
* Added payment to groups and pay with group plan with Stripe

* Added edit card for Stripe

* Added stripe cancel

* Added subscribe with Amazon payments

* Added Amazon cancel for group subscription

* Added group subscription with paypal

* Added paypal cancel

* Added ipn cancel for Group plan

* Added a subscription tab and hid only the task tab when group is not subscribed

* Fixed linting issues

* Fixed tests

* Added payment unit tests

* Added back refresh after stripe payment

* Fixed style issues

* Limited grouop query fields and checked access

* Abstracted subscription schema

* Added year group plan and more access checks

* Maded purchase fields private

* Removed id and timestampes

* Added else checks to ensure user subscription is not altered. Removed active field from group model

* Added toJSONTransform function

* Moved plan active check to other toJson function

* Added check to see if purchaed has been populated

* Added purchase details to private

* Added correct data usage when paying for group sub
2016-11-01 21:51:30 +01:00
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
Keith Holliday
6801dae75d Fixed history test 2016-10-30 03:23:01 -05:00
Keith Holliday
59e1de6771 Moved approval to subdoc 2016-10-29 14:19:16 -05:00
Keith Holliday
5b240a1950 Updated notification name and other minor fixes 2016-10-29 14:19:16 -05:00
Keith Holliday
3ec3722038 Moved approval fields to group subdoc 2016-10-29 14:19:16 -05:00
Keith Holliday
d798ebadfe Fixed line endings 2016-10-29 14:19:16 -05:00
Keith Holliday
6cbddef627 Added get approvals route 2016-10-29 14:19:16 -05:00
Keith Holliday
016de411c9 Added notifications 2016-10-29 14:19:16 -05:00
Keith Holliday
2173f53883 Added fields for more approver details 2016-10-29 14:19:15 -05:00
Keith Holliday
f2e5bc52e5 Added requested approval fields and logic 2016-10-29 14:19:15 -05:00
Keith Holliday
393a9290e9 Added approval test and fixed line endings 2016-10-29 14:19:15 -05:00
Keith Holliday
ad5045bc09 Added git score approved task test 2016-10-29 14:19:15 -05:00
Keith Holliday
9b515ebdd1 Added task approve route 2016-10-29 14:19:15 -05:00
Keith Holliday
97bf9ee8e8 Added inital group task approval 2016-10-29 14:19:15 -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
1894 changed files with 87364 additions and 34907 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/**/*
@@ -25,4 +24,4 @@ gulp
webpack
test/client/e2e
test/client/unit/index.js
test/client/unit/karma.conf.js
test/client/unit/karma.conf.js

View File

@@ -9,5 +9,6 @@ Fixes put_issue_url_here
[//]: # (Put User ID in here - found in Settings -> API)
----
UUID:

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

@@ -57,7 +57,7 @@ module.exports = function(grunt) {
files: [
{expand: true, cwd: 'website/client-old/', src: 'favicon.ico', dest: 'website/build/'},
{expand: true, cwd: 'website/client-old/', src: 'favicon_192x192.png', dest: 'website/build/'},
{expand: true, cwd: 'website/assets/sprites/dist/', src: 'spritesmith*.png', dest: 'website/build/'},
{expand: true, cwd: 'website/assets/sprites/dist/', src: 'spritesmith*.png', dest: 'website/build/static/sprites'},
{expand: true, cwd: 'website/assets/sprites/', src: 'backer-only/*.gif', dest: 'website/build/'},
{expand: true, cwd: 'website/assets/sprites/', src: 'npc_ian.gif', dest: 'website/build/'},
{expand: true, cwd: 'website/assets/sprites/', src: 'quest_*.gif', dest: 'website/build/'},
@@ -78,6 +78,7 @@ module.exports = function(grunt) {
'website/build/favicon.ico',
'website/build/favicon_192x192.png',
'website/build/*.png',
'website/build/static/sprites/*.png',
'website/build/*.gif',
'website/build/bower_components/bootstrap/dist/fonts/*'
],
@@ -126,15 +127,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.2.0"
"pusher-websocket-iso": "pusher#^3.2.0",
"taggle": "^1.11.1"
},
"devDependencies": {
"angular-mocks": "1.3.9"

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

@@ -12,6 +12,9 @@ import {each} from 'lodash';
const MAX_SPRITESHEET_SIZE = 1024 * 1024 * 3;
const DIST_PATH = 'website/assets/sprites/dist/';
const IMG_DIST_PATH_NEW_CLIENT = 'website/static/sprites/';
const CSS_DIST_PATH_NEW_CLIENT = 'website/client/assets/css/sprites/';
gulp.task('sprites:compile', ['sprites:clean', 'sprites:main', 'sprites:largeSprites', 'sprites:checkCompiledDimensions']);
gulp.task('sprites:main', () => {
@@ -25,7 +28,7 @@ gulp.task('sprites:largeSprites', () => {
});
gulp.task('sprites:clean', (done) => {
clean(`${DIST_PATH}spritesmith*`, done);
clean(`{${DIST_PATH}spritesmith*,${IMG_DIST_PATH_NEW_CLIENT}spritesmith*,${CSS_DIST_PATH_NEW_CLIENT}spritesmith*}`, done);
});
gulp.task('sprites:checkCompiledDimensions', ['sprites:main', 'sprites:largeSprites'], () => {
@@ -66,14 +69,16 @@ function createSpritesStream (name, src) {
algorithm: 'binary-tree',
padding: 1,
cssTemplate: 'website/assets/sprites/css/css.template.handlebars',
cssVarMap: cssVarMap
cssVarMap: cssVarMap,
}));
let imgStream = spriteData.img
.pipe(imagemin())
.pipe(gulp.dest(IMG_DIST_PATH_NEW_CLIENT))
.pipe(gulp.dest(DIST_PATH));
let cssStream = spriteData.css
.pipe(gulp.dest(CSS_DIST_PATH_NEW_CLIENT))
.pipe(gulp.dest(DIST_PATH));
stream.add(imgStream);
@@ -148,4 +153,9 @@ function cssVarMap (sprite) {
}
if (~sprite.name.indexOf('shirt'))
sprite.custom.px.offset_y = `-${ sprite.y + 30 }px`; // even more for shirts
if (~sprite.name.indexOf('hair_base')) {
let styleArray = sprite.name.split('_').slice(2,3);
if (Number(styleArray[0]) > 14)
sprite.custom.px.offset_y = `-${ sprite.y }px`; // don't crop updos
}
}

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

@@ -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

@@ -0,0 +1,75 @@
var migrationName = '20161102_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:['d1be0965-e909-4d30-82fa-9a0011f885b2']}
};
// 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.head_special_takeThis !== 'undefined') {
set = {'migration':migrationName, 'items.gear.owned.body_special_takeThis':false};
} else 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,74 @@
var migrationName = '20161122_turkey_ladder.js';
var authorName = 'Sabe'; // in case script author needs to know when their ...
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
/*
* Yearly Turkey Day award. Turkey pet, Turkey mount, Gilded Turkey pet, Gilded Turkey mount
*/
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},
'auth.timestamps.loggedin':{$gt:new Date('2016-10-31')} // Extend timeframe each run of migration
};
// specify fields we are interested in to limit retrieved data (empty if we're not reading data):
var fields = {
'migration': 1,
'items.mounts': 1,
'items.pets': 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 (user.items.pets['Turkey-Gilded']) {
set = {'migration':migrationName, 'items.mounts.Turkey-Gilded':true};
} else if (user.items.mounts['Turkey-Base']) {
set = {'migration':migrationName, 'items.pets.Turkey-Gilded':5};
} else if (user.items.pets['Turkey-Base']) {
set = {'migration':migrationName, 'items.mounts.Turkey-Base':true};
} else {
set = {'migration':migrationName, 'items.pets.Turkey-Base':5};
}
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

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

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();
}

5508
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.44.2",
"version": "3.56.0",
"main": "./website/server/index.js",
"dependencies": {
"@slack/client": "3.6.0",
@@ -35,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",
@@ -64,11 +64,13 @@
"image-size": "~0.3.2",
"in-app-purchase": "^1.1.6",
"jade": "~1.11.0",
"jquery": "https://registry.npmjs.org/jquery/-/jquery-3.1.1.tgz",
"js2xmlparser": "~1.0.0",
"json-loader": "^0.5.4",
"less": "^2.7.1",
"less-loader": "^2.2.3",
"lodash": "^3.10.1",
"lodash.pickby": "^4.2.0",
"lodash.setwith": "^4.2.0",
"merge-stream": "^1.0.0",
"method-override": "^2.3.5",
@@ -89,12 +91,13 @@
"passport-google-oauth20": "1.0.0",
"paypal-ipn": "3.0.0",
"paypal-rest-sdk": "^1.2.1",
"postcss-easy-import": "^1.0.1",
"pretty-data": "^0.40.0",
"ps-tree": "^1.0.0",
"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",
@@ -128,6 +131,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",
@@ -203,7 +207,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

@@ -5,7 +5,7 @@ import {
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
describe('GET challenges/group/:groupId', () => {
describe('GET challenges/groups/:groupId', () => {
context('Public Guild', () => {
let publicGuild, user, nonMember, challenge, challenge2;

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

@@ -10,12 +10,17 @@ describe('GET /export/history.csv', () => {
it('should return a valid CSV file with tasks history data', async () => {
let user = await generateUser();
let tasks = await user.post('/tasks/user', [
{type: 'habit', text: 'habit 1'},
{type: 'daily', text: 'daily 1'},
{type: 'habit', text: 'habit 1'},
{type: 'habit', text: 'habit 2'},
{type: 'todo', text: 'todo 1'},
]);
// to handle occasional inconsistency in task creation order
tasks.sort(function (a, b) {
return a.text.localeCompare(b.text);
});
// score all the tasks twice
await user.post(`/tasks/${tasks[0]._id}/score/up`);
await user.post(`/tasks/${tasks[1]._id}/score/up`);
@@ -28,7 +33,7 @@ describe('GET /export/history.csv', () => {
await user.post(`/tasks/${tasks[3]._id}/score/up`);
// adding an history entry to daily 1 manually because cron didn't run yet
await updateDocument('tasks', tasks[1], {
await updateDocument('tasks', tasks[0], {
history: [{value: 3.2, date: Number(new Date())}],
});
@@ -41,11 +46,11 @@ describe('GET /export/history.csv', () => {
let splitRes = res.split('\n');
expect(splitRes[0]).to.equal('Task Name,Task ID,Task Type,Date,Value');
expect(splitRes[1]).to.equal(`habit 1,${tasks[0]._id},habit,${moment(tasks[0].history[0].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[0].history[0].value}`);
expect(splitRes[2]).to.equal(`habit 1,${tasks[0]._id},habit,${moment(tasks[0].history[1].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[0].history[1].value}`);
expect(splitRes[3]).to.equal(`habit 2,${tasks[2]._id},habit,${moment(tasks[2].history[0].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[2].history[0].value}`);
expect(splitRes[4]).to.equal(`habit 2,${tasks[2]._id},habit,${moment(tasks[2].history[1].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[2].history[1].value}`);
expect(splitRes[5]).to.equal(`daily 1,${tasks[1]._id},daily,${moment(tasks[1].history[0].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[1].history[0].value}`);
expect(splitRes[1]).to.equal(`daily 1,${tasks[0]._id},daily,${moment(tasks[0].history[0].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[0].history[0].value}`);
expect(splitRes[2]).to.equal(`habit 1,${tasks[1]._id},habit,${moment(tasks[1].history[0].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[1].history[0].value}`);
expect(splitRes[3]).to.equal(`habit 1,${tasks[1]._id},habit,${moment(tasks[1].history[1].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[1].history[1].value}`);
expect(splitRes[4]).to.equal(`habit 2,${tasks[2]._id},habit,${moment(tasks[2].history[0].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[2].history[0].value}`);
expect(splitRes[5]).to.equal(`habit 2,${tasks[2]._id},habit,${moment(tasks[2].history[1].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[2].history[1].value}`);
expect(splitRes[6]).to.equal('');
});
});

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

@@ -15,7 +15,7 @@ describe('GET /shops/seasonal', () => {
expect(shop.identifier).to.equal('seasonalShop');
expect(shop.text).to.eql(t('seasonalShop'));
expect(shop.notes).to.eql(t('seasonalShopFallText'));
expect(shop.notes).to.be.a('string');
expect(shop.imageName).to.be.a('string');
expect(shop.categories).to.be.an('array');
});

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

@@ -5,12 +5,17 @@ import {
translate as t,
} from '../../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
import { find } from 'lodash';
describe('POST /tasks/challenge/:challengeId', () => {
let user;
let guild;
let challenge;
function findUserChallengeTask (memberTask) {
return memberTask.challenge.id === challenge._id;
}
beforeEach(async () => {
user = await generateUser({balance: 1});
guild = await generateGroup(user);
@@ -88,6 +93,9 @@ describe('POST /tasks/challenge/:challengeId', () => {
});
let challengeWithTask = await user.get(`/challenges/${challenge._id}`);
let memberTasks = await user.get('/tasks/user');
let userChallengeTask = find(memberTasks, findUserChallengeTask);
expect(challengeWithTask.tasksOrder.habits.indexOf(task._id)).to.be.above(-1);
expect(task.challenge.id).to.equal(challenge._id);
expect(task.text).to.eql('test habit');
@@ -95,6 +103,8 @@ describe('POST /tasks/challenge/:challengeId', () => {
expect(task.type).to.eql('habit');
expect(task.up).to.eql(false);
expect(task.down).to.eql(true);
expect(userChallengeTask.notes).to.eql(task.notes);
});
it('creates a todo', async () => {
@@ -105,11 +115,16 @@ describe('POST /tasks/challenge/:challengeId', () => {
});
let challengeWithTask = await user.get(`/challenges/${challenge._id}`);
let memberTasks = await user.get('/tasks/user');
let userChallengeTask = find(memberTasks, findUserChallengeTask);
expect(challengeWithTask.tasksOrder.todos.indexOf(task._id)).to.be.above(-1);
expect(task.challenge.id).to.equal(challenge._id);
expect(task.text).to.eql('test todo');
expect(task.notes).to.eql('1976');
expect(task.type).to.eql('todo');
expect(userChallengeTask.notes).to.eql(task.notes);
});
it('creates a daily', async () => {
@@ -124,6 +139,9 @@ describe('POST /tasks/challenge/:challengeId', () => {
});
let challengeWithTask = await user.get(`/challenges/${challenge._id}`);
let memberTasks = await user.get('/tasks/user');
let userChallengeTask = find(memberTasks, findUserChallengeTask);
expect(challengeWithTask.tasksOrder.dailys.indexOf(task._id)).to.be.above(-1);
expect(task.challenge.id).to.equal(challenge._id);
expect(task.text).to.eql('test daily');
@@ -132,5 +150,7 @@ describe('POST /tasks/challenge/:challengeId', () => {
expect(task.frequency).to.eql('daily');
expect(task.everyX).to.eql(5);
expect(new Date(task.startDate)).to.eql(now);
expect(userChallengeTask.notes).to.eql(task.notes);
});
});

View File

@@ -0,0 +1,58 @@
import {
createAndPopulateGroup,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import { find } from 'lodash';
describe('GET /approvals/group/:groupId', () => {
let user, guild, member, task, syncedTask;
function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
}
beforeEach(async () => {
let {group, members, groupLeader} = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
},
members: 1,
});
guild = group;
user = groupLeader;
member = members[0];
task = await user.post(`/tasks/group/${guild._id}`, {
text: 'test todo',
type: 'todo',
requiresApproval: true,
});
await user.post(`/tasks/${task._id}/assign/${member._id}`);
let memberTasks = await member.get('/tasks/user');
syncedTask = find(memberTasks, findAssignedTask);
try {
await member.post(`/tasks/${syncedTask._id}/score/up`);
} catch (e) {
// eslint-disable-next-line no-empty
}
});
it('errors when user is not the group leader', async () => {
await expect(member.get(`/approvals/group/${guild._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlyGroupLeaderCanEditTasks'),
});
});
it('gets a list of task that need approval', async () => {
let approvals = await user.get(`/approvals/group/${guild._id}`);
expect(approvals[0]._id).to.equal(syncedTask._id);
});
});

View File

@@ -0,0 +1,70 @@
import {
createAndPopulateGroup,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import { find } from 'lodash';
describe('POST /tasks/:id/approve/:userId', () => {
let user, guild, member, task;
function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
}
beforeEach(async () => {
let {group, members, groupLeader} = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
},
members: 1,
});
guild = group;
user = groupLeader;
member = members[0];
task = await user.post(`/tasks/group/${guild._id}`, {
text: 'test todo',
type: 'todo',
requiresApproval: true,
});
});
it('errors when user is not assigned', async () => {
await expect(user.post(`/tasks/${task._id}/approve/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 404,
error: 'NotFound',
message: t('taskNotFound'),
});
});
it('errors when user is not the group leader', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await expect(member.post(`/tasks/${task._id}/approve/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlyGroupLeaderCanEditTasks'),
});
});
it('approves an assigned user', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await user.post(`/tasks/${task._id}/approve/${member._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await member.sync();
expect(member.notifications.length).to.equal(1);
expect(member.notifications[0].type).to.equal('GROUP_TASK_APPROVED');
expect(member.notifications[0].data.message).to.equal(t('yourTaskHasBeenApproved'));
expect(syncedTask.group.approval.approved).to.be.true;
expect(syncedTask.group.approval.approvingUser).to.equal(user._id);
expect(syncedTask.group.approval.dateApproved).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
});
});

View File

@@ -0,0 +1,93 @@
import {
createAndPopulateGroup,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import { find } from 'lodash';
describe('POST /tasks/:id/score/:direction', () => {
let user, guild, member, task;
function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
}
beforeEach(async () => {
let {group, members, groupLeader} = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
},
members: 1,
});
guild = group;
user = groupLeader;
member = members[0];
task = await user.post(`/tasks/group/${guild._id}`, {
text: 'test todo',
type: 'todo',
requiresApproval: true,
});
await user.post(`/tasks/${task._id}/assign/${member._id}`);
});
it('prevents user from scoring a task that needs to be approved', async () => {
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
let updatedTask = await member.get(`/tasks/${syncedTask._id}`);
await user.sync();
expect(user.notifications.length).to.equal(1);
expect(user.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
expect(user.notifications[0].data.message).to.equal(t('userHasRequestedTaskApproval', {
user: member.auth.local.username,
taskName: updatedTask.text,
}));
expect(user.notifications[0].data.groupId).to.equal(guild._id);
expect(updatedTask.group.approval.requested).to.equal(true);
expect(updatedTask.group.approval.requestedDate).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
});
it('errors when approval has already been requested', async () => {
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskRequiresApproval'),
});
});
it('allows a user to score an apporoved task', async () => {
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await user.post(`/tasks/${task._id}/approve/${member._id}`);
await member.post(`/tasks/${syncedTask._id}/score/up`);
let updatedTask = await member.get(`/tasks/${syncedTask._id}`);
expect(updatedTask.completed).to.equal(true);
expect(updatedTask.dateCompleted).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
});
});

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

@@ -20,6 +20,6 @@ describe('POST /user/purchase-hourglass/:type/:key', () => {
expect(response.message).to.eql(t('hourglassPurchase'));
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
expect(user.items.pets).to.eql({'MantisShrimp-Base': 5});
expect(user.items.pets['MantisShrimp-Base']).to.eql(5);
});
});

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

@@ -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

@@ -277,6 +277,7 @@ describe('analyticsService', () => {
dailys: [{_id: 'daily'}],
todos: [{_id: 'todo'}],
rewards: [{_id: 'reward'}],
balance: 12,
};
data.user = user;
@@ -300,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

@@ -3,15 +3,27 @@ import * as api from '../../../../../website/server/libs/payments';
import analytics from '../../../../../website/server/libs/analyticsService';
import notifications from '../../../../../website/server/libs/pushNotifications';
import { model as User } from '../../../../../website/server/models/user';
import { model as Group } from '../../../../../website/server/models/group';
import moment from 'moment';
import {
generateGroup,
} from '../../../../helpers/api-unit.helper.js';
describe('payments/index', () => {
let user, data, plan;
let user, group, data, plan;
beforeEach(() => {
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
await group.save();
sandbox.stub(sender, 'sendTxn');
sandbox.stub(user, 'sendMessage');
sandbox.stub(analytics, 'trackPurchase');
@@ -80,6 +92,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;
@@ -168,6 +198,7 @@ describe('payments/index', () => {
expect(analytics.trackPurchase).to.be.calledOnce;
expect(analytics.trackPurchase).to.be.calledWith({
uuid: user._id,
groupId: undefined,
itemPurchased: 'Subscription',
sku: 'payment method-subscription',
purchaseType: 'subscribe',
@@ -210,6 +241,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 +268,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);
@@ -239,6 +289,7 @@ describe('payments/index', () => {
expect(analytics.trackPurchase).to.be.calledOnce;
expect(analytics.trackPurchase).to.be.calledWith({
uuid: user._id,
groupId: undefined,
itemPurchased: 'Subscription',
sku: 'payment method-subscription',
purchaseType: 'subscribe',
@@ -254,6 +305,53 @@ describe('payments/index', () => {
});
});
context('Purchasing a subscription for group', () => {
it('creates a subscription', async () => {
expect(group.purchased.plan.planId).to.not.exist;
data.groupId = group._id;
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.planId).to.eql('basic_3mo');
expect(updatedGroup.purchased.plan.customerId).to.eql('customer-id');
expect(updatedGroup.purchased.plan.dateUpdated).to.exist;
expect(updatedGroup.purchased.plan.gemsBought).to.eql(0);
expect(updatedGroup.purchased.plan.paymentMethod).to.eql('Payment Method');
expect(updatedGroup.purchased.plan.extraMonths).to.eql(0);
expect(updatedGroup.purchased.plan.dateTerminated).to.eql(null);
expect(updatedGroup.purchased.plan.lastBillingDate).to.not.exist;
expect(updatedGroup.purchased.plan.dateCreated).to.exist;
});
it('sets extraMonths if plan has dateTerminated date', async () => {
group.purchased.plan = plan;
group.purchased.plan.dateTerminated = moment(new Date()).add(2, 'months');
await group.save();
expect(group.purchased.plan.extraMonths).to.eql(0);
data.groupId = group._id;
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.extraMonths).to.within(1.9, 2);
});
it('does not set negative extraMonths if plan has past dateTerminated date', async () => {
group.purchased.plan = plan;
group.purchased.plan.dateTerminated = moment(new Date()).subtract(2, 'months');
await group.save();
expect(group.purchased.plan.extraMonths).to.eql(0);
data.groupId = group._id;
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.extraMonths).to.eql(0);
});
});
context('Block subscription perks', () => {
it('adds block months to plan.consecutive.offset', async () => {
await api.createSubscription(data);
@@ -389,61 +487,136 @@ describe('payments/index', () => {
data = { user };
});
it('adds a month termination date by default', async () => {
await api.cancelSubscription(data);
context('Canceling a subscription for self', () => {
it('adds a month termination date by default', async () => {
await api.cancelSubscription(data);
let now = new Date();
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
let now = new Date();
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
expect(daysTillTermination).to.be.within(29, 30); // 1 month +/- 1 days
expect(daysTillTermination).to.be.within(29, 30); // 1 month +/- 1 days
});
it('adds extraMonths to dateTerminated value', async () => {
user.purchased.plan.extraMonths = 2;
await api.cancelSubscription(data);
let now = new Date();
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
expect(daysTillTermination).to.be.within(89, 90); // 3 months +/- 1 days
});
it('handles extra month fractions', async () => {
user.purchased.plan.extraMonths = 0.3;
await api.cancelSubscription(data);
let now = new Date();
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
expect(daysTillTermination).to.be.within(38, 39); // should be about 1 month + 1/3 month
});
it('terminates at next billing date if it exists', async () => {
data.nextBill = moment().add({ days: 15 });
await api.cancelSubscription(data);
let now = new Date();
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
expect(daysTillTermination).to.be.within(13, 15);
});
it('resets plan.extraMonths', async () => {
user.purchased.plan.extraMonths = 5;
await api.cancelSubscription(data);
expect(user.purchased.plan.extraMonths).to.eql(0);
});
it('sends an email', async () => {
await api.cancelSubscription(data);
expect(sender.sendTxn).to.be.calledOnce;
expect(sender.sendTxn).to.be.calledWith(user, 'cancel-subscription');
});
});
it('adds extraMonths to dateTerminated value', async () => {
user.purchased.plan.extraMonths = 2;
context('Canceling a subscription for group', () => {
it('adds a month termination date by default', async () => {
data.groupId = group._id;
await api.cancelSubscription(data);
await api.cancelSubscription(data);
let now = new Date();
let updatedGroup = await Group.findById(group._id).exec();
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
let now = new Date();
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
expect(daysTillTermination).to.be.within(29, 30); // 1 month +/- 1 days
});
expect(daysTillTermination).to.be.within(89, 90); // 3 months +/- 1 days
});
it('adds extraMonths to dateTerminated value', async () => {
group.purchased.plan.extraMonths = 2;
await group.save();
data.groupId = group._id;
it('handles extra month fractions', async () => {
user.purchased.plan.extraMonths = 0.3;
await api.cancelSubscription(data);
await api.cancelSubscription(data);
let now = new Date();
let updatedGroup = await Group.findById(group._id).exec();
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
let now = new Date();
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
expect(daysTillTermination).to.be.within(89, 90); // 3 months +/- 1 days
});
expect(daysTillTermination).to.be.within(38, 39); // should be about 1 month + 1/3 month
});
it('handles extra month fractions', async () => {
group.purchased.plan.extraMonths = 0.3;
await group.save();
data.groupId = group._id;
it('terminates at next billing date if it exists', async () => {
data.nextBill = moment().add({ days: 15 });
await api.cancelSubscription(data);
await api.cancelSubscription(data);
let now = new Date();
let updatedGroup = await Group.findById(group._id).exec();
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
let now = new Date();
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
expect(daysTillTermination).to.be.within(38, 39); // should be about 1 month + 1/3 month
});
expect(daysTillTermination).to.be.within(13, 15);
});
it('terminates at next billing date if it exists', async () => {
data.nextBill = moment().add({ days: 15 });
data.groupId = group._id;
it('resets plan.extraMonths', async () => {
user.purchased.plan.extraMonths = 5;
await api.cancelSubscription(data);
await api.cancelSubscription(data);
let now = new Date();
let updatedGroup = await Group.findById(group._id).exec();
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
expect(user.purchased.plan.extraMonths).to.eql(0);
});
expect(daysTillTermination).to.be.within(13, 15);
});
it('sends an email', async () => {
await api.cancelSubscription(data);
it('resets plan.extraMonths', async () => {
group.purchased.plan.extraMonths = 5;
await group.save();
data.groupId = group._id;
expect(sender.sendTxn).to.be.calledOnce;
expect(sender.sendTxn).to.be.calledWith(user, 'cancel-subscription');
await api.cancelSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.extraMonths).to.eql(0);
});
it('sends an email', async () => {
data.groupId = group._id;
await api.cancelSubscription(data);
expect(sender.sendTxn).to.be.calledOnce;
expect(sender.sendTxn).to.be.calledWith(user, 'cancel-subscription');
});
});
});

View File

@@ -8,27 +8,33 @@ 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',
},
preferences: {
language: 'flagger-lang',
},
},
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,19 +43,15 @@ 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({
text: 'flagger (flagger-id) flagged a message',
text: 'flagger (flagger-id) flagged a message (language: flagger-lang)',
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 +64,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 +77,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 +92,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 +111,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

@@ -6,32 +6,36 @@ import common from '../../../../../website/common/';
import { each, find } from 'lodash';
describe('Challenge Model', () => {
let guild, leader, challenge, task, tasksToTest;
let guild, leader, challenge, task;
let tasksToTest = {
habit: {
text: 'test habit',
type: 'habit',
up: false,
down: true,
notes: 'test notes',
},
todo: {
text: 'test todo',
type: 'todo',
notes: 'test notes',
},
daily: {
text: 'test daily',
type: 'daily',
frequency: 'daily',
everyX: 5,
startDate: new Date(),
notes: 'test notes',
},
reward: {
text: 'test reward',
type: 'reward',
notes: 'test notes',
},
};
beforeEach(async () => {
tasksToTest = {
habit: {
text: 'test habit',
type: 'habit',
up: false,
down: true,
},
todo: {
text: 'test todo',
type: 'todo',
},
daily: {
text: 'test daily',
type: 'daily',
frequency: 'daily',
everyX: 5,
startDate: new Date(),
},
reward: {
text: 'test reward',
type: 'reward',
},
};
guild = new Group({
name: 'test party',
type: 'guild',
@@ -77,6 +81,7 @@ describe('Challenge Model', () => {
});
expect(syncedTask).to.exist;
expect(syncedTask.notes).to.eql(task.notes);
});
it('syncs a challenge to a user', async () => {
@@ -96,8 +101,8 @@ describe('Challenge Model', () => {
});
expect(updatedNewMember.challenges).to.contain(challenge._id);
expect(updatedNewMember.tags[3].id).to.equal(challenge._id);
expect(updatedNewMember.tags[3].name).to.equal(challenge.shortName);
expect(updatedNewMember.tags[7].id).to.equal(challenge._id);
expect(updatedNewMember.tags[7].name).to.equal(challenge.shortName);
expect(syncedTask).to.exist;
});

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

@@ -107,6 +107,7 @@ describe('Group Task Methods', () => {
let updatedTaskName = 'Update Task name';
task.text = updatedTaskName;
task.group.approval.required = true;
await guild.updateTask(task);
@@ -121,10 +122,12 @@ describe('Group Task Methods', () => {
expect(task.group.assignedUsers).to.contain(leader._id);
expect(syncedTask).to.exist;
expect(syncedTask.text).to.equal(task.text);
expect(syncedTask.group.approval.required).to.equal(true);
expect(task.group.assignedUsers).to.contain(newMember._id);
expect(syncedMemberTask).to.exist;
expect(syncedMemberTask.text).to.equal(task.text);
expect(syncedMemberTask.group.approval.required).to.equal(true);
});
it('removes an assigned task and unlinks assignees', async () => {

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

@@ -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();

View File

@@ -36,7 +36,10 @@ webpackConfig.module.preLoaders = webpackConfig.module.preLoaders || [];
webpackConfig.module.preLoaders.unshift({
test: /\.js$/,
loader: 'isparta',
include: path.resolve(projectRoot, 'website/client'),
include: [
path.resolve(projectRoot, 'website/client'),
path.resolve(projectRoot, 'website/common'),
],
});
// only apply babel for test files when using isparta

View File

@@ -1,55 +0,0 @@
import randomVal from '../../../website/common/script/libs/randomVal';
import {
generateUser,
} from '../../helpers/common.helper';
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('uses Math.random to determine the property', () => {
sandbox.spy(Math, 'random');
randomVal(obj);
expect(Math.random).to.be.calledOnce;
});
it('can pass in a custom random function that takes in the user and a seed argument', () => {
let user = generateUser();
let randomSpy = sandbox.stub().returns(0.3);
sandbox.spy(Math, 'random');
let result = randomVal(obj, {
user,
seed: 100,
predictableRandom: randomSpy,
});
expect(Math.random).to.not.be.called;
expect(randomSpy).to.be.calledOnce;
expect(result).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

@@ -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

@@ -18,7 +18,7 @@ describe('shared.ops.blockUser', () => {
it('validates uuid', (done) => {
try {
blockUser(user, { params: { uuid: 1 } });
blockUser(user, { params: { uuid: '1' } });
} catch (error) {
expect(error.message).to.eql(i18n.t('invalidUUID'));
done();

View File

@@ -5,6 +5,7 @@ import {
} from '../../helpers/common.helper';
import count from '../../../website/common/script/count';
import buyArmoire from '../../../website/common/script/ops/buyArmoire';
import randomVal from '../../../website/common/script/libs/randomVal';
import content from '../../../website/common/script/content/index';
import {
NotAuthorized,
@@ -43,11 +44,11 @@ describe('shared.ops.buyArmoire', () => {
user.stats.exp = 0;
user.items.food = {};
sandbox.stub(Math, 'random');
sandbox.stub(randomVal, 'trueRandom');
});
afterEach(() => {
Math.random.restore();
randomVal.trueRandom.restore();
});
context('failure conditions', () => {
@@ -89,7 +90,7 @@ describe('shared.ops.buyArmoire', () => {
context('non-gear awards', () => {
it('gives Experience', () => {
let previousExp = user.stats.exp;
Math.random.returns(YIELD_EXP);
randomVal.trueRandom.returns(YIELD_EXP);
buyArmoire(user);
@@ -102,7 +103,7 @@ describe('shared.ops.buyArmoire', () => {
it('gives food', () => {
let previousExp = user.stats.exp;
Math.random.returns(YIELD_FOOD);
randomVal.trueRandom.returns(YIELD_FOOD);
buyArmoire(user);
@@ -113,7 +114,7 @@ describe('shared.ops.buyArmoire', () => {
});
it('does not give equipment if all equipment has been found', () => {
Math.random.returns(YIELD_EQUIPMENT);
randomVal.trueRandom.returns(YIELD_EQUIPMENT);
user.items.gear.owned = getFullArmoire();
user.stats.gp = 150;
@@ -131,7 +132,7 @@ describe('shared.ops.buyArmoire', () => {
context('gear awards', () => {
it('always drops equipment the first time', () => {
delete user.flags.armoireOpened;
Math.random.returns(YIELD_EXP);
randomVal.trueRandom.returns(YIELD_EXP);
expect(_.size(user.items.gear.owned)).to.equal(1);
@@ -148,7 +149,7 @@ describe('shared.ops.buyArmoire', () => {
});
it('gives more equipment', () => {
Math.random.returns(YIELD_EQUIPMENT);
randomVal.trueRandom.returns(YIELD_EQUIPMENT);
user.items.gear.owned = {
weapon_warrior_0: true,
head_armoire_hornedIronHelm: true,

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

@@ -11,7 +11,7 @@ import {
} from '../../helpers/common.helper';
describe('shared.ops.purchase', () => {
const SEASONAL_FOOD = 'Candy_Base';
const SEASONAL_FOOD = 'Meat';
let user;
let goldPoints = 40;
let gemsBought = 40;

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

@@ -1,3 +1,5 @@
/* eslint-disable camelcase */
import revive from '../../../website/common/script/ops/revive';
import i18n from '../../../website/common/script/i18n';
import {
@@ -53,6 +55,22 @@ describe('shared.ops.revive', () => {
expect(user.stats.str).to.equal(1);
});
it('it decreases a random stat from str, con, per, int by one', () => {
let stats = ['str', 'con', 'per', 'int'];
_.each(stats, (s) => {
user.stats[s] = 1;
});
revive(user);
let statSum = _.reduce(stats, (m, k) => {
return m + user.stats[k];
}, 0);
expect(statSum).to.equal(3);
});
it('removes a random item from user gear owned', () => {
let weaponKey = 'weapon_warrior_0';
user.items.gear.owned[weaponKey] = true;
@@ -63,7 +81,60 @@ 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', () => {
user.items.gear.owned = {
eyewear_special_yellowTopFrame: true,
};
revive(user);
expect(user.items.gear.owned.eyewear_special_yellowTopFrame).to.be.true;
});
it('allows removing warrior sword (0 value item)', () => {
user.items.gear.owned = {
weapon_warrior_0: true,
};
let weaponKey = 'weapon_warrior_0';
let [, message] = revive(user);
expect(message).to.equal(i18n.t('messageLostItem', { itemText: content.gear.flat[weaponKey].text()}));
expect(user.items.gear.owned[weaponKey]).to.be.false;
});
it('does not remove items of a different class', () => {
let weaponKey = 'weapon_wizard_1';
user.items.gear.owned[weaponKey] = true;
let [, message] = revive(user);
expect(message).to.equal('');
expect(user.items.gear.owned[weaponKey]).to.be.true;
});
it('removes "special" items', () => {
let weaponKey = 'weapon_special_1';
user.items.gear.owned[weaponKey] = true;
let [, message] = revive(user);
expect(message).to.equal(i18n.t('messageLostItem', { itemText: content.gear.flat[weaponKey].text()}));
expect(user.items.gear.owned[weaponKey]).to.be.false;
});
it('removes "armoire" items', () => {
let weaponKey = 'armor_armoire_goldenToga';
user.items.gear.owned[weaponKey] = true;
let [, message] = revive(user);
expect(message).to.equal(i18n.t('messageLostItem', { itemText: content.gear.flat[weaponKey].text()}));
expect(user.items.gear.owned[weaponKey]).to.be.false;
});
it('dequips lost item from user if user had it equipped', () => {
let weaponKey = 'weapon_warrior_0';
let itemToLose = content.gear.flat[weaponKey];
@@ -76,7 +147,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

@@ -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

@@ -2,6 +2,7 @@ var path = require('path');
var config = require('./config');
var utils = require('./utils');
var projectRoot = path.resolve(__dirname, '../');
var webpack = require('webpack');
var IS_PROD = process.env.NODE_ENV === 'production';
var baseConfig = {
@@ -17,6 +18,7 @@ var baseConfig = {
extensions: ['', '.js', '.vue'],
fallback: [path.join(__dirname, '../node_modules')],
alias: {
jquery: 'jquery/src/jquery',
client: path.resolve(__dirname, '../website/client'),
assets: path.resolve(__dirname, '../website/client/assets'),
components: path.resolve(__dirname, '../website/client/components'),
@@ -25,6 +27,12 @@ var baseConfig = {
resolveLoader: {
fallback: [path.join(__dirname, '../node_modules')],
},
plugins: [
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
}),
],
module: {
preLoaders: !IS_PROD ? [
{
@@ -79,6 +87,9 @@ var baseConfig = {
require('autoprefixer')({
browsers: ['last 2 versions'],
}),
require('postcss-easy-import')({
glob: true,
}),
],
},
};

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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