Compare commits

...

470 Commits

Author SHA1 Message Date
Sabe Jones
fd5ab32c43 4.27.0 2018-02-14 02:30:55 +00:00
Sabe Jones
e0933dc0a1 chore(i18n): update locales 2018-02-14 02:29:58 +00:00
Sabe Jones
d69e7e66ee World Boss 2018 (Server) (#9995)
* feat(world-boss): barebones API

* fix(world): use Express respond for better JSON

* fix(api): respond with code 200

* feat(content): canonical Dysheartener desc and Rage

* fix(world-boss): enable progress

* WIP(test): world state API

* WIP(test): refactor world boss setup

* WIP(test): better expectations

* fix(test): more expect polishing

* feat(event): server side World Boss

* fix(strings): accidental deletion

* fix(content): include encouragement after Rage

* refactor(world-boss): address comments
2018-02-13 19:21:39 -06:00
Matteo Pagliazzi
fa06628361 add new locales 2018-02-13 12:41:47 +01:00
Matteo Pagliazzi
7f1067e1ab chore(i18n): update locales 2018-02-13 12:40:05 +01:00
Matteo Pagliazzi
db6644a572 chore(i18n): update locales 2018-02-13 12:35:05 +01:00
Sabe Jones
b7b8faedd6 4.26.3 2018-02-12 22:07:52 +00:00
Sabe Jones
7da0b641dd chore(i18n): update locales 2018-02-12 22:07:00 +00:00
SabreCat
82ff241e39 chore(sprites): compile 2018-02-12 21:58:54 +00:00
SabreCat
cf4aaf1618 feat(event): enable Valentines 2018-02-12 21:56:29 +00:00
Sabe Jones
31eeb13598 4.26.2 2018-02-10 11:56:30 +00:00
Sabe Jones
818c201afe chore(i18n): update locales 2018-02-10 11:46:36 +00:00
Sabe Jones
f78bcc20c9 chore(i18n): update locales 2018-02-10 05:18:40 +00:00
Keith Holliday
9017eea0c4 4.26.1 2018-02-09 10:05:12 -06:00
Keith Holliday
92abb19f9c Docker update (#9976)
* Updated docker prod

* Added new gulp cli
2018-02-09 08:59:49 -07:00
Keith Holliday
3bee0446b8 Loaded profile when not cached (#9977) 2018-02-09 08:57:55 -07:00
Sabe Jones
e1e5a6cc34 4.26.0 2018-02-09 00:58:23 +00:00
Sabe Jones
9eb70ee9ba Merge branch 'develop' into release 2018-02-09 00:58:13 +00:00
Sabe Jones
7485300ee9 chore(i18n): update locales 2018-02-09 00:43:01 +00:00
SabreCat
762c202154 feat(content): enable Cupid Potions 2018-02-09 00:35:51 +00:00
Matteo Pagliazzi
7613e50917 remove mongoskin and mongodb packages 2018-02-08 19:10:01 +01:00
Matteo Pagliazzi
4c77434bf0 Upgrade karma and related deps (#9974)
* upgrade karma and karma babel

* upgrade karma-chai

* upgrade karma-coverage

* upgrade karma-mocha

* upgrade karma-mocha-reporter

* upgrade karma-sinon-chai

* upgrade karma-spec-reporter
2018-02-08 19:02:19 +01:00
Matteo Pagliazzi
1ab26a200e Upgrade tests packages (#9973)
* upgrade mochat to v5

* upgrade cross-spawn

* upgrade chalk

* try to fix mongoose

* upgrade shelljs

* upgrade expect.js

* upgrade coveralls

* upgrade cross-env

* upgrade lcov-result-merger

* upgrade image-size
2018-02-08 19:00:49 +01:00
Matteo Pagliazzi
b738824f76 fix indentation in app.vue 2018-02-07 18:47:14 +01:00
Sabe Jones
694c440b55 Merge branch 'release' into develop 2018-02-07 02:16:48 +00:00
Sabe Jones
667bb28fe3 4.25.0 2018-02-07 02:16:24 +00:00
Sabe Jones
8993337ad7 chore(i18n): update locales 2018-02-07 02:15:58 +00:00
SabreCat
a251a5d089 chore(sprites): compile 2018-02-07 02:04:37 +00:00
SabreCat
dcdeec6256 feat(content): Badger Pet Quest 2018-02-07 02:03:30 +00:00
SabreCat
9fd0df9f2f fix(food): add missing text functions to Bare Bones Cake 2018-02-06 20:34:33 +00:00
Matteo Pagliazzi
1061fb0c31 remove async (only used for old migrations) (#9961) 2018-02-06 18:13:13 +01:00
Keith Holliday
c14fdd3fed Fixed attribute binding. Removed member url (#9963) 2018-02-06 09:53:43 -07:00
Alys
e219ad6bdf adjust slurs / banned words. TRIGGER / CONTENT WARNING: assault, slurs, swearwords, etc 2018-02-06 08:40:31 +10:00
Alys
42d7fd0861 fix typo in equipment description (Armorie) 2018-02-06 08:37:39 +10:00
Nathan Sollenberger
7ade91a8b8 Updates notification text for armoire items in shops:genericPurchase. (#9240)
Adjusts column widths for 'drop' type notifications to prevent .text from overlapping .icon-item.
Adds new 'flavorMessage' property to Snackbar notifications, which overrides standard computed message.
Vertically center XP gain icon/amount.
Fixes unrelated typo in i18n key name.
2018-02-05 19:43:30 +01:00
Feywood
7c6dd6a6bd (in)definite articles for food items. Partial fix for https://github.com/HabitRPG/habitica/issues/3571 (#9658)
* testing additional event trigger for sendMessage

* moved keyup event to newmessage

* added keyup event to tavern vue too

* all food items now have version with definite and indefinite article. foodText also adapted in messages json

* modified api.food, and feed, armoire and drop mechanism

* drops now ok, removed dropArticle, corrected feed test

* test correction

* api doc modification for task completion
2018-02-05 19:43:12 +01:00
Jon Lim
9a00779698 Adding support for loading more members for Groups (#9740)
* Adding support for loading more members for Groups

Addresses issue #9720

Member modal component was only loading the maximum limit for queries
made to _getMembersForItem in /members, except with Challenges, which
was able to display a "Load More" button to retrieve another set of
users from the Challenge.

Made a few changes to how the GET request was being made when querying
for more members, added an easier way to know whether or not to display
the Load More button, and extracted some of the actions that were
too tightly coupled with the membersModal.vue.

* Fixes for failing lint tests

* Removing unnecessary async/await usage

* Fixing party view in header section

* Resolving missed conflict

* Adding necessary data for View Party in index header for web client to load party members
2018-02-05 19:42:54 +01:00
Lula Villalobos
a61d911c48 Feature - Store Exact Completion Date For Dailies (#9813)
* dailies history date added in scoreTask instead of cron

* fix lint issues

* changes based on feedback. Undo cron code deletion and deleted iteration on scoreTask

* fix lint issues

* add task history entry in cron for dailies that weren't completed

* add history entry after value is fully evaluated
2018-02-05 19:42:20 +01:00
Alexey Pyltsyn
fbacb56700 Fixed footer to bottom of page (#9860) 2018-02-05 19:42:01 +01:00
Alexey Pyltsyn
185717e6c3 Fixed a task tag tooltip position (#9910) 2018-02-05 19:41:46 +01:00
Mason Hahn
505dd4969d add contributor info for inbox tier styling (#9911) 2018-02-05 19:41:28 +01:00
Feywood
944e4fe399 collection items shown in quest progress (#9945) 2018-02-05 19:41:11 +01:00
Keith Holliday
804dd087f8 Updated notes on armoire when purchasing (#9891) 2018-02-05 10:27:10 -07:00
Keith Holliday
cb1136aadc Added markdown to tag edit fields/popup (#9867) 2018-02-05 09:54:42 -07:00
Keith Holliday
0f4b8f5f30 Added summary field tests (#9883)
* Added summary field tests

* Fixed typo
2018-02-05 09:52:44 -07:00
Keith Holliday
2d612b655d Added copy as todo (#9884) 2018-02-05 09:43:00 -07:00
negue
46877fb20c Purchase Card: fixes (#9803)
* prevent re-showing buy-modal after buying a card / fix class-badge in member-selection

* show notifications on card purchase

* move string to generic.json

* translation param "profileName"
2018-02-05 09:39:42 -07:00
Keith Holliday
38cded2083 4.24.6 2018-02-04 23:03:51 -06:00
Matteo Pagliazzi
65074df668 fix build task 2018-02-04 19:30:30 +01:00
Matteo Pagliazzi
b0ab09c352 Upgrade gulp (and gulp deps) (#9950)
* upgrade gulp to v4

* fix imagemin

* fixes
2018-02-04 18:18:10 +01:00
Matteo Pagliazzi
3a60e8de66 Merge branch 'develop' of github.com:HabitRPG/habitrpg into develop 2018-02-04 15:54:35 +01:00
Matteo Pagliazzi
886a96dac9 Merge branch 'release' into develop 2018-02-04 15:53:40 +01:00
Matteo Pagliazzi
20556e2af4 refactor casting spells on the client side (#9949) 2018-02-04 15:51:17 +01:00
Matteo Pagliazzi
3e849ec9a8 Revert "refactor casting spells on the client side (#9921)" (#9948)
This reverts commit 03946e6a87.
2018-02-04 15:50:36 +01:00
Matteo Pagliazzi
03946e6a87 refactor casting spells on the client side (#9921) 2018-02-04 14:47:34 +01:00
Matteo Pagliazzi
fdcc69fe4a Merge branch 'upgrade-universal-analytics' into develop 2018-02-04 14:45:31 +01:00
Matteo Pagliazzi
322b6a5e44 Merge branch 'develop' into upgrade-universal-analytics 2018-02-04 14:45:20 +01:00
Matteo Pagliazzi
2243a2f3dc upgrade winston-loggly-bulk (#9942) 2018-02-04 14:40:33 +01:00
Matteo Pagliazzi
59c848add5 upgrade request (#9941) 2018-02-04 14:39:54 +01:00
Matteo Pagliazzi
2d6a1fe709 remove jade, upgrade pug (#9940) 2018-02-04 14:39:46 +01:00
Matteo Pagliazzi
fac0338437 Merge branch 'release' into develop 2018-02-04 14:39:30 +01:00
Matteo Pagliazzi
e46f30894c fix test timeout 2018-02-04 14:39:16 +01:00
Matteo Pagliazzi
553dc116c5 Merge branch 'release' into develop 2018-02-04 14:21:35 +01:00
Matteo Pagliazzi
2029cd884c notifications: fix tests 2018-02-04 14:20:12 +01:00
Matteo Pagliazzi
ab89941ed5 notifications: fix tests 2018-02-04 14:04:41 +01:00
Matteo Pagliazzi
5dc0f5bb9b Merge branch 'release' into develop 2018-02-04 13:41:53 +01:00
Matteo Pagliazzi
d46a7ba985 notifications: fix typos and add extra check 2018-02-04 13:40:49 +01:00
Matteo Pagliazzi
86b3228a59 Merge branch 'release' into develop 2018-02-04 13:28:34 +01:00
Matteo Pagliazzi
4efbbd7bac notifications: fixes 2018-02-04 13:28:05 +01:00
Matteo Pagliazzi
d39e8a3587 upgrade universal-analytics 2018-02-03 22:40:40 +01:00
Matteo Pagliazzi
8bd3ef6f24 fix mongoose options 2018-02-03 22:17:10 +01:00
Matteo Pagliazzi
6d0917964b fix mongoose options 2018-02-03 22:15:40 +01:00
Sabe Jones
d463e2373e Merge branch 'release' into develop 2018-02-02 20:51:24 +00:00
Sabe Jones
2af99d7c65 4.24.5 2018-02-02 19:53:57 +00:00
Keith Holliday
4e01b14874 Removed required fields from user notifications (#9929) 2018-02-02 12:31:52 -07:00
Keith Holliday
944781c2f8 Added p parameter to group party query (#9857) 2018-02-02 11:24:41 -07:00
Keith Holliday
027eed1b25 Reset market on gear purchase (#9855) 2018-02-02 11:22:25 -07:00
Keith Holliday
803f63d991 Added two handed message to weapons (#9839)
* Added two handed message to weapons

* Added two handed message to content api

* Fixed static notes text. Added support for RTL
2018-02-02 11:21:53 -07:00
Keith Holliday
78ad1cd8b0 Added cache for user styles on chat (#9679)
* Added cache for user styles on chat

* Added loading on new message and other minor checks

* Added null checks

* Updated chat tests

* Added costume preference to chat

* Removed single profile cacheing for new chat messages

* Remove owned gear from cache

* Updated stats to only use buffs
2018-02-02 11:18:25 -07:00
Sabe Jones
a3ddd0746c 4.24.4 2018-02-02 16:55:55 +00:00
SabreCat
94845ec629 fix(event): remove seasonal pinned items 2018-02-02 16:54:25 +00:00
Sabe Jones
7e63856e64 4.24.3 2018-02-02 16:44:09 +00:00
Matteo Pagliazzi
3727d69d51 Merge branch 'release' into develop 2018-02-02 16:38:27 +01:00
Matteo Pagliazzi
1fbdb7dbd0 Mongoose 4.x (#9928)
* update mongoose to ^4.x

* another fix
2018-02-02 16:37:36 +01:00
Matteo Pagliazzi
b430b6ccb6 Revert "Mongoose 4.13 (#9926)" (#9927)
This reverts commit 7859f20a40.
2018-02-02 16:36:54 +01:00
Matteo Pagliazzi
7859f20a40 Mongoose 4.13 (#9926)
* update mongoose to ^4.x

* another fix
2018-02-02 16:35:05 +01:00
Matteo Pagliazzi
389d6f18b4 travis split unit and integration tests (#9925) 2018-02-02 15:07:39 +01:00
Sabe Jones
fd1d1da509 Revert "fix(test): update for event"
This reverts commit ede28ac33a.
2018-02-02 12:09:45 +00:00
Matteo Pagliazzi
014b367f56 do not use lodash to find notification 2018-02-02 12:24:43 +01:00
Matteo Pagliazzi
4580b33e43 fix progress bar hidden behind the menu 2018-02-02 09:57:39 +01:00
Sabe Jones
48bcc1e968 4.24.2 2018-02-02 03:35:00 +00:00
Sabe Jones
35368eb6ad chore(i18n): update locales 2018-02-02 03:33:27 +00:00
Sabe Jones
9b337983ed fix(strings): Capital Party, comrade 2018-02-02 03:27:19 +00:00
Sabe Jones
cd4951d204 fix(notifs): brevity 2018-02-02 03:19:13 +00:00
Sabe Jones
bd83d6f5aa 4.24.1 2018-02-02 01:51:08 +00:00
Sabe Jones
9c241a6159 fix(classes): remove winter preview
Also adds a new index to the Mongo doc and fixes a typo in that file.
2018-02-02 01:49:33 +00:00
Sabe Jones
7a6baeadbd 4.24.0 2018-02-02 00:48:36 +00:00
Sabe Jones
5495acea96 chore(i18n): update locales 2018-02-02 00:46:45 +00:00
SabreCat
a3aa2cc175 chore(sprites): compile 2018-02-02 00:33:04 +00:00
SabreCat
8b0d02a16b feat(content): backgrounds and Armoire Feb 2018
Also ends Winter Wonderland event
2018-02-02 00:31:38 +00:00
SabreCat
c3c0eb974a Merge branch 'develop' into release 2018-02-02 00:29:58 +00:00
Keith Holliday
56539100e3 Fixed zindex for progress bar (#9922) 2018-02-01 18:12:34 -06:00
SabreCat
b706db43e4 Merge branch 'release' into develop 2018-02-01 21:09:57 +00:00
Keith Holliday
3e0a7c70ed Event off fixes (#9918)
* Added removes for all events

* Moved to beforeDestroy
2018-02-01 10:14:21 -07:00
Sabe Jones
6a5bd1b0a5 Merge branch 'release' into develop 2018-01-31 22:23:15 +00:00
Sabe Jones
7f8a9be766 4.23.2 2018-01-31 22:20:51 +00:00
Sabe Jones
dbdf679e4a chore(i18n): update locales 2018-01-31 22:20:39 +00:00
SabreCat
eb28dfadf9 chore(news): Last Chance Bailey
Also fixes a linting issue with birthday fun.
2018-01-31 22:10:50 +00:00
SabreCat
ede28ac33a fix(test): update for event 2018-01-31 22:00:54 +00:00
Matteo Pagliazzi
33b249d078 Notifications v2 and Bailey API (#9716)
* Added initial bailey api

* wip

* implement new panel header

* Fixed lint

* add ability to mark notification as seen

* add notification count, remove top badge from user and add ability to mark multiple notifications as seen

* add support dismissall and mark all as read

* do not dismiss actionable notif

* mark as seen when menu is opened instead of closed

* implement ordering, list of actionable notifications

* add groups messages and fix badges count

* add notifications for received cards

* send card received notification to target not sender

* rename notificaion field

* fix integration tests

* mark cards notifications as read and update tests

* add mystery items notifications

* add unallocated stats points notifications

* fix linting

* simplify code

* refactoring and fixes

* fix dropdown opening

* start splitting notifications into their own component

* add notifications for inbox messages

* fix unit tests

* fix default buttons styles

* add initial bailey support

* add title and tests to new stuff notification

* add notification if a group task needs more work

* add tests and fixes for marking a task as needing more work

* make sure user._v is updated

* remove console.log

* notification: hover status and margins

* start styling notifications, add separate files and basic functionalities

* fix tests

* start adding mystery items notification

* wip card notification

* fix cards text

* initial implementation inbox messages

* initial implementation group messages

* disable inbox notifications until mobile is ready

* wip group chat messages

* finish mystery and card notifications

* add bailey notification and fix a lot of stuff

* start adding guilds and parties invitations

* misc invitation fixes

* fix lint issues

* remove old code and add key to notifications

* fix tests

* remove unused code

* add link for public guilds invite

* starts to implement needs work notification design and feature

* fixes to needs work, add group task approved notification

* finish needs work feature

* lots of fixes

* implement quest notification

* bailey fixes and static page

* routing fixes

* fixes #      this.$store.dispatch(guilds:join, {groupId: group.id, type: party});

* read notifications on click

* chat notifications

* fix tests for chat notifications

* fix chat notification test

* fix tests

* fix tests (again)

* try awaiting

* remove only

* more sleep

* add bailey tests

* fix icons alignment

* fix issue with multiple points notifications

* remove merge code

* fix rejecting guild invitation

* make remove area bigger

* fix error with notifications and add migration

* fix migration

* fix typos

* add cleanup migration too

* notifications empty state, new counter color, fix marking messages as seen in guilds

* fixes

* add image and install correct packages

* fix mongoose version

* update bailey

* typo

* make sure chat is marked as read after other requests
2018-01-31 11:55:39 +01:00
Matteo Pagliazzi
a85282763f Upgrade sinonjs (and related libs) (#9914)
* update sinon

* remove errors

* fix unit tests
2018-01-31 10:56:32 +01:00
Sabe Jones
ba9d7b3b5e 4.23.1 2018-01-31 00:53:27 +00:00
Sabe Jones
3b794c017a fix(event): update birthday vars and hooks 2018-01-31 00:41:06 +00:00
SabreCat
47dbe4561f fix(test): update for event 2018-01-30 22:22:02 +00:00
Sabe Jones
97135a1ac3 Merge branch 'release' into develop 2018-01-30 21:38:56 +00:00
Sabe Jones
a636e15d11 4.23.0 2018-01-30 21:38:34 +00:00
Sabe Jones
3cc15e869e chore(i18n): update locales 2018-01-30 21:38:00 +00:00
SabreCat
88c8b92a68 chore(sprites): compile 2018-01-30 21:26:15 +00:00
SabreCat
cee4d7e87b feat(event): Habit Birthday 2018 2018-01-30 21:25:19 +00:00
Matteo Pagliazzi
9488ec2eb0 Merge branch 'release' into develop 2018-01-30 18:56:25 +01:00
Keith Holliday
4fe6c8db64 Clone challenges api (#9684)
* Added clone api

* Added new clone UI

* Fixed challenge clone

* Fixed lint and added mongo toObject

* Removed clone field, fixed type, fixed challenge task query

* Auto selected group

* Accounted for group balance when creating challenge

* Added check for if user is leader of guild

* Added leader existence check

* Added fix for leader and prizecost equal to
2018-01-30 08:23:20 -07:00
Keith Holliday
ccf8e0b320 Removed gitattributes 2018-01-29 21:34:18 -06:00
Matteo Pagliazzi
1dc558ddba prevent ex-participants appearing in challenge export file - Fix #9844 (#9846)
* possible fix for 9844

* fix typo in challengeModal file

* remove lines for empty users
2018-01-29 16:23:40 -06:00
Alexey Pyltsyn
ae27ae0090 Improved Party page UI (#9892) 2018-01-29 15:44:22 -06:00
Alys
47c2a3a21a prevent "Zero-day streak" giving a 21-day streak achievement + tests - fixes #2578 (#9688)
* prevent "Zero-day streak" giving a 21-day streak achievement - fixes #2578

* add tests for streak achievements

* remove .only from set of tests

* refactor(test): fix linting
2018-01-29 15:43:59 -06:00
Cassidy Pignatello
5d4e1362bb updates Orb of Rebirth description to more accurately list its effects (#9894) 2018-01-29 15:32:21 -06:00
James Hwang
25cecf298f Fixed formatting of modal strings to correctly display message #9818 (#9862) 2018-01-29 15:30:05 -06:00
Alexey Pyltsyn
2de3b63e87 Fixed a community guidelines layout (#9893)
* Fixed a community guidelines layout

* Refactoring: add community guidelines component
2018-01-29 15:18:20 -06:00
John Zhou
7abb8a81a7 Do not show visual buffs on choose class avatars (#9812) 2018-01-29 15:16:49 -06:00
Daniel Reeves
3eb3891899 Update copyright year 2017->2018 (#9889) 2018-01-29 15:14:00 -06:00
Alexey Pyltsyn
4b0ad422f1 Use translatable strings in footer (#9866) 2018-01-29 15:11:30 -06:00
Alexey Pyltsyn
3c603e3bb1 Fixed horizontal scrollbar (#9853) 2018-01-29 15:07:59 -06:00
Nicole Massaro
4ee788f541 Hide stats points allocation on locked or disabled class system (#9826) (#9851) 2018-01-29 15:05:06 -06:00
Sabe Jones
99ab9726b4 Allow user to buy numbered special gear (#9823)
* fix(buy): allow user to buy numbered special gear

* fix(buy): correct content constant
2018-01-29 15:02:55 -06:00
Maru de Vera
23dd402e79 Unpin Regained Items from Enchanted Armoire (#9766) 2018-01-29 15:00:19 -06:00
Irina Brennen
6bd90807f3 Fixing issue with hair bangs, styles and colors not getting the option.active class (#9759) 2018-01-29 14:57:35 -06:00
Alys
563a5845f0 refund gems when deleting a "Public Challenge" (Tavern challenge) (#9752) 2018-01-29 14:56:46 -06:00
Brad Lugo
2e580baf27 Update the docker compose process (#9724)
Since the client side code and server side code run independently, the
docker compose process needed to be updated to reflect this change.
This fix included updating the docker-compose files' versions.
2018-01-29 14:56:08 -06:00
Clement134
44ded25f6d Add .gitattributes file fixes #9717 (#9722)
* chore(gitattributes): add .gitattributes file

*  chore(gitattributes): specify file type for each extension

* fix(presskit): use LF line endings
2018-01-29 14:53:14 -06:00
Alys
70da5940a7 clarify that "Leave" refers to guild/party; fix pluralisation in keep/remove challenge tasks (#9706)
* change "Keep/Remove It" to "Keep/Remove Them" when asking about all challenge tasks while leaving a challenge

* change "Leave" button on groups to "Leave Guild" or "Leave Party"

This is because the button is underneath the challenges so this
clarifies that it is referring to the group, not a challenge.

* change "Keep/Remove Them" to "Keep/Remove Tasks"
2018-01-29 14:52:28 -06:00
Cai Lu
12aa8a78c1 Track sleeping in the inn with analytics (fixes #9561) (#9685)
* Sleep status is tracked by analytics when toggled.

* Modify test: test that analytics is called with 'sleep' event and data passed includes the user's new sleep status
2018-01-29 14:48:24 -06:00
Keith Holliday
94619737e8 Adjusted zindex of navbar to be above snackbars and modals (#9873) 2018-01-29 08:25:07 -07:00
Alys
ccc9e6611c adjust terminology in delete To-Dos message; move similar messages together 2018-01-28 12:29:24 +10:00
Keith Holliday
1f1459b0d8 4.22.1 2018-01-27 11:31:59 -06:00
Keith Holliday
6489e74b6b Added tests for new username restrictions (#9900) 2018-01-27 10:30:17 -07:00
Alys
c1e264955f describe Login Name limitations on Registration form and Add Local Auth form (#9896)
* describe Login Name limitations on registration form and Add Local Auth form

* adjust text in response to this change: c69687f935

* update max length
2018-01-27 09:38:23 -07:00
Alys
f302d15bc4 add length and character limitations for login name (username) (#9895)
* update API comments to for `username` restrictions and to use Login Name terminology

We use "login name" rather than "username" in user-visible text
on the website and (usually) when communicating with users because
"username" could be confused with "profile name".
Using it in the docs allows you to search for that term.

* add alphanumeric and length validation for creating new login name (username)

The 'en-US' locale is specified explicitly to ensure we never use
another locale. The point of this change is to limit the character
set to prevent login names being used to send spam in the Welcome
emails, such as Chinese language spam we've had trouble with.

* add error messages for bad login names

* allow login name to also contain hyphens

This is because our automated tests generate user accounts using:
  let username = generateUUID();

* allow login names to be up to 36 characters long because we use UUIDs as login names in our tests

* revert back to using max 20 characters and only a-z, 0-9 for login name.

It's been decided to change the username generation in the tests instead.

* disable test that is failing because it's redundant

Spaces are now prohibited by other code.

We can probably delete this test later. I don't want to delete it
now, but instead give us time to think about that.

* fix typos

* revert to login name restrictions that allow us to keep using our existing test code

I'm really not comfortable changing our test suite in ways that
aren't essential, especially since we're working in a hurry with
a larger chance than normal of breaking things.
The 36 character length is larger than we initially decided but
not so much larger that it's a huge problem.
We can reduce it to 20 when we have more time.

* limit username length to 20 chars

* fix tests
2018-01-27 09:33:56 -07:00
Keith Holliday
8c70c8839b 4.22.0 2018-01-25 17:35:45 -06:00
Keith Holliday
3fcd04fd8a Updated encryption 2018-01-25 17:33:50 -06:00
Sabe Jones
d85f18751c chore(i18n): update locales 2018-01-25 23:26:24 +00:00
SabreCat
1390c4eae5 Merge branch 'develop' into release 2018-01-25 23:20:01 +00:00
Sabe Jones
18ade8ca65 Analytics: track challenge and task events (#9885)
* feat(analytics): track challenge and task events

* feat(analytics): add more challenge events
Also tweaks data for better troubleshooting utility

* refactor(analytics): include IDs for challenges/groups

* refactor(analytics): term for award challenge is "close"
2018-01-25 17:14:41 -06:00
Keith Holliday
7b026fa32c Added existence check for lastMessageText (#9886) 2018-01-25 13:46:49 -07:00
Keith Holliday
33698c219f Added existence check for lastMessageText (#9881) 2018-01-25 11:33:37 -07:00
Matteo Pagliazzi
b76d731cee fix gold icon alignment 2018-01-25 16:53:53 +01:00
Matteo Pagliazzi
4d1ac51543 upgrade datepicker 2018-01-25 15:13:21 +01:00
Matteo Pagliazzi
3818fbdd3e update boostrap and bootstrap-vue (no breaking changes) 2018-01-24 19:08:07 +01:00
Sabe Jones
af245b63d9 Merge branch 'release' into develop 2018-01-24 01:31:27 +00:00
Sabe Jones
028da1d6a9 4.21.0 2018-01-23 23:07:20 +00:00
Sabe Jones
49397244c4 chore(i18n): update locales 2018-01-23 23:07:05 +00:00
SabreCat
2b04ed3246 feat(content): Mystery Items 2018/01 2018-01-23 22:38:23 +00:00
Keith Holliday
51aebb540c Removed display of users personal checklist on challenge tasks (#9837) 2018-01-23 10:55:55 -07:00
Keith Holliday
f5d7777b2c Reloaded user data after cancelling subscription (#9836) 2018-01-23 10:28:44 -07:00
Keith Holliday
be1ffbd671 Removed promot to leader from challenge member modal (#9831) 2018-01-23 09:58:12 -07:00
Keith Holliday
5640139ef1 Awaited unlink so the UI refreshes removed tasks (#9815) 2018-01-23 09:36:46 -07:00
Keith Holliday
0959499450 Changed width to maxwidth (#9830) 2018-01-22 09:15:55 -07:00
Keith Holliday
90ffe587dd Added analytics to drop (#9792)
* Added analytics to drop

* Updated tracking category
2018-01-22 09:15:23 -07:00
Keith Holliday
38aafb6c7b Added clear completed todos (#9782) 2018-01-22 08:28:42 -07:00
Keith Holliday
ecfcf09184 Added more responsive styles to filters (#9820) 2018-01-22 08:20:34 -07:00
Keith Holliday
7083dc7e05 Added resync for completed todos (#9821) 2018-01-22 08:19:47 -07:00
Keith Holliday
d4e0417c48 Added challenge tag when challenge task is added to existing challenge (#9833) 2018-01-22 08:19:27 -07:00
Matteo Pagliazzi
ec7c25de9f add new notifications types 2018-01-22 11:46:51 +01:00
Matteo Pagliazzi
6f9db87843 Upgrade travis (#9850)
* upgrade travis

* upgrade travis
2018-01-21 22:47:58 +01:00
Matteo Pagliazzi
46c9038f54 Remove unused packages (#9849)
* remove unused packages

* update lock package

* try with paypal-rest-sdk 1.7.1

* setup nconf
2018-01-21 21:23:25 +01:00
Keith Holliday
1ce09aeb34 Added chatscroll check (#9814) 2018-01-20 17:00:54 -07:00
Keith Holliday
2ba327ef14 Fixed gem purchasing and error catching 2018-01-20 15:09:08 -06:00
Matteo Pagliazzi
de93b47493 add new notifications types 2018-01-20 17:28:50 +01:00
Sabe Jones
b0a21e116a 4.20.3 2018-01-19 03:59:29 +00:00
Sabe Jones
53d1a5f9dc chore(i18n): update locales 2018-01-19 03:58:59 +00:00
SabreCat
274f942b1e chore(news): Add note re audio themes 2018-01-18 21:45:15 +00:00
SabreCat
4aad52242c chore(news): iOS + Blog Bailey 2018-01-18 21:25:59 +00:00
Keith Holliday
166a48e139 Temporarily removed task allocation settings (#9822) 2018-01-18 14:16:30 -06:00
SabreCat
13de97dde6 fix(market): hide nav buttons when too few items
Fixes https://github.com/HabitRPG/habitica/issues/9802#issuecomment-358687806
2018-01-18 20:05:16 +00:00
Matteo Pagliazzi
6d8407ff94 fix tags selection in groups 2018-01-18 17:39:50 +01:00
Alys
663b794435 adjust Ram Barbarian equipment set to say "(Item .. of 3)"
"Item" had been missing.
2018-01-18 21:25:26 +10:00
Keith Holliday
c0276e3663 More staging fixes (#9816)
* Added ability to adjust challenge task copy's streak

* Disabled stat allocation if method is incorrect
2018-01-18 11:51:56 +01:00
Matteo Pagliazzi
6d57ce3050 fix typo 2018-01-18 11:41:19 +01:00
Matteo Pagliazzi
2159df785f Staging fixes (#9819)
* categories can be selected

* quick inventory fixes
2018-01-18 11:30:39 +01:00
Matteo Pagliazzi
9762258975 Merge branch 'develop' of github.com:HabitRPG/habitrpg into develop 2018-01-17 19:33:30 +01:00
Matteo Pagliazzi
deea64e839 more fixes for the task modal 2018-01-17 19:33:21 +01:00
Matteo Pagliazzi
9e615ba862 move delete task btn outside of advanced settings 2018-01-17 19:27:53 +01:00
SabreCat
d34beca3cc Merge branch 'release' into develop 2018-01-17 18:24:48 +00:00
Sabe Jones
07ed989862 4.20.2 2018-01-16 22:46:08 +00:00
Sabe Jones
049844ea7d chore(i18n): update locales 2018-01-16 22:35:00 +00:00
SabreCat
ff4c76165a fix(event): end New Year's cards 2018-01-16 22:09:24 +00:00
Keith Holliday
c3220e7c03 Pushed zindex of progress bar above modals (#9781)
* Pushed zindex of progress bar above modals

* Moved notifications above modals
2018-01-15 13:30:03 -07:00
Keith Holliday
cb4c6b3ca6 Added redirects for old url styles (#9780) 2018-01-15 12:43:02 -07:00
Keith Holliday
ba36ba0157 Added max width to profile image (#9783) 2018-01-15 12:34:00 -07:00
Keith Holliday
dd95acf436 Added coupon purchasing back to stripe (#9794) 2018-01-15 12:12:19 -07:00
Keith Holliday
a73b03452a Fixed overflow for member lists larger than 10 (#9777) 2018-01-15 10:32:32 -07:00
Keith Holliday
935fa1baae Fixed update query to revert challenge tags when broken (#9791) 2018-01-15 10:31:33 -07:00
Keith Holliday
745f930749 Added docs for mongo indexes (#9790) 2018-01-15 10:30:49 -07:00
Keith Holliday
d87db40c52 Staging fixes (#9804)
* Fixed party member loading

* Fixed quest details

* Fixed party creating

* Fixed challenge habit restore streak permissions

* Fixed fetch recent messages for party

* Adjusted category box placement for challenges

* Fixed zindex for input on group

* Changed reset streak restriction and allowed for adjust streak
2018-01-15 10:21:08 -07:00
Keith Holliday
0ea91016f8 Trimmed username spaces (#9793) 2018-01-15 10:20:36 -07:00
Keith Holliday
d4f634c3d8 Fixed disabling button and dismissing modals (#9805) 2018-01-15 09:16:15 -07:00
Matteo Pagliazzi
286566fc0c misc fixes for task modal 2018-01-13 11:27:02 +01:00
Cassidy Pignatello
2ed4df0b7c unhides scroll bar and adjusts overflow to remove whitespace at bottom (#9767) 2018-01-12 16:38:54 -06:00
Alys
9bb7c6ece0 allow user to restore streak on their copy of a challenge Daily (#9757) 2018-01-12 16:37:36 -06:00
Lula Villalobos
db0a6f6bb8 Fix Server Errors Appear As Blank Red Bar (#9747)
* fix path to error message

* changed error message path to check user auth error
2018-01-12 16:33:00 -06:00
Brad Lugo
b6305826be Update Vagrantfile.example to forward port 8080 (#9732)
There was a change in how the code is run locally and part
of this change included using port 8080. This change includes
the port forwarding for said port in the example file for Vagrant
2018-01-12 16:32:09 -06:00
Mel
f00ab86eff Update tier icons to correct size and color (#9727)
* update tier icons to correct size and color

* make tier text bold
2018-01-12 16:29:37 -06:00
Cassidy Pignatello
a44f29dad8 replaces btn-default with btn-secondary (#9704) 2018-01-12 16:25:02 -06:00
Pizilden
67b396bf16 Added Lunasol Theme (#9635) 2018-01-12 16:22:57 -06:00
Grayson Gilmore
ce14a9dadb Challenge modal optimization - remove unnecessary API call - partial fix for #9371 (#9546)
* Attempt to use party data from the store rather than always fetching it from the API

* Move init code to shown() to prevent unnecessary network requests

* Use store party data in getGroup action if possible to save an API call

* Use store data rather than API call for party in challengeModal; remove unnecessary code in guilds:getGroup action

* Create party:getParty action and employ it in Group and ChallengeModal

* Use store instead of action return for party data

* Change how party data is stored
2018-01-12 16:18:56 -06:00
MathWhiz
183c90ac3a Fix presskit (#9418)
* Move presskit to static folder

* Fix image location

* Add press kit FAQ strings

* Add faq section, fix images

* Update Images

* Add images to presskit page

* Remove unecessary parts
2018-01-12 16:18:20 -06:00
borisabramovich86
9e1a262f96 Fixes Purchase contributor equipment (fixes #9179) (#9306)
* Able to see all non class related items in market

* Fix lint errors

* Able to see all non class related items in market

* Fix lint errors

* add test for showing contributor gear

* Added previously owned items to test with eslint exception
2018-01-12 16:16:28 -06:00
MathWhiz
06dd9fe859 Add SpacePenguin's Theme (#9237)
* Add SpacePenguin's Theme

* Fix files
2018-01-12 16:15:55 -06:00
zags
2a2c525c2d Add support for multiDaysCountAsOneDay == false to evasion (#9077)
* Add support for `multiDaysCountAsOneDay == false` to evasion

`if (dailiesDaysMissed > 1) dailiesDaysMissed = 1;` causes the evasion for-loop to only evaluate once even if `multiDaysCountAsOneDay == false`.  This statement isn't necessary because `if (multiDaysCountAsOneDay) break;` will cause the for-loop to evaluate only once if `multiDaysCountAsOneDay == true`.  Removing this statement makes the `dailiesDaysMissed` variable unnecessary.

* Moves break statement out of conditional
2018-01-12 16:12:19 -06:00
Sabe Jones
b2c1c9d9dc Merge branch 'release' into develop 2018-01-12 21:54:20 +00:00
Sabe Jones
c33eba6736 4.20.1 2018-01-12 21:47:21 +00:00
Sabe Jones
56434cce71 chore(i18n): update locales 2018-01-12 21:46:32 +00:00
SabreCat
c41123c36c chore(news): Blog Bailey 2018-01-12 21:38:16 +00:00
SabreCat
043a6cd4ba chore(shops): update Featured Items 2018-01-12 21:25:09 +00:00
SabreCat
0ca2f9034f Revert "WIP: Buy-1-Get-1 Gift Subs (#9719)"
This reverts commit dc3d694d0e, with the exception of locale strings that need not be purged.
2018-01-12 21:15:42 +00:00
Keith Holliday
4c7157807b Synced isdue/next due when user joins challenge (#9779) 2018-01-12 10:16:51 -06:00
Keith Holliday
0afe797bae Fixed display when quest has no completion function (#9778) 2018-01-12 10:16:21 -06:00
Alys
1c8797e473 change Attribute to Stat and upper-case Stat Points and a couple of other terms 2018-01-12 08:09:31 +10:00
Keith Holliday
e0bf6d2e55 Reverted group flag code (#9784)
* Reverted group flag code

* Reverted all flagging code

* Added hyphens back
2018-01-11 12:04:07 -06:00
Matteo Pagliazzi
e96d0659cb fix typos in migration to convert field for Apple subscribers 2018-01-10 18:19:32 +01:00
Matteo Pagliazzi
72d70236ea fix typos in migration to convert field for Apple subscribers 2018-01-10 18:10:01 +01:00
Matteo Pagliazzi
ee2fc8c763 add migration to convert field for Apple subscribers 2018-01-10 18:08:32 +01:00
Sabe Jones
b53c03bca8 Merge branch 'release' into develop 2018-01-10 16:42:13 +00:00
Sabe Jones
9545f692ef 4.20.0 2018-01-10 16:41:44 +00:00
Sabe Jones
0112bd9b5a chore(i18n): update locales 2018-01-10 16:40:59 +00:00
SabreCat
d235576e18 chore(sprites): compile 2018-01-10 16:32:34 +00:00
SabreCat
3d5d5da933 feat(content): Pept quest 2018-01-10 16:31:56 +00:00
Alys
9b19477e2f adjust wording for Keys to Kennels
Also moves the new Keys text strings to the same place as the old ones in the locales file.
Also fixes minor typos in code that was preventing the new text from being displayed.
2018-01-10 21:21:58 +10:00
Sabe Jones
5a9c95f07e fix(guilds): include exact breakpoints 2018-01-09 18:29:59 -06:00
Keith Holliday
3000e2b72c Removed keys and inbox flagging (#9776) 2018-01-09 12:58:01 -06:00
Matteo Pagliazzi
c1f6f0398e fix positioning of checkmarks for checklist items 2018-01-09 11:31:02 +01:00
Matteo Pagliazzi
cb6488fa05 fix path for KeysToKennel module 2018-01-09 11:29:12 +01:00
Keith Holliday
126d90f471 Added keys to the kennel (#9772)
* Added keys to the kennel

* Added titles and descriptions
2018-01-08 13:15:19 -06:00
Keith Holliday
98d4fb0f34 Chat flag inbox (#9761)
* Refactored chat reporting

* Added inbox flag chat

* Added flag chat to inbox

* Added you have flagged message
2018-01-08 13:13:25 -06:00
Keith Holliday
d3ee3ca53d Updated plan updated date if user has cancelled (#9773)
* Updated plan updated date if user has cancelled

* Added test for plan with only date updated
2018-01-08 12:50:15 -06:00
Matteo Pagliazzi
7eac5cebf5 fix typo 2018-01-08 18:47:59 +01:00
Matteo Pagliazzi
6a109adbc5 Task Modal Improvements (#9560)
* start adding advanced options

* new imput

* partial colors

* update deps

* misc adds

* fix text color

* add advanced options

* initial reordering of task modal labels

* start to fix colors in the modal

* wip colors

* update package-lock.json

* fix merge

* finish modal

* refactor colors

* fix quick add

* fix colors

* new icon colors

* add markdown formatting help

* fix habits colors

* fix rewards colors

* fixed remaining colors

* start to inline inputs

* fix typ

* fixes

* habits hover state

* wip new task modal inputs

* bootstrap: upgrade to v4-beta3

* finish reward edit modal

* fix attributes allocation, checklists and add help tooltips for attributes and difficulty

* lots of fixes

* update datepicker

* misc fixes
2018-01-08 18:43:57 +01:00
Sabe Jones
587847f5e9 4.19.1 2018-01-08 17:23:28 +00:00
Sabe Jones
7842cd8a41 chore(i18n): update locales 2018-01-08 17:18:12 +00:00
SabreCat
2f9cf02932 fix(sprites): rename 2018/01 background icons 2018-01-08 17:11:20 +00:00
Pizilden
daa796454c Added Farvoid Theme (new, clean branch) (#9634)
* Added Farvoid Theme

* Updated generic.json and index.js

* fix(merge): address conflicts
2018-01-05 14:34:19 -06:00
Cassidy Pignatello
c531239618 Fixes equipment sorting by name in inventory (#9692)
* returns items sorted in ascending order when sorted by name

* returns items sorted in ascending order when sorted by name
2018-01-05 13:51:47 -06:00
Matti Petrelius
f6ac7b890a Both eggs and potions should hatch pets from Items (#9645)
* Show click on hatching potion when egg is clicked

* Fix translation parameter name

* Show egg as active when clicked

* Remove itemDragStart event handler from egg items

* Change isHatchable to take in potion and egg objects

* Add margin to item popover content
2018-01-05 13:31:34 -06:00
kartik adur
229e39facf Other user checkins: user profile modal checkin count (#9646)
* add loginIncentives to public fields to show non-loggedin user checkins

* update integration tests for update in api response
2018-01-05 13:29:38 -06:00
Allison Virgil
75b00ce2df Fixed Challenge Winner Text (partial fix for #9629) (#9664)
* Fixed Challenge Winner Text (#9629)

* Fixing typo
2018-01-05 13:29:00 -06:00
Julius Jung
4576353f26 inital commit to add confirmation to animalEars (#9666) 2018-01-05 13:26:45 -06:00
Allister
acf4b4da63 Change focused state of introjs button (#9677) 2018-01-05 13:24:32 -06:00
josteins1
8b5933177a Given back header the priority over snackbar with z-index value and a padding to avoid collision. (#9687)
* increased top padding to match main header

referring to issue 9678
https://github.com/HabitRPG/habitica/issues/9678

* adjusted the z value to appropriate levels

z-index adjusted from 99999 to 999
2018-01-05 13:23:03 -06:00
Julius Jung
a6ddd6d233 fix drawer-slider carousel function && convert to pointer logic (fixes #9366 AND #9638) (#9711)
* fix drawer-slider carousel function && convert to pointer logic

* refactor conditional logic

* get rid of absolute value
2018-01-05 13:20:21 -06:00
Sabe Jones
5ca5adc774 4.19.0 2018-01-04 21:49:53 +00:00
Sabe Jones
005ffe850a chore(i18n): update locales 2018-01-04 21:44:47 +00:00
SabreCat
71cb4e8510 feat(content): enable Wintery Skins and Hair 2018-01-04 21:35:26 +00:00
SabreCat
40244ab81b Merge branch 'develop' into release 2018-01-04 21:11:44 +00:00
Keith Holliday
15b65b342a Removed extra achievement sound (#9763) 2018-01-04 10:17:33 -06:00
Matteo Pagliazzi
7df3aba71b make search case insensitive in equipment and inventory pages (#9762) 2018-01-04 13:06:15 +01:00
Keith Holliday
6bb535c129 Reset chat options when change guild routes (#9743) 2018-01-02 22:56:31 -07:00
Sabe Jones
e3bf3d29f7 4.18.1 2018-01-03 00:54:23 +00:00
SabreCat
df9c42c1b5 fix(backgrounds): add 2018 group 2018-01-03 00:52:54 +00:00
Sabe Jones
7e241bb76f Merge branch 'release' into develop 2018-01-03 00:07:42 +00:00
Sabe Jones
17fb681671 4.18.0 2018-01-03 00:05:22 +00:00
Sabe Jones
0069aee5b0 chore(i18n): update locales 2018-01-03 00:00:07 +00:00
SabreCat
240dd1b965 chore(sprites): compile 2018-01-02 23:52:35 +00:00
SabreCat
88e6b2da7c feat(content): Armoire and BGs 2018-01
Also ends New Year's hat fanciness
2018-01-02 23:49:50 +00:00
Sabe Jones
6e7f4a231d chore(sprites): compile 2018-01-02 21:41:37 +00:00
Sabe Jones
822a0e56af feat(notifications): recanvas sprites for notifs 2018-01-02 21:38:34 +00:00
Sabe Jones
da73c5c418 Merge branch 'release' into develop 2017-12-31 02:45:04 +00:00
Sabe Jones
2cf8439bd1 4.17.0 2017-12-31 02:44:43 +00:00
Sabe Jones
0e404ad6ba chore(i18n): update locales 2017-12-31 02:43:40 +00:00
SabreCat
b9f709ab30 chore(sprites): compile 2017-12-31 02:35:50 +00:00
SabreCat
d57c525fab feat(event): New Year's 2017-18 2017-12-31 02:34:52 +00:00
Alys
9a3a104ba4 fix typo in apidocs comment block 2017-12-30 08:43:11 +10:00
Keith Holliday
63bba13b5f Changed paypal redirect to subscription page (#9742) 2017-12-26 17:31:23 -06:00
Keith Holliday
d90d781740 Added mobile style fixes (#9741) 2017-12-26 17:31:00 -06:00
Alys
a3bf329c44 make reset password apidocs comment more accurate 2017-12-24 09:23:32 +00:00
Sabe Jones
22a12e37fa 4.16.2 2017-12-22 21:58:16 +00:00
SabreCat
446e0422c7 Merge branch 'release' into develop 2017-12-22 21:57:43 +00:00
SabreCat
5220cc1bf3 fix(market): add broken Armoire items 2017-12-22 21:56:39 +00:00
Sabe Jones
e8976b40f4 Merge branch 'release' into develop 2017-12-22 21:42:02 +00:00
Sabe Jones
3b4b459e68 4.16.1 2017-12-22 21:41:37 +00:00
Sabe Jones
bbbdd89ade chore(i18n): update locales 2017-12-22 21:41:18 +00:00
Sabe Jones
a20c1ba751 Include seasonal gear in Market class lists (#9739)
* fix(shops): include seasonal gear in Market class lists

* fix(market): display non-seasonal broken special items
Also fixes a bug where if a current seasonal item was broken, it would show up twice.
2017-12-22 15:29:51 -06:00
Alys
d725b5be19 fix typo in loading screen tip 8 2017-12-22 20:45:35 +00:00
Keith Holliday
545b052c10 Added fix for quantity confirmation (#9735) 2017-12-22 10:23:55 -06:00
SabreCat
028b9d569d Merge branch 'release' into develop 2017-12-21 23:20:51 +00:00
Sabe Jones
85b861c4a9 4.16.0 2017-12-21 23:20:00 +00:00
Sabe Jones
762e87a82a chore(i18n): update locales 2017-12-21 23:19:35 +00:00
SabreCat
b68e69e1a1 chore(sprites): compile 2017-12-21 23:12:06 +00:00
SabreCat
4764f115b1 feat(content): Subscriber Items Dec 2017 2017-12-21 23:10:54 +00:00
Keith Holliday
95c99295c1 Removed gold locally when user buys a card (#9736) 2017-12-21 10:27:09 -06:00
Keith Holliday
a7617fa947 Added broken megaphone icon (#9737) 2017-12-21 10:25:54 -06:00
Sabe Jones
2da2a47f32 4.15.3 2017-12-20 19:28:52 +00:00
Sabe Jones
8f744565e2 chore(i18n): update locales 2017-12-20 19:28:22 +00:00
SabreCat
714512b0a3 fix(promo): send payment method with promo 2017-12-20 18:41:19 +00:00
SabreCat
9538c86d02 fix(seasonal): include current season gear in Shop 2017-12-20 18:31:10 +00:00
Keith Holliday
afc1ffd90b Refactored duplicate card section (#9730)
* Refactored duplicate card section

* Updated to new computed methods
2017-12-20 12:27:21 -06:00
Matteo Pagliazzi
6988875e8a fix promo gifting 2017-12-20 19:09:15 +01:00
Keith Holliday
6e0b6171c6 Many ie style fixes (#9728) 2017-12-20 10:33:21 -06:00
Sabe Jones
53bbd93d80 4.15.2 2017-12-20 04:44:07 +00:00
Sabe Jones
75092336c4 fix(news): Holly, not Peppermint, potions 2017-12-20 04:43:43 +00:00
Sabe Jones
310bdf8cb5 4.15.1 2017-12-20 02:43:46 +00:00
Sabe Jones
9435a3089a chore(i18n): update locales 2017-12-20 02:43:00 +00:00
SabreCat
bb6dac2e84 4.15.0 2017-12-20 02:27:56 +00:00
SabreCat
acf34e2344 chore(sprites): compile 2017-12-20 02:27:43 +00:00
SabreCat
1aac4c713d feat(event): Winter Wonderland sprites (2/2) 2017-12-20 02:27:13 +00:00
SabreCat
bb527caa06 feat(content): Winter Wonderland sprites (1/2) 2017-12-20 02:26:21 +00:00
SabreCat
98bb6fd7ce feat(content): Winter Wonderland 2017-18
and Starry Night Hatching Potions
2017-12-20 02:23:11 +00:00
Keith Holliday
b8c716ff82 Fixed pending damage display and nav size (#9723) 2017-12-18 12:31:16 -06:00
Sabe Jones
9830fce760 4.14.2 2017-12-15 19:26:15 +00:00
Sabe Jones
7fccf59f50 Merge branch 'release' into develop 2017-12-15 15:54:09 +00:00
Sabe Jones
dd79f2be60 4.14.1 2017-12-15 15:02:22 +00:00
Sabe Jones
fbdcd4b0a3 chore(i18n): update locales 2017-12-15 15:01:52 +00:00
SabreCat
e229bc5042 Merge branch 'release' into develop 2017-12-15 05:37:04 +00:00
SabreCat
44c7e8c9dc 4.14.0 2017-12-15 05:36:01 +00:00
SabreCat
c4ffe39ec9 chore(news): Bailey 2017-12-15 05:34:30 +00:00
Sabe Jones
dc3d694d0e WIP: Buy-1-Get-1 Gift Subs (#9719)
* feat(promo): Buy-1-Get-1 Gift Subs

* feat(promo): add explanatory text to subscription screens
Also adds some add'l test coverage and creates a test context for this event
2017-12-14 23:09:02 -06:00
Sabe Jones
4f0ce77205 Winter Quest Bundle (#9718)
* feat(content): Winter Quest Bundle

* fix(sprites): revert spritesheet changes
2017-12-14 22:40:42 -06:00
Keith Holliday
c28ec24c33 Added notification for when leader is updated (#9674)
* Added notification for when leader is updated

* Abstracted challenge member search component

* Added challenge member search modal to challenge detail

* Added group search
2017-12-14 12:12:43 -06:00
Keith Holliday
54db84fddc Payment tests refactor (#9695)
* Reorganized files, reduced function size, reduced duplication

* Refactored amazon tests organization

* Reduced duplication

* Reorganized paypal tests

* Reorganized stripe tests

* Fixed lint issues

* Fixed gem purchase expectations

* Added cloning so we don't modify the common block
2017-12-14 09:59:45 -06:00
Keith Holliday
e7fd2b4c79 [WIP] Added initial inapp loading screen with tips (#9710)
* Added initial inapp loading screen with tips

* Added new tips and styles

* Removed unrelated readme
2017-12-14 09:35:10 -06:00
Keith Holliday
05640f513e Added test to recreate early cron issue (#9668)
* Added test to recreate early cron issue

* Gave user extra time based on reverse timezone change
2017-12-14 09:09:11 -06:00
SabreCat
b0ebdfeb65 Merge branch 'release' into develop 2017-12-13 22:16:30 +00:00
SabreCat
6c01db8d81 4.13.4 2017-12-13 22:15:11 +00:00
SabreCat
5a3751cbac chore(news): Blog Bailey 2017-12-13 22:14:19 +00:00
Keith Holliday
7802e30e80 Added static/front and meta tags (#9712)
* Added static/front and meta tags

* Moved meta to index
2017-12-13 15:11:22 -06:00
Matteo Pagliazzi
899452279b fix ie 11 rendering (#9713) 2017-12-13 13:51:04 -06:00
Keith Holliday
566716e2fe Added display logic for npcs (#9709)
* Added display logic for npcs

* Fixed lint
2017-12-12 19:05:18 -06:00
SabreCat
2a42bc9450 Merge branch 'release' into develop 2017-12-12 21:27:04 +00:00
SabreCat
1ef62d1b66 4.13.3 2017-12-12 21:25:39 +00:00
SabreCat
355773ecf3 chore(news): FCC CTA
Also fixes an issue with sprite alignment of the Lamplighter Set.
2017-12-12 21:24:18 +00:00
Keith Holliday
2bb5751f33 Added float rounding (#9657)
* Added float rounding

* Changed to isNaN
2017-12-11 11:48:50 -06:00
Keith Holliday
2570c59130 Ensured admin can always PM user (#9653)
* Ensured admin can always PM user

* Fixed lint issues

* Updated admin check and removed async

* Removed console log
2017-12-11 11:48:17 -06:00
Keith Holliday
2dfcda068b Added streak to export of challenge tasks (#9625)
* Added streak to export of challenge tasks

* Fixed tests
2017-12-11 11:39:43 -06:00
Keith Holliday
507133c76e Added client side logging (#9643) 2017-12-11 11:07:16 -06:00
Keith Holliday
a7c115877f Added query option to limit query fields (#9642)
* Added query option to limit query fields

* Removed only
2017-12-11 10:24:19 -06:00
Keith Holliday
1750a0c2e6 Updated responsive styles (#9696)
* Updated responsive styles

* Font adjustments

* Changed to max height
2017-12-09 23:23:16 -06:00
Keith Holliday
759ce61492 Added support for party invites by email (#9665)
* Added support for party invites by email

* Changed to groupInvite
2017-12-07 12:57:01 -05:00
Keith Holliday
57193bd5f3 Ensured quest drops are only from incomplete progress (#9671)
* Ensured quest drops are only from incomplete progress

* Fixed spelling error
2017-12-07 12:33:40 -05:00
Keith Holliday
e1a1b4eab6 Added body param for remove message (#9669)
* Added body param for remove message

* Removed console.log
2017-12-07 11:52:28 -05:00
Keith Holliday
350894f985 Added achievement restore migration (#9641)
* Added achievement restore migration

* Updated checks
2017-12-07 10:18:16 -05:00
Keith Holliday
0184d774c2 Disabled challenge button when loading (#9686) 2017-12-06 20:16:40 -05:00
Sabe Jones
d136162d48 4.13.2 2017-12-06 19:16:35 +00:00
SabreCat
2be8ddb60d fix(news): winner typo 2017-12-06 19:15:34 +00:00
Keith Holliday
3c67f91525 Quest refactor (#9681)
* Separated out quest sidebar component

* Added accepted count
2017-12-06 11:13:44 -05:00
Keith Holliday
c02aadfac4 Made user pay for amoire locally (#9673) 2017-12-06 10:50:15 -05:00
Julius Jung
2f956252ab Don't show shield when previewing two-handed weapon (fixes #9495) (#9676)
* initial commit to not show shield when previewing two-hand weapon

* revert error made

* update to fix all combinations of equipping / trying two-handed weapons

* clarify conditional logic

* refactor to let avatar check for twoHanded display/hide logic

* add case when avatar doesn't have weapon equipped
2017-12-05 17:05:59 -06:00
Andrew Bustos
341f16cc82 Fixed block icon not showing in profile modal. (#9667)
* Fixed block and add icon not showing and the ordering of the buttons. Also fixed and added tooltips for buttons.

* Changed unnecessary change of color in svg icon files and changed tooltip attribute to not be empty
2017-12-05 15:01:18 -06:00
Julius Jung
ec179182e7 submit initial working solution to disallow selling of Saddle (#9663) 2017-12-05 14:22:59 -06:00
Pizilden
b886d7bb33 Added MAFL Theme (#9633) 2017-12-05 14:20:58 -06:00
Pizilden
a8f8f4f544 Added Pizilden's Theme (#9632) 2017-12-05 14:14:45 -06:00
Feywood
4047bf6943 Rewards permit decimal value. Fixes https://github.com/HabitRPG/habitica/issues/9513 (#9617)
* testing additional event trigger for sendMessage

* moved keyup event to newmessage

* added keyup event to tavern vue too

* removed number.toFixed, changed to placeholder and step
2017-12-05 14:13:54 -06:00
Julius Jung
a5a985fd00 Don't show shield when previewing two-handed weapon (fixes #9495) (#9607)
* initial commit to not show shield when previewing two-hand weapon

* revert error made

* update to fix all combinations of equipping / trying two-handed weapons

* clarify conditional logic

* refactor to let avatar check for twoHanded display/hide logic
2017-12-05 14:11:19 -06:00
Alys
444d6889de add subscription cancellation instructions for free Group Plans subscriptions (#9606) 2017-12-05 14:10:15 -06:00
negue
c56c69d464 market fixes 28th nov (#9593)
* list special gear by the `specialClass` - fixes #9485

* only disable the currencly label + value not the amount input - fixes #9492

* disable transformations on equipment previews - fixes #9497

* show boss strength - fixes #9522

* pin time travelers animals - closes #9382

* clean up + package-lock ?

* fix quest info
2017-12-05 14:09:34 -06:00
Kip Raske
4b610ba3f1 Fixing the pig flying too high in the stable square bug (#9592)
The `.FlyingPig` css class necessary to re-center the pig in its square
is no longer applied when the square is greyed out. So I am adding that
to the greyed out square. It seems to not have any affect on the other
pets.
2017-12-05 14:06:35 -06:00
Feywood
65e3b599e6 Fix for sending chat with ctrl enter for windows chrome/firefox. Fixes https://github.com/HabitRPG/habitica/issues/9380 (#9588)
* testing additional event trigger for sendMessage

* moved keyup event to newmessage

* added keyup event to tavern vue too

* removed obsolete check from _updateCarretPosition

* fixed lint issue
2017-12-05 14:05:01 -06:00
aalsehly86
7caf211bec Fix issue #9534 - changed the character-name (#9547)
* Fix issue #9534 - changed the character-name from $white to $header-dark-background

* changes to #9534 - changed character-name color from -dark-background to -color.

* character-name color is back to  #9534

* changed character-name color to -200

* Changed colors for character name and level details #9534
2017-12-05 14:02:29 -06:00
Julius Jung
d4bc7c77a9 Check previous gear owned before purchasing next level gear (fixes #9071) (#9466)
* add another check if previous gear is owned

* respect gear purchase order

* catch error with miscalculation of equipment number floor

* add integration test for proper equipment purchasing order

* fix syntax

* add 'previousGearNotOwned' string

* rewrite logic for different starting levels for wep vs others

* separate and add tests for armor and weapon

* rename variable for clarification

* skip check if itemIndex is NaN

* change obscure NaN check for readability

* change conditional from checking NaN to Int
2017-12-05 13:58:12 -06:00
Tyler Nychka
bfaa7c0fea Validate that everyX values in dailies are integers bounded by 0 and 9999 fixes #8782 (#9268)
* Validate that everyX values are integers bounded by 0 and 9999

* Added client side check

* Updated tests

* Added migration for bad dailies

Near idential to the other task migration.

* fix(typo): camelCase function call
2017-12-05 13:55:32 -06:00
Sabe Jones
8367de34bf Merge branch 'release' into develop 2017-12-05 19:47:52 +00:00
Sabe Jones
ea6b78b7ca 4.13.1 2017-12-05 19:47:26 +00:00
Sabe Jones
401067bfed chore(i18n): update locales 2017-12-05 19:46:40 +00:00
Sabe Jones
b457daa616 Merge branch 'release' into develop 2017-12-05 19:29:34 +00:00
Sabe Jones
54c2441934 4.13.0 2017-12-05 19:29:06 +00:00
Sabe Jones
9e8807c40d Armoire and Backgrounds December 2017 (#9659)
* feat(content): Armoire and Backgrounds 2017/12

* chore(sprites): compile

* chore(news): Bailey
2017-12-05 19:29:00 +00:00
Sabe Jones
9bfbeaf93e Armoire and Backgrounds December 2017 (#9659)
* feat(content): Armoire and Backgrounds 2017/12

* chore(sprites): compile

* chore(news): Bailey
2017-12-05 13:25:52 -06:00
Keith Holliday
860efefdb2 Removed client side armoire call (#9660) 2017-12-05 12:23:31 -06:00
Keith Holliday
6310482b9d Added popovers to quests (#9655) 2017-12-05 10:57:28 -05:00
Keith Holliday
2d4928cd2b Removed challenge access restriction (#9652) 2017-12-04 15:57:38 -05:00
SabreCat
f3c2c0f901 Merge branch 'release' into develop 2017-12-04 19:23:55 +00:00
Keith Holliday
1fc84c2357 Added calculated property (#9637) 2017-12-04 11:24:04 -06:00
Keith Holliday
13cdcedcba Added support for scoring group tasks down after approval (#9623)
* Added support for scoring group tasks down after approval

* Fixed lint issues
2017-12-04 11:23:47 -06:00
Matteo Pagliazzi
72f0b8ed7c send correct email when user is removed from group plan 2017-12-04 12:04:07 +01:00
thehollidayinn
95f9479d7a 4.12.6 2017-12-02 08:37:36 -06:00
Keith Holliday
af095d8450 Revert query optimization (#9636) 2017-12-02 08:35:31 -06:00
Sabe Jones
470495387c 4.12.5 2017-12-02 03:28:44 +00:00
Keith Holliday
bdef1ca23c Fixed max width none (#9631) 2017-12-01 21:26:52 -06:00
SabreCat
1835804e86 4.12.4 2017-12-01 21:33:06 +00:00
SabreCat
cb58994bdf Merge branch 'release' into develop 2017-12-01 21:32:28 +00:00
SabreCat
44f3b73183 fix(avatar): layer base/bangs correctly 2017-12-01 21:10:33 +00:00
Luan Muniz
7e23fdc22a Fix install node permissions (#9621)
Signed-off-by: Luan <luan@luanmuniz.com.br>
2017-12-01 16:05:17 +00:00
Keith Holliday
e138d2b67b Added needs cron check to achievements (#9624) 2017-12-01 09:54:43 -06:00
Keith Holliday
3e3248fecb Changed row adding when user blurs/focuses (#9610) 2017-12-01 08:37:37 -06:00
Keith Holliday
78ee60611a Remove cancel button when clicked (#9616) 2017-12-01 08:03:54 -06:00
Keith Holliday
3c7aaa605b Fixed text being cut off (#9612) 2017-12-01 08:03:36 -06:00
Keith Holliday
00343da266 Added max width seetings to screens larger than 1300 (#9609) 2017-12-01 07:47:45 -06:00
Sabe Jones
56d09411d9 Merge branch 'release' into develop 2017-12-01 00:31:23 +00:00
Sabe Jones
ae0df2242a 4.12.3 2017-12-01 00:30:49 +00:00
Sabe Jones
5b06b28c97 chore(event): end Thunderstorm Potions 2017-12-01 00:30:06 +00:00
Keith Holliday
c6a3bfb291 Added exp reset when changing level (#9611) 2017-11-30 15:45:26 -06:00
Keith Holliday
7797794cd5 Deselect a tag if it is selected when removing (#9614) 2017-11-30 15:45:14 -06:00
Keith Holliday
d9e09a5f3d Fixed streak bonus style (#9608) 2017-11-30 12:37:53 -06:00
Keith Holliday
4e73c8513e Hide progress if user is not on quest (#9597) 2017-11-30 12:37:31 -06:00
Keith Holliday
9421fd7ced Added analytics to backgrounds (#9615) 2017-11-30 12:10:49 -06:00
Keith Holliday
699de64328 Added more fields to scoring (#9613) 2017-11-30 10:09:04 -06:00
Keith Holliday
6f9cbf9ca1 Only update the user when editing profile (#9601) 2017-11-30 08:19:03 -06:00
Keith Holliday
a097819b72 User auth performance improvements (#9589)
* Added initial user projecting in auth and fixed projection for get user tasks

* Added fields to score route

* Added another field to get tasks

* Added group fields to user
2017-11-30 08:17:28 -06:00
Keith Holliday
77f71b5415 Fixed saving in progress tag when clicking save (#9598) 2017-11-30 08:16:54 -06:00
Keith Holliday
ced3621dea Fixed leaving from guild list item (#9599) 2017-11-30 08:16:00 -06:00
Keith Holliday
e321d85b3c Donate buy modal fix (#9604)
* Added donate back and buy modal

* Fixed login check

* Added ability to remove mustache
2017-11-30 08:15:28 -06:00
SabreCat
d72b40d5b0 Merge branch 'release' into develop 2017-11-29 05:07:42 +00:00
SabreCat
54443a2980 4.12.2 2017-11-29 05:04:46 +00:00
SabreCat
00dc990974 chore(event): end Thanksgiving, add Bailey 2017-11-29 05:03:01 +00:00
Keith Holliday
3737aa045d Fixed text when cloning (#9594) 2017-11-28 19:18:04 -06:00
Keith Holliday
b03ddf6f7d Added fix for task order using / for arrays (#9590) 2017-11-28 14:57:08 -06:00
tim1234ltp
4ab89fd3e0 Bug fixes on Subscription termination date format [Fixes Issues #9186] (#9583)
* Fixed date.

* Got rid of the filter and returned moment.

* fix the return value

* Stupid typo.
2017-11-28 09:19:29 +01:00
negue
f1e200c0f5 autofix pinned seasonal gear - fixes #9448 (#9570)
* auto-remove officialPinned item from userPinned-array on pinning

* hide event limited message if an item was already owned by the user
2017-11-28 09:11:40 +01:00
Ryan Holinshead
218664dfcc (ISSUE-9353) Fix pinned item alignment (#9358)
* (ISSUE-9353) Fix pinned item alignment
- Get rid of justify-content: space-between to allow flex-start default to be used
- Add margin to direct children (item-wrappers)

* Issue-9353: Change selector to & > div instead of just > for more explicit selection of direct child divs

* Fix rewards item spacing to match Zeplin mockups
- Make sure horizontal/vertical spacing between items is 16px
- Add use of grid if supported, else use flex
2017-11-27 20:54:03 -06:00
Trevor Ford
a0f29e970d fix Stable sidebar width and center inventory drawer (fixes #9263) (#9419)
* fix Stable sidebar width and center inventory drawer (fixes #9263)

* hide all .standard-sidebars on small/mobile devices
2017-11-27 20:48:52 -06:00
MathWhiz
200cd66d66 Use config when starting development server (#9410)
* Use config when starting development server

* import nconf setup from website

* Add comment explaining choice

* Fix lint issues
2017-11-27 20:38:27 -06:00
MathWhiz
dd05a8d608 Contributor Title tooltip (#9413)
* Remove usage of cachedProfileData when determining contributor level

* Add tooltip

* Remove directive import

* /s/msg.contributor.title/msg.contributor.text

* move tooltip placement

* update tooltip position
2017-11-27 20:37:36 -06:00
Asif Mallik
299e88233c Fixes multiple complete and uncomplete for todos and daily (Fixes #8669) (#8971)
* Fixed bug that allows users to complete todo and daily multiple times

* Added tests

* Fix syntax

* Fix existing tests that rely on multiple complete or uncomplete

* Undoes removal of website/client/README.md

* Change sessionOutdated string to reflect separate client needs

* Fix should update history test by changing lastCron
2017-11-27 20:13:18 -06:00
Ryan Holinshead
26bde1f766 Character Create Modal - Prevent Options From Jumping When Selected (#9252)
* Prevent options from jumping when selected due to border being added/removed based on active option. Instead, always have a border on the option but set its color when active

* Use gray instead of white border in order to match background so that it isn't visible while unselected. Add margin-bottom back

* Make sure the locked option style remains unchanged
- Nest .locked in .customize-options .option to get specificity
- Override border, border-radius, and margin-top for .locked
- Set the override values to what would be applied without other style changes
2017-11-27 20:11:15 -06:00
Garrett Scott
d95836b881 Translator minor changes fixes #8917 (#9297)
* Updated userItemsNotEnough string

* Added a variable to be passed to the deleteSocialAccountText string. This variable name is `magic_word` and is set as DELETE where used

* modified incorrectDeletePhrase to use a variable rather than translatable string for the word DELETE. Updated the DELETE-user test and the user api

* Changed noSudoAccess from translatable string to static

* Changed enterprisePlansEmailSubject from a translatable string to a static string within groupPlans.vue

* Fixed test problems with translation fixes

* Added no sudo access string to api messages

* changed plain string to apiMessage for no sudo access messages
2017-11-27 20:08:39 -06:00
Tyler Nychka
fac81bb9ee Long names overflow task box fixes #9403 (#9404)
* Issue 9403 Long names overflow task box

* Added padding

* Enabled overflow-wrap: break-word;

Added min width to allow overflow-wrap to actually break content
2017-11-27 20:07:13 -06:00
Sarvesh Kakodkar
b323abd225 Added confirmation step at begin button for quest (#9199)
* Added confirmation step at begin button for quest

* Fixed the 3 errors caused by questConfirm method in travis-ci
2017-11-27 20:05:05 -06:00
negue
b3870e5f34 multiple market fixes (#9468)
* show `selectMemberModal` to send a card, even if the user doesn't have a party yet

* market - prevent filter reset on pinning items

* hide buy amount for gear, backgrounds, mystery_set, card, rebirth_orb, fortify, armoire - fix mystery set preview in timetravelers

* purchase confirmation on gem / hourglass purchases

* fix lint
2017-11-27 19:54:55 -06:00
kartik adur
29dc56c12f Party roster sorter: Member Modal Component (#9472)
* modify sort options for party members

* add unittest for membersModalComponent sort

* updates as requested in PR

* removed duplicates for `class` and `background` from flavour text

* fix linting error thrown by travis ci
2017-11-27 19:54:13 -06:00
Esben Sparre Andreasen
b62f08d500 Misc. bug fixes from lgtm.com (2) (#9474)
* Remove dead branch of ternary: `gift` is always truthy here

Problem found here:

- https://lgtm.com/projects/g/HabitRPG/habitrpg/snapshot/dist-98076885-1510577633582/files/website/server/libs/amazonPayments.js?sort=name&dir=ASC&mode=heatmap&excluded=false#x5a22f31110a55091:1

* Remove superfluous argument, preenUserHistory only takes two args

Problem found here:

- https://lgtm.com/projects/g/HabitRPG/habitrpg/snapshot/dist-98076885-1510577633582/files/website/server/libs/cron.js?sort=name&dir=ASC&mode=heatmap&excluded=false#xf16a045ecabb07f6:1

* Cleanup: remove useless assignments

Problems found here:

- https://lgtm.com/projects/g/HabitRPG/habitrpg/snapshot/dist-98076885-1510577633582/files/website/client/store/actions/shops.js?sort=name&dir=ASC&mode=heatmap&excluded=false#xf782ed2cf920441%3A1
- https://lgtm.com/projects/g/HabitRPG/habitrpg/snapshot/dist-98076885-1510577633582/files/website/client/app.vue?sort=name&dir=ASC&mode=heatmap&excluded=false#x172c1dda85e84dc8%3A1
- https://lgtm.com/projects/g/HabitRPG/habitrpg/snapshot/dist-98076885-1510577633582/files/website/client/components/settings/site.vue#x9b3afee802a3a8f8%3A1
- https://lgtm.com/projects/g/HabitRPG/habitrpg/snapshot/dist-98076885-1510577633582/files/website/client/components/selectMembersModal.vue?sort=name&dir=ASC&mode=heatmap&excluded=false#x1fbc2a3d62facd70:1
- https://lgtm.com/projects/g/HabitRPG/habitrpg/snapshot/dist-98076885-1510577633582/files/website/common/script/libs/taskClasses.js?sort=name&dir=ASC&mode=heatmap&excluded=false#x41ce0e121a4defee:1

* Fix online editor whitespace change.
2017-11-27 19:51:25 -06:00
Esben Sparre Andreasen
f62177fb1a Misc. bug fixes from lgtm.com (#9325)
* Bugfix: declare variable locally

* Bugfix: fix syntax error

* Bugfix: regex char-class with alternatives

The old implementation used character classes instead of
alternatives. As a consequence, the regex would match:

- a_warrior_0
- r_warrior_0
- m_warrior_0
- o_warrior_0
- r_warrior_0
- |_warrior_0
- h_warrior_0
- ...

The regex will now match:

- armor_warrior_0
- head_warrior_0
- shield_warrior_0
2017-11-27 19:51:02 -06:00
Paul
885f2998ae System messages flaggable (#9408)
* Remove flag from system messages, throw an error if system messages are flagged

* Modify unflag system message test to check if flagging a system message throws an error

* Move email from nconf to top
2017-11-27 19:45:04 -06:00
Paul
2afd96e11c Remove spaces from filters (#9524) 2017-11-27 19:44:33 -06:00
Paul
cd92f44365 Fix difficult to edit checklists in Firefox (#9525)
* Change checklist item hover from move to text

* Add vue draggable

* Add vue draggable

* Replace sortablejs directive with Vue Draggable component

* Indent draggable properties
2017-11-27 19:43:24 -06:00
Paul
863177902a Remove extra 'to' paramater from router link (#9529) 2017-11-27 19:42:53 -06:00
Cassidy Pignatello
96974461e5 Change badge tooltips fixes https://github.com/HabitRPG/habitica/issues/9520 (#9539)
* updates text for costume contestant

* updates text for contributor badge

* removes unnecessary "Badge" text from contribName
2017-11-27 19:37:18 -06:00
Paul
8895b70ffa Adjust left positioning of the left-panel to unobscure sroll-bar, change left panel overflow to overflow-y (#9544) 2017-11-27 19:35:30 -06:00
Kip Raske
03480ebfc7 Passing quantity to the API call when buying quests from the shop (#9565)
You can buy multiple quests from the buy modal, but the quanitity is
never passed to the server. So the client thinks that you are buying the
number you said you did, but the server only buys one regardless. This
can lead to syncing problems down the road.
2017-11-27 19:33:08 -06:00
Joseti
9b8676f02e Changed "Donate" button to "Contribute" button (#9581)
* Changed functionality of "donate"-button on static pages

* Changed strings to reflect change from "donate" to "contribute"
2017-11-27 19:29:45 -06:00
Keith Holliday
3e7738b5b1 Added keys to for loops (#9584) 2017-11-27 12:46:25 -06:00
Keith Holliday
33a235b46c Fixed equiping glasses and ears (#9585) 2017-11-27 12:19:36 -06:00
Keith Holliday
137d6c1f9d Set default empty array when party members haven't loaded (#9579)
* Set default empty array when party members haven't loaded

* Corrected variable usage
2017-11-27 11:59:42 -06:00
Keith Holliday
1a5e820d88 Fixed when user unclaims a task assigned to them (#9578)
* Fixed when user unclaims a task assigned to them

* Removed test

* Fixed lint
2017-11-27 11:38:59 -06:00
Keith Holliday
0c7f9ca6bb Changed search to regex (#9575)
* Changed search to regex

* Changed  to array
2017-11-27 11:10:26 -06:00
Keith Holliday
3e6b3ce3ff Added habitica event for profile display (#9576) 2017-11-27 10:29:20 -06:00
Keith Holliday
ea5ba965e7 Fixed promoting group leader (#9574) 2017-11-27 10:27:30 -06:00
Keith Holliday
7215a550b5 Added page increment when page is loaded to prevent scroll stopping (#9573) 2017-11-27 10:23:04 -06:00
Keith Holliday
3235dfa236 Added equipment filter (#9572) 2017-11-27 10:22:34 -06:00
Keith Holliday
9baf7a7c67 Do not reset item when buying cards (#9571) 2017-11-27 10:22:00 -06:00
Alys
cd629ef7fa add missing @ before a contributor's name in quest content 2017-11-27 09:16:13 +00:00
Keith Holliday
9ef7c45241 Ensured user is saved after validation checks (#9569) 2017-11-23 20:46:02 -06:00
Alys
fef3d09f2d remove words related to alcohol
This is because they're causing problems in housework guilds (alcohol
is used for cleaning) and since it's coming up to Christmas a lot of
guilds are starting to have permissible conversations about drinks,
rum balls, etc.

We might put these back after Christmas but make specific exceptions
for the housework guilds.

When a more advanced word blocker is introduced, we'll be able to
ban alcohol words only in the Tavern. For irony.
2017-11-23 17:52:22 +00:00
Matteo Pagliazzi
53c83c585a fix navbar not showing up in homepage 2017-11-23 16:07:58 +01:00
Keith Holliday
e628c5dc3b Fixed task best color (#9563) 2017-11-21 14:02:53 -06:00
Keith Holliday
9eaa531f66 Amazon payment fixes (#9562)
* Added custom amazon event, removed redundency, fixed variable names

* Fixed more variables and group plan data
2017-11-21 14:02:40 -06:00
Keith Holliday
3ffea4332e Added extra confirmation incase the class modal shows multiple times (#9557) 2017-11-20 15:57:33 -06:00
Sabe Jones
4618fd8954 Merge branch 'release' into develop 2017-11-20 19:21:17 +00:00
Sabe Jones
791c19b5f1 4.12.1 2017-11-20 19:20:54 +00:00
Sabe Jones
7193cc6bae chore(i18n): update locales 2017-11-20 19:20:03 +00:00
Keith Holliday
1845bd1e35 Fixed display of RYA behind bailey (#9555) 2017-11-20 12:38:26 -06:00
Keith Holliday
5f468d16b7 Cancel users free group plan when they leave a group (#9543)
* Cancel users free group plan when they leave a group

* Fixed lint
2017-11-20 12:34:41 -06:00
Keith Holliday
20a99e526d Should do diff week fix (#9551)
* Fixed week difference when changing year

* Added year switchover test

* Fixed start date setting
2017-11-20 12:22:01 -06:00
Keith Holliday
1e69f42d0f Added account transfer migration (#9548)
* Added account transfer migration

* Removed bad comment
2017-11-19 16:53:08 -06:00
Keith Holliday
9c2f5213cb Challenge fixes (#9528)
* Added challenge member search to progress dropdown

* Added leave challenge modal

* Allowed editing for challenge leader only

* Pevented users from editing challenge task info

* Set default progress default to daily

* Removed reward filters from user challenge progress
2017-11-17 17:13:07 -06:00
Sabe Jones
c06d5107ac Merge branch 'release' into develop 2017-11-17 22:50:27 +00:00
Sabe Jones
1eb0f5baa5 4.12.0 2017-11-17 22:50:05 +00:00
Sabe Jones
b28189fff5 chore(i18n): update locales 2017-11-17 22:30:34 +00:00
SabreCat
82497e4041 chore(sprites): compile 2017-11-17 22:22:27 +00:00
SabreCat
2a5e9c0780 feat(event): Turkey Day 2017
and (content) November Subscriber Items
2017-11-17 22:21:05 +00:00
SabreCat
c8ca67aa64 fix(seasonal-shop): short circuit if categories empty 2017-11-17 18:32:55 +00:00
MathWhiz
89e4cbcffe Disable immutable inputs when editing a challenge (#9412)
* Disable uneditable inputs when editing a challenge

* Revert prize display when creating
2017-11-17 23:45:38 +11:00
Keith Holliday
67564317fb Group plan fixes (#9518)
* Fixed group plan editing

* Added translations

* Abstracted query for group or challenge tasks
2017-11-17 20:31:39 +11:00
Keith Holliday
dc2269a307 Hot fixes nov 15 (#9519)
* Added fortify sync. Removed multiple buy

* Added yesterdaily check

* Fixed checks for running yesterdailies
2017-11-17 20:31:29 +11:00
1584 changed files with 66152 additions and 47977 deletions

View File

@@ -1,36 +1,27 @@
language: node_js
node_js:
- '6'
sudo: required
dist: precise
services:
- mongodb
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-4.8
cache:
directories:
- 'node_modules'
before_install:
- $CXX --version
- npm install -g npm@5
- 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
- sleep 15
- sleep 5
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 COVERAGE=true
- TEST="test:api-v3:unit" REQUIRES_SERVER=true COVERAGE=true
- TEST="test:api-v3:integration" REQUIRES_SERVER=true COVERAGE=true
- TEST="test:sanity"
- TEST="test:content" COVERAGE=true
- TEST="test:common" COVERAGE=true

View File

@@ -15,12 +15,12 @@ ENV STRIPE_PUB_KEY pk_85fQ0yMECHNfHTSsZoxZXlPSwSNfA
# The used solution is suggested here https://github.com/npm/npm/issues/16807#issuecomment-313591975
RUN yarn global add npm@5
# Install global packages
RUN npm install -g gulp mocha
RUN npm install -g gulp-cli mocha
# Clone Habitica repo and install dependencies
RUN mkdir -p /usr/src/habitrpg
WORKDIR /usr/src/habitrpg
RUN git clone --branch v4.11.0 https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
RUN git clone --branch v4.26.3 https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
RUN npm install
RUN gulp build:prod --force

View File

@@ -16,5 +16,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.hostname = "habitrpg"
config.vm.network "forwarded_port", guest: 3000, host: 3000, auto_correct: true
config.vm.usable_port_range = (3000..3050)
config.vm.network "forwarded_port", guest: 8080, host: 8080, auto_correct: true
config.vm.usable_port_range = (8080..8130)
config.vm.provision :shell, :path => "vagrant_scripts/vagrant.sh"
end

View File

@@ -22,6 +22,8 @@
"CRON_SEMI_SAFE_MODE":"false",
"MAINTENANCE_MODE": "false",
"SESSION_SECRET":"YOUR SECRET HERE",
"SESSION_SECRET_KEY": "1234567891234567891234567891234567891234567891234567891234567891",
"SESSION_SECRET_IV": "12345678912345678912345678912345",
"ADMIN_EMAIL": "you@example.com",
"SMTP_USER":"user@example.com",
"SMTP_PASS":"password",
@@ -71,6 +73,7 @@
},
"IAP_GOOGLE_KEYDIR": "/path/to/google/public/key/dir/",
"LOGGLY_TOKEN": "token",
"LOGGLY_CLIENT_TOKEN": "token",
"LOGGLY_ACCOUNT": "account",
"PUSH_CONFIGS": {
"GCM_SERVER_API_KEY": "",

View File

@@ -1,3 +1,10 @@
web:
volumes:
- '.:/usr/src/habitrpg'
version: "3"
services:
client:
volumes:
- '.:/usr/src/habitrpg'
server:
volumes:
- '.:/usr/src/habitrpg'

View File

@@ -1,13 +1,36 @@
web:
build: .
ports:
- "3000:3000"
links:
- mongo
environment:
- NODE_DB_URI=mongodb://mongo/habitrpg
version: "3"
services:
mongo:
image: mongo
ports:
- "27017:27017"
client:
build: .
networks:
- habitica
environment:
- BASE_URL=http://server:3000
ports:
- "8080:8080"
command: ["npm", "run", "client:dev"]
depends_on:
- server
server:
build: .
ports:
- "3000:3000"
networks:
- habitica
environment:
- NODE_DB_URI=mongodb://mongo/habitrpg
depends_on:
- mongo
mongo:
image: mongo
ports:
- "27017:27017"
networks:
- habitica
networks:
habitica:
driver: bridge

View File

@@ -8,7 +8,7 @@ gulp.task('apidoc:clean', (done) => {
clean(APIDOC_DEST_PATH, done);
});
gulp.task('apidoc', ['apidoc:clean'], (done) => {
gulp.task('apidoc', gulp.series('apidoc:clean', (done) => {
let result = apidoc.createDoc({
src: APIDOC_SRC_PATH,
dest: APIDOC_DEST_PATH,
@@ -19,8 +19,8 @@ gulp.task('apidoc', ['apidoc:clean'], (done) => {
} else {
done();
}
});
}));
gulp.task('apidoc:watch', ['apidoc'], () => {
return gulp.watch(`${APIDOC_SRC_PATH}/**/*.js`, ['apidoc']);
});
gulp.task('apidoc:watch', gulp.series('apidoc', (done) => {
return gulp.watch(`${APIDOC_SRC_PATH}/**/*.js`, gulp.series('apidoc', done));
}));

View File

@@ -2,12 +2,6 @@ import gulp from 'gulp';
import babel from 'gulp-babel';
import webpackProductionBuild from '../webpack/build';
gulp.task('build', () => {
if (process.env.NODE_ENV === 'production') { // eslint-disable-line no-process-env
gulp.start('build:prod');
}
});
gulp.task('build:src', () => {
return gulp.src('website/server/**/*.js')
.pipe(babel())
@@ -20,18 +14,30 @@ gulp.task('build:common', () => {
.pipe(gulp.dest('website/common/transpiled-babel/'));
});
gulp.task('build:server', ['build:src', 'build:common']);
gulp.task('build:server', gulp.series('build:src', 'build:common', done => done()));
// Client Production Build
gulp.task('build:client', (done) => {
webpackProductionBuild((err, output) => {
if (err) return done(err);
console.log(output); // eslint-disable-line no-console
done();
});
});
gulp.task('build:prod', [
gulp.task('build:prod', gulp.series(
'build:server',
'build:client',
'apidoc',
]);
done => done()
));
let buildArgs = [];
if (process.env.NODE_ENV === 'production') { // eslint-disable-line no-process-env
buildArgs.push('build:prod');
}
gulp.task('build', gulp.series(buildArgs, (done) => {
done();
}));

View File

@@ -1,5 +1,4 @@
import mongoose from 'mongoose';
import autoinc from 'mongoose-id-autoinc';
import logger from '../website/server/libs/logger';
import nconf from 'nconf';
import repl from 'repl';
@@ -25,23 +24,23 @@ let improveRepl = (context) => {
const isProd = nconf.get('NODE_ENV') === 'production';
const mongooseOptions = !isProd ? {} : {
replset: { socketOptions: { keepAlive: 1, connectTimeoutMS: 30000 } },
server: { socketOptions: { keepAlive: 1, connectTimeoutMS: 30000 } },
keepAlive: 1,
connectTimeoutMS: 30000,
useMongoClient: true,
};
autoinc.init(
mongoose.connect(
nconf.get('NODE_DB_URI'),
mongooseOptions,
(err) => {
if (err) throw err;
logger.info('Connected with Mongoose');
}
)
mongoose.connect(
nconf.get('NODE_DB_URI'),
mongooseOptions,
(err) => {
if (err) throw err;
logger.info('Connected with Mongoose');
}
);
};
gulp.task('console', () => {
gulp.task('console', (done) => {
improveRepl(repl.start({
prompt: 'Habitica > ',
}).context);
done();
});

View File

@@ -7,6 +7,7 @@ import mergeStream from 'merge-stream';
import {basename} from 'path';
import {sync} from 'glob';
import {each} from 'lodash';
import vinylBuffer from 'vinyl-buffer';
// https://github.com/Ensighten/grunt-spritesmith/issues/67#issuecomment-34786248
const MAX_SPRITESHEET_SIZE = 1024 * 1024 * 3;
@@ -104,6 +105,7 @@ function createSpritesStream (name, src) {
}));
let imgStream = spriteData.img
.pipe(vinylBuffer())
.pipe(imagemin())
.pipe(gulp.dest(IMG_DIST_PATH));
@@ -117,8 +119,6 @@ function createSpritesStream (name, src) {
return stream;
}
gulp.task('sprites:compile', ['sprites:clean', 'sprites:main', 'sprites:largeSprites', 'sprites:checkCompiledDimensions']);
gulp.task('sprites:main', () => {
let mainSrc = sync('website/raw_sprites/spritesmith/**/*.png');
return createSpritesStream('main', mainSrc);
@@ -133,7 +133,7 @@ gulp.task('sprites:clean', (done) => {
clean(`${IMG_DIST_PATH}spritesmith*,${CSS_DIST_PATH}spritesmith*}`, done);
});
gulp.task('sprites:checkCompiledDimensions', ['sprites:main', 'sprites:largeSprites'], () => {
gulp.task('sprites:checkCompiledDimensions', gulp.series('sprites:main', 'sprites:largeSprites', (done) => {
console.log('Verifiying that images do not exceed max dimensions'); // eslint-disable-line no-console
let numberOfSheetsThatAreTooBig = 0;
@@ -159,4 +159,7 @@ gulp.task('sprites:checkCompiledDimensions', ['sprites:main', 'sprites:largeSpri
} else {
console.log('All images are within the correct dimensions'); // eslint-disable-line no-console
}
});
done();
}));
gulp.task('sprites:compile', gulp.series('sprites:clean', 'sprites:main', 'sprites:largeSprites', 'sprites:checkCompiledDimensions', done => done()));

View File

@@ -3,7 +3,7 @@ import nodemon from 'gulp-nodemon';
let pkg = require('../package.json');
gulp.task('nodemon', () => {
gulp.task('nodemon', (done) => {
nodemon({
script: pkg.main,
ignore: [
@@ -12,4 +12,5 @@ gulp.task('nodemon', () => {
'common/dist/script/content/*',
],
});
done();
});

View File

@@ -4,7 +4,6 @@ import {
import mongoose from 'mongoose';
import { exec } from 'child_process';
import gulp from 'gulp';
import runSequence from 'run-sequence';
import os from 'os';
import nconf from 'nconf';
@@ -39,23 +38,23 @@ let testBin = (string, additionalEnvVariables = '') => {
}
};
gulp.task('test:nodemon', () => {
gulp.task('test:nodemon', gulp.series(function setupNodemon (done) {
process.env.PORT = TEST_SERVER_PORT; // eslint-disable-line no-process-env
process.env.NODE_DB_URI = TEST_DB_URI; // eslint-disable-line no-process-env
runSequence('nodemon');
});
done();
}, 'nodemon'));
gulp.task('test:prepare:mongo', (cb) => {
mongoose.connect(TEST_DB_URI, (err) => {
if (err) return cb(`Unable to connect to mongo database. Are you sure it's running? \n\n${err}`);
mongoose.connection.db.dropDatabase();
mongoose.connection.close();
cb();
mongoose.connection.dropDatabase((err2) => {
if (err2) return cb(err2);
mongoose.connection.close(cb);
});
});
});
gulp.task('test:prepare:server', ['test:prepare:mongo'], () => {
gulp.task('test:prepare:server', gulp.series('test:prepare:mongo', (done) => {
if (!server) {
server = exec(testBin('node ./website/server/index.js', `NODE_DB_URI=${TEST_DB_URI} PORT=${TEST_SERVER_PORT}`), (error, stdout, stderr) => {
if (error) {
@@ -64,16 +63,18 @@ gulp.task('test:prepare:server', ['test:prepare:mongo'], () => {
if (stderr) {
console.error(stderr); // eslint-disable-line no-console
}
done();
});
}
});
}));
gulp.task('test:prepare:build', ['build']);
gulp.task('test:prepare:build', gulp.series('build', done => done()));
gulp.task('test:prepare', [
gulp.task('test:prepare', gulp.series(
'test:prepare:build',
'test:prepare:mongo',
]);
done => done()
));
gulp.task('test:sanity', (cb) => {
let runner = exec(
@@ -88,7 +89,7 @@ gulp.task('test:sanity', (cb) => {
pipe(runner);
});
gulp.task('test:common', ['test:prepare:build'], (cb) => {
gulp.task('test:common', gulp.series('test:prepare:build', (cb) => {
let runner = exec(
testBin(COMMON_TEST_COMMAND),
(err) => {
@@ -99,17 +100,17 @@ gulp.task('test:common', ['test:prepare:build'], (cb) => {
}
);
pipe(runner);
});
}));
gulp.task('test:common:clean', (cb) => {
pipe(exec(testBin(COMMON_TEST_COMMAND), () => cb()));
});
gulp.task('test:common:watch', ['test:common:clean'], () => {
gulp.watch(['common/script/**/*', 'test/common/**/*'], ['test:common:clean']);
});
gulp.task('test:common:watch', gulp.series('test:common:clean', () => {
return gulp.watch(['common/script/**/*', 'test/common/**/*'], gulp.series('test:common:clean', done => done()));
}));
gulp.task('test:common:safe', ['test:prepare:build'], (cb) => {
gulp.task('test:common:safe', gulp.series('test:prepare:build', (cb) => {
let runner = exec(
testBin(COMMON_TEST_COMMAND),
(err, stdout) => { // eslint-disable-line handle-callback-err
@@ -123,9 +124,9 @@ gulp.task('test:common:safe', ['test:prepare:build'], (cb) => {
}
);
pipe(runner);
});
}));
gulp.task('test:content', ['test:prepare:build'], (cb) => {
gulp.task('test:content', gulp.series('test:prepare:build', (cb) => {
let runner = exec(
testBin(CONTENT_TEST_COMMAND),
CONTENT_OPTIONS,
@@ -137,17 +138,17 @@ gulp.task('test:content', ['test:prepare:build'], (cb) => {
}
);
pipe(runner);
});
}));
gulp.task('test:content:clean', (cb) => {
pipe(exec(testBin(CONTENT_TEST_COMMAND), CONTENT_OPTIONS, () => cb()));
});
gulp.task('test:content:watch', ['test:content:clean'], () => {
gulp.watch(['common/script/content/**', 'test/**'], ['test:content:clean']);
});
gulp.task('test:content:watch', gulp.series('test:content:clean', () => {
return gulp.watch(['common/script/content/**', 'test/**'], gulp.series('test:content:clean', done => done()));
}));
gulp.task('test:content:safe', ['test:prepare:build'], (cb) => {
gulp.task('test:content:safe', gulp.series('test:prepare:build', (cb) => {
let runner = exec(
testBin(CONTENT_TEST_COMMAND),
CONTENT_OPTIONS,
@@ -162,7 +163,7 @@ gulp.task('test:content:safe', ['test:prepare:build'], (cb) => {
}
);
pipe(runner);
});
}));
gulp.task('test:api-v3:unit', (done) => {
let runner = exec(
@@ -179,7 +180,7 @@ gulp.task('test:api-v3:unit', (done) => {
});
gulp.task('test:api-v3:unit:watch', () => {
gulp.watch(['website/server/libs/*', 'test/api/v3/unit/**/*', 'website/server/controllers/**/*'], ['test:api-v3:unit']);
return gulp.watch(['website/server/libs/*', 'test/api/v3/unit/**/*', 'website/server/controllers/**/*'], gulp.series('test:api-v3:unit', done => done()));
});
gulp.task('test:api-v3:integration', (done) => {
@@ -198,8 +199,10 @@ gulp.task('test:api-v3:integration', (done) => {
});
gulp.task('test:api-v3:integration:watch', () => {
gulp.watch(['website/server/controllers/api-v3/**/*', 'common/script/ops/*', 'website/server/libs/*.js',
'test/api/v3/integration/**/*'], ['test:api-v3:integration']);
return gulp.watch([
'website/server/controllers/api-v3/**/*', 'common/script/ops/*', 'website/server/libs/*.js',
'test/api/v3/integration/**/*',
], gulp.series('test:api-v3:integration', done => done()));
});
gulp.task('test:api-v3:integration:separate-server', (done) => {
@@ -212,21 +215,17 @@ gulp.task('test:api-v3:integration:separate-server', (done) => {
pipe(runner);
});
gulp.task('test', (done) => {
runSequence(
'test:sanity',
'test:content',
'test:common',
'test:api-v3:unit',
'test:api-v3:integration',
done
);
});
gulp.task('test', gulp.series(
'test:sanity',
'test:content',
'test:common',
'test:api-v3:unit',
'test:api-v3:integration',
done => done()
));
gulp.task('test:api-v3', (done) => {
runSequence(
'test:api-v3:unit',
'test:api-v3:integration',
done
);
});
gulp.task('test:api-v3', gulp.series(
'test:api-v3:unit',
'test:api-v3:integration',
done => done()
));

View File

@@ -93,9 +93,7 @@ const malformedStringExceptions = {
feedPet: true,
};
gulp.task('transifex', ['transifex:missingFiles', 'transifex:missingStrings', 'transifex:malformedStrings']);
gulp.task('transifex:missingFiles', () => {
gulp.task('transifex:missingFiles', (done) => {
let missingStrings = [];
eachTranslationFile(ALL_LANGUAGES, (error) => {
@@ -109,9 +107,10 @@ gulp.task('transifex:missingFiles', () => {
let formattedMessage = formatMessageForPosting(message, missingStrings);
postToSlack(formattedMessage, SLACK_CONFIG);
}
done();
});
gulp.task('transifex:missingStrings', () => {
gulp.task('transifex:missingStrings', (done) => {
let missingStrings = [];
eachTranslationString(ALL_LANGUAGES, (language, filename, key, englishString, translationString) => {
@@ -126,9 +125,10 @@ gulp.task('transifex:missingStrings', () => {
let formattedMessage = formatMessageForPosting(message, missingStrings);
postToSlack(formattedMessage, SLACK_CONFIG);
}
done();
});
gulp.task('transifex:malformedStrings', () => {
gulp.task('transifex:malformedStrings', (done) => {
let jsonFiles = stripOutNonJsonFiles(fs.readdirSync(ENGLISH_LOCALE));
let interpolationRegex = /<%= [a-zA-Z]* %>/g;
let stringsToLookFor = getStringsWith(jsonFiles, interpolationRegex);
@@ -170,4 +170,11 @@ gulp.task('transifex:malformedStrings', () => {
let formattedMessage = formatMessageForPosting(message, stringsWithIncorrectNumberOfInterpolations);
postToSlack(formattedMessage, SLACK_CONFIG);
}
done();
});
gulp.task(
'transifex',
gulp.series('transifex:missingFiles', 'transifex:missingStrings', 'transifex:malformedStrings'),
(done) => done()
);

View File

@@ -8,10 +8,12 @@
require('babel-register');
const gulp = require('gulp');
if (process.env.NODE_ENV === 'production') { // eslint-disable-line no-process-env
require('./gulp/gulp-apidoc'); // eslint-disable-line global-require
require('./gulp/gulp-build'); // eslint-disable-line global-require
} else {
require('glob').sync('./gulp/gulp-*').forEach(require); // eslint-disable-line global-require
require('gulp').task('default', ['test']); // eslint-disable-line global-require
require('gulp').task('default', gulp.series('test')); // eslint-disable-line global-require
}

View File

@@ -16,7 +16,7 @@ var migrationName = '20140831_increase_gems_for_previous_contributions';
* https://github.com/HabitRPG/habitrpg/issues/3933
* Increase Number of Gems for Contributors
* author: Alys (d904bd62-da08-416b-a816-ba797c9ee265)
*
*
* Increase everyone's gems per their contribution level.
* Originally they were given 2 gems per tier.
* Now they are given 3 gems per tier for tiers 1,2,3
@@ -70,7 +70,7 @@ dbUsers.findEach(query, fields, function(err, user) {
var extraGems = tier; // tiers 1,2,3
if (tier > 3) { extraGems = 3 + (tier - 3) * 2; }
if (tier == 8) { extraGems = 11; }
extraBalance = extraGems / 4;
var extraBalance = extraGems / 4;
set['balance'] = user.balance + extraBalance;
// Capture current state of user:

View File

@@ -39,7 +39,7 @@ function findUsers(gt){
console.log('User: ', countUsers, user._id);
var update = {
$set: {};
$set: {}
};
if(user.auth && user.auth.local) {
@@ -60,4 +60,4 @@ function findUsers(gt){
});
};
findUsers();
findUsers();

View File

@@ -0,0 +1,128 @@
var migrationName = '20171117_turkey_ladder.js';
var authorName = 'Sabe'; // in case script author needs to know when their ...
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
/*
* Award the Turkey Day ladder:
* Grant Turkey Costume to those who have the Gilded Turkey mount
* Grant Gilded Turkey mount to those who have the Gilded Turkey pet
* Grant Gilded Turkey pet to those who have the Base Turkey mount
* Grant Base Turkey mount to those who have the Base Turkey pet
* Grant Base Turkey pet to those who have none of the above yet
*/
var monk = require('monk');
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 = {
'migration':{$ne:migrationName},
'auth.timestamps.loggedin':{$gt:new Date('2017-11-01')},
};
if (lastId) {
query._id = {
$gt: lastId
}
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 250,
fields: [
'items.pets',
'items.mounts',
] // 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 = {};
if (user && user.items && user.items.mounts && user.items.mounts['Turkey-Gilded']) {
set = {
migration: migrationName,
'items.gear.owned.head_special_turkeyHelmBase': false,
'items.gear.owned.armor_special_turkeyArmorBase': false,
'items.gear.owned.back_special_turkeyTailBase': false,
};
var push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_turkeyHelmBase',
_id: monk.id(),
},
{
type: 'marketGear',
path: 'gear.flat.armor_special_turkeyArmorBase',
_id: monk.id(),
},
{
type: 'marketGear',
path: 'gear.flat.back_special_turkeyTailBase',
_id: monk.id(),
},
];
} else if (user && user.items && user.items.pets && user.items.pets['Turkey-Gilded']) {
set = {'migration':migrationName, 'items.mounts.Turkey-Gilded':true};
} else if (user && user.items && user.items.mounts && user.items.mounts['Turkey-Base']) {
set = {'migration':migrationName, 'items.pets.Turkey-Gilded':5};
} else if (user && user.items && user.items.pets && user.items.pets['Turkey-Base']) {
set = {'migration':migrationName, 'items.mounts.Turkey-Base':true};
} else {
set = {'migration':migrationName, 'items.pets.Turkey-Base':5};
}
dbUsers.update({_id: user._id}, {$set: set});
if (push) {
dbUsers.update({_id: user._id}, {$push: {pinnedItems: {$each: push}}});
}
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;

View File

@@ -0,0 +1,103 @@
var migrationName = '20171230_nye_hats.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 New Year's Eve party hats to users in sequence
*/
var monk = require('monk');
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 = {
'migration': {$ne:migrationName},
'auth.timestamps.loggedin': {$gt:new Date('2017-11-30')},
};
if (lastId) {
query._id = {
$gt: lastId
}
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 250,
fields: [
'items.gear.owned',
] // 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 = {};
var push = {};
if (typeof user.items.gear.owned.head_special_nye2016 !== 'undefined') {
set = {'migration':migrationName, 'items.gear.owned.head_special_nye2017':false};
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.head_special_nye2017', '_id': monk.id()}};
} else if (typeof user.items.gear.owned.head_special_nye2015 !== 'undefined') {
set = {'migration':migrationName, 'items.gear.owned.head_special_nye2016':false};
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.head_special_nye2016', '_id': monk.id()}};
} else if (typeof user.items.gear.owned.head_special_nye2014 !== 'undefined') {
set = {'migration':migrationName, 'items.gear.owned.head_special_nye2015':false};
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.head_special_nye2015', '_id': monk.id()}};
} else if (typeof user.items.gear.owned.head_special_nye !== 'undefined') {
set = {'migration':migrationName, 'items.gear.owned.head_special_nye2014':false};
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.head_special_nye2014', '_id': monk.id()}};
} else {
set = {'migration':migrationName, 'items.gear.owned.head_special_nye':false};
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.head_special_nye', '_id': monk.id()}};
}
dbUsers.update({_id: user._id}, {$set: set, $push: push});
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;

View File

@@ -0,0 +1,79 @@
/*
* Convert purchased.plan.nextPaymentProcessing from a double to a date field for Apple 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 });
function processUsers(lastId) {
// specify a query to limit the affected users (empty for all users):
var query = {
'purchased.plan.paymentMethod': "Apple",
'purchased.plan.nextPaymentProcessing': {$type: 'double'},
};
if (lastId) {
query._id = {
$gt: lastId
}
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 250,
})
.then(updateUsers)
.catch(function (err) {
console.log(err);
return exiting(1, 'ERROR! ' + err);
});
}
var progressCount = 100;
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 = {
'purchased.plan.nextPaymentProcessing': new Date(user.purchased.plan.nextPaymentProcessing),
};
dbUsers.update({_id: user._id}, {$set: set});
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
}
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;

View File

@@ -0,0 +1,93 @@
const UserNotification = require('../website/server/models/userNotification').model;
const content = require('../website/common/script/content/index');
const migrationName = '20180125_clean_new_migrations';
const authorName = 'paglias'; // in case script author needs to know when their ...
const authorUuid = 'ed4c688c-6652-4a92-9d03-a5a79844174a'; // ... own data is done
/*
* Clean new migration types for processed users
*/
const monk = require('monk');
const connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
const dbUsers = monk(connectionString).get('users', { castIds: false });
const progressCount = 1000;
let count = 0;
function updateUser (user) {
count++;
const types = ['NEW_MYSTERY_ITEMS', 'CARD_RECEIVED', 'NEW_CHAT_MESSAGE'];
dbUsers.update({_id: user._id}, {
$pull: {notifications: { type: {$in: types } } },
$set: {migration: migrationName},
});
if (count % progressCount === 0) console.warn(`${count } ${ user._id}`);
if (user._id === authorUuid) console.warn(`${authorName } processed`);
}
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);
}
function displayData () {
console.warn(`\n${ count } users processed\n`);
return exiting(0);
}
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
displayData();
return;
}
const userPromises = users.map(updateUser);
const lastUser = users[users.length - 1];
return Promise.all(userPromises)
.then(() => {
processUsers(lastUser._id);
});
}
function processUsers (lastId) {
// specify a query to limit the affected users (empty for all users):
const query = {
migration: {$ne: migrationName},
'auth.timestamps.loggedin': {$gt: new Date('2010-01-24')},
};
if (lastId) {
query._id = {
$gt: lastId,
};
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 250,
})
.then(updateUsers)
.catch((err) => {
console.log(err);
return exiting(1, `ERROR! ${ err}`);
});
}
module.exports = processUsers;

View File

@@ -0,0 +1,149 @@
const UserNotification = require('../website/server/models/userNotification').model;
const content = require('../website/common/script/content/index');
const migrationName = '20180125_migrations-v2';
const authorName = 'paglias'; // in case script author needs to know when their ...
const authorUuid = 'ed4c688c-6652-4a92-9d03-a5a79844174a'; // ... own data is done
/*
* Migrate to new notifications system
*/
const monk = require('monk');
const connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
const dbUsers = monk(connectionString).get('users', { castIds: false });
const progressCount = 1000;
let count = 0;
function updateUser (user) {
count++;
const notifications = [];
// UNALLOCATED_STATS_POINTS skipped because added on each save
// NEW_STUFF skipped because it's a new type
// GROUP_TASK_NEEDS_WORK because it's a new type
// NEW_INBOX_MESSAGE not implemented yet
// NEW_MYSTERY_ITEMS
const mysteryItems = user.purchased && user.purchased.plan && user.purchased.plan.mysteryItems;
if (Array.isArray(mysteryItems) && mysteryItems.length > 0) {
const newMysteryNotif = new UserNotification({
type: 'NEW_MYSTERY_ITEMS',
data: {
items: mysteryItems,
},
}).toJSON();
notifications.push(newMysteryNotif);
}
// CARD_RECEIVED
Object.keys(content.cardTypes).forEach(cardType => {
const existingCards = user.items.special[`${cardType}Received`] || [];
existingCards.forEach(sender => {
const newNotif = new UserNotification({
type: 'CARD_RECEIVED',
data: {
card: cardType,
from: {
// id is missing in old notifications
name: sender,
},
},
}).toJSON();
notifications.push(newNotif);
});
});
// NEW_CHAT_MESSAGE
Object.keys(user.newMessages).forEach(groupId => {
const existingNotif = user.newMessages[groupId];
if (existingNotif) {
const newNotif = new UserNotification({
type: 'NEW_CHAT_MESSAGE',
data: {
group: {
id: groupId,
name: existingNotif.name,
},
},
}).toJSON();
notifications.push(newNotif);
}
});
dbUsers.update({_id: user._id}, {
$push: {notifications: { $each: notifications } },
$set: {migration: migrationName},
});
if (count % progressCount === 0) console.warn(`${count } ${ user._id}`);
if (user._id === authorUuid) console.warn(`${authorName } processed`);
}
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);
}
function displayData () {
console.warn(`\n${ count } users processed\n`);
return exiting(0);
}
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
displayData();
return;
}
const userPromises = users.map(updateUser);
const lastUser = users[users.length - 1];
return Promise.all(userPromises)
.then(() => {
processUsers(lastUser._id);
});
}
function processUsers (lastId) {
// specify a query to limit the affected users (empty for all users):
const query = {
migration: {$ne: migrationName},
'auth.timestamps.loggedin': {$gt: new Date('2010-01-24')},
};
if (lastId) {
query._id = {
$gt: lastId,
};
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 250,
})
.then(updateUsers)
.catch((err) => {
console.log(err);
return exiting(1, `ERROR! ${ err}`);
});
}
module.exports = processUsers;

View File

@@ -0,0 +1,118 @@
var migrationName = '20180130_habit_birthday.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 party robes: most recent user doesn't have of 2014-2018. Also cake!
*/
var monk = require('monk');
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 = {
'migration':{$ne:migrationName},
'auth.timestamps.loggedin':{$gt:new Date('2018-01-01')}, // remove after first run to cover remaining users
};
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)
'items.gear.owned'
],
})
.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 push;
var set = {'migration':migrationName};
if (user.items && user.items.gear && user.items.gear.owned && user.items.gear.owned.hasOwnProperty('armor_special_birthday2017')) {
set['items.gear.owned.armor_special_birthday2018'] = false;
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.armor_special_birthday2018', '_id': monk.id()}};
} else if (user.items && user.items.gear && user.items.gear.owned && user.items.gear.owned.hasOwnProperty('armor_special_birthday2016')) {
set['items.gear.owned.armor_special_birthday2017'] = false;
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.armor_special_birthday2017', '_id': monk.id()}};
} else if (user.items && user.items.gear && user.items.gear.owned && user.items.gear.owned.hasOwnProperty('armor_special_birthday2015')) {
set['items.gear.owned.armor_special_birthday2016'] = false;
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.armor_special_birthday2016', '_id': monk.id()}};
} else if (user.items && user.items.gear && user.items.gear.owned && user.items.gear.owned.hasOwnProperty('armor_special_birthday')) {
set['items.gear.owned.armor_special_birthday2015'] = false;
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.armor_special_birthday2015', '_id': monk.id()}};
} else {
set['items.gear.owned.armor_special_birthday'] = false;
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.armor_special_birthday', '_id': monk.id()}};
}
var inc = {
'items.food.Cake_Skeleton':1,
'items.food.Cake_Base':1,
'items.food.Cake_CottonCandyBlue':1,
'items.food.Cake_CottonCandyPink':1,
'items.food.Cake_Shade':1,
'items.food.Cake_White':1,
'items.food.Cake_Golden':1,
'items.food.Cake_Zombie':1,
'items.food.Cake_Desert':1,
'items.food.Cake_Red':1,
'achievements.habitBirthdays':1
};
dbUsers.update({_id: user._id}, {$set: set, $inc: inc, $push: push});
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;

View File

@@ -0,0 +1,59 @@
# Indexes
This file contains a list of indexes that are on Habitica's production Mongo server.
If we ever have an issue, use this list to reindex.
## Challenges
- `{ "group": 1, "official": -1, "timestamp": -1 }`
- `{ "leader": 1, "official": -1, "timestamp": -1 }`
- `{ "official": -1, "timestamp": -1 }`
## Groups
- `{ "privacy": 1, "type": 1, "memberCount": -1 }`
- `{ "privacy": 1 }`
- `{ "purchased.plan.customerId": 1 }`
- `{ "purchased.plan.paymentMethod": 1 }`
- `{ "purchased.plan.planId": 1, "purchased.plan.dateTerminated": 1 }`
- `{ "type": 1, "memberCount": -1, "_id": 1 }`
- `{ "type": 1 }`
## Tasks
- `{ "challenge.id": 1 }`
- `{ "challenge.taskId": 1 }`
- `{ "group.id": 1 }`
- `{ "group.taskId": 1 }`
- `{ "type": 1, "everyX": 1, "frequency": 1 }`
- `{ "userId": 1 }`
- `{ "yesterDaily": 1, "type": 1 }`
## Users
- `{ "_id": 1, "apiToken": 1 }`
- `{ "auth.facebook.emails.value": 1 }`
- `{ "auth.facebook.id": 1 }`
- `{ "auth.google.emails.value": 1 }`
- `{ "auth.google.id": 1 }`
- `{ "auth.local.email": 1 }`
- `{ "auth.local.lowerCaseUsername": 1 }`
- `{ "auth.local.username": 1 }`
- `{ "auth.timestamps.created": 1 }`
- `{ "auth.timestamps.loggedin": 1, "_lastPushNotification": 1, "preferences.timezoneOffset": 1 }`
- `{ "auth.timestamps.loggedin": 1 }`
- `{ "backer.tier": -1 }`
- `{ "challenges": 1, "_id": 1 }`
- `{ "contributor.admin": 1, "contributor.level": -1, "backer.npc": -1, "profile.name": 1 }`
- `{ "contributor.level": 1 }`
- `{ "flags.newStuff": 1 }`
- `{ "flags.armoireEmpty": 1 }`
- `{ "guilds": 1, "_id": 1 }`
- `{ "invitations.guilds.id": 1, "_id": 1 }`
- `{ "invitations.party.id": 1 }`
- `{ "loginIncentives": 1 }`
- `{ "migration": 1 }`
- `{ "party._id": 1, "_id": 1 }`
- `{ "preferences.sleep": 1, "_id": 1, "flags.lastWeeklyRecap": 1, "preferences.emailNotifications.unsubscribeFromAll": 1, "preferences.emailNotifications.weeklyRecaps": 1 }`
- `{ "preferences.sleep": 1, "_id": 1, "lastCron": 1, "preferences.emailNotifications.importantAnnouncements": 1, "preferences.emailNotifications.unsubscribeFromAll": 1, "flags.recaptureEmailsPhase": 1 }`
- `{ "profile.name": 1 }`
- `{ "purchased.plan.customerId": 1 }`
- `{ "purchased.plan.paymentMethod": 1 }`
- `{ "stats.score.overall": 1 }`
- `{ "webhooks.type": 1 }`

View File

@@ -17,8 +17,5 @@ function setUpServer () {
setUpServer();
// Replace this with your migration
var processUsers = require('./groups/update-groups-with-group-plans');
processUsers()
.catch(function (err) {
console.log(err)
})
const processUsers = require('./20180125_clean_new_notifications.js');
processUsers();

View File

@@ -1,10 +1,23 @@
var UserNotification = require('../website/server/models/userNotification').model
var _id = '';
var items = ['back_mystery_201801','headAccessory_mystery_201801']
var update = {
$addToSet: {
'purchased.plan.mysteryItems':{
$each:['armor_mystery_201710','head_mystery_201710']
$each: items,
}
}
},
$push: {
notifications: (new UserNotification({
type: 'NEW_MYSTERY_ITEMS',
data: {
items: items,
},
})).toJSON(),
},
};
/*var update = {

View File

@@ -18,71 +18,94 @@ var authorUuid = '3e595299-3d8a-4a10-bfe0-88f555e4aa0c'; //... own data is done
*
*/
var dbserver = 'localhost:27017'; // FOR TEST DATABASE
var dbserver = 'username:password@ds031379-a0.mongolab.com:31379'; // FOR PRODUCTION DATABASE
var dbname = 'habitrpg';
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
var mongo = require('mongoskin');
var _ = require('lodash');
var monk = require('monk');
var dbUsers = monk(connectionString).get('users', { castIds: false });
var dbUsers = mongo.db(dbserver + '/' + dbname + '?auto_reconnect').collection('users');
// specify a query to limit the affected users (empty for all users):
var query = {
'auth.timestamps.loggedin':{$gt:new Date('2016-01-04')}
// '_id': authorUuid // FOR TESTING
};
function processUsers(lastId) {
// specify a query to limit the affected users (empty for all users):
var query = {
'auth.timestamps.loggedin':{$gt:new Date('2016-01-04')}
// '_id': authorUuid // FOR TESTING
};
// specify fields we are interested in to limit retrieved data (empty if we're not reading data):
var fields = {
'flags.armoireEmpty':1,
'items.gear.owned':1
};
// specify a query to limit the affected users (empty for all users):
var fields = {
'flags.armoireEmpty':1,
'items.gear.owned':1
};
// specify user data to change:
var set = {'migration':migrationName, 'flags.armoireEmpty':false};
console.warn('Updating users...');
var progressCount = 1000;
var countSearched = 0;
var countModified = 0;
dbUsers.findEach(query, fields, {batchSize:250}, function(err, user) {
if (err) { return exiting(1, 'ERROR! ' + err); }
if (!user) {
console.warn('All appropriate users found and modified.');
return displayData();
if (lastId) {
query._id = {
$gt: lastId
}
}
countSearched++;
dbUsers.find(query, {
sort: {_id: 1},
limit: 250,
fields: {
'flags.armoireEmpty':1,
'items.gear.owned':1
} // 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 = {'migration':migrationName, 'flags.armoireEmpty':false};
if (user.flags.armoireEmpty) {
// this user believes their armoire has no more items in it
if (user.items.gear.owned.weapon_armoire_barristerGavel && user.items.gear.owned.armor_armoire_barristerRobes && user.items.gear.owned.head_armoire_jesterCap && user.items.gear.owned.armor_armoire_jesterCostume && user.items.gear.owned.head_armoire_barristerWig && user.items.gear.owned.weapon_armoire_jesterBaton && user.items.gear.owned.weapon_armoire_lunarSceptre && user.items.gear.owned.armor_armoire_gladiatorArmor && user.items.gear.owned.weapon_armoire_basicCrossbow && user.items.gear.owned.head_armoire_gladiatorHelm && user.items.gear.owned.armor_armoire_lunarArmor && user.items.gear.owned.head_armoire_redHairbow && user.items.gear.owned.head_armoire_violetFloppyHat && user.items.gear.owned.head_armoire_rancherHat && user.items.gear.owned.shield_armoire_gladiatorShield && user.items.gear.owned.head_armoire_blueHairbow && user.items.gear.owned.weapon_armoire_mythmakerSword && user.items.gear.owned.head_armoire_royalCrown && user.items.gear.owned.head_armoire_hornedIronHelm && user.items.gear.owned.weapon_armoire_rancherLasso && user.items.gear.owned.armor_armoire_rancherRobes && user.items.gear.owned.armor_armoire_hornedIronArmor && user.items.gear.owned.armor_armoire_goldenToga && user.items.gear.owned.weapon_armoire_ironCrook && user.items.gear.owned.head_armoire_goldenLaurels && user.items.gear.owned.head_armoire_redFloppyHat && user.items.gear.owned.armor_armoire_plagueDoctorOvercoat && user.items.gear.owned.head_armoire_plagueDoctorHat && user.items.gear.owned.weapon_armoire_goldWingStaff && user.items.gear.owned.head_armoire_yellowHairbow && user.items.gear.owned.eyewear_armoire_plagueDoctorMask && user.items.gear.owned.head_armoire_blackCat && user.items.gear.owned.weapon_armoire_batWand && user.items.gear.owned.head_armoire_orangeCat && user.items.gear.owned.shield_armoire_midnightShield && user.items.gear.owned.armor_armoire_royalRobes && user.items.gear.owned.head_armoire_blueFloppyHat && user.items.gear.owned.shield_armoire_royalCane && user.items.gear.owned.weapon_armoire_shepherdsCrook && user.items.gear.owned.armor_armoire_shepherdRobes && user.items.gear.owned.head_armoire_shepherdHeaddress && user.items.gear.owned.weapon_armoire_blueLongbow && user.items.gear.owned.weapon_armoire_crystalCrescentStaff && user.items.gear.owned.head_armoire_crystalCrescentHat && user.items.gear.owned.armor_armoire_dragonTamerArmor && user.items.gear.owned.head_armoire_dragonTamerHelm && user.items.gear.owned.armor_armoire_crystalCrescentRobes && user.items.gear.owned.shield_armoire_dragonTamerShield && user.items.gear.owned.weapon_armoire_glowingSpear) {
// this user does have all the armoire items so we don't change the flag
// console.log("don't change: " + user._id); // FOR TESTING
// console.log("don't change: " + user._id); // FOR TESTING
} else {
// console.log("change: " + user._id); // FOR TESTING
dbUsers.update({_id: user._id}, {$set: set});
}
else {
countModified++;
// console.log("change: " + user._id); // FOR TESTING
dbUsers.update({_id:user._id}, {$set:set});
}
}
else {
} else {
// this user already has armoire marked as containing items to be bought
// so don't change the flag
// console.log("DON'T CHANGE: " + user._id); // FOR TESTING
// console.log("DON'T CHANGE: " + user._id); // FOR TESTING
}
if (countSearched%progressCount == 0) console.warn(countSearched + ' ' + user._id);
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
if (user._id == authorUuid) console.warn(authorName + ' processed');
});
function displayData() {
console.warn('\n' + countSearched + ' users searched\n');
console.warn('\n' + countModified + ' users modified\n');
return exiting(0);
}
function displayData() {
console.warn('\n' + count + ' users processed\n');
return exiting(0);
}
function exiting(code, msg) {
code = code || 0; // 0 = success
@@ -93,3 +116,5 @@ function exiting(code, msg) {
}
process.exit(code);
}
module.exports = processUsers;

View File

@@ -1,4 +1,4 @@
var migrationName = '20170502_takeThis.js'; // Update per month
var migrationName = '20180102_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
@@ -7,14 +7,14 @@ var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
*/
var monk = require('monk');
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
var connectionString = 'mongodb://sabrecat:z8e8jyRA8CTofMQ@ds013393-a0.mlab.com:13393/habitica?auto_reconnect=true';
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 = {
'migration':{$ne:migrationName},
'challenges':{$in:['69999331-d4ea-45a0-8c3f-f725d22b56c8']} // Update per month
'challenges':{$in:['5f70ce5b-2d82-4114-8e44-ca65615aae62']} // Update per month
};
if (lastId) {

View File

@@ -0,0 +1,88 @@
var migrationName = 'tasks-set-everyX';
var authorName = 'Sabe'; // in case script author needs to know when their ...
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
/*
* Iterates over all tasks and sets invalid everyX values (less than 0 or more than 9999 or not an int) field to 0
*/
var monk = require('monk');
var connectionString = 'mongodb://sabrecat:z8e8jyRA8CTofMQ@ds013393-a0.mlab.com:13393/habitica?auto_reconnect=true';
var dbTasks = monk(connectionString).get('tasks', { castIds: false });
function processTasks(lastId) {
// specify a query to limit the affected tasks (empty for all tasks):
var query = {
type: "daily",
everyX: {
$not: {
$gte: 0,
$lte: 9999,
$type: "int",
}
},
};
if (lastId) {
query._id = {
$gt: lastId
}
}
dbTasks.find(query, {
sort: {_id: 1},
limit: 250,
fields: [],
})
.then(updateTasks)
.catch(function (err) {
console.log(err);
return exiting(1, 'ERROR! ' + err);
});
}
var progressCount = 1000;
var count = 0;
function updateTasks (tasks) {
if (!tasks || tasks.length === 0) {
console.warn('All appropriate tasks found and modified.');
displayData();
return;
}
var taskPromises = tasks.map(updatetask);
var lasttask = tasks[tasks.length - 1];
return Promise.all(taskPromises)
.then(function () {
processTasks(lasttask._id);
});
}
function updatetask (task) {
count++;
var set = {'everyX': 0};
dbTasks.update({_id: task._id}, {$set:set});
if (count % progressCount == 0) console.warn(count + ' ' + task._id);
if (task._id == authorUuid) console.warn(authorName + ' processed');
}
function displayData() {
console.warn('\n' + count + ' tasks 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 = processTasks;

View File

@@ -0,0 +1,38 @@
var migrationName = 'AccountTransfer';
var authorName = 'TheHollidayInn'; // in case script author needs to know when their ...
var authorUuid = ''; //... own data is done
/*
* This migraition will copy user data from prod to test
*/
const monk = require('monk');
const connectionString = '';
const Users = monk(connectionString).get('users', { castIds: false });
import uniq from 'lodash/uniq';
import Bluebird from 'bluebird';
module.exports = async function accountTransfer () {
const fromAccountId = '';
const toAccountId = '';
const fromAccount = await Users.findOne({_id: fromAccountId});
const toAccount = await Users.findOne({_id: toAccountId});
const newMounts = Object.assign({}, fromAccount.items.mounts, toAccount.items.mounts);
const newPets = Object.assign({}, fromAccount.items.pets, toAccount.items.pets);
const newBackgrounds = Object.assign({}, fromAccount.purchased.background, toAccount.purchased.background);
await Users.update({_id: toAccountId}, {
$set: {
'items.pets': newPets,
'items.mounts': newMounts,
'purchased.background': newBackgrounds,
},
})
.then((result) => {
console.log(result);
});
};

View File

@@ -0,0 +1,93 @@
const migrationName = 'AchievementRestore';
const authorName = 'TheHollidayInn'; // in case script author needs to know when their ...
const authorUuid = ''; //... own data is done
/*
* This migraition will copy user data from prod to test
*/
import Bluebird from 'bluebird';
const monk = require('monk');
const connectionString = 'mongodb://localhost/new-habit';
const Users = monk(connectionString).get('users', { castIds: false });
const monkOld = require('monk');
const oldConnectionSting = 'mongodb://localhost/old-habit';
const UsersOld = monk(oldConnectionSting).get('users', { castIds: false });
function getAchievementUpdate (newUser, oldUser) {
const oldAchievements = oldUser.achievements;
const newAchievements = newUser.achievements;
let achievementsUpdate = Object.assign({}, newAchievements);
// ultimateGearSets
if (!achievementsUpdate.ultimateGearSets && oldAchievements.ultimateGearSets) {
achievementsUpdate.ultimateGearSets = oldAchievements.ultimateGearSets;
} else if (oldAchievements.ultimateGearSets) {
for (let index in oldAchievements.ultimateGearSets) {
if (oldAchievements.ultimateGearSets[index]) achievementsUpdate.ultimateGearSets[index] = true;
}
}
// challenges
if (!newAchievements.challenges) newAchievements.challenges = [];
if (!oldAchievements.challenges) oldAchievements.challenges = [];
achievementsUpdate.challenges = newAchievements.challenges.concat(oldAchievements.challenges);
// Quests
if (!achievementsUpdate.quests) achievementsUpdate.quests = {};
for (let index in oldAchievements.quests) {
if (!achievementsUpdate.quests[index]) {
achievementsUpdate.quests[index] = oldAchievements.quests[index];
} else {
achievementsUpdate.quests[index] += oldAchievements.quests[index];
}
}
// Rebirth level
if (achievementsUpdate.rebirthLevel) {
achievementsUpdate.rebirthLevel = Math.max(achievementsUpdate.rebirthLevel, oldAchievements.rebirthLevel);
} else if (oldAchievements.rebirthLevel) {
achievementsUpdate.rebirthLevel = oldAchievements.rebirthLevel;
}
//All others
const indexsToIgnore = ['ultimateGearSets', 'challenges', 'quests', 'rebirthLevel'];
for (let index in oldAchievements) {
if (indexsToIgnore.indexOf(index) !== -1) continue;
if (!achievementsUpdate[index]) {
achievementsUpdate[index] = oldAchievements[index];
continue;
}
if (Number.isInteger(oldAchievements[index])) {
achievementsUpdate[index] += oldAchievements[index];
} else {
if (oldAchievements[index] === true) achievementsUpdate[index] = true;
}
}
return achievementsUpdate;
}
module.exports = async function achievementRestore () {
const userIds = [
];
for (let index in userIds) {
const userId = userIds[index];
const oldUser = await UsersOld.findOne({_id: userId}, 'achievements');
const newUser = await Users.findOne({_id: userId}, 'achievements');
const achievementUpdate = getAchievementUpdate(newUser, oldUser);
await Users.update(
{_id: userId},
{
$set: {
'achievements': achievementUpdate,
},
});
console.log(`Updated ${userId}`);
}
};

12138
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "4.11.1",
"version": "4.27.0",
"main": "./website/server/index.js",
"dependencies": {
"@slack/client": "^3.8.1",
@@ -10,7 +10,6 @@
"amplitude": "^2.0.3",
"apidoc": "^0.17.5",
"apn": "^1.7.6",
"async": "^1.5.0",
"autoprefixer": "^6.4.0",
"aws-sdk": "^2.0.25",
"axios": "^0.16.0",
@@ -27,106 +26,89 @@
"babel-preset-es2015": "^6.6.0",
"babel-register": "^6.6.0",
"babel-runtime": "^6.11.6",
"babelify": "^7.2.0",
"bcrypt": "^1.0.2",
"bluebird": "^3.3.5",
"body-parser": "^1.15.0",
"bootstrap": "4.0.0-beta.2",
"bootstrap-vue": "^1.0.2",
"browserify": "~12.0.1",
"bootstrap": "^4.0.0",
"bootstrap-vue": "^1.5.0",
"compression": "^1.6.1",
"connect-ratelimit": "0.0.7",
"cookie-session": "^1.2.0",
"coupon-code": "^0.4.5",
"cross-env": "^4.0.0",
"cross-env": "^5.1.3",
"css-loader": "^0.28.0",
"csv-stringify": "^1.0.2",
"cwait": "~1.0.1",
"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",
"file-loader": "^0.10.0",
"glob": "^4.3.5",
"glob": "^7.1.2",
"got": "^6.1.1",
"gulp": "^3.9.0",
"gulp": "^4.0.0",
"gulp-babel": "^6.1.2",
"gulp-imagemin": "^2.4.0",
"gulp-nodemon": "^2.0.4",
"gulp-sourcemaps": "^1.6.0",
"gulp-uglify": "^1.4.2",
"gulp.spritesmith": "^4.1.0",
"gulp-imagemin": "^4.1.0",
"gulp-nodemon": "^2.2.1",
"gulp.spritesmith": "^6.9.0",
"habitica-markdown": "^1.3.0",
"hellojs": "^1.15.1",
"html-webpack-plugin": "^2.8.1",
"image-size": "~0.3.2",
"image-size": "^0.6.2",
"in-app-purchase": "^1.1.6",
"intro.js": "^2.6.0",
"jade": "~1.11.0",
"jquery": ">=3.0.0",
"js2xmlparser": "~1.0.0",
"lodash": "^4.17.4",
"merge-stream": "^1.0.0",
"method-override": "^2.3.5",
"moment": "^2.13.0",
"moment-recur": "git://github.com/habitrpg/moment-recur#f147ef27bbc26ca67638385f3db4a44084c76626",
"mongoose": "~4.8.6",
"mongoose-id-autoinc": "~2013.7.14-4",
"moment-recur": "git://github.com/habitrpg/moment-recur.git#f147ef27bbc26ca67638385f3db4a44084c76626",
"mongoose": "^4.13.10",
"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",
"pageres": "^4.1.1",
"passport": "^0.3.2",
"passport-facebook": "^2.0.0",
"passport-google-oauth20": "1.0.0",
"paypal-ipn": "3.0.0",
"paypal-rest-sdk": "^1.2.1",
"popper.js": "^1.11.0",
"paypal-rest-sdk": "^1.8.1",
"popper.js": "^1.13.0",
"postcss-easy-import": "^2.0.0",
"pretty-data": "^0.40.0",
"ps-tree": "^1.0.0",
"pug": "^2.0.0-beta.12",
"push-notify": "git://github.com/habitrpg/push-notify#6bc2b5fdb1bdc9649b9ec1964d79ca50187fc8a9",
"pug": "^2.0.0-rc.4",
"push-notify": "git://github.com/habitrpg/push-notify.git#6bc2b5fdb1bdc9649b9ec1964d79ca50187fc8a9",
"pusher": "^1.3.0",
"request": "~2.74.0",
"request": "^2.83.0",
"rimraf": "^2.4.3",
"run-sequence": "^1.1.4",
"s3-upload-stream": "^1.0.6",
"sass-loader": "^6.0.2",
"serve-favicon": "^2.3.0",
"shelljs": "^0.7.6",
"sortablejs": "^1.6.1",
"shelljs": "^0.8.1",
"stripe": "^4.2.0",
"superagent": "^3.4.3",
"svg-inline-loader": "^0.7.1",
"svg-url-loader": "^2.0.2",
"svgo-loader": "^1.2.1",
"universal-analytics": "~0.3.2",
"universal-analytics": "^0.4.16",
"url-loader": "^0.5.7",
"useragent": "^2.1.9",
"uuid": "^3.0.1",
"validator": "^4.9.0",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0",
"vinyl-buffer": "^1.0.1",
"vue": "^2.5.2",
"vue-loader": "^13.3.0",
"vue-mugen-scroll": "^0.2.1",
"vue-router": "^3.0.0",
"vue-style-loader": "^3.0.0",
"vue-template-compiler": "^2.5.2",
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker#825a866b6a9c52dd8c588a3e8b900880875ce914",
"vuedraggable": "^2.15.0",
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#5d237615463a84a23dd6f3f77c6ab577d68593ec",
"webpack": "^2.2.1",
"webpack-merge": "^4.0.0",
"winston": "^2.1.0",
"winston-loggly-bulk": "^1.4.2",
"winston-loggly-bulk": "^2.0.2",
"xml2js": "^0.4.4"
},
"private": true,
@@ -162,55 +144,44 @@
"babel-plugin-istanbul": "^4.0.0",
"chai": "^3.4.0",
"chai-as-promised": "^5.1.0",
"chalk": "^1.1.3",
"chalk": "^2.3.0",
"chromedriver": "^2.27.2",
"connect-history-api-fallback": "^1.1.0",
"coveralls": "^2.11.2",
"cross-spawn": "^5.0.1",
"coveralls": "^3.0.0",
"cross-spawn": "^6.0.4",
"csv": "~0.3.6",
"deep-diff": "~0.1.4",
"eslint": "^3.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",
"eslint-plugin-mocha": "^4.7.0",
"event-stream": "^3.2.2",
"eventsource-polyfill": "^0.9.6",
"expect.js": "~0.2.0",
"expect.js": "^0.3.1",
"http-proxy-middleware": "^0.17.0",
"inject-loader": "^3.0.0-beta4",
"istanbul": "^1.1.0-alpha.1",
"karma": "^1.3.0",
"karma-babel-preprocessor": "^6.0.1",
"karma-chai-plugins": "~0.6.0",
"karma-coverage": "^0.5.3",
"karma-mocha": "^0.2.0",
"karma-mocha-reporter": "^1.1.1",
"karma": "^2.0.0",
"karma-babel-preprocessor": "^7.0.0",
"karma-chai-plugins": "^0.9.0",
"karma-coverage": "^1.1.1",
"karma-mocha": "^1.3.0",
"karma-mocha-reporter": "^2.2.5",
"karma-phantomjs-launcher": "^1.0.0",
"karma-sinon-chai": "~1.2.0",
"karma-sinon-chai": "^1.3.3",
"karma-sinon-stub-promise": "^1.0.0",
"karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "0.0.24",
"karma-spec-reporter": "0.0.32",
"karma-webpack": "^2.0.2",
"lcov-result-merger": "^1.0.2",
"lolex": "^1.4.0",
"mocha": "^3.2.0",
"mongodb": "^2.0.46",
"mongoskin": "~2.1.0",
"lcov-result-merger": "^2.0.0",
"mocha": "^5.0.0",
"monk": "^4.0.0",
"nightwatch": "^0.9.12",
"phantomjs-prebuilt": "^2.1.12",
"protractor": "^3.1.1",
"raw-loader": "^0.5.1",
"require-again": "^2.0.0",
"rewire": "^2.3.3",
"selenium-server": "^3.0.1",
"sinon": "^1.17.2",
"sinon": "^4.2.2",
"sinon-chai": "^2.8.0",
"sinon-stub-promise": "^4.0.0",
"superagent-defaults": "^0.1.13",
"vinyl-transform": "^1.0.0",
"webpack-bundle-analyzer": "^2.2.1",
"webpack-dev-middleware": "^1.10.0",
"webpack-hot-middleware": "^2.6.1"

View File

@@ -64,11 +64,11 @@ describe('GET /challenges/:challengeId/export/csv', () => {
let sortedMembers = _.sortBy([members[0], members[1], members[2], groupLeader], '_id');
let splitRes = res.split('\n');
expect(splitRes[0]).to.equal('UUID,name,Task,Value,Notes,Task,Value,Notes');
expect(splitRes[1]).to.equal(`${sortedMembers[0]._id},${sortedMembers[0].profile.name},habit:Task 1,0,,todo:Task 2,0,`);
expect(splitRes[2]).to.equal(`${sortedMembers[1]._id},${sortedMembers[1].profile.name},habit:Task 1,0,,todo:Task 2,0,`);
expect(splitRes[3]).to.equal(`${sortedMembers[2]._id},${sortedMembers[2].profile.name},habit:Task 1,0,,todo:Task 2,0,`);
expect(splitRes[4]).to.equal(`${sortedMembers[3]._id},${sortedMembers[3].profile.name},habit:Task 1,0,,todo:Task 2,0,`);
expect(splitRes[0]).to.equal('UUID,name,Task,Value,Notes,Streak,Task,Value,Notes,Streak');
expect(splitRes[1]).to.equal(`${sortedMembers[0]._id},${sortedMembers[0].profile.name},habit:Task 1,0,,0,todo:Task 2,0,,0`);
expect(splitRes[2]).to.equal(`${sortedMembers[1]._id},${sortedMembers[1].profile.name},habit:Task 1,0,,0,todo:Task 2,0,,0`);
expect(splitRes[3]).to.equal(`${sortedMembers[2]._id},${sortedMembers[2].profile.name},habit:Task 1,0,,0,todo:Task 2,0,,0`);
expect(splitRes[4]).to.equal(`${sortedMembers[3]._id},${sortedMembers[3].profile.name},habit:Task 1,0,,0,todo:Task 2,0,,0`);
expect(splitRes[5]).to.equal('');
});
});

View File

@@ -149,7 +149,10 @@ describe('GET /challenges/:challengeId/members', () => {
let usersToGenerate = [];
for (let i = 0; i < 3; i++) {
usersToGenerate.push(generateUser({challenges: [challenge._id]}));
usersToGenerate.push(generateUser({
challenges: [challenge._id],
'profile.name': `${i}profilename`,
}));
}
let generatedUsers = await Promise.all(usersToGenerate);
let profileNames = generatedUsers.map(generatedUser => generatedUser.profile.name);

View File

@@ -95,13 +95,23 @@ describe('GET /challenges/:challengeId/members/:memberId', () => {
expect(memberProgress.tasks[0].challenge.taskId).to.equal(chalTasks[0]._id);
});
it('returns the tasks without the tags', async () => {
it('returns the tasks without the tags and checklist', async () => {
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
let challenge = await generateChallenge(user, group);
let taskText = 'Test Text';
await user.post(`/tasks/challenge/${challenge._id}`, [{type: 'habit', text: taskText}]);
await user.post(`/tasks/challenge/${challenge._id}`, [{
type: 'todo',
text: taskText,
checklist: [
{
_id: 123,
text: 'test',
},
],
}]);
let memberProgress = await user.get(`/challenges/${challenge._id}/members/${user._id}`);
expect(memberProgress.tasks[0]).not.to.have.key('tags');
expect(memberProgress.tasks[0].checklist).to.eql([]);
});
});

View File

@@ -314,5 +314,33 @@ describe('POST /challenges', () => {
groupLeader = await groupLeader.sync();
expect(groupLeader.achievements.joinedChallenge).to.be.true;
});
it('sets summary to challenges name when not supplied', async () => {
const name = 'Test Challenge';
const challenge = await groupLeader.post('/challenges', {
group: group._id,
name,
shortName: 'TC Label',
});
const updatedChallenge = await groupLeader.get(`/challenges/${challenge._id}`);
expect(updatedChallenge.summary).to.eql(name);
});
it('sets summary to challenges', async () => {
const name = 'Test Challenge';
const summary = 'Test Summary Challenge';
const challenge = await groupLeader.post('/challenges', {
group: group._id,
name,
shortName: 'TC Label',
summary,
});
const updatedChallenge = await groupLeader.get(`/challenges/${challenge._id}`);
expect(updatedChallenge.summary).to.eql(summary);
});
});
});

View File

@@ -101,19 +101,21 @@ describe('POST /challenges/:challengeId/join', () => {
});
it('syncs challenge tasks to joining user', async () => {
let taskText = 'A challenge task text';
const taskText = 'A challenge task text';
await groupLeader.post(`/tasks/challenge/${challenge._id}`, [
{type: 'habit', text: taskText},
{type: 'daily', text: taskText},
]);
await authorizedUser.post(`/challenges/${challenge._id}/join`);
let tasks = await authorizedUser.get('/tasks/user');
let tasksTexts = tasks.map((task) => {
return task.text;
const tasks = await authorizedUser.get('/tasks/user');
const syncedTask = tasks.find((task) => {
return task.text === taskText;
});
expect(tasksTexts).to.include(taskText);
expect(syncedTask.text).to.eql(taskText);
expect(syncedTask.isDue).to.exist;
expect(syncedTask.nextDue).to.exist;
});
it('adds challenge tag to user tags', async () => {

View File

@@ -149,13 +149,19 @@ describe('POST /challenges/:challengeId/winner/:winnerId', () => {
await sleep(0.5);
let tasks = await winningUser.get('/tasks/user');
let testTask = _.find(tasks, (task) => {
const tasks = await winningUser.get('/tasks/user');
const testTask = _.find(tasks, (task) => {
return task.text === taskText;
});
const updatedUser = await winningUser.sync();
const challengeTag = updatedUser.tags.find(tags => {
return tags.id === challenge._id;
});
expect(testTask.challenge.broken).to.eql('CHALLENGE_CLOSED');
expect(testTask.challenge.winner).to.eql(winningUser.profile.name);
expect(challengeTag.challenge).to.eql('false');
});
});
});

View File

@@ -0,0 +1,43 @@
import {
generateUser,
generateGroup,
} from '../../../../helpers/api-v3-integration.helper';
describe('POST /challenges/:challengeId/clone', () => {
it('clones a challenge', async () => {
const user = await generateUser({balance: 10});
const group = await generateGroup(user);
const name = 'Test Challenge';
const shortName = 'TC Label';
const description = 'Test Description';
const prize = 1;
const challenge = await user.post('/challenges', {
group: group._id,
name,
shortName,
description,
prize,
});
const challengeTask = await user.post(`/tasks/challenge/${challenge._id}`, {
text: 'test habit',
type: 'habit',
up: false,
down: true,
notes: 1976,
});
const cloneChallengeResponse = await user.post(`/challenges/${challenge._id}/clone`, {
group: group._id,
name: `${name} cloned`,
shortName,
description,
prize,
});
expect(cloneChallengeResponse.clonedTasks[0].text).to.eql(challengeTask.text);
expect(cloneChallengeResponse.clonedTasks[0]._id).to.not.eql(challengeTask._id);
expect(cloneChallengeResponse.clonedTasks[0].challenge.id).to.eql(cloneChallengeResponse.clonedChallenge._id);
});
});

View File

@@ -71,11 +71,9 @@ describe('DELETE /groups/:groupId/chat/:chatId', () => {
});
it('returns the update chat when previous message parameter is passed and the chat is updated', async () => {
await expect(user.del(`/groups/${groupWithChat._id}/chat/${nextMessage.id}?previousMsg=${message.id}`))
.eventually
.is.an('array')
.to.include(message)
.to.be.lengthOf(1);
let deleteResult = await user.del(`/groups/${groupWithChat._id}/chat/${nextMessage.id}?previousMsg=${message.id}`);
expect(deleteResult[0].id).to.eql(message.id);
});
});
});

View File

@@ -1,5 +1,6 @@
import {
createAndPopulateGroup,
generateUser,
translate as t,
sleep,
server,
@@ -233,7 +234,7 @@ describe('POST /chat', () => {
// Email sent to mods
await sleep(0.5);
expect(email.sendTxn).to.be.calledOnce;
expect(email.sendTxn.args[0][1]).to.be.eql('slur-report-to-mods');
expect(email.sendTxn.args[0][1]).to.eql('slur-report-to-mods');
// Slack message to mods
expect(IncomingWebhook.prototype.send).to.be.calledOnce;
@@ -286,7 +287,7 @@ describe('POST /chat', () => {
// Email sent to mods
await sleep(0.5);
expect(email.sendTxn).to.be.calledThrice;
expect(email.sendTxn.args[2][1]).to.be.eql('slur-report-to-mods');
expect(email.sendTxn.args[2][1]).to.eql('slur-report-to-mods');
// Slack message to mods
expect(IncomingWebhook.prototype.send).to.be.calledOnce;
@@ -363,6 +364,48 @@ describe('POST /chat', () => {
expect(message.message.id).to.exist;
});
it('creates a chat with user styles', async () => {
const mount = 'test-mount';
const pet = 'test-pet';
const style = 'test-style';
const userWithStyle = await generateUser({
'items.currentMount': mount,
'items.currentPet': pet,
'preferences.style': style,
});
await userWithStyle.sync();
const message = await userWithStyle.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage});
expect(message.message.id).to.exist;
expect(message.message.userStyles.items.currentMount).to.eql(userWithStyle.items.currentMount);
expect(message.message.userStyles.items.currentPet).to.eql(userWithStyle.items.currentPet);
expect(message.message.userStyles.preferences.style).to.eql(userWithStyle.preferences.style);
expect(message.message.userStyles.preferences.hair).to.eql(userWithStyle.preferences.hair);
expect(message.message.userStyles.preferences.skin).to.eql(userWithStyle.preferences.skin);
expect(message.message.userStyles.preferences.shirt).to.eql(userWithStyle.preferences.shirt);
expect(message.message.userStyles.preferences.chair).to.eql(userWithStyle.preferences.chair);
expect(message.message.userStyles.preferences.background).to.eql(userWithStyle.preferences.background);
});
it('adds backer info to chat', async () => {
const backerInfo = {
npc: 'Town Crier',
tier: 800,
tokensApplied: true,
};
const backer = await generateUser({
backer: backerInfo,
});
const message = await backer.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage});
const messageBackerInfo = message.message.backer;
expect(messageBackerInfo.npc).to.equal(backerInfo.npc);
expect(messageBackerInfo.tier).to.equal(backerInfo.tier);
expect(messageBackerInfo.tokensApplied).to.equal(backerInfo.tokensApplied);
});
it('sends group chat received webhooks', async () => {
let userUuid = generateUUID();
let memberUuid = generateUUID();
@@ -407,6 +450,9 @@ describe('POST /chat', () => {
expect(message.message.id).to.exist;
expect(memberWithNotification.newMessages[`${groupWithChat._id}`]).to.exist;
expect(memberWithNotification.notifications.find(n => {
return n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupWithChat._id;
})).to.exist;
});
it('notifies other users of new messages for a party', async () => {
@@ -424,6 +470,9 @@ describe('POST /chat', () => {
expect(message.message.id).to.exist;
expect(memberWithNotification.newMessages[`${group._id}`]).to.exist;
expect(memberWithNotification.notifications.find(n => {
return n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === group._id;
})).to.exist;
});
context('Spam prevention', () => {

View File

@@ -1,5 +1,6 @@
import {
createAndPopulateGroup,
sleep,
} from '../../../../helpers/api-v3-integration.helper';
describe('POST /groups/:id/chat/seen', () => {
@@ -24,10 +25,15 @@ describe('POST /groups/:id/chat/seen', () => {
});
it('clears new messages for a guild', async () => {
await guildMember.sync();
const initialNotifications = guildMember.notifications.length;
await guildMember.post(`/groups/${guild._id}/chat/seen`);
await sleep(0.5);
let guildThatHasSeenChat = await guildMember.get('/user');
expect(guildThatHasSeenChat.notifications.length).to.equal(initialNotifications - 1);
expect(guildThatHasSeenChat.newMessages).to.be.empty;
});
});
@@ -53,10 +59,13 @@ describe('POST /groups/:id/chat/seen', () => {
});
it('clears new messages for a party', async () => {
await partyMember.sync();
const initialNotifications = partyMember.notifications.length;
await partyMember.post(`/groups/${party._id}/chat/seen`);
let partyMemberThatHasSeenChat = await partyMember.get('/user');
expect(partyMemberThatHasSeenChat.notifications.length).to.equal(initialNotifications - 1);
expect(partyMemberThatHasSeenChat.newMessages).to.be.empty;
});
});

View File

@@ -3,6 +3,7 @@ import {
generateUser,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
import config from '../../../../../config.json';
import { v4 as generateUUID } from 'uuid';
describe('POST /groups/:id/chat/:id/clearflags', () => {
@@ -74,7 +75,7 @@ describe('POST /groups/:id/chat/:id/clearflags', () => {
expect(messages[0].flagCount).to.eql(0);
});
it('can unflag a system message', async () => {
it('can\'t flag a system message', async () => {
let { group, members } = await createAndPopulateGroup({
groupDetails: {
type: 'party',
@@ -95,13 +96,15 @@ describe('POST /groups/:id/chat/:id/clearflags', () => {
await member.post('/user/class/cast/mpheal');
let [skillMsg] = await member.get(`/groups/${group.id}/chat`);
await member.post(`/groups/${group._id}/chat/${skillMsg.id}/flag`);
await admin.post(`/groups/${group._id}/chat/${skillMsg.id}/clearflags`);
let messages = await members[0].get(`/groups/${group._id}/chat`);
expect(messages[0].id).to.eql(skillMsg.id);
expect(messages[0].flagCount).to.eql(0);
await expect(member.post(`/groups/${group._id}/chat/${skillMsg.id}/flag`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('messageCannotFlagSystemMessages', {communityManagerEmail: config.EMAILS.COMMUNITY_MANAGER_EMAIL}),
});
// let messages = await members[0].get(`/groups/${group._id}/chat`);
// expect(messages[0].id).to.eql(skillMsg.id);
// expect(messages[0].flagCount).to.eql(0);
});
});

View File

@@ -1,8 +1,8 @@
import {
generateUser,
translate as t,
resetHabiticaDB,
} from '../../../../helpers/api-v3-integration.helper';
import apiMessages from '../../../../../website/server/libs/apiMessages';
describe('GET /coupons/', () => {
let user;
@@ -19,7 +19,7 @@ describe('GET /coupons/', () => {
await expect(user.get('/coupons')).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('noSudoAccess'),
message: apiMessages('noSudoAccess'),
});
});

View File

@@ -4,6 +4,7 @@ import {
resetHabiticaDB,
} from '../../../../helpers/api-v3-integration.helper';
import couponCode from 'coupon-code';
import apiMessages from '../../../../../website/server/libs/apiMessages';
describe('POST /coupons/generate/:event', () => {
let user;
@@ -25,7 +26,7 @@ describe('POST /coupons/generate/:event', () => {
await expect(user.post('/coupons/generate/aaa')).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('noSudoAccess'),
message: apiMessages('noSudoAccess'),
});
});

View File

@@ -72,7 +72,7 @@ describe('GET /groups/:groupId/members', () => {
expect(memberRes).to.have.all.keys([ // works as: object has all and only these keys
'_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party',
'backer', 'contributor', 'auth', 'items', 'inbox',
'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives',
]);
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
expect(Object.keys(memberRes.preferences).sort()).to.eql([
@@ -93,7 +93,7 @@ describe('GET /groups/:groupId/members', () => {
expect(memberRes).to.have.all.keys([ // works as: object has all and only these keys
'_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party',
'backer', 'contributor', 'auth', 'items', 'inbox',
'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives',
]);
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
expect(Object.keys(memberRes.preferences).sort()).to.eql([
@@ -161,4 +161,19 @@ describe('GET /groups/:groupId/members', () => {
let resIds = res.concat(res2).map(member => member._id);
expect(resIds).to.eql(expectedIds.sort());
});
it('searches members', async () => {
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
let usersToGenerate = [];
for (let i = 0; i < 2; i++) {
usersToGenerate.push(generateUser({party: {_id: group._id}}));
}
const usersCreated = await Promise.all(usersToGenerate);
const userToSearch = usersCreated[0].profile.name;
let res = await user.get(`/groups/party/members?search=${userToSearch}`);
expect(res.length).to.equal(1);
expect(res[0].profile.name).to.equal(userToSearch);
});
});

View File

@@ -44,6 +44,32 @@ describe('POST /group', () => {
},
});
});
it('sets summary to groups name when not supplied', async () => {
const name = 'Test Group';
const group = await user.post('/groups', {
name,
type: 'guild',
});
const updatedGroup = await user.get(`/groups/${group._id}`);
expect(updatedGroup.summary).to.eql(name);
});
it('sets summary to groups', async () => {
const name = 'Test Group';
const summary = 'Test Summary';
const group = await user.post('/groups', {
name,
type: 'guild',
summary,
});
const updatedGroup = await user.get(`/groups/${group._id}`);
expect(updatedGroup.summary).to.eql(summary);
});
});
context('Guilds', () => {

View File

@@ -10,6 +10,8 @@ import { v4 as generateUUID } from 'uuid';
import {
each,
} from 'lodash';
import { model as User } from '../../../../../website/server/models/user';
import * as payments from '../../../../../website/server/libs/payments';
describe('POST /groups/:groupId/leave', () => {
let typesOfGroups = {
@@ -68,13 +70,21 @@ describe('POST /groups/:groupId/leave', () => {
it('removes new messages for that group from user', async () => {
await member.post(`/groups/${groupToLeave._id}/chat`, { message: 'Some message' });
await sleep(0.5);
await leader.sync();
expect(leader.notifications.find(n => {
return n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupToLeave._id;
})).to.exist;
expect(leader.newMessages[groupToLeave._id]).to.not.be.empty;
await leader.post(`/groups/${groupToLeave._id}/leave`);
await leader.sync();
expect(leader.notifications.find(n => {
return n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupToLeave._id;
})).to.not.exist;
expect(leader.newMessages[groupToLeave._id]).to.be.empty;
});
@@ -254,7 +264,7 @@ describe('POST /groups/:groupId/leave', () => {
it('deletes non existant party from user when user tries to leave', async () => {
let nonExistentPartyId = generateUUID();
let userWithNonExistentParty = await generateUser({'party._id': nonExistentPartyId});
expect(userWithNonExistentParty.party._id).to.be.eql(nonExistentPartyId);
expect(userWithNonExistentParty.party._id).to.eql(nonExistentPartyId);
await expect(userWithNonExistentParty.post(`/groups/${nonExistentPartyId}/leave`))
.to.eventually.be.rejected;
@@ -264,4 +274,45 @@ describe('POST /groups/:groupId/leave', () => {
expect(userWithNonExistentParty.party).to.eql({});
});
});
context('Leaving a group plan', () => {
it('cancels the free subscription', async () => {
// Create group
let { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Private Guild',
type: 'guild',
},
members: 1,
});
let leader = groupLeader;
let member = members[0];
let userWithFreePlan = await User.findById(leader._id).exec();
// Create subscription
let paymentData = {
user: userWithFreePlan,
groupId: group._id,
sub: {
key: 'basic_3mo',
},
customerId: 'customer-id',
paymentMethod: 'Payment Method',
headers: {
'x-client': 'habitica-web',
'user-agent': '',
},
};
await payments.createSubscription(paymentData);
await member.sync();
expect(member.purchased.plan.planId).to.equal('group_plan_auto');
expect(member.purchased.plan.dateTerminated).to.not.exist;
// Leave
await member.post(`/groups/${group._id}/leave`);
await member.sync();
expect(member.purchased.plan.dateTerminated).to.exist;
});
});
});

View File

@@ -2,6 +2,7 @@ import {
generateUser,
createAndPopulateGroup,
translate as t,
sleep,
} from '../../../../helpers/api-v3-integration.helper';
import * as email from '../../../../../website/server/libs/email';
@@ -119,16 +120,16 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
await leader.post(`/groups/${guild._id}/removeMember/${invitedUser._id}`);
expect(email.sendTxn).to.be.calledOnce;
expect(email.sendTxn.args[0][0]._id).to.be.eql(invitedUser._id);
expect(email.sendTxn.args[0][1]).to.be.eql('guild-invite-rescinded');
expect(email.sendTxn.args[0][0]._id).to.eql(invitedUser._id);
expect(email.sendTxn.args[0][1]).to.eql('guild-invite-rescinded');
});
it('sends email to removed user', async () => {
await leader.post(`/groups/${guild._id}/removeMember/${member._id}`);
expect(email.sendTxn).to.be.calledOnce;
expect(email.sendTxn.args[0][0]._id).to.be.eql(member._id);
expect(email.sendTxn.args[0][1]).to.be.eql('kicked-from-guild');
expect(email.sendTxn.args[0][0]._id).to.eql(member._id);
expect(email.sendTxn.args[0][1]).to.eql('kicked-from-guild');
});
});
@@ -188,13 +189,20 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
it('removes new messages from a member who is removed', async () => {
await partyLeader.post(`/groups/${party._id}/chat`, { message: 'Some message' });
await sleep(0.5);
await removedMember.sync();
expect(removedMember.notifications.find(n => {
return n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === party._id;
})).to.exist;
expect(removedMember.newMessages[party._id]).to.not.be.empty;
await partyLeader.post(`/groups/${party._id}/removeMember/${removedMember._id}`);
await removedMember.sync();
expect(removedMember.notifications.find(n => {
return n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === party._id;
})).to.not.exist;
expect(removedMember.newMessages[party._id]).to.be.empty;
});
@@ -247,16 +255,16 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
await partyLeader.post(`/groups/${party._id}/removeMember/${partyInvitedUser._id}`);
expect(email.sendTxn).to.be.calledOnce;
expect(email.sendTxn.args[0][0]._id).to.be.eql(partyInvitedUser._id);
expect(email.sendTxn.args[0][1]).to.be.eql('party-invite-rescinded');
expect(email.sendTxn.args[0][0]._id).to.eql(partyInvitedUser._id);
expect(email.sendTxn.args[0][1]).to.eql('party-invite-rescinded');
});
it('sends email to removed user', async () => {
await partyLeader.post(`/groups/${party._id}/removeMember/${partyMember._id}`);
expect(email.sendTxn).to.be.calledOnce;
expect(email.sendTxn.args[0][0]._id).to.be.eql(partyMember._id);
expect(email.sendTxn.args[0][1]).to.be.eql('kicked-from-party');
expect(email.sendTxn.args[0][0]._id).to.eql(partyMember._id);
expect(email.sendTxn.args[0][1]).to.eql('kicked-from-party');
});
});
});

View File

@@ -110,6 +110,7 @@ describe('Post /groups/:groupId/invite', () => {
id: group._id,
name: groupName,
inviter: inviter._id,
publicGuild: false,
}]);
await expect(userToInvite.get('/user'))
@@ -127,11 +128,13 @@ describe('Post /groups/:groupId/invite', () => {
id: group._id,
name: groupName,
inviter: inviter._id,
publicGuild: false,
},
{
id: group._id,
name: groupName,
inviter: inviter._id,
publicGuild: false,
},
]);

View File

@@ -32,7 +32,7 @@ describe('GET /members/:memberId', () => {
let memberRes = await user.get(`/members/${member._id}`);
expect(memberRes).to.have.all.keys([ // works as: object has all and only these keys
'_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party',
'backer', 'contributor', 'auth', 'items', 'inbox',
'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives',
]);
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
expect(Object.keys(memberRes.preferences).sort()).to.eql([

View File

@@ -98,6 +98,7 @@ describe('POST /members/send-private-message', () => {
it('sends a private message to a user', async () => {
let receiver = await generateUser();
// const initialNotifications = receiver.notifications.length;
await userToSendMessage.post('/members/send-private-message', {
message: messageToSend,
@@ -115,6 +116,92 @@ describe('POST /members/send-private-message', () => {
return message.uuid === receiver._id && message.text === messageToSend;
});
// @TODO waiting for mobile support
// expect(updatedReceiver.notifications.length).to.equal(initialNotifications + 1);
// const notification = updatedReceiver.notifications[updatedReceiver.notifications.length - 1];
// expect(notification.type).to.equal('NEW_INBOX_MESSAGE');
// expect(notification.data.messageId).to.equal(sendersMessageInReceiversInbox.id);
// expect(notification.data.excerpt).to.equal(messageToSend);
// expect(notification.data.sender.id).to.equal(updatedSender._id);
// expect(notification.data.sender.name).to.equal(updatedSender.profile.name);
expect(sendersMessageInReceiversInbox).to.exist;
expect(sendersMessageInSendersInbox).to.exist;
});
// @TODO waiting for mobile support
xit('creates a notification with an excerpt if the message is too long', async () => {
let receiver = await generateUser();
let longerMessageToSend = 'A very long message, that for sure exceeds the limit of 100 chars for the excerpt that we set to 100 chars';
let messageExcerpt = `${longerMessageToSend.substring(0, 100)}...`;
await userToSendMessage.post('/members/send-private-message', {
message: longerMessageToSend,
toUserId: receiver._id,
});
let updatedReceiver = await receiver.get('/user');
let sendersMessageInReceiversInbox = _.find(updatedReceiver.inbox.messages, (message) => {
return message.uuid === userToSendMessage._id && message.text === longerMessageToSend;
});
const notification = updatedReceiver.notifications[updatedReceiver.notifications.length - 1];
expect(notification.type).to.equal('NEW_INBOX_MESSAGE');
expect(notification.data.messageId).to.equal(sendersMessageInReceiversInbox.id);
expect(notification.data.excerpt).to.equal(messageExcerpt);
});
it('allows admin to send when sender has blocked the admin', async () => {
userToSendMessage = await generateUser({
'contributor.admin': 1,
});
const receiver = await generateUser({'inbox.blocks': [userToSendMessage._id]});
await userToSendMessage.post('/members/send-private-message', {
message: messageToSend,
toUserId: receiver._id,
});
const updatedReceiver = await receiver.get('/user');
const updatedSender = await userToSendMessage.get('/user');
const sendersMessageInReceiversInbox = _.find(updatedReceiver.inbox.messages, (message) => {
return message.uuid === userToSendMessage._id && message.text === messageToSend;
});
const sendersMessageInSendersInbox = _.find(updatedSender.inbox.messages, (message) => {
return message.uuid === receiver._id && message.text === messageToSend;
});
expect(sendersMessageInReceiversInbox).to.exist;
expect(sendersMessageInSendersInbox).to.exist;
});
it('allows admin to send when to user has opted out of messaging', async () => {
userToSendMessage = await generateUser({
'contributor.admin': 1,
});
const receiver = await generateUser({'inbox.optOut': true});
await userToSendMessage.post('/members/send-private-message', {
message: messageToSend,
toUserId: receiver._id,
});
const updatedReceiver = await receiver.get('/user');
const updatedSender = await userToSendMessage.get('/user');
const sendersMessageInReceiversInbox = _.find(updatedReceiver.inbox.messages, (message) => {
return message.uuid === userToSendMessage._id && message.text === messageToSend;
});
const sendersMessageInSendersInbox = _.find(updatedSender.inbox.messages, (message) => {
return message.uuid === receiver._id && message.text === messageToSend;
});
expect(sendersMessageInReceiversInbox).to.exist;
expect(sendersMessageInSendersInbox).to.exist;
});

View File

@@ -0,0 +1,16 @@
import {
requester,
} from '../../../../helpers/api-v3-integration.helper';
describe('GET /news', () => {
let api;
beforeEach(async () => {
api = requester();
});
it('returns the latest news in html format, does not require authentication', async () => {
const res = await api.get('/news');
expect(res).to.be.a.string;
});
});

View File

@@ -0,0 +1,42 @@
import {
generateUser,
} from '../../../../helpers/api-v3-integration.helper';
describe('POST /news/tell-me-later', () => {
let user;
beforeEach(async () => {
user = await generateUser({
'flags.newStuff': true,
});
});
it('marks new stuff as read and adds notification', async () => {
expect(user.flags.newStuff).to.equal(true);
const initialNotifications = user.notifications.length;
await user.post('/news/tell-me-later');
await user.sync();
expect(user.flags.newStuff).to.equal(false);
expect(user.notifications.length).to.equal(initialNotifications + 1);
const notification = user.notifications[user.notifications.length - 1];
expect(notification.type).to.equal('NEW_STUFF');
// should be marked as seen by default so it's not counted in the number of notifications
expect(notification.seen).to.equal(true);
expect(notification.data.title).to.be.a.string;
});
it('never adds two notifications', async () => {
const initialNotifications = user.notifications.length;
await user.post('/news/tell-me-later');
await user.post('/news/tell-me-later');
await user.sync();
expect(user.notifications.length).to.equal(initialNotifications + 1);
});
});

View File

@@ -47,6 +47,7 @@ describe('POST /notifications/:notificationId/read', () => {
id: id2,
type: 'LOGIN_INCENTIVE',
data: {},
seen: false,
}]);
await user.sync();

View File

@@ -0,0 +1,59 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
describe('POST /notifications/:notificationId/see', () => {
let user;
before(async () => {
user = await generateUser();
});
it('errors when notification is not found', async () => {
let dummyId = generateUUID();
await expect(user.post(`/notifications/${dummyId}/see`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('messageNotificationNotFound'),
});
});
it('mark a notification as seen', async () => {
expect(user.notifications.length).to.equal(0);
const id = generateUUID();
const id2 = generateUUID();
await user.update({
notifications: [{
id,
type: 'DROPS_ENABLED',
data: {},
}, {
id: id2,
type: 'LOGIN_INCENTIVE',
data: {},
}],
});
const userObj = await user.get('/user'); // so we can check that defaults have been applied
expect(userObj.notifications.length).to.equal(2);
expect(userObj.notifications[0].seen).to.equal(false);
const res = await user.post(`/notifications/${id}/see`);
expect(res).to.deep.equal({
id,
type: 'DROPS_ENABLED',
data: {},
seen: true,
});
await user.sync();
expect(user.notifications.length).to.equal(2);
expect(user.notifications[0].id).to.equal(id);
expect(user.notifications[0].seen).to.equal(true);
});
});

View File

@@ -4,7 +4,7 @@ import {
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
describe('POST /notifications/:notificationId/read', () => {
describe('POST /notifications/read', () => {
let user;
before(async () => {
@@ -57,6 +57,7 @@ describe('POST /notifications/:notificationId/read', () => {
id: id2,
type: 'LOGIN_INCENTIVE',
data: {},
seen: false,
}]);
await user.sync();

View File

@@ -0,0 +1,88 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
describe('POST /notifications/see', () => {
let user;
before(async () => {
user = await generateUser();
});
it('errors when notification is not found', async () => {
let dummyId = generateUUID();
await expect(user.post('/notifications/see', {
notificationIds: [dummyId],
})).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('messageNotificationNotFound'),
});
});
it('mark multiple notifications as seen', async () => {
expect(user.notifications.length).to.equal(0);
const id = generateUUID();
const id2 = generateUUID();
const id3 = generateUUID();
await user.update({
notifications: [{
id,
type: 'DROPS_ENABLED',
data: {},
seen: false,
}, {
id: id2,
type: 'LOGIN_INCENTIVE',
data: {},
seen: false,
}, {
id: id3,
type: 'CRON',
data: {},
seen: false,
}],
});
await user.sync();
expect(user.notifications.length).to.equal(3);
const res = await user.post('/notifications/see', {
notificationIds: [id, id3],
});
expect(res).to.deep.equal([
{
id,
type: 'DROPS_ENABLED',
data: {},
seen: true,
}, {
id: id2,
type: 'LOGIN_INCENTIVE',
data: {},
seen: false,
}, {
id: id3,
type: 'CRON',
data: {},
seen: true,
}]);
await user.sync();
expect(user.notifications.length).to.equal(3);
expect(user.notifications[0].id).to.equal(id);
expect(user.notifications[0].seen).to.equal(true);
expect(user.notifications[1].id).to.equal(id2);
expect(user.notifications[1].seen).to.equal(false);
expect(user.notifications[2].id).to.equal(id3);
expect(user.notifications[2].seen).to.equal(true);
});
});

View File

@@ -130,6 +130,7 @@ describe('POST /tasks/:id/score/:direction', () => {
});
it('uncompletes todo when direction is down', async () => {
await user.post(`/tasks/${todo._id}/score/up`);
await user.post(`/tasks/${todo._id}/score/down`);
let updatedTask = await user.get(`/tasks/${todo._id}`);
@@ -137,9 +138,23 @@ describe('POST /tasks/:id/score/:direction', () => {
expect(updatedTask.dateCompleted).to.be.a('undefined');
});
it('scores up todo even if it is already completed'); // Yes?
it('doesn\'t let a todo be completed twice', async () => {
await user.post(`/tasks/${todo._id}/score/up`);
await expect(user.post(`/tasks/${todo._id}/score/up`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('sessionOutdated'),
});
});
it('scores down todo even if it is already uncompleted'); // Yes?
it('doesn\'t let a todo be uncompleted twice', async () => {
await expect(user.post(`/tasks/${todo._id}/score/down`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('sessionOutdated'),
});
});
context('user stats when direction is up', () => {
let updatedUser;
@@ -163,23 +178,25 @@ describe('POST /tasks/:id/score/:direction', () => {
});
context('user stats when direction is down', () => {
let updatedUser;
let updatedUser, initialUser;
beforeEach(async () => {
await user.post(`/tasks/${todo._id}/score/up`);
initialUser = await user.get('/user');
await user.post(`/tasks/${todo._id}/score/down`);
updatedUser = await user.get('/user');
});
it('decreases user\'s mp', () => {
expect(updatedUser.stats.mp).to.be.lessThan(user.stats.mp);
expect(updatedUser.stats.mp).to.be.lessThan(initialUser.stats.mp);
});
it('decreases user\'s exp', () => {
expect(updatedUser.stats.exp).to.be.lessThan(user.stats.exp);
expect(updatedUser.stats.exp).to.be.lessThan(initialUser.stats.exp);
});
it('decreases user\'s gold', () => {
expect(updatedUser.stats.gp).to.be.lessThan(user.stats.gp);
expect(updatedUser.stats.gp).to.be.lessThan(initialUser.stats.gp);
});
});
});
@@ -202,6 +219,7 @@ describe('POST /tasks/:id/score/:direction', () => {
});
it('uncompletes daily when direction is down', async () => {
await user.post(`/tasks/${daily._id}/score/up`);
await user.post(`/tasks/${daily._id}/score/down`);
let task = await user.get(`/tasks/${daily._id}`);
@@ -222,9 +240,22 @@ describe('POST /tasks/:id/score/:direction', () => {
expect(task.nextDue.length).to.eql(6);
});
it('scores up daily even if it is already completed'); // Yes?
it('doesn\'t let a daily be completed twice', async () => {
await user.post(`/tasks/${daily._id}/score/up`);
await expect(user.post(`/tasks/${daily._id}/score/up`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('sessionOutdated'),
});
});
it('scores down daily even if it is already uncompleted'); // Yes?
it('doesn\'t let a daily be uncompleted twice', async () => {
await expect(user.post(`/tasks/${daily._id}/score/down`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('sessionOutdated'),
});
});
context('user stats when direction is up', () => {
let updatedUser;
@@ -248,23 +279,25 @@ describe('POST /tasks/:id/score/:direction', () => {
});
context('user stats when direction is down', () => {
let updatedUser;
let updatedUser, initialUser;
beforeEach(async () => {
await user.post(`/tasks/${daily._id}/score/up`);
initialUser = await user.get('/user');
await user.post(`/tasks/${daily._id}/score/down`);
updatedUser = await user.get('/user');
});
it('decreases user\'s mp', () => {
expect(updatedUser.stats.mp).to.be.lessThan(user.stats.mp);
expect(updatedUser.stats.mp).to.be.lessThan(initialUser.stats.mp);
});
it('decreases user\'s exp', () => {
expect(updatedUser.stats.exp).to.be.lessThan(user.stats.exp);
expect(updatedUser.stats.exp).to.be.lessThan(initialUser.stats.exp);
});
it('decreases user\'s gold', () => {
expect(updatedUser.stats.gp).to.be.lessThan(user.stats.gp);
expect(updatedUser.stats.gp).to.be.lessThan(initialUser.stats.gp);
});
});
});

View File

@@ -302,6 +302,17 @@ describe('POST /tasks/user', () => {
expect(task.alias).to.eql('a_alias012');
});
// This is a special case for iOS requests
it('will round a priority (difficulty)', async () => {
let task = await user.post('/tasks/user', {
text: 'test habit',
type: 'habit',
priority: 0.10000000000005,
});
expect(task.priority).to.eql(0.1);
});
});
context('habits', () => {
@@ -628,6 +639,43 @@ describe('POST /tasks/user', () => {
});
});
it('returns an error if everyX is a non int', async () => {
await expect(user.post('/tasks/user', {
text: 'test daily',
type: 'daily',
everyX: 2.5,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'daily validation failed',
});
});
it('returns an error if everyX is negative', async () => {
await expect(user.post('/tasks/user', {
text: 'test daily',
type: 'daily',
everyX: -1,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'daily validation failed',
});
});
it('returns an error if everyX is above 9999', async () => {
await expect(user.post('/tasks/user', {
text: 'test daily',
type: 'daily',
everyX: 10000,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'daily validation failed',
});
});
it('can create checklists', async () => {
let task = await user.post('/tasks/user', {
text: 'test daily',

View File

@@ -82,6 +82,13 @@ describe('POST /tasks/:id/score/:direction', () => {
});
it('should update the history', async () => {
let newCron = new Date(2015, 11, 20);
await user.post('/debug/set-cron', {
lastCron: newCron,
});
await user.post('/cron');
await user.post(`/tasks/${usersChallengeTaskId}/score/up`);
let tasks = await user.get(`/tasks/challenge/${challenge._id}`);

View File

@@ -139,6 +139,23 @@ describe('PUT /tasks/:id', () => {
expect(savedHabit.up).to.eql(false);
expect(savedHabit.down).to.eql(false);
});
it('allows user to update their copy', async () => {
const userTasks = await user.get('/tasks/user');
const userChallengeTasks = userTasks.filter(task => task.challenge.id === challenge._id);
const userCopyOfChallengeTask = userChallengeTasks[0];
await user.put(`/tasks/${userCopyOfChallengeTask._id}`, {
notes: 'some new notes',
counterDown: 1,
counterUp: 2,
});
const savedHabit = await user.get(`/tasks/${userCopyOfChallengeTask._id}`);
expect(savedHabit.notes).to.eql('some new notes');
expect(savedHabit.counterDown).to.eql(1);
expect(savedHabit.counterUp).to.eql(2);
});
});
context('todos', () => {

View File

@@ -0,0 +1,189 @@
import {
createAndPopulateGroup,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import { find } from 'lodash';
describe('POST /tasks/:id/needs-work/:userId', () => {
let user, guild, member, member2, task;
function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
}
beforeEach(async () => {
let {group, members, groupLeader} = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
},
members: 2,
});
guild = group;
user = groupLeader;
member = members[0];
member2 = members[1];
task = await user.post(`/tasks/group/${guild._id}`, {
text: 'test todo',
type: 'todo',
requiresApproval: true,
});
});
it('errors when user is not assigned', async () => {
await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 404,
error: 'NotFound',
message: t('taskNotFound'),
});
});
it('errors when user is not the group leader', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await expect(member.post(`/tasks/${task._id}/needs-work/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlyGroupLeaderCanEditTasks'),
});
});
it('marks as task as needing more work', async () => {
const initialNotifications = member.notifications.length;
await user.post(`/tasks/${task._id}/assign/${member._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
// score task to require approval
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.post(`/tasks/${task._id}/needs-work/${member._id}`);
[memberTasks] = await Promise.all([member.get('/tasks/user'), member.sync()]);
syncedTask = find(memberTasks, findAssignedTask);
// Check that the notification approval request has been removed
expect(syncedTask.group.approval.requested).to.equal(false);
expect(syncedTask.group.approval.requestedDate).to.equal(undefined);
// Check that the notification is correct
expect(member.notifications.length).to.equal(initialNotifications + 1);
const notification = member.notifications[member.notifications.length - 1];
expect(notification.type).to.equal('GROUP_TASK_NEEDS_WORK');
const taskText = syncedTask.text;
const managerName = user.profile.name;
expect(notification.data.message).to.equal(t('taskNeedsWork', {taskText, managerName}));
expect(notification.data.task.id).to.equal(syncedTask._id);
expect(notification.data.task.text).to.equal(taskText);
expect(notification.data.group.id).to.equal(syncedTask.group.id);
expect(notification.data.group.name).to.equal(guild.name);
expect(notification.data.manager.id).to.equal(user._id);
expect(notification.data.manager.name).to.equal(managerName);
// Check that the managers' GROUP_TASK_APPROVAL notifications have been removed
await user.sync();
expect(user.notifications.find(n => {
n.data.taskId === syncedTask._id && n.type === 'GROUP_TASK_APPROVAL';
})).to.equal(undefined);
});
it('allows a manager to mark a task as needing work', 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);
// score task to require approval
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
const initialNotifications = member.notifications.length;
await member2.post(`/tasks/${task._id}/needs-work/${member._id}`);
[memberTasks] = await Promise.all([member.get('/tasks/user'), member.sync()]);
syncedTask = find(memberTasks, findAssignedTask);
// Check that the notification approval request has been removed
expect(syncedTask.group.approval.requested).to.equal(false);
expect(syncedTask.group.approval.requestedDate).to.equal(undefined);
expect(member.notifications.length).to.equal(initialNotifications + 1);
const notification = member.notifications[member.notifications.length - 1];
expect(notification.type).to.equal('GROUP_TASK_NEEDS_WORK');
const taskText = syncedTask.text;
const managerName = member2.profile.name;
expect(notification.data.message).to.equal(t('taskNeedsWork', {taskText, managerName}));
expect(notification.data.task.id).to.equal(syncedTask._id);
expect(notification.data.task.text).to.equal(taskText);
expect(notification.data.group.id).to.equal(syncedTask.group.id);
expect(notification.data.group.name).to.equal(guild.name);
expect(notification.data.manager.id).to.equal(member2._id);
expect(notification.data.manager.name).to.equal(managerName);
// Check that the managers' GROUP_TASK_APPROVAL notifications have been removed
await Promise.all([user.sync(), member2.sync()]);
expect(user.notifications.find(n => {
n.data.taskId === syncedTask._id && n.type === 'GROUP_TASK_APPROVAL';
})).to.equal(undefined);
expect(member2.notifications.find(n => {
n.data.taskId === syncedTask._id && n.type === 'GROUP_TASK_APPROVAL';
})).to.equal(undefined);
});
it('prevents marking a task as needing work if it was already approved', 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}/needs-work/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('canOnlyApproveTaskOnce'),
});
});
it('prevents marking a task as needing work if it is not waiting for approval', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalWasNotRequested'),
});
});
});

View File

@@ -41,8 +41,9 @@ describe('POST /tasks/:id/score/:direction', () => {
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
const direction = 'up';
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
await expect(member.post(`/tasks/${syncedTask._id}/score/${direction}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
@@ -58,6 +59,7 @@ describe('POST /tasks/:id/score/:direction', () => {
user: member.auth.local.username,
taskName: updatedTask.text,
taskId: updatedTask._id,
direction,
}, 'cs')); // This test only works if we have the notification translated
expect(user.notifications[1].data.groupId).to.equal(guild._id);
@@ -71,8 +73,9 @@ describe('POST /tasks/:id/score/:direction', () => {
});
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
const direction = 'up';
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
await expect(member.post(`/tasks/${syncedTask._id}/score/${direction}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
@@ -88,6 +91,7 @@ describe('POST /tasks/:id/score/:direction', () => {
user: member.auth.local.username,
taskName: updatedTask.text,
taskId: updatedTask._id,
direction,
}));
expect(user.notifications[1].data.groupId).to.equal(guild._id);
@@ -97,6 +101,7 @@ describe('POST /tasks/:id/score/:direction', () => {
user: member.auth.local.username,
taskName: updatedTask.text,
taskId: updatedTask._id,
direction,
}));
expect(member2.notifications[0].data.groupId).to.equal(guild._id);
});

View File

@@ -75,15 +75,6 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
});
});
it('returns error when non leader tries to create a task', async () => {
await expect(member.post(`/tasks/${task._id}/unassign/${member._id}`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlyGroupLeaderCanEditTasks'),
});
});
it('unassigns a user from a task', async () => {
await user.post(`/tasks/${task._id}/unassign/${member._id}`);
@@ -129,4 +120,26 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
expect(groupTask[0].group.assignedUsers).to.not.contain(member._id);
expect(syncedTask).to.not.exist;
});
it('allows a user to unassign themselves', async () => {
await member.post(`/tasks/${task._id}/unassign/${member._id}`);
let groupTask = await user.get(`/tasks/group/${guild._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
expect(groupTask[0].group.assignedUsers).to.not.contain(member._id);
expect(syncedTask).to.not.exist;
});
// @TODO: Which do we want? The user to unassign themselves or not. This test was in
// here, but then we had a request to allow to unaissgn.
xit('returns error when non leader tries to unassign their a task', async () => {
await expect(member.post(`/tasks/${task._id}/unassign/${member._id}`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlyGroupLeaderCanEditTasks'),
});
});
});

View File

@@ -308,7 +308,7 @@ describe('DELETE /user', () => {
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('incorrectDeletePhrase'),
message: t('incorrectDeletePhrase', {magicWord: 'DELETE'}),
});
});

View File

@@ -27,4 +27,13 @@ describe('GET /user', () => {
expect(returnedUser.auth.local.salt).to.not.exist;
expect(returnedUser.apiToken).to.not.exist;
});
it('returns only user properties requested', async () => {
let returnedUser = await user.get('/user?userFields=achievements,items.mounts');
expect(returnedUser._id).to.equal(user._id);
expect(returnedUser.achievements).to.exist;
expect(returnedUser.items.mounts).to.exist;
expect(returnedUser.stats).to.not.exist;
});
});

View File

@@ -25,6 +25,7 @@ describe('GET /user/anonymized', () => {
'achievements.challenges': 'some',
'inbox.messages': [{ text: 'some text' }],
tags: [{ name: 'some name', challenge: 'some challenge' }],
notifications: [],
});
await generateHabit({ userId: user._id });
@@ -65,6 +66,7 @@ describe('GET /user/anonymized', () => {
expect(returnedUser.stats.toNextLevel).to.eql(common.tnl(user.stats.lvl));
expect(returnedUser.stats.maxMP).to.eql(30); // TODO why 30?
expect(returnedUser.newMessages).to.not.exist;
expect(returnedUser.notifications).to.not.exist;
expect(returnedUser.profile).to.not.exist;
expect(returnedUser.purchased.plan).to.not.exist;
expect(returnedUser.contributor).to.not.exist;

View File

@@ -30,7 +30,7 @@ describe('POST /user/feed/:pet/:food', () => {
data: user.items.pets['Wolf-Base'],
message: t('messageDontEnjoyFood', {
egg: pet.text(),
foodText: food.text(),
foodText: food.textThe(),
}),
});

View File

@@ -13,15 +13,20 @@ describe('POST /user/open-mystery-item', () => {
beforeEach(async () => {
user = await generateUser({
'purchased.plan.mysteryItems': [mysteryItemKey],
notifications: [
{type: 'NEW_MYSTERY_ITEMS', data: { items: [mysteryItemKey] }},
],
});
});
// More tests in common code unit tests
it('opens a mystery item', async () => {
expect(user.notifications.length).to.equal(1);
let response = await user.post('/user/open-mystery-item');
await user.sync();
expect(user.notifications.length).to.equal(0);
expect(user.items.gear.owned[mysteryItemKey]).to.be.true;
expect(response.message).to.equal(t('mysteryItemOpened'));
expect(response.data.key).to.eql(mysteryItemKey);

View File

@@ -26,13 +26,21 @@ describe('POST /user/read-card/:cardType', () => {
await user.update({
'items.special.greetingReceived': [true],
'flags.cardReceived': true,
notifications: [{
type: 'CARD_RECEIVED',
data: {card: cardType},
}],
});
await user.sync();
expect(user.notifications.length).to.equal(1);
let response = await user.post(`/user/read-card/${cardType}`);
await user.sync();
expect(response.message).to.equal(t('readCard', {cardType}));
expect(user.items.special[`${cardType}Received`]).to.be.empty;
expect(user.flags.cardReceived).to.be.false;
expect(user.notifications.length).to.equal(0);
});
});

View File

@@ -1,6 +1,7 @@
import {
generateUser,
} from '../../../../helpers/api-integration/v3';
import { mockAnalyticsService as analytics } from '../../../../../website/server/libs/analyticsService';
describe('POST /user/sleep', () => {
let user;
@@ -22,4 +23,15 @@ describe('POST /user/sleep', () => {
await user.sync();
expect(user.preferences.sleep).to.be.false;
});
it('sends sleep status to analytics service', async () => {
sandbox.spy(analytics, 'track');
await user.post('/user/sleep');
await user.sync();
expect(analytics.track).to.be.calledOnce;
expect(analytics.track).to.be.calledWith('sleep', sandbox.match.has('status', user.preferences.sleep));
sandbox.restore();
});
});

View File

@@ -6,10 +6,14 @@ import {
getProperty,
} from '../../../../../helpers/api-integration/v3';
import { ApiUser } from '../../../../../helpers/api-integration/api-classes';
import { v4 as generateRandomUserName } from 'uuid';
import { v4 as uuid } from 'uuid';
import { each } from 'lodash';
import { encrypt } from '../../../../../../website/server/libs/encryption';
function generateRandomUserName () {
return (Date.now() + uuid()).substring(0, 20);
}
describe('POST /user/auth/local/register', () => {
context('username and email are free', () => {
let api;
@@ -37,6 +41,71 @@ describe('POST /user/auth/local/register', () => {
expect(user.newUser).to.eql(true);
});
xit('remove spaces from username', async () => {
// TODO can probably delete this test now
let username = ' usernamewithspaces ';
let email = 'test@example.com';
let password = 'password';
let user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.auth.local.username).to.eql(username.trim());
expect(user.profile.name).to.eql(username.trim());
});
context('validates username', () => {
const email = 'test@example.com';
const password = 'password';
it('requires to username to be less than 20', async () => {
const username = (Date.now() + uuid()).substring(0, 21);
await expect(api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Invalid request parameters.',
});
});
it('rejects chracters not in [-_a-zA-Z0-9]', async () => {
const username = 'a-zA_Z09*';
await expect(api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Invalid request parameters.',
});
});
it('allows only [-_a-zA-Z0-9] characters', async () => {
const username = 'a-zA_Z09';
const user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
expect(user.auth.local.username).to.eql(username);
});
});
context('provides default tags and tasks', async () => {
it('for a generic API consumer', async () => {
let username = generateRandomUserName();

View File

@@ -25,12 +25,32 @@ describe('POST /user/buy-gear/:key', () => {
});
});
it('buys a piece of gear', async () => {
it('buys the first level weapon gear', async () => {
let key = 'weapon_warrior_0';
await user.post(`/user/buy-gear/${key}`);
await user.sync();
expect(user.items.gear.owned[key]).to.eql(true);
});
it('buys the first level armor gear', async () => {
let key = 'armor_warrior_1';
await user.post(`/user/buy-gear/${key}`);
await user.sync();
expect(user.items.gear.owned.armor_warrior_1).to.eql(true);
expect(user.items.gear.owned[key]).to.eql(true);
});
it('tries to buy subsequent, level gear', async () => {
let key = 'armor_warrior_2';
return expect(user.post(`/user/buy-gear/${key}`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: 'You need to purchase a lower level gear before this one.',
});
});
});

View File

@@ -0,0 +1,35 @@
import { TAVERN_ID } from '../../../../../website/server/models/group';
import { updateDocument } from '../../../../helpers/mongo';
import {
requester,
resetHabiticaDB,
} from '../../../../helpers/api-v3-integration.helper';
describe('GET /world-state', () => {
before(async () => {
await resetHabiticaDB();
});
it('returns empty worldBoss object when world boss is not active (and does not require authentication)', async () => {
const res = await requester().get('/world-state');
expect(res.worldBoss).to.eql({});
});
it('returns Tavern quest data when world boss is active', async () => {
await updateDocument('groups', {_id: TAVERN_ID}, {quest: {active: true, key: 'dysheartener', progress: {hp: 50000, rage: 9999}}});
const res = await requester().get('/world-state');
expect(res).to.have.deep.property('worldBoss');
expect(res.worldBoss).to.eql({
active: true,
extra: {},
key: 'dysheartener',
progress: {
collect: {},
hp: 50000,
rage: 9999,
},
});
});
});

View File

@@ -1,739 +0,0 @@
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';
import common from '../../../../../website/common';
const i18n = common.i18n;
describe('Amazon Payments', () => {
let subKey = 'basic_3mo';
describe('checkout', () => {
let user, orderReferenceId, headers;
let setOrderReferenceDetailsSpy;
let confirmOrderReferenceSpy;
let authorizeSpy;
let closeOrderReferenceSpy;
let paymentBuyGemsStub;
let paymentCreateSubscritionStub;
let amount = 5;
function expectAmazonStubs () {
expect(setOrderReferenceDetailsSpy).to.be.calledOnce;
expect(setOrderReferenceDetailsSpy).to.be.calledWith({
AmazonOrderReferenceId: orderReferenceId,
OrderReferenceAttributes: {
OrderTotal: {
CurrencyCode: amzLib.constants.CURRENCY_CODE,
Amount: amount,
},
SellerNote: amzLib.constants.SELLER_NOTE,
SellerOrderAttributes: {
SellerOrderId: common.uuid(),
StoreName: amzLib.constants.STORE_NAME,
},
},
});
expect(confirmOrderReferenceSpy).to.be.calledOnce;
expect(confirmOrderReferenceSpy).to.be.calledWith({ AmazonOrderReferenceId: orderReferenceId });
expect(authorizeSpy).to.be.calledOnce;
expect(authorizeSpy).to.be.calledWith({
AmazonOrderReferenceId: orderReferenceId,
AuthorizationReferenceId: common.uuid().substring(0, 32),
AuthorizationAmount: {
CurrencyCode: amzLib.constants.CURRENCY_CODE,
Amount: amount,
},
SellerAuthorizationNote: amzLib.constants.SELLER_NOTE,
TransactionTimeout: 0,
CaptureNow: true,
});
expect(closeOrderReferenceSpy).to.be.calledOnce;
expect(closeOrderReferenceSpy).to.be.calledWith({ AmazonOrderReferenceId: orderReferenceId });
}
beforeEach(function () {
user = new User();
headers = {};
orderReferenceId = 'orderReferenceId';
setOrderReferenceDetailsSpy = sinon.stub(amzLib, 'setOrderReferenceDetails');
setOrderReferenceDetailsSpy.returnsPromise().resolves({});
confirmOrderReferenceSpy = sinon.stub(amzLib, 'confirmOrderReference');
confirmOrderReferenceSpy.returnsPromise().resolves({});
authorizeSpy = sinon.stub(amzLib, 'authorize');
authorizeSpy.returnsPromise().resolves({});
closeOrderReferenceSpy = sinon.stub(amzLib, 'closeOrderReference');
closeOrderReferenceSpy.returnsPromise().resolves({});
paymentBuyGemsStub = sinon.stub(payments, 'buyGems');
paymentBuyGemsStub.returnsPromise().resolves({});
paymentCreateSubscritionStub = sinon.stub(payments, 'createSubscription');
paymentCreateSubscritionStub.returnsPromise().resolves({});
sinon.stub(common, 'uuid').returns('uuid-generated');
});
afterEach(function () {
amzLib.setOrderReferenceDetails.restore();
amzLib.confirmOrderReference.restore();
amzLib.authorize.restore();
amzLib.closeOrderReference.restore();
payments.buyGems.restore();
payments.createSubscription.restore();
common.uuid.restore();
});
it('should purchase gems', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
await amzLib.checkout({user, orderReferenceId, headers});
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,
paymentMethod: amzLib.constants.PAYMENT_METHOD,
headers,
});
expectAmazonStubs();
expect(user.canGetGems).to.be.calledOnce;
user.canGetGems.restore();
});
it('should error if gem amount is too low', async () => {
let receivingUser = new User();
receivingUser.save();
let gift = {
type: 'gems',
gems: {
amount: 0,
uuid: receivingUser._id,
},
};
await expect(amzLib.checkout({gift, user, orderReferenceId, headers}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
message: 'Amount must be at least 1.',
name: 'BadRequest',
});
});
it('should error if user cannot get gems gems', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
await expect(amzLib.checkout({user, orderReferenceId, headers})).to.eventually.be.rejected.and.to.eql({
httpCode: 401,
message: i18n.t('groupPolicyCannotGetGems'),
name: 'NotAuthorized',
});
user.canGetGems.restore();
});
it('should gift gems', async () => {
let receivingUser = new User();
await receivingUser.save();
let gift = {
type: 'gems',
uuid: receivingUser._id,
gems: {
amount: 16,
},
};
amount = 16 / 4;
await amzLib.checkout({gift, user, orderReferenceId, headers});
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,
paymentMethod: amzLib.constants.PAYMENT_METHOD_GIFT,
headers,
gift,
});
expectAmazonStubs();
});
it('should gift a subscription', async () => {
let receivingUser = new User();
receivingUser.save();
let gift = {
type: 'subscription',
subscription: {
key: subKey,
uuid: receivingUser._id,
},
};
amount = common.content.subscriptionBlocks[subKey].price;
await amzLib.checkout({user, orderReferenceId, headers, gift});
gift.member = receivingUser;
expect(paymentCreateSubscritionStub).to.be.calledOnce;
expect(paymentCreateSubscritionStub).to.be.calledWith({
user,
paymentMethod: amzLib.constants.PAYMENT_METHOD_GIFT,
headers,
gift,
});
expectAmazonStubs();
});
});
describe('subscribe', () => {
let user, group, amount, billingAgreementId, sub, coupon, groupId, headers;
let amazonSetBillingAgreementDetailsSpy;
let amazonConfirmBillingAgreementSpy;
let amazongAuthorizeOnBillingAgreementSpy;
let createSubSpy;
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
user.purchased.plan.customerId = 'customer-id';
user.purchased.plan.planId = subKey;
user.purchased.plan.lastBillingDate = new Date();
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
group.purchased.plan.customerId = 'customer-id';
group.purchased.plan.planId = subKey;
await group.save();
amount = common.content.subscriptionBlocks[subKey].price;
billingAgreementId = 'billingAgreementId';
sub = {
key: subKey,
price: amount,
};
groupId = group._id;
headers = {};
amazonSetBillingAgreementDetailsSpy = sinon.stub(amzLib, 'setBillingAgreementDetails');
amazonSetBillingAgreementDetailsSpy.returnsPromise().resolves({});
amazonConfirmBillingAgreementSpy = sinon.stub(amzLib, 'confirmBillingAgreement');
amazonConfirmBillingAgreementSpy.returnsPromise().resolves({});
amazongAuthorizeOnBillingAgreementSpy = sinon.stub(amzLib, 'authorizeOnBillingAgreement');
amazongAuthorizeOnBillingAgreementSpy.returnsPromise().resolves({});
createSubSpy = sinon.stub(payments, 'createSubscription');
createSubSpy.returnsPromise().resolves({});
sinon.stub(common, 'uuid').returns('uuid-generated');
});
afterEach(function () {
amzLib.setBillingAgreementDetails.restore();
amzLib.confirmBillingAgreement.restore();
amzLib.authorizeOnBillingAgreement.restore();
payments.createSubscription.restore();
common.uuid.restore();
});
it('should throw an error if we are missing a subscription', async () => {
await expect(amzLib.subscribe({
billingAgreementId,
coupon,
user,
groupId,
headers,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
message: i18n.t('missingSubscriptionCode'),
});
});
it('should throw an error if we are missing a billingAgreementId', async () => {
await expect(amzLib.subscribe({
sub,
coupon,
user,
groupId,
headers,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
message: 'Missing req.body.billingAgreementId',
});
});
it('should throw an error when coupon code is missing', async () => {
sub.discount = 40;
await expect(amzLib.subscribe({
billingAgreementId,
sub,
coupon,
user,
groupId,
headers,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
message: i18n.t('couponCodeRequired'),
});
});
it('should throw an error when coupon code is invalid', async () => {
sub.discount = 40;
sub.key = 'google_6mo';
coupon = 'example-coupon';
let couponModel = new Coupon();
couponModel.event = 'google_6mo';
await couponModel.save();
sinon.stub(cc, 'validate').returns('invalid');
await expect(amzLib.subscribe({
billingAgreementId,
sub,
coupon,
user,
groupId,
headers,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('invalidCoupon'),
});
cc.validate.restore();
});
it('subscribes with amazon with a coupon', async () => {
sub.discount = 40;
sub.key = 'google_6mo';
coupon = 'example-coupon';
let couponModel = new Coupon();
couponModel.event = 'google_6mo';
let updatedCouponModel = await couponModel.save();
sinon.stub(cc, 'validate').returns(updatedCouponModel._id);
await amzLib.subscribe({
billingAgreementId,
sub,
coupon,
user,
groupId,
headers,
});
expect(createSubSpy).to.be.calledOnce;
expect(createSubSpy).to.be.calledWith({
user,
customerId: billingAgreementId,
paymentMethod: amzLib.constants.PAYMENT_METHOD,
sub,
headers,
groupId,
});
cc.validate.restore();
});
it('subscribes with amazon', async () => {
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,
});
});
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,
});
});
});
describe('cancelSubscription', () => {
let user, group, headers, billingAgreementId, subscriptionBlock, subscriptionLength;
let getBillingAgreementDetailsSpy;
let paymentCancelSubscriptionSpy;
function expectAmazonStubs () {
expect(getBillingAgreementDetailsSpy).to.be.calledOnce;
expect(getBillingAgreementDetailsSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
});
}
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
user.purchased.plan.customerId = 'customer-id';
user.purchased.plan.planId = subKey;
user.purchased.plan.lastBillingDate = new Date();
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
group.purchased.plan.customerId = 'customer-id';
group.purchased.plan.planId = subKey;
group.purchased.plan.lastBillingDate = new Date();
await group.save();
subscriptionBlock = common.content.subscriptionBlocks[subKey];
subscriptionLength = subscriptionBlock.months * 30;
headers = {};
getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails');
getBillingAgreementDetailsSpy.returnsPromise().resolves({
BillingAgreementDetails: {
BillingAgreementStatus: {State: 'Closed'},
},
});
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription');
paymentCancelSubscriptionSpy.returnsPromise().resolves({});
});
afterEach(function () {
amzLib.getBillingAgreementDetails.restore();
payments.cancelSubscription.restore();
});
it('should throw an error if we are missing a subscription', async () => {
user.purchased.plan.customerId = undefined;
await expect(amzLib.cancelSubscription({user}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('missingSubscription'),
});
});
it('should cancel a user subscription', async () => {
billingAgreementId = user.purchased.plan.customerId;
await amzLib.cancelSubscription({user, headers});
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
user,
groupId: undefined,
nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: amzLib.constants.PAYMENT_METHOD,
headers,
cancellationReason: undefined,
});
expectAmazonStubs();
});
it('should close a user subscription if amazon not closed', async () => {
amzLib.getBillingAgreementDetails.restore();
getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails')
.returnsPromise()
.resolves({
BillingAgreementDetails: {
BillingAgreementStatus: {State: 'Open'},
},
});
let closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').returnsPromise().resolves({});
billingAgreementId = user.purchased.plan.customerId;
await amzLib.cancelSubscription({user, headers});
expectAmazonStubs();
expect(closeBillingAgreementSpy).to.be.calledOnce;
expect(closeBillingAgreementSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
});
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
user,
groupId: undefined,
nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: amzLib.constants.PAYMENT_METHOD,
headers,
cancellationReason: undefined,
});
amzLib.closeBillingAgreement.restore();
});
it('should throw an error if group is not found', async () => {
await expect(amzLib.cancelSubscription({user, groupId: 'fake-id'}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 404,
name: 'NotFound',
message: i18n.t('groupNotFound'),
});
});
it('should throw an error if user is not group leader', async () => {
let nonLeader = new User();
nonLeader.guilds.push(group._id);
await nonLeader.save();
await expect(amzLib.cancelSubscription({user: nonLeader, groupId: group._id}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
});
});
it('should cancel a group subscription', async () => {
billingAgreementId = group.purchased.plan.customerId;
await amzLib.cancelSubscription({user, groupId: group._id, headers});
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
user,
groupId: group._id,
nextBill: moment(group.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: amzLib.constants.PAYMENT_METHOD,
headers,
cancellationReason: undefined,
});
expectAmazonStubs();
});
it('should close a group subscription if amazon not closed', async () => {
amzLib.getBillingAgreementDetails.restore();
getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails')
.returnsPromise()
.resolves({
BillingAgreementDetails: {
BillingAgreementStatus: {State: 'Open'},
},
});
let closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').returnsPromise().resolves({});
billingAgreementId = group.purchased.plan.customerId;
await amzLib.cancelSubscription({user, groupId: group._id, headers});
expectAmazonStubs();
expect(closeBillingAgreementSpy).to.be.calledOnce;
expect(closeBillingAgreementSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
});
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
user,
groupId: group._id,
nextBill: moment(group.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: amzLib.constants.PAYMENT_METHOD,
headers,
cancellationReason: undefined,
});
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,
},
});
});
});
});

View File

@@ -153,6 +153,24 @@ describe('payments/index', () => {
expect(recipient.purchased.plan.dateUpdated).to.exist;
});
it('sets plan.dateUpdated if it did exist but the user has cancelled', async () => {
recipient.purchased.plan.dateUpdated = moment().subtract(1, 'days').toDate();
recipient.purchased.plan.dateTerminated = moment().subtract(1, 'days').toDate();
recipient.purchased.plan.customerId = 'testing';
await api.createSubscription(data);
expect(moment(recipient.purchased.plan.dateUpdated).date()).to.eql(moment().date());
});
it('sets plan.dateUpdated if it did exist but the user has a corrupt plan', async () => {
recipient.purchased.plan.dateUpdated = moment().subtract(1, 'days').toDate();
await api.createSubscription(data);
expect(moment(recipient.purchased.plan.dateUpdated).date()).to.eql(moment().date());
});
it('sets plan.dateCreated if it did not previously exist', async () => {
expect(recipient.purchased.plan.dateCreated).to.not.exist;
@@ -399,13 +417,19 @@ describe('payments/index', () => {
it('awards mystery items when within the timeframe for a mystery item', async () => {
let mayMysteryItemTimeframe = 1464725113000; // May 31st 2016
let fakeClock = sinon.useFakeTimers(mayMysteryItemTimeframe);
data = { paymentMethod: 'PaymentMethod', user, sub: { key: 'basic_3mo' } };
const oldNotificationsCount = user.notifications.length;
await api.createSubscription(data);
expect(user.notifications.find(n => n.type === 'NEW_MYSTERY_ITEMS')).to.not.be.undefined;
expect(user.purchased.plan.mysteryItems).to.have.a.lengthOf(2);
expect(user.purchased.plan.mysteryItems).to.include('armor_mystery_201605');
expect(user.purchased.plan.mysteryItems).to.include('head_mystery_201605');
expect(user.notifications.length).to.equal(oldNotificationsCount + 1);
expect(user.notifications[0].type).to.equal('NEW_MYSTERY_ITEMS');
fakeClock.restore();
});

View File

@@ -0,0 +1,180 @@
import moment from 'moment';
import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
import amzLib from '../../../../../../../website/server/libs/amazonPayments';
import payments from '../../../../../../../website/server/libs/payments';
import common from '../../../../../../../website/common';
import { createNonLeaderGroupMember } from '../paymentHelpers';
const i18n = common.i18n;
describe('Amazon Payments - Cancel Subscription', () => {
const subKey = 'basic_3mo';
let user, group, headers, billingAgreementId, subscriptionBlock, subscriptionLength;
let getBillingAgreementDetailsSpy;
let paymentCancelSubscriptionSpy;
function expectAmazonStubs () {
expect(getBillingAgreementDetailsSpy).to.be.calledOnce;
expect(getBillingAgreementDetailsSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
});
}
function expectAmazonCancelSubscriptionSpy (groupId, lastBillingDate) {
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
user,
groupId,
nextBill: moment(lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: amzLib.constants.PAYMENT_METHOD,
headers,
cancellationReason: undefined,
});
}
function expectAmazonCancelUserSubscriptionSpy () {
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
expectAmazonCancelSubscriptionSpy(undefined, user.purchased.plan.lastBillingDate);
}
function expectAmazonCancelGroupSubscriptionSpy (groupId) {
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
expectAmazonCancelSubscriptionSpy(groupId, group.purchased.plan.lastBillingDate);
}
function expectBillingAggreementDetailSpy () {
getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails')
.returnsPromise()
.resolves({
BillingAgreementDetails: {
BillingAgreementStatus: {State: 'Open'},
},
});
}
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
user.purchased.plan.customerId = 'customer-id';
user.purchased.plan.planId = subKey;
user.purchased.plan.lastBillingDate = new Date();
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
group.purchased.plan.customerId = 'customer-id';
group.purchased.plan.planId = subKey;
group.purchased.plan.lastBillingDate = new Date();
await group.save();
subscriptionBlock = common.content.subscriptionBlocks[subKey];
subscriptionLength = subscriptionBlock.months * 30;
headers = {};
getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails');
getBillingAgreementDetailsSpy.returnsPromise().resolves({
BillingAgreementDetails: {
BillingAgreementStatus: {State: 'Closed'},
},
});
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription');
paymentCancelSubscriptionSpy.returnsPromise().resolves({});
});
afterEach(function () {
amzLib.getBillingAgreementDetails.restore();
payments.cancelSubscription.restore();
});
it('should throw an error if we are missing a subscription', async () => {
user.purchased.plan.customerId = undefined;
await expect(amzLib.cancelSubscription({user}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('missingSubscription'),
});
});
it('should cancel a user subscription', async () => {
billingAgreementId = user.purchased.plan.customerId;
await amzLib.cancelSubscription({user, headers});
expectAmazonCancelUserSubscriptionSpy();
expectAmazonStubs();
});
it('should close a user subscription if amazon not closed', async () => {
amzLib.getBillingAgreementDetails.restore();
expectBillingAggreementDetailSpy();
let closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').returnsPromise().resolves({});
billingAgreementId = user.purchased.plan.customerId;
await amzLib.cancelSubscription({user, headers});
expectAmazonStubs();
expect(closeBillingAgreementSpy).to.be.calledOnce;
expect(closeBillingAgreementSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
});
expectAmazonCancelUserSubscriptionSpy();
amzLib.closeBillingAgreement.restore();
});
it('should throw an error if group is not found', async () => {
await expect(amzLib.cancelSubscription({user, groupId: 'fake-id'}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 404,
name: 'NotFound',
message: i18n.t('groupNotFound'),
});
});
it('should throw an error if user is not group leader', async () => {
let nonLeader = await createNonLeaderGroupMember(group);
await expect(amzLib.cancelSubscription({user: nonLeader, groupId: group._id}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
});
});
it('should cancel a group subscription', async () => {
billingAgreementId = group.purchased.plan.customerId;
await amzLib.cancelSubscription({user, groupId: group._id, headers});
expectAmazonCancelGroupSubscriptionSpy(group._id);
expectAmazonStubs();
});
it('should close a group subscription if amazon not closed', async () => {
amzLib.getBillingAgreementDetails.restore();
expectBillingAggreementDetailSpy();
let closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').returnsPromise().resolves({});
billingAgreementId = group.purchased.plan.customerId;
await amzLib.cancelSubscription({user, groupId: group._id, headers});
expectAmazonStubs();
expect(closeBillingAgreementSpy).to.be.calledOnce;
expect(closeBillingAgreementSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
});
expectAmazonCancelGroupSubscriptionSpy(group._id);
amzLib.closeBillingAgreement.restore();
});
});

View File

@@ -0,0 +1,193 @@
import { model as User } from '../../../../../../../website/server/models/user';
import amzLib from '../../../../../../../website/server/libs/amazonPayments';
import payments from '../../../../../../../website/server/libs/payments';
import common from '../../../../../../../website/common';
const i18n = common.i18n;
describe('Amazon Payments - Checkout', () => {
const subKey = 'basic_3mo';
let user, orderReferenceId, headers;
let setOrderReferenceDetailsSpy;
let confirmOrderReferenceSpy;
let authorizeSpy;
let closeOrderReferenceSpy;
let paymentBuyGemsStub;
let paymentCreateSubscritionStub;
let amount = 5;
function expectOrderReferenceSpy () {
expect(setOrderReferenceDetailsSpy).to.be.calledOnce;
expect(setOrderReferenceDetailsSpy).to.be.calledWith({
AmazonOrderReferenceId: orderReferenceId,
OrderReferenceAttributes: {
OrderTotal: {
CurrencyCode: amzLib.constants.CURRENCY_CODE,
Amount: amount,
},
SellerNote: amzLib.constants.SELLER_NOTE,
SellerOrderAttributes: {
SellerOrderId: common.uuid(),
StoreName: amzLib.constants.STORE_NAME,
},
},
});
}
function expectAuthorizeSpy () {
expect(authorizeSpy).to.be.calledOnce;
expect(authorizeSpy).to.be.calledWith({
AmazonOrderReferenceId: orderReferenceId,
AuthorizationReferenceId: common.uuid().substring(0, 32),
AuthorizationAmount: {
CurrencyCode: amzLib.constants.CURRENCY_CODE,
Amount: amount,
},
SellerAuthorizationNote: amzLib.constants.SELLER_NOTE,
TransactionTimeout: 0,
CaptureNow: true,
});
}
function expectAmazonStubs () {
expectOrderReferenceSpy();
expect(confirmOrderReferenceSpy).to.be.calledOnce;
expect(confirmOrderReferenceSpy).to.be.calledWith({ AmazonOrderReferenceId: orderReferenceId });
expectAuthorizeSpy();
expect(closeOrderReferenceSpy).to.be.calledOnce;
expect(closeOrderReferenceSpy).to.be.calledWith({ AmazonOrderReferenceId: orderReferenceId });
}
beforeEach(function () {
user = new User();
headers = {};
orderReferenceId = 'orderReferenceId';
setOrderReferenceDetailsSpy = sinon.stub(amzLib, 'setOrderReferenceDetails');
setOrderReferenceDetailsSpy.returnsPromise().resolves({});
confirmOrderReferenceSpy = sinon.stub(amzLib, 'confirmOrderReference');
confirmOrderReferenceSpy.returnsPromise().resolves({});
authorizeSpy = sinon.stub(amzLib, 'authorize');
authorizeSpy.returnsPromise().resolves({});
closeOrderReferenceSpy = sinon.stub(amzLib, 'closeOrderReference');
closeOrderReferenceSpy.returnsPromise().resolves({});
paymentBuyGemsStub = sinon.stub(payments, 'buyGems');
paymentBuyGemsStub.returnsPromise().resolves({});
paymentCreateSubscritionStub = sinon.stub(payments, 'createSubscription');
paymentCreateSubscritionStub.returnsPromise().resolves({});
sinon.stub(common, 'uuid').returns('uuid-generated');
});
afterEach(function () {
amzLib.setOrderReferenceDetails.restore();
amzLib.confirmOrderReference.restore();
amzLib.authorize.restore();
amzLib.closeOrderReference.restore();
payments.buyGems.restore();
payments.createSubscription.restore();
common.uuid.restore();
});
function expectBuyGemsStub (paymentMethod, gift) {
expect(paymentBuyGemsStub).to.be.calledOnce;
let expectedArgs = {
user,
paymentMethod,
headers,
};
if (gift) expectedArgs.gift = gift;
expect(paymentBuyGemsStub).to.be.calledWith(expectedArgs);
}
it('should purchase gems', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
await amzLib.checkout({user, orderReferenceId, headers});
expectBuyGemsStub(amzLib.constants.PAYMENT_METHOD);
expectAmazonStubs();
expect(user.canGetGems).to.be.calledOnce;
user.canGetGems.restore();
});
it('should error if gem amount is too low', async () => {
let receivingUser = new User();
receivingUser.save();
let gift = {
type: 'gems',
gems: {
amount: 0,
uuid: receivingUser._id,
},
};
await expect(amzLib.checkout({gift, user, orderReferenceId, headers}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
message: 'Amount must be at least 1.',
name: 'BadRequest',
});
});
it('should error if user cannot get gems gems', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
await expect(amzLib.checkout({user, orderReferenceId, headers})).to.eventually.be.rejected.and.to.eql({
httpCode: 401,
message: i18n.t('groupPolicyCannotGetGems'),
name: 'NotAuthorized',
});
user.canGetGems.restore();
});
it('should gift gems', async () => {
let receivingUser = new User();
await receivingUser.save();
let gift = {
type: 'gems',
uuid: receivingUser._id,
gems: {
amount: 16,
},
};
amount = 16 / 4;
await amzLib.checkout({gift, user, orderReferenceId, headers});
expectBuyGemsStub(amzLib.constants.PAYMENT_METHOD_GIFT, gift);
expectAmazonStubs();
});
it('should gift a subscription', async () => {
let receivingUser = new User();
receivingUser.save();
let gift = {
type: 'subscription',
subscription: {
key: subKey,
uuid: receivingUser._id,
},
};
amount = common.content.subscriptionBlocks[subKey].price;
await amzLib.checkout({user, orderReferenceId, headers, gift});
gift.member = receivingUser;
expect(paymentCreateSubscritionStub).to.be.calledOnce;
expect(paymentCreateSubscritionStub).to.be.calledWith({
user,
paymentMethod: amzLib.constants.PAYMENT_METHOD_GIFT,
headers,
gift,
});
expectAmazonStubs();
});
});

View File

@@ -0,0 +1,267 @@
import cc from 'coupon-code';
import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
import { model as Coupon } from '../../../../../../../website/server/models/coupon';
import amzLib from '../../../../../../../website/server/libs/amazonPayments';
import payments from '../../../../../../../website/server/libs/payments';
import common from '../../../../../../../website/common';
const i18n = common.i18n;
describe('Amazon Payments - Subscribe', () => {
const subKey = 'basic_3mo';
let user, group, amount, billingAgreementId, sub, coupon, groupId, headers;
let amazonSetBillingAgreementDetailsSpy;
let amazonConfirmBillingAgreementSpy;
let amazonAuthorizeOnBillingAgreementSpy;
let createSubSpy;
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
user.purchased.plan.customerId = 'customer-id';
user.purchased.plan.planId = subKey;
user.purchased.plan.lastBillingDate = new Date();
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
group.purchased.plan.customerId = 'customer-id';
group.purchased.plan.planId = subKey;
await group.save();
amount = common.content.subscriptionBlocks[subKey].price;
billingAgreementId = 'billingAgreementId';
sub = {
key: subKey,
price: amount,
};
groupId = group._id;
headers = {};
amazonSetBillingAgreementDetailsSpy = sinon.stub(amzLib, 'setBillingAgreementDetails');
amazonSetBillingAgreementDetailsSpy.returnsPromise().resolves({});
amazonConfirmBillingAgreementSpy = sinon.stub(amzLib, 'confirmBillingAgreement');
amazonConfirmBillingAgreementSpy.returnsPromise().resolves({});
amazonAuthorizeOnBillingAgreementSpy = sinon.stub(amzLib, 'authorizeOnBillingAgreement');
amazonAuthorizeOnBillingAgreementSpy.returnsPromise().resolves({});
createSubSpy = sinon.stub(payments, 'createSubscription');
createSubSpy.returnsPromise().resolves({});
sinon.stub(common, 'uuid').returns('uuid-generated');
});
afterEach(function () {
amzLib.setBillingAgreementDetails.restore();
amzLib.confirmBillingAgreement.restore();
amzLib.authorizeOnBillingAgreement.restore();
payments.createSubscription.restore();
common.uuid.restore();
});
function expectAmazonAuthorizeBillingAgreementSpy () {
expect(amazonAuthorizeOnBillingAgreementSpy).to.be.calledOnce;
expect(amazonAuthorizeOnBillingAgreementSpy).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,
},
});
}
function expectAmazonSetBillingAgreementDetailsSpy () {
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,
},
},
});
}
function expectCreateSpy () {
expect(createSubSpy).to.be.calledOnce;
expect(createSubSpy).to.be.calledWith({
user,
customerId: billingAgreementId,
paymentMethod: amzLib.constants.PAYMENT_METHOD,
sub,
headers,
groupId,
});
}
it('should throw an error if we are missing a subscription', async () => {
await expect(amzLib.subscribe({
billingAgreementId,
coupon,
user,
groupId,
headers,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
message: i18n.t('missingSubscriptionCode'),
});
});
it('should throw an error if we are missing a billingAgreementId', async () => {
await expect(amzLib.subscribe({
sub,
coupon,
user,
groupId,
headers,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
message: 'Missing req.body.billingAgreementId',
});
});
it('should throw an error when coupon code is missing', async () => {
sub.discount = 40;
await expect(amzLib.subscribe({
billingAgreementId,
sub,
coupon,
user,
groupId,
headers,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
message: i18n.t('couponCodeRequired'),
});
});
it('should throw an error when coupon code is invalid', async () => {
sub.discount = 40;
sub.key = 'google_6mo';
coupon = 'example-coupon';
let couponModel = new Coupon();
couponModel.event = 'google_6mo';
await couponModel.save();
sinon.stub(cc, 'validate').returns('invalid');
await expect(amzLib.subscribe({
billingAgreementId,
sub,
coupon,
user,
groupId,
headers,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('invalidCoupon'),
});
cc.validate.restore();
});
it('subscribes with amazon with a coupon', async () => {
sub.discount = 40;
sub.key = 'google_6mo';
coupon = 'example-coupon';
let couponModel = new Coupon();
couponModel.event = 'google_6mo';
let updatedCouponModel = await couponModel.save();
sinon.stub(cc, 'validate').returns(updatedCouponModel._id);
await amzLib.subscribe({
billingAgreementId,
sub,
coupon,
user,
groupId,
headers,
});
expectCreateSpy();
cc.validate.restore();
});
it('subscribes with amazon', async () => {
await amzLib.subscribe({
billingAgreementId,
sub,
coupon,
user,
groupId,
headers,
});
expectAmazonSetBillingAgreementDetailsSpy();
expect(amazonConfirmBillingAgreementSpy).to.be.calledOnce;
expect(amazonConfirmBillingAgreementSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
});
expectAmazonAuthorizeBillingAgreementSpy();
expectCreateSpy();
});
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,
});
expectAmazonSetBillingAgreementDetailsSpy();
expect(amazonConfirmBillingAgreementSpy).to.be.calledOnce;
expect(amazonConfirmBillingAgreementSpy).to.be.calledWith({
AmazonBillingAgreementId: billingAgreementId,
});
expectAmazonAuthorizeBillingAgreementSpy();
expectCreateSpy();
});
});

View File

@@ -0,0 +1,83 @@
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 amzLib from '../../../../../../../website/server/libs/amazonPayments';
import payments from '../../../../../../../website/server/libs/payments';
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 () {
amzLib.authorizeOnBillingAgreement.restore();
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,
},
});
});
});

View File

@@ -0,0 +1,7 @@
import { model as User } from '../../../../../../website/server/models/user';
export async function createNonLeaderGroupMember (group) {
let nonLeader = new User();
nonLeader.guilds.push(group._id);
return await nonLeader.save();
}

View File

@@ -0,0 +1,87 @@
/* eslint-disable camelcase */
import payments from '../../../../../../../website/server/libs/payments';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
import { model as User } from '../../../../../../../website/server/models/user';
describe('checkout success', () => {
const subKey = 'basic_3mo';
let user, gift, customerId, paymentId;
let paypalPaymentExecuteStub, paymentBuyGemsStub, paymentsCreateSubscritionStub;
beforeEach(() => {
user = new User();
customerId = 'customerId-test';
paymentId = 'paymentId-test';
paypalPaymentExecuteStub = sinon.stub(paypalPayments, 'paypalPaymentExecute').returnsPromise().resolves({});
paymentBuyGemsStub = sinon.stub(payments, 'buyGems').returnsPromise().resolves({});
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({});
});
afterEach(() => {
paypalPayments.paypalPaymentExecute.restore();
payments.buyGems.restore();
payments.createSubscription.restore();
});
it('purchases gems', async () => {
await paypalPayments.checkoutSuccess({user, gift, paymentId, customerId});
expect(paypalPaymentExecuteStub).to.be.calledOnce;
expect(paypalPaymentExecuteStub).to.be.calledWith(paymentId, { payer_id: customerId });
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,
customerId,
paymentMethod: 'Paypal',
});
});
it('gifts gems', async () => {
let receivingUser = new User();
await receivingUser.save();
gift = {
type: 'gems',
gems: {
amount: 16,
uuid: receivingUser._id,
},
};
await paypalPayments.checkoutSuccess({user, gift, paymentId, customerId});
expect(paypalPaymentExecuteStub).to.be.calledOnce;
expect(paypalPaymentExecuteStub).to.be.calledWith(paymentId, { payer_id: customerId });
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,
customerId,
paymentMethod: 'PayPal (Gift)',
gift,
});
});
it('gifts subscription', async () => {
let receivingUser = new User();
await receivingUser.save();
gift = {
type: 'subscription',
subscription: {
key: subKey,
uuid: receivingUser._id,
},
};
await paypalPayments.checkoutSuccess({user, gift, paymentId, customerId});
expect(paypalPaymentExecuteStub).to.be.calledOnce;
expect(paypalPaymentExecuteStub).to.be.calledWith(paymentId, { payer_id: customerId });
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
expect(paymentsCreateSubscritionStub).to.be.calledWith({
user,
customerId,
paymentMethod: 'PayPal (Gift)',
gift,
});
});
});

View File

@@ -0,0 +1,127 @@
/* eslint-disable camelcase */
import nconf from 'nconf';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
import { model as User } from '../../../../../../../website/server/models/user';
import common from '../../../../../../../website/common';
const BASE_URL = nconf.get('BASE_URL');
const i18n = common.i18n;
describe('checkout', () => {
const subKey = 'basic_3mo';
let paypalPaymentCreateStub;
let approvalHerf;
function getPaypalCreateOptions (description, amount) {
return {
intent: 'sale',
payer: { payment_method: 'Paypal' },
redirect_urls: {
return_url: `${BASE_URL}/paypal/checkout/success`,
cancel_url: `${BASE_URL}`,
},
transactions: [{
item_list: {
items: [{
name: description,
price: amount,
currency: 'USD',
quantity: 1,
}],
},
amount: {
currency: 'USD',
total: amount,
},
description,
}],
};
}
beforeEach(() => {
approvalHerf = 'approval_href';
paypalPaymentCreateStub = sinon.stub(paypalPayments, 'paypalPaymentCreate')
.returnsPromise().resolves({
links: [{ rel: 'approval_url', href: approvalHerf }],
});
});
afterEach(() => {
paypalPayments.paypalPaymentCreate.restore();
});
it('creates a link for gem purchases', async () => {
let link = await paypalPayments.checkout({user: new User()});
expect(paypalPaymentCreateStub).to.be.calledOnce;
expect(paypalPaymentCreateStub).to.be.calledWith(getPaypalCreateOptions('Habitica Gems', 5.00));
expect(link).to.eql(approvalHerf);
});
it('should error if gem amount is too low', async () => {
let receivingUser = new User();
receivingUser.save();
let gift = {
type: 'gems',
gems: {
amount: 0,
uuid: receivingUser._id,
},
};
await expect(paypalPayments.checkout({gift}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
message: 'Amount must be at least 1.',
name: 'BadRequest',
});
});
it('should error if the user cannot get gems', async () => {
let user = new User();
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
await expect(paypalPayments.checkout({user})).to.eventually.be.rejected.and.to.eql({
httpCode: 401,
message: i18n.t('groupPolicyCannotGetGems'),
name: 'NotAuthorized',
});
});
it('creates a link for gifting gems', async () => {
let receivingUser = new User();
await receivingUser.save();
let gift = {
type: 'gems',
uuid: receivingUser._id,
gems: {
amount: 16,
},
};
let link = await paypalPayments.checkout({gift});
expect(paypalPaymentCreateStub).to.be.calledOnce;
expect(paypalPaymentCreateStub).to.be.calledWith(getPaypalCreateOptions('Habitica Gems (Gift)', '4.00'));
expect(link).to.eql(approvalHerf);
});
it('creates a link for gifting a subscription', async () => {
let receivingUser = new User();
receivingUser.save();
let gift = {
type: 'subscription',
subscription: {
key: subKey,
uuid: receivingUser._id,
},
};
let link = await paypalPayments.checkout({gift});
expect(paypalPaymentCreateStub).to.be.calledOnce;
expect(paypalPaymentCreateStub).to.be.calledWith(getPaypalCreateOptions('mo. Habitica Subscription (Gift)', '15.00'));
expect(link).to.eql(approvalHerf);
});
});

View File

@@ -0,0 +1,66 @@
/* eslint-disable camelcase */
import payments from '../../../../../../../website/server/libs/payments';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
describe('ipn', () => {
const subKey = 'basic_3mo';
let user, group, txn_type, userPaymentId, groupPaymentId;
let ipnVerifyAsyncStub, paymentCancelSubscriptionSpy;
beforeEach(async () => {
txn_type = 'recurring_payment_profile_cancel';
userPaymentId = 'userPaymentId-test';
groupPaymentId = 'groupPaymentId-test';
user = new User();
user.profile.name = 'sender';
user.purchased.plan.customerId = userPaymentId;
user.purchased.plan.planId = subKey;
user.purchased.plan.lastBillingDate = new Date();
await user.save();
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
group.purchased.plan.customerId = groupPaymentId;
group.purchased.plan.planId = subKey;
group.purchased.plan.lastBillingDate = new Date();
await group.save();
ipnVerifyAsyncStub = sinon.stub(paypalPayments, 'ipnVerifyAsync').returnsPromise().resolves({});
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
});
afterEach(function () {
paypalPayments.ipnVerifyAsync.restore();
payments.cancelSubscription.restore();
});
it('should cancel a user subscription', async () => {
await paypalPayments.ipn({txn_type, recurring_payment_id: userPaymentId});
expect(ipnVerifyAsyncStub).to.be.calledOnce;
expect(ipnVerifyAsyncStub).to.be.calledWith({txn_type, recurring_payment_id: userPaymentId});
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy.args[0][0].user._id).to.eql(user._id);
expect(paymentCancelSubscriptionSpy.args[0][0].paymentMethod).to.eql('Paypal');
});
it('should cancel a group subscription', async () => {
await paypalPayments.ipn({txn_type, recurring_payment_id: groupPaymentId});
expect(ipnVerifyAsyncStub).to.be.calledOnce;
expect(ipnVerifyAsyncStub).to.be.calledWith({txn_type, recurring_payment_id: groupPaymentId});
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy).to.be.calledWith({ groupId: group._id, paymentMethod: 'Paypal' });
});
});

View File

@@ -0,0 +1,124 @@
/* eslint-disable camelcase */
import payments from '../../../../../../../website/server/libs/payments';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
import common from '../../../../../../../website/common';
import { createNonLeaderGroupMember } from '../paymentHelpers';
const i18n = common.i18n;
describe('subscribeCancel', () => {
const subKey = 'basic_3mo';
let user, group, groupId, customerId, groupCustomerId, nextBillingDate;
let paymentCancelSubscriptionSpy, paypalBillingAgreementCancelStub, paypalBillingAgreementGetStub;
beforeEach(async () => {
customerId = 'customer-id';
groupCustomerId = 'groupCustomerId-test';
user = new User();
user.profile.name = 'sender';
user.purchased.plan.customerId = customerId;
user.purchased.plan.planId = subKey;
user.purchased.plan.lastBillingDate = new Date();
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
group.purchased.plan.customerId = groupCustomerId;
group.purchased.plan.planId = subKey;
group.purchased.plan.lastBillingDate = new Date();
await group.save();
nextBillingDate = new Date();
paypalBillingAgreementCancelStub = sinon.stub(paypalPayments, 'paypalBillingAgreementCancel').returnsPromise().resolves({});
paypalBillingAgreementGetStub = sinon.stub(paypalPayments, 'paypalBillingAgreementGet')
.returnsPromise().resolves({
agreement_details: {
next_billing_date: nextBillingDate,
cycles_completed: 1,
},
});
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
});
afterEach(function () {
paypalPayments.paypalBillingAgreementGet.restore();
paypalPayments.paypalBillingAgreementCancel.restore();
payments.cancelSubscription.restore();
});
it('should throw an error if we are missing a subscription', async () => {
user.purchased.plan.customerId = undefined;
await expect(paypalPayments.subscribeCancel({user}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('missingSubscription'),
});
});
it('should throw an error if group is not found', async () => {
await expect(paypalPayments.subscribeCancel({user, groupId: 'fake-id'}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 404,
name: 'NotFound',
message: i18n.t('groupNotFound'),
});
});
it('should throw an error if user is not group leader', async () => {
let nonLeader = await createNonLeaderGroupMember(group);
await expect(paypalPayments.subscribeCancel({user: nonLeader, groupId: group._id}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
});
});
it('should cancel a user subscription', async () => {
await paypalPayments.subscribeCancel({user});
expect(paypalBillingAgreementGetStub).to.be.calledOnce;
expect(paypalBillingAgreementGetStub).to.be.calledWith(customerId);
expect(paypalBillingAgreementCancelStub).to.be.calledOnce;
expect(paypalBillingAgreementCancelStub).to.be.calledWith(customerId, { note: i18n.t('cancelingSubscription') });
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
user,
groupId,
paymentMethod: 'Paypal',
nextBill: nextBillingDate,
cancellationReason: undefined,
});
});
it('should cancel a group subscription', async () => {
await paypalPayments.subscribeCancel({user, groupId: group._id});
expect(paypalBillingAgreementGetStub).to.be.calledOnce;
expect(paypalBillingAgreementGetStub).to.be.calledWith(groupCustomerId);
expect(paypalBillingAgreementCancelStub).to.be.calledOnce;
expect(paypalBillingAgreementCancelStub).to.be.calledWith(groupCustomerId, { note: i18n.t('cancelingSubscription') });
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
user,
groupId: group._id,
paymentMethod: 'Paypal',
nextBill: nextBillingDate,
cancellationReason: undefined,
});
});
});

View File

@@ -0,0 +1,77 @@
/* eslint-disable camelcase */
import payments from '../../../../../../../website/server/libs/payments';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
import common from '../../../../../../../website/common';
describe('subscribeSuccess', () => {
const subKey = 'basic_3mo';
let user, group, block, groupId, token, headers, customerId;
let paypalBillingAgreementExecuteStub, paymentsCreateSubscritionStub;
beforeEach(async () => {
user = new User();
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
token = 'test-token';
headers = {};
block = common.content.subscriptionBlocks[subKey];
customerId = 'test-customerId';
paypalBillingAgreementExecuteStub = sinon.stub(paypalPayments, 'paypalBillingAgreementExecute')
.returnsPromise({}).resolves({
id: customerId,
});
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({});
});
afterEach(() => {
paypalPayments.paypalBillingAgreementExecute.restore();
payments.createSubscription.restore();
});
it('creates a user subscription', async () => {
await paypalPayments.subscribeSuccess({user, block, groupId, token, headers});
expect(paypalBillingAgreementExecuteStub).to.be.calledOnce;
expect(paypalBillingAgreementExecuteStub).to.be.calledWith(token, {});
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
expect(paymentsCreateSubscritionStub).to.be.calledWith({
user,
groupId,
customerId,
paymentMethod: 'Paypal',
sub: block,
headers,
});
});
it('create a group subscription', async () => {
groupId = group._id;
await paypalPayments.subscribeSuccess({user, block, groupId, token, headers});
expect(paypalBillingAgreementExecuteStub).to.be.calledOnce;
expect(paypalBillingAgreementExecuteStub).to.be.calledWith(token, {});
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
expect(paymentsCreateSubscritionStub).to.be.calledWith({
user,
groupId,
customerId,
paymentMethod: 'Paypal',
sub: block,
headers,
});
});
});

View File

@@ -0,0 +1,112 @@
/* eslint-disable camelcase */
import moment from 'moment';
import cc from 'coupon-code';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
import { model as Coupon } from '../../../../../../../website/server/models/coupon';
import common from '../../../../../../../website/common';
const i18n = common.i18n;
describe('subscribe', () => {
const subKey = 'basic_3mo';
let coupon, sub, approvalHerf;
let paypalBillingAgreementCreateStub;
beforeEach(() => {
approvalHerf = 'approvalHerf-test';
sub = Object.assign({}, common.content.subscriptionBlocks[subKey]);
paypalBillingAgreementCreateStub = sinon.stub(paypalPayments, 'paypalBillingAgreementCreate')
.returnsPromise().resolves({
links: [{ rel: 'approval_url', href: approvalHerf }],
});
});
afterEach(() => {
paypalPayments.paypalBillingAgreementCreate.restore();
});
it('should throw an error when coupon code is missing', async () => {
sub.discount = 40;
await expect(paypalPayments.subscribe({sub, coupon}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
message: i18n.t('couponCodeRequired'),
});
});
it('should throw an error when coupon code is invalid', async () => {
sub.discount = 40;
sub.key = 'google_6mo';
coupon = 'example-coupon';
let couponModel = new Coupon();
couponModel.event = 'google_6mo';
await couponModel.save();
sinon.stub(cc, 'validate').returns('invalid');
await expect(paypalPayments.subscribe({sub, coupon}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('invalidCoupon'),
});
cc.validate.restore();
});
it('subscribes with paypal with a coupon', async () => {
sub.discount = 40;
sub.key = 'google_6mo';
coupon = 'example-coupon';
let couponModel = new Coupon();
couponModel.event = 'google_6mo';
let updatedCouponModel = await couponModel.save();
sinon.stub(cc, 'validate').returns(updatedCouponModel._id);
let link = await paypalPayments.subscribe({sub, coupon});
expect(link).to.eql(approvalHerf);
expect(paypalBillingAgreementCreateStub).to.be.calledOnce;
let billingPlanTitle = `Habitica Subscription ($${sub.price} every ${sub.months} months, recurring)`;
expect(paypalBillingAgreementCreateStub).to.be.calledWith({
name: billingPlanTitle,
description: billingPlanTitle,
start_date: moment().add({ minutes: 5 }).format(),
plan: {
id: sub.paypalKey,
},
payer: {
payment_method: 'Paypal',
},
});
cc.validate.restore();
});
it('creates a link for a subscription', async () => {
delete sub.discount;
let link = await paypalPayments.subscribe({sub, coupon});
expect(link).to.eql(approvalHerf);
expect(paypalBillingAgreementCreateStub).to.be.calledOnce;
let billingPlanTitle = `Habitica Subscription ($${sub.price} every ${sub.months} months, recurring)`;
expect(paypalBillingAgreementCreateStub).to.be.calledWith({
name: billingPlanTitle,
description: billingPlanTitle,
start_date: moment().add({ minutes: 5 }).format(),
plan: {
id: sub.paypalKey,
},
payer: {
payment_method: 'Paypal',
},
});
});
});

View File

@@ -0,0 +1,143 @@
import stripeModule from 'stripe';
import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
import payments from '../../../../../../../website/server/libs/payments';
import common from '../../../../../../../website/common';
const i18n = common.i18n;
describe('cancel subscription', () => {
const subKey = 'basic_3mo';
const stripe = stripeModule('test');
let user, groupId, group;
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
user.purchased.plan.customerId = 'customer-id';
user.purchased.plan.planId = subKey;
user.purchased.plan.lastBillingDate = new Date();
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
group.purchased.plan.customerId = 'customer-id';
group.purchased.plan.planId = subKey;
await group.save();
groupId = group._id;
});
it('throws an error if there is no customer id', async () => {
user.purchased.plan.customerId = undefined;
await expect(stripePayments.cancelSubscription({
user,
groupId: undefined,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('missingSubscription'),
});
});
it('throws an error if the group is not found', async () => {
await expect(stripePayments.cancelSubscription({
user,
groupId: 'fake-group',
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 404,
name: 'NotFound',
message: i18n.t('groupNotFound'),
});
});
it('throws an error if user is not the group leader', async () => {
let nonLeader = new User();
nonLeader.guilds.push(groupId);
await nonLeader.save();
await expect(stripePayments.cancelSubscription({
user: nonLeader,
groupId,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
});
});
describe('success', () => {
let stripeDeleteCustomerStub, paymentsCancelSubStub, stripeRetrieveStub, subscriptionId, currentPeriodEndTimeStamp;
beforeEach(() => {
subscriptionId = 'subId';
stripeDeleteCustomerStub = sinon.stub(stripe.customers, 'del').returnsPromise().resolves({});
paymentsCancelSubStub = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
currentPeriodEndTimeStamp = (new Date()).getTime();
stripeRetrieveStub = sinon.stub(stripe.customers, 'retrieve')
.returnsPromise().resolves({
subscriptions: {
data: [{id: subscriptionId, current_period_end: currentPeriodEndTimeStamp}], // eslint-disable-line camelcase
},
});
});
afterEach(() => {
stripe.customers.del.restore();
stripe.customers.retrieve.restore();
payments.cancelSubscription.restore();
});
it('cancels a user subscription', async () => {
await stripePayments.cancelSubscription({
user,
groupId: undefined,
}, stripe);
expect(stripeDeleteCustomerStub).to.be.calledOnce;
expect(stripeDeleteCustomerStub).to.be.calledWith(user.purchased.plan.customerId);
expect(stripeRetrieveStub).to.be.calledOnce;
expect(stripeRetrieveStub).to.be.calledWith(user.purchased.plan.customerId);
expect(paymentsCancelSubStub).to.be.calledOnce;
expect(paymentsCancelSubStub).to.be.calledWith({
user,
groupId: undefined,
nextBill: currentPeriodEndTimeStamp * 1000, // timestamp in seconds
paymentMethod: 'Stripe',
cancellationReason: undefined,
});
});
it('cancels a group subscription', async () => {
await stripePayments.cancelSubscription({
user,
groupId,
}, stripe);
expect(stripeDeleteCustomerStub).to.be.calledOnce;
expect(stripeDeleteCustomerStub).to.be.calledWith(group.purchased.plan.customerId);
expect(stripeRetrieveStub).to.be.calledOnce;
expect(stripeRetrieveStub).to.be.calledWith(user.purchased.plan.customerId);
expect(paymentsCancelSubStub).to.be.calledOnce;
expect(paymentsCancelSubStub).to.be.calledWith({
user,
groupId,
nextBill: currentPeriodEndTimeStamp * 1000, // timestamp in seconds
paymentMethod: 'Stripe',
cancellationReason: undefined,
});
});
});
});

View File

@@ -0,0 +1,307 @@
import stripeModule from 'stripe';
import cc from 'coupon-code';
import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
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';
const i18n = common.i18n;
describe('checkout with subscription', () => {
const subKey = 'basic_3mo';
const stripe = stripeModule('test');
let user, group, data, gift, sub, groupId, email, headers, coupon, customerIdResponse, subscriptionId, token;
let spy;
let stripeCreateCustomerSpy;
let stripePaymentsCreateSubSpy;
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
user.purchased.plan.customerId = 'customer-id';
user.purchased.plan.planId = subKey;
user.purchased.plan.lastBillingDate = new Date();
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
group.purchased.plan.customerId = 'customer-id';
group.purchased.plan.planId = subKey;
await group.save();
sub = {
key: 'basic_3mo',
};
data = {
user,
sub,
customerId: 'customer-id',
paymentMethod: 'Payment Method',
};
email = 'example@example.com';
customerIdResponse = 'test-id';
subscriptionId = 'test-sub-id';
token = 'test-token';
spy = sinon.stub(stripe.subscriptions, 'update');
spy.returnsPromise().resolves;
stripeCreateCustomerSpy = sinon.stub(stripe.customers, 'create');
let stripCustomerResponse = {
id: customerIdResponse,
subscriptions: {
data: [{id: subscriptionId}],
},
};
stripeCreateCustomerSpy.returnsPromise().resolves(stripCustomerResponse);
stripePaymentsCreateSubSpy = sinon.stub(payments, 'createSubscription');
stripePaymentsCreateSubSpy.returnsPromise().resolves({});
data.groupId = group._id;
data.sub.quantity = 3;
});
afterEach(function () {
stripe.subscriptions.update.restore();
stripe.customers.create.restore();
payments.createSubscription.restore();
});
it('should throw an error if we are missing a token', async () => {
await expect(stripePayments.checkout({
user,
gift,
sub,
groupId,
email,
headers,
coupon,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
message: 'Missing req.body.id',
});
});
it('should throw an error when coupon code is missing', async () => {
sub.discount = 40;
await expect(stripePayments.checkout({
token,
user,
gift,
sub,
groupId,
email,
headers,
coupon,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
message: i18n.t('couponCodeRequired'),
});
});
it('should throw an error when coupon code is invalid', async () => {
sub.discount = 40;
sub.key = 'google_6mo';
coupon = 'example-coupon';
let couponModel = new Coupon();
couponModel.event = 'google_6mo';
await couponModel.save();
sinon.stub(cc, 'validate').returns('invalid');
await expect(stripePayments.checkout({
token,
user,
gift,
sub,
groupId,
email,
headers,
coupon,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
message: i18n.t('invalidCoupon'),
});
cc.validate.restore();
});
it('subscribes with stripe with a coupon', async () => {
sub.discount = 40;
sub.key = 'google_6mo';
coupon = 'example-coupon';
let couponModel = new Coupon();
couponModel.event = 'google_6mo';
let updatedCouponModel = await couponModel.save();
sinon.stub(cc, 'validate').returns(updatedCouponModel._id);
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,
});
expect(stripePaymentsCreateSubSpy).to.be.calledOnce;
expect(stripePaymentsCreateSubSpy).to.be.calledWith({
user,
customerId: customerIdResponse,
paymentMethod: 'Stripe',
sub,
headers,
groupId: undefined,
subscriptionId: undefined,
});
cc.validate.restore();
});
it('subscribes a user', async () => {
sub = data.sub;
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,
});
expect(stripePaymentsCreateSubSpy).to.be.calledOnce;
expect(stripePaymentsCreateSubSpy).to.be.calledWith({
user,
customerId: customerIdResponse,
paymentMethod: 'Stripe',
sub,
headers,
groupId: undefined,
subscriptionId: undefined,
});
});
it('subscribes a group', async () => {
token = 'test-token';
sub = data.sub;
groupId = group._id;
email = 'test@test.com';
headers = {};
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: 3,
});
expect(stripePaymentsCreateSubSpy).to.be.calledOnce;
expect(stripePaymentsCreateSubSpy).to.be.calledWith({
user,
customerId: customerIdResponse,
paymentMethod: 'Stripe',
sub,
headers,
groupId,
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,
});
});
});

View File

@@ -0,0 +1,193 @@
import stripeModule from 'stripe';
import { model as User } from '../../../../../../../website/server/models/user';
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
import payments from '../../../../../../../website/server/libs/payments';
import common from '../../../../../../../website/common';
const i18n = common.i18n;
describe('checkout', () => {
const subKey = 'basic_3mo';
const stripe = stripeModule('test');
let stripeChargeStub, paymentBuyGemsStub, paymentCreateSubscritionStub;
let user, gift, groupId, email, headers, coupon, customerIdResponse, token;
beforeEach(() => {
user = new User();
user.profile.name = 'sender';
user.purchased.plan.customerId = 'customer-id';
user.purchased.plan.planId = subKey;
user.purchased.plan.lastBillingDate = new Date();
token = 'test-token';
customerIdResponse = 'example-customerIdResponse';
let stripCustomerResponse = {
id: customerIdResponse,
};
stripeChargeStub = sinon.stub(stripe.charges, 'create').returnsPromise().resolves(stripCustomerResponse);
paymentBuyGemsStub = sinon.stub(payments, 'buyGems').returnsPromise().resolves({});
paymentCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({});
});
afterEach(() => {
stripe.charges.create.restore();
payments.buyGems.restore();
payments.createSubscription.restore();
});
it('should error if gem amount is too low', async () => {
let receivingUser = new User();
receivingUser.save();
gift = {
type: 'gems',
gems: {
amount: 0,
uuid: receivingUser._id,
},
};
await expect(stripePayments.checkout({
token,
user,
gift,
groupId,
email,
headers,
coupon,
}, stripe))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
message: 'Amount must be at least 1.',
name: 'BadRequest',
});
});
it('should error if user cannot get gems', async () => {
gift = undefined;
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
await expect(stripePayments.checkout({
token,
user,
gift,
groupId,
email,
headers,
coupon,
}, stripe)).to.eventually.be.rejected.and.to.eql({
httpCode: 401,
message: i18n.t('groupPolicyCannotGetGems'),
name: 'NotAuthorized',
});
});
it('should purchase gems', async () => {
gift = undefined;
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
await stripePayments.checkout({
token,
user,
gift,
groupId,
email,
headers,
coupon,
}, stripe);
expect(stripeChargeStub).to.be.calledOnce;
expect(stripeChargeStub).to.be.calledWith({
amount: 500,
currency: 'usd',
card: token,
});
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,
customerId: customerIdResponse,
paymentMethod: 'Stripe',
gift,
});
expect(user.canGetGems).to.be.calledOnce;
user.canGetGems.restore();
});
it('should gift gems', async () => {
let receivingUser = new User();
await receivingUser.save();
gift = {
type: 'gems',
uuid: receivingUser._id,
gems: {
amount: 16,
},
};
await stripePayments.checkout({
token,
user,
gift,
groupId,
email,
headers,
coupon,
}, stripe);
expect(stripeChargeStub).to.be.calledOnce;
expect(stripeChargeStub).to.be.calledWith({
amount: '400',
currency: 'usd',
card: token,
});
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,
customerId: customerIdResponse,
paymentMethod: 'Gift',
gift,
});
});
it('should gift a subscription', async () => {
let receivingUser = new User();
receivingUser.save();
gift = {
type: 'subscription',
subscription: {
key: subKey,
uuid: receivingUser._id,
},
};
await stripePayments.checkout({
token,
user,
gift,
groupId,
email,
headers,
coupon,
}, stripe);
gift.member = receivingUser;
expect(stripeChargeStub).to.be.calledOnce;
expect(stripeChargeStub).to.be.calledWith({
amount: '1500',
currency: 'usd',
card: token,
});
expect(paymentCreateSubscritionStub).to.be.calledOnce;
expect(paymentCreateSubscritionStub).to.be.calledWith({
user,
customerId: customerIdResponse,
paymentMethod: 'Gift',
gift,
});
});
});

View File

@@ -0,0 +1,147 @@
import stripeModule from 'stripe';
import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
import common from '../../../../../../../website/common';
const i18n = common.i18n;
describe('edit subscription', () => {
const subKey = 'basic_3mo';
const stripe = stripeModule('test');
let user, groupId, group, token;
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
user.purchased.plan.customerId = 'customer-id';
user.purchased.plan.planId = subKey;
user.purchased.plan.lastBillingDate = new Date();
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
group.purchased.plan.customerId = 'customer-id';
group.purchased.plan.planId = subKey;
await group.save();
groupId = group._id;
token = 'test-token';
});
it('throws an error if there is no customer id', async () => {
user.purchased.plan.customerId = undefined;
await expect(stripePayments.editSubscription({
user,
groupId: undefined,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('missingSubscription'),
});
});
it('throws an error if a token is not provided', async () => {
await expect(stripePayments.editSubscription({
user,
groupId: undefined,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
message: 'Missing req.body.id',
});
});
it('throws an error if the group is not found', async () => {
await expect(stripePayments.editSubscription({
token,
user,
groupId: 'fake-group',
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 404,
name: 'NotFound',
message: i18n.t('groupNotFound'),
});
});
it('throws an error if user is not the group leader', async () => {
let nonLeader = new User();
nonLeader.guilds.push(groupId);
await nonLeader.save();
await expect(stripePayments.editSubscription({
token,
user: nonLeader,
groupId,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
});
});
describe('success', () => {
let stripeListSubscriptionStub, stripeUpdateSubscriptionStub, subscriptionId;
beforeEach(() => {
subscriptionId = 'subId';
stripeListSubscriptionStub = sinon.stub(stripe.customers, 'listSubscriptions')
.returnsPromise().resolves({
data: [{id: subscriptionId}],
});
stripeUpdateSubscriptionStub = sinon.stub(stripe.customers, 'updateSubscription').returnsPromise().resolves({});
});
afterEach(() => {
stripe.customers.listSubscriptions.restore();
stripe.customers.updateSubscription.restore();
});
it('edits a user subscription', async () => {
await stripePayments.editSubscription({
token,
user,
groupId: undefined,
}, stripe);
expect(stripeListSubscriptionStub).to.be.calledOnce;
expect(stripeListSubscriptionStub).to.be.calledWith(user.purchased.plan.customerId);
expect(stripeUpdateSubscriptionStub).to.be.calledOnce;
expect(stripeUpdateSubscriptionStub).to.be.calledWith(
user.purchased.plan.customerId,
subscriptionId,
{ card: token }
);
});
it('edits a group subscription', async () => {
await stripePayments.editSubscription({
token,
user,
groupId,
}, stripe);
expect(stripeListSubscriptionStub).to.be.calledOnce;
expect(stripeListSubscriptionStub).to.be.calledWith(group.purchased.plan.customerId);
expect(stripeUpdateSubscriptionStub).to.be.calledOnce;
expect(stripeUpdateSubscriptionStub).to.be.calledWith(
group.purchased.plan.customerId,
subscriptionId,
{ card: token }
);
});
});
});

View File

@@ -0,0 +1,260 @@
import stripeModule from 'stripe';
import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
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;
describe('Stripe - Webhooks', () => {
const stripe = stripeModule('test');
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;
const calledWith = logger.error.getCall(0).args;
expect(calledWith[0].message).to.equal(error.message);
expect(calledWith[1].event).to.equal(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();
});
});
});
});

View File

@@ -0,0 +1,66 @@
import stripeModule from 'stripe';
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 stripePayments from '../../../../../../../website/server/libs/stripePayments';
import payments from '../../../../../../../website/server/libs/payments';
describe('Stripe - Upgrade Group Plan', () => {
const stripe = stripeModule('test');
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 () {
stripe.subscriptions.update.restore();
});
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);
});
});

View File

@@ -1,561 +0,0 @@
/* eslint-disable camelcase */
import nconf from 'nconf';
import moment from 'moment';
import cc from 'coupon-code';
import payments from '../../../../../website/server/libs/payments';
import paypalPayments from '../../../../../website/server/libs/paypalPayments';
import {
generateGroup,
} from '../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../website/server/models/user';
import { model as Coupon } from '../../../../../website/server/models/coupon';
import common from '../../../../../website/common';
const BASE_URL = nconf.get('BASE_URL');
const i18n = common.i18n;
describe('Paypal Payments', () => {
let subKey = 'basic_3mo';
describe('checkout', () => {
let paypalPaymentCreateStub;
let approvalHerf;
function getPaypalCreateOptions (description, amount) {
return {
intent: 'sale',
payer: { payment_method: 'Paypal' },
redirect_urls: {
return_url: `${BASE_URL}/paypal/checkout/success`,
cancel_url: `${BASE_URL}`,
},
transactions: [{
item_list: {
items: [{
name: description,
price: amount,
currency: 'USD',
quantity: 1,
}],
},
amount: {
currency: 'USD',
total: amount,
},
description,
}],
};
}
beforeEach(() => {
approvalHerf = 'approval_href';
paypalPaymentCreateStub = sinon.stub(paypalPayments, 'paypalPaymentCreate')
.returnsPromise().resolves({
links: [{ rel: 'approval_url', href: approvalHerf }],
});
});
afterEach(() => {
paypalPayments.paypalPaymentCreate.restore();
});
it('creates a link for gem purchases', async () => {
let link = await paypalPayments.checkout({user: new User()});
expect(paypalPaymentCreateStub).to.be.calledOnce;
expect(paypalPaymentCreateStub).to.be.calledWith(getPaypalCreateOptions('Habitica Gems', 5.00));
expect(link).to.eql(approvalHerf);
});
it('should error if gem amount is too low', async () => {
let receivingUser = new User();
receivingUser.save();
let gift = {
type: 'gems',
gems: {
amount: 0,
uuid: receivingUser._id,
},
};
await expect(paypalPayments.checkout({gift}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
message: 'Amount must be at least 1.',
name: 'BadRequest',
});
});
it('should error if the user cannot get gems', async () => {
let user = new User();
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
await expect(paypalPayments.checkout({user})).to.eventually.be.rejected.and.to.eql({
httpCode: 401,
message: i18n.t('groupPolicyCannotGetGems'),
name: 'NotAuthorized',
});
});
it('creates a link for gifting gems', async () => {
let receivingUser = new User();
await receivingUser.save();
let gift = {
type: 'gems',
uuid: receivingUser._id,
gems: {
amount: 16,
},
};
let link = await paypalPayments.checkout({gift});
expect(paypalPaymentCreateStub).to.be.calledOnce;
expect(paypalPaymentCreateStub).to.be.calledWith(getPaypalCreateOptions('Habitica Gems (Gift)', '4.00'));
expect(link).to.eql(approvalHerf);
});
it('creates a link for gifting a subscription', async () => {
let receivingUser = new User();
receivingUser.save();
let gift = {
type: 'subscription',
subscription: {
key: subKey,
uuid: receivingUser._id,
},
};
let link = await paypalPayments.checkout({gift});
expect(paypalPaymentCreateStub).to.be.calledOnce;
expect(paypalPaymentCreateStub).to.be.calledWith(getPaypalCreateOptions('mo. Habitica Subscription (Gift)', '15.00'));
expect(link).to.eql(approvalHerf);
});
});
describe('checkout success', () => {
let user, gift, customerId, paymentId;
let paypalPaymentExecuteStub, paymentBuyGemsStub, paymentsCreateSubscritionStub;
beforeEach(() => {
user = new User();
customerId = 'customerId-test';
paymentId = 'paymentId-test';
paypalPaymentExecuteStub = sinon.stub(paypalPayments, 'paypalPaymentExecute').returnsPromise().resolves({});
paymentBuyGemsStub = sinon.stub(payments, 'buyGems').returnsPromise().resolves({});
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({});
});
afterEach(() => {
paypalPayments.paypalPaymentExecute.restore();
payments.buyGems.restore();
payments.createSubscription.restore();
});
it('purchases gems', async () => {
await paypalPayments.checkoutSuccess({user, gift, paymentId, customerId});
expect(paypalPaymentExecuteStub).to.be.calledOnce;
expect(paypalPaymentExecuteStub).to.be.calledWith(paymentId, { payer_id: customerId });
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,
customerId,
paymentMethod: 'Paypal',
});
});
it('gifts gems', async () => {
let receivingUser = new User();
await receivingUser.save();
gift = {
type: 'gems',
gems: {
amount: 16,
uuid: receivingUser._id,
},
};
await paypalPayments.checkoutSuccess({user, gift, paymentId, customerId});
expect(paypalPaymentExecuteStub).to.be.calledOnce;
expect(paypalPaymentExecuteStub).to.be.calledWith(paymentId, { payer_id: customerId });
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,
customerId,
paymentMethod: 'PayPal (Gift)',
gift,
});
});
it('gifts subscription', async () => {
let receivingUser = new User();
await receivingUser.save();
gift = {
type: 'subscription',
subscription: {
key: subKey,
uuid: receivingUser._id,
},
};
await paypalPayments.checkoutSuccess({user, gift, paymentId, customerId});
expect(paypalPaymentExecuteStub).to.be.calledOnce;
expect(paypalPaymentExecuteStub).to.be.calledWith(paymentId, { payer_id: customerId });
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
expect(paymentsCreateSubscritionStub).to.be.calledWith({
user,
customerId,
paymentMethod: 'PayPal (Gift)',
gift,
});
});
});
describe('subscribe', () => {
let coupon, sub, approvalHerf;
let paypalBillingAgreementCreateStub;
beforeEach(() => {
approvalHerf = 'approvalHerf-test';
sub = common.content.subscriptionBlocks[subKey];
paypalBillingAgreementCreateStub = sinon.stub(paypalPayments, 'paypalBillingAgreementCreate')
.returnsPromise().resolves({
links: [{ rel: 'approval_url', href: approvalHerf }],
});
});
afterEach(() => {
paypalPayments.paypalBillingAgreementCreate.restore();
});
it('should throw an error when coupon code is missing', async () => {
sub.discount = 40;
await expect(paypalPayments.subscribe({sub, coupon}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
message: i18n.t('couponCodeRequired'),
});
});
it('should throw an error when coupon code is invalid', async () => {
sub.discount = 40;
sub.key = 'google_6mo';
coupon = 'example-coupon';
let couponModel = new Coupon();
couponModel.event = 'google_6mo';
await couponModel.save();
sinon.stub(cc, 'validate').returns('invalid');
await expect(paypalPayments.subscribe({sub, coupon}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('invalidCoupon'),
});
cc.validate.restore();
});
it('subscribes with amazon with a coupon', async () => {
sub.discount = 40;
sub.key = 'google_6mo';
coupon = 'example-coupon';
let couponModel = new Coupon();
couponModel.event = 'google_6mo';
let updatedCouponModel = await couponModel.save();
sinon.stub(cc, 'validate').returns(updatedCouponModel._id);
let link = await paypalPayments.subscribe({sub, coupon});
expect(link).to.eql(approvalHerf);
expect(paypalBillingAgreementCreateStub).to.be.calledOnce;
let billingPlanTitle = `Habitica Subscription ($${sub.price} every ${sub.months} months, recurring)`;
expect(paypalBillingAgreementCreateStub).to.be.calledWith({
name: billingPlanTitle,
description: billingPlanTitle,
start_date: moment().add({ minutes: 5 }).format(),
plan: {
id: sub.paypalKey,
},
payer: {
payment_method: 'Paypal',
},
});
cc.validate.restore();
});
it('creates a link for a subscription', async () => {
delete sub.discount;
let link = await paypalPayments.subscribe({sub, coupon});
expect(link).to.eql(approvalHerf);
expect(paypalBillingAgreementCreateStub).to.be.calledOnce;
let billingPlanTitle = `Habitica Subscription ($${sub.price} every ${sub.months} months, recurring)`;
expect(paypalBillingAgreementCreateStub).to.be.calledWith({
name: billingPlanTitle,
description: billingPlanTitle,
start_date: moment().add({ minutes: 5 }).format(),
plan: {
id: sub.paypalKey,
},
payer: {
payment_method: 'Paypal',
},
});
});
});
describe('subscribeSuccess', () => {
let user, group, block, groupId, token, headers, customerId;
let paypalBillingAgreementExecuteStub, paymentsCreateSubscritionStub;
beforeEach(async () => {
user = new User();
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
token = 'test-token';
headers = {};
block = common.content.subscriptionBlocks[subKey];
customerId = 'test-customerId';
paypalBillingAgreementExecuteStub = sinon.stub(paypalPayments, 'paypalBillingAgreementExecute')
.returnsPromise({}).resolves({
id: customerId,
});
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({});
});
afterEach(() => {
paypalPayments.paypalBillingAgreementExecute.restore();
payments.createSubscription.restore();
});
it('creates a user subscription', async () => {
await paypalPayments.subscribeSuccess({user, block, groupId, token, headers});
expect(paypalBillingAgreementExecuteStub).to.be.calledOnce;
expect(paypalBillingAgreementExecuteStub).to.be.calledWith(token, {});
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
expect(paymentsCreateSubscritionStub).to.be.calledWith({
user,
groupId,
customerId,
paymentMethod: 'Paypal',
sub: block,
headers,
});
});
it('create a group subscription', async () => {
groupId = group._id;
await paypalPayments.subscribeSuccess({user, block, groupId, token, headers});
expect(paypalBillingAgreementExecuteStub).to.be.calledOnce;
expect(paypalBillingAgreementExecuteStub).to.be.calledWith(token, {});
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
expect(paymentsCreateSubscritionStub).to.be.calledWith({
user,
groupId,
customerId,
paymentMethod: 'Paypal',
sub: block,
headers,
});
});
});
describe('subscribeCancel', () => {
let user, group, groupId, customerId, groupCustomerId, nextBillingDate;
let paymentCancelSubscriptionSpy, paypalBillingAgreementCancelStub, paypalBillingAgreementGetStub;
beforeEach(async () => {
customerId = 'customer-id';
groupCustomerId = 'groupCustomerId-test';
user = new User();
user.profile.name = 'sender';
user.purchased.plan.customerId = customerId;
user.purchased.plan.planId = subKey;
user.purchased.plan.lastBillingDate = new Date();
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
group.purchased.plan.customerId = groupCustomerId;
group.purchased.plan.planId = subKey;
group.purchased.plan.lastBillingDate = new Date();
await group.save();
nextBillingDate = new Date();
paypalBillingAgreementCancelStub = sinon.stub(paypalPayments, 'paypalBillingAgreementCancel').returnsPromise().resolves({});
paypalBillingAgreementGetStub = sinon.stub(paypalPayments, 'paypalBillingAgreementGet')
.returnsPromise().resolves({
agreement_details: {
next_billing_date: nextBillingDate,
cycles_completed: 1,
},
});
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
});
afterEach(function () {
paypalPayments.paypalBillingAgreementGet.restore();
paypalPayments.paypalBillingAgreementCancel.restore();
payments.cancelSubscription.restore();
});
it('should throw an error if we are missing a subscription', async () => {
user.purchased.plan.customerId = undefined;
await expect(paypalPayments.subscribeCancel({user}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('missingSubscription'),
});
});
it('should throw an error if group is not found', async () => {
await expect(paypalPayments.subscribeCancel({user, groupId: 'fake-id'}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 404,
name: 'NotFound',
message: i18n.t('groupNotFound'),
});
});
it('should throw an error if user is not group leader', async () => {
let nonLeader = new User();
nonLeader.guilds.push(group._id);
await nonLeader.save();
await expect(paypalPayments.subscribeCancel({user: nonLeader, groupId: group._id}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
});
});
it('should cancel a user subscription', async () => {
await paypalPayments.subscribeCancel({user});
expect(paypalBillingAgreementGetStub).to.be.calledOnce;
expect(paypalBillingAgreementGetStub).to.be.calledWith(customerId);
expect(paypalBillingAgreementCancelStub).to.be.calledOnce;
expect(paypalBillingAgreementCancelStub).to.be.calledWith(customerId, { note: i18n.t('cancelingSubscription') });
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
user,
groupId,
paymentMethod: 'Paypal',
nextBill: nextBillingDate,
cancellationReason: undefined,
});
});
it('should cancel a group subscription', async () => {
await paypalPayments.subscribeCancel({user, groupId: group._id});
expect(paypalBillingAgreementGetStub).to.be.calledOnce;
expect(paypalBillingAgreementGetStub).to.be.calledWith(groupCustomerId);
expect(paypalBillingAgreementCancelStub).to.be.calledOnce;
expect(paypalBillingAgreementCancelStub).to.be.calledWith(groupCustomerId, { note: i18n.t('cancelingSubscription') });
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
user,
groupId: group._id,
paymentMethod: 'Paypal',
nextBill: nextBillingDate,
cancellationReason: undefined,
});
});
});
describe('ipn', () => {
let user, group, txn_type, userPaymentId, groupPaymentId;
let ipnVerifyAsyncStub, paymentCancelSubscriptionSpy;
beforeEach(async () => {
txn_type = 'recurring_payment_profile_cancel';
userPaymentId = 'userPaymentId-test';
groupPaymentId = 'groupPaymentId-test';
user = new User();
user.profile.name = 'sender';
user.purchased.plan.customerId = userPaymentId;
user.purchased.plan.planId = subKey;
user.purchased.plan.lastBillingDate = new Date();
await user.save();
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
group.purchased.plan.customerId = groupPaymentId;
group.purchased.plan.planId = subKey;
group.purchased.plan.lastBillingDate = new Date();
await group.save();
ipnVerifyAsyncStub = sinon.stub(paypalPayments, 'ipnVerifyAsync').returnsPromise().resolves({});
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
});
afterEach(function () {
paypalPayments.ipnVerifyAsync.restore();
payments.cancelSubscription.restore();
});
it('should cancel a user subscription', async () => {
await paypalPayments.ipn({txn_type, recurring_payment_id: userPaymentId});
expect(ipnVerifyAsyncStub).to.be.calledOnce;
expect(ipnVerifyAsyncStub).to.be.calledWith({txn_type, recurring_payment_id: userPaymentId});
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy.args[0][0].user._id).to.eql(user._id);
expect(paymentCancelSubscriptionSpy.args[0][0].paymentMethod).to.eql('Paypal');
});
it('should cancel a group subscription', async () => {
await paypalPayments.ipn({txn_type, recurring_payment_id: groupPaymentId});
expect(ipnVerifyAsyncStub).to.be.calledOnce;
expect(ipnVerifyAsyncStub).to.be.calledWith({txn_type, recurring_payment_id: groupPaymentId});
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy).to.be.calledWith({ groupId: group._id, paymentMethod: 'Paypal' });
});
});
});

View File

@@ -8,7 +8,10 @@ describe('preenHistory', () => {
beforeEach(() => {
// Replace system clocks so we can get predictable results
clock = sinon.useFakeTimers(Number(moment('2013-10-20').zone(0).startOf('day').toDate()), 'Date');
clock = sinon.useFakeTimers({
now: Number(moment('2013-10-20').zone(0).startOf('day').toDate()),
toFake: ['Date'],
});
});
afterEach(() => {
return clock.restore();

View File

@@ -22,7 +22,7 @@ describe('pushNotifications', () => {
sandbox.stub(nconf, 'get').returns('true-key');
sandbox.stub(gcmLib.Sender.prototype, 'send', fcmSendSpy);
sandbox.stub(gcmLib.Sender.prototype, 'send').callsFake(fcmSendSpy);
sandbox.stub(pushNotify, 'apn').returns({
on: () => null,

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,7 @@ import {
import i18n from '../../../../../website/common/script/i18n';
import { ensureAdmin, ensureSudo } from '../../../../../website/server/middlewares/ensureAccessRight';
import { NotAuthorized } from '../../../../../website/server/libs/errors';
import apiMessages from '../../../../../website/server/libs/apiMessages';
describe('ensure access middlewares', () => {
let res, req, next;
@@ -23,7 +24,9 @@ describe('ensure access middlewares', () => {
ensureAdmin(req, res, next);
expect(next).to.be.calledWith(new NotAuthorized(i18n.t('noAdminAccess')));
const calledWith = next.getCall(0).args;
expect(calledWith[0].message).to.equal(i18n.t('noAdminAccess'));
expect(calledWith[0] instanceof NotAuthorized).to.equal(true);
});
it('passes when user is an admin', () => {
@@ -42,7 +45,9 @@ describe('ensure access middlewares', () => {
ensureSudo(req, res, next);
expect(next).to.be.calledWith(new NotAuthorized(i18n.t('noSudoAccess')));
const calledWith = next.getCall(0).args;
expect(calledWith[0].message).to.equal(apiMessages('noSudoAccess'));
expect(calledWith[0] instanceof NotAuthorized).to.equal(true);
});
it('passes when user is a sudo user', () => {

View File

@@ -22,7 +22,8 @@ describe('developmentMode middleware', () => {
ensureDevelpmentMode(req, res, next);
expect(next).to.be.calledWith(new NotFound());
const calledWith = next.getCall(0).args;
expect(calledWith[0] instanceof NotFound).to.equal(true);
});
it('passes when not in production', () => {

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