Compare commits

..

190 Commits

Author SHA1 Message Date
SabreCat 168ed02226 3.90.1 2017-05-09 19:30:31 +00:00
Sabe Jones aa253cf72e chore(i18n): update locales 2017-05-09 19:25:30 +00:00
SabreCat 247742c60d chore(news): Bailey
also adds Managers info to Group Plans page
2017-05-09 19:18:29 +00:00
yugensoft 7d89deb094 Continuation of PR #8675, fix internationalization (#8698)
* Made uneditable habits show counter reset frequency
Added counter reset frequency to counter tooltip
Solves issue https://github.com/HabitRPG/habitica/issues/8571

* Internationalization fix, pursuant to https://github.com/HabitRPG/habitica/pull/8675#discussion_r112982947

(cherry picked from commit 2adac35e31)
2017-05-09 18:54:38 +00:00
Sabe Jones c128b701fa chore(i18n): update locales 2017-05-09 18:52:38 +00:00
SabreCat f01e13ffc0 3.90.0 2017-05-09 17:53:03 +00:00
Alys 4e9d631b71 adjust banned words list -- TRIGGER / CONTENT WARNING: words about assault, slurs, etc 2017-05-09 09:21:06 +10:00
SabreCat f1de7c02e9 Merge branch 'develop' into release 2017-05-08 23:14:45 +00:00
yugensoft 4f305bd505 Updated vagrant script to fix bcrypt missing error (#8670)
* Updated npm version to match http://habitica.wikia.com/wiki/Setting_up_Habitica_Locally; added npm-pre-gyp dependency necessary for bcrypt to install

* changed npm version as per request https://github.com/HabitRPG/habitica/pull/8670#discussion_r112010106
2017-05-07 07:57:50 +10:00
Alys 024feaa2f4 add banned words to be blocked in Tavern Chat -- CONTENT WARNING: this commit contains slurs, swear words, etc 2017-05-06 07:37:17 +10:00
Sabe Jones d2dc8f1856 Release Mergeback 2017/05/03 (#8716)
* Content 2017/05 (#8714)

* feat(content): May 2017
Add Armoire, add BGs, end Spring Fling

* chore(sprites): compile

* chore(i18n): update locales

* 3.89.0
2017-05-03 15:02:54 -05:00
SabreCat 364ed8dbab 3.89.0 2017-05-03 18:22:12 +00:00
Sabe Jones 73328b6dab Content 2017/05 (#8714)
* feat(content): May 2017
Add Armoire, add BGs, end Spring Fling

* chore(sprites): compile

* chore(i18n): update locales
2017-05-03 13:21:25 -05:00
Keith Holliday e3a08c1905 Added loggly integration (#8690)
* Added loggly integration

* Added back console logger

* Updated shrinkwrap
2017-05-02 09:17:23 -06:00
Sabe Jones dad0eea9e0 Merge branch 'release' into develop 2017-04-28 19:31:22 +00:00
Sabe Jones 76849cdcaa 3.88.1 2017-04-28 19:30:33 +00:00
Sabe Jones eb6ac42717 chore(i18n): update locales 2017-04-28 19:29:56 +00:00
SabreCat d44a298e2d chore(news): Bailey 2017-04-28 19:24:32 +00:00
Alys 864ca91144 modify Market intro for eggs to clarify that quest eggs don't drop (#8680)
Ref: https://habitica.slack.com/archives/C02RK7DKF/p1491847171061587?thread_ts=1491779437.902223&cid=C02RK7DKF
2017-04-27 21:10:43 +10:00
Cai Lu 78816dd4cb Fix character values (#8541)
* Display an error when invalid values are given

* Add character values tests for settingsCtrl

* Only check for invalid level values

* Remove unnecessary validation function

* Min level of 1 on user model

* (tests) Removed stubbing on restore
2017-04-26 15:10:56 -06:00
Keith Holliday 6a99daebac continuation of PR #8074 Adding spam prevention - fixes #8060 (#8687)
* Adding code to look over the most recent messages to look for spam from a user

* Adding in translatable error message

* Adding 2 tests for spam detection

* Fixing changes requested for pull request

* Adding unit tests for group and fixing requested changes

* Fixing message and tests

* Forgot to remove this import

* Fixing lint errors

* Cleaning up the code and tests to be more readable

* Fixing lint errors

* Fixed linting issues

* Syntax fixes

* Updated grammar
2017-04-26 13:37:18 -06:00
SabreCat c9ee6c7f73 Merge branch 'release' into develop 2017-04-26 17:40:51 +00:00
SabreCat ffc4618657 3.88.0 2017-04-26 02:07:17 +00:00
Sabe Jones 30fde273b8 chore(i18n): update locales 2017-04-26 02:05:38 +00:00
SabreCat 38573ad357 chore(sprites): compile 2017-04-26 01:57:19 +00:00
SabreCat c23180e6eb chore(news): Bailey 2017-04-26 01:56:59 +00:00
SabreCat f7e2a0464f fix(incentives): backfill
Also adds various missing data from prior commit
2017-04-26 01:04:02 +00:00
SabreCat 6994c6769a feat(content): More Check-in Incentives 2017-04-25 22:13:07 +00:00
SabreCat 984e7f8005 feat(content): Subscriber Items 2017-04 2017-04-25 22:11:20 +00:00
Keith Holliday e2f4b0e3dc Group managers (#8591)
* Added abiltiy to add group managers

* Added ability to remove managers

* Added ability for managers to add group tasks

* Allower managers to assign tasks

* Allowed managers to unassign tasks

* Allow managers to delete group tasks

* Allowed managers to approve

* Added initial ui

* Added approval view for managers

* Allowed managers to edit

* Fixed lint issues

* Added spacing to buttons

* Removed leader from selection of group managers

* Code review updates

* Ensured approvals are only done once

* Added ability for parties to add managers

* Add notifications to all managers when approval is requests

* Removed tasks need approval notifications from all managers when task is approve

* Fixed linting issues

* Hid add managers UI from groups that are not subscribed

* Removed let from front end

* Fixed issues with post task url params

* Fixed string locales

* Removed extra limited strings

* Added cannotedit tasks function

* Added limit fields and notification check by taskId

* Localized string and other minor issues

* Added manager and leader indicator

* Added group notifications refresh on sync

* Added close button for group notifications

* Removed group approval notifications when manager is removed

* Moved leader/manager indicators to after hp

* Added manager fields to groups

* Spelling and syntax fixes
2017-04-25 08:28:56 -06:00
Keith Holliday 369702884a Prevented ios and android subs from being upgrade to group plans (#8646)
* Prevented ios and android subs from being upgrade to group plans

* Add admin email sender

* Updated admin email

* Updated get user email

* Fixed grammar issue
2017-04-24 08:01:27 -06:00
Keith Holliday d438990d18 Added block when user types a swear word listed in banned words (#8197)
* Added block when user types a swear word listed in banned words

* Moved banned words check to server

* Removed unused code

* Moved banned words to separate file and fixed grammar.

* Updated chat test

* Changed error to BadRequest

* Fixed regex matching

* Updated test banned word

* Moved banned words and cached regex

* Updated banned word message

* Add ban filter only for tavern

* Added tavern id constant

* Added more tests for banned words

* Added warning to banned words

* Added alert

* Added new regex to capture markdown

* Fixed lint, spelling and importing
2017-04-24 07:55:42 -06:00
yugensoft 7d3213fd66 Made uneditable habits show counter reset frequency (#8675)
Added counter reset frequency to counter tooltip
Solves issue https://github.com/HabitRPG/habitica/issues/8571
2017-04-22 14:01:06 -05:00
Alys bde4eafc05 add mountText for all drop eggs and quest eggs (#8678)
I've checked that all these added strings do exist in locales/en/content.json
2017-04-22 14:00:18 -05:00
Travis 5a2ba27808 Creating api for gifting Hall Of Heroes Item Reference (#8118)
* Creating specific api to vend item reference for gifting items to a user through the hall of heroes. closes #8100

* removed 'When in doubt, ask Tyler' from the hall of heroes line

* Moving hall of heroes item path call from server side to client side

* updating let to var

* Addressing PR comments
2017-04-22 13:57:41 -05:00
Keith Holliday 024268a51e Added wintery skins to content (#8645)
* Added wintery skins to content

* Removed features json

* Fixed some name issues
2017-04-22 13:54:57 -05:00
MathWhiz f7281e71e8 Change icon for NPC achievement (#8628)
* Change icon for NPC achievement

* Add npc icon
2017-04-22 13:52:45 -05:00
SabreCat f3712c0641 3.87.1 2017-04-21 20:40:03 +00:00
Sabe Jones 7df10d51b0 PRs 2017/04/07 - 2017/04/14 (#8691)
* Issue 8432: Pre-test for initial fix

* Add failing test, seeking help/advice

* Fixes achievements injection. Now trying with Guide

* Fixes tests

* Remoed logging

* Mock ENV function

* Removes test focus

* Remove Beat Master unlock for Rebirth

* Special message when complete all check-in prizes

* Updated comment

* fix(test): unpend party cap test

* remove wrong subscriptions from gift modal

* edit the new issue template to emphasise that the Report a Bug guild should be used first (#8659)

* fix(translation): resolve merge conflict

* fix(news): merge conflict

* Added email invite limit (#8664)

* Added email invite limit

* change error message for sending too many invitations to instruct them to email us

* fix test error message to use variable in locales string

* add comment to warn about keeping INVITES_LIMIT low

If INVITES_LIMIT is allowed to be greater than MAX_EMAIL_INVITES_BY_USER
then the inviter can send more than MAX_EMAIL_INVITES_BY_USER invitations
at once.

* Fix for automatic allocation not persisting #8641 (#8661)

* Fixed text of check-in prize when it is a set of backgrounds (#8599)

* Fixed text of check-in prize when it is a set of backgrounds

* Use existing i18n string for BGs name

* Added user.preferences.language as second parameter

* fix Shiny Seeds info about achievement (not given to caster) (#8679)

Ref: https://habitica.slack.com/archives/C02RK7DKF/p1492032261365388

* Release mergeback v3.86.0 (#8685)

* 3.85.0

* New User Tasks for Mobile (#8682)

* feat(mobile): different default tasks

* fix(linting): missing space

* fix(user): correct client type logic

* test(integration): tasks by platform

* fix(test): remove only

* test(user): deeper checks on tasks

* refactor(test): whitespace for readability

* feat(subs): Jackalope Pets (#8684)

* chore(sprites): compile

* chore(i18n): update locales

* 3.86.0
2017-04-21 15:38:24 -05:00
SabreCat 91438aff90 Merge branch 'release' into develop 2017-04-20 22:21:33 +00:00
Sabe Jones 0c3f40716b 3.87.0 2017-04-20 21:29:01 +00:00
Sabe Jones ec306b614a chore(news): Bailey 2017-04-20 20:02:37 +00:00
Sabe Jones a6d8beff9d chore(i18n): update locales 2017-04-20 19:48:09 +00:00
SabreCat bebf03ee91 chore(sprites): compile 2017-04-20 19:39:19 +00:00
SabreCat 2ea35c673a fix(quest): strings and fetch items 2017-04-20 19:35:16 +00:00
SabreCat 90d15b18f8 feat(content): Fairy Potions, Mistiflying Quests 2017-04-20 19:01:51 +00:00
Sabe Jones 91ed55cf66 Release mergeback v3.86.0 (#8685)
* 3.85.0

* New User Tasks for Mobile (#8682)

* feat(mobile): different default tasks

* fix(linting): missing space

* fix(user): correct client type logic

* test(integration): tasks by platform

* fix(test): remove only

* test(user): deeper checks on tasks

* refactor(test): whitespace for readability

* feat(subs): Jackalope Pets (#8684)

* chore(sprites): compile

* chore(i18n): update locales

* 3.86.0
2017-04-18 19:23:24 -05:00
Sabe Jones 93aa37a164 3.86.0 2017-04-18 22:57:50 +00:00
Alys 4275da0a2e fix Shiny Seeds info about achievement (not given to caster) (#8679)
Ref: https://habitica.slack.com/archives/C02RK7DKF/p1492032261365388
2017-04-19 08:56:15 +10:00
Sabe Jones f782687609 chore(i18n): update locales 2017-04-18 22:50:18 +00:00
Sabe Jones 164fb69108 chore(sprites): compile 2017-04-18 22:43:33 +00:00
Sabe Jones b1678e1769 feat(subs): Jackalope Pets (#8684) 2017-04-18 17:38:53 -05:00
Sabe Jones 7f8851c72b New User Tasks for Mobile (#8682)
* feat(mobile): different default tasks

* fix(linting): missing space

* fix(user): correct client type logic

* test(integration): tasks by platform

* fix(test): remove only

* test(user): deeper checks on tasks

* refactor(test): whitespace for readability
2017-04-18 15:50:50 -05:00
Mateus Etto d9f48dcbb0 Fixed text of check-in prize when it is a set of backgrounds (#8599)
* Fixed text of check-in prize when it is a set of backgrounds

* Use existing i18n string for BGs name

* Added user.preferences.language as second parameter
2017-04-17 14:39:05 +10:00
Tyler Nychka bd6f901ccf Fix for automatic allocation not persisting #8641 (#8661) 2017-04-17 06:49:16 +10:00
SabreCat 884bf02961 Merge branch 'sabrecat/butterflies' into develop 2017-04-13 20:31:19 +00:00
SabreCat 4aad44e29e 3.85.0 2017-04-13 20:29:51 +00:00
Sabe Jones b40ee88165 chore(i18n): update locales 2017-04-13 20:28:14 +00:00
SabreCat 5d90aff51b chore(sprites): compile
Also add promo images to Bailey
2017-04-13 20:19:57 +00:00
SabreCat 9e36a531ea feat(content): Butterfly Pet Quest 2017-04-13 20:11:36 +00:00
Sabe Jones f9b40a699a 3.84.3 2017-04-12 21:09:05 +00:00
Alys 33380f63f6 add comment to warn about keeping INVITES_LIMIT low
If INVITES_LIMIT is allowed to be greater than MAX_EMAIL_INVITES_BY_USER
then the inviter can send more than MAX_EMAIL_INVITES_BY_USER invitations
at once.
2017-04-12 21:07:46 +00:00
Alys 2f010e4689 fix test error message to use variable in locales string 2017-04-12 21:07:35 +00:00
Alys fbda3a87ef change error message for sending too many invitations to instruct them to email us 2017-04-12 21:07:25 +00:00
TheHollidayInn 8a2e6a98c2 Added email invite limit 2017-04-12 21:03:10 +00:00
Keith Holliday 7d42e8fc71 Added email invite limit (#8664)
* Added email invite limit

* change error message for sending too many invitations to instruct them to email us

* fix test error message to use variable in locales string

* add comment to warn about keeping INVITES_LIMIT low

If INVITES_LIMIT is allowed to be greater than MAX_EMAIL_INVITES_BY_USER
then the inviter can send more than MAX_EMAIL_INVITES_BY_USER invitations
at once.
2017-04-12 15:54:35 -05:00
SabreCat 0442b87608 fix(news): merge conflict 2017-04-11 22:23:27 +00:00
SabreCat c1d1a3e14e fix(translation): resolve merge conflict 2017-04-11 22:04:55 +00:00
SabreCat dd5a9aa6cc Merge branch 'sabrecat/egg-quest' into develop 2017-04-11 20:10:49 +00:00
Alys 0bb6e5f3fc edit the new issue template to emphasise that the Report a Bug guild should be used first (#8659) 2017-04-11 14:52:19 -05:00
SabreCat ad0a51167d 3.84.2 2017-04-11 19:23:42 +00:00
SabreCat b7f1001b1a chore(news): Bailey 2017-04-11 19:23:28 +00:00
Sabe Jones 81ea1a0f9e chore(i18n): update locales 2017-04-11 19:02:17 +00:00
SabreCat a466d20935 Revert "Unlimit parties (#8653)"
This reverts commit a4feae4dbb.
2017-04-11 18:54:30 +00:00
SabreCat 635c0cf3d1 feat(event): enable Egg Hunt 2017-04-11 18:54:07 +00:00
Matteo Pagliazzi 69347df050 Merge branch 'Yutsuten-orb-rebirth' into develop 2017-04-11 17:10:50 +02:00
Matteo Pagliazzi 9cad5525e6 Merge branch 'orb-rebirth' of https://github.com/Yutsuten/habitica into Yutsuten-orb-rebirth 2017-04-11 17:10:42 +02:00
Matteo Pagliazzi b1e6aceffe Merge branch 'Yutsuten-checkin-complete-msg' into develop 2017-04-11 17:08:51 +02:00
Matteo Pagliazzi 17068875f4 Merge branch 'checkin-complete-msg' of https://github.com/Yutsuten/habitica into Yutsuten-checkin-complete-msg 2017-04-11 17:08:43 +02:00
Matteo Pagliazzi 342fc2e344 Merge branch 'paglias/fix-wrong-subs' into develop 2017-04-11 17:07:15 +02:00
Matteo Pagliazzi b8b1557e49 Merge branch 'develop' into paglias/fix-wrong-subs 2017-04-11 17:07:05 +02:00
Matteo Pagliazzi f90ef04e83 Merge branch 'eastwood-develop' into develop 2017-04-11 17:03:20 +02:00
Matteo Pagliazzi d0561512de Merge branch 'develop' of https://github.com/eastwood/habitica into eastwood-develop 2017-04-11 17:03:09 +02:00
Matteo Pagliazzi b84c672f33 remove wrong subscriptions from gift modal 2017-04-11 10:15:54 +02:00
Mateus Etto aafbb889be Merge branch 'develop' into checkin-complete-msg 2017-04-09 19:58:53 -03:00
SabreCat 8bfafa6df0 Merge branch 'viirus/androidFixes' into release 2017-04-06 22:15:14 +00:00
SabreCat 13865bcf49 Merge branch 'viirus/androidFixes' into develop 2017-04-06 22:10:02 +00:00
Phillip Thelen 78a99bf314 add assertion for newUser field on registration 2017-04-06 22:03:50 +02:00
Phillip Thelen 38edc5b416 fix linter error 2017-04-06 22:03:33 +02:00
SabreCat f9ca69196a fix(test): unpend party cap test 2017-04-06 19:58:33 +00:00
SabreCat 0e77df6e7b Merge branch 'sabrecat/shiny-seeds' into develop 2017-04-06 19:56:20 +00:00
SabreCat 91b6d3db02 3.84.1 2017-04-06 19:42:01 +00:00
Sabe Jones d16ce1ce48 chore(i18n): update locales 2017-04-06 19:41:13 +00:00
SabreCat 564c366bfb chore(sprites): compile 2017-04-06 19:35:16 +00:00
SabreCat c8c65a4f4f feat(event): Shiny Seeds 2017-04-06 19:35:05 +00:00
Phillip Thelen 8d168a0318 return newUser as true for new local accounts 2017-04-05 22:20:08 +02:00
Phillip Thelen a3dd2f497e fix setup process for new android users 2017-04-05 22:19:49 +02:00
Sabe Jones a4feae4dbb Unlimit parties (#8653)
* fix(party): unlimit parties
to address premature feature release

* fix(test): pend irrelevant test
2017-04-04 21:51:17 -05:00
Sabe Jones d8620e1636 v3.84.0 - Backgrounds and Armoire 2017/04 (#8652)
* Release Mergeback (#8644)

* Remove email addresses from translatable strings (#8448)

* Fix User > Profile showing {getProgressDisplay()}

* Remove bad nextRewardAt check

* 1st iteration of issue #8385 - more pending

* #8385 config and jade fixes, tests pending

* #8385 fixing lint errors

* Fix faqs string and test

* Fix faq.jade and add workaround for faq.js

* Fixing accidental checking for faq.js

* fix emails in faq.js

* fetch emails once in auth.js

* Fixing community manager email in auth.js

(cherry picked from commit 842fbe42a8)

* chore(i18n): update locales

(cherry picked from commit b2225f05e5)

* Merge branch 'stripe-webhook' into develop

(cherry picked from commit 30f514e46f)

* add recent Grand Gala seasonal special equipment names (#8606)

This is to help translators add good glossary entries now for keeping the current wiki pages consistent with future additions to the website's seasonal shop.
(cherry picked from commit 4846bc5769)

* stripe webhook for unpaid subs: add 3 days of remaining time

(cherry picked from commit 1d7b733759)

* New default background (#8597)

* feat(bgs): new default background

* feat(bgs): backfill migration

* fix(migration): extraneous imports, bad paths

* fix(bgs): address comments

* fix(test): assert equality

(cherry picked from commit 03088f1d9f)

* chore(sprites): compile

(cherry picked from commit 831b122ce2)

* chore(i18n): update locales

(cherry picked from commit be1754ab07)

* chore(public-docs): Community Guidelines update
Also Bailey announcement

(cherry picked from commit 565d50dd99)

* chore(i18n): update locales

(cherry picked from commit d4198f8913)

* 3.83.0

(cherry picked from commit ea18489991)

* chore(news): Bailey

* rebuild shrinkwrap

(cherry picked from commit 96ce948e1a)

* stripe webhook: fix handling of automatic requests

(cherry picked from commit c4463f991b)

* 3.83.1

* v3.83.2 - April Fools 2017 (#8632)

* feat(event): April Fools 2017

* feat(event): NPCs and Bailey

* fix(event): tweak NPCs, add challenge link

* chore(sprites): compile

* 3.83.2

* v3.83.3 Fooling Fix (#8633)

* fix(fooling): add logic to party and member modals

* 3.83.3

* v3.83.4 Export fooling (#8634)

* fix(fooling): allow export

* 3.83.4

* v3.83.5 End Fooling (#8638)

* chore(event): no more foolin

* chore(sprites): compile

* 3.83.5

* fix(merge-conflict): prefer develop

for config.json.example

* fix(merge-conflict): fewer istanbul deps

* Replace golden rock mount body sprite

* feat(content): BGs and Armoire 2017-04
Also fixes a positioning issue on Spring Healer headgear.

* chore(sprites): compile

* chore(i18n): update locales

* 3.84.0
2017-04-04 15:49:05 -05:00
Sabe Jones 8e6f4a15a7 v3.84.0 - Backgrounds and Armoire 2017/04 (#8651)
* Remove email addresses from translatable strings (#8448)

* Fix User > Profile showing {getProgressDisplay()}

* Remove bad nextRewardAt check

* 1st iteration of issue #8385 - more pending

* #8385 config and jade fixes, tests pending

* #8385 fixing lint errors

* Fix faqs string and test

* Fix faq.jade and add workaround for faq.js

* Fixing accidental checking for faq.js

* fix emails in faq.js

* fetch emails once in auth.js

* Fixing community manager email in auth.js

(cherry picked from commit 842fbe42a8)

* chore(i18n): update locales

(cherry picked from commit b2225f05e5)

* Merge branch 'stripe-webhook' into develop

(cherry picked from commit 30f514e46f)

* add recent Grand Gala seasonal special equipment names (#8606)

This is to help translators add good glossary entries now for keeping the current wiki pages consistent with future additions to the website's seasonal shop.
(cherry picked from commit 4846bc5769)

* stripe webhook for unpaid subs: add 3 days of remaining time

(cherry picked from commit 1d7b733759)

* New default background (#8597)

* feat(bgs): new default background

* feat(bgs): backfill migration

* fix(migration): extraneous imports, bad paths

* fix(bgs): address comments

* fix(test): assert equality

(cherry picked from commit 03088f1d9f)

* chore(sprites): compile

(cherry picked from commit 831b122ce2)

* chore(i18n): update locales

(cherry picked from commit be1754ab07)

* chore(public-docs): Community Guidelines update
Also Bailey announcement

(cherry picked from commit 565d50dd99)

* chore(i18n): update locales

(cherry picked from commit d4198f8913)

* 3.83.0

(cherry picked from commit ea18489991)

* chore(news): Bailey

* rebuild shrinkwrap

(cherry picked from commit 96ce948e1a)

* stripe webhook: fix handling of automatic requests

(cherry picked from commit c4463f991b)

* 3.83.1

* v3.83.2 - April Fools 2017 (#8632)

* feat(event): April Fools 2017

* feat(event): NPCs and Bailey

* fix(event): tweak NPCs, add challenge link

* chore(sprites): compile

* 3.83.2

* v3.83.3 Fooling Fix (#8633)

* fix(fooling): add logic to party and member modals

* 3.83.3

* v3.83.4 Export fooling (#8634)

* fix(fooling): allow export

* 3.83.4

* v3.83.5 End Fooling (#8638)

* chore(event): no more foolin

* chore(sprites): compile

* 3.83.5

* fix(merge-conflict): prefer develop

for config.json.example

* fix(merge-conflict): fewer istanbul deps

* Replace golden rock mount body sprite

* feat(content): BGs and Armoire 2017-04
Also fixes a positioning issue on Spring Healer headgear.

* chore(sprites): compile

* chore(i18n): update locales

* 3.84.0
2017-04-04 15:48:41 -05:00
Sabe Jones 7c516b7cbb Release Mergeback (#8644)
* Remove email addresses from translatable strings (#8448)

* Fix User > Profile showing {getProgressDisplay()}

* Remove bad nextRewardAt check

* 1st iteration of issue #8385 - more pending

* #8385 config and jade fixes, tests pending

* #8385 fixing lint errors

* Fix faqs string and test

* Fix faq.jade and add workaround for faq.js

* Fixing accidental checking for faq.js

* fix emails in faq.js

* fetch emails once in auth.js

* Fixing community manager email in auth.js

(cherry picked from commit 842fbe42a8)

* chore(i18n): update locales

(cherry picked from commit b2225f05e5)

* Merge branch 'stripe-webhook' into develop

(cherry picked from commit 30f514e46f)

* add recent Grand Gala seasonal special equipment names (#8606)

This is to help translators add good glossary entries now for keeping the current wiki pages consistent with future additions to the website's seasonal shop.
(cherry picked from commit 4846bc5769)

* stripe webhook for unpaid subs: add 3 days of remaining time

(cherry picked from commit 1d7b733759)

* New default background (#8597)

* feat(bgs): new default background

* feat(bgs): backfill migration

* fix(migration): extraneous imports, bad paths

* fix(bgs): address comments

* fix(test): assert equality

(cherry picked from commit 03088f1d9f)

* chore(sprites): compile

(cherry picked from commit 831b122ce2)

* chore(i18n): update locales

(cherry picked from commit be1754ab07)

* chore(public-docs): Community Guidelines update
Also Bailey announcement

(cherry picked from commit 565d50dd99)

* chore(i18n): update locales

(cherry picked from commit d4198f8913)

* 3.83.0

(cherry picked from commit ea18489991)

* chore(news): Bailey

* rebuild shrinkwrap

(cherry picked from commit 96ce948e1a)

* stripe webhook: fix handling of automatic requests

(cherry picked from commit c4463f991b)

* 3.83.1

* v3.83.2 - April Fools 2017 (#8632)

* feat(event): April Fools 2017

* feat(event): NPCs and Bailey

* fix(event): tweak NPCs, add challenge link

* chore(sprites): compile

* 3.83.2

* v3.83.3 Fooling Fix (#8633)

* fix(fooling): add logic to party and member modals

* 3.83.3

* v3.83.4 Export fooling (#8634)

* fix(fooling): allow export

* 3.83.4

* v3.83.5 End Fooling (#8638)

* chore(event): no more foolin

* chore(sprites): compile

* 3.83.5

* fix(merge-conflict): prefer develop

for config.json.example

* fix(merge-conflict): fewer istanbul deps
2017-04-03 16:34:37 -05:00
Sabe Jones 46d96b444b fix(merge-conflict): fewer istanbul deps 2017-04-03 12:51:07 -05:00
Sabe Jones 60c9434b14 fix(merge-conflict): prefer develop
for config.json.example
2017-04-03 12:49:37 -05:00
Sabe Jones c3901e8615 Merge branch 'develop' into release 2017-04-03 12:20:25 -05:00
Sabe Jones d166de8ad0 v3.83.5 End Fooling (#8638)
* chore(event): no more foolin

* chore(sprites): compile

* 3.83.5
2017-04-02 09:02:15 -05:00
Sabe Jones 82e9afe9ce v3.83.4 Export fooling (#8634)
* fix(fooling): allow export

* 3.83.4
2017-03-31 22:23:02 -05:00
Sabe Jones 999202a8a5 v3.83.3 Fooling Fix (#8633)
* fix(fooling): add logic to party and member modals

* 3.83.3
2017-03-31 20:26:40 -05:00
Sabe Jones 8b53adfcb1 v3.83.2 - April Fools 2017 (#8632)
* feat(event): April Fools 2017

* feat(event): NPCs and Bailey

* fix(event): tweak NPCs, add challenge link

* chore(sprites): compile

* 3.83.2
2017-03-31 19:52:32 -05:00
SabreCat c23062f87e chore(npm): update shrinkwrap 2017-03-31 16:44:10 +00:00
Alys ec98541df6 fix text problems in Community Guidelines and broken email links; also add press kit link (#8623) 2017-03-31 18:07:18 +02:00
Alys 4fed13afdd add note to encourage reporting of begging for gems (#8605)
Also changes the email addresses in config.json.example to the real addresses so that we can use local install screenshots to tell if the correct address variable has been used.
2017-03-31 18:06:22 +02:00
SabreCat 866b28ec15 chore(news): Bailey
(cherry picked from commit ea7c07e21d)
2017-03-30 19:04:41 +00:00
SabreCat bdf4a69eaf 3.83.1 2017-03-30 19:04:07 +00:00
Matteo Pagliazzi 4c121fba19 stripe webhook: fix handling of automatic requests
(cherry picked from commit c4463f991b)
2017-03-30 19:03:58 +00:00
Matteo Pagliazzi 0ecb95a294 rebuild shrinkwrap
(cherry picked from commit 96ce948e1a)
2017-03-30 19:03:26 +00:00
SabreCat ea7c07e21d chore(news): Bailey 2017-03-30 19:01:31 +00:00
Matteo Pagliazzi c4463f991b stripe webhook: fix handling of automatic requests 2017-03-30 00:14:51 +02:00
Matteo Pagliazzi 96ce948e1a rebuild shrinkwrap 2017-03-29 16:10:32 +02:00
Sabe Jones 6d06685dfa 3.83.0
(cherry picked from commit ea18489991)
2017-03-29 03:58:14 +00:00
Sabe Jones 32b6566e37 chore(i18n): update locales
(cherry picked from commit d4198f8913)
2017-03-29 03:57:44 +00:00
SabreCat ca3b4cd8ae chore(public-docs): Community Guidelines update
Also Bailey announcement

(cherry picked from commit 565d50dd99)
2017-03-29 03:57:27 +00:00
Sabe Jones c90b4b488e chore(i18n): update locales
(cherry picked from commit be1754ab07)
2017-03-29 03:57:15 +00:00
SabreCat 8c203637d7 chore(sprites): compile
(cherry picked from commit 831b122ce2)
2017-03-29 03:57:06 +00:00
Sabe Jones 58f72b7eaa New default background (#8597)
* feat(bgs): new default background

* feat(bgs): backfill migration

* fix(migration): extraneous imports, bad paths

* fix(bgs): address comments

* fix(test): assert equality

(cherry picked from commit 03088f1d9f)
2017-03-29 03:56:55 +00:00
Matteo Pagliazzi f0bbe84bd1 stripe webhook for unpaid subs: add 3 days of remaining time
(cherry picked from commit 1d7b733759)
2017-03-29 03:56:45 +00:00
Alys 038e3f3235 add recent Grand Gala seasonal special equipment names (#8606)
This is to help translators add good glossary entries now for keeping the current wiki pages consistent with future additions to the website's seasonal shop.
(cherry picked from commit 4846bc5769)
2017-03-29 03:56:26 +00:00
Sabe Jones 1a7c8c1f87 Merge branch 'stripe-webhook' into develop
(cherry picked from commit 30f514e46f)
2017-03-29 03:55:47 +00:00
Matteo Pagliazzi c73d6154a8 chore(i18n): update locales
(cherry picked from commit b2225f05e5)
2017-03-29 03:55:28 +00:00
Gerardo Saca e8b77ad2b2 Remove email addresses from translatable strings (#8448)
* Fix User > Profile showing {getProgressDisplay()}

* Remove bad nextRewardAt check

* 1st iteration of issue #8385 - more pending

* #8385 config and jade fixes, tests pending

* #8385 fixing lint errors

* Fix faqs string and test

* Fix faq.jade and add workaround for faq.js

* Fixing accidental checking for faq.js

* fix emails in faq.js

* fetch emails once in auth.js

* Fixing community manager email in auth.js

(cherry picked from commit 842fbe42a8)
2017-03-29 03:55:11 +00:00
Sabe Jones ea18489991 3.83.0 2017-03-29 03:33:13 +00:00
Sabe Jones 5eb1b6684e 3.82.0 2017-03-29 03:33:09 +00:00
Sabe Jones a2f77eeba2 3.81.0 2017-03-29 03:32:29 +00:00
Sabe Jones d4198f8913 chore(i18n): update locales 2017-03-29 03:28:07 +00:00
SabreCat 565d50dd99 chore(public-docs): Community Guidelines update
Also Bailey announcement
2017-03-29 03:21:44 +00:00
Sabe Jones be1754ab07 chore(i18n): update locales 2017-03-28 22:05:20 +00:00
SabreCat 831b122ce2 chore(sprites): compile 2017-03-28 21:53:12 +00:00
Sabe Jones 03088f1d9f New default background (#8597)
* feat(bgs): new default background

* feat(bgs): backfill migration

* fix(migration): extraneous imports, bad paths

* fix(bgs): address comments

* fix(test): assert equality
2017-03-28 16:49:24 -05:00
Matteo Pagliazzi 1d7b733759 stripe webhook for unpaid subs: add 3 days of remaining time 2017-03-28 21:51:55 +02:00
Keith Holliday d170f0b1bd Fixed injection minification issue (#8608) 2017-03-28 13:12:51 -06:00
Alys 4846bc5769 add recent Grand Gala seasonal special equipment names (#8606)
This is to help translators add good glossary entries now for keeping the current wiki pages consistent with future additions to the website's seasonal shop.
2017-03-28 11:32:42 -05:00
Sabe Jones 30f514e46f Merge branch 'stripe-webhook' into develop 2017-03-28 16:11:13 +00:00
Matteo Pagliazzi 6aad018eb2 Mocha 3 and Coverage (#8601)
* upgrade mocha to v3

* shrinkwrap

* import changes from PR #8487

* fix bin

* upgrade istanbul

* use correct mocha bin
2017-03-28 13:50:34 +02:00
Mateus Etto f2d9bdf7ae Updated comment 2017-03-28 06:57:20 -03:00
Matteo Pagliazzi daf9421f4f old client: guilds: show invite button and completely hide box for party info 2017-03-28 11:24:01 +02:00
Mateus Etto 2d7e280598 Special message when complete all check-in prizes 2017-03-27 22:24:47 -03:00
Mateus Etto d1de41290d Remove Beat Master unlock for Rebirth 2017-03-27 19:59:24 -03:00
Matteo Pagliazzi b2225f05e5 chore(i18n): update locales 2017-03-27 18:19:45 +02:00
Gerardo Saca 842fbe42a8 Remove email addresses from translatable strings (#8448)
* Fix User > Profile showing {getProgressDisplay()}

* Remove bad nextRewardAt check

* 1st iteration of issue #8385 - more pending

* #8385 config and jade fixes, tests pending

* #8385 fixing lint errors

* Fix faqs string and test

* Fix faq.jade and add workaround for faq.js

* Fixing accidental checking for faq.js

* fix emails in faq.js

* fetch emails once in auth.js

* Fixing community manager email in auth.js
2017-03-27 18:03:31 +02:00
Mateus Etto 5eadf9e486 Fixed subscription popup too narrow in Japanese (#8581) (#8583) 2017-03-27 09:57:49 -06:00
Keith Holliday 68ad3e2d4a Task notes modals (#8521)
* Merged in develop

* Show task notes modal on click

* Began adding tests

* Removed extra characters

* Fixed lingering popup

* Added markdown

* Fixed line endings
2017-03-27 09:10:21 -06:00
madpink de947f8069 Updating User API Doc (part 1) (#8476)
* Update API Doc #8087

Includes: GET /api/v3/user – POST /api/v3/user/buy/:key

* User API Doc update 1

Changed "GET user" description to a URL to the user model

* Update API DOC User 1

Cleaned up stray spaces

* Updated API Doc for User (part 1)

for GET user:
restored apiDescription from first PR
put link to model into "apiSuccessExample"

* Remove notifications from example responses

* Fixed trailing spaces
2017-03-26 21:42:21 +02:00
Mateus Etto d541e3aa31 Fixed collection quest progress not being updated (#8584) 2017-03-26 21:27:44 +02:00
Mateus Etto b0eda344f1 Limit party size to 30 members (#8589)
* Added a field in Party page with members count and maximum members in party

* Added information of invitations counter

* Limited party to 2 members on server (API)

* Fixed english text

* Consider current number of invitations in the party

* Moved PARTY_LIMIT_MEMBERS to common folder

* Access the PARTY_LIMIT_MEMBERS through groupsCtrl

* Some corrections

* Hide invite button when invite limit is reached

* Added missing trailing comma

* Do not test 'returns only first 30 invites' in a party anymore, but in a guild: party is limited to 30 members, so it would always fail

* Test: allow 30 members in a party

* Test: do not allow 30+ members in a party

* Improved 'allow 30 members in a party' test

* Test: 'allow 30+ members in a guild'

* Added missing trailing comma

* Code style corrections

* Fixed new line position

* Party limit check done inside Group.validateInvitations function

* Improved members count query

* Fixed tests

* Rewrite tests

* Removed import of BadRequest: value became unused

* Added 'await' to remaining 'Group.validateInvitations' functions

* Fixed tests that would always success
2017-03-26 21:23:19 +02:00
Alys 02708a7b10 remove "Habitica - Gamify Your Life" video from Press Kit (#8595)
- removes embedded youtube video from press kit page
- removes "Habitica - Gamify Your Life.mp4" from presskit.zip
2017-03-26 21:12:09 +02:00
Matteo Pagliazzi fd9f3a32c4 fix linting 2017-03-25 17:48:51 +01:00
Matteo Pagliazzi 6e0341a4ff add more logging 2017-03-25 17:33:35 +01:00
Matteo Pagliazzi 625077fc1a add tests and fix bugs 2017-03-25 17:22:28 +01:00
Matteo Pagliazzi 9d456e934c remove un-necessary JSON.parse 2017-03-25 11:46:40 +01:00
Matteo Pagliazzi 771d8f492a update stripe webhooks url 2017-03-25 11:46:40 +01:00
Matteo Pagliazzi f3fab88f0b add console.log statements for debugging 2017-03-25 11:46:40 +01:00
Matteo Pagliazzi 207e3476e6 add stripe webhook to handle cancelled subscriptions 2017-03-25 11:46:40 +01:00
Clint Ryan 6956f5345e Removes test focus 2017-03-24 23:45:56 +11:00
Sabe Jones 0ec293bd15 chore(news): Bailey 2017-03-23 21:00:48 +00:00
Sabe Jones cb00ecc0be chore(i18n): update locales 2017-03-23 19:53:14 +00:00
SabreCat 94ef4f80cc chore(sprites): compile 2017-03-23 19:45:05 +00:00
Sabe Jones 814b163e1a March 2017 Content (#8594)
* feat(content): Mystery 032017, Shimmer Potions

* fix(date): end 4/19 not 30

* fix(dates): Floral after Shimmer
2017-03-23 14:41:41 -05:00
Sabe Jones 421bdce38b fix(sprites): don't get blocked (#8576) 2017-03-23 12:23:29 -05:00
Keith Holliday 624566ecec Added end date option to group plan migration (#8588) 2017-03-23 10:40:15 -06:00
Sabe Jones 77ff91868e Redirection fixes (#8592)
* fix(redirects): logic update

* test(middlewares): redirects tests

* fix(nconf): IS_PROD is boolean

* fix(test): treat IS_PROD as Boolean here too

* fix(test): apiUrl test copypasta
2017-03-23 10:25:58 -05:00
Clint Ryan cc68abb67e Mock ENV function 2017-03-24 00:36:05 +11:00
Clint Ryan b48be4e619 Remoed logging 2017-03-23 00:43:59 +11:00
Clint Ryan 7a543d07a4 Fixes tests 2017-03-23 00:41:28 +11:00
SabreCat 59f490d178 fix(sprites): accidental project_file deletion 2017-03-21 19:42:02 +00:00
SabreCat ae64ef94ae fix(event): update Justin sprite 2017-03-21 19:06:43 +00:00
Sabe Jones 2335e22a0c chore(i18n): update locales 2017-03-21 18:58:46 +00:00
SabreCat 910154b3ed chore(news): Bailey 2017-03-21 18:46:17 +00:00
SabreCat 31e36339c4 fix(event): update Daniel sprite 2017-03-20 19:12:43 +00:00
SabreCat 07fe1df024 chore(sprites): compile 2017-03-20 18:36:37 +00:00
Matteo Pagliazzi 258742f6b7 Optional HTTP Basic Auth (#8586)
* add ability to add http basic auth to the website

* debug

* remove console.log
2017-03-20 15:02:48 +01:00
Clint Ryan 9115be68b2 Fixes achievements injection. Now trying with Guide 2017-03-19 10:11:11 +11:00
Matteo Pagliazzi d9d7c69432 Client: async resources, make store reusable, move plugins and add getTaskFor getter (#8575)
Add library to manage async resource
Make Store reusable for easier testing
Move plugin to libs
Add getTaskFor getter with tests
2017-03-18 18:33:08 +01:00
Sabe Jones 03d6c459bf chore(i18n): update locales 2017-03-18 17:09:34 +00:00
Sabe Jones 12cefe4e9f Spring Fling 2017 (#8579)
* feat(event): Spring Fling 2017

* fix(sprites): adjustments
Also enables pastel hair/skin purchases
2017-03-18 12:01:41 -05:00
Keith Holliday 21ad808cc1 Aded challenge migration sync (#8492)
* Aded challenge migration sync

* Fixed async grouping

* Mapped promises and added error catching

* Added placholders for syncing specific challenges

* Prvented overriding of attribute and createdAt
2017-03-17 16:58:55 -05:00
SabreCat db9befde17 Merge branch 'release' into develop 2017-03-16 19:45:11 +00:00
Lulock cc9bca5f63 invalid class change throws error (#8496) (#8531)
* invalid class-change throws error

minor fixes

indentation fixes

indentation fixes

indentation fixes

* minor fixes
2017-03-16 13:38:24 -05:00
Matteo Pagliazzi 05d75a4d5c chore(i18n): update locales 2017-03-16 15:56:01 +01:00
Blade Barringer 164177f010 Enable shouldDo tests 2017-03-14 20:59:27 -05:00
Clint Ryan 83bd4dcf60 Add failing test, seeking help/advice 2017-03-14 12:35:48 +11:00
Clint Ryan 92b02295b5 Issue 8432: Pre-test for initial fix 2017-03-11 23:29:51 +11:00
1049 changed files with 68889 additions and 56189 deletions
+5 -7
View File
@@ -1,21 +1,19 @@
[//]: # (Before logging this issue, look through common problems at https://github.com/HabitRPG/habitrpg/issues If you find your issue there, read at least the first post to see if there is a workaround for you)
[//]: # (Before logging this issue, please post to the Report a Bug guild from the Habitica website's Help menu. Most bugs can be handled quickly there. If a GitHub issue is needed, you will be advised of that by a moderator or staff member -- a player with a dark blue or purple name. It is recommended that you don't create a new issue unless advised to.)
[//]: # (Github is primarily used for reporting bugs. If you have a feature request, use "Help > Request a Feature" so that the feature request can be vetted by the larger Habitica community)
[//]: # (Bugs in the mobile apps can also be reported there.)
[//]: # (To report a bug in one of the mobile apps, please report it in the correct repository. Android: https://github.com/HabitRPG/habitrpg-android, iOS: https://github.com/HabitRPG/habitrpg-ios)
[//]: # (If you have a feature request, use "Help > Request a Feature", not GitHub or the Report a Bug guild.)
[//]: # (For more guidelines see https://github.com/HabitRPG/habitrpg/issues/2760)
[//]: # (Fill out relevant information - UUID is found in Settings -> API)
General Info
### General Info
* UUID:
* Browser:
* OS:
### Description
[//]: # (Describe bug in detail here. Include pictures if helpful.)
[//]: # (Describe bug in detail here. Include screenshots if helpful.)
#### Console Errors
[//]: # (Include any JavaScript console errors here.)
+8 -8
View File
@@ -16,18 +16,18 @@ before_script:
- npm run test:build
- cp config.json.example config.json
- 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
script: npm run $TEST
script:
- npm run $TEST
- if [ $COVERAGE ]; then ./node_modules/.bin/lcov-result-merger 'coverage/**/*.info' | ./node_modules/coveralls/bin/coveralls.js; fi
env:
global:
- CXX=g++-4.8
- DISABLE_REQUEST_LOGGING=true
matrix:
- TEST="lint"
- TEST="test:api-v3" REQUIRES_SERVER=true
- TEST="test:api-v3" REQUIRES_SERVER=true COVERAGE=true
- TEST="test:sanity"
- TEST="test:content"
- TEST="test:common"
- TEST="test:karma"
- TEST="client:unit"
- TEST="test:content" COVERAGE=true
- TEST="test:common" COVERAGE=true
- TEST="test:karma" COVERAGE=true
- TEST="client:unit" COVERAGE=true
+15 -1
View File
@@ -76,6 +76,11 @@
"APN_ENABLED": "false",
"FCM_SERVER_API_KEY": ""
},
"SITE_HTTP_AUTH": {
"ENABLED": "false",
"USERNAME": "admin",
"PASSWORD": "password"
},
"PUSHER": {
"ENABLED": "false",
"APP_ID": "appId",
@@ -87,5 +92,14 @@
"FLAGGING_FOOTER_LINK": "https://habitrpg.github.io/flag-o-rama/",
"SUBSCRIPTIONS_URL": "https://hooks.slack.com/services/id/id/id"
},
"ITUNES_SHARED_SECRET": "aaaabbbbccccddddeeeeffff00001111"
"ITUNES_SHARED_SECRET": "aaaabbbbccccddddeeeeffff00001111",
"EMAILS" : {
"COMMUNITY_MANAGER_EMAIL" : "leslie@habitica.com",
"TECH_ASSISTANCE_EMAIL" : "admin@habitica.com",
"PRESS_ENQUIRY_EMAIL" : "leslie@habitica.com"
},
"LOGGLY" : {
"TOKEN" : "example-token",
"SUBDOMAIN" : "exmaple-subdomain"
}
}
+2 -2
View File
@@ -280,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 --require ./test/helpers/start-server'),
testBin('node_modules/.bin/istanbul cover --dir coverage/api-v3-unit --report lcovonly node_modules/mocha/bin/_mocha -- test/api/v3/unit --recursive --require ./test/helpers/start-server'),
(err, stdout, stderr) => {
if (err) {
process.exit(1);
@@ -298,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 --require ./test/helpers/start-server'),
testBin('node_modules/.bin/istanbul cover --dir coverage/api-v3-integration --report lcovonly node_modules/mocha/bin/_mocha -- test/api/v3/integration --recursive --require ./test/helpers/start-server'),
{maxBuffer: 500 * 1024},
(err, stdout, stderr) => {
if (err) {
@@ -0,0 +1,88 @@
var migrationName = '20170418_subscriber_jackalopes.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 Royal Purple Jackalope pet to all current subscribers
*/
var monk = require('monk');
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
var dbUsers = monk(connectionString).get('users', { castIds: false });
var now = new Date();
function processUsers(lastId) {
// specify a query to limit the affected users (empty for all users):
var query = {
'purchased.plan.customerId': {$type: 2},
$or: [
{'purchased.plan.dateTerminated': null},
{'purchased.plan.dateTerminated': {$exists: false}},
{'purchased.plan.dateTerminated': {$gt: now}},
]
};
if (lastId) {
query._id = {
$gt: lastId
}
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 250,
fields: [] // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
})
.then(updateUsers)
.catch(function (err) {
console.log(err);
return exiting(1, 'ERROR! ' + err);
});
}
var progressCount = 1000;
var count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
displayData();
return;
}
var userPromises = users.map(updateUser);
var lastUser = users[users.length - 1];
return Promise.all(userPromises)
.then(function () {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
var set = {'items.pets.Jackalope-RoyalPurple': 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);
}
module.exports = processUsers;
+207
View File
@@ -0,0 +1,207 @@
var migrationName = '20170425_missing_incentives';
var authorName = 'Sabe'; // in case script author needs to know when their ...
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
/*
* Award missing Royal Purple Hatching Potion to users with 55+ check-ins
* Reduce users with impossible check-in counts to a reasonable number
*/
import monk from 'monk';
import common from '../website/common';
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
var dbUsers = monk(connectionString).get('users', { castIds: false });
function processUsers(lastId) {
// specify a query to limit the affected users (empty for all users):
var query = {
'loginIncentives': {$gt:99},
'migration': {$ne: migrationName},
};
if (lastId) {
query._id = {
$gt: lastId
}
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 250,
fields: [] // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
})
.then(updateUsers)
.catch(function (err) {
console.log(err);
return exiting(1, 'ERROR! ' + err);
});
}
var progressCount = 1000;
var count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
displayData();
return;
}
var userPromises = users.map(updateUser);
var lastUser = users[users.length - 1];
return Promise.all(userPromises)
.then(function () {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
var language = user.preferences.language || 'en';
var set = {'migration': migrationName};
var inc = {
'items.eggs.BearCub': 0,
'items.eggs.Cactus': 0,
'items.eggs.Dragon': 0,
'items.eggs.FlyingPig': 0,
'items.eggs.Fox': 0,
'items.eggs.LionCub': 0,
'items.eggs.PandaCub': 0,
'items.eggs.TigerCub': 0,
'items.eggs.Wolf': 0,
'items.food.Chocolate': 0,
'items.food.CottonCandyBlue': 0,
'items.food.CottonCandyPink': 0,
'items.food.Fish': 0,
'items.food.Honey': 0,
'items.food.Meat': 0,
'items.food.Milk': 0,
'items.food.Potatoe': 0,
'items.food.RottenMeat': 0,
'items.food.Strawberry': 0,
'items.hatchingPotions.Base': 0,
'items.hatchingPotions.CottonCandyBlue': 0,
'items.hatchingPotions.CottonCandyPink': 0,
'items.hatchingPotions.Desert': 0,
'items.hatchingPotions.Golden': 0,
'items.hatchingPotions.Red': 0,
'items.hatchingPotions.RoyalPurple': 0,
'items.hatchingPotions.Shade': 0,
'items.hatchingPotions.Skeleton': 0,
'items.hatchingPotions.White': 0,
'items.hatchingPotions.Zombie': 0,
};
var nextReward;
if (user.loginIncentives >= 105) {
inc['items.hatchingPotions.RoyalPurple'] += 1;
nextReward = 110;
}
if (user.loginIncentives >= 110) {
inc['items.eggs.BearCub'] += 1;
inc['items.eggs.Cactus'] += 1;
inc['items.eggs.Dragon'] += 1;
inc['items.eggs.FlyingPig'] += 1;
inc['items.eggs.Fox'] += 1;
inc['items.eggs.LionCub'] += 1;
inc['items.eggs.PandaCub'] += 1;
inc['items.eggs.TigerCub'] += 1;
inc['items.eggs.Wolf'] += 1;
nextReward = 115;
}
if (user.loginIncentives >= 115) {
inc['items.hatchingPotions.RoyalPurple'] += 1;
nextReward = 120;
}
if (user.loginIncentives >= 120) {
inc['items.hatchingPotions.Base'] += 1;
inc['items.hatchingPotions.CottonCandyBlue'] += 1;
inc['items.hatchingPotions.CottonCandyPink'] += 1;
inc['items.hatchingPotions.Desert'] += 1;
inc['items.hatchingPotions.Golden'] += 1;
inc['items.hatchingPotions.Red'] += 1;
inc['items.hatchingPotions.Shade'] += 1;
inc['items.hatchingPotions.Skeleton'] += 1;
inc['items.hatchingPotions.White'] += 1;
inc['items.hatchingPotions.Zombie'] += 1;
nextReward = 125;
}
if (user.loginIncentives >= 125) {
inc['items.hatchingPotions.RoyalPurple'] += 1;
nextReward = 130;
}
if (user.loginIncentives >= 130) {
inc['items.food.Chocolate'] += 3;
inc['items.food.CottonCandyBlue'] += 3;
inc['items.food.CottonCandyPink'] += 3;
inc['items.food.Fish'] += 3;
inc['items.food.Honey'] += 3;
inc['items.food.Meat'] += 3;
inc['items.food.Milk'] += 3;
inc['items.food.Potatoe'] += 3;
inc['items.food.RottenMeat'] += 3;
inc['items.food.Strawberry'] += 3;
}
if (user.loginIncentives >= 135) {
inc['items.hatchingPotions.RoyalPurple'] += 1;
nextReward = 140;
}
if (user.loginIncentives >= 140) {
set['items.gear.owned.weapon_special_skeletonKey'] = true;
set['items.gear.owned.shield_special_lootBag'] = true;
nextReward = 145;
}
if (user.loginIncentives >= 145) {
inc['items.hatchingPotions.RoyalPurple'] += 1;
nextReward = 150;
}
if (user.loginIncentives >= 150) {
set['items.gear.owned.head_special_clandestineCowl'] = true;
set['items.gear.owned.armor_special_sneakthiefRobes'] = true;
nextReward = 155;
}
if (user.loginIncentives > 155) {
set.loginIncentives = 155;
nextReward = 160;
}
var push = {
'notifications': {
'type': 'LOGIN_INCENTIVE',
'data': {
'nextRewardAt': nextReward,
'rewardKey': [
'shop_armoire',
],
'rewardText': common.i18n.t('checkInRewards', language),
'reward': [],
'message': common.i18n.t('backloggedCheckInRewards', language),
},
'id': common.uuid(),
}
};
dbUsers.update({_id: user._id}, {$set:set, $push:push, $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);
}
module.exports = processUsers;
@@ -0,0 +1,47 @@
import Bluebird from 'Bluebird';
import { model as Challenges } from '../../website/server/models/challenge';
import { model as User } from '../../website/server/models/user';
async function syncChallengeToMembers (challenges) {
let challengSyncPromises = challenges.map(async function (challenge) {
let users = await User.find({
// _id: '',
challenges: challenge._id,
}).exec();
let promises = [];
users.forEach(function (user) {
promises.push(challenge.syncToUser(user));
promises.push(challenge.save());
promises.push(user.save());
});
return Bluebird.all(promises);
});
return await Bluebird.all(challengSyncPromises);
}
async function syncChallenges (lastChallengeDate) {
let query = {
// _id: '',
};
if (lastChallengeDate) {
query.createdOn = { $lte: lastChallengeDate };
}
let challengesFound = await Challenges.find(query)
.limit(10)
.sort('-createdAt')
.exec();
let syncedChallenges = await syncChallengeToMembers(challengesFound)
.catch(reason => console.error(reason));
let lastChallenge = challengesFound[challengesFound.length - 1];
if (lastChallenge) syncChallenges(lastChallenge.createdAt);
return syncedChallenges;
};
module.exports = syncChallenges;
@@ -5,11 +5,12 @@ var authorUuid = ''; //... own data is done
/*
* This migrations will add a free subscription to a specified group
*/
import moment from 'moment';
import { model as Group } from '../../website/server/models/group';
// @TODO: this should probably be a GroupManager library method
async function addUnlimitedSubscription (groupId) {
async function addUnlimitedSubscription (groupId, dateTerminated) {
let group = await Group.findById(groupId);
group.purchased.plan.customerId = "group-unlimited";
@@ -18,6 +19,10 @@ async function addUnlimitedSubscription (groupId) {
group.purchased.plan.paymentMethod = "Group Unlimited";
group.purchased.plan.planId = "group_monthly";
group.purchased.plan.dateTerminated = null;
if (dateTerminated) {
let dateToEnd = moment(dateTerminated).toDate();
group.purchased.plan.dateTerminated = dateToEnd;
}
// group.purchased.plan.owner = ObjectId();
group.purchased.plan.subscriptionId = "";
@@ -29,5 +34,7 @@ module.exports = async function addUnlimitedSubscriptionCreator () {
if (!groupId) throw Error('Group ID is required');
let result = await addUnlimitedSubscription(groupId)
let dateTerminated = process.argv[3];
let result = await addUnlimitedSubscription(groupId, dateTerminated);
};
+1 -1
View File
@@ -21,4 +21,4 @@ var processUsers = require('./groups/update-groups-with-group-plans');
processUsers()
.catch(function (err) {
console.log(err)
})
})
+1 -1
View File
@@ -2,7 +2,7 @@ var _id = '';
var update = {
$addToSet: {
'purchased.plan.mysteryItems':{
$each:['head_mystery_201702','back_mystery_201702']
$each:['back_mystery_201704','armor_mystery_201704']
}
}
};
+2 -2
View File
@@ -1,4 +1,4 @@
var migrationName = '20170201_takeThis.js'; // Update per month
var migrationName = '20170502_takeThis.js'; // Update per month
var authorName = 'Sabe'; // in case script author needs to know when their ...
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
@@ -14,7 +14,7 @@ function processUsers(lastId) {
// specify a query to limit the affected users (empty for all users):
var query = {
'migration':{$ne:migrationName},
'challenges':{$in:['b1d436b5-c784-42e3-9b07-7072479a6f8e']} // Update per month
'challenges':{$in:['69999331-d4ea-45a0-8c3f-f725d22b56c8']} // Update per month
};
if (lastId) {
+838 -1395
View File
File diff suppressed because it is too large Load Diff
+9 -7
View File
@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "3.81.0",
"version": "3.90.1",
"main": "./website/server/index.js",
"dependencies": {
"@slack/client": "^3.8.1",
@@ -41,6 +41,7 @@
"domain-middleware": "~0.1.0",
"estraverse": "^4.1.1",
"express": "~4.14.0",
"express-basic-auth": "^1.0.1",
"express-csv": "~0.6.0",
"express-validator": "^2.18.0",
"extract-text-webpack-plugin": "^2.0.0-rc.3",
@@ -55,7 +56,7 @@
"grunt-contrib-stylus": "~0.20.0",
"grunt-contrib-uglify": "~0.6.0",
"grunt-contrib-watch": "~0.6.1",
"grunt-hashres": "~0.4.1",
"grunt-hashres": "habitrpg/grunt-hashres#v0.4.2",
"gulp": "^3.9.0",
"gulp-babel": "^6.1.2",
"gulp-grunt": "^0.5.2",
@@ -123,6 +124,7 @@
"webpack": "^2.2.1",
"webpack-merge": "^2.6.1",
"winston": "^2.1.0",
"winston-loggly-bulk": "^1.4.2",
"xml2js": "^0.4.4"
},
"private": true,
@@ -138,9 +140,9 @@
"test:api-v3:unit": "gulp test:api-v3:unit",
"test:api-v3:integration": "gulp test:api-v3:integration",
"test:api-v3:integration:separate-server": "NODE_ENV=test gulp test:api-v3:integration:separate-server",
"test:sanity": "mocha test/sanity --recursive",
"test:common": "mocha test/common --recursive",
"test:content": "mocha test/content --recursive",
"test:sanity": "istanbul cover --dir coverage/sanity --report lcovonly node_modules/mocha/bin/_mocha -- test/sanity --recursive",
"test:common": "istanbul cover --dir coverage/common --report lcovonly node_modules/mocha/bin/_mocha -- test/common --recursive",
"test:content": "istanbul cover --dir coverage/content --report lcovonly node_modules/mocha/bin/_mocha -- test/content --recursive",
"test:karma": "karma start test/client-old/spec/karma.conf.js --single-run",
"test:karma:watch": "karma start test/client-old/spec/karma.conf.js",
"test:prepare:webdriver": "webdriver-manager update",
@@ -182,7 +184,7 @@
"grunt-karma": "~0.12.1",
"http-proxy-middleware": "^0.17.0",
"inject-loader": "^3.0.0-beta4",
"istanbul": "^0.3.14",
"istanbul": "^1.1.0-alpha.1",
"karma": "^1.3.0",
"karma-babel-preprocessor": "^6.0.1",
"karma-chai-plugins": "~0.6.0",
@@ -197,7 +199,7 @@
"karma-webpack": "^2.0.2",
"lcov-result-merger": "^1.0.2",
"lolex": "^1.4.0",
"mocha": "^2.3.3",
"mocha": "^3.2.0",
"mongodb": "^2.0.46",
"mongoskin": "~2.1.0",
"monk": "^4.0.0",
+125 -2
View File
@@ -4,11 +4,17 @@ import {
sleep,
server,
} from '../../../../helpers/api-v3-integration.helper';
import {
SPAM_MESSAGE_LIMIT,
SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
TAVERN_ID,
} from '../../../../../website/server/models/group';
import { v4 as generateUUID } from 'uuid';
describe('POST /chat', () => {
let user, groupWithChat, userWithChatRevoked, member;
let user, groupWithChat, member, additionalMember;
let testMessage = 'Test Message';
let testBannedWordMessage = 'TEST_PLACEHOLDER_SWEAR_WORD_HERE';
before(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
@@ -22,8 +28,8 @@ describe('POST /chat', () => {
user = groupLeader;
groupWithChat = group;
userWithChatRevoked = await members[0].update({'flags.chatRevoked': true});
member = members[0];
additionalMember = members[1];
});
it('Returns an error when no message is provided', async () => {
@@ -62,6 +68,7 @@ describe('POST /chat', () => {
});
it('returns an error when chat privileges are revoked when sending a message to a public guild', async () => {
let userWithChatRevoked = await member.update({'flags.chatRevoked': true});
await expect(userWithChatRevoked.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage})).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
@@ -69,6 +76,96 @@ describe('POST /chat', () => {
});
});
context('banned word', () => {
it('returns an error when chat message contains a banned word in tavern', async () => {
await expect(user.post('/groups/habitrpg/chat', { message: testBannedWordMessage}))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedWordUsed'),
});
});
it('errors when word is part of a phrase', async () => {
let wordInPhrase = `phrase ${testBannedWordMessage} end`;
await expect(user.post('/groups/habitrpg/chat', { message: wordInPhrase}))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedWordUsed'),
});
});
it('errors when word is surrounded by non alphabet characters', async () => {
let wordInPhrase = `_!${testBannedWordMessage}@_`;
await expect(user.post('/groups/habitrpg/chat', { message: wordInPhrase}))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedWordUsed'),
});
});
it('does not error when bad word is suffix of a word', async () => {
let wordAsSuffix = `prefix${testBannedWordMessage}`;
let message = await user.post('/groups/habitrpg/chat', { message: wordAsSuffix});
expect(message.message.id).to.exist;
});
it('does not error when bad word is prefix of a word', async () => {
let wordAsPrefix = `${testBannedWordMessage}suffix`;
let message = await user.post('/groups/habitrpg/chat', { message: wordAsPrefix});
expect(message.message.id).to.exist;
});
it('does not error when sending a chat message containing a banned word to a party', async () => {
let { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'Party',
type: 'party',
privacy: 'private',
},
members: 1,
});
let message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage});
expect(message.message.id).to.exist;
});
it('does not error when sending a chat message containing a banned word to a public guild', async () => {
let { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'public guild',
type: 'guild',
privacy: 'public',
},
members: 1,
});
let message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage});
expect(message.message.id).to.exist;
});
it('does not error when sending a chat message containing a banned word to a private guild', async () => {
let { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'private guild',
type: 'guild',
privacy: 'private',
},
members: 1,
});
let message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage});
expect(message.message.id).to.exist;
});
});
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: {
@@ -173,4 +270,30 @@ describe('POST /chat', () => {
expect(message.message.id).to.exist;
expect(memberWithNotification.newMessages[`${group._id}`]).to.exist;
});
context('Spam prevention', () => {
it('Returns an error when the user has been posting too many messages', async () => {
// Post as many messages are needed to reach the spam limit
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
let result = await additionalMember.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage }); // eslint-disable-line no-await-in-loop
expect(result.message.id).to.exist;
}
await expect(additionalMember.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage })).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageGroupChatSpam'),
});
});
it('contributor should not receive spam alert', async () => {
let userSocialite = await member.update({'contributor.level': SPAM_MIN_EXEMPT_CONTRIB_LEVEL, 'flags.chatRevoked': false});
// Post 1 more message than the spam limit to ensure they do not reach the limit
for (let i = 0; i < SPAM_MESSAGE_LIMIT + 1; i++) {
let result = await userSocialite.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage }); // eslint-disable-line no-await-in-loop
expect(result.message.id).to.exist;
}
});
});
});
@@ -63,15 +63,17 @@ describe('GET /groups/:groupId/invites', () => {
});
it('returns only first 30 invites', async () => {
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
let leader = await generateUser({balance: 4});
let group = await generateGroup(leader, {type: 'guild', privacy: 'public', name: generateUUID()});
let invitesToGenerate = [];
for (let i = 0; i < 31; i++) {
invitesToGenerate.push(generateUser());
}
let generatedInvites = await Promise.all(invitesToGenerate);
await user.post(`/groups/${group._id}/invite`, {uuids: generatedInvites.map(invite => invite._id)});
await leader.post(`/groups/${group._id}/invite`, {uuids: generatedInvites.map(invite => invite._id)});
let res = await user.get('/groups/party/invites');
let res = await leader.get(`/groups/${group._id}/invites`);
expect(res.length).to.equal(30);
res.forEach(member => {
expect(member).to.have.all.keys(['_id', 'id', 'profile']);
@@ -0,0 +1,93 @@
import {
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
import { find } from 'lodash';
describe('POST /group/:groupId/remove-manager', () => {
let leader, nonLeader, groupToUpdate;
let groupName = 'Test Public Guild';
let groupType = 'guild';
let nonManager;
function findAssignedTask (memberTask) {
return memberTask.group.id === groupToUpdate._id;
}
beforeEach(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: groupName,
type: groupType,
privacy: 'public',
},
members: 1,
});
groupToUpdate = group;
leader = groupLeader;
nonLeader = members[0];
nonManager = members[0];
});
it('returns an error when a non group leader tries to add member', async () => {
await expect(nonLeader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
managerId: nonLeader._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageGroupOnlyLeaderCanUpdate'),
});
});
it('returns an error when manager does not exist', async () => {
await expect(leader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
managerId: nonManager._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('userIsNotManager'),
});
});
it('allows a leader to remove managers', async () => {
await leader.post(`/groups/${groupToUpdate._id}/add-manager`, {
managerId: nonLeader._id,
});
let updatedGroup = await leader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
managerId: nonLeader._id,
});
expect(updatedGroup.managers[nonLeader._id]).to.not.exist;
});
it('removes group approval notifications from a manager that is removed', async () => {
await leader.post(`/groups/${groupToUpdate._id}/add-manager`, {
managerId: nonLeader._id,
});
let task = await leader.post(`/tasks/group/${groupToUpdate._id}`, {
text: 'test todo',
type: 'todo',
requiresApproval: true,
});
await nonLeader.post(`/tasks/${task._id}/assign/${leader._id}`);
let memberTasks = await leader.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(leader.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
let updatedGroup = await leader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
managerId: nonLeader._id,
});
await nonLeader.sync();
expect(nonLeader.notifications.length).to.equal(0);
expect(updatedGroup.managers[nonLeader._id]).to.not.exist;
});
});
@@ -4,8 +4,11 @@ import {
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
import nconf from 'nconf';
const INVITES_LIMIT = 100;
const PARTY_LIMIT_MEMBERS = 30;
const MAX_EMAIL_INVITES_BY_USER = 200;
describe('Post /groups/:groupId/invite', () => {
let inviter;
@@ -204,13 +207,37 @@ describe('Post /groups/:groupId/invite', () => {
});
});
it('returns an error when a user has sent the max number of email invites', async () => {
let inviterWithMax = await generateUser({
invitesSent: MAX_EMAIL_INVITES_BY_USER,
balance: 4,
});
let tmpGroup = await inviterWithMax.post('/groups', {
name: groupName,
type: 'guild',
});
await expect(inviterWithMax.post(`/groups/${tmpGroup._id}/invite`, {
emails: [testInvite],
inviter: 'inviter name',
}))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('inviteLimitReached', {techAssistanceEmail: nconf.get('EMAILS:TECH_ASSISTANCE_EMAIL')}),
});
});
it('invites a user to a group by email', async () => {
let res = await inviter.post(`/groups/${group._id}/invite`, {
emails: [testInvite],
inviter: 'inviter name',
});
let updatedUser = await inviter.sync();
expect(res).to.exist;
expect(updatedUser.invitesSent).to.eql(1);
});
it('invites multiple users to a group by email', async () => {
@@ -218,7 +245,10 @@ describe('Post /groups/:groupId/invite', () => {
emails: [testInvite, {name: 'test2', email: 'test2@habitica.com'}],
});
let updatedUser = await inviter.sync();
expect(res).to.exist;
expect(updatedUser.invitesSent).to.eql(2);
});
});
@@ -321,6 +351,19 @@ describe('Post /groups/:groupId/invite', () => {
});
});
it('allows 30+ members in a guild', async () => {
let invitesToGenerate = [];
// Generate 30 users to invite (30 + leader = 31 members)
for (let i = 0; i < PARTY_LIMIT_MEMBERS; i++) {
invitesToGenerate.push(generateUser());
}
let generatedInvites = await Promise.all(invitesToGenerate);
// Invite users
expect(await inviter.post(`/groups/${group._id}/invite`, {
uuids: generatedInvites.map(invite => invite._id),
})).to.be.an('array');
});
// @TODO: Add this after we are able to mock the group plan route
xit('returns an error when a non-leader invites to a group plan', async () => {
let userToInvite = await generateUser();
@@ -410,5 +453,36 @@ describe('Post /groups/:groupId/invite', () => {
});
expect((await userToInvite.get('/user')).invitations.party.id).to.equal(party._id);
});
it('allows 30 members in a party', async () => {
let invitesToGenerate = [];
// Generate 29 users to invite (29 + leader = 30 members)
for (let i = 0; i < PARTY_LIMIT_MEMBERS - 1; i++) {
invitesToGenerate.push(generateUser());
}
let generatedInvites = await Promise.all(invitesToGenerate);
// Invite users
expect(await inviter.post(`/groups/${party._id}/invite`, {
uuids: generatedInvites.map(invite => invite._id),
})).to.be.an('array');
});
it('does not allow 30+ members in a party', async () => {
let invitesToGenerate = [];
// Generate 30 users to invite (30 + leader = 31 members)
for (let i = 0; i < PARTY_LIMIT_MEMBERS; i++) {
invitesToGenerate.push(generateUser());
}
let generatedInvites = await Promise.all(invitesToGenerate);
// Invite users
await expect(inviter.post(`/groups/${party._id}/invite`, {
uuids: generatedInvites.map(invite => invite._id),
}))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('partyExceedsMembersLimit', {maxMembersParty: PARTY_LIMIT_MEMBERS}),
});
});
});
});
@@ -0,0 +1,85 @@
import {
generateUser,
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
describe('POST /group/:groupId/add-manager', () => {
let leader, nonLeader, groupToUpdate;
let groupName = 'Test Public Guild';
let groupType = 'guild';
let nonMember;
context('Guilds', () => {
beforeEach(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: groupName,
type: groupType,
privacy: 'public',
},
members: 1,
});
groupToUpdate = group;
leader = groupLeader;
nonLeader = members[0];
nonMember = await generateUser();
});
it('returns an error when a non group leader tries to add member', async () => {
await expect(nonLeader.post(`/groups/${groupToUpdate._id}/add-manager`, {
managerId: nonLeader._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageGroupOnlyLeaderCanUpdate'),
});
});
it('returns an error when trying to promote a non member', async () => {
await expect(leader.post(`/groups/${groupToUpdate._id}/add-manager`, {
managerId: nonMember._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('userMustBeMember'),
});
});
it('allows a leader to add managers', async () => {
let updatedGroup = await leader.post(`/groups/${groupToUpdate._id}/add-manager`, {
managerId: nonLeader._id,
});
expect(updatedGroup.managers[nonLeader._id]).to.be.true;
});
});
context('Party', () => {
let party, partyLeader, partyNonLeader;
beforeEach(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: groupName,
type: 'party',
privacy: 'private',
},
members: 1,
});
party = group;
partyLeader = groupLeader;
partyNonLeader = members[0];
});
it('allows leader of party to add managers', async () => {
let updatedGroup = await partyLeader.post(`/groups/${party._id}/add-manager`, {
managerId: partyNonLeader._id,
});
expect(updatedGroup.managers[partyNonLeader._id]).to.be.true;
});
});
});
@@ -4,7 +4,7 @@ import {
} from '../../../../../helpers/api-integration/v3';
import { find } from 'lodash';
describe('DELETE /tasks/:id', () => {
describe('Groups DELETE /tasks/:id', () => {
let user, guild, member, member2, task;
function findAssignedTask (memberTask) {
@@ -48,6 +48,21 @@ describe('DELETE /tasks/:id', () => {
});
});
it('allows a manager to delete a group task', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.del(`/tasks/${task._id}`);
await expect(user.get(`/tasks/${task._id}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('taskNotFound'),
});
});
it('unlinks assigned user', async () => {
await user.del(`/tasks/${task._id}`);
@@ -55,4 +55,13 @@ describe('GET /approvals/group/:groupId', () => {
let approvals = await user.get(`/approvals/group/${guild._id}`);
expect(approvals[0]._id).to.equal(syncedTask._id);
});
it('allows managers to get a list of task that need approval', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member._id,
});
let approvals = await member.get(`/approvals/group/${guild._id}`);
expect(approvals[0]._id).to.equal(syncedTask._id);
});
});
@@ -5,7 +5,7 @@ import {
import { find } from 'lodash';
describe('POST /tasks/:id/approve/:userId', () => {
let user, guild, member, task;
let user, guild, member, member2, task;
function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
@@ -17,12 +17,13 @@ describe('POST /tasks/:id/approve/:userId', () => {
name: 'Test Guild',
type: 'guild',
},
members: 1,
members: 2,
});
guild = group;
user = groupLeader;
member = members[0];
member2 = members[1];
task = await user.post(`/tasks/group/${guild._id}`, {
text: 'test todo',
@@ -69,4 +70,74 @@ describe('POST /tasks/:id/approve/:userId', () => {
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
});
it('allows a manager to approve an assigned user', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
await member2.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(2);
expect(member.notifications[0].type).to.equal('GROUP_TASK_APPROVED');
expect(member.notifications[0].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
expect(member.notifications[1].type).to.equal('SCORED_TASK');
expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
expect(syncedTask.group.approval.approved).to.be.true;
expect(syncedTask.group.approval.approvingUser).to.equal(member2._id);
expect(syncedTask.group.approval.dateApproved).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
});
it('removes approval pending notifications from managers', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
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 user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(1);
expect(user.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
expect(member2.notifications.length).to.equal(1);
expect(member2.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(0);
expect(member2.notifications.length).to.equal(0);
});
it('prevents double approval on a task', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await expect(user.post(`/tasks/${task._id}/approve/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('canOnlyApproveTaskOnce'),
});
});
});
@@ -5,7 +5,7 @@ import {
import { find } from 'lodash';
describe('POST /tasks/:id/score/:direction', () => {
let user, guild, member, task;
let user, guild, member, member2, task;
function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
@@ -17,12 +17,13 @@ describe('POST /tasks/:id/score/:direction', () => {
name: 'Test Guild',
type: 'guild',
},
members: 1,
members: 2,
});
guild = group;
user = groupLeader;
member = members[0];
member2 = members[1];
task = await user.post(`/tasks/group/${guild._id}`, {
text: 'test todo',
@@ -56,6 +57,7 @@ describe('POST /tasks/:id/score/:direction', () => {
expect(user.notifications[0].data.message).to.equal(t('userHasRequestedTaskApproval', {
user: member.auth.local.username,
taskName: updatedTask.text,
taskId: updatedTask._id,
}, 'cs')); // This test only works if we have the notification translated
expect(user.notifications[0].data.groupId).to.equal(guild._id);
@@ -63,6 +65,42 @@ describe('POST /tasks/:id/score/:direction', () => {
expect(updatedTask.group.approval.requestedDate).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
});
it('sends notifications to all managers', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
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();
await member2.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,
taskId: updatedTask._id,
}));
expect(user.notifications[0].data.groupId).to.equal(guild._id);
expect(member2.notifications.length).to.equal(1);
expect(member2.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
expect(member2.notifications[0].data.message).to.equal(t('userHasRequestedTaskApproval', {
user: member.auth.local.username,
taskName: updatedTask.text,
taskId: updatedTask._id,
}));
expect(member2.notifications[0].data.groupId).to.equal(guild._id);
});
it('errors when approval has already been requested', async () => {
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
@@ -1,16 +1,29 @@
import {
generateUser,
generateGroup,
createAndPopulateGroup,
translate as t,
} from '../../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
describe('POST /tasks/group/:groupid', () => {
let user, guild;
let user, guild, manager;
let groupName = 'Test Public Guild';
let groupType = 'guild';
beforeEach(async () => {
user = await generateUser({balance: 1});
guild = await generateGroup(user, {type: 'guild'});
let { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: groupName,
type: groupType,
privacy: 'private',
},
members: 1,
});
guild = group;
user = groupLeader;
manager = members[0];
});
it('returns error when group is not found', async () => {
@@ -116,4 +129,27 @@ describe('POST /tasks/group/:groupid', () => {
expect(task.everyX).to.eql(5);
expect(new Date(task.startDate)).to.eql(now);
});
it('allows a manager to add a group task', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: manager._id,
});
let task = await manager.post(`/tasks/group/${guild._id}`, {
text: 'test habit',
type: 'habit',
up: false,
down: true,
notes: 1976,
});
let groupTask = await manager.get(`/tasks/group/${guild._id}`);
expect(groupTask[0].group.id).to.equal(guild._id);
expect(task.text).to.eql('test habit');
expect(task.notes).to.eql('1976');
expect(task.type).to.eql('habit');
expect(task.up).to.eql(false);
expect(task.down).to.eql(true);
});
});
@@ -6,7 +6,7 @@ import {
import { v4 as generateUUID } from 'uuid';
import { find } from 'lodash';
describe('POST /tasks/:taskId', () => {
describe('POST /tasks/:taskId/assign/:memberId', () => {
let user, guild, member, member2, task;
function findAssignedTask (memberTask) {
@@ -130,4 +130,19 @@ describe('POST /tasks/:taskId', () => {
expect(member1SyncedTask).to.exist;
expect(member2SyncedTask).to.exist;
});
it('allows a manager to assign tasks', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
let groupTask = await member2.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;
});
});
@@ -114,4 +114,19 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
expect(groupTask[0].group.assignedUsers).to.contain(member2._id);
expect(member2SyncedTask).to.exist;
});
it('allows a manager to unassign a user from a task', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/unassign/${member._id}`);
let groupTask = await member2.get(`/tasks/group/${guild._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
expect(groupTask[0].group.assignedUsers).to.not.contain(member._id);
expect(syncedTask).to.not.exist;
});
});
@@ -89,4 +89,25 @@ describe('PUT /tasks/:id', () => {
expect(member2SyncedTask.up).to.eql(false);
expect(member2SyncedTask.down).to.eql(false);
});
it('updates the linked tasks', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.put(`/tasks/${task._id}`, {
text: 'some new text',
up: false,
down: false,
notes: 'some new notes',
});
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
expect(syncedTask.text).to.eql('some new text');
expect(syncedTask.up).to.eql(false);
expect(syncedTask.down).to.eql(false);
});
});
@@ -9,6 +9,8 @@ import {
sha1Encrypt as sha1EncryptPassword,
} from '../../../../../../website/server/libs/password';
import nconf from 'nconf';
describe('POST /user/auth/local/login', () => {
let api;
let user;
@@ -43,7 +45,7 @@ describe('POST /user/auth/local/login', () => {
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('accountSuspended', { userId: user._id }),
message: t('accountSuspended', { communityManagerEmail: nconf.get('EMAILS:COMMUNITY_MANAGER_EMAIL'), userId: user._id }),
});
});
@@ -34,25 +34,209 @@ describe('POST /user/auth/local/register', () => {
expect(user.apiToken).to.exist;
expect(user.auth.local.username).to.eql(username);
expect(user.profile.name).to.eql(username);
expect(user.newUser).to.eql(true);
});
it('provides default tags and tasks', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
context('provides default tags and tasks', async () => {
it('for a generic API consumer', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
let requests = new ApiUser(user);
let habits = await requests.get('/tasks/user?type=habits');
let dailys = await requests.get('/tasks/user?type=dailys');
let todos = await requests.get('/tasks/user?type=todos');
let rewards = await requests.get('/tasks/user?type=rewards');
let tags = await requests.get('/tags');
expect(habits).to.have.a.lengthOf(0);
expect(dailys).to.have.a.lengthOf(0);
expect(todos).to.have.a.lengthOf(1);
expect(todos[0].text).to.eql(t('defaultTodo1Text'));
expect(todos[0].notes).to.eql(t('defaultTodoNotes'));
expect(rewards).to.have.a.lengthOf(0);
expect(tags).to.have.a.lengthOf(7);
expect(tags[0].name).to.eql(t('defaultTag1'));
expect(tags[1].name).to.eql(t('defaultTag2'));
expect(tags[2].name).to.eql(t('defaultTag3'));
expect(tags[3].name).to.eql(t('defaultTag4'));
expect(tags[4].name).to.eql(t('defaultTag5'));
expect(tags[5].name).to.eql(t('defaultTag6'));
expect(tags[6].name).to.eql(t('defaultTag7'));
});
expect(user.tags).to.have.a.lengthOf(7);
expect(user.tasksOrder.todos).to.have.a.lengthOf(1);
expect(user.tasksOrder.dailys).to.have.a.lengthOf(0);
expect(user.tasksOrder.rewards).to.have.a.lengthOf(0);
expect(user.tasksOrder.habits).to.have.a.lengthOf(0);
it('for Web', async () => {
api = requester(
null,
{'x-client': 'habitica-web'},
);
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
let requests = new ApiUser(user);
let habits = await requests.get('/tasks/user?type=habits');
let dailys = await requests.get('/tasks/user?type=dailys');
let todos = await requests.get('/tasks/user?type=todos');
let rewards = await requests.get('/tasks/user?type=rewards');
let tags = await requests.get('/tags');
expect(habits).to.have.a.lengthOf(3);
expect(habits[0].text).to.eql(t('defaultHabit1Text'));
expect(habits[0].notes).to.eql('');
expect(habits[1].text).to.eql(t('defaultHabit2Text'));
expect(habits[1].notes).to.eql('');
expect(habits[2].text).to.eql(t('defaultHabit3Text'));
expect(habits[2].notes).to.eql('');
expect(dailys).to.have.a.lengthOf(0);
expect(todos).to.have.a.lengthOf(1);
expect(todos[0].text).to.eql(t('defaultTodo1Text'));
expect(todos[0].notes).to.eql(t('defaultTodoNotes'));
expect(rewards).to.have.a.lengthOf(1);
expect(rewards[0].text).to.eql(t('defaultReward1Text'));
expect(rewards[0].notes).to.eql('');
expect(tags).to.have.a.lengthOf(7);
expect(tags[0].name).to.eql(t('defaultTag1'));
expect(tags[1].name).to.eql(t('defaultTag2'));
expect(tags[2].name).to.eql(t('defaultTag3'));
expect(tags[3].name).to.eql(t('defaultTag4'));
expect(tags[4].name).to.eql(t('defaultTag5'));
expect(tags[5].name).to.eql(t('defaultTag6'));
expect(tags[6].name).to.eql(t('defaultTag7'));
});
it('for Android', async () => {
api = requester(
null,
{'x-client': 'habitica-android'},
);
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
let requests = new ApiUser(user);
let habits = await requests.get('/tasks/user?type=habits');
let dailys = await requests.get('/tasks/user?type=dailys');
let todos = await requests.get('/tasks/user?type=todos');
let rewards = await requests.get('/tasks/user?type=rewards');
let tags = await requests.get('/tags');
expect(habits).to.have.a.lengthOf(2);
expect(habits[0].text).to.eql(t('defaultHabit4Text'));
expect(habits[0].notes).to.eql(t('defaultHabit4Notes'));
expect(habits[1].text).to.eql(t('defaultHabit5Text'));
expect(habits[1].notes).to.eql(t('defaultHabit5Notes'));
expect(dailys).to.have.a.lengthOf(1);
expect(dailys[0].text).to.eql(t('defaultDaily1Text'));
expect(dailys[0].notes).to.eql('');
expect(todos).to.have.a.lengthOf(2);
expect(todos[0].text).to.eql(t('defaultTodo1Text'));
expect(todos[0].notes).to.eql(t('defaultTodoNotes'));
expect(todos[1].text).to.eql(t('defaultTodo2Text'));
expect(todos[1].notes).to.eql(t('defaultTodo2Notes'));
expect(rewards).to.have.a.lengthOf(1);
expect(rewards[0].text).to.eql(t('defaultReward2Text'));
expect(rewards[0].notes).to.eql(t('defaultReward2Notes'));
expect(tags).to.have.a.lengthOf(7);
expect(tags[0].name).to.eql(t('defaultTag1'));
expect(tags[1].name).to.eql(t('defaultTag2'));
expect(tags[2].name).to.eql(t('defaultTag3'));
expect(tags[3].name).to.eql(t('defaultTag4'));
expect(tags[4].name).to.eql(t('defaultTag5'));
expect(tags[5].name).to.eql(t('defaultTag6'));
expect(tags[6].name).to.eql(t('defaultTag7'));
});
it('for iOS', async () => {
api = requester(
null,
{'x-client': 'habitica-ios'},
);
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
let requests = new ApiUser(user);
let habits = await requests.get('/tasks/user?type=habits');
let dailys = await requests.get('/tasks/user?type=dailys');
let todos = await requests.get('/tasks/user?type=todos');
let rewards = await requests.get('/tasks/user?type=rewards');
let tags = await requests.get('/tags');
expect(habits).to.have.a.lengthOf(2);
expect(habits[0].text).to.eql(t('defaultHabit4Text'));
expect(habits[0].notes).to.eql(t('defaultHabit4Notes'));
expect(habits[1].text).to.eql(t('defaultHabit5Text'));
expect(habits[1].notes).to.eql(t('defaultHabit5Notes'));
expect(dailys).to.have.a.lengthOf(1);
expect(dailys[0].text).to.eql(t('defaultDaily1Text'));
expect(dailys[0].notes).to.eql('');
expect(todos).to.have.a.lengthOf(2);
expect(todos[0].text).to.eql(t('defaultTodo1Text'));
expect(todos[0].notes).to.eql(t('defaultTodoNotes'));
expect(todos[1].text).to.eql(t('defaultTodo2Text'));
expect(todos[1].notes).to.eql(t('defaultTodo2Notes'));
expect(rewards).to.have.a.lengthOf(1);
expect(rewards[0].text).to.eql(t('defaultReward2Text'));
expect(rewards[0].notes).to.eql(t('defaultReward2Notes'));
expect(tags).to.have.a.lengthOf(7);
expect(tags[0].name).to.eql(t('defaultTag1'));
expect(tags[1].name).to.eql(t('defaultTag2'));
expect(tags[2].name).to.eql(t('defaultTag3'));
expect(tags[3].name).to.eql(t('defaultTag4'));
expect(tags[4].name).to.eql(t('defaultTag5'));
expect(tags[5].name).to.eql(t('defaultTag6'));
expect(tags[6].name).to.eql(t('defaultTag7'));
});
});
it('enrolls new users in an A/B test', async () => {
@@ -71,6 +255,23 @@ describe('POST /user/auth/local/register', () => {
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
});
it('includes items awarded by default when creating a new user', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.items.quests.dustbunnies).to.equal(1);
expect(user.purchased.background.violet).to.be.ok;
expect(user.preferences.background).to.equal('violet');
});
it('requires password and confirmPassword to match', async () => {
let username = generateRandomUserName();
let email = `${username}@example.com`;
@@ -8,6 +8,8 @@ import {
sha1Encrypt as sha1EncryptPassword,
} from '../../../../../../website/server/libs/password';
import nconf from 'nconf';
const ENDPOINT = '/user/auth/update-email';
describe('PUT /user/auth/update-email', () => {
@@ -68,7 +70,7 @@ describe('PUT /user/auth/update-email', () => {
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('cannotFulfillReq'),
message: t('cannotFulfillReq', { techAssistanceEmail: nconf.get('EMAILS:TECH_ASSISTANCE_EMAIL') }),
});
});
+20
View File
@@ -83,6 +83,12 @@ describe('payments/index', () => {
};
});
it('awards the Royal Purple Jackalope pet', async () => {
await api.createSubscription(data);
expect(recipient.items.pets['Jackalope-RoyalPurple']).to.eql(5);
});
it('adds extra months to an existing subscription', async () => {
recipient.purchased.plan = plan;
@@ -241,6 +247,12 @@ describe('payments/index', () => {
expect(user.purchased.plan.dateCreated).to.exist;
});
it('awards the Royal Purple Jackalope pet', async () => {
await api.createSubscription(data);
expect(user.items.pets['Jackalope-RoyalPurple']).to.eql(5);
});
it('sets extraMonths if plan has dateTerminated date', async () => {
user.purchased.plan = plan;
user.purchased.plan.dateTerminated = moment(new Date()).add(2, 'months');
@@ -633,5 +645,13 @@ describe('payments/index', () => {
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
expect(updatedUser.purchased.plan.dateCreated).to.exist;
});
it('awards the Royal Purple Jackalope pet', async () => {
await api.addSubToGroupUser(user, group);
let updatedUser = await User.findById(user._id).exec();
expect(updatedUser.items.pets['Jackalope-RoyalPurple']).to.eql(5);
});
});
});
@@ -603,6 +603,64 @@ describe('Purchasing a subscription for group', () => {
expect(updatedUser.purchased.plan.dateCreated).to.exist;
});
it('does not modify a user with a Google subscription', async () => {
plan.customerId = 'random';
plan.paymentMethod = api.constants.GOOGLE_PAYMENT_METHOD;
let recipient = new User();
recipient.profile.name = 'recipient';
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
user.guilds.push(group._id);
await user.save();
data.groupId = group._id;
await api.createSubscription(data);
let updatedUser = await User.findById(recipient._id).exec();
expect(updatedUser.purchased.plan.planId).to.eql('basic_3mo');
expect(updatedUser.purchased.plan.customerId).to.eql('random');
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
expect(updatedUser.purchased.plan.paymentMethod).to.eql(api.constants.GOOGLE_PAYMENT_METHOD);
expect(updatedUser.purchased.plan.extraMonths).to.eql(0);
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
expect(updatedUser.purchased.plan.lastBillingDate).to.exist;
expect(updatedUser.purchased.plan.dateCreated).to.exist;
});
it('does not modify a user with an iOS subscription', async () => {
plan.customerId = 'random';
plan.paymentMethod = api.constants.IOS_PAYMENT_METHOD;
let recipient = new User();
recipient.profile.name = 'recipient';
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
user.guilds.push(group._id);
await user.save();
data.groupId = group._id;
await api.createSubscription(data);
let updatedUser = await User.findById(recipient._id).exec();
expect(updatedUser.purchased.plan.planId).to.eql('basic_3mo');
expect(updatedUser.purchased.plan.customerId).to.eql('random');
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
expect(updatedUser.purchased.plan.paymentMethod).to.eql(api.constants.IOS_PAYMENT_METHOD);
expect(updatedUser.purchased.plan.extraMonths).to.eql(0);
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
expect(updatedUser.purchased.plan.lastBillingDate).to.exist;
expect(updatedUser.purchased.plan.dateCreated).to.exist;
});
it('updates a user with a cancelled but active group subscription', async () => {
plan.key = 'basic_earned';
plan.customerId = api.constants.GROUP_PLAN_CUSTOMER_ID;
@@ -10,6 +10,9 @@ import { model as Coupon } from '../../../../../website/server/models/coupon';
import stripePayments from '../../../../../website/server/libs/stripePayments';
import payments from '../../../../../website/server/libs/payments';
import common from '../../../../../website/common';
import logger from '../../../../../website/server/libs/logger';
import { v4 as uuid } from 'uuid';
import moment from 'moment';
const i18n = common.i18n;
@@ -759,4 +762,245 @@ describe('Stripe Payments', () => {
expect(updatedGroup.purchased.plan.quantity).to.eql(4);
});
});
describe('handleWebhooks', () => {
describe('all events', () => {
const eventType = 'account.updated';
const event = {id: 123};
const eventRetrieved = {type: eventType};
beforeEach(() => {
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves(eventRetrieved);
sinon.stub(logger, 'error');
});
afterEach(() => {
stripe.events.retrieve.restore();
logger.error.restore();
});
it('logs an error if an unsupported webhook event is passed', async () => {
const error = new Error(`Missing handler for Stripe webhook ${eventType}`);
await stripePayments.handleWebhooks({requestBody: event}, stripe);
expect(logger.error).to.have.been.called.once;
expect(logger.error).to.have.been.calledWith(error, {event: eventRetrieved});
});
it('retrieves and validates the event from Stripe', async () => {
await stripePayments.handleWebhooks({requestBody: event}, stripe);
expect(stripe.events.retrieve).to.have.been.called.once;
expect(stripe.events.retrieve).to.have.been.calledWith(event.id);
});
});
describe('customer.subscription.deleted', () => {
const eventType = 'customer.subscription.deleted';
beforeEach(() => {
sinon.stub(stripe.customers, 'del').returnsPromise().resolves({});
sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
});
afterEach(() => {
stripe.customers.del.restore();
payments.cancelSubscription.restore();
});
it('does not do anything if event.request is null (subscription cancelled manually)', async () => {
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
id: 123,
type: eventType,
request: 123,
});
await stripePayments.handleWebhooks({requestBody: {}}, stripe);
expect(stripe.events.retrieve).to.have.been.called.once;
expect(stripe.customers.del).to.not.have.been.called;
expect(payments.cancelSubscription).to.not.have.been.called;
stripe.events.retrieve.restore();
});
describe('user subscription', () => {
it('throws an error if the user is not found', async () => {
const customerId = 456;
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
id: 123,
type: eventType,
data: {
object: {
plan: {
id: 'basic_earned',
},
customer: customerId,
},
},
request: null,
});
await expect(stripePayments.handleWebhooks({requestBody: {}}, stripe)).to.eventually.be.rejectedWith({
message: i18n.t('userNotFound'),
httpCode: 404,
name: 'NotFound',
});
expect(stripe.customers.del).to.not.have.been.called;
expect(payments.cancelSubscription).to.not.have.been.called;
stripe.events.retrieve.restore();
});
it('deletes the customer on Stripe and calls payments.cancelSubscription', async () => {
const customerId = '456';
let subscriber = new User();
subscriber.purchased.plan.customerId = customerId;
subscriber.purchased.plan.paymentMethod = 'Stripe';
await subscriber.save();
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
id: 123,
type: eventType,
data: {
object: {
plan: {
id: 'basic_earned',
},
customer: customerId,
},
},
request: null,
});
await stripePayments.handleWebhooks({requestBody: {}}, stripe);
expect(stripe.customers.del).to.have.been.calledOnce;
expect(stripe.customers.del).to.have.been.calledWith(customerId);
expect(payments.cancelSubscription).to.have.been.calledOnce;
let cancelSubscriptionOpts = payments.cancelSubscription.lastCall.args[0];
expect(cancelSubscriptionOpts.user._id).to.equal(subscriber._id);
expect(cancelSubscriptionOpts.paymentMethod).to.equal('Stripe');
expect(Math.round(moment(cancelSubscriptionOpts.nextBill).diff(new Date(), 'days', true))).to.equal(3);
expect(cancelSubscriptionOpts.groupId).to.be.undefined;
stripe.events.retrieve.restore();
});
});
describe('group plan subscription', () => {
it('throws an error if the group is not found', async () => {
const customerId = 456;
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
id: 123,
type: eventType,
data: {
object: {
plan: {
id: 'group_monthly',
},
customer: customerId,
},
},
request: null,
});
await expect(stripePayments.handleWebhooks({requestBody: {}}, stripe)).to.eventually.be.rejectedWith({
message: i18n.t('groupNotFound'),
httpCode: 404,
name: 'NotFound',
});
expect(stripe.customers.del).to.not.have.been.called;
expect(payments.cancelSubscription).to.not.have.been.called;
stripe.events.retrieve.restore();
});
it('throws an error if the group leader is not found', async () => {
const customerId = 456;
let subscriber = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: uuid(),
});
subscriber.purchased.plan.customerId = customerId;
subscriber.purchased.plan.paymentMethod = 'Stripe';
await subscriber.save();
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
id: 123,
type: eventType,
data: {
object: {
plan: {
id: 'group_monthly',
},
customer: customerId,
},
},
request: null,
});
await expect(stripePayments.handleWebhooks({requestBody: {}}, stripe)).to.eventually.be.rejectedWith({
message: i18n.t('userNotFound'),
httpCode: 404,
name: 'NotFound',
});
expect(stripe.customers.del).to.not.have.been.called;
expect(payments.cancelSubscription).to.not.have.been.called;
stripe.events.retrieve.restore();
});
it('deletes the customer on Stripe and calls payments.cancelSubscription', async () => {
const customerId = '456';
let leader = new User();
await leader.save();
let subscriber = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: leader._id,
});
subscriber.purchased.plan.customerId = customerId;
subscriber.purchased.plan.paymentMethod = 'Stripe';
await subscriber.save();
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
id: 123,
type: eventType,
data: {
object: {
plan: {
id: 'group_monthly',
},
customer: customerId,
},
},
request: null,
});
await stripePayments.handleWebhooks({requestBody: {}}, stripe);
expect(stripe.customers.del).to.have.been.calledOnce;
expect(stripe.customers.del).to.have.been.calledWith(customerId);
expect(payments.cancelSubscription).to.have.been.calledOnce;
let cancelSubscriptionOpts = payments.cancelSubscription.lastCall.args[0];
expect(cancelSubscriptionOpts.user._id).to.equal(leader._id);
expect(cancelSubscriptionOpts.paymentMethod).to.equal('Stripe');
expect(Math.round(moment(cancelSubscriptionOpts.nextBill).diff(new Date(), 'days', true))).to.equal(3);
expect(cancelSubscriptionOpts.groupId).to.equal(subscriber._id);
stripe.events.retrieve.restore();
});
});
});
});
});
+182
View File
@@ -0,0 +1,182 @@
import {
generateRes,
generateReq,
generateNext,
} from '../../../../helpers/api-unit.helper';
import nconf from 'nconf';
import requireAgain from 'require-again';
describe('redirects middleware', () => {
let res, req, next;
let pathToRedirectsMiddleware = '../../../../../website/server/middlewares/redirects';
beforeEach(() => {
res = generateRes();
req = generateReq();
next = generateNext();
});
context('forceSSL', () => {
it('sends http requests to https', () => {
let nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
nconfStub.withArgs('IS_PROD').returns(true);
req.header = sandbox.stub().withArgs('x-forwarded-proto').returns('http');
req.originalUrl = '/static/front';
let attachRedirects = requireAgain(pathToRedirectsMiddleware);
attachRedirects.forceSSL(req, res, next);
expect(res.redirect).to.be.calledOnce;
expect(res.redirect).to.be.calledWith('https://habitica.com/static/front');
});
it('does not redirect https forwarded requests', () => {
let nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
nconfStub.withArgs('IS_PROD').returns(true);
req.header = sandbox.stub().withArgs('x-forwarded-proto').returns('https');
req.originalUrl = '/static/front';
let attachRedirects = requireAgain(pathToRedirectsMiddleware);
attachRedirects.forceSSL(req, res, next);
expect(res.redirect).to.be.notCalled;
});
it('does not redirect outside of production environments', () => {
let nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
nconfStub.withArgs('IS_PROD').returns(false);
req.header = sandbox.stub().withArgs('x-forwarded-proto').returns('http');
req.originalUrl = '/static/front';
let attachRedirects = requireAgain(pathToRedirectsMiddleware);
attachRedirects.forceSSL(req, res, next);
expect(res.redirect).to.be.notCalled;
});
it('does not redirect if base URL is not https', () => {
let nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('BASE_URL').returns('http://habitica.com');
nconfStub.withArgs('IS_PROD').returns(true);
req.header = sandbox.stub().withArgs('x-forwarded-proto').returns('http');
req.originalUrl = '/static/front';
let attachRedirects = requireAgain(pathToRedirectsMiddleware);
attachRedirects.forceSSL(req, res, next);
expect(res.redirect).to.be.notCalled;
});
});
context('forceHabitica', () => {
it('sends requests with differing hostname to base URL host', () => {
let nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
nconfStub.withArgs('IGNORE_REDIRECT').returns('false');
nconfStub.withArgs('IS_PROD').returns(true);
req.hostname = 'www.habitica.com';
req.method = 'GET';
req.originalUrl = '/static/front';
req.url = '/static/front';
let attachRedirects = requireAgain(pathToRedirectsMiddleware);
attachRedirects.forceHabitica(req, res, next);
expect(res.redirect).to.be.calledOnce;
expect(res.redirect).to.be.calledWith(301, 'https://habitica.com/static/front');
});
it('does not redirect outside of production environments', () => {
let nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
nconfStub.withArgs('IGNORE_REDIRECT').returns('false');
nconfStub.withArgs('IS_PROD').returns(false);
req.hostname = 'www.habitica.com';
req.method = 'GET';
req.originalUrl = '/static/front';
req.url = '/static/front';
let attachRedirects = requireAgain(pathToRedirectsMiddleware);
attachRedirects.forceHabitica(req, res, next);
expect(res.redirect).to.be.notCalled;
});
it('does not redirect if env is set to ignore redirection', () => {
let nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
nconfStub.withArgs('IGNORE_REDIRECT').returns('true');
nconfStub.withArgs('IS_PROD').returns(true);
req.hostname = 'www.habitica.com';
req.method = 'GET';
req.originalUrl = '/static/front';
req.url = '/static/front';
let attachRedirects = requireAgain(pathToRedirectsMiddleware);
attachRedirects.forceHabitica(req, res, next);
expect(res.redirect).to.be.notCalled;
});
it('does not redirect if request hostname matches base URL host', () => {
let nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
nconfStub.withArgs('IGNORE_REDIRECT').returns('false');
nconfStub.withArgs('IS_PROD').returns(true);
req.hostname = 'habitica.com';
req.method = 'GET';
req.originalUrl = '/static/front';
req.url = '/static/front';
let attachRedirects = requireAgain(pathToRedirectsMiddleware);
attachRedirects.forceHabitica(req, res, next);
expect(res.redirect).to.be.notCalled;
});
it('does not redirect if request is an API URL', () => {
let nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
nconfStub.withArgs('IGNORE_REDIRECT').returns('false');
nconfStub.withArgs('IS_PROD').returns(true);
req.hostname = 'www.habitica.com';
req.method = 'GET';
req.originalUrl = '/api/v3/challenges';
req.url = '/api/v3/challenges';
let attachRedirects = requireAgain(pathToRedirectsMiddleware);
attachRedirects.forceHabitica(req, res, next);
expect(res.redirect).to.be.notCalled;
});
it('does not redirect if request method is not GET', () => {
let nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
nconfStub.withArgs('IGNORE_REDIRECT').returns('false');
nconfStub.withArgs('IS_PROD').returns(true);
req.hostname = 'www.habitica.com';
req.method = 'POST';
req.originalUrl = '/static/front';
req.url = '/static/front';
let attachRedirects = requireAgain(pathToRedirectsMiddleware);
attachRedirects.forceHabitica(req, res, next);
expect(res.redirect).to.be.notCalled;
});
});
});
+34
View File
@@ -104,6 +104,40 @@ describe('Challenge Model', () => {
expect(updatedNewMember.tags[7].id).to.equal(challenge._id);
expect(updatedNewMember.tags[7].name).to.equal(challenge.shortName);
expect(syncedTask).to.exist;
expect(syncedTask.attribute).to.eql('str');
});
it('syncs a challenge to a user with the existing task', async () => {
await challenge.addTasks([task]);
let updatedLeader = await User.findOne({_id: leader._id});
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
let syncedTask = find(updatedLeadersTasks, function findNewTask (updatedLeadersTask) {
return updatedLeadersTask.challenge.taskId === task._id;
});
let createdAtBefore = syncedTask.createdAt;
let attributeBefore = syncedTask.attribute;
let newTitle = 'newName';
task.text = newTitle;
task.attribute = 'int';
await task.save();
await challenge.syncToUser(leader);
updatedLeader = await User.findOne({_id: leader._id});
updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
syncedTask = find(updatedLeadersTasks, function findNewTask (updatedLeadersTask) {
return updatedLeadersTask.challenge.taskId === task._id;
});
let createdAtAfter = syncedTask.createdAt;
let attributeAfter = syncedTask.attribute;
expect(createdAtBefore).to.eql(createdAtAfter);
expect(attributeBefore).to.eql(attributeAfter);
expect(syncedTask.text).to.eql(newTitle);
});
it('updates tasks to challenge and challenge members', async () => {
+170 -97
View File
@@ -2,11 +2,14 @@ import moment from 'moment';
import { v4 as generateUUID } from 'uuid';
import validator from 'validator';
import { sleep } from '../../../../helpers/api-unit.helper';
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';
SPAM_MESSAGE_LIMIT,
SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
SPAM_WINDOW_LENGTH,
INVITES_LIMIT,
model as Group,
} from '../../../../../website/server/models/group';
import { model as User } from '../../../../../website/server/models/user';
import { quests as questScrolls } from '../../../../../website/common/script/content';
import { groupChatReceivedWebhook } from '../../../../../website/server/libs/webhook';
import * as email from '../../../../../website/server/libs/email';
@@ -460,73 +463,67 @@ describe('Group Model', () => {
};
});
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 no uuids or emails are passed in', async () => {
await expect(Group.validateInvitations(null, null, res)).to.eventually.be.rejected.and.eql({
httpCode: 400,
message: 'Bad request.',
name: 'BadRequest',
});
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('canOnlyInviteEmailUuid');
});
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 uuids are passed in, but they are not an array', async () => {
await expect(Group.validateInvitations({ uuid: 'user-id'}, null, res)).to.eventually.be.rejected.and.eql({
httpCode: 400,
message: 'Bad request.',
name: 'BadRequest',
});
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('uuidsMustBeAnArray');
});
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 only emails are passed in, but they are not an array', async () => {
await expect(Group.validateInvitations(null, { emails: 'user@example.com'}, res)).to.eventually.be.rejected.and.eql({
httpCode: 400,
message: 'Bad request.',
name: 'BadRequest',
});
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('emailsMustBeAnArray');
});
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 emails are not passed in, and uuid array is empty', async () => {
await expect(Group.validateInvitations([], null, res)).to.eventually.be.rejected.and.eql({
httpCode: 400,
message: 'Bad request.',
name: 'BadRequest',
});
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('inviteMissingUuid');
});
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 are not passed in, and email array is empty', async () => {
await expect(Group.validateInvitations(null, [], res)).to.eventually.be.rejected.and.eql({
httpCode: 400,
message: 'Bad request.',
name: 'BadRequest',
});
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('inviteMissingEmail');
});
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 uuids and emails are passed in as empty arrays', async () => {
await expect(Group.validateInvitations([], [], res)).to.eventually.be.rejected.and.eql({
httpCode: 400,
message: 'Bad request.',
name: 'BadRequest',
});
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('inviteMustNotBeEmpty');
});
it('throws an error if total invites exceed max invite constant', (done) => {
it('throws an error if total invites exceed max invite constant', async () => {
let uuids = [];
let emails = [];
@@ -537,17 +534,16 @@ describe('Group Model', () => {
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();
}
await expect(Group.validateInvitations(uuids, emails, res)).to.eventually.be.rejected.and.eql({
httpCode: 400,
message: 'Bad request.',
name: 'BadRequest',
});
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('canOnlyInviteMaxInvites', {maxInvites: INVITES_LIMIT });
});
it('does not throw error if number of invites matches max invite limit', () => {
it('does not throw error if number of invites matches max invite limit', async () => {
let uuids = [];
let emails = [];
@@ -556,49 +552,33 @@ describe('Group Model', () => {
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();
await Group.validateInvitations(uuids, emails, res);
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();
it('does not throw an error if only user ids are passed in', async () => {
await Group.validateInvitations(['user-id', 'user-id2'], null, res);
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();
it('does not throw an error if only emails are passed in', async () => {
await Group.validateInvitations(null, ['user1@example.com', 'user2@example.com'], res);
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();
it('does not throw an error if both uuids and emails are passed in', async () => {
await Group.validateInvitations(['user-id', 'user-id2'], ['user1@example.com', 'user2@example.com'], res);
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();
it('does not throw an error if uuids are passed in and emails are an empty array', async () => {
await Group.validateInvitations(['user-id', 'user-id2'], [], res);
expect(res.t).to.not.be.called;
});
it('does not throw an error if emails are passed in and uuids are an empty array', async () => {
await Group.validateInvitations([], ['user1@example.com', 'user2@example.com'], res);
expect(res.t).to.not.be.called;
});
});
@@ -621,6 +601,99 @@ describe('Group Model', () => {
});
});
describe('#checkChatSpam', () => {
let testUser, testTime, tavern;
let testUserID = '1';
beforeEach(async () => {
testTime = Date.now();
tavern = new Group({
name: 'test tavern',
type: 'guild',
privacy: 'public',
});
tavern._id = TAVERN_ID;
testUser = {
_id: testUserID,
};
});
function generateTestMessage (overrides = {}) {
return Object.assign({}, {
text: 'test message',
uuid: testUserID,
timestamp: testTime,
}, overrides);
}
it('group that is not the tavern returns false, while tavern returns true', async () => {
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
party.chat.push(generateTestMessage());
}
expect(party.checkChatSpam(testUser)).to.eql(false);
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
tavern.chat.push(generateTestMessage());
}
expect(tavern.checkChatSpam(testUser)).to.eql(true);
});
it('high enough contributor returns false', async () => {
let highContributor = testUser;
highContributor.contributor = {
level: SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
};
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
tavern.chat.push(generateTestMessage());
}
expect(tavern.checkChatSpam(highContributor)).to.eql(false);
});
it('chat with no messages returns false', async () => {
expect(tavern.chat.length).to.eql(0);
expect(tavern.checkChatSpam(testUser)).to.eql(false);
});
it('user has not reached limit but another one has returns false', async () => {
let otherUserID = '2';
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
tavern.chat.push(generateTestMessage({uuid: otherUserID}));
}
expect(tavern.checkChatSpam(testUser)).to.eql(false);
});
it('user messages is less than the limit returns false', async () => {
for (let i = 0; i < SPAM_MESSAGE_LIMIT - 1; i++) {
tavern.chat.push(generateTestMessage());
}
expect(tavern.checkChatSpam(testUser)).to.eql(false);
});
it('user has reached the message limit outside of window returns false', async () => {
for (let i = 0; i < SPAM_MESSAGE_LIMIT - 1; i++) {
tavern.chat.push(generateTestMessage());
}
let earlierTimestamp = testTime - SPAM_WINDOW_LENGTH - 1;
tavern.chat.push(generateTestMessage({timestamp: earlierTimestamp}));
expect(tavern.checkChatSpam(testUser)).to.eql(false);
});
it('user has posted too many messages in limit returns true', async () => {
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
tavern.chat.push(generateTestMessage());
}
expect(tavern.checkChatSpam(testUser)).to.eql(true);
});
});
describe('#leaveGroup', () => {
it('removes user from group quest', async () => {
party.quest.members = {
@@ -334,4 +334,34 @@ describe('Settings Controller', function () {
});
});
});
context('Fixing character values', function () {
describe('#restore', function () {
var blankRestoreValues = {
stats: {
hp: 0,
exp: 0,
gp: 0,
lvl: 0,
mp: 0,
},
achievements: {
streak: 0,
},
};
it('doesn\'t update character values when level is less than 1', function () {
scope.restoreValues = blankRestoreValues;
scope.restore();
expect(User.set).to.not.be.called;
});
it('updates character values when level is at least 1', function () {
scope.restoreValues = blankRestoreValues;
scope.restoreValues.stats.lvl = 1;
scope.restore();
expect(User.set).to.be.called;
});
});
});
});
@@ -0,0 +1,69 @@
'use strict';
describe('User Controller', function() {
var $rootScope, $window, User, shared, scope, ctrl, content;
beforeEach(function() {
module(function ($provide) {
var user = specHelper.newUser();
User = {user: user}
$provide.value('Guide', sandbox.stub());
$provide.value('User', User);
$provide.value('Achievement', sandbox.stub());
$provide.value('Social', sandbox.stub());
$provide.value('Shared', {
achievements: {
getAchievementsForProfile: sandbox.stub()
},
shops: {
getBackgroundShopSets: sandbox.stub()
}
});
$provide.value('Content', {
loginIncentives: sandbox.stub()
})
});
inject(function($rootScope, $controller, User, Content) {
scope = $rootScope.$new();
content = Content;
$controller('RootCtrl', { $scope: scope, User: User});
ctrl = $controller('UserCtrl', { $scope: scope, User: User, $window: $window});
});
});
describe('getProgressDisplay', function() {
beforeEach(() => {
sandbox.stub(window.env, 't');
window.env.t.onFirstCall().returns('Progress until next');
});
it('should return initial progress', function() {
scope.profile.loginIncentives = 0;
content.loginIncentives = [{
nextRewardAt: 1,
reward: true
}];
var actual = scope.getProgressDisplay();
expect(actual.trim()).to.eql('Progress until next 0/1');
});
it('should return progress between next reward and current reward', function() {
scope.profile.loginIncentives = 1;
content.loginIncentives = [{
nextRewardAt: 1,
reward: true
}, {
prevRewardAt: 0,
nextRewardAt: 2,
reward: true
}, {
prevRewardAt: 1,
nextRewardAt: 3
}];
var actual = scope.getProgressDisplay();
expect(actual.trim()).to.eql('Progress until next 0/1');
});
});
});
@@ -0,0 +1,35 @@
describe('task Directive', () => {
var compile, scope, directiveElem, $modal;
beforeEach(function(){
module(function($provide) {
$modal = {
open: sandbox.spy(),
};
$provide.value('$modal', $modal);
});
inject(function($compile, $rootScope, $templateCache) {
compile = $compile;
scope = $rootScope.$new();
$templateCache.put('templates/task.html', '<div>Task</div>');
});
directiveElem = getCompiledElement();
});
function getCompiledElement(){
var element = angular.element('<task></task>');
var compiledElement = compile(element)(scope);
scope.$digest();
return compiledElement;
}
xit('opens task note modal', () => {
scope.showNoteDetails();
expect($modal.open).to.be.calledOnce;
});
})
+5 -2
View File
@@ -81,8 +81,11 @@ module.exports = function karmaConfig (config) {
},
coverageReporter: {
type: 'lcov',
dir: 'coverage/karma',
reporters: [
{ type: 'lcov', subdir: '.' },
{ type: 'text-summary' },
],
dir: '../../../coverage/karma',
},
// Enable mocha-style reporting, for better test visibility
+1 -1
View File
@@ -28,7 +28,7 @@ module.exports = function (config) {
noInfo: true,
},
coverageReporter: {
dir: './coverage',
dir: '../../../coverage/client-unit',
reporters: [
{ type: 'lcov', subdir: '.' },
{ type: 'text-summary' },
@@ -0,0 +1,151 @@
import { asyncResourceFactory, loadAsyncResource } from 'client/libs/asyncResource';
import axios from 'axios';
import generateStore from 'client/store';
import { sleep } from '../../../../helpers/sleep';
describe('async resource', () => {
it('asyncResourceFactory', () => {
const resource = asyncResourceFactory();
expect(resource.loadingStatus).to.equal('NOT_LOADED');
expect(resource.data).to.equal(null);
expect(resource).to.not.equal(asyncResourceFactory);
});
describe('loadAsyncResource', () => {
context('errors', () => {
it('store is missing', () => {
expect(() => loadAsyncResource({})).to.throw;
});
it('path is missing', () => {
expect(() => loadAsyncResource({
store: 'store',
})).to.throw;
});
it('url is missing', () => {
expect(() => loadAsyncResource({
store: 'store',
path: 'path',
})).to.throw;
});
it('deserialize is missing', () => {
expect(() => loadAsyncResource({
store: 'store',
path: 'path',
url: 'url',
})).to.throw;
});
it('resource not found', () => {
const store = generateStore();
expect(() => loadAsyncResource({
store,
path: 'not existing path',
url: 'url',
deserialize: 'deserialize',
})).to.throw;
});
it('invalid loading status', () => {
const store = generateStore();
store.state.user.loadingStatus = 'INVALID';
expect(loadAsyncResource({
store,
path: 'user',
url: 'url',
deserialize: 'deserialize',
})).to.eventually.be.rejected;
});
});
it('returns the resource if it is already loaded and forceLoad is false', async () => {
const store = generateStore();
store.state.user.loadingStatus = 'LOADED';
store.state.user.data = {_id: 1};
sandbox.stub(axios, 'get');
const resource = await loadAsyncResource({
store,
path: 'user',
url: 'url',
deserialize: 'deserialize',
});
expect(resource).to.equal(store.state.user);
expect(axios.get).to.not.have.been.called;
});
it('load the resource if it is not loaded', async () => {
const store = generateStore();
store.state.user = asyncResourceFactory();
sandbox.stub(axios, 'get').withArgs('/api/v3/user').returns(Promise.resolve({data: {data: {_id: 1}}}));
const resource = await loadAsyncResource({
store,
path: 'user',
url: '/api/v3/user',
deserialize (response) {
return response.data.data;
},
});
expect(resource).to.equal(store.state.user);
expect(resource.loadingStatus).to.equal('LOADED');
expect(resource.data._id).to.equal(1);
expect(axios.get).to.have.been.calledOnce;
});
it('load the resource if it is loaded but forceLoad is true', async () => {
const store = generateStore();
store.state.user.loadingStatus = 'LOADED';
sandbox.stub(axios, 'get').withArgs('/api/v3/user').returns(Promise.resolve({data: {data: {_id: 1}}}));
const resource = await loadAsyncResource({
store,
path: 'user',
url: '/api/v3/user',
deserialize (response) {
return response.data.data;
},
forceLoad: true,
});
expect(resource).to.equal(store.state.user);
expect(resource.loadingStatus).to.equal('LOADED');
expect(resource.data._id).to.equal(1);
expect(axios.get).to.have.been.calledOnce;
});
it('does not send multiple requests if the resource is being loaded', async () => {
const store = generateStore();
store.state.user.loadingStatus = 'LOADING';
sandbox.stub(axios, 'get').withArgs('/api/v3/user').returns(Promise.resolve({data: {data: {_id: 1}}}));
const resourcePromise = loadAsyncResource({
store,
path: 'user',
url: '/api/v3/user',
deserialize (response) {
return response.data.data;
},
forceLoad: true,
});
await sleep(0.1);
const userData = {_id: 1};
expect(store.state.user.loadingStatus).to.equal('LOADING');
expect(axios.get).to.not.have.been.called;
store.state.user.data = userData;
store.state.user.loadingStatus = 'LOADED';
const result = await resourcePromise;
expect(axios.get).to.not.have.been.called;
expect(result).to.equal(store.state.user);
});
});
});
+1 -1
View File
@@ -1,7 +1,7 @@
import deepFreeze from 'client/libs/deepFreeze';
describe('deepFreeze', () => {
it('works as expected', () => {
it('deeply freezes an object', () => {
let obj = {
a: 1,
b () {
@@ -1,10 +1,10 @@
import i18n from 'client/plugins/i18n';
import i18n from 'client/libs/i18n';
import commoni18n from 'common/script/i18n';
import Vue from 'vue';
describe('i18n plugin', () => {
before(() => {
i18n.install(Vue);
Vue.use(i18n);
});
it('adds $t to Vue.prototype', () => {
+178
View File
@@ -0,0 +1,178 @@
import Vue from 'vue';
import StoreModule, { mapState, mapGetters, mapActions } from 'client/libs/store';
import { flattenAndNamespace } from 'client/libs/store/helpers/internals';
describe('Store', () => {
let store;
beforeEach(() => {
store = new StoreModule({ // eslint-disable-line babel/new-cap
state: {
name: 'test',
nested: {
name: 'nested state test',
},
},
getters: {
computedName ({ state }) {
return `${state.name} computed!`;
},
...flattenAndNamespace({
nested: {
computedName ({ state }) {
return `${state.name} computed!`;
},
},
}),
},
actions: {
getName ({ state }, ...args) {
return [state.name, ...args];
},
...flattenAndNamespace({
nested: {
getName ({ state }, ...args) {
return [state.name, ...args];
},
},
}),
},
});
Vue.use(StoreModule);
});
it('injects itself in all component', (done) => {
new Vue({ // eslint-disable-line no-new
store,
created () {
expect(this.$store).to.equal(store);
done();
},
});
});
it('can watch a function on the state', (done) => {
store.watch(state => state.name, (newName) => {
expect(newName).to.equal('test updated');
done();
});
store.state.name = 'test updated';
});
describe('getters', () => {
it('supports getters', () => {
expect(store.getters.computedName).to.equal('test computed!');
store.state.name = 'test updated';
expect(store.getters.computedName).to.equal('test updated computed!');
});
it('supports nested getters', () => {
expect(store.getters['nested:computedName']).to.equal('test computed!');
store.state.name = 'test updated';
expect(store.getters['nested:computedName']).to.equal('test updated computed!');
});
});
describe('actions', () => {
it('can dispatch an action', () => {
expect(store.dispatch('getName', 1, 2, 3)).to.deep.equal(['test', 1, 2, 3]);
});
it('can dispatch a nested action', () => {
expect(store.dispatch('nested:getName', 1, 2, 3)).to.deep.equal(['test', 1, 2, 3]);
});
it('throws an error is the action doesn\'t exists', () => {
expect(() => store.dispatched('wrong')).to.throw;
});
});
describe('helpers', () => {
it('mapState', (done) => {
new Vue({ // eslint-disable-line no-new
store,
data: {
title: 'internal',
},
computed: {
...mapState(['name']),
...mapState({
nameComputed (state, getters) {
return `${this.title} ${getters.computedName} ${state.name}`;
},
}),
...mapState({nestedTest: 'nested.name'}),
},
created () {
expect(this.name).to.equal('test');
expect(this.nameComputed).to.equal('internal test computed! test');
expect(this.nestedTest).to.equal('nested state test');
done();
},
});
});
it('mapGetters', (done) => {
new Vue({ // eslint-disable-line no-new
store,
data: {
title: 'internal',
},
computed: {
...mapGetters(['computedName']),
...mapGetters({
nameComputedTwice: 'computedName',
}),
},
created () {
expect(this.computedName).to.equal('test computed!');
expect(this.nameComputedTwice).to.equal('test computed!');
done();
},
});
});
it('mapActions', (done) => {
new Vue({ // eslint-disable-line no-new
store,
data: {
title: 'internal',
},
methods: {
...mapActions(['getName']),
...mapActions({
getNameRenamed: 'getName',
}),
},
created () {
expect(this.getName('123')).to.deep.equal(['test', '123']);
expect(this.getNameRenamed('123')).to.deep.equal(['test', '123']);
done();
},
});
});
it('flattenAndNamespace', () => {
let result = flattenAndNamespace({
nested: {
computed ({ state }, ...args) {
return [state.name, ...args];
},
getName ({ state }, ...args) {
return [state.name, ...args];
},
},
nested2: {
getName ({ state }, ...args) {
return [state.name, ...args];
},
},
});
expect(Object.keys(result).length).to.equal(3);
expect(Object.keys(result).sort()).to.deep.equal(['nested2:getName', 'nested:computed', 'nested:getName']);
});
});
});
@@ -1,17 +0,0 @@
import { fetchAll as fetchAllGuilds } from 'client/store/actions/guilds';
import axios from 'axios';
import store from 'client/store';
describe('guilds actions', () => {
it('fetchAll', async () => {
const guilds = [{_id: 1}];
sandbox
.stub(axios, 'get')
.withArgs('/api/v3/groups?type=publicGuilds')
.returns(Promise.resolve({data: {data: guilds}}));
await fetchAllGuilds(store);
expect(store.state.guilds).to.equal(guilds);
});
});
+47 -7
View File
@@ -1,14 +1,54 @@
import { fetchUserTasks } from 'client/store/actions/tasks';
import axios from 'axios';
import store from 'client/store';
import generateStore from 'client/store';
describe('tasks actions', () => {
it('fetchUserTasks', async () => {
const tasks = [{_id: 1}];
sandbox.stub(axios, 'get').withArgs('/api/v3/tasks/user').returns(Promise.resolve({data: {data: tasks}}));
let store;
await fetchUserTasks(store);
beforeEach(() => {
store = generateStore();
});
expect(store.state.tasks).to.equal(tasks);
describe('fetchUserTasks', () => {
it('fetches user tasks', async () => {
expect(store.state.tasks.loadingStatus).to.equal('NOT_LOADED');
const tasks = [{_id: 1}];
sandbox.stub(axios, 'get').withArgs('/api/v3/tasks/user').returns(Promise.resolve({data: {data: tasks}}));
await store.dispatch('tasks:fetchUserTasks');
expect(store.state.tasks.data).to.equal(tasks);
expect(store.state.tasks.loadingStatus).to.equal('LOADED');
});
it('does not reload tasks by default', async () => {
const originalTask = [{_id: 1}];
store.state.tasks = {
loadingStatus: 'LOADED',
data: originalTask,
};
const tasks = [{_id: 2}];
sandbox.stub(axios, 'get').withArgs('/api/v3/tasks/user').returns(Promise.resolve({data: {data: tasks}}));
await store.dispatch('tasks:fetchUserTasks');
expect(store.state.tasks.data).to.equal(originalTask);
expect(store.state.tasks.loadingStatus).to.equal('LOADED');
});
it('can reload tasks if forceLoad is true', async () => {
store.state.tasks = {
loadingStatus: 'LOADED',
data: [{_id: 1}],
};
const tasks = [{_id: 2}];
sandbox.stub(axios, 'get').withArgs('/api/v3/tasks/user').returns(Promise.resolve({data: {data: tasks}}));
await store.dispatch('tasks:fetchUserTasks', true);
expect(store.state.tasks.data).to.equal(tasks);
expect(store.state.tasks.loadingStatus).to.equal('LOADED');
});
});
});
+48 -8
View File
@@ -1,14 +1,54 @@
import { fetch as fetchUser } from 'client/store/actions/user';
import axios from 'axios';
import store from 'client/store';
import generateStore from 'client/store';
describe('user actions', () => {
it('fetch', async () => {
const user = {_id: 1};
sandbox.stub(axios, 'get').withArgs('/api/v3/user').returns(Promise.resolve({data: {data: user}}));
describe('tasks actions', () => {
let store;
await fetchUser(store);
beforeEach(() => {
store = generateStore();
});
expect(store.state.user).to.equal(user);
describe('fetch', () => {
it('loads the user', async () => {
expect(store.state.user.loadingStatus).to.equal('NOT_LOADED');
const user = {_id: 1};
sandbox.stub(axios, 'get').withArgs('/api/v3/user').returns(Promise.resolve({data: {data: user}}));
await store.dispatch('user:fetch');
expect(store.state.user.data).to.equal(user);
expect(store.state.user.loadingStatus).to.equal('LOADED');
});
it('does not reload user by default', async () => {
const originalUser = {_id: 1};
store.state.user = {
loadingStatus: 'LOADED',
data: originalUser,
};
const user = {_id: 2};
sandbox.stub(axios, 'get').withArgs('/api/v3/user').returns(Promise.resolve({data: {data: user}}));
await store.dispatch('user:fetch');
expect(store.state.user.data).to.equal(originalUser);
expect(store.state.user.loadingStatus).to.equal('LOADED');
});
it('can reload user if forceLoad is true', async () => {
store.state.user = {
loadingStatus: 'LOADED',
data: {_id: 1},
};
const user = {_id: 2};
sandbox.stub(axios, 'get').withArgs('/api/v3/user').returns(Promise.resolve({data: {data: user}}));
await store.dispatch('user:fetch', true);
expect(store.state.user.data).to.equal(user);
expect(store.state.user.loadingStatus).to.equal('LOADED');
});
});
});
@@ -0,0 +1,16 @@
import generateStore from 'client/store';
describe('getTagsFor getter', () => {
it('returns the tags for a task', () => {
const store = generateStore();
store.state.user.data = {
tags: [
{id: 1, name: 'tag 1'},
{id: 2, name: 'tag 2'},
],
};
const task = {tags: [2]};
expect(store.getters['tasks:getTagsFor'](task)).to.deep.equal(['tag 2']);
});
});
@@ -5,7 +5,7 @@ describe('userGems getter', () => {
expect(userGems({
state: {
user: {
balance: 4.5,
data: {balance: 4.5},
},
},
})).to.equal(18);
+5 -165
View File
@@ -1,168 +1,8 @@
import Vue from 'vue';
import storeInjector from 'inject-loader?-vue!client/store';
import { mapState, mapGetters, mapActions } from 'client/store';
import { flattenAndNamespace } from 'client/store/helpers/internals';
import generateStore from 'client/store';
import Store from 'client/libs/store';
describe('Store', () => {
let injectedStore;
beforeEach(() => {
injectedStore = storeInjector({ // eslint-disable-line babel/new-cap
'./state': {
name: 'test',
},
'./getters': {
computedName ({ state }) {
return `${state.name} computed!`;
},
...flattenAndNamespace({
nested: {
computedName ({ state }) {
return `${state.name} computed!`;
},
},
}),
},
'./actions': {
getName ({ state }, ...args) {
return [state.name, ...args];
},
...flattenAndNamespace({
nested: {
getName ({ state }, ...args) {
return [state.name, ...args];
},
},
}),
},
}).default;
});
it('injects itself in all component', (done) => {
new Vue({ // eslint-disable-line no-new
created () {
expect(this.$store).to.equal(injectedStore);
done();
},
});
});
it('can watch a function on the state', (done) => {
injectedStore.watch(state => state.name, (newName) => {
expect(newName).to.equal('test updated');
done();
});
injectedStore.state.name = 'test updated';
});
describe('getters', () => {
it('supports getters', () => {
expect(injectedStore.getters.computedName).to.equal('test computed!');
injectedStore.state.name = 'test updated';
expect(injectedStore.getters.computedName).to.equal('test updated computed!');
});
it('supports nested getters', () => {
expect(injectedStore.getters['nested:computedName']).to.equal('test computed!');
injectedStore.state.name = 'test updated';
expect(injectedStore.getters['nested:computedName']).to.equal('test updated computed!');
});
});
describe('actions', () => {
it('can dispatch an action', () => {
expect(injectedStore.dispatch('getName', 1, 2, 3)).to.deep.equal(['test', 1, 2, 3]);
});
it('can dispatch a nested action', () => {
expect(injectedStore.dispatch('nested:getName', 1, 2, 3)).to.deep.equal(['test', 1, 2, 3]);
});
it('throws an error is the action doesn\'t exists', () => {
expect(() => injectedStore.dispatched('wrong')).to.throw;
});
});
describe('helpers', () => {
it('mapState', (done) => {
new Vue({ // eslint-disable-line no-new
data: {
title: 'internal',
},
computed: {
...mapState(['name']),
...mapState({
nameComputed (state, getters) {
return `${this.title} ${getters.computedName} ${state.name}`;
},
}),
},
created () {
expect(this.name).to.equal('test');
expect(this.nameComputed).to.equal('internal test computed! test');
done();
},
});
});
it('mapGetters', (done) => {
new Vue({ // eslint-disable-line no-new
data: {
title: 'internal',
},
computed: {
...mapGetters(['computedName']),
...mapGetters({
nameComputedTwice: 'computedName',
}),
},
created () {
expect(this.computedName).to.equal('test computed!');
expect(this.nameComputedTwice).to.equal('test computed!');
done();
},
});
});
it('mapActions', (done) => {
new Vue({ // eslint-disable-line no-new
data: {
title: 'internal',
},
methods: {
...mapActions(['getName']),
...mapActions({
getNameRenamed: 'getName',
}),
},
created () {
expect(this.getName('123')).to.deep.equal(['test', '123']);
expect(this.getNameRenamed('123')).to.deep.equal(['test', '123']);
done();
},
});
});
it('flattenAndNamespace', () => {
let result = flattenAndNamespace({
nested: {
computed ({ state }, ...args) {
return [state.name, ...args];
},
getName ({ state }, ...args) {
return [state.name, ...args];
},
},
nested2: {
getName ({ state }, ...args) {
return [state.name, ...args];
},
},
});
expect(Object.keys(result).length).to.equal(3);
expect(Object.keys(result).sort()).to.deep.equal(['nested2:getName', 'nested:computed', 'nested:getName']);
});
describe('Application store', () => {
it('is an instance of Store', () => {
expect(generateStore()).to.be.an.instanceof(Store);
});
});
+14
View File
@@ -1,6 +1,7 @@
import changeClass from '../../../website/common/script/ops/changeClass';
import {
NotAuthorized,
BadRequest,
} from '../../../website/common/script/libs/errors';
import i18n from '../../../website/common/script/i18n';
import {
@@ -27,6 +28,19 @@ describe('shared.ops.changeClass', () => {
}
});
it('req.query.class is an invalid class', (done) => {
user.flags.classSelected = false;
user.preferences.disableClasses = false;
try {
changeClass(user, {query: {class: 'cellist'}});
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidClass'));
done();
}
});
context('req.query.class is a valid class', () => {
it('errors if user.stats.flagSelected is true and user.balance < 0.75', (done) => {
user.flags.classSelected = true;
+323 -283
View File
@@ -1,310 +1,350 @@
// import { shouldDo, DAY_MAPPING } from '../../website/common/script/cron';
// import moment from 'moment';
import { shouldDo } from '../../website/common/script/cron';
import moment from 'moment';
// import 'moment-recur';
// describe('shouldDo', () => {
// let day, dailyTask;
// let options = {};
describe('shouldDo', () => {
let day, dailyTask;
let options = {};
// beforeEach(() => {
// day = new Date();
// dailyTask = {
// completed: 'false',
// everyX: 1,
// frequency: 'weekly',
// type: 'daily',
// repeat: {
// su: true,
// s: true,
// f: true,
// th: true,
// w: true,
// t: true,
// m: true,
// },
// startDate: new Date(),
// };
// });
beforeEach(() => {
day = new Date();
dailyTask = {
completed: 'false',
everyX: 1,
frequency: 'weekly',
type: 'daily',
repeat: {
su: true,
s: true,
f: true,
th: true,
w: true,
t: true,
m: true,
},
startDate: new Date(),
};
});
// it('leaves Daily inactive before start date', () => {
// dailyTask.startDate = moment().add(1, 'days').toDate();
it('returns false if task type is not a daily', () => {
expect(shouldDo(day, {type: 'todo'})).to.equal(false);
expect(shouldDo(day, {type: 'habit'})).to.equal(false);
expect(shouldDo(day, {type: 'reward'})).to.equal(false);
});
// expect(shouldDo(day, dailyTask, options)).to.equal(false);
// });
it('returns false if startDate is in the future', () => {
dailyTask.startDate = moment().add(1, 'days').toDate();
// context('Every X Days', () => {
// it('leaves Daily inactive in between X Day intervals', () => {
// dailyTask.startDate = moment().subtract(1, 'days').toDate();
// dailyTask.frequency = 'daily';
// dailyTask.everyX = 2;
expect(shouldDo(day, dailyTask, options)).to.equal(false);
});
// expect(shouldDo(day, dailyTask, options)).to.equal(false);
// });
context('Every X Days', () => {
beforeEach(() => {
dailyTask.frequency = 'daily';
});
// it('activates Daily on multiples of X Days', () => {
// dailyTask.startDate = moment().subtract(7, 'days').toDate();
// dailyTask.frequency = 'daily';
// dailyTask.everyX = 7;
it('returns false if daily does not have an everyX property', () => {
delete dailyTask.everyX;
// expect(shouldDo(day, dailyTask, options)).to.equal(true);
// });
// });
expect(shouldDo(day, dailyTask, options)).to.equal(false);
});
// context('Certain Days of the Week', () => {
// it('leaves Daily inactive if day of the week does not match', () => {
// dailyTask.repeat = {
// su: false,
// s: false,
// f: false,
// th: false,
// w: false,
// t: false,
// m: false,
// };
it('returns false in between X Day intervals', () => {
dailyTask.startDate = moment().subtract(1, 'days').toDate();
dailyTask.everyX = 2;
// for (let weekday of [0, 1, 2, 3, 4, 5, 6]) {
// day = moment().day(weekday).toDate();
expect(shouldDo(day, dailyTask, options)).to.equal(false);
});
// expect(shouldDo(day, dailyTask, options)).to.equal(false);
// }
// });
it('returns true on multiples of x', () => {
dailyTask.startDate = moment().subtract(7, 'days').toDate();
dailyTask.everyX = 7;
// it('leaves Daily inactive if day of the week does not match and active on the day it matches', () => {
// dailyTask.repeat = {
// su: false,
// s: false,
// f: false,
// th: true,
// w: false,
// t: false,
// m: false,
// };
expect(shouldDo(day, dailyTask, options)).to.equal(true);
// for (let weekday of [0, 1, 2, 3, 4, 5, 6]) {
// day = moment().add(1, 'weeks').day(weekday).toDate();
day = moment(day).add(7, 'days');
expect(shouldDo(day, dailyTask, options)).to.equal(true);
// if (weekday === 4) {
// expect(shouldDo(day, dailyTask, options)).to.equal(true);
// } else {
// expect(shouldDo(day, dailyTask, options)).to.equal(false);
// }
// }
// });
day = moment(day).add(7, 'days');
expect(shouldDo(day, dailyTask, options)).to.equal(true);
});
});
// it('activates Daily on matching days of the week', () => {
// expect(shouldDo(day, dailyTask, options)).to.equal(true);
// });
// });
context('Certain Days of the Week', () => {
beforeEach(() => {
dailyTask.frequency = 'weekly';
// context('Every X Weeks', () => {
// it('leaves daily inactive if it has not been the specified number of weeks', () => {
// dailyTask.everyX = 3;
// let tomorrow = moment().add(1, 'day').toDate();
dailyTask.repeat = {
su: true,
s: true,
f: true,
th: true,
w: true,
t: true,
m: true,
};
});
// expect(shouldDo(tomorrow, dailyTask, options)).to.equal(false);
// });
it('returns false if task does not have a repeat property', () => {
delete dailyTask.repeat;
// it('leaves daily inactive if on every (x) week on weekday it is incorrect weekday', () => {
// dailyTask.repeat = {
// su: false,
// s: false,
// f: false,
// th: false,
// w: false,
// t: false,
// m: false,
// };
expect(shouldDo(day, dailyTask, options)).to.equal(false);
});
// day = moment();
// dailyTask.repeat[DAY_MAPPING[day.day()]] = true;
// dailyTask.everyX = 3;
// let threeWeeksFromTodayPlusOne = day.add(1, 'day').add(3, 'weeks').toDate();
it('returns false if day of the week does not match', () => {
dailyTask.repeat = {
su: false,
s: false,
f: false,
th: false,
w: false,
t: false,
m: false,
};
// expect(shouldDo(threeWeeksFromTodayPlusOne, dailyTask, options)).to.equal(false);
// });
for (let weekday of [0, 1, 2, 3, 4, 5, 6]) {
day = moment().day(weekday).toDate();
// it('activates Daily on matching week', () => {
// dailyTask.everyX = 3;
// let threeWeeksFromToday = moment().add(3, 'weeks').toDate();
expect(shouldDo(day, dailyTask, options)).to.equal(false);
}
});
// expect(shouldDo(threeWeeksFromToday, dailyTask, options)).to.equal(true);
// });
it('returns false if day of the week does not match and active on the day it matches', () => {
dailyTask.repeat = {
su: false,
s: false,
f: false,
th: true,
w: false,
t: false,
m: false,
};
// it('activates Daily on every (x) week on weekday', () => {
// dailyTask.repeat = {
// su: false,
// s: false,
// f: false,
// th: false,
// w: false,
// t: false,
// m: false,
// };
for (let weekday of [0, 1, 2, 3, 4, 5, 6]) {
day = moment().add(1, 'weeks').day(weekday).toDate();
// day = moment();
// dailyTask.repeat[DAY_MAPPING[day.day()]] = true;
// dailyTask.everyX = 3;
// let threeWeeksFromToday = day.add(6, 'weeks').day(day.day()).toDate();
if (weekday === 4) {
expect(shouldDo(day, dailyTask, options)).to.equal(true);
} else {
expect(shouldDo(day, dailyTask, options)).to.equal(false);
}
}
});
// expect(shouldDo(threeWeeksFromToday, dailyTask, options)).to.equal(true);
// });
// });
it('returns true if Daily on matching days of the week', () => {
expect(shouldDo(day, dailyTask, options)).to.equal(true);
});
});
// context('Monthly - Every X Months on a specified date', () => {
// it('leaves daily inactive if not day of the month', () => {
// dailyTask.everyX = 1;
// dailyTask.frequency = 'monthly';
// dailyTask.daysOfMonth = [15];
// let tomorrow = moment().add(1, 'day').toDate();// @TODO: make sure this is not the 15
// expect(shouldDo(tomorrow, dailyTask, options)).to.equal(false);
// });
// it('activates Daily on matching day of month', () => {
// day = moment();
// dailyTask.everyX = 1;
// dailyTask.frequency = 'monthly';
// dailyTask.daysOfMonth = [day.date()];
// day = day.add(1, 'months').date(day.date()).toDate();
// expect(shouldDo(day, dailyTask, options)).to.equal(true);
// });
// it('leaves daily inactive if not on date of the x month', () => {
// dailyTask.everyX = 2;
// dailyTask.frequency = 'monthly';
// dailyTask.daysOfMonth = [15];
// let tomorrow = moment().add(2, 'months').add(1, 'day').toDate();
// expect(shouldDo(tomorrow, dailyTask, options)).to.equal(false);
// });
// it('activates Daily if on date of the x month', () => {
// dailyTask.everyX = 2;
// dailyTask.frequency = 'monthly';
// dailyTask.daysOfMonth = [15];
// day = moment().add(2, 'months').date(15).toDate();
// expect(shouldDo(day, dailyTask, options)).to.equal(true);
// });
// });
// context('Monthly - Certain days of the nth Week', () => {
// it('leaves daily inactive if not the correct week of the month on the day of the start date', () => {
// dailyTask.repeat = {
// su: false,
// s: false,
// f: false,
// th: false,
// w: false,
// t: false,
// m: false,
// };
// let today = moment('01/27/2017');
// let week = today.monthWeek();
// let dayOfWeek = today.day();
// dailyTask.startDate = today.toDate();
// dailyTask.weeksOfMonth = [week];
// dailyTask.repeat[DAY_MAPPING[dayOfWeek]] = true;
// dailyTask.everyX = 1;
// dailyTask.frequency = 'monthly';
// day = moment('02/23/2017');
// expect(shouldDo(day, dailyTask, options)).to.equal(false);
// });
// it('activates Daily if correct week of the month on the day of the start date', () => {
// dailyTask.repeat = {
// su: false,
// s: false,
// f: false,
// th: false,
// w: false,
// t: false,
// m: false,
// };
// let today = moment('01/27/2017');
// let week = today.monthWeek();
// let dayOfWeek = today.day();
// dailyTask.startDate = today.toDate();
// dailyTask.weeksOfMonth = [week];
// dailyTask.repeat[DAY_MAPPING[dayOfWeek]] = true;
// dailyTask.everyX = 1;
// dailyTask.frequency = 'monthly';
// day = moment('02/24/2017');
// expect(shouldDo(day, dailyTask, options)).to.equal(true);
// });
// it('leaves daily inactive if not day of the month with every x month on weekday', () => {
// dailyTask.repeat = {
// su: false,
// s: false,
// f: false,
// th: false,
// w: false,
// t: false,
// m: false,
// };
// let today = moment('01/26/2017');
// let week = today.monthWeek();
// let dayOfWeek = today.day();
// dailyTask.startDate = today.toDate();
// dailyTask.weeksOfMonth = [week];
// dailyTask.repeat[DAY_MAPPING[dayOfWeek]] = true;
// dailyTask.everyX = 2;
// dailyTask.frequency = 'monthly';
// day = moment('03/24/2017');
// expect(shouldDo(day, dailyTask, options)).to.equal(false);
// });
// it('activates Daily if on nth weekday of the x month', () => {
// dailyTask.repeat = {
// su: false,
// s: false,
// f: false,
// th: false,
// w: false,
// t: false,
// m: false,
// };
// let today = moment('01/27/2017');
// let week = today.monthWeek();
// let dayOfWeek = today.day();
// dailyTask.startDate = today.toDate();
// dailyTask.weeksOfMonth = [week];
// dailyTask.repeat[DAY_MAPPING[dayOfWeek]] = true;
// dailyTask.everyX = 2;
// dailyTask.frequency = 'monthly';
// day = moment('03/24/2017');
// expect(shouldDo(day, dailyTask, options)).to.equal(true);
// });
// });
// context('Every X Years', () => {
// it('leaves daily inactive if not the correct year', () => {
// day = moment();
// dailyTask.everyX = 2;
// dailyTask.frequency = 'yearly';
// day = day.add(1, 'day').toDate();
// expect(shouldDo(day, dailyTask, options)).to.equal(false);
// });
// it('activates Daily on matching year', () => {
// day = moment();
// dailyTask.everyX = 2;
// dailyTask.frequency = 'yearly';
// day = day.add(2, 'years').toDate();
// expect(shouldDo(day, dailyTask, options)).to.equal(true);
// });
// });
// });
// context('Every X Weeks', () => {
// it('leaves daily inactive if it has not been the specified number of weeks', () => {
// dailyTask.everyX = 3;
// let tomorrow = moment().add(1, 'day').toDate();
//
// expect(shouldDo(tomorrow, dailyTask, options)).to.equal(false);
// });
//
// it('leaves daily inactive if on every (x) week on weekday it is incorrect weekday', () => {
// dailyTask.repeat = {
// su: false,
// s: false,
// f: false,
// th: false,
// w: false,
// t: false,
// m: false,
// };
//
// day = moment();
// dailyTask.repeat[DAY_MAPPING[day.day()]] = true;
// dailyTask.everyX = 3;
// let threeWeeksFromTodayPlusOne = day.add(1, 'day').add(3, 'weeks').toDate();
//
// expect(shouldDo(threeWeeksFromTodayPlusOne, dailyTask, options)).to.equal(false);
// });
//
// it('activates Daily on matching week', () => {
// dailyTask.everyX = 3;
// let threeWeeksFromToday = moment().add(3, 'weeks').toDate();
//
// expect(shouldDo(threeWeeksFromToday, dailyTask, options)).to.equal(true);
// });
//
// it('activates Daily on every (x) week on weekday', () => {
// dailyTask.repeat = {
// su: false,
// s: false,
// f: false,
// th: false,
// w: false,
// t: false,
// m: false,
// };
//
// day = moment();
// dailyTask.repeat[DAY_MAPPING[day.day()]] = true;
// dailyTask.everyX = 3;
// let threeWeeksFromToday = day.add(6, 'weeks').day(day.day()).toDate();
//
// expect(shouldDo(threeWeeksFromToday, dailyTask, options)).to.equal(true);
// });
// });
//
// context('Monthly - Every X Months on a specified date', () => {
// it('leaves daily inactive if not day of the month', () => {
// dailyTask.everyX = 1;
// dailyTask.frequency = 'monthly';
// dailyTask.daysOfMonth = [15];
// let tomorrow = moment().add(1, 'day').toDate();// @TODO: make sure this is not the 15
//
// expect(shouldDo(tomorrow, dailyTask, options)).to.equal(false);
// });
//
// it('activates Daily on matching day of month', () => {
// day = moment();
// dailyTask.everyX = 1;
// dailyTask.frequency = 'monthly';
// dailyTask.daysOfMonth = [day.date()];
// day = day.add(1, 'months').date(day.date()).toDate();
//
// expect(shouldDo(day, dailyTask, options)).to.equal(true);
// });
//
// it('leaves daily inactive if not on date of the x month', () => {
// dailyTask.everyX = 2;
// dailyTask.frequency = 'monthly';
// dailyTask.daysOfMonth = [15];
// let tomorrow = moment().add(2, 'months').add(1, 'day').toDate();
//
// expect(shouldDo(tomorrow, dailyTask, options)).to.equal(false);
// });
//
// it('activates Daily if on date of the x month', () => {
// dailyTask.everyX = 2;
// dailyTask.frequency = 'monthly';
// dailyTask.daysOfMonth = [15];
// day = moment().add(2, 'months').date(15).toDate();
// expect(shouldDo(day, dailyTask, options)).to.equal(true);
// });
// });
//
// context('Monthly - Certain days of the nth Week', () => {
// it('leaves daily inactive if not the correct week of the month on the day of the start date', () => {
// dailyTask.repeat = {
// su: false,
// s: false,
// f: false,
// th: false,
// w: false,
// t: false,
// m: false,
// };
//
// let today = moment('01/27/2017');
// let week = today.monthWeek();
// let dayOfWeek = today.day();
// dailyTask.startDate = today.toDate();
// dailyTask.weeksOfMonth = [week];
// dailyTask.repeat[DAY_MAPPING[dayOfWeek]] = true;
// dailyTask.everyX = 1;
// dailyTask.frequency = 'monthly';
// day = moment('02/23/2017');
//
// expect(shouldDo(day, dailyTask, options)).to.equal(false);
// });
//
// it('activates Daily if correct week of the month on the day of the start date', () => {
// dailyTask.repeat = {
// su: false,
// s: false,
// f: false,
// th: false,
// w: false,
// t: false,
// m: false,
// };
//
// let today = moment('01/27/2017');
// let week = today.monthWeek();
// let dayOfWeek = today.day();
// dailyTask.startDate = today.toDate();
// dailyTask.weeksOfMonth = [week];
// dailyTask.repeat[DAY_MAPPING[dayOfWeek]] = true;
// dailyTask.everyX = 1;
// dailyTask.frequency = 'monthly';
// day = moment('02/24/2017');
//
// expect(shouldDo(day, dailyTask, options)).to.equal(true);
// });
//
// it('leaves daily inactive if not day of the month with every x month on weekday', () => {
// dailyTask.repeat = {
// su: false,
// s: false,
// f: false,
// th: false,
// w: false,
// t: false,
// m: false,
// };
//
// let today = moment('01/26/2017');
// let week = today.monthWeek();
// let dayOfWeek = today.day();
// dailyTask.startDate = today.toDate();
// dailyTask.weeksOfMonth = [week];
// dailyTask.repeat[DAY_MAPPING[dayOfWeek]] = true;
// dailyTask.everyX = 2;
// dailyTask.frequency = 'monthly';
//
// day = moment('03/24/2017');
//
// expect(shouldDo(day, dailyTask, options)).to.equal(false);
// });
//
// it('activates Daily if on nth weekday of the x month', () => {
// dailyTask.repeat = {
// su: false,
// s: false,
// f: false,
// th: false,
// w: false,
// t: false,
// m: false,
// };
//
// let today = moment('01/27/2017');
// let week = today.monthWeek();
// let dayOfWeek = today.day();
// dailyTask.startDate = today.toDate();
// dailyTask.weeksOfMonth = [week];
// dailyTask.repeat[DAY_MAPPING[dayOfWeek]] = true;
// dailyTask.everyX = 2;
// dailyTask.frequency = 'monthly';
//
// day = moment('03/24/2017');
//
// expect(shouldDo(day, dailyTask, options)).to.equal(true);
// });
// });
//
// context('Every X Years', () => {
// it('leaves daily inactive if not the correct year', () => {
// day = moment();
// dailyTask.everyX = 2;
// dailyTask.frequency = 'yearly';
// day = day.add(1, 'day').toDate();
//
// expect(shouldDo(day, dailyTask, options)).to.equal(false);
// });
//
// it('activates Daily on matching year', () => {
// day = moment();
// dailyTask.everyX = 2;
// dailyTask.frequency = 'yearly';
// day = day.add(2, 'years').toDate();
//
// expect(shouldDo(day, dailyTask, options)).to.equal(true);
// });
// });
});
+5 -4
View File
@@ -30,16 +30,17 @@ export function generateChallenge (options = {}) {
export function generateRes (options = {}) {
let defaultRes = {
render: sandbox.stub(),
send: sandbox.stub(),
status: sandbox.stub().returnsThis(),
sendStatus: sandbox.stub().returnsThis(),
json: sandbox.stub(),
locals: {
user: generateUser(options.localsUser),
group: generateGroup(options.localsGroup),
},
redirect: sandbox.stub(),
render: sandbox.stub(),
send: sandbox.stub(),
sendStatus: sandbox.stub().returnsThis(),
set: sandbox.stub(),
status: sandbox.stub().returnsThis(),
t (string) {
return i18n.t(string);
},
Executable → Regular
+2 -2
View File
@@ -16,7 +16,7 @@ nvm use
nvm alias default current
echo Update npm...
npm install -g npm@3
npm install -g npm@4
echo Installing global modules...
npm install -g gulp bower grunt-cli mocha
npm install -g gulp bower grunt-cli mocha node-pre-gyp
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

+2 -2
View File
@@ -1,9 +1,9 @@
/* Comment out for holiday events */
/* .npc_ian {
.npc_ian {
background: url("/npc_ian.gif") no-repeat;
width: 78px;
height: 135px;
} */
}
.quest_burnout {
background: url("/quest_burnout.gif") no-repeat;
+133 -181
View File
@@ -1,36 +1,36 @@
.promo_android {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1651px -296px;
background-position: -1715px -176px;
width: 175px;
height: 175px;
}
.promo_backgrounds_armoire_201602 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -845px -1041px;
background-position: -1573px -295px;
width: 141px;
height: 294px;
}
.promo_backgrounds_armoire_201603 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -987px -1041px;
background-position: -1573px 0px;
width: 141px;
height: 294px;
}
.promo_backgrounds_armoire_201604 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -141px -1041px;
background-position: -1150px -442px;
width: 140px;
height: 441px;
}
.promo_backgrounds_armoire_201605 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1265px -442px;
background-position: -1291px -442px;
width: 140px;
height: 441px;
}
.promo_backgrounds_armoire_201606 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -699px 0px;
background-position: -699px -148px;
width: 140px;
height: 447px;
}
@@ -42,49 +42,67 @@
}
.promo_backgrounds_armoire_201608 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -142px -600px;
background-position: -710px -600px;
width: 140px;
height: 439px;
}
.promo_backgrounds_armoire_201609 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -283px -600px;
background-position: -851px -600px;
width: 139px;
height: 438px;
}
.promo_backgrounds_armoire_201610 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1406px -442px;
background-position: -1432px 0px;
width: 140px;
height: 441px;
}
.promo_backgrounds_armoire_201611 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1124px 0px;
background-position: 0px -1042px;
width: 140px;
height: 441px;
}
.promo_backgrounds_armoire_201612 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1124px -442px;
background-position: -141px -1042px;
width: 140px;
height: 441px;
}
.promo_backgrounds_armoire_201701 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1265px 0px;
background-position: -282px -1042px;
width: 140px;
height: 441px;
}
.promo_backgrounds_armoire_201702 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -840px 0px;
background-position: -284px -600px;
width: 141px;
height: 441px;
}
.promo_backgrounds_armoire_201703 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -982px 0px;
background-position: -840px -148px;
width: 141px;
height: 441px;
}
.promo_backgrounds_armoire_201704 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -426px -600px;
width: 141px;
height: 441px;
}
.promo_backgrounds_armoire_201705 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: 0px -600px;
width: 141px;
height: 441px;
}
.promo_bees {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -142px -600px;
width: 141px;
height: 441px;
}
@@ -96,151 +114,163 @@
}
.promo_chairs_glasses {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1827px 0px;
background-position: -1821px -352px;
width: 51px;
height: 210px;
}
.promo_checkin_incentives {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -703px -1041px;
background-position: -991px -600px;
width: 141px;
height: 294px;
}
.promo_classes_fall_2014 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1066px -1336px;
background-position: -201px -1484px;
width: 321px;
height: 100px;
}
.promo_classes_fall_2015 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -423px -896px;
background-position: -703px -1338px;
width: 377px;
height: 99px;
}
.promo_classes_fall_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1547px -296px;
background-position: -1573px -590px;
width: 103px;
height: 348px;
}
.promo_coffee_mug {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1547px -645px;
background-position: 0px -1484px;
width: 200px;
height: 179px;
}
.promo_contrib_spotlight_Keith {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -181px -1735px;
background-position: -1715px -1536px;
width: 87px;
height: 111px;
}
.promo_contrib_spotlight_beffymaroo {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1406px -884px;
background-position: -1715px -626px;
width: 114px;
height: 147px;
}
.promo_contrib_spotlight_blade {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -91px -1735px;
background-position: -1715px -1424px;
width: 89px;
height: 111px;
}
.promo_contrib_spotlight_cantras {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -269px -1735px;
background-position: -1803px -1536px;
width: 87px;
height: 109px;
}
.promo_contrib_spotlight_dewines {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1573px -939px;
width: 89px;
height: 108px;
}
.promo_contrib_spotlight_megan {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -984px -748px;
background-position: -1715px -1200px;
width: 90px;
height: 111px;
}
.promo_contrib_spotlight_shanaqui {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: 0px -1735px;
background-position: -1715px -1312px;
width: 90px;
height: 111px;
}
.promo_cow {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: 0px -1041px;
background-position: -1432px -442px;
width: 140px;
height: 441px;
}
.promo_cupid_potions {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -564px -1041px;
background-position: -564px -1042px;
width: 138px;
height: 441px;
}
.promo_dilatoryDistress {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1879px -788px;
background-position: -1474px -1664px;
width: 90px;
height: 90px;
}
.promo_egg_mounts {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -423px -748px;
background-position: -703px -1190px;
width: 280px;
height: 147px;
}
.promo_enchanted_armoire {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: 0px -1483px;
background-position: -1081px -1338px;
width: 374px;
height: 76px;
}
.promo_enchanted_armoire_201507 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1547px -1378px;
background-position: -1341px -1042px;
width: 217px;
height: 90px;
}
.promo_enchanted_armoire_201508 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1270px -1224px;
background-position: -468px -1664px;
width: 180px;
height: 90px;
}
.promo_enchanted_armoire_201509 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1879px -970px;
background-position: -928px -1664px;
width: 90px;
height: 90px;
}
.promo_enchanted_armoire_201511 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -840px -442px;
background-position: -1715px -1018px;
width: 122px;
height: 90px;
}
.promo_enchanted_armoire_201601 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1879px -1516px;
background-position: -364px -1767px;
width: 90px;
height: 90px;
}
.promo_fairy_potions {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -982px -148px;
width: 141px;
height: 441px;
}
.promo_floral_potions {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1547px -1001px;
background-position: -1715px -352px;
width: 105px;
height: 273px;
}
.promo_ghost_potions {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -423px -1041px;
background-position: -1291px 0px;
width: 140px;
height: 441px;
}
.promo_habitica {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1547px -825px;
background-position: -1715px 0px;
width: 175px;
height: 175px;
}
@@ -252,217 +282,223 @@
}
.promo_habitoween_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1406px 0px;
background-position: -1150px 0px;
width: 140px;
height: 441px;
}
.promo_haunted_hair {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1124px -884px;
background-position: -1715px -774px;
width: 100px;
height: 137px;
}
.promo_holly_potions {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: 0px -600px;
background-position: -568px -600px;
width: 141px;
height: 440px;
}
.promo_item_notif {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1547px -1275px;
background-position: 0px -1664px;
width: 249px;
height: 102px;
}
.promo_jackalope {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1547px -148px;
background-position: -1264px -1190px;
width: 276px;
height: 147px;
}
.promo_more_checkin_incentives {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -699px 0px;
width: 450px;
height: 147px;
}
.promo_mystery_201405 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1879px -1334px;
background-position: 0px -1767px;
width: 90px;
height: 90px;
}
.promo_mystery_201406 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1879px -418px;
background-position: -1432px -884px;
width: 90px;
height: 96px;
}
.promo_mystery_201407 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1827px -412px;
background-position: -1821px -563px;
width: 42px;
height: 62px;
}
.promo_mystery_201408 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1765px -1378px;
background-position: -1821px -912px;
width: 60px;
height: 71px;
}
.promo_mystery_201409 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1879px -879px;
background-position: -182px -1767px;
width: 90px;
height: 90px;
}
.promo_mystery_201410 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -840px -533px;
background-position: -1806px -1200px;
width: 72px;
height: 63px;
}
.promo_mystery_201411 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1879px -606px;
background-position: -837px -1664px;
width: 90px;
height: 90px;
}
.promo_mystery_201412 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1827px -345px;
background-position: -1836px -1109px;
width: 42px;
height: 66px;
}
.promo_mystery_201501 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1827px -211px;
background-position: -1830px -708px;
width: 48px;
height: 63px;
}
.promo_mystery_201502 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1879px -1425px;
background-position: -91px -1767px;
width: 90px;
height: 90px;
}
.promo_mystery_201503 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1879px -1698px;
background-position: -1656px -1664px;
width: 90px;
height: 90px;
}
.promo_mystery_201504 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1469px -1132px;
background-position: -1806px -1312px;
width: 60px;
height: 69px;
}
.promo_mystery_201505 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1879px -1152px;
background-position: -1110px -1664px;
width: 90px;
height: 90px;
}
.promo_mystery_201506 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1827px -275px;
background-position: -1838px -1018px;
width: 42px;
height: 69px;
}
.promo_mystery_201507 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1879px 0px;
background-position: -1573px -1260px;
width: 90px;
height: 105px;
}
.promo_mystery_201508 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -477px -1644px;
background-position: -1291px -884px;
width: 93px;
height: 90px;
}
.promo_mystery_201509 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1879px -1607px;
background-position: -273px -1767px;
width: 90px;
height: 90px;
}
.promo_mystery_201510 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -383px -1644px;
background-position: -1150px -884px;
width: 93px;
height: 90px;
}
.promo_mystery_201511 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -571px -1644px;
background-position: -1019px -1664px;
width: 90px;
height: 90px;
}
.promo_mystery_201512 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1748px -730px;
background-position: -1830px -626px;
width: 60px;
height: 81px;
}
.promo_mystery_201601 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -699px -448px;
background-position: -1715px -1109px;
width: 120px;
height: 90px;
}
.promo_mystery_201602 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1879px -697px;
background-position: -1292px -1664px;
width: 90px;
height: 90px;
}
.promo_mystery_201603 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1879px -515px;
background-position: -1383px -1664px;
width: 90px;
height: 90px;
}
.promo_mystery_201604 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -289px -1644px;
background-position: -991px -895px;
width: 93px;
height: 90px;
}
.promo_mystery_201605 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1879px -1243px;
background-position: -1565px -1664px;
width: 90px;
height: 90px;
}
.promo_mystery_201606 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1879px -212px;
background-position: -1573px -1048px;
width: 90px;
height: 105px;
}
.promo_mystery_201607 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1879px -1061px;
background-position: -1747px -1664px;
width: 90px;
height: 90px;
}
.promo_mystery_201608 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1388px -1336px;
background-position: -649px -1664px;
width: 93px;
height: 90px;
}
.promo_mystery_201609 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1451px -1224px;
background-position: -743px -1664px;
width: 93px;
height: 90px;
}
.promo_mystery_201610 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1748px -645px;
background-position: -1816px -774px;
width: 63px;
height: 84px;
}
.promo_mystery_201611 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1879px -318px;
background-position: -1573px -1366px;
width: 90px;
height: 99px;
}
@@ -474,157 +510,73 @@
}
.promo_mystery_201701 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1879px -106px;
background-position: -1573px -1154px;
width: 90px;
height: 105px;
}
.promo_mystery_201702 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -704px -748px;
background-position: -984px -1190px;
width: 279px;
height: 147px;
}
.promo_mystery_201703 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1058px -1042px;
width: 282px;
height: 147px;
}
.promo_mystery_201704 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1201px -1664px;
width: 90px;
height: 90px;
}
.promo_mystery_3014 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1270px -1041px;
background-position: -250px -1664px;
width: 217px;
height: 90px;
}
.promo_new_hair_fall2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -282px -1041px;
background-position: -423px -1042px;
width: 140px;
height: 441px;
}
.promo_orca {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -982px -442px;
background-position: -1715px -912px;
width: 105px;
height: 105px;
}
.promo_partyhats {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -982px -548px;
background-position: -1432px -981px;
width: 115px;
height: 47px;
}
.promo_pastel_skin {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: 0px -1560px;
background-position: -523px -1484px;
width: 330px;
height: 83px;
}
.customize-option.promo_pastel_skin {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -25px -1575px;
background-position: -548px -1499px;
width: 60px;
height: 60px;
}
.promo_pastel_skin_hair {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -423px -600px;
background-position: -703px -1042px;
width: 354px;
height: 147px;
}
.customize-option.promo_pastel_skin_hair {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -448px -615px;
background-position: -728px -1057px;
width: 60px;
height: 60px;
}
.promo_peppermint_flame {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1651px -472px;
width: 140px;
height: 147px;
}
.promo_pet_skins {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1653px -1001px;
width: 140px;
height: 147px;
}
.customize-option.promo_pet_skins {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1678px -1016px;
width: 60px;
height: 60px;
}
.promo_pyromancer {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1653px -1149px;
width: 113px;
height: 113px;
}
.promo_rainbow_armor {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -357px -1735px;
width: 92px;
height: 103px;
}
.promo_seasonal_shop_fall_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1547px 0px;
width: 279px;
height: 147px;
}
.promo_shimmer_hair {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -331px -1560px;
width: 330px;
height: 83px;
}
.promo_splashyskins {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1270px -1132px;
width: 198px;
height: 91px;
}
.customize-option.promo_splashyskins {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1295px -1147px;
width: 60px;
height: 60px;
}
.promo_spooky_sparkles_fall_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1129px -1041px;
width: 140px;
height: 294px;
}
.promo_spring_classes_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -703px -1336px;
width: 362px;
height: 102px;
}
.promo_spring_classes_2017 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -778px -600px;
width: 309px;
height: 147px;
}
.promo_springclasses2014 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: 0px -1644px;
width: 288px;
height: 90px;
}
.promo_springclasses2015 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -801px -896px;
width: 288px;
height: 90px;
}
.promo_staff_spotlight_Lemoness {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1723px -825px;
width: 102px;
height: 146px;
}
.promo_staff_spotlight_paglias {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1265px -884px;
width: 99px;
height: 147px;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1006 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

+155 -35
View File
@@ -1,210 +1,330 @@
.promo_peppermint_flame {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1629px -934px;
width: 140px;
height: 147px;
}
.promo_pet_skins {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1488px -934px;
width: 140px;
height: 147px;
}
.customize-option.promo_pet_skins {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1513px -949px;
width: 60px;
height: 60px;
}
.promo_pyromancer {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1639px -632px;
width: 113px;
height: 113px;
}
.promo_rainbow_armor {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1358px -556px;
width: 92px;
height: 103px;
}
.promo_seasonal_shop_fall_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -301px -1207px;
width: 279px;
height: 147px;
}
.promo_shimmer_hair {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -581px -1207px;
width: 330px;
height: 83px;
}
.promo_shimmer_potions {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -934px 0px;
width: 141px;
height: 441px;
}
.promo_shinySeeds {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -792px 0px;
width: 141px;
height: 441px;
}
.promo_splashyskins {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1488px -1230px;
width: 198px;
height: 91px;
}
.customize-option.promo_splashyskins {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1513px -1245px;
width: 60px;
height: 60px;
}
.promo_spooky_sparkles_fall_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1217px -377px;
width: 140px;
height: 294px;
}
.promo_spring_classes_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -282px -1091px;
width: 362px;
height: 102px;
}
.promo_spring_classes_2017 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -899px -916px;
width: 309px;
height: 147px;
}
.promo_springclasses2014 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1488px -91px;
width: 288px;
height: 90px;
}
.promo_springclasses2015 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1488px 0px;
width: 288px;
height: 90px;
}
.promo_staff_spotlight_Lemoness {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1639px -330px;
width: 102px;
height: 146px;
}
.promo_staff_spotlight_Viirus {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -139px -1054px;
background-position: -1645px -182px;
width: 119px;
height: 147px;
}
.promo_staff_spotlight_paglias {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1639px -481px;
width: 99px;
height: 147px;
}
.promo_steampunk_3017 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -141px -440px;
background-position: -141px -765px;
width: 140px;
height: 441px;
}
.promo_summer_classes_2014 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -885px -591px;
background-position: -340px -220px;
width: 429px;
height: 102px;
}
.promo_summer_classes_2015 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -584px -882px;
background-position: 0px -1358px;
width: 300px;
height: 88px;
}
.promo_summer_classes_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -452px -145px;
background-position: -282px -765px;
width: 400px;
height: 150px;
}
.promo_takeThis_gear {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -885px -882px;
background-position: -1639px -783px;
width: 114px;
height: 87px;
}
.promo_takethis_armor {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1000px -882px;
background-position: -1358px -377px;
width: 114px;
height: 87px;
}
.promo_task_planning {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -885px -96px;
background-position: -1217px -181px;
width: 240px;
height: 195px;
}
.promo_turkey_day_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -282px -440px;
background-position: -1076px 0px;
width: 140px;
height: 441px;
}
.promo_unconventional_armor {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1315px -591px;
background-position: -1639px -871px;
width: 60px;
height: 60px;
}
.promo_unconventional_armor2 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1283px -694px;
background-position: -1687px -1230px;
width: 70px;
height: 74px;
}
.promo_updos {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1195px -292px;
background-position: -1488px -182px;
width: 156px;
height: 147px;
}
.promo_veteran_pets {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -576px -796px;
background-position: -1627px -1082px;
width: 146px;
height: 75px;
}
.promo_winter_classes_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -223px -882px;
background-position: -645px -1091px;
width: 360px;
height: 90px;
}
.promo_winter_classes_2017 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -452px 0px;
background-position: 0px -620px;
width: 432px;
height: 144px;
}
.promo_winter_fireworks {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: 0px -1054px;
background-position: -1488px -1082px;
width: 138px;
height: 147px;
}
.promo_winterclasses2015 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -452px -296px;
background-position: -433px -620px;
width: 325px;
height: 110px;
}
.promo_wintery_skins {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: 0px -440px;
background-position: 0px -765px;
width: 140px;
height: 441px;
}
.customize-option.promo_wintery_skins {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -25px -455px;
background-position: -25px -780px;
width: 60px;
height: 60px;
}
.promo_winteryhair {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -423px -796px;
background-position: -1488px -1322px;
width: 152px;
height: 75px;
}
.avatar_variety {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -885px 0px;
background-position: -683px -765px;
width: 498px;
height: 95px;
}
.npc_viirus {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -259px -1054px;
background-position: -1358px -465px;
width: 108px;
height: 90px;
}
.party_preview {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: 0px 0px;
background-position: -340px 0px;
width: 451px;
height: 219px;
}
.promo_backtoschool {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1132px -694px;
background-position: -1488px -632px;
width: 150px;
height: 150px;
}
.promo_cooking {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: 0px -220px;
background-position: -328px -343px;
width: 396px;
height: 219px;
}
.promo_startingover {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -730px -440px;
background-position: -1488px -783px;
width: 150px;
height: 150px;
}
.promo_valentines {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -885px -292px;
background-position: -589px -916px;
width: 309px;
height: 147px;
}
.promo_working_out {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -885px -440px;
background-position: 0px -1207px;
width: 300px;
height: 150px;
}
.scene_coding {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -694px -615px;
background-position: -1488px -481px;
width: 150px;
height: 150px;
}
.scene_dailies {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: 0px -343px;
width: 327px;
height: 276px;
}
.scene_eco_friendly {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: 0px -882px;
background-position: -1217px -1004px;
width: 222px;
height: 171px;
}
.scene_habits {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -423px -440px;
background-position: -282px -916px;
width: 306px;
height: 174px;
}
.scene_phone_peek {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1186px -440px;
background-position: -1488px -330px;
width: 150px;
height: 150px;
}
.scene_video_games {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: 0px 0px;
width: 339px;
height: 342px;
}
.welcome_basic_avatars {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -885px -694px;
background-position: -1217px -838px;
width: 246px;
height: 165px;
}
.welcome_promo_party {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -423px -615px;
background-position: -1217px 0px;
width: 270px;
height: 180px;
}
.welcome_sample_tasks {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1126px -96px;
background-position: -1217px -672px;
width: 246px;
height: 165px;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 KiB

After

Width:  |  Height:  |  Size: 340 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 478 KiB

After

Width:  |  Height:  |  Size: 507 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 72 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 141 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 138 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 158 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

After

Width:  |  Height:  |  Size: 146 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 154 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 177 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

After

Width:  |  Height:  |  Size: 159 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 122 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 67 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 57 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 110 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 153 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 143 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 KiB

After

Width:  |  Height:  |  Size: 195 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 332 KiB

After

Width:  |  Height:  |  Size: 440 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

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