Compare commits

...

368 Commits

Author SHA1 Message Date
Sabe Jones 4a78514308 3.94.1 2017-06-04 23:41:14 +00:00
Sabe Jones 265b48752d Merge branch 'develop' into release 2017-06-04 23:40:26 +00:00
Sabe Jones db0b0d6b6d fix(migrations): return to generic state 2017-06-04 02:59:49 +00:00
Sabe Jones 20f1087552 fix(migrations): return to generic state 2017-06-04 02:58:42 +00:00
Keith Holliday 2e9bc2c31c New client guilds (#8736)
* add colors palette

* add secondary menu component and style it

* add box shadow to secondary menu

* misc css, fixes for secondary menu

* client: add equipment page with grouping, css: add some styles

* add typography

* more equipment

* stable: fix linting

* equipment: add styles (lots of general styles too)

* remove duplicate google fonts loading

* add dropdowns

* design: white search input background, remove gray from items

* start adding drawer and selected indicator

* wip equipment

* fix equipment

* equipment: correctly bind new properties on items.gear.equipped

* equipment: fix vue binding. version 2

* equipment: fix vue binding. version 3

* back to first fix for equip op, fix for sourcemaps, send http request when an item is equipped, load bootstrap-vue components where needed

* checkboxes and radio buttons

* correctly renders selected items in first postion during the first render

* add search

* general changes, constants part of app state, add popovers

* add toggle switch, rename css

* correct offset

* upgrade deps

* upgrade deps

* drawer and lot of other work

* update equipping mechanism

* finish equipment

* fix compilation and upgrade deps

* use v-show in place of v-if to fix ui issues

* v-show -> v-if

* Start of guild syyles

* fix linting in test/client

* fix es6 compilation in test/client

* fix babel compilation for tests

* fix groupsUtilities mixin tests

* More designs

* Added public guild state

* Added my guilds store

* client: buttons

* client: buttons: fix colors

* Added join and leave

* Began adding new guild form

* Create form updates

* Added search to local data

* Added filtering

* Added initial code for group create

* Added more create checks

* Added more guild routes

* Added styles to guild page

* Added more chat styles

* Began porting over angular functions

* Moved over group service functions

* Added paging

* Updated sidebar

* Updated join/leave and minor text

* Added new sidebar functions

* Updated paging

* Added some form updates

* Added more translations and styles

* Updated shrinkwrap

* Removed features config

* Lint cleanup

* Added member modal

* Added more member actions

* Updated nav

* Fixed filter toggling

* Updated create guild

* Added no guild page

* Added sort select

* Added more styles

* Added update guild form

* Removed extra css and other minor changes

* Many css and syntax fixes

* Fixed color and merge conflic

* Removed paging from my guilds

* Removed extra strings

* Many requests updates

* Small style fixes
2017-06-02 14:55:02 -06:00
SabreCat b606dd1c40 fix(strings): remove extraneous title text 2017-06-02 16:19:42 +00:00
SabreCat db1c2fd5a2 fix(shops): don't push if empty
Also corrects text on hatching potions
2017-06-02 15:28:28 +00:00
Sabe Jones de1e477ce2 Merge branch 'release' into develop 2017-06-02 00:49:00 +00:00
Sabe Jones 67318177a2 3.94.0 2017-06-01 23:18:07 +00:00
Sabe Jones cd1be828ca chore(i18n): update locales 2017-06-01 23:17:32 +00:00
SabreCat 9ffebc10a7 chore(sprites): compile 2017-06-01 23:09:05 +00:00
SabreCat 5cd11ed343 feat(content): Armoire and BGs 2017-06-01 23:08:00 +00:00
SabreCat 9de118f0d9 Merge branch 'release' into develop 2017-05-30 21:00:53 +00:00
SabreCat 5fbec4069e 3.93.2 2017-05-30 20:59:24 +00:00
SabreCat a0f10cbf4b Merge branch 'THI/sleep-dailies-fix' into release 2017-05-30 20:58:33 +00:00
Keith Holliday 0e069e78d5 Set isDue and NextDue during sleep (#8769) 2017-05-30 15:57:38 -05:00
SabreCat dea847ba1a chore(news): Bailey 2017-05-30 20:11:21 +00:00
Sabe Jones 46ed1813c6 Optional feedback on account deletion (#8750)
* Fixed rebase.

* Removed commented out mail sending to pass linting. Styles from settings.styl still not propagating to app.css

* fix(feedback): address PR comments

* fix(style): linting errors
2017-05-30 11:54:42 -05:00
Keith Holliday e9750353a7 Set isDue and NextDue during sleep 2017-05-30 09:12:09 -06:00
Sabe Jones 05ea2c1ce6 Merge branch 'release' into develop 2017-05-29 01:11:00 +00:00
Sabe Jones 74d1c7763e 3.93.1 2017-05-29 01:08:31 +00:00
Keith Holliday 6f034bb5dd Fixed issue when repeat object is malformed (#8765)
* Fixed issue when repeat object is malformed

* Removed only

* Changed numeric check to lodash isFinite

* Removed newer lodash function
2017-05-28 20:07:29 -05:00
Alys aeb8d4f500 fix typo in mageText: Habit > Habitica (thanks to Janmetdepet for finding it) (#8748) 2017-05-27 18:56:10 +10:00
Keith Holliday 58d910fe62 Added fix for double click on amazon pay (#8708) 2017-05-25 16:10:59 -05:00
SabreCat 71f2f31606 Merge branch 'release' into develop 2017-05-25 02:51:10 +00:00
SabreCat feae40cf0a 3.93.0 2017-05-25 01:00:21 +00:00
SabreCat 0d3fe53155 chore(news): Bailey 2017-05-25 00:59:37 +00:00
SabreCat c3a3c1514a Merge branch 'monthlies' into release 2017-05-25 00:50:19 +00:00
Keith Holliday cc532fa993 Enabled repeatables (#8572)
* Enabled repeatables

* Added every x to weekly

* Updated new recur logic to work with tests

* Added repeatable tests back

* Added custom day start support

* Moved back to zone function

* Added zone back

* Added nextDue field

* Abstracted set next due logic, set offset, and mapped to ISO

* Removed extra codes

* Removed clone deep

* Added summary local

* Fixed every x weekly

* Prevented edit of repeats on

* Added next due date

* Fixed display of next due dates

* Fixed broken tests

* added next due date as today for weekly

* Fixed integration tests

* Updated common test

* Use user's format

* Allow user to deselect all days during week

* Removed let from front end
2017-05-24 19:49:33 -05:00
Kevin Smith ba66a1c098 Removed cancel sub button and added info for apple/google subs (Fixes #8642) (#8666)
* Removed cancel sub button and added info for apple/google subs

* Refactored logic and constants from subscriptions view
2017-05-24 10:13:44 -06:00
Josh Holland b4d5c634b3 Close dropdowns when user clicks outside of them (fixes #5490) (#8657)
* Close dropdowns when user clicks outside of them

Fixes #5490

* Remove expandMenu and closeMenu directives and tests

* Remove unnecessary HTML attributes
2017-05-24 10:12:38 -06:00
Sabe Jones 216006beab Merge branch 'release' into develop 2017-05-23 22:16:32 +00:00
Sabe Jones 7c236e7e0e 3.92.0 2017-05-23 22:15:40 +00:00
Sabe Jones 0221d2d7f9 chore(i18n): update locales 2017-05-23 22:12:33 +00:00
SabreCat 567eb1d98b chore(sprites): compile 2017-05-23 22:04:26 +00:00
SabreCat 3cf533f261 feat(content): new Mystery Items
and reenable Floral Potions
2017-05-23 22:03:31 +00:00
taldin c30c51f386 Fixes apidoc error with Cast Skill (#8709)
* Fixes apidoc error with Cast Skill

Changes Body to Query, changed example from  POST body

* Updated to remove trailing space

* Wording fix per Lady Alys

* Update user.js

Kicking off another test.

* Update user.js
2017-05-23 14:06:58 -06:00
Céline O'Neil 2de794c32b Show task alias advanced option for Rewards (#8705) 2017-05-23 13:58:38 -06:00
Keith Holliday 7d000d2cf6 Removed let from front end 2017-05-23 08:56:36 -06:00
Keith Holliday 280b720c13 Allow user to deselect all days during week 2017-05-22 08:37:45 -06:00
Matteo Pagliazzi 9e1f7f3811 Client/Inventory/Items (#8734)
* client: start working on Inventory/Items

* i18n changes and fixes

* initial displaying of eggs, food and potions + sorting

* add missing files

* remove comment

* show food, eggs and potions

* add label to dropdowns acting as select menus

* popovers

* move badge to slot and component if necessary, general refactor

* fix quantity ordering

* some special items, reorganize
2017-05-22 16:30:52 +02:00
Keith Holliday fefaed368a Use user's format 2017-05-22 08:16:12 -06:00
Keith Holliday 6f370395ea Updated common test 2017-05-22 08:06:14 -06:00
Keith Holliday 65566f7607 Fixed integration tests 2017-05-22 07:54:07 -06:00
Keith Holliday c0117706e4 Merge remote-tracking branch 'upstream/develop' into enable-repeatables 2017-05-22 07:49:01 -06:00
Matteo Pagliazzi f267456a30 client: fix drawer 2017-05-21 15:38:33 +02:00
Matteo Pagliazzi 228b724d52 fix missing trailing comma 2017-05-21 14:43:25 +02:00
Keith Holliday 19b09b4894 Merged in develop 2017-05-20 19:24:25 -06:00
Keith Holliday f49d21d7b4 added next due date as today for weekly 2017-05-20 18:38:53 -06:00
Keith Holliday c08c0685f3 Fixed broken tests 2017-05-20 17:19:35 -06:00
negue 59bfe66c94 Client/item content (#8738)
* extract item popover-content as component

# Conflicts:
#	website/client/components/inventory/item.vue

* extract item-content as slot

* scoped context to pass the item

* itemContentClass instead of itemContent-slot
2017-05-20 19:59:45 +02:00
SabreCat 023fd6e6b0 3.91.2 2017-05-19 20:51:10 +00:00
SabreCat 7ee2f90f37 fix(docs): move apiParamExamples to newlines 2017-05-19 20:49:59 +00:00
SabreCat 46709ddadd 3.91.1 2017-05-19 20:22:06 +00:00
SabreCat a3ee09e764 chore(event): disable Fairy Potions 2017-05-19 20:17:51 +00:00
Sabe Jones 4127f36c02 chore(i18n): update locales 2017-05-19 20:16:18 +00:00
SabreCat dad924ac7d chore(sprites): compile 2017-05-19 20:05:55 +00:00
Sabe Jones 547c87dee7 Guild A/B test and Achievement (#8740)
* WIP(guilds): AB test pester modal

* WIP(AB-test): guild pester cont'd

* fix(style): linting error

* fix(AB-test): markModified and notif enum

* fix(tests): update AB expectations

* fix(modal): remove extra includes

* feat(achievements): add Joined Guild cheevo
Also removes unused achievement sprites, and properly saves counter used in A/B testing

* fix(style): linting error from conflict
2017-05-19 14:45:11 -05:00
Keith Holliday 99a2013767 Fixed display of next due dates 2017-05-18 14:12:36 -06:00
Keith Holliday a5e0e171cc Added next due date 2017-05-18 10:42:11 -06:00
Keith Holliday 1d93943458 Prevented edit of repeats on 2017-05-18 10:26:57 -06:00
Keith Holliday 7f2719a75c Fixed every x weekly 2017-05-18 10:11:29 -06:00
Sabe Jones 8a9ed04f5e Merge branch 'release' into develop 2017-05-18 02:13:14 +00:00
Sabe Jones 4e66c038a6 3.91.0 2017-05-18 02:11:56 +00:00
Sabe Jones bd9618e1c6 chore(i18n): update locales 2017-05-18 02:08:40 +00:00
SabreCat 342034a80d chore(sprites): compile 2017-05-18 01:59:44 +00:00
SabreCat f2aed3cb27 chore(news): Bailey
Also rolls back a leftover bit of the Spring Fling.
2017-05-18 01:58:37 +00:00
SabreCat 4f5f15d162 fix(test): account for cumulative balance 2017-05-18 01:44:07 +00:00
SabreCat b5c1b78789 docs(content): comment bundle structure 2017-05-18 01:44:07 +00:00
SabreCat f2ace5bb63 test(bundles): check balance deduction 2017-05-18 01:44:07 +00:00
SabreCat 72b8ba2d5c fix(test): remove only 2017-05-18 01:44:07 +00:00
SabreCat 8555e4bea8 test(bundles): shop and purchase tests 2017-05-18 01:44:07 +00:00
SabreCat 4de5b4253b fix(style): address linting errors 2017-05-18 01:44:07 +00:00
SabreCat 410004f8c6 feat(purchases): sell bundled quests 2017-05-18 01:44:07 +00:00
SabreCat 76c7e15497 refactor(content): split quests file 2017-05-18 01:44:06 +00:00
Sabe Jones e6f605f23a Discount Bundled Quests (#8731)
* refactor(content): split quests file

* feat(purchases): sell bundled quests

* fix(style): address linting errors

* test(bundles): shop and purchase tests

* fix(test): remove only

* test(bundles): check balance deduction

* docs(content): comment bundle structure

* fix(test): account for cumulative balance
2017-05-17 20:36:34 -05:00
Matteo Pagliazzi 0af1203832 Client Redesign: Inventory pages, secondary menu, misc css and design items (#8631)
* add colors palette

* add secondary menu component and style it

* add box shadow to secondary menu

* misc css, fixes for secondary menu

* client: add equipment page with grouping, css: add some styles

* add typography

* more equipment

* stable: fix linting

* equipment: add styles (lots of general styles too)

* remove duplicate google fonts loading

* add dropdowns

* design: white search input background, remove gray from items

* start adding drawer and selected indicator

* wip equipment

* fix equipment

* equipment: correctly bind new properties on items.gear.equipped

* equipment: fix vue binding. version 2

* equipment: fix vue binding. version 3

* back to first fix for equip op, fix for sourcemaps, send http request when an item is equipped, load bootstrap-vue components where needed

* checkboxes and radio buttons

* correctly renders selected items in first postion during the first render

* add search

* general changes, constants part of app state, add popovers

* add toggle switch, rename css

* correct offset

* upgrade deps

* upgrade deps

* drawer and lot of other work

* update equipping mechanism

* finish equipment

* fix compilation and upgrade deps

* use v-show in place of v-if to fix ui issues

* v-show -> v-if

* fix linting in test/client

* fix es6 compilation in test/client

* fix babel compilation for tests

* fix groupsUtilities mixin tests

* client: buttons

* client: buttons: fix colors

* client: finish buttons and dropdowns

* upgrade bootstrap-vue, finish buttons and dropdowns

* fix tasks page layout

* misc fixes for buttons

* add textareas

* fix app menu

* add inputs

* fixes for toggleSwitch

* typography

* checkboxes and radio buttons

* add checkbox icon

* fix equip.js

* extract strings to newClient.json

* add Popover above 'Use Costume' / 'Auto Equip' slider - disable item select if costume-mode and 'useCostume' isn't active

* show "you have disabled your costume" error above the drawer items

* check errorMessage for null

* hide star if costume not enabled

* fix errorMessage (!errorMessage seems not to work for string)

* show minimize / expand icon - always centered by css

* drawer test

* drawer: fix centering on large screens

* fix show more button

* add margin when two dropdowns are next to each other

* adjust the page padding based on the drawer, misc fixes

* drawer fixes
2017-05-16 21:09:55 +02:00
Alys 1de379a2c3 adjust banned words. TRIGGER / CONTENT WARNING: assault, slurs, swearwords, etc 2017-05-16 13:08:09 +10:00
Keith Holliday 388861b503 Added summary local 2017-05-15 09:15:14 -06:00
Alys f52806ed69 minor text changes: Shield-Hand Item; space in quest completion modal; Rebirth clarification (#8681)
* replace Shield with Shield-Hand Item where appropriate for consistency and clarity

* add space in quest completion modal heading between name of quest and "Completed!"

* clarify Rebirth text - restart your current character, not create a new one

* correct the instructions for editing a group (edit button not pencil icon)

* correct typo in questVice3Completion: breath > breathe

* fix type: Starststemic > Starsystemic
2017-05-14 20:53:15 +10:00
Alys d89b9e08af update / delete outdated README.md files (#8719)
Information from test/api/README.md and test/api/v3/README.md has been moved to http://habitica.wikia.com/wiki/Using_Your_Local_Install_to_Modify_Habitica's_Website_and_API for consistency with other Blacksmith documentation.
2017-05-14 14:40:13 +10:00
Keith Holliday f8a99bd127 Removed clone deep 2017-05-12 11:15:09 -06:00
Keith Holliday a82b60f144 Removed extra codes 2017-05-12 09:21:29 -06:00
Alys 65e71140ee adjust banned words list -- TRIGGER / CONTENT WARNING: assault, slurs, swearwords, etc 2017-05-13 00:48:35 +10:00
Keith Holliday f192ca4c6f Abstracted set next due logic, set offset, and mapped to ISO 2017-05-12 07:39:32 -06:00
SabreCat 36b09d40b9 Merge branch 'release' into develop 2017-05-11 22:30:23 +00:00
Sabe Jones 4fdd754b31 3.90.2 2017-05-11 20:16:28 +00:00
Sabe Jones 36ff063991 chore(i18n): update locales 2017-05-11 20:14:21 +00:00
SabreCat 20dcb1cb8a chore(sprites): compile 2017-05-11 20:08:33 +00:00
SabreCat 3a1527073f chore(news): Bailey 2017-05-11 20:08:12 +00:00
Keith Holliday 1292f9a3d5 Added nextDue field 2017-05-11 13:11:16 -06:00
Keith Holliday e7418472f6 Added zone back 2017-05-10 22:06:31 -06:00
Keith Holliday 850f332ddc MErged in develop 2017-05-10 15:51:50 -06:00
Keith Holliday 59fb32ea2e Moved back to zone function 2017-05-10 15:47:32 -06:00
Keith Holliday 727cdc9402 Tasks is due (#8711)
* Added isDue field and isDue set on create

* Added isDue update on update task

* Add isdue calc to score task

* Added isdue calc to cron

* Fixed lint issue

* Added isDue to no set and updated grammar
2017-05-10 07:40:45 -06:00
SabreCat 638c9dee89 Merge branch 'release' into develop 2017-05-09 19:36:42 +00:00
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
yugensoft 2adac35e31 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
2017-05-09 13:22:09 -05: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
SabreCat 15d4f7d6ab Revert "Add tests for shouldDo - fixes #8585 (#8660)"
This reverts commit 4bbebdd237. The change to moment.utcOffset() from moment.zone() broke cron code for determining today's date.
2017-05-08 22:08:24 +00:00
Keith Holliday 76222ac344 Added custom day start support 2017-05-08 10:20:47 -06:00
Keith Holliday 2659a4117b Added repeatable tests back 2017-05-08 09:52:15 -06:00
Keith Holliday a0ee73e944 Updated new recur logic to work with tests 2017-05-08 09:39:50 -06:00
Keith Holliday 6174624b89 Merged develop 2017-05-08 08:32:03 -06:00
Keith Holliday c8b6e8ea7c Updated docker file to use Node (#8704) 2017-05-08 07:40:55 -06:00
madpink 409b5d5965 Updating User API Doc (part 2) (#8602)
* Updating APIDOC for issue 8087

* Updating User API Doc (round 2)

cleaned up trailing sapces

* Updating User API Doc (round 2)

Changed mpHeal to mpheal
2017-05-08 07:37:01 -06:00
MathWhiz e7209511ca Challenge API Doc Updates (#8626)
* Improve API Documentation for Challenges API

* Fix previously raised issues

* Change suggestions by @Alys
2017-05-08 07:36:02 -06:00
Mateus Etto 8c76ccd39b Fix bugs with approved tasks in a Group Plan (#8629)
* Store all approved tasks in an array

* Created bulkScore using score callback

* Removed unnecessary code

* Added verification to run the code only for Approved Tasks

* Created scoreTasks on server and necessary code on client

* Revert "Created scoreTasks on server and necessary code on client"

This reverts commit b786c0e71a.

* Fixed gold/xp earn-lose-earnAgain problem

* Do not read already read notifications

* Removed unnecessary variable
2017-05-08 07:35:08 -06:00
Jaka Kranjc 399c91ccab travis: don't pollute the log with npm output (#8706)
unless it fails; saves ~7k lines
2017-05-08 07:33:49 -06:00
Grace Chen 4bbebdd237 Add tests for shouldDo - fixes #8585 (#8660)
* Fix #8585 - Add shouldDo CDS and timezone variation test suite

* Fix #8585 - resolve feedback
2017-05-08 07:32:38 -06: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
TheHollidayInn 0fd85c0d60 Added every x to weekly 2017-03-27 09:52:32 -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
SabreCat 01af658c2d 3.80.1 2017-03-16 19:43:18 +00:00
SabreCat 52738575fa chore(news): Bailey 2017-03-16 19:43:08 +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
Keith Holliday 8c68f450c6 Enabled repeatables 2017-03-15 14:13:15 -06:00
Blade Barringer 164177f010 Enable shouldDo tests 2017-03-14 20:59:27 -05:00
SabreCat fc69a3a960 3.80.0 2017-03-15 00:49:36 +00:00
SabreCat 797adbb1dc chore(news): Bailey 2017-03-15 00:17:10 +00:00
Keith Holliday 3dc20a9832 Ensured upgraded groups are charge correctly (#8567)
* Ensured upgraded groups are charge correctly

* Added names to magic numbers

* Limited group fields query
2017-03-14 18:09:12 -05:00
Sabe Jones cfa433aa75 chore(i18n): update locales 2017-03-14 21:03:46 +00:00
SabreCat 9bc2d22d30 chore(sprites): compile 2017-03-14 20:54:27 +00:00
Sabe Jones 4909f67ded feat(content): Pet Quest and Future Mystery Sets (#8569) 2017-03-14 14:31:10 -05:00
Matteo Pagliazzi 8bc6534ff5 Merge pull request #8558 from HabitRPG/fix-achievements
Attempt to fix achievements awarding
2017-03-14 13:11:04 +01:00
Clint Ryan 83bd4dcf60 Add failing test, seeking help/advice 2017-03-14 12:35:48 +11:00
Matteo Pagliazzi a8ebd04ac8 fix typo in if condition and write test 2017-03-13 21:43:40 +01:00
Matteo Pagliazzi e0d499abab expand explanation 2017-03-13 21:18:22 +01:00
Matteo Pagliazzi f67e065de2 attempt to fix achievements awarding 2017-03-13 21:18:22 +01:00
Matteo Pagliazzi 283403d6c8 Merge pull request #8563 from Alys/20170312-canceled-group-plan
use correct wording and date format for cancelled Group Plan in Payment Details screen
2017-03-13 21:16:13 +01:00
Matteo Pagliazzi 55c7d6a191 Merge pull request #8542 from HabitRPG/client/guilds-infinite-loading
Client: Guilds: infinite loading
2017-03-13 21:15:06 +01:00
Alys a5011f000e add new feature descriptions to Social > Group Plans page (#8564)
* add new features to Social > Group Plans page and remove windows line breaks

* add price to Social > Group Plans page and remove windows line breaks
2017-03-13 21:14:14 +01:00
Matteo Pagliazzi c5633e2074 client redesign: add infinite loading for guilds + misc fixes 2017-03-13 19:47:09 +01:00
Matteo Pagliazzi 939712ad1f api: add pagination for guilds
start adding apiMessages

add apiMessages lib with tests

use apiMessage and fix tests

fix content tests

guilds pagination: add api docs

guilds pagination: improve api docs
2017-03-13 19:46:53 +01:00
Alys 09490551d4 correct wording and date format for cancelled Group Plan in Payment Details screen 2017-03-12 18:42:34 +10:00
Clint Ryan 92b02295b5 Issue 8432: Pre-test for initial fix 2017-03-11 23:29:51 +11:00
Matteo Pagliazzi 767763fbf6 upgrade hello.js to use Facebook API v2.7 2017-03-11 12:25:38 +01:00
SabreCat 237e0df611 3.79.2 2017-03-11 00:20:04 +00:00
Sabe Jones ca903f0dc3 Group migration fixes (#8555)
* fix(migration): better subs handling

* feat(migration): award jackalopes

* fix(lint): no console logs
2017-03-10 18:18:02 -06:00
Keith Holliday bde41699ee Reverted should do file (#8556) 2017-03-10 17:37:52 -06:00
Keith Holliday d0d4b47c47 Fixed purchased check (#8552) 2017-03-10 17:27:30 -06:00
Sabe Jones c616346233 3.79.1 2017-03-10 17:05:21 +00:00
Sabe Jones 311d3a256a Moment-recur fix (#8553)
* fix(npm): no pre/post for moment-recur

* chore(npm): update shrinkwrap
2017-03-10 11:01:51 -06:00
SabreCat af6f3f9656 chore(npm): update shrinkwrap 2017-03-10 15:38:01 +00:00
Sabe Jones 15681fedcc Merge branch 'release' into develop 2017-03-10 04:30:45 +00:00
Sabe Jones bf12f8aa71 v3.79.0 Release (#8551)
* Habits v2: adding counter to habits (cleaned up branch) - fixes #8113 (#8198)

* Clean version of PR 8175

The original PR for this was here:
https://github.com/HabitRPG/habitica/pull/8175

Unfortunately while fixing a conflict in tasks.json, I messed up the rebase and wound up pulling in too many commits and making a giant mess. Sorry. :P

* Fixing test failure

This test seems to occasionally start failing (another coder reported the same thing happening to them in the blacksmiths’ guild) because the order in which the tasks are created can sometimes not match the order in the array. So I have sorted the tasks array after creation by the task name to ensure a consistent ordering, and slightly reordered the expect statements to match.

* Debounce `$scope` updates when typing in chat. (#8485)

Fixes #6462, by saving a bunch of time per frame. See the issue for evidence of
the win.

* fix(nextRewardUnlocksIn): Check-in prize message was always plural --… (#8458)

* fix(nextRewardUnlocksIn): Check-in prize message was always plural -- moving to a non-sentence like structure to fix incorrect grammar.

* fix(countLeft): Check-in prize message was always plural -- moving to a non-sentence like structure to fix incorrect grammar.

* Added support for grouping tasks by challenge (#8469)

* Added support for grouping tasks by chllenge

* Fixed tests and updated default challenge model name

* Fixed broken member test

* Updated setting string

* Changed to shortName

* Began abstracting task grouping

* Added initial task directive code

* Added new directives to help with grouping of tasks

* Removed random console.log

* Leaving a group (#8517)

* Leaving a group or a guild no longer removes the user from the challenges of that group or guild.

* Updating api docs for leaving group to take into account the default path no longer leaving challenges when leaving a group.

* Updating api docs for leaving group to take into account the default path no longer leaving challenges when leaving a group.

* refactored according to blade's comments to not be a breaking change. The api now accepts a body parameter to specify wether the user
should remain in the groups challenges or leave them. The change also adds more tests around this behavior to confirm that it works
as expected.

* Tasks score notes (#8507)

* Added setting and modal for score notes

* Added persistent score notes

* Fixed linting issues and documented new field

* Added max length to task score notes

* Added check for score notes existence

* Combined tasks perferences

* Repeatables (#8444)

* Added initial should do weekly tests

* Added support back in for days of the week and every x day

* Added better week day mapper

* Added initial monthly

* Added every x months

* Added yearlies

* Fixed every nth weekdy of month

* Fixed tests to check every x week on weekday

* Began combining x month with nth weekday

* Added every x month combined with date and weekday

* Fixed lint issues

* Saved moment-recurr to package.json

* Added new repeat fields

* Added UI for repeatables

* Ensured only dalies are affected by summary

* Added local strings

* Updated npm shrinkwrap

* Shared day map constant

* Updated shrinkwrap

* Added ui back

* Updated copy of test cases

* Added new translation strings

* Updated shrinkwrap

* Fixed broken test

* Made should do tests static for better consitency

* Fixed issue with no repeat

* Fixed line endings

* Added frequency enum values

* Fixed spacing

* Upgrade lodash to v4 and lint more files (#8495)

* common: import lodash modules separately

* remove test/content from .eslintignore, fix with eslint --fix content/index

* lint test/content

* lint content/index except for lodash methods

* upgrade server/models

* upgrade server/middlewares and server/libs

* port server/controllers/top-level

* port server/controllers/api-v3

* port views and tests

* client old port lodash and _(, missing _.

* upgrade client-old

* port common/script (root level files only)

* port common/script/fns

* port common/libs

* port common/script/ops

* port common/script/content and common/script/libs/shops.js

* misc fixes

* misc fixes

* misc fixes

* more tests fixes

* fix payments test stubbing, down to 2 failing tests

* remove more instances of lodash wrapping

* fix bug where toObject does not clone object

* fix tests

* upgrade migration or add lodash 4 note

* update shrinkwrap

* fix linting

* upgrade eslint-config-habitrpg

* update shrinkwrap

* recompile shrinkwrap

* chore(i18n): update locales

* Fixed lint issues with model in task services (#8523)

* ApiDoc Group (#8522)

* ApiDoc Group

* Remove space

* Add tags to default tasks (#8419)

* Add ability to add tags to default tasks

* fix missing semicolon

* fix nesting callbacks error

* Add tags to default tasks

* fix default tags

* Start test

* Finish test

* Fix tests

* Move test

* Fix padded-bock

* Fix test

* Fix request

* fix requests

* fix test

* fix lint

* Refine test

* Fix test

* Fix Test

* Fix tests

* Please work :(

* Fix stupid mistake

* Fix lint

* Fixes

* fix function

* fix lint

* fix lint

* Client/inventory WIP (#8527)

* some stable work and faster less recompilation

*  user with zero tasks can use the app

* wip work to show loading status of resources

* revert changes to sync

* Client: Guilds Discovery (#8529)

* wip: add guilds discovery page

* add public guilds page

* fix and add tests for the groups utilities mixin

* Properly format a subscription end date according to user preference (#8514)

* Client: Guild page and mix changes (#8533)

* update deps

* add guilds page

* improve karma conf, add tests for actions

* Client: semantic ui -> bootstrap 4 and less -> scss (#8535)

* client: semantic ui -> bootstrap 4 and less -> scss

* start porting components to boostrap

* port header, start porting menu

* port loading screen

* port most of the menu

* port secondary menus

* port guilds and stable

* disable tavern for now, port inbox

* typo

* put back old tavern code

* fix: remove semanticui from gulpfile

* client: add some margin to the avatar

* Group plans subs to all (#8394)

* Added subscriptions to all members when group subs

* Added unsub when group cancels

* Give user a subscription when they join a subbed group

* Removed subscription when user leaves or is removed from group

* Fixed linting issues:

* Added tests for users with a subscription being upgraded to group plan

* Added tests for checking if existing recurring user sub gets updated during group plan. Added better merging for plans

* Added test for existing gift subscriptions

* Added additional months to user when they have an existing recurring subscription and get upgraded to group sub

* Adds test for user who has cancelled with date termined in the future

* Added test to ensure date termined is reset

* Added tests for extra months carrying over

* Added test for gems bought field

* Add tests to for fields that should remain when upgrading

* Added test for all payment methods

* Added prevention for when a user joins a second group plan

* Fixed subscribing tests

* Separated group plan payment tests

* Added prevention of editing a user with a unlimited sub

* Add tests to ensure group keeps plan if they are in two and leave one

* Ensured users with two group plans do not get cancelled when on group plan is cancelled

* Ensured users without group sub are untouched when group cancels

* Fixed lint issues

* Added new emails

* Added fix for cron tests

* Add restore to stubbed methods

* Ensured cancelled group subscriptions are updated

* Changed group plan exist check to check for date terminated

* Updated you cannont delete active group message

* Removed description requirement

* Added upgrade group plan for Amazon payments

* Fixed lint issues

* Fixed broken tests

* Fixed user delete tests

* Fixed function calls

* Hid cancel button if user has group plan

* Hide difficulty from rewards

* Prevented add user functions to be called when group plan is cancelled

* Fixed merge issue

* Correctly displayed group price

* Added message when you are about to join canclled group plan

* Fixed linting issues

* Updated tests to have no redirect to homes

* Allowed leaving a group with a canceld subscription

* Fixed spelling issues

* Prevented user from changing leader with active sub

* Added payment details title to replace subscription title

* Ensured we do not count leader when displaying upcoming cost

* Prevented party tasks from being displayed twice

* Prevented cancelling and already cancelled sub

* Fixed styles of subscriptions

* Added more specific mystery item tests

* Fixed test to refer to leader

* Extended test range to account for short months

* Fixed merge conflicts

* Updated yarn file

* Added missing locales

* Trigger notification

* Removed yarn

* Fixed locales

* Fixed scope mispelling

* Fixed line endings

* Removed extra advanced options from rewards

* Prevent group leader from leaving an active group plan

* Fixed issue with extra months applied to cancelled group plan

* Ensured member count is calculated when updatedGroupPlan

* Updated amazon payment method constant name

* Added comment to cancel sub user method

* Fixed smantic issues

* Added unite test for user isSubscribed and hasNotCancelled

* Add tests for isSubscribed and hasNotCanceled

* Changed default days remaining to 2 days for group plans

* Fixed logic with adding canceled notice to group invite

* mongoose: upgrade to 4.8.6 and remove un-necessary _.clone calls now that toObject clones correctly

* config.json: do not enable APN by default

* Moved show counters to directive (#8537)

* Moved show counters to directive

- hide counters on challenge page
- hide counters on group page

* Changed let to var

* Tasks sort delete fix (#8526)

* Fixed task sorting

* Add sync when group task is deleted

* Added sync when user tasks reorder

* Abstracted show logic and removed task grouping from group page

* Fixed scope typo

* Localized the default challenge short name

* Removed default shortName

* Fixed test for challenge shortName

* client: router: always scroll to the top

* fix(sprites): adjust eggs (#8543)

* adjust apidocs comment to remove unnecessary id parameter with incorrect syntax

* Repeatables fixes (#8538)

* Prevented watch functions from being called when task._edit is removed

* Added start date support on the UI task summary

* Fixed setting of monthly and calculations

* Fixed linting issues

* Added check for existence

* Added existence check

* Ensured correct start date is used on update

* Hid repeat options from anything not a daily

* Added missing locales

* Moved repeatables out of advance options

* Minification fix hide features (#8544)

* Added minification fix

* Hid settings for features we will not release yet

* Hid repeatables UI

* Removed extra file

* Removed repeats every from weekly

* Added start date back

* Hid counter reset when advance is collpased

* Group migrations (#8528)

* Added create group migration

* Add migration for unlimited group subscription

* Add migration to update group members with group plans

* Added error catch

* Added comments

* Group plans copy changes (#8546)

* Added new message for when user has group plan

* Changed subscription wording to group plan

* Updated copy

* Jackalopes (#8547)

* feat(content): add Jackalope rare mount

* chore(news): Bailey for Group Plans

* fix(sprites): correct inconsistent file perms

* Group plans add mount (#8548)

* Added jakcalop mount to group plan members

* Changed pet assignment to mount

* Updated migration to not update canceled group plans (#8550)

* Send group plans subscription message to Slack (#8549)

* feat(Slack): send group plans sub message

* fix(Slack): grab more relevant user data

* 3.79.0

* chore(i18n): update locales

* chore(sprites): compile
2017-03-09 21:50:22 -06:00
SabreCat 41ee72d407 chore(sprites): compile 2017-03-10 02:06:18 +00:00
Sabe Jones 9a5f6d4ad6 chore(i18n): update locales 2017-03-10 01:58:16 +00:00
SabreCat d19237cdbe 3.79.0 2017-03-10 01:47:32 +00:00
Sabe Jones 18bd3f8c54 Send group plans subscription message to Slack (#8549)
* feat(Slack): send group plans sub message

* fix(Slack): grab more relevant user data
2017-03-09 19:11:21 -06:00
Keith Holliday 8b65ce3053 Updated migration to not update canceled group plans (#8550) 2017-03-09 17:50:42 -06:00
Keith Holliday 38b894db56 Group plans add mount (#8548)
* Added jakcalop mount to group plan members

* Changed pet assignment to mount
2017-03-09 16:15:00 -07:00
Sabe Jones 61db283473 Jackalopes (#8547)
* feat(content): add Jackalope rare mount

* chore(news): Bailey for Group Plans

* fix(sprites): correct inconsistent file perms
2017-03-09 14:50:28 -07:00
Keith Holliday d70d39cc49 Group plans copy changes (#8546)
* Added new message for when user has group plan

* Changed subscription wording to group plan

* Updated copy
2017-03-09 14:27:52 -07:00
Keith Holliday c26b884bc7 Group migrations (#8528)
* Added create group migration

* Add migration for unlimited group subscription

* Add migration to update group members with group plans

* Added error catch

* Added comments
2017-03-09 14:26:31 -07:00
Keith Holliday 11c8f2a775 Minification fix hide features (#8544)
* Added minification fix

* Hid settings for features we will not release yet

* Hid repeatables UI

* Removed extra file

* Removed repeats every from weekly

* Added start date back

* Hid counter reset when advance is collpased
2017-03-08 18:50:57 -07:00
Keith Holliday 1082359f2c Repeatables fixes (#8538)
* Prevented watch functions from being called when task._edit is removed

* Added start date support on the UI task summary

* Fixed setting of monthly and calculations

* Fixed linting issues

* Added check for existence

* Added existence check

* Ensured correct start date is used on update

* Hid repeat options from anything not a daily

* Added missing locales

* Moved repeatables out of advance options
2017-03-08 16:48:30 -07:00
Alys 6486862242 adjust apidocs comment to remove unnecessary id parameter with incorrect syntax 2017-03-09 08:06:15 +10:00
Sabe Jones f68cc569d6 fix(sprites): adjust eggs (#8543) 2017-03-08 11:12:49 -06:00
Matteo Pagliazzi b10751e874 client: router: always scroll to the top 2017-03-08 09:25:36 +01:00
Keith Holliday b75c57f130 Tasks sort delete fix (#8526)
* Fixed task sorting

* Add sync when group task is deleted

* Added sync when user tasks reorder

* Abstracted show logic and removed task grouping from group page

* Fixed scope typo

* Localized the default challenge short name

* Removed default shortName

* Fixed test for challenge shortName
2017-03-07 14:28:49 -07:00
Keith Holliday 28c93ea869 Moved show counters to directive (#8537)
* Moved show counters to directive

- hide counters on challenge page
- hide counters on group page

* Changed let to var
2017-03-07 13:58:39 -07:00
Matteo Pagliazzi 7f630f2b86 config.json: do not enable APN by default 2017-03-07 14:19:27 +01:00
Matteo Pagliazzi 1a8f591251 mongoose: upgrade to 4.8.6 and remove un-necessary _.clone calls now that toObject clones correctly 2017-03-07 14:16:14 +01:00
Keith Holliday be60fb0635 Group plans subs to all (#8394)
* Added subscriptions to all members when group subs

* Added unsub when group cancels

* Give user a subscription when they join a subbed group

* Removed subscription when user leaves or is removed from group

* Fixed linting issues:

* Added tests for users with a subscription being upgraded to group plan

* Added tests for checking if existing recurring user sub gets updated during group plan. Added better merging for plans

* Added test for existing gift subscriptions

* Added additional months to user when they have an existing recurring subscription and get upgraded to group sub

* Adds test for user who has cancelled with date termined in the future

* Added test to ensure date termined is reset

* Added tests for extra months carrying over

* Added test for gems bought field

* Add tests to for fields that should remain when upgrading

* Added test for all payment methods

* Added prevention for when a user joins a second group plan

* Fixed subscribing tests

* Separated group plan payment tests

* Added prevention of editing a user with a unlimited sub

* Add tests to ensure group keeps plan if they are in two and leave one

* Ensured users with two group plans do not get cancelled when on group plan is cancelled

* Ensured users without group sub are untouched when group cancels

* Fixed lint issues

* Added new emails

* Added fix for cron tests

* Add restore to stubbed methods

* Ensured cancelled group subscriptions are updated

* Changed group plan exist check to check for date terminated

* Updated you cannont delete active group message

* Removed description requirement

* Added upgrade group plan for Amazon payments

* Fixed lint issues

* Fixed broken tests

* Fixed user delete tests

* Fixed function calls

* Hid cancel button if user has group plan

* Hide difficulty from rewards

* Prevented add user functions to be called when group plan is cancelled

* Fixed merge issue

* Correctly displayed group price

* Added message when you are about to join canclled group plan

* Fixed linting issues

* Updated tests to have no redirect to homes

* Allowed leaving a group with a canceld subscription

* Fixed spelling issues

* Prevented user from changing leader with active sub

* Added payment details title to replace subscription title

* Ensured we do not count leader when displaying upcoming cost

* Prevented party tasks from being displayed twice

* Prevented cancelling and already cancelled sub

* Fixed styles of subscriptions

* Added more specific mystery item tests

* Fixed test to refer to leader

* Extended test range to account for short months

* Fixed merge conflicts

* Updated yarn file

* Added missing locales

* Trigger notification

* Removed yarn

* Fixed locales

* Fixed scope mispelling

* Fixed line endings

* Removed extra advanced options from rewards

* Prevent group leader from leaving an active group plan

* Fixed issue with extra months applied to cancelled group plan

* Ensured member count is calculated when updatedGroupPlan

* Updated amazon payment method constant name

* Added comment to cancel sub user method

* Fixed smantic issues

* Added unite test for user isSubscribed and hasNotCancelled

* Add tests for isSubscribed and hasNotCanceled

* Changed default days remaining to 2 days for group plans

* Fixed logic with adding canceled notice to group invite
2017-03-06 15:09:50 -07:00
Matteo Pagliazzi 03a1d61c08 client: add some margin to the avatar 2017-03-06 21:09:19 +01:00
Matteo Pagliazzi 0767dc97b7 fix: remove semanticui from gulpfile 2017-03-06 20:58:41 +01:00
Matteo Pagliazzi 4978a62829 Client: semantic ui -> bootstrap 4 and less -> scss (#8535)
* client: semantic ui -> bootstrap 4 and less -> scss

* start porting components to boostrap

* port header, start porting menu

* port loading screen

* port most of the menu

* port secondary menus

* port guilds and stable

* disable tavern for now, port inbox

* typo

* put back old tavern code
2017-03-06 20:09:34 +01:00
Matteo Pagliazzi 0a35e63897 Client: Guild page and mix changes (#8533)
* update deps

* add guilds page

* improve karma conf, add tests for actions
2017-03-05 19:07:48 +01:00
Megan Tiu 03b3e79ea0 Properly format a subscription end date according to user preference (#8514) 2017-03-03 11:52:57 -07:00
Matteo Pagliazzi dc8598ae81 Client: Guilds Discovery (#8529)
* wip: add guilds discovery page

* add public guilds page

* fix and add tests for the groups utilities mixin
2017-03-03 19:38:17 +01:00
Sabe Jones 3629f7f8a5 Merge branch 'release' into develop 2017-03-03 17:36:25 +00:00
Matteo Pagliazzi 8805f81b96 Client/inventory WIP (#8527)
* some stable work and faster less recompilation

*  user with zero tasks can use the app

* wip work to show loading status of resources

* revert changes to sync
2017-03-03 15:40:21 +01:00
MathWhiz 207dbf35d6 Add tags to default tasks (#8419)
* Add ability to add tags to default tasks

* fix missing semicolon

* fix nesting callbacks error

* Add tags to default tasks

* fix default tags

* Start test

* Finish test

* Fix tests

* Move test

* Fix padded-bock

* Fix test

* Fix request

* fix requests

* fix test

* fix lint

* Refine test

* Fix test

* Fix Test

* Fix tests

* Please work :(

* Fix stupid mistake

* Fix lint

* Fixes

* fix function

* fix lint

* fix lint
2017-03-03 14:57:57 +01:00
Sabe Jones 448a953147 Backgrounds and Armoire 2017/03 (#8525)
* feat(content): add Armoire and BGs 2017-03

* chore(sprites): compile

* chore(event): disable Cupid Potions

* fix(sprites): correct shop canvas

* chore(news): Bailey 2017-03-02

* 3.78.0
2017-03-02 15:49:07 -06:00
MathWhiz 4fb1ff2baa ApiDoc Group (#8522)
* ApiDoc Group

* Remove space
2017-03-02 18:11:50 +01:00
Keith Holliday be64274be4 Fixed lint issues with model in task services (#8523) 2017-03-02 08:16:28 -07:00
Matteo Pagliazzi 0cb254d5fc chore(i18n): update locales 2017-03-01 18:33:35 +01:00
Matteo Pagliazzi 98c019a0b6 Upgrade lodash to v4 and lint more files (#8495)
* common: import lodash modules separately

* remove test/content from .eslintignore, fix with eslint --fix content/index

* lint test/content

* lint content/index except for lodash methods

* upgrade server/models

* upgrade server/middlewares and server/libs

* port server/controllers/top-level

* port server/controllers/api-v3

* port views and tests

* client old port lodash and _(, missing _.

* upgrade client-old

* port common/script (root level files only)

* port common/script/fns

* port common/libs

* port common/script/ops

* port common/script/content and common/script/libs/shops.js

* misc fixes

* misc fixes

* misc fixes

* more tests fixes

* fix payments test stubbing, down to 2 failing tests

* remove more instances of lodash wrapping

* fix bug where toObject does not clone object

* fix tests

* upgrade migration or add lodash 4 note

* update shrinkwrap

* fix linting

* upgrade eslint-config-habitrpg

* update shrinkwrap

* recompile shrinkwrap
2017-03-01 17:10:48 +01:00
Keith Holliday ef02e59590 Repeatables (#8444)
* Added initial should do weekly tests

* Added support back in for days of the week and every x day

* Added better week day mapper

* Added initial monthly

* Added every x months

* Added yearlies

* Fixed every nth weekdy of month

* Fixed tests to check every x week on weekday

* Began combining x month with nth weekday

* Added every x month combined with date and weekday

* Fixed lint issues

* Saved moment-recurr to package.json

* Added new repeat fields

* Added UI for repeatables

* Ensured only dalies are affected by summary

* Added local strings

* Updated npm shrinkwrap

* Shared day map constant

* Updated shrinkwrap

* Added ui back

* Updated copy of test cases

* Added new translation strings

* Updated shrinkwrap

* Fixed broken test

* Made should do tests static for better consitency

* Fixed issue with no repeat

* Fixed line endings

* Added frequency enum values

* Fixed spacing
2017-02-27 15:41:21 -07:00
Keith Holliday 93befcebcc Tasks score notes (#8507)
* Added setting and modal for score notes

* Added persistent score notes

* Fixed linting issues and documented new field

* Added max length to task score notes

* Added check for score notes existence

* Combined tasks perferences
2017-02-27 14:56:34 -07:00
Keith Holliday 68a042cdb9 Leaving a group (#8517)
* Leaving a group or a guild no longer removes the user from the challenges of that group or guild.

* Updating api docs for leaving group to take into account the default path no longer leaving challenges when leaving a group.

* Updating api docs for leaving group to take into account the default path no longer leaving challenges when leaving a group.

* refactored according to blade's comments to not be a breaking change. The api now accepts a body parameter to specify wether the user
should remain in the groups challenges or leave them. The change also adds more tests around this behavior to confirm that it works
as expected.
2017-02-27 13:58:30 -07:00
Keith Holliday 30954fe7c5 Added support for grouping tasks by challenge (#8469)
* Added support for grouping tasks by chllenge

* Fixed tests and updated default challenge model name

* Fixed broken member test

* Updated setting string

* Changed to shortName

* Began abstracting task grouping

* Added initial task directive code

* Added new directives to help with grouping of tasks

* Removed random console.log
2017-02-27 11:34:03 -07:00
deonna 44f23a7675 fix(nextRewardUnlocksIn): Check-in prize message was always plural --… (#8458)
* fix(nextRewardUnlocksIn): Check-in prize message was always plural -- moving to a non-sentence like structure to fix incorrect grammar.

* fix(countLeft): Check-in prize message was always plural -- moving to a non-sentence like structure to fix incorrect grammar.
2017-02-27 11:20:57 -07:00
Matt Handley 705a78e835 Debounce $scope updates when typing in chat. (#8485)
Fixes #6462, by saving a bunch of time per frame. See the issue for evidence of
the win.
2017-02-27 11:19:42 -07:00
astolat 6d0df78441 Habits v2: adding counter to habits (cleaned up branch) - fixes #8113 (#8198)
* Clean version of PR 8175

The original PR for this was here:
https://github.com/HabitRPG/habitica/pull/8175

Unfortunately while fixing a conflict in tasks.json, I messed up the rebase and wound up pulling in too many commits and making a giant mess. Sorry. :P

* Fixing test failure

This test seems to occasionally start failing (another coder reported the same thing happening to them in the blacksmiths’ guild) because the order in which the tasks are created can sometimes not match the order in the array. So I have sorted the tasks array after creation by the task name to ensure a consistent ordering, and slightly reordered the expect statements to match.
2017-02-27 11:15:45 -07:00
1583 changed files with 93331 additions and 67779 deletions
+2 -5
View File
@@ -8,16 +8,13 @@ dist/
dist-client/
# Not linted
migrations/*
website/client-old/
scripts/*
test/server_side/**/*
test/client-old/spec/**/*
# Temporarilly disabled. These should be removed when the linting errors are fixed TODO
website/common/script/content/index.js
migrations/*
scripts/*
website/common/browserify.js
test/content/**/*
Gruntfile.js
gulpfile.js
gulp
+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.)
+10 -8
View File
@@ -12,22 +12,24 @@ before_install:
- $CXX --version
- npm install -g npm@4
- if [ $REQUIRES_SERVER ]; then sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10; echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list; sudo apt-get update; sudo apt-get install mongodb-org-server; fi
install:
- npm install &> npm.install.log || (cat npm.install.log; false)
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
+5 -32
View File
@@ -1,43 +1,16 @@
FROM ubuntu:trusty
MAINTAINER Sabe Jones <sabe@habitica.com>
# Avoid ERROR: invoke-rc.d: policy-rc.d denied execution of start.
RUN echo -e '#!/bin/sh\nexit 0' > /usr/sbin/policy-rc.d
# Install prerequisites
RUN apt-get update
RUN apt-get install -y \
build-essential \
curl \
git \
libfontconfig1 \
libfreetype6 \
libkrb5-dev \
python
# Install NodeJS
RUN curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
RUN apt-get install -y nodejs
# Install npm@latest
RUN curl -sL https://www.npmjs.org/install.sh | sh
# Clean up package management
RUN apt-get clean
RUN rm -rf /var/lib/apt/lists/*
FROM node:boron
# Install global packages
RUN npm install -g gulp grunt-cli bower mocha
# Clone Habitica repo and install dependencies
WORKDIR /habitrpg
RUN git clone https://github.com/HabitRPG/habitica.git /habitrpg
RUN mkdir -p /usr/src/habitrpg
WORKDIR /usr/src/habitrpg
RUN git clone https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
RUN npm install
RUN bower install --allow-root
# Create environment config file and build directory
RUN cp config.json.example config.json
# Create Build dir
RUN mkdir -p ./website/build
# Start Habitica
+2 -3
View File
@@ -5,8 +5,7 @@ Habitica [![Build Status](https://travis-ci.org/HabitRPG/habitica.svg?branch=dev
We need more programmers! Your assistance will be greatly appreciated.
For an introduction to the technologies used and how the software is organized, refer to [Contributing to Habitica](http://habitica.wikia.com/wiki/Contributing_to_Habitica#Coders_.28Web_.26_Mobile.29) - "Coders (Web & Mobile)" section.
For an introduction to the technologies used and how the software is organized, refer to [Guidance for Blacksmiths](http://habitica.wikia.com/wiki/Guidance_for_Blacksmiths).
To set up a local install of Habitica for development and testing, see [Setting up Habitica Locally](http://habitica.wikia.com/wiki/Setting_up_Habitica_Locally), which contains instructions for Windows, *nix / Mac OS, and Vagrant.
To set up a local install of Habitica for development and testing on various platforms, see [Setting up Habitica Locally](http://habitica.wikia.com/wiki/Setting_up_Habitica_Locally).
Then read [Guidance for Blacksmiths](http://habitica.wikia.com/wiki/Guidance_for_Blacksmiths) for additional instructions and useful tips.
+1 -1
View File
@@ -30,7 +30,7 @@
"bootstrap-tour": "0.10.1",
"css-social-buttons": "samcollins/css-social-buttons#v1.1.1 ",
"github-buttons": "mdo/github-buttons#v3.0.0",
"hello": "1.13.4",
"hello": "1.14.1",
"jquery": "2.1.0",
"jquery-colorbox": "1.4.36",
"jquery-ui": "1.10.3",
+16 -2
View File
@@ -73,9 +73,14 @@
"LOGGLY_ACCOUNT": "account",
"PUSH_CONFIGS": {
"GCM_SERVER_API_KEY": "",
"APN_ENABLED": "true",
"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
@@ -25,7 +25,7 @@ gulp.task('build:common', () => {
gulp.task('build:server', ['build:src', 'build:common']);
gulp.task('build:dev', ['browserify', 'prepare:staticNewStuff', 'semantic-ui'], (done) => {
gulp.task('build:dev', ['browserify', 'prepare:staticNewStuff'], (done) => {
gulp.start('grunt-build:dev', done);
});
@@ -33,7 +33,7 @@ gulp.task('build:dev:watch', ['build:dev'], () => {
gulp.watch(['website/client-old/**/*.styl', 'website/common/script/*']);
});
gulp.task('build:prod', ['browserify', 'build:server', 'prepare:staticNewStuff', 'semantic-ui'], (done) => {
gulp.task('build:prod', ['browserify', 'build:server', 'prepare:staticNewStuff'], (done) => {
runSequence(
'grunt-build:prod',
'apidoc',
-43
View File
@@ -1,43 +0,0 @@
import gulp from 'gulp';
import fs from 'fs';
// Make semantic-ui-less work with a theme in a different folder
// Code taken from https://www.artembutusov.com/webpack-semantic-ui/
// Relative to node_modules/semantic-ui-less
const SEMANTIC_THEME_PATH = '../../website/client/assets/less/semantic-ui/theme.config';
// fix well known bug with default distribution
function fixFontPath (filename) {
return new Promise((resolve, reject) => {
fs.readFile(filename, 'utf8', (err, content) => {
if (err) return reject(err);
let newContent = content.replace(
'@fontPath : \'../../themes/',
'@fontPath : \'../../../themes/'
);
fs.writeFile(filename, newContent, 'utf8', (err1) => {
if (err) return reject(err1);
resolve();
});
});
});
}
gulp.task('semantic-ui', (done) => {
// relocate default config
fs.writeFile(
'node_modules/semantic-ui-less/theme.config',
`@import '${SEMANTIC_THEME_PATH}';\n`,
'utf8',
(err) => {
if (err) return done(err);
fixFontPath('node_modules/semantic-ui-less/themes/default/globals/site.variables')
.then(() => done())
.catch(done);
}
);
});
+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) {
+6 -6
View File
@@ -67,7 +67,7 @@ gulp.task('transifex:malformedStrings', () => {
let stringsWithIncorrectNumberOfInterpolations = [];
let count = 0;
_(ALL_LANGUAGES).each(function (lang) {
_.each(ALL_LANGUAGES, function (lang) {
_.each(stringsToLookFor, function (strings, file) {
let translationFile = fs.readFileSync(LOCALES + lang + '/' + file);
@@ -89,7 +89,7 @@ gulp.task('transifex:malformedStrings', () => {
}
});
});
}).value();
});
if (!_.isEmpty(stringsWithMalformedInterpolations)) {
let message = 'The following strings have malformed or missing interpolations';
@@ -114,7 +114,7 @@ function getArrayOfLanguages () {
function eachTranslationFile (languages, cb) {
let jsonFiles = stripOutNonJsonFiles(fs.readdirSync(ENGLISH_LOCALE));
_(languages).each((lang) => {
_.each(languages, (lang) => {
_.each(jsonFiles, (filename) => {
try {
var translationFile = fs.readFileSync(LOCALES + lang + '/' + filename);
@@ -128,7 +128,7 @@ function eachTranslationFile (languages, cb) {
cb(null, lang, filename, parsedEnglishFile, parsedTranslationFile);
});
}).value();
});
}
function eachTranslationString (languages, cb) {
@@ -153,7 +153,7 @@ function formatMessageForPosting (msg, items) {
function getStringsWith (json, interpolationRegex) {
var strings = {};
_(json).each(function (file_name) {
_.each(json, function (file_name) {
var raw_file = fs.readFileSync(ENGLISH_LOCALE + file_name);
var parsed_json = JSON.parse(raw_file);
@@ -162,7 +162,7 @@ function getStringsWith (json, interpolationRegex) {
var match = value.match(interpolationRegex);
if (match) strings[file_name][key] = match;
});
}).value();
});
return strings;
}
-1
View File
@@ -9,7 +9,6 @@
require('babel-register');
if (process.env.NODE_ENV === 'production') {
require('./gulp/gulp-semanticui');
require('./gulp/gulp-apidoc');
require('./gulp/gulp-newstuff');
require('./gulp/gulp-build');
+7
View File
@@ -1,5 +1,12 @@
// %mongo server:27017/dbname underscore.js my_commands.js
// %mongo server:27017/dbname underscore.js --shell
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
var habits = 0,
dailies = 0,
todos = 0,
+6
View File
@@ -3,6 +3,12 @@
*/
// mongo habitrpg ./node_modules/underscore/underscore.js ./migrations/20130326_migrate_pets.js
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
var mapping = {
bearcub: {name:'BearCub', modifier: 'Base'},
cactus: {name:'Cactus', modifier:'Base'},
+6
View File
@@ -4,6 +4,12 @@
// mongo habitrpg ./node_modules/underscore/underscore.js migrations/20130327_apply_tokens.js
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
var mapping = [
{
tier: 1,
@@ -6,6 +6,11 @@
* mongo habitrpg ./node_modules/underscore/underscore.js ./migrations/20130508_fix_duff_party_subscriptions.js
*/
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
// since our primary subscription will first hit parties now, we *definitely* need an index there
db.parties.ensureIndex( { 'members': 1}, {background: true} );
+6
View File
@@ -1,5 +1,11 @@
//mongo habitrpg ./node_modules/lodash/lodash.js migrations/20130602_survey_rewards.js
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
var members = []
members = _.uniq(members);
@@ -3,6 +3,12 @@
// Racer was notorious for adding duplicates, randomly deleting documents, etc. Once we pull the plug on old.habit,
// run this migration to cleanup all the corruption
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
db.users.find().forEach(function(user){
// remove corrupt tasks, which will either be null-value or no id
@@ -5,6 +5,12 @@
// @see http://stackoverflow.com/questions/14867697/mongoose-full-collection-scan
//Also, what do we think of a Mongoose Migration module? something like https://github.com/madhums/mongoose-migrate
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
db.users.find().forEach(function(user){
// Add invites to groups
@@ -8,6 +8,12 @@
var mongo = require('mongoskin');
var _ = require('lodash');
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
var backupUsers = mongo.db('localhost:27017/habitrpg_old?auto_reconnect').collection('users');
var liveUsers = mongo.db('localhost:27017/habitrpg_new?auto_reconnect').collection('users');
+6
View File
@@ -1,5 +1,11 @@
// node .migrations/20131127_restore_dayStart.js
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
var mongo = require('mongoskin');
var _ = require('lodash');
+6
View File
@@ -8,6 +8,12 @@ mongo = require('mongoskin')
_ = require('lodash')
async = require('async')
# IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
# We've now upgraded to lodash v4 but the code used in this migration has not been
# adapted to work with it. Before this migration is used again any lodash method should
# be checked for compatibility against the v4 changelog and changed if necessary.
# https://github.com/lodash/lodash/wiki/Changelog#v400
db = mongo.db('localhost:27017/habitrpg?auto_reconnect')
###
@@ -1,5 +1,11 @@
// node .migrations/20131221_restore_NaN_history.js
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
/**
* After the classes migration, users lost some history entries
*/
+6
View File
@@ -1,5 +1,11 @@
// node .migrations/20131225_restore_streaks.js
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
/**
* After the classes migration, users lost some history entries
*/
@@ -5,6 +5,12 @@ var migrationName = '20140823_remove_undefined_and_false_notifications';
var authorName = 'Alys'; // in case script author needs to know when their ...
var authorUuid = 'd904bd62-da08-416b-a816-ba797c9ee265'; //... own data is done
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
/**
* https://github.com/HabitRPG/habitrpg/pull/3907
*/
@@ -4,6 +4,12 @@ var migrationName = '20140829_change_headAccessory_to_eyewear';
var authorName = 'Alys'; // in case script author needs to know when their ...
var authorUuid = 'd904bd62-da08-416b-a816-ba797c9ee265'; //... own data is done
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
/**
* https://github.com/HabitRPG/habitrpg/issues/3645
*/
@@ -4,6 +4,11 @@
//
// node 20140831_increase_gems_for_previous_contributions.js > 20140831_increase_gems_for_previous_contributions_output.txt
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
var migrationName = '20140831_increase_gems_for_previous_contributions';
@@ -8,6 +8,12 @@ var authorUuid = 'd904bd62-da08-416b-a816-ba797c9ee265'; //... own data is done
* Convert Tier 7 contributors with admin flag to Tier 8 (moderators).
*/
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
var mongo = require('mongoskin');
var _ = require('lodash');
@@ -1,3 +1,9 @@
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
// require moment, lodash
db.users.find(
{'purchased.plan.customerId':{$ne:null}},
+6
View File
@@ -9,6 +9,12 @@ var authorUuid = 'd904bd62-da08-416b-a816-ba797c9ee265'; //... own data is done
var dbserver = 'localhost:27017' // CHANGE THIS FOR PRODUCTION DATABASE
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
var mongo = require('mongoskin');
var _ = require('lodash');
@@ -8,6 +8,12 @@ var authorUuid = 'd904bd62-da08-416b-a816-ba797c9ee265'; //... own data is done
var dbserver = 'localhost:27017' // CHANGE THIS FOR PRODUCTION DATABASE
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
var mongo = require('mongoskin');
var _ = require('lodash');
@@ -19,6 +19,12 @@ var authorUuid = 'd904bd62-da08-416b-a816-ba797c9ee265'; //... own data is done
var dbserver = 'localhost:27017' // CHANGE THIS FOR PRODUCTION DATABASE
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
var mongo = require('mongoskin');
var _ = require('lodash');
var moment = require('moment');
@@ -6,6 +6,12 @@ var authorUuid = 'd904bd62-da08-416b-a816-ba797c9ee265'; //... own data is done
* force all active players to rest in the inn due to massive server fail
*/
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
var dbserver = 'localhost:27017' // CHANGE THIS FOR PRODUCTION DATABASE
var mongo = require('mongoskin');
+6
View File
@@ -19,6 +19,12 @@ var authorUuid = 'd904bd62-da08-416b-a816-ba797c9ee265'; //... own data is done
* means minimal new testing.
*/
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
var dbserver = 'localhost:27017' // FOR TEST DATABASE
// var dbserver = 'username:password@ds031379-a0.mongolab.com:31379' // FOR PRODUCTION DATABASE
var dbname = 'habitrpg';
@@ -7,6 +7,12 @@ var migrationName = '20160111_challenges_condense_same_day_history_entries.js';
var dbserver = '';
var dbname = '';
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
var mongo = require('mongoskin');
var _ = require('lodash');
var moment = require('moment');
+6
View File
@@ -11,6 +11,12 @@ var dbserver = 'localhost:27017'; // FOR TEST DATABASE
// var dbserver = 'username:password@ds031379-a0.mongolab.com:31379'; // FOR PRODUCTION DATABASE
var dbname = 'habitrpg';
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
var mongo = require('mongoskin');
var _ = require('lodash');
+6
View File
@@ -14,6 +14,12 @@ var dbname = 'habitrpg';
var mongo = require('mongoskin');
var _ = require('lodash');
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
var dbUsers = mongo.db(dbserver + '/' + dbname + '?auto_reconnect').collection('users');
// specify a query to limit the affected users (empty for all users):
@@ -2,6 +2,12 @@ var uuid = require('uuid').v4;
var mongo = require('mongodb').MongoClient;
var _ = require('lodash');
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
var taskIds = require('checklists-no-id.json').map(function (obj) {
return obj._id;
});
@@ -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;
+6
View File
@@ -6,6 +6,12 @@
// Due to some big user profiles it needs more RAM than is allowed by default by v8 (arounf 1.7GB).
// Run the script with --max-old-space-size=4096 to allow up to 4GB of RAM
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
console.log('Starting migrations/api_v3/challenges.js.');
require('babel-register');
+6
View File
@@ -9,6 +9,12 @@
// Run the script with --max-old-space-size=4096 to allow up to 4GB of RAM
console.log('Starting migrations/api_v3/challengesMembers.js.');
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
require('babel-register');
require('babel-polyfill');
+6
View File
@@ -8,6 +8,12 @@
// Run the script with --max-old-space-size=4096 to allow up to 4GB of RAM
console.log('Starting migrations/api_v3/coupons.js.');
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
require('babel-register');
require('babel-polyfill');
@@ -8,6 +8,12 @@
// Run the script with --max-old-space-size=4096 to allow up to 4GB of RAM
console.log('Starting migrations/api_v3/unsubscriptions.js.');
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
require('babel-register');
require('babel-polyfill');
+6
View File
@@ -16,6 +16,12 @@
// Run the script with --max-old-space-size=4096 to allow up to 4GB of RAM
console.log('Starting migrations/api_v3/groups.js.');
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
require('babel-register');
require('babel-polyfill');
+6
View File
@@ -9,6 +9,12 @@
// Run the script with --max-old-space-size=4096 to allow up to 4GB of RAM
console.log('Starting migrations/api_v3/users.js.');
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
// We've now upgraded to lodash v4 but the code used in this migration has not been
// adapted to work with it. Before this migration is used again any lodash method should
// be checked for compatibility against the v4 changelog and changed if necessary.
// https://github.com/lodash/lodash/wiki/Changelog#v400
require('babel-register');
require('babel-polyfill');
@@ -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;
+1 -1
View File
@@ -7,6 +7,6 @@
db.users.find().forEach(function(user){
user.tasks = user.habits.concat(user.dailys).concat(user.todos).concat(user.rewards);
var found = _.any(user.tasks, {text: ""})
var found = _.some(user.tasks, {text: ""})
if (found) printjson({id:user._id, auth:user.auth});
})
@@ -0,0 +1,40 @@
var migrationName = 'AddUnlimitedSubscription';
var authorName = 'TheHollidayInn'; // in case script author needs to know when their ...
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, dateTerminated) {
let group = await Group.findById(groupId);
group.purchased.plan.customerId = "group-unlimited";
group.purchased.plan.dateCreated = new Date();
group.purchased.plan.dateUpdated = new Date();
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 = "";
return group.save();
};
module.exports = async function addUnlimitedSubscriptionCreator () {
let groupId = process.argv[2];
if (!groupId) throw Error('Group ID is required');
let dateTerminated = process.argv[3];
let result = await addUnlimitedSubscription(groupId, dateTerminated);
};
+32
View File
@@ -0,0 +1,32 @@
import Bluebird from 'bluebird';
import { model as Group } from '../../website/server/models/group';
import { model as User } from '../../website/server/models/user';
// @TODO: this should probably be a GroupManager library method
async function createGroup (name, privacy, type, leaderId) {
let user = await User.findById(leaderId);
let group = new Group({
name,
privacy,
type,
});
group.leader = user._id;
user.guilds.push(group._id);
return Bluebird.all([group.save(), user.save()]);
};
module.exports = async function groupCreator () {
let name = process.argv[2];
let privacy = process.argv[3];
let type = process.argv[4];
let leaderId = process.argv[5];
let result = await createGroup(name, privacy, type, leaderId)
};
+46
View File
@@ -0,0 +1,46 @@
var migrationName = 'Jackalopes for Unlimited Subscribers';
/*
* This migration will find users with unlimited subscriptions who are also eligible for Jackalope mounts, and award them
*/
import Bluebird from 'bluebird';
import { model as Group } from '../../website/server/models/group';
import { model as User } from '../../website/server/models/user';
import * as payments from '../../website/server/libs/payments';
async function handOutJackalopes () {
let promises = [];
let cursor = User.find({
'purchased.plan.customerId':'habitrpg',
}).cursor();
cursor.on('data', async function(user) {
console.log('User: ' + user._id);
let groupList = [];
if (user.party._id) groupList.push(user.party._id);
groupList = groupList.concat(user.guilds);
let subscribedGroup =
await Group.findOne({
'_id': {$in: groupList},
'purchased.plan.planId': 'group_monthly',
'purchased.plan.dateTerminated': null,
},
{'_id':1}
);
if (subscribedGroup) {
User.update({'_id':user._id},{$set:{'items.mounts.Jackalope-RoyalPurple':true}}).exec();
promises.push(user.save());
}
});
cursor.on('close', async function() {
console.log('done');
return await Bluebird.all(promises);
});
};
module.exports = handOutJackalopes;
@@ -0,0 +1,33 @@
var migrationName = 'ResyncGroupPlanMembers';
var authorName = 'TheHollidayInn'; // in case script author needs to know when their ...
var authorUuid = ''; //... own data is done
/*
* This migrations will iterate through all groups with a group plan a subscription and resync the free
* subscription to all members
*/
import Bluebird from 'bluebird';
import { model as Group } from '../../website/server/models/group';
import * as payments from '../../website/server/libs/payments';
async function updateGroupsWithGroupPlans () {
let cursor = Group.find({
'purchased.plan.planId': 'group_monthly',
'purchased.plan.dateTerminated': null,
}).cursor();
let promises = [];
cursor.on('data', function(group) {
promises.push(payments.addSubscriptionToGroupUsers(group));
promises.push(group.save())
});
cursor.on('close', async function() {
return await Bluebird.all(promises);
});
};
module.exports = updateGroupsWithGroupPlans;
+3
View File
@@ -1,6 +1,9 @@
// EMAIL="x@y.com" node ./migrations/manual_password_reset.js
// Be sure to have PRODUCTION_DB in your config.json
// IMPORTANT: this script isn't updated to use the new password encryption that uses bcrypt
// using it will break accounts and should not be used until upgraded
var nconf = require('nconf'),
path = require('path');
nconf.argv().env().file('user', path.join(path.resolve(__dirname, '../config.json')));
+24 -21
View File
@@ -1,21 +1,24 @@
require("babel-register");
require("babel-polyfill");
// This file must use ES5, everything required can be in ES6
function setUpServer () {
var nconf = require('nconf');
var mongoose = require('mongoose');
var Bluebird = require('bluebird');
var setupNconf = require('../website/server/libs/setupNconf');
setupNconf();
// We require src/server and npt src/index because
// 1. nconf is already setup
// 2. we don't need clustering
require('../website/server/server'); // eslint-disable-line global-require
}
setUpServer();
// Replace this with your migration
var processUsers = require('./new_stuff');
processUsers();
require("babel-register");
require("babel-polyfill");
// This file must use ES5, everything required can be in ES6
function setUpServer () {
var nconf = require('nconf');
var mongoose = require('mongoose');
var Bluebird = require('bluebird');
var setupNconf = require('../website/server/libs/setupNconf');
setupNconf();
// We require src/server and npt src/index because
// 1. nconf is already setup
// 2. we don't need clustering
require('../website/server/server'); // eslint-disable-line global-require
}
setUpServer();
// Replace this with your migration
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:['body_mystery_201705','head_mystery_201705']
}
}
};
+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) {
+1941 -2205
View File
File diff suppressed because it is too large Load Diff
+26 -22
View File
@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "3.77.0",
"version": "3.94.1",
"main": "./website/server/index.js",
"dependencies": {
"@slack/client": "^3.8.1",
@@ -13,7 +13,7 @@
"async": "^1.5.0",
"autoprefixer": "^6.4.0",
"aws-sdk": "^2.0.25",
"axios": "^0.15.3",
"axios": "^0.16.0",
"babel-core": "^6.0.0",
"babel-loader": "^6.0.0",
"babel-plugin-syntax-async-functions": "^6.13.0",
@@ -28,18 +28,21 @@
"bcrypt": "^1.0.2",
"bluebird": "^3.3.5",
"body-parser": "^1.15.0",
"bootstrap": "^4.0.0-alpha.6",
"bootstrap-vue": "^0.15.8",
"bower": "~1.3.12",
"browserify": "~12.0.1",
"compression": "^1.6.1",
"connect-ratelimit": "0.0.7",
"cookie-session": "^1.2.0",
"coupon-code": "^0.4.5",
"css-loader": "^0.26.1",
"css-loader": "^0.28.0",
"csv-stringify": "^1.0.2",
"cwait": "^1.0.0",
"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",
@@ -54,7 +57,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",
@@ -70,20 +73,18 @@
"jade": "~1.11.0",
"jquery": "^3.1.1",
"js2xmlparser": "~1.0.0",
"less": "^2.7.1",
"less-loader": "^2.2.3",
"lodash": "^3.10.1",
"lodash.pickby": "^4.2.0",
"lodash.setwith": "^4.2.0",
"lodash": "^4.17.4",
"merge-stream": "^1.0.0",
"method-override": "^2.3.5",
"moment": "^2.13.0",
"mongoose": "^4.7.1",
"moment-recur": "habitrpg/moment-recur#v1.0.6",
"mongoose": "^4.8.6",
"mongoose-id-autoinc": "~2013.7.14-4",
"morgan": "^1.7.0",
"nconf": "~0.8.2",
"nib": "^1.1.0",
"node-gcm": "^0.14.4",
"node-sass": "^4.5.0",
"nodemailer": "^2.3.2",
"object-path": "^0.9.2",
"ora": "^1.1.0",
@@ -96,14 +97,14 @@
"postcss-easy-import": "^2.0.0",
"pretty-data": "^0.40.0",
"ps-tree": "^1.0.0",
"pug": "^2.0.0-beta11",
"pug": "^2.0.0-beta.12",
"push-notify": "habitrpg/push-notify#v1.2.0",
"pusher": "^1.3.0",
"request": "~2.74.0",
"rimraf": "^2.4.3",
"run-sequence": "^1.1.4",
"s3-upload-stream": "^1.0.6",
"semantic-ui-less": "~2.2.4",
"sass-loader": "^6.0.2",
"serve-favicon": "^2.3.0",
"shelljs": "^0.7.6",
"stripe": "^4.2.0",
@@ -117,12 +118,14 @@
"vinyl-source-stream": "^1.1.0",
"vue": "^2.1.0",
"vue-loader": "^11.0.0",
"vue-mugen-scroll": "^0.2.1",
"vue-router": "^2.0.0-rc.5",
"vue-style-loader": "^2.0.0",
"vue-style-loader": "^3.0.0",
"vue-template-compiler": "^2.1.10",
"webpack": "^2.2.1",
"webpack-merge": "^2.6.1",
"webpack-merge": "^4.0.0",
"winston": "^2.1.0",
"winston-loggly-bulk": "^1.4.2",
"xml2js": "^0.4.4"
},
"private": true,
@@ -138,9 +141,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",
@@ -166,12 +169,12 @@
"chromedriver": "^2.27.2",
"connect-history-api-fallback": "^1.1.0",
"coveralls": "^2.11.2",
"cross-env": "^3.1.4",
"cross-env": "^4.0.0",
"cross-spawn": "^5.0.1",
"csv": "~0.3.6",
"deep-diff": "~0.1.4",
"eslint": "^3.0.0",
"eslint-config-habitrpg": "^2.0.0",
"eslint-config-habitrpg": "^3.0.0",
"eslint-friendly-formatter": "^2.0.5",
"eslint-loader": "^1.3.0",
"eslint-plugin-html": "^2.0.0",
@@ -182,7 +185,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",
@@ -190,13 +193,14 @@
"karma-mocha": "^0.2.0",
"karma-mocha-reporter": "^1.1.1",
"karma-phantomjs-launcher": "^1.0.0",
"karma-sinon-chai": "^1.2.0",
"karma-sinon-chai": "~1.2.0",
"karma-sinon-stub-promise": "^1.0.0",
"karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "0.0.24",
"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",
+1
View File
@@ -0,0 +1 @@
For information about writing and running tests, see [Using Your Local Install to Modify Habitica's Website and API](http://habitica.wikia.com/wiki/Using_Your_Local_Install_to_Modify_Habitica%27s_Website_and_API).
-123
View File
@@ -1,123 +0,0 @@
# So you want to write API integration tests?
@TODO rewrite
That's great! This README will serve as a quick primer for style conventions and practices for these tests.
## What is this?
These are integration tests for the Habitica API. They are performed by making HTTP requests to the API's endpoints and asserting on the data that is returned.
If the javascript looks weird to you, that's because it's written in [ES2015](http://www.ecma-international.org/ecma-262/6.0/) and transpiled by [Babel](https://babeljs.io/docs/learn-es2015/).
## How to run the tests
First, install gulp.
```bash
$ npm install -g gulp
```
To run the api tests, make sure the mongo db is up and running and then type this on the command line:
```bash
$ gulp test:api-v2
```
It may take a little while to get going, since it requires the web server to start up before the tests can run.
You can also run a watch command for the api tests. This will allow the tests to re-run automatically when you make changes in the `/test/api/v2/`.
```bash
$ gulp test:api-v2:watch
```
One caveat. If you have a severe syntax error in your files, the tests may fail to run, but they won't alert you that they are not running. If you ever find your test hanging for a while, run this to get the stackstrace. Once you've fixed the problem, you can resume running the watch comand.
```bash
$ gulp test:api:safe
```
If you'd like to run the tests individually and inspect the output from the server, in one pane you can run:
```bash
$ gulp test:nodemon
```
And run your tests in another pane:
```bash
$ mocha path/to/file.js
# Mark a test with the `.only` attribute
$ mocha
```
## Structure
Each top level route has it's own directory. So, all the routes that begin with `/groups/` live in `/test/api/groups/`.
Each test should:
* encompase a single route
* begin with the REST parameter for the route
* display the full name of the route, swapping out `/` for `_`
* end with test.js
So, for the `POST` route `/groups/:id/leave`, it would be
```bash
POST-groups_id_leave.test.js
```
## Promises
To mitigate [callback hell](http://callbackhell.com/) :imp:, we've written a helper method to generate a user object that can make http requests that [return promises](https://babeljs.io/docs/learn-es2015/#promises). This makes it very easy to chain together commands. All you need to do to make a subsequent request is return another promise and then call `.then((result) => {})` on the surrounding block, like so:
```js
it('does something', () => {
let user;
return generateUser().then((_user) => { // We return the initial promise so this test can be run asyncronously
user = _user;
return user.post('/groups', {
type: 'party',
});
}).then((party) => { // the result of the promise above is the argument of the function
return user.put(`/groups/${party._id}`, {
name: 'My party',
});
}).then((result) => {
return user.get('/groups/party');
}).then((party) => {
expect(party.name).to.eql('My party');
});
});
```
If the test is simple, you can use the [chai-as-promised](http://chaijs.com/plugins/chai-as-promised) `return expect(somePromise).to.eventually` syntax to make your assertion.
```js
it('makes the party creator the leader automatically', () => {
return expect(user.post('/groups', {
type: 'party',
})).to.eventually.have.deep.property('leader._id', user._id);
});
```
If the test is checking that the request returns an error, use the `.eventually.be.rejected.and.eql` syntax.
```js
it('returns an error', () => {
return expect(user.get('/groups/id-of-a-party-that-user-does-not-belong-to'))
.to.eventually.be.rejected.and.eql({
code: 404,
text: t('messageGroupNotFound'),
});
});
```
## Questions?
Ask in the [Aspiring Coder's Guild](https://habitica.com/#/options/groups/guilds/68d4a01e-db97-4786-8ee3-05d612c5af6f)!
-4
View File
@@ -1,4 +0,0 @@
# How to run tests:
1. `npm test` is equivalent to `gulp test:api-v3` which will run, in order, `gulp lint`, `gulp test:api-v3:unit` and `gulp test:api-v3:integration`. If one of these fails, the whole `npm test` command blocks and fails. Each of these commands can also be run as a standalone command.
2. To run the server and the integrations tests in two different terminals (to better inspect the output in the server) run `npm start` in one and `npm test:api-v3:integration:separate-server` in the other
@@ -117,7 +117,6 @@ describe('POST /challenges/:challengeId/leave', () => {
});
expect(testTask).to.not.be.undefined;
expect(testTask.challenge).to.eql({});
});
});
});
+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;
}
});
});
});
@@ -7,6 +7,7 @@ import {
import {
TAVERN_ID,
} from '../../../../../website/server/models/group';
import apiMessages from '../../../../../website/server/libs/apiMessages';
describe('GET /groups', () => {
let user;
@@ -14,6 +15,7 @@ describe('GET /groups', () => {
const NUMBER_OF_PUBLIC_GUILDS_USER_IS_MEMBER = 1;
const NUMBER_OF_USERS_PRIVATE_GUILDS = 1;
const NUMBER_OF_GROUPS_USER_CAN_VIEW = 5;
const GUILD_PER_PAGE = 30;
before(async () => {
await resetHabiticaDB();
@@ -98,6 +100,60 @@ describe('GET /groups', () => {
.to.eventually.have.a.lengthOf(NUMBER_OF_PUBLIC_GUILDS);
});
describe('public guilds pagination', () => {
it('req.query.paginate must be a boolean string', async () => {
await expect(user.get('/groups?paginate=aString&type=publicGuilds'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Invalid request parameters.',
});
});
it('req.query.paginate can only be true when req.query.type includes publicGuilds', async () => {
await expect(user.get('/groups?paginate=true&type=notPublicGuilds'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: apiMessages('guildsOnlyPaginate'),
});
});
it('req.query.page can\'t be negative', async () => {
await expect(user.get('/groups?paginate=true&page=-1&type=publicGuilds'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Invalid request parameters.',
});
});
it('returns 30 guilds per page ordered by number of members', async () => {
await user.update({balance: 9000});
let groups = await Promise.all(_.times(60, (i) => {
return generateGroup(user, {
name: `public guild ${i} - is member`,
type: 'guild',
privacy: 'public',
});
}));
// update group number 32 and not the first to make sure sorting works
await groups[32].update({name: 'guild with most members', memberCount: 199});
await groups[33].update({name: 'guild with less members', memberCount: -100});
let page0 = await expect(user.get('/groups?type=publicGuilds&paginate=true'))
.to.eventually.have.a.lengthOf(GUILD_PER_PAGE);
expect(page0[0].name).to.equal('guild with most members');
await expect(user.get('/groups?type=publicGuilds&paginate=true&page=1'))
.to.eventually.have.a.lengthOf(GUILD_PER_PAGE);
let page2 = await expect(user.get('/groups?type=publicGuilds&paginate=true&page=2'))
.to.eventually.have.a.lengthOf(1 + 2); // 1 created now, 2 by other tests
expect(page2[2].name).to.equal('guild with less members');
});
});
it('returns all the user\'s guilds when guilds passed in as query', async () => {
await expect(user.get('/groups?type=guilds'))
.to.eventually.have.a.lengthOf(NUMBER_OF_PUBLIC_GUILDS_USER_IS_MEMBER + NUMBER_OF_USERS_PRIVATE_GUILDS);
@@ -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']);
@@ -84,7 +84,7 @@ describe('GET /groups/:groupId/members', () => {
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
expect(Object.keys(memberRes.preferences).sort()).to.eql([
'size', 'hair', 'skin', 'shirt',
'chair', 'costume', 'sleep', 'background',
'chair', 'costume', 'sleep', 'background', 'tasks',
].sort());
expect(memberRes.stats.maxMP).to.exist;
@@ -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;
});
});
@@ -74,6 +74,18 @@ describe('POST /group', () => {
expect(updatedUser.guilds).to.include(guild._id);
});
it('awards the Joined Guild achievement', async () => {
await user.post('/groups', {
name: 'some guild',
type: 'guild',
privacy: 'public',
});
let updatedUser = await user.get('/user');
expect(updatedUser.achievements.joinedGuild).to.eql(true);
});
context('public guild', () => {
it('creates a group', async () => {
let groupName = 'Test Public Guild';
@@ -68,6 +68,12 @@ describe('POST /group/:groupId/join', () => {
await expect(joiningUser.get(`/groups/${publicGuild._id}`)).to.eventually.have.property('memberCount', oldMemberCount + 1);
});
it('awards Joined Guild achievement', async () => {
await joiningUser.post(`/groups/${publicGuild._id}/join`);
await expect(joiningUser.get('/user')).to.eventually.have.deep.property('achievements.joinedGuild', true);
});
});
context('Joining a private guild', () => {
@@ -147,8 +153,14 @@ describe('POST /group/:groupId/join', () => {
}),
};
expect(inviter.notifications[0].type).to.eql('GROUP_INVITE_ACCEPTED');
expect(inviter.notifications[0].data).to.eql(expectedData);
expect(inviter.notifications[1].type).to.eql('GROUP_INVITE_ACCEPTED');
expect(inviter.notifications[1].data).to.eql(expectedData);
});
it('awards Joined Guild achievement', async () => {
await invitedUser.post(`/groups/${guild._id}/join`);
await expect(invitedUser.get('/user')).to.eventually.have.deep.property('achievements.joinedGuild', true);
});
});
});
@@ -78,7 +78,7 @@ describe('POST /groups/:groupId/leave', () => {
expect(leader.newMessages[groupToLeave._id]).to.be.empty;
});
context('With challenges', () => {
context('with challenges', () => {
let challenge;
beforeEach(async () => {
@@ -106,10 +106,25 @@ describe('POST /groups/:groupId/leave', () => {
let userWithChallengeTasks = await leader.get('/user');
expect(userWithChallengeTasks.challenges).to.not.include(challenge._id);
// @TODO find elegant way to assert against the task existing
expect(userWithChallengeTasks.tasksOrder.habits).to.not.be.empty;
});
it('keeps the user in the challenge when the keepChallenges parameter is set to remain-in-challenges', async () => {
await leader.post(`/groups/${groupToLeave._id}/leave`, {keepChallenges: 'remain-in-challenges'});
let userWithChallengeTasks = await leader.get('/user');
expect(userWithChallengeTasks.challenges).to.include(challenge._id);
});
it('drops the user in the challenge when the keepChallenges parameter isn\'t set', async () => {
await leader.post(`/groups/${groupToLeave._id}/leave`);
let userWithChallengeTasks = await leader.get('/user');
expect(userWithChallengeTasks.challenges).to.not.include(challenge._id);
});
});
it('prevents quest leader from leaving a groupToLeave');
@@ -1,10 +1,14 @@
import {
generateUser,
generateGroup,
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;
@@ -12,7 +16,7 @@ describe('Post /groups/:groupId/invite', () => {
let groupName = 'Test Public Guild';
beforeEach(async () => {
inviter = await generateUser({balance: 1});
inviter = await generateUser({balance: 4});
group = await inviter.post('/groups', {
name: groupName,
type: 'guild',
@@ -203,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 () => {
@@ -217,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);
});
});
@@ -265,6 +296,25 @@ describe('Post /groups/:groupId/invite', () => {
expect(invitedUser.invitations.guilds[0].id).to.equal(group._id);
expect(invite).to.exist;
});
it('invites marks invite with cancelled plan', async () => {
let cancelledPlanGroup = await generateGroup(inviter, {
type: 'guild',
name: generateUUID(),
});
await cancelledPlanGroup.createCancelledSubscription();
let newUser = await generateUser();
let invite = await inviter.post(`/groups/${cancelledPlanGroup._id}/invite`, {
uuids: [newUser._id],
emails: [{name: 'test', email: 'test@habitica.com'}],
});
let invitedUser = await newUser.get('/user');
expect(invitedUser.invitations.guilds[0].id).to.equal(cancelledPlanGroup._id);
expect(invitedUser.invitations.guilds[0].cancelledPlan).to.be.true;
expect(invite).to.exist;
});
});
describe('guild invites', () => {
@@ -301,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();
@@ -390,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;
});
});
});
@@ -43,4 +43,15 @@ describe('PUT /group', () => {
expect(updatedGroup.leader.profile.name).to.eql(leader.profile.name);
expect(updatedGroup.name).to.equal(groupUpdatedName);
});
it('allows a leader to change leaders', async () => {
let updatedGroup = await leader.put(`/groups/${groupToUpdate._id}`, {
name: groupUpdatedName,
leader: nonLeader._id,
});
expect(updatedGroup.leader._id).to.eql(nonLeader._id);
expect(updatedGroup.leader.profile.name).to.eql(nonLeader.profile.name);
expect(updatedGroup.name).to.equal(groupUpdatedName);
});
});
@@ -37,7 +37,7 @@ describe('GET /members/:memberId', () => {
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
expect(Object.keys(memberRes.preferences).sort()).to.eql([
'size', 'hair', 'skin', 'shirt',
'chair', 'costume', 'sleep', 'background',
'chair', 'costume', 'sleep', 'background', 'tasks',
].sort());
expect(memberRes.stats.maxMP).to.exist;
@@ -208,6 +208,20 @@ describe('POST /tasks/:id/score/:direction', () => {
expect(task.completed).to.equal(false);
});
it('computes isDue', async () => {
await user.post(`/tasks/${daily._id}/score/up`);
let task = await user.get(`/tasks/${daily._id}`);
expect(task.isDue).to.equal(true);
});
it('computes nextDue', async () => {
await user.post(`/tasks/${daily._id}/score/up`);
let task = await user.get(`/tasks/${daily._id}`);
expect(task.nextDue.length).to.eql(6);
});
it('scores up daily even if it is already completed'); // Yes?
it('scores down daily even if it is already uncompleted'); // Yes?
@@ -315,6 +329,30 @@ describe('POST /tasks/:id/score/:direction', () => {
expect(updatedUser.stats.gp).to.be.greaterThan(user.stats.gp);
});
it('adds score notes to task', async () => {
let scoreNotesString = 'test-notes';
await user.post(`/tasks/${habit._id}/score/up`, {
scoreNotes: scoreNotesString,
});
let updatedTask = await user.get(`/tasks/${habit._id}`);
expect(updatedTask.history[0].scoreNotes).to.eql(scoreNotesString);
});
it('errors when score notes are too large', async () => {
let scoreNotesString = new Array(258).join('a');
await expect(user.post(`/tasks/${habit._id}/score/up`, {
scoreNotes: scoreNotesString,
}))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskScoreNotesTooLong'),
});
});
});
context('reward', () => {
@@ -496,6 +496,8 @@ describe('POST /tasks/user', () => {
frequency: 'daily',
everyX: 5,
startDate: now,
daysOfMonth: [15],
weeksOfMonth: [3],
});
expect(task.userId).to.equal(user._id);
@@ -504,7 +506,11 @@ describe('POST /tasks/user', () => {
expect(task.type).to.eql('daily');
expect(task.frequency).to.eql('daily');
expect(task.everyX).to.eql(5);
expect(task.daysOfMonth).to.eql([15]);
expect(task.weeksOfMonth).to.eql([3]);
expect(new Date(task.startDate)).to.eql(now);
expect(task.isDue).to.be.true;
expect(task.nextDue.length).to.eql(6);
});
it('creates multiple dailys', async () => {
@@ -1,3 +1,4 @@
import moment from 'moment';
import {
generateUser,
generateGroup,
@@ -395,12 +396,15 @@ describe('PUT /tasks/:id', () => {
notes: 'some new notes',
frequency: 'daily',
everyX: 5,
startDate: moment().add(1, 'days').toDate(),
});
expect(savedDaily.text).to.eql('some new text');
expect(savedDaily.notes).to.eql('some new notes');
expect(savedDaily.frequency).to.eql('daily');
expect(savedDaily.everyX).to.eql(5);
expect(savedDaily.isDue).to.be.false;
expect(savedDaily.nextDue.length).to.eql(6);
});
it('can update checklists (replace it)', async () => {
@@ -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(2);
expect(user.notifications[1].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(1);
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',
@@ -51,18 +52,55 @@ describe('POST /tasks/:id/score/:direction', () => {
await user.sync();
expect(user.notifications.length).to.equal(1);
expect(user.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
expect(user.notifications[0].data.message).to.equal(t('userHasRequestedTaskApproval', {
expect(user.notifications.length).to.equal(2);
expect(user.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
expect(user.notifications[1].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);
expect(user.notifications[1].data.groupId).to.equal(guild._id);
expect(updatedTask.group.approval.requested).to.equal(true);
expect(updatedTask.group.approval.requestedDate).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
});
it('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(2);
expect(user.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
expect(user.notifications[1].data.message).to.equal(t('userHasRequestedTaskApproval', {
user: member.auth.local.username,
taskName: updatedTask.text,
taskId: updatedTask._id,
}));
expect(user.notifications[1].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);
});
});
@@ -3,6 +3,7 @@ import {
createAndPopulateGroup,
generateGroup,
generateUser,
generateChallenge,
translate as t,
} from '../../../../helpers/api-integration/v3';
import {
@@ -15,6 +16,7 @@ import {
sha1MakeSalt,
sha1Encrypt as sha1EncryptPassword,
} from '../../../../../website/server/libs/password';
import * as email from '../../../../../website/server/libs/email';
describe('DELETE /user', () => {
let user;
@@ -24,7 +26,7 @@ describe('DELETE /user', () => {
user = await generateUser({balance: 10});
});
it('returns an errors if password is wrong', async () => {
it('returns an error if password is wrong', async () => {
await expect(user.del('/user', {
password: 'wrong-password',
})).to.eventually.be.rejected.and.eql({
@@ -34,6 +36,33 @@ describe('DELETE /user', () => {
});
});
it('returns an error if password is not supplied', async () => {
await expect(user.del('/user', {
password: '',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingPassword'),
});
});
it('returns an error if excessive feedback is supplied', async () => {
let feedbackText = 'spam feedback ';
let feedback = feedbackText;
while (feedback.length < 10000) {
feedback = feedback + feedbackText;
}
await expect(user.del('/user', {
password,
feedback,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Account deletion feedback is limited to 10,000 characters. For lengthy feedback, email admin@habitica.com.',
});
});
it('returns an error if user has active subscription', async () => {
let userWithSubscription = await generateUser({'purchased.plan.customerId': 'fake-customer-id'});
@@ -64,6 +93,30 @@ describe('DELETE /user', () => {
}));
});
it('reduces memberCount in challenges user is linked to', async () => {
let populatedGroup = await createAndPopulateGroup({
members: 2,
});
let group = populatedGroup.group;
let authorizedUser = populatedGroup.members[1];
let challenge = await generateChallenge(populatedGroup.groupLeader, group);
await authorizedUser.post(`/challenges/${challenge._id}/join`);
await challenge.sync();
expect(challenge.memberCount).to.eql(2);
await authorizedUser.del('/user', {
password,
});
await challenge.sync();
expect(challenge.memberCount).to.eql(1);
});
it('deletes the user', async () => {
await user.del('/user', {
password,
@@ -71,6 +124,32 @@ describe('DELETE /user', () => {
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
});
it('sends feedback to the admin email', async () => {
sandbox.spy(email, 'sendTxn');
let feedback = 'Reasons for Deletion';
await user.del('/user', {
password,
feedback,
});
expect(email.sendTxn).to.be.calledOnce;
sandbox.restore();
});
it('does not send email if no feedback is supplied', async () => {
sandbox.spy(email, 'sendTxn');
await user.del('/user', {
password,
});
expect(email.sendTxn).to.not.be.called;
sandbox.restore();
});
it('deletes the user with a legacy sha1 password', async () => {
let textPassword = 'mySecretPassword';
let salt = sha1MakeSalt();
@@ -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 }),
});
});
@@ -5,6 +5,7 @@ import {
createAndPopulateGroup,
getProperty,
} from '../../../../../helpers/api-integration/v3';
import { ApiUser } from '../../../../../helpers/api-integration/api-classes';
import { v4 as generateRandomUserName } from 'uuid';
import { each } from 'lodash';
import { encrypt } from '../../../../../../website/server/libs/encryption';
@@ -33,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 () => {
@@ -66,10 +251,26 @@ describe('POST /user/auth/local/register', () => {
confirmPassword: password,
});
await expect(getProperty('users', user._id, '_ABtest')).to.eventually.be.a('string');
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`;
@@ -416,5 +617,37 @@ describe('POST /user/auth/local/register', () => {
expect(user.tags).to.not.be.empty;
});
it('adds the correct tags to the correct tasks', async () => {
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 todos = await requests.get('/tasks/user?type=todos');
function findTag (tagName) {
let tag = user.tags.find((userTag) => {
return userTag.name === t(tagName);
});
return tag.id;
}
expect(habits[0].tags).to.have.a.lengthOf(3);
expect(habits[0].tags).to.include.members(['defaultTag1', 'defaultTag4', 'defaultTag6'].map(findTag));
expect(habits[1].tags).to.have.a.lengthOf(1);
expect(habits[1].tags).to.include.members(['defaultTag3'].map(findTag));
expect(habits[2].tags).to.have.a.lengthOf(2);
expect(habits[2].tags).to.include.members(['defaultTag2', 'defaultTag3'].map(findTag));
expect(todos[0].tags).to.have.a.lengthOf(0);
});
});
});
@@ -83,7 +83,7 @@ describe('POST /user/auth/social', () => {
network,
});
await expect(getProperty('users', user._id, '_ABtest')).to.eventually.be.a('string');
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
});
});
@@ -139,7 +139,7 @@ describe('POST /user/auth/social', () => {
network,
});
await expect(getProperty('users', user._id, '_ABtest')).to.eventually.be.a('string');
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
});
});
});
@@ -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') }),
});
});
+151 -9
View File
@@ -1,10 +1,12 @@
import moment from 'moment';
import cc from 'coupon-code';
import uuid from 'uuid';
import {
generateGroup,
} from '../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../website/server/models/user';
import { model as Group } from '../../../../../website/server/models/group';
import { model as Coupon } from '../../../../../website/server/models/coupon';
import amzLib from '../../../../../website/server/libs/amazonPayments';
import payments from '../../../../../website/server/libs/payments';
@@ -105,7 +107,7 @@ describe('Amazon Payments', () => {
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
paymentMethod: amzLib.constants.PAYMENT_METHOD,
headers,
});
expectAmazonStubs();
@@ -128,7 +130,7 @@ describe('Amazon Payments', () => {
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON_GIFT,
paymentMethod: amzLib.constants.PAYMENT_METHOD_GIFT,
headers,
gift,
});
@@ -153,7 +155,7 @@ describe('Amazon Payments', () => {
expect(paymentCreateSubscritionStub).to.be.calledOnce;
expect(paymentCreateSubscritionStub).to.be.calledWith({
user,
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON_GIFT,
paymentMethod: amzLib.constants.PAYMENT_METHOD_GIFT,
headers,
gift,
});
@@ -316,7 +318,7 @@ describe('Amazon Payments', () => {
expect(createSubSpy).to.be.calledWith({
user,
customerId: billingAgreementId,
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
paymentMethod: amzLib.constants.PAYMENT_METHOD,
sub,
headers,
groupId,
@@ -375,7 +377,73 @@ describe('Amazon Payments', () => {
expect(createSubSpy).to.be.calledWith({
user,
customerId: billingAgreementId,
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
paymentMethod: amzLib.constants.PAYMENT_METHOD,
sub,
headers,
groupId,
});
});
it('subscribes with amazon with price to existing users', async () => {
user = new User();
user.guilds.push(groupId);
await user.save();
group.memberCount = 2;
await group.save();
sub.key = 'group_monthly';
sub.price = 9;
amount = 12;
await amzLib.subscribe({
billingAgreementId,
sub,
coupon,
user,
groupId,
headers,
});
expect(amazonSetBillingAgreementDetailsSpy).to.be.calledOnce;
expect(amazonSetBillingAgreementDetailsSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
BillingAgreementAttributes: {
SellerNote: amzLib.constants.SELLER_NOTE_SUBSCRIPTION,
SellerBillingAgreementAttributes: {
SellerBillingAgreementId: common.uuid(),
StoreName: amzLib.constants.STORE_NAME,
CustomInformation: amzLib.constants.SELLER_NOTE_SUBSCRIPTION,
},
},
});
expect(amazonConfirmBillingAgreementSpy).to.be.calledOnce;
expect(amazonConfirmBillingAgreementSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
});
expect(amazongAuthorizeOnBillingAgreementSpy).to.be.calledOnce;
expect(amazongAuthorizeOnBillingAgreementSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
AuthorizationReferenceId: common.uuid().substring(0, 32),
AuthorizationAmount: {
CurrencyCode: amzLib.constants.CURRENCY_CODE,
Amount: amount,
},
SellerAuthorizationNote: amzLib.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION,
TransactionTimeout: 0,
CaptureNow: true,
SellerNote: amzLib.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION,
SellerOrderAttributes: {
SellerOrderId: common.uuid(),
StoreName: amzLib.constants.STORE_NAME,
},
});
expect(createSubSpy).to.be.calledOnce;
expect(createSubSpy).to.be.calledWith({
user,
customerId: billingAgreementId,
paymentMethod: amzLib.constants.PAYMENT_METHOD,
sub,
headers,
groupId,
@@ -455,7 +523,7 @@ describe('Amazon Payments', () => {
user,
groupId: undefined,
nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
paymentMethod: amzLib.constants.PAYMENT_METHOD,
headers,
});
expectAmazonStubs();
@@ -485,7 +553,7 @@ describe('Amazon Payments', () => {
user,
groupId: undefined,
nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
paymentMethod: amzLib.constants.PAYMENT_METHOD,
headers,
});
amzLib.closeBillingAgreement.restore();
@@ -523,7 +591,7 @@ describe('Amazon Payments', () => {
user,
groupId: group._id,
nextBill: moment(group.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
paymentMethod: amzLib.constants.PAYMENT_METHOD,
headers,
});
expectAmazonStubs();
@@ -553,10 +621,84 @@ describe('Amazon Payments', () => {
user,
groupId: group._id,
nextBill: moment(group.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: amzLib.constants.PAYMENT_METHOD_AMAZON,
paymentMethod: amzLib.constants.PAYMENT_METHOD,
headers,
});
amzLib.closeBillingAgreement.restore();
});
});
describe('#upgradeGroupPlan', () => {
let spy, data, user, group, uuidString;
beforeEach(async function () {
user = new User();
user.profile.name = 'sender';
data = {
user,
sub: {
key: 'basic_3mo', // @TODO: Validate that this is group
},
customerId: 'customer-id',
paymentMethod: 'Payment Method',
headers: {
'x-client': 'habitica-web',
'user-agent': '',
},
};
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
await group.save();
spy = sinon.stub(amzLib, 'authorizeOnBillingAgreement');
spy.returnsPromise().resolves([]);
uuidString = 'uuid-v4';
sinon.stub(uuid, 'v4').returns(uuidString);
data.groupId = group._id;
data.sub.quantity = 3;
});
afterEach(function () {
sinon.restore(amzLib.authorizeOnBillingAgreement);
uuid.v4.restore();
});
it('charges for a new member', async () => {
data.paymentMethod = amzLib.constants.PAYMENT_METHOD;
await payments.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
updatedGroup.memberCount += 1;
await updatedGroup.save();
await amzLib.chargeForAdditionalGroupMember(updatedGroup);
expect(spy.calledOnce).to.be.true;
expect(spy).to.be.calledWith({
AmazonBillingAgreementId: updatedGroup.purchased.plan.customerId,
AuthorizationReferenceId: uuidString.substring(0, 32),
AuthorizationAmount: {
CurrencyCode: amzLib.constants.CURRENCY_CODE,
Amount: 3,
},
SellerAuthorizationNote: amzLib.constants.SELLER_NOTE_GROUP_NEW_MEMBER,
TransactionTimeout: 0,
CaptureNow: true,
SellerNote: amzLib.constants.SELLER_NOTE_GROUP_NEW_MEMBER,
SellerOrderAttributes: {
SellerOrderId: uuidString,
StoreName: amzLib.constants.STORE_NAME,
},
});
});
});
});
+31
View File
@@ -0,0 +1,31 @@
import apiMessages from '../../../../../website/server/libs/apiMessages';
describe('API Messages', () => {
const message = 'Only public guilds support pagination.';
it('returns an API message', () => {
expect(apiMessages('guildsOnlyPaginate')).to.equal(message);
});
it('throws if the API message does not exist', () => {
expect(() => apiMessages('iDoNotExist')).to.throw;
});
it('clones the passed variables', () => {
let vars = {a: 1};
sandbox.stub(_, 'clone').returns({});
apiMessages('guildsOnlyPaginate', vars);
expect(_.clone).to.have.been.called.once;
expect(_.clone).to.have.been.calledWith(vars);
});
it('pass the message through _.template', () => {
let vars = {a: 1};
let stub = sinon.stub().returns('string');
sandbox.stub(_, 'template').returns(stub);
apiMessages('guildsOnlyPaginate', vars);
expect(_.template).to.have.been.called.once;
expect(_.template).to.have.been.calledWith(message);
expect(stub).to.have.been.called.once;
expect(stub).to.have.been.calledWith(vars);
});
});
+100 -3
View File
@@ -6,7 +6,6 @@ import requireAgain from 'require-again';
import { recoverCron, cron } from '../../../../../website/server/libs/cron';
import { model as User } from '../../../../../website/server/models/user';
import * as Tasks from '../../../../../website/server/models/task';
import { clone } from 'lodash';
import common from '../../../../../website/common';
import analytics from '../../../../../website/server/libs/analyticsService';
@@ -135,7 +134,10 @@ describe('cron', () => {
user.purchased.plan.dateUpdated = moment().subtract(6, 'months').toDate();
user.purchased.plan.dateTerminated = moment().subtract(3, 'months').toDate();
user.purchased.plan.consecutive.count = 5;
user.purchased.plan.consecutive.trinkets = 1;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.trinkets).to.equal(1);
});
@@ -311,6 +313,24 @@ describe('cron', () => {
expect(tasksByType.dailys[0].completed).to.be.false;
expect(user.stats.hp).to.equal(healthBefore);
});
it('sets isDue for daily', () => {
let daily = {
text: 'test daily',
type: 'daily',
frequency: 'daily',
everyX: 5,
startDate: new Date(),
};
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line new-cap
tasksByType.dailys.push(task);
tasksByType.dailys[0].completed = true;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].isDue).to.be.exist;
});
});
describe('todos', () => {
@@ -356,6 +376,22 @@ describe('cron', () => {
};
});
it('computes isDue', () => {
tasksByType.dailys[0].frequency = 'daily';
tasksByType.dailys[0].everyX = 5;
tasksByType.dailys[0].startDate = moment().add(1, 'days').toDate();
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].isDue).to.be.false;
});
it('computes nextDue', () => {
tasksByType.dailys[0].frequency = 'daily';
tasksByType.dailys[0].everyX = 5;
tasksByType.dailys[0].startDate = moment().add(1, 'days').toDate();
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].nextDue.length).to.eql(6);
});
it('should add history', () => {
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].history).to.be.lengthOf(1);
@@ -481,6 +517,67 @@ describe('cron', () => {
expect(tasksByType.habits[0].value).to.equal(1);
});
describe('counters', () => {
let notStartOfWeekOrMonth = new Date(2016, 9, 28).getTime(); // a Friday
let clock;
beforeEach(() => {
// Replace system clocks so we can get predictable results
clock = sinon.useFakeTimers(notStartOfWeekOrMonth);
});
afterEach(() => {
return clock.restore();
});
it('should reset a daily habit counter each day', () => {
tasksByType.habits[0].counterUp = 1;
tasksByType.habits[0].counterDown = 1;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.habits[0].counterUp).to.equal(0);
expect(tasksByType.habits[0].counterDown).to.equal(0);
});
it('should reset a weekly habit counter each Monday', () => {
tasksByType.habits[0].frequency = 'weekly';
tasksByType.habits[0].counterUp = 1;
tasksByType.habits[0].counterDown = 1;
// should not reset
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.habits[0].counterUp).to.equal(1);
expect(tasksByType.habits[0].counterDown).to.equal(1);
// should reset
daysMissed = 8;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.habits[0].counterUp).to.equal(0);
expect(tasksByType.habits[0].counterDown).to.equal(0);
});
it('should reset a monthly habit counter the first day of each month', () => {
tasksByType.habits[0].frequency = 'monthly';
tasksByType.habits[0].counterUp = 1;
tasksByType.habits[0].counterDown = 1;
// should not reset
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.habits[0].counterUp).to.equal(1);
expect(tasksByType.habits[0].counterDown).to.equal(1);
// should reset
daysMissed = 32;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.habits[0].counterUp).to.equal(0);
expect(tasksByType.habits[0].counterDown).to.equal(0);
});
});
});
describe('perfect day', () => {
@@ -533,7 +630,7 @@ describe('cron', () => {
tasksByType.dailys[0].completed = true;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
let previousBuffs = clone(user.stats.buffs);
let previousBuffs = user.stats.buffs.toObject();
cron({user, tasksByType, daysMissed, analytics});
@@ -598,7 +695,7 @@ describe('cron', () => {
tasksByType.dailys[0].completed = false;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
let previousBuffs = clone(user.stats.buffs);
let previousBuffs = user.stats.buffs.toObject();
cronOverride({user, tasksByType, daysMissed, analytics});
+35 -198
View File
@@ -1,22 +1,18 @@
import moment from 'moment';
import * as sender from '../../../../../website/server/libs/email';
import * as api from '../../../../../website/server/libs/payments';
import analytics from '../../../../../website/server/libs/analyticsService';
import notifications from '../../../../../website/server/libs/pushNotifications';
import { model as User } from '../../../../../website/server/models/user';
import { model as Group } from '../../../../../website/server/models/group';
import stripeModule from 'stripe';
import moment from 'moment';
import { translate as t } from '../../../../helpers/api-v3-integration.helper';
import {
generateGroup,
} from '../../../../helpers/api-unit.helper.js';
import i18n from '../../../../../website/common/script/i18n';
describe('payments/index', () => {
let user, group, data, plan;
let stripe = stripeModule('test');
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
@@ -87,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;
@@ -245,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');
@@ -319,53 +327,6 @@ describe('payments/index', () => {
});
});
context('Purchasing a subscription for group', () => {
it('creates a subscription', async () => {
expect(group.purchased.plan.planId).to.not.exist;
data.groupId = group._id;
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.planId).to.eql('basic_3mo');
expect(updatedGroup.purchased.plan.customerId).to.eql('customer-id');
expect(updatedGroup.purchased.plan.dateUpdated).to.exist;
expect(updatedGroup.purchased.plan.gemsBought).to.eql(0);
expect(updatedGroup.purchased.plan.paymentMethod).to.eql('Payment Method');
expect(updatedGroup.purchased.plan.extraMonths).to.eql(0);
expect(updatedGroup.purchased.plan.dateTerminated).to.eql(null);
expect(updatedGroup.purchased.plan.lastBillingDate).to.not.exist;
expect(updatedGroup.purchased.plan.dateCreated).to.exist;
});
it('sets extraMonths if plan has dateTerminated date', async () => {
group.purchased.plan = plan;
group.purchased.plan.dateTerminated = moment(new Date()).add(2, 'months');
await group.save();
expect(group.purchased.plan.extraMonths).to.eql(0);
data.groupId = group._id;
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.extraMonths).to.within(1.9, 2);
});
it('does not set negative extraMonths if plan has past dateTerminated date', async () => {
group.purchased.plan = plan;
group.purchased.plan.dateTerminated = moment(new Date()).subtract(2, 'months');
await group.save();
expect(group.purchased.plan.extraMonths).to.eql(0);
data.groupId = group._id;
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.extraMonths).to.eql(0);
});
});
context('Block subscription perks', () => {
it('adds block months to plan.consecutive.offset', async () => {
await api.createSubscription(data);
@@ -485,7 +446,6 @@ describe('payments/index', () => {
sandbox.spy(user.purchased.plan.mysteryItems, 'push');
data = { paymentMethod: 'PaymentMethod', user, sub: { key: 'basic_3mo' } };
await api.createSubscription(data);
expect(user.purchased.plan.mysteryItems.push).to.be.calledOnce;
@@ -559,112 +519,6 @@ describe('payments/index', () => {
expect(sender.sendTxn).to.be.calledWith(user, 'cancel-subscription');
});
});
context('Canceling a subscription for group', () => {
it('adds a month termination date by default', async () => {
data.groupId = group._id;
await api.cancelSubscription(data);
let now = new Date();
let updatedGroup = await Group.findById(group._id).exec();
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
expect(daysTillTermination).to.be.within(29, 30); // 1 month +/- 1 days
});
it('adds extraMonths to dateTerminated value', async () => {
group.purchased.plan.extraMonths = 2;
await group.save();
data.groupId = group._id;
await api.cancelSubscription(data);
let now = new Date();
let updatedGroup = await Group.findById(group._id).exec();
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
expect(daysTillTermination).to.be.within(89, 90); // 3 months +/- 1 days
});
it('handles extra month fractions', async () => {
group.purchased.plan.extraMonths = 0.3;
await group.save();
data.groupId = group._id;
await api.cancelSubscription(data);
let now = new Date();
let updatedGroup = await Group.findById(group._id).exec();
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
expect(daysTillTermination).to.be.within(38, 39); // should be about 1 month + 1/3 month
});
it('terminates at next billing date if it exists', async () => {
data.nextBill = moment().add({ days: 15 });
data.groupId = group._id;
await api.cancelSubscription(data);
let now = new Date();
let updatedGroup = await Group.findById(group._id).exec();
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
expect(daysTillTermination).to.be.within(13, 15);
});
it('resets plan.extraMonths', async () => {
group.purchased.plan.extraMonths = 5;
await group.save();
data.groupId = group._id;
await api.cancelSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.extraMonths).to.eql(0);
});
it('sends an email', async () => {
data.groupId = group._id;
await api.cancelSubscription(data);
expect(sender.sendTxn).to.be.calledOnce;
expect(sender.sendTxn).to.be.calledWith(user, 'group-cancel-subscription');
});
it('prevents non group leader from manging subscription', async () => {
let groupMember = new User();
data.user = groupMember;
data.groupId = group._id;
await expect(api.cancelSubscription(data))
.eventually.be.rejected.and.to.eql({
httpCode: 401,
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
name: 'NotAuthorized',
});
});
it('allows old group leader to cancel if they created the subscription', async () => {
data.groupId = group._id;
data.sub = {
key: 'group_monthly',
};
data.paymentMethod = 'Payment Method';
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
let newLeader = new User();
updatedGroup.leader = newLeader._id;
await updatedGroup.save();
await api.cancelSubscription(data);
updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.dateTerminated).to.exist;
});
});
});
describe('#buyGems', () => {
@@ -772,49 +626,32 @@ describe('payments/index', () => {
});
});
describe('#upgradeGroupPlan', () => {
let spy;
beforeEach(function () {
spy = sinon.stub(stripe.subscriptions, 'update');
spy.returnsPromise().resolves([]);
describe('addSubToGroupUser', () => {
it('adds a group subscription to a new user', async () => {
expect(group.purchased.plan.planId).to.not.exist;
data.groupId = group._id;
data.sub.quantity = 3;
await api.addSubToGroupUser(user, group);
let updatedUser = await User.findById(user._id).exec();
expect(updatedUser.purchased.plan.planId).to.eql('group_plan_auto');
expect(updatedUser.purchased.plan.customerId).to.eql('group-plan');
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan');
expect(updatedUser.purchased.plan.extraMonths).to.eql(0);
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
expect(updatedUser.purchased.plan.dateCreated).to.exist;
});
afterEach(function () {
sinon.restore(stripe.subscriptions.update);
});
it('awards the Royal Purple Jackalope pet', async () => {
await api.addSubToGroupUser(user, group);
it('updates a group plan quantity', async () => {
data.paymentMethod = 'Stripe';
await api.createSubscription(data);
let updatedUser = await User.findById(user._id).exec();
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.quantity).to.eql(3);
updatedGroup.memberCount += 1;
await updatedGroup.save();
await api.updateStripeGroupPlan(updatedGroup, stripe);
expect(spy.calledOnce).to.be.true;
expect(updatedGroup.purchased.plan.quantity).to.eql(4);
});
it('does not update a group plan quantity that has a payment method other than stripe', async () => {
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.quantity).to.eql(3);
updatedGroup.memberCount += 1;
await updatedGroup.save();
await api.updateStripeGroupPlan(updatedGroup, stripe);
expect(spy.calledOnce).to.be.false;
expect(updatedGroup.purchased.plan.quantity).to.eql(3);
expect(updatedUser.items.pets['Jackalope-RoyalPurple']).to.eql(5);
});
});
});
@@ -0,0 +1,326 @@
import moment from 'moment';
import * as sender from '../../../../../../../website/server/libs/email';
import * as api from '../../../../../../../website/server/libs/payments';
import { model as User } from '../../../../../../../website/server/models/user';
import { model as Group } from '../../../../../../../website/server/models/group';
import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
import i18n from '../../../../../../../website/common/script/i18n';
describe('Canceling a subscription for group', () => {
let plan, group, user, data;
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
await user.save();
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
await group.save();
data = {
user,
sub: {
key: 'basic_3mo',
},
customerId: 'customer-id',
paymentMethod: 'Payment Method',
headers: {
'x-client': 'habitica-web',
'user-agent': '',
},
};
plan = {
planId: 'basic_3mo',
customerId: 'customer-id',
dateUpdated: new Date(),
gemsBought: 0,
paymentMethod: 'paymentMethod',
extraMonths: 0,
dateTerminated: null,
lastBillingDate: new Date(),
dateCreated: new Date(),
mysteryItems: [],
consecutive: {
trinkets: 0,
offset: 0,
gemCapExtra: 0,
},
};
sandbox.stub(sender, 'sendTxn');
});
afterEach(() => {
sender.sendTxn.restore();
});
it('adds a month termination date by default', async () => {
data.groupId = group._id;
await api.cancelSubscription(data);
let now = new Date();
let updatedGroup = await Group.findById(group._id).exec();
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
expect(daysTillTermination).to.be.within(29, 30); // 1 month +/- 1 days
});
it('adds extraMonths to dateTerminated value', async () => {
group.purchased.plan.extraMonths = 2;
await group.save();
data.groupId = group._id;
await api.cancelSubscription(data);
let now = new Date();
let updatedGroup = await Group.findById(group._id).exec();
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
expect(daysTillTermination).to.be.within(89, 90); // 3 months +/- 1 days
});
it('handles extra month fractions', async () => {
group.purchased.plan.extraMonths = 0.3;
await group.save();
data.groupId = group._id;
await api.cancelSubscription(data);
let now = new Date();
let updatedGroup = await Group.findById(group._id).exec();
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
expect(daysTillTermination).to.be.within(38, 39); // should be about 1 month + 1/3 month
});
it('terminates at next billing date if it exists', async () => {
data.nextBill = moment().add({ days: 15 });
data.groupId = group._id;
await api.cancelSubscription(data);
let now = new Date();
let updatedGroup = await Group.findById(group._id).exec();
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
expect(daysTillTermination).to.be.within(13, 15);
});
it('resets plan.extraMonths', async () => {
group.purchased.plan.extraMonths = 5;
await group.save();
data.groupId = group._id;
await api.cancelSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.extraMonths).to.eql(0);
});
it('sends an email', async () => {
data.groupId = group._id;
await api.cancelSubscription(data);
expect(sender.sendTxn).to.be.calledOnce;
expect(sender.sendTxn.firstCall.args[0]._id).to.equal(user._id);
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-cancel-subscription');
expect(sender.sendTxn.firstCall.args[2]).to.eql([
{name: 'GROUP_NAME', content: group.name},
]);
});
it('prevents non group leader from manging subscription', async () => {
let groupMember = new User();
data.user = groupMember;
data.groupId = group._id;
await expect(api.cancelSubscription(data))
.eventually.be.rejected.and.to.eql({
httpCode: 401,
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
name: 'NotAuthorized',
});
});
it('allows old group leader to cancel if they created the subscription', async () => {
data.groupId = group._id;
data.sub = {
key: 'group_monthly',
};
data.paymentMethod = 'Payment Method';
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
let newLeader = new User();
updatedGroup.leader = newLeader._id;
await updatedGroup.save();
await api.cancelSubscription(data);
updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.dateTerminated).to.exist;
});
it('cancels member subscriptions', async () => {
data = {
user,
sub: {
key: 'basic_3mo',
},
customerId: 'customer-id',
paymentMethod: 'Payment Method',
headers: {
'x-client': 'habitica-web',
'user-agent': '',
},
};
user.guilds.push(group._id);
await user.save();
expect(group.purchased.plan.planId).to.not.exist;
data.groupId = group._id;
await api.createSubscription(data);
await api.cancelSubscription(data);
let now = new Date();
now.setHours(0, 0, 0, 0);
let updatedLeader = await User.findById(user._id).exec();
let daysTillTermination = moment(updatedLeader.purchased.plan.dateTerminated).diff(now, 'days');
expect(daysTillTermination).to.be.within(2, 3); // only a few days
});
it('sends an email to members of group', async () => {
let recipient = new User();
recipient.profile.name = 'recipient';
recipient.guilds.push(group._id);
await recipient.save();
data.groupId = group._id;
await api.createSubscription(data);
await api.cancelSubscription(data);
expect(sender.sendTxn).to.be.have.callCount(4);
expect(sender.sendTxn.thirdCall.args[0]._id).to.equal(recipient._id);
expect(sender.sendTxn.thirdCall.args[1]).to.equal('group-member-cancel');
expect(sender.sendTxn.thirdCall.args[2]).to.eql([
{name: 'LEADER', content: user.profile.name},
{name: 'GROUP_NAME', content: group.name},
]);
});
it('does not cancel member subscriptions when member does not have a group plan sub (i.e. UNLIMITED_CUSTOMER_ID)', async () => {
plan.key = 'basic_earned';
plan.customerId = api.constants.UNLIMITED_CUSTOMER_ID;
let recipient = new User();
recipient.profile.name = 'recipient';
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
data.groupId = group._id;
await api.cancelSubscription(data);
let updatedLeader = await User.findById(user._id).exec();
expect(updatedLeader.purchased.plan.dateTerminated).to.not.exist;
});
it('does not cancel a user subscription if they are still in another active group plan', async () => {
let recipient = new User();
recipient.profile.name = 'recipient';
plan.key = 'basic_earned';
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();
let firstDateCreated = updatedUser.purchased.plan.dateCreated;
let extraMonthsBeforeSecond = updatedUser.purchased.plan.extraMonths;
let group2 = generateGroup({
name: 'test group2',
type: 'guild',
privacy: 'public',
leader: user._id,
});
data.groupId = group2._id;
await group2.save();
recipient.guilds.push(group2._id);
await recipient.save();
await api.createSubscription(data);
await api.cancelSubscription(data);
updatedUser = await User.findById(recipient._id).exec();
expect(updatedUser.purchased.plan.planId).to.eql('group_plan_auto');
expect(updatedUser.purchased.plan.customerId).to.eql('group-plan');
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan');
expect(updatedUser.purchased.plan.extraMonths).to.eql(extraMonthsBeforeSecond);
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
expect(updatedUser.purchased.plan.dateCreated).to.eql(firstDateCreated);
});
it('does cancel a leader subscription with two cancelled group plans', async () => {
user.guilds.push(group._id);
await user.save();
data.groupId = group._id;
await api.createSubscription(data);
let updatedUser = await User.findById(user._id).exec();
let firstDateCreated = updatedUser.purchased.plan.dateCreated;
let extraMonthsBeforeSecond = updatedUser.purchased.plan.extraMonths;
let group2 = generateGroup({
name: 'test group2',
type: 'guild',
privacy: 'public',
leader: user._id,
});
user.guilds.push(group2._id);
await user.save();
data.groupId = group2._id;
await group2.save();
await api.createSubscription(data);
await api.cancelSubscription(data);
data.groupId = group._id;
await api.cancelSubscription(data);
updatedUser = await User.findById(user._id).exec();
expect(updatedUser.purchased.plan.planId).to.eql('group_plan_auto');
expect(updatedUser.purchased.plan.customerId).to.eql('group-plan');
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan');
expect(updatedUser.purchased.plan.extraMonths).to.eql(extraMonthsBeforeSecond);
expect(updatedUser.purchased.plan.dateTerminated).to.exist;
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
expect(updatedUser.purchased.plan.dateCreated).to.eql(firstDateCreated);
});
});
@@ -0,0 +1,693 @@
import moment from 'moment';
import stripeModule from 'stripe';
import * as sender from '../../../../../../../website/server/libs/email';
import * as api from '../../../../../../../website/server/libs/payments';
import amzLib from '../../../../../../../website/server/libs/amazonPayments';
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
import { model as User } from '../../../../../../../website/server/models/user';
import { model as Group } from '../../../../../../../website/server/models/group';
import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
describe('Purchasing a subscription for group', () => {
let plan, group, user, data;
let stripe = stripeModule('test');
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
await user.save();
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
await group.save();
data = {
user,
sub: {
key: 'basic_3mo',
},
customerId: 'customer-id',
paymentMethod: 'Payment Method',
headers: {
'x-client': 'habitica-web',
'user-agent': '',
},
};
plan = {
planId: 'basic_3mo',
customerId: 'customer-id',
dateUpdated: new Date(),
gemsBought: 0,
paymentMethod: 'paymentMethod',
extraMonths: 0,
dateTerminated: null,
lastBillingDate: new Date(),
dateCreated: new Date(),
mysteryItems: [],
consecutive: {
trinkets: 0,
offset: 0,
gemCapExtra: 0,
},
};
let subscriptionId = 'subId';
sinon.stub(stripe.customers, 'del').returnsPromise().resolves({});
let currentPeriodEndTimeStamp = moment().add(3, 'months').unix();
sinon.stub(stripe.customers, 'retrieve')
.returnsPromise().resolves({
subscriptions: {
data: [{id: subscriptionId, current_period_end: currentPeriodEndTimeStamp}], // eslint-disable-line camelcase
},
});
stripePayments.setStripeApi(stripe);
sandbox.stub(sender, 'sendTxn');
});
afterEach(() => {
stripe.customers.del.restore();
stripe.customers.retrieve.restore();
sender.sendTxn.restore();
});
it('creates a subscription', async () => {
expect(group.purchased.plan.planId).to.not.exist;
data.groupId = group._id;
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.planId).to.eql('basic_3mo');
expect(updatedGroup.purchased.plan.customerId).to.eql('customer-id');
expect(updatedGroup.purchased.plan.dateUpdated).to.exist;
expect(updatedGroup.purchased.plan.gemsBought).to.eql(0);
expect(updatedGroup.purchased.plan.paymentMethod).to.eql('Payment Method');
expect(updatedGroup.purchased.plan.extraMonths).to.eql(0);
expect(updatedGroup.purchased.plan.dateTerminated).to.eql(null);
expect(updatedGroup.purchased.plan.lastBillingDate).to.not.exist;
expect(updatedGroup.purchased.plan.dateCreated).to.exist;
});
it('sends an email', async () => {
expect(group.purchased.plan.planId).to.not.exist;
data.groupId = group._id;
await api.createSubscription(data);
expect(sender.sendTxn).to.be.calledWith(user, 'group-subscription-begins');
});
it('sets extraMonths if plan has dateTerminated date', async () => {
group.purchased.plan = plan;
group.purchased.plan.dateTerminated = moment(new Date()).add(2, 'months');
await group.save();
expect(group.purchased.plan.extraMonths).to.eql(0);
data.groupId = group._id;
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.extraMonths).to.within(1.9, 2);
});
it('does not set negative extraMonths if plan has past dateTerminated date', async () => {
group.purchased.plan = plan;
group.purchased.plan.dateTerminated = moment(new Date()).subtract(2, 'months');
await group.save();
expect(group.purchased.plan.extraMonths).to.eql(0);
data.groupId = group._id;
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.extraMonths).to.eql(0);
});
it('grants all members of a group a subscription', async () => {
user.guilds.push(group._id);
await user.save();
expect(group.purchased.plan.planId).to.not.exist;
data.groupId = group._id;
await api.createSubscription(data);
let updatedLeader = await User.findById(user._id).exec();
expect(updatedLeader.purchased.plan.planId).to.eql('group_plan_auto');
expect(updatedLeader.purchased.plan.customerId).to.eql('group-plan');
expect(updatedLeader.purchased.plan.dateUpdated).to.exist;
expect(updatedLeader.purchased.plan.gemsBought).to.eql(0);
expect(updatedLeader.purchased.plan.paymentMethod).to.eql('Group Plan');
expect(updatedLeader.purchased.plan.extraMonths).to.eql(0);
expect(updatedLeader.purchased.plan.dateTerminated).to.eql(null);
expect(updatedLeader.purchased.plan.lastBillingDate).to.not.exist;
expect(updatedLeader.purchased.plan.dateCreated).to.exist;
expect(updatedLeader.items.mounts['Jackalope-RoyalPurple']).to.be.true;
});
it('sends an email to members of group', async () => {
let recipient = new User();
recipient.profile.name = 'recipient';
recipient.guilds.push(group._id);
await recipient.save();
data.groupId = group._id;
await api.createSubscription(data);
expect(sender.sendTxn).to.be.calledTwice;
expect(sender.sendTxn.firstCall.args[0]._id).to.equal(recipient._id);
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-joining');
expect(sender.sendTxn.firstCall.args[2]).to.eql([
{name: 'LEADER', content: user.profile.name},
{name: 'GROUP_NAME', content: group.name},
]);
});
it('adds months to members with existing gift subscription', async () => {
let recipient = new User();
recipient.profile.name = 'recipient';
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
plan.planId = 'basic_earned';
plan.paymentMethod = 'paymentMethod';
data.gift = {
member: recipient,
subscription: {
key: 'basic_earned',
months: 1,
},
};
await api.createSubscription(data);
await recipient.save();
data.gift = undefined;
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('group_plan_auto');
expect(updatedUser.purchased.plan.customerId).to.eql('group-plan');
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan');
expect(updatedUser.purchased.plan.extraMonths).to.within(1, 3);
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
expect(updatedUser.purchased.plan.dateCreated).to.exist;
});
it('adds months to members with existing multi-month gift subscription', async () => {
let recipient = new User();
recipient.profile.name = 'recipient';
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
data.gift = {
member: recipient,
subscription: {
key: 'basic_3mo',
months: 3,
},
};
await api.createSubscription(data);
await recipient.save();
data.gift = undefined;
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('group_plan_auto');
expect(updatedUser.purchased.plan.customerId).to.eql('group-plan');
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan');
expect(updatedUser.purchased.plan.extraMonths).to.within(3, 5);
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
expect(updatedUser.purchased.plan.dateCreated).to.exist;
});
it('adds months to members with existing recurring subscription (Stripe)', async () => {
let recipient = new User();
recipient.profile.name = 'recipient';
plan.key = 'basic_earned';
plan.paymentMethod = stripePayments.constants.PAYMENT_METHOD;
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.extraMonths).to.within(2, 3);
});
it('adds months to members with existing recurring subscription (Amazon)', async () => {
sinon.stub(amzLib, 'getBillingAgreementDetails')
.returnsPromise()
.resolves({
BillingAgreementDetails: {
BillingAgreementStatus: {State: 'Closed'},
},
});
let recipient = new User();
recipient.profile.name = 'recipient';
plan.planId = 'basic_earned';
plan.paymentMethod = amzLib.constants.PAYMENT_METHOD;
plan.lastBillingDate = moment().add(3, 'months');
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.extraMonths).to.within(3, 4);
});
it('adds months to members with existing recurring subscription (Paypal)', async () => {
sinon.stub(paypalPayments, 'paypalBillingAgreementCancel').returnsPromise().resolves({});
sinon.stub(paypalPayments, 'paypalBillingAgreementGet')
.returnsPromise().resolves({
agreement_details: { // eslint-disable-line camelcase
next_billing_date: moment().add(3, 'months').toDate(), // eslint-disable-line camelcase
cycles_completed: 1, // eslint-disable-line camelcase
},
});
let recipient = new User();
recipient.profile.name = 'recipient';
plan.planId = 'basic_earned';
plan.paymentMethod = paypalPayments.constants.PAYMENT_METHOD;
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.extraMonths).to.within(2, 3);
paypalPayments.paypalBillingAgreementGet.restore();
paypalPayments.paypalBillingAgreementCancel.restore();
});
it('adds months to members with existing recurring subscription (Android)');
it('adds months to members with existing recurring subscription (iOs)');
it('adds months to members who already cancelled but not yet terminated recurring subscription', async () => {
let recipient = new User();
recipient.profile.name = 'recipient';
plan.key = 'basic_earned';
plan.paymentMethod = stripePayments.constants.PAYMENT_METHOD;
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 recipient.cancelSubscription();
await api.createSubscription(data);
let updatedUser = await User.findById(recipient._id).exec();
expect(updatedUser.purchased.plan.extraMonths).to.within(2, 3);
});
it('adds months to members who already cancelled but not yet terminated group plan subscription', async () => {
let recipient = new User();
recipient.profile.name = 'recipient';
plan.key = 'basic_earned';
plan.paymentMethod = api.constants.GROUP_PLAN_PAYMENT_METHOD;
plan.extraMonths = 2.94;
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 recipient.cancelSubscription();
await api.createSubscription(data);
let updatedUser = await User.findById(recipient._id).exec();
expect(updatedUser.purchased.plan.extraMonths).to.within(3, 4);
});
it('resets date terminated if user has old subscription', async () => {
let recipient = new User();
recipient.profile.name = 'recipient';
plan.key = 'basic_earned';
plan.paymentMethod = stripePayments.constants.PAYMENT_METHOD;
plan.dateTerminated = moment().subtract(1, 'days').toDate();
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.dateTerminated).to.not.exist;
});
it('adds months to members with existing recurring subscription and includes existing extraMonths', async () => {
let recipient = new User();
recipient.profile.name = 'recipient';
plan.key = 'basic_earned';
plan.paymentMethod = stripePayments.constants.PAYMENT_METHOD;
plan.extraMonths = 5;
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.extraMonths).to.within(7, 8);
});
it('adds months to members with existing recurring subscription and ignores existing negative extraMonths', async () => {
let recipient = new User();
recipient.profile.name = 'recipient';
plan.key = 'basic_earned';
plan.paymentMethod = stripePayments.constants.PAYMENT_METHOD;
plan.extraMonths = -5;
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.extraMonths).to.within(2, 3);
});
it('does not override gemsBought, mysteryItems, dateCreated, and consective fields', async () => {
let planCreatedDate = moment().toDate();
let mysteryItem = {title: 'item'};
let mysteryItems = [mysteryItem];
let consecutive = {
trinkets: 3,
gemCapExtra: 20,
offset: 1,
count: 13,
};
let recipient = new User();
recipient.profile.name = 'recipient';
plan.key = 'basic_earned';
plan.gemsBought = 3;
plan.dateCreated = planCreatedDate;
plan.mysteryItems = mysteryItems;
plan.consecutive = consecutive;
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.gemsBought).to.equal(3);
expect(updatedUser.purchased.plan.mysteryItems[0]).to.eql(mysteryItem);
expect(updatedUser.purchased.plan.consecutive.count).to.equal(consecutive.count);
expect(updatedUser.purchased.plan.consecutive.offset).to.equal(consecutive.offset);
expect(updatedUser.purchased.plan.consecutive.gemCapExtra).to.equal(consecutive.gemCapExtra);
expect(updatedUser.purchased.plan.consecutive.trinkets).to.equal(consecutive.trinkets);
expect(updatedUser.purchased.plan.dateCreated).to.eql(planCreatedDate);
});
it('does not modify a user with a group subscription when they join another group', async () => {
let recipient = new User();
recipient.profile.name = 'recipient';
plan.key = 'basic_earned';
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();
let firstDateCreated = updatedUser.purchased.plan.dateCreated;
let extraMonthsBeforeSecond = updatedUser.purchased.plan.extraMonths;
let group2 = generateGroup({
name: 'test group2',
type: 'guild',
privacy: 'public',
leader: user._id,
});
data.groupId = group2._id;
await group2.save();
recipient.guilds.push(group2._id);
await recipient.save();
await api.createSubscription(data);
updatedUser = await User.findById(recipient._id).exec();
expect(updatedUser.purchased.plan.planId).to.eql('group_plan_auto');
expect(updatedUser.purchased.plan.customerId).to.eql('group-plan');
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan');
expect(updatedUser.purchased.plan.extraMonths).to.eql(extraMonthsBeforeSecond);
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
expect(updatedUser.purchased.plan.dateCreated).to.eql(firstDateCreated);
});
it('does not remove a user who is in two groups plans and leaves one', async () => {
let recipient = new User();
recipient.profile.name = 'recipient';
plan.key = 'basic_earned';
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();
let firstDateCreated = updatedUser.purchased.plan.dateCreated;
let extraMonthsBeforeSecond = updatedUser.purchased.plan.extraMonths;
let group2 = generateGroup({
name: 'test group2',
type: 'guild',
privacy: 'public',
leader: user._id,
});
data.groupId = group2._id;
await group2.save();
recipient.guilds.push(group2._id);
await recipient.save();
await api.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
await updatedGroup.leave(recipient);
updatedUser = await User.findById(recipient._id).exec();
expect(updatedUser.purchased.plan.planId).to.eql('group_plan_auto');
expect(updatedUser.purchased.plan.customerId).to.eql('group-plan');
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan');
expect(updatedUser.purchased.plan.extraMonths).to.eql(extraMonthsBeforeSecond);
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
expect(updatedUser.purchased.plan.dateCreated).to.eql(firstDateCreated);
});
it('does not modify a user with an unlimited subscription', async () => {
plan.key = 'basic_earned';
plan.customerId = api.constants.UNLIMITED_CUSTOMER_ID;
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(api.constants.UNLIMITED_CUSTOMER_ID);
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
expect(updatedUser.purchased.plan.paymentMethod).to.eql('paymentMethod');
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 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;
plan.dateTerminated = moment().add(1, 'months');
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('group_plan_auto');
expect(updatedUser.purchased.plan.customerId).to.eql(api.constants.GROUP_PLAN_CUSTOMER_ID);
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
expect(updatedUser.purchased.plan.paymentMethod).to.eql('Group Plan');
expect(updatedUser.purchased.plan.extraMonths).to.within(0, 2);
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
expect(updatedUser.purchased.plan.lastBillingDate).to.not.exist;
expect(updatedUser.purchased.plan.dateCreated).to.exist;
});
});
@@ -5,10 +5,14 @@ import {
generateGroup,
} from '../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../website/server/models/user';
import { model as Group } from '../../../../../website/server/models/group';
import { 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;
@@ -394,6 +398,50 @@ describe('Stripe Payments', () => {
subscriptionId,
});
});
it('subscribes a group with the correct number of group members', async () => {
token = 'test-token';
sub = data.sub;
groupId = group._id;
email = 'test@test.com';
headers = {};
user = new User();
user.guilds.push(groupId);
await user.save();
group.memberCount = 2;
await group.save();
await stripePayments.checkout({
token,
user,
gift,
sub,
groupId,
email,
headers,
coupon,
}, stripe);
expect(stripeCreateCustomerSpy).to.be.calledOnce;
expect(stripeCreateCustomerSpy).to.be.calledWith({
email,
metadata: { uuid: user._id },
card: token,
plan: sub.key,
quantity: 4,
});
expect(stripePaymentsCreateSubSpy).to.be.calledOnce;
expect(stripePaymentsCreateSubSpy).to.be.calledWith({
user,
customerId: customerIdResponse,
paymentMethod: 'Stripe',
sub,
headers,
groupId,
subscriptionId,
});
});
});
describe('edit subscription', () => {
@@ -658,4 +706,301 @@ describe('Stripe Payments', () => {
});
});
});
describe('#upgradeGroupPlan', () => {
let spy, data, user, group;
beforeEach(async function () {
user = new User();
user.profile.name = 'sender';
data = {
user,
sub: {
key: 'basic_3mo', // @TODO: Validate that this is group
},
customerId: 'customer-id',
paymentMethod: 'Payment Method',
headers: {
'x-client': 'habitica-web',
'user-agent': '',
},
};
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
await group.save();
spy = sinon.stub(stripe.subscriptions, 'update');
spy.returnsPromise().resolves([]);
data.groupId = group._id;
data.sub.quantity = 3;
stripePayments.setStripeApi(stripe);
});
afterEach(function () {
sinon.restore(stripe.subscriptions.update);
});
it('updates a group plan quantity', async () => {
data.paymentMethod = 'Stripe';
await payments.createSubscription(data);
let updatedGroup = await Group.findById(group._id).exec();
expect(updatedGroup.purchased.plan.quantity).to.eql(3);
updatedGroup.memberCount += 1;
await updatedGroup.save();
await stripePayments.chargeForAdditionalGroupMember(updatedGroup);
expect(spy.calledOnce).to.be.true;
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();
});
});
});
});
});
+77 -47
View File
@@ -4,7 +4,6 @@ import {
generateTodo,
generateDaily,
} from '../../../../helpers/api-unit.helper';
import { cloneDeep } from 'lodash';
import cronMiddleware from '../../../../../website/server/middlewares/cron';
import moment from 'moment';
import { model as User } from '../../../../../website/server/models/user';
@@ -60,7 +59,7 @@ describe('cron middleware', () => {
cronMiddleware(req, res, done);
});
it('should clear todos older than 30 days for free users', async (done) => {
it('should clear todos older than 30 days for free users', async () => {
user.lastCron = moment(new Date()).subtract({days: 2});
let task = generateTodo(user);
task.dateCompleted = moment(new Date()).subtract({days: 31});
@@ -68,16 +67,21 @@ describe('cron middleware', () => {
await task.save();
await user.save();
cronMiddleware(req, res, (err) => {
Tasks.Task.findOne({_id: task}, function (secondErr, taskFound) {
expect(secondErr).to.not.exist;
expect(taskFound).to.not.exist;
done(err);
await new Promise((resolve, reject) => {
cronMiddleware(req, res, (err) => {
if (err) return reject(err);
Tasks.Task.findOne({_id: task}, function (secondErr, taskFound) {
if (secondErr) return reject(err);
expect(secondErr).to.not.exist;
expect(taskFound).to.not.exist;
resolve();
});
});
});
});
it('should not clear todos older than 30 days for subscribed users', async (done) => {
it('should not clear todos older than 30 days for subscribed users', async () => {
user.purchased.plan.customerId = 'subscribedId';
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
user.lastCron = moment(new Date()).subtract({days: 2});
@@ -87,16 +91,20 @@ describe('cron middleware', () => {
await task.save();
await user.save();
cronMiddleware(req, res, (err) => {
Tasks.Task.findOne({_id: task}, function (secondErr, taskFound) {
expect(secondErr).to.not.exist;
expect(taskFound).to.exist;
done(err);
await new Promise((resolve, reject) => {
cronMiddleware(req, res, (err) => {
if (err) return reject(err);
Tasks.Task.findOne({_id: task}, function (secondErr, taskFound) {
if (secondErr) return reject(secondErr);
expect(secondErr).to.not.exist;
expect(taskFound).to.exist;
resolve();
});
});
});
});
it('should clear todos older than 90 days for subscribed users', async (done) => {
it('should clear todos older than 90 days for subscribed users', async () => {
user.purchased.plan.customerId = 'subscribedId';
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
user.lastCron = moment(new Date()).subtract({days: 2});
@@ -107,39 +115,49 @@ describe('cron middleware', () => {
await task.save();
await user.save();
cronMiddleware(req, res, (err) => {
Tasks.Task.findOne({_id: task}, function (secondErr, taskFound) {
expect(secondErr).to.not.exist;
expect(taskFound).to.not.exist;
done(err);
await new Promise((resolve, reject) => {
cronMiddleware(req, res, (err) => {
if (err) return reject(err);
Tasks.Task.findOne({_id: task}, function (secondErr, taskFound) {
if (secondErr) return reject(secondErr);
expect(secondErr).to.not.exist;
expect(taskFound).to.not.exist;
resolve();
});
});
});
});
it('should call next if user was not modified after cron', async (done) => {
it('should call next if user was not modified after cron', async () => {
let hpBefore = user.stats.hp;
user.lastCron = moment(new Date()).subtract({days: 2});
await user.save();
cronMiddleware(req, res, (err) => {
expect(hpBefore).to.equal(user.stats.hp);
done(err);
await new Promise((resolve, reject) => {
cronMiddleware(req, res, (err) => {
if (err) return reject(err);
expect(hpBefore).to.equal(user.stats.hp);
resolve();
});
});
});
it('updates user.auth.timestamps.loggedin and lastCron', async (done) => {
it('updates user.auth.timestamps.loggedin and lastCron', async () => {
user.lastCron = moment(new Date()).subtract({days: 2});
let now = new Date();
await user.save();
cronMiddleware(req, res, (err) => {
expect(moment(now).isSame(user.lastCron, 'day'));
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
done(err);
await new Promise((resolve, reject) => {
cronMiddleware(req, res, (err) => {
if (err) return reject(err);
expect(moment(now).isSame(user.lastCron, 'day'));
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
resolve();
});
});
});
it('does damage for missing dailies', async (done) => {
it('does damage for missing dailies', async () => {
let hpBefore = user.stats.hp;
user.lastCron = moment(new Date()).subtract({days: 2});
let daily = generateDaily(user);
@@ -147,28 +165,34 @@ describe('cron middleware', () => {
await daily.save();
await user.save();
cronMiddleware(req, res, (err) => {
expect(user.stats.hp).to.be.lessThan(hpBefore);
done(err);
await new Promise((resolve, reject) => {
cronMiddleware(req, res, (err) => {
if (err) return reject(err);
expect(user.stats.hp).to.be.lessThan(hpBefore);
resolve();
});
});
});
it('updates tasks', async (done) => {
it('updates tasks', async () => {
user.lastCron = moment(new Date()).subtract({days: 2});
let todo = generateTodo(user);
let todoValueBefore = todo.value;
await user.save();
cronMiddleware(req, res, () => {
Tasks.Task.findOne({_id: todo._id}, function (err, todoFound) {
expect(err).to.not.exist;
expect(todoFound.value).to.be.lessThan(todoValueBefore);
done();
await new Promise((resolve, reject) => {
cronMiddleware(req, res, (err) => {
if (err) return reject(err);
Tasks.Task.findOne({_id: todo._id}, function (secondErr, todoFound) {
if (secondErr) return reject(secondErr);
expect(todoFound.value).to.be.lessThan(todoValueBefore);
resolve();
});
});
});
});
it('applies quest progress', async (done) => {
it('applies quest progress', async () => {
let hpBefore = user.stats.hp;
user.lastCron = moment(new Date()).subtract({days: 2});
let daily = generateDaily(user);
@@ -192,17 +216,20 @@ describe('cron middleware', () => {
party.startQuest(user);
cronMiddleware(req, res, () => {
expect(user.stats.hp).to.be.lessThan(hpBefore);
done();
await new Promise((resolve, reject) => {
cronMiddleware(req, res, (err) => {
if (err) return reject(err);
expect(user.stats.hp).to.be.lessThan(hpBefore);
resolve();
});
});
});
it('recovers from failed cron and does not error when user is already cronning', async (done) => {
it('recovers from failed cron and does not error when user is already cronning', async () => {
user.lastCron = moment(new Date()).subtract({days: 2});
await user.save();
let updatedUser = cloneDeep(user);
let updatedUser = user.toObject();
updatedUser.nMatched = 0;
sandbox.spy(cronLib, 'recoverCron');
@@ -215,10 +242,13 @@ describe('cron middleware', () => {
},
});
cronMiddleware(req, res, () => {
expect(cronLib.recoverCron).to.be.calledOnce;
await new Promise((resolve, reject) => {
cronMiddleware(req, res, (err) => {
if (err) return reject(err);
expect(cronLib.recoverCron).to.be.calledOnce;
done();
resolve();
});
});
});
});
+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 () => {

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