Compare commits

...

380 Commits

Author SHA1 Message Date
Sabe Jones
16c9e42ad8 4.51.2 2018-07-10 17:24:18 +00:00
Sabe Jones
0e1d00c95f chore(i18n): update locales 2018-07-10 17:23:40 +00:00
Sabe Jones
166f4683ca feat(content): enable Splashy Skins 2018-07-10 12:18:41 -05:00
Sabe Jones
75e5b20f93 4.51.1 2018-07-05 15:14:05 +00:00
Sabe Jones
f9db432794 chore(i18n): update locales 2018-07-05 15:12:59 +00:00
Sabe Jones
6cec7cbba2 Merge branch 'release' into develop 2018-07-03 17:39:27 +00:00
Sabe Jones
f76d097313 4.51.0 2018-07-03 17:38:44 +00:00
Sabe Jones
59af471438 chore(i18n): update locales 2018-07-03 17:38:00 +00:00
Sabe Jones
d0da303b7d chore(sprites): fix shop icon canvases, compile 2018-07-03 12:33:20 -05:00
Sabe Jones
596383e7a8 feat(content): Armoire and Backgrounds July 2018 2018-07-03 12:26:20 -05:00
Matteo Pagliazzi
accba7fc13 fix bug in task approval notification 2018-07-03 11:23:44 +02:00
Sabe Jones
dfc54f1600 Merge branch 'release' into develop 2018-07-02 19:58:23 +00:00
Sabe Jones
b76ab58e3e 4.50.6 2018-07-02 19:58:00 +00:00
Sabe Jones
6f093a94c4 chore(i18n): update locales 2018-07-02 19:57:39 +00:00
Sabe Jones
a9a9e7a4ab chore(sprites): compile 2018-07-02 14:51:59 -05:00
Sabe Jones
b2058ec23d chore(news): Bailey challenge announcements 2018-07-02 14:51:40 -05:00
Keith Holliday
2461f53cf5 4.50.5 2018-07-01 18:25:23 -05:00
Keith Holliday
73fc288f3b 4.50.4 2018-07-01 18:25:20 -05:00
Keith Holliday
ea78b6feb9 Changed docker file names (#10489) 2018-07-01 18:23:40 -05:00
Keith Holliday
7c141614ed Api login party invites (#10486)
* Fixed incorrect variable

* Fixed redirect params
2018-06-30 22:06:18 -05:00
Alys
c072935e80 document the dueDate parameter for API task functions - partial fix for #8087 2018-06-30 15:55:27 +10:00
Alys
54e49ca3b9 adjust README file for recent changes in wiki Blacksmith pages
Also replace non-ascii quotes with ascii ones (nicer for
command-line reading).
2018-06-30 15:05:05 +10:00
Sabe Jones
b16a245d61 Merge branch 'release' into develop 2018-06-29 19:06:20 +00:00
Sabe Jones
0a8109e496 4.50.3 2018-06-29 19:05:53 +00:00
Sabe Jones
cdfcc6419f chore(i18n): update locales 2018-06-29 19:03:36 +00:00
Matteo Pagliazzi
9c25c2452f fixes #10485 2018-06-29 20:29:14 +02:00
Matteo Pagliazzi
573f2e4732 fix preening when history entries are null 2018-06-29 20:03:45 +02:00
Keith Holliday
81ffcf9c1b Added login path for Spritely (#10484) 2018-06-29 11:35:22 -05:00
Sabe Jones
d8925a8811 Merge branch 'release' into develop 2018-06-29 00:22:18 +00:00
Sabe Jones
217c16988b 4.50.2 2018-06-29 00:21:56 +00:00
Sabe Jones
de7f953b67 chore(i18n): update locales 2018-06-29 00:20:37 +00:00
SabreCat
8ee2a02e73 chore(news): Bailey 2018-06-29 00:18:13 +00:00
negue
d9b573b430 AbstractGemItemOperation - BuyQuestWithGemOperation (#10476) 2018-06-28 12:37:21 +02:00
Keith Holliday
cbcf5a03e1 Ensured leader doesn't change with group plans (#10480) 2018-06-28 11:57:24 +02:00
Matteo Pagliazzi
487523f64b client: disable broken test 2018-06-28 11:04:35 +02:00
Matteo Pagliazzi
74cfc2cf52 add ability to specify pool size for mongodb (#10481) 2018-06-28 11:02:26 +02:00
Sabe Jones
2d489e870f Merge branch 'release' into develop 2018-06-27 18:34:06 +00:00
Sabe Jones
6659d4fa52 4.50.1 2018-06-27 18:33:43 +00:00
Sabe Jones
5471af74fa chore(i18n): update locales 2018-06-27 18:33:34 +00:00
Sabe Jones
6eb484605a fix(messages): clarify opt-in/out wording and leave label static (#10470) 2018-06-27 19:13:13 +02:00
Sabe Jones
8969b755a6 fix(analytics): remove spurious click tracking (#10469) 2018-06-27 19:12:51 +02:00
Sabe Jones
0062e5b1f1 fix(time-travelers): don't timeshift background without Hourglass (#10468) 2018-06-27 19:12:36 +02:00
Sabe Jones
50b98d8d92 fix(deletion): show feedback for social accounts (#10467) 2018-06-27 19:12:18 +02:00
Isabelle Lavandero
7ddf4b1f7b Remove "add multiple" tip once a task has been added (fixes #10440) (#10459)
* remove tip once a task has been added

* blur quickadd on enter but not on shift

* blur quickadd after tasks are created
2018-06-27 19:08:45 +02:00
Dexx Mandele
c91da86b89 Remember equipment drawer tab (#10458)
* Remember equipment drawer tab

* Split local setting value constants
2018-06-27 19:08:21 +02:00
Jerell Mendoza
d549fea4ed 10282: Added code for blocking party and guild invitations from block… (#10454)
* 10282: Added code for blocking party and guild invitations from blocked players, added tests

* 10282: fixed test label

* Update POST-groups_invite.test.js

removed `it.only` which was used for testing
2018-06-27 19:07:57 +02:00
Patricia Beier
a362914f93 added ctrl+enter to private messages #10413 (#10436)
* added ctrl+enter to private messages #10413

* Fixes #10413
2018-06-27 19:07:41 +02:00
Hayden Betts
61001d0e9a WIP Fix flicker when user mouses over the very leftmost edge of party member avatar (#10407)
* added 1px margin-right to .member-stats

* added unit test for flicker prevention style

* remove .only() from unit test

* rewrote margin test using computed style
2018-06-27 19:07:21 +02:00
Matteo Pagliazzi
fb95d001ab hotfix for bailey not scrolling (might reintroduce a double scrollbar), fixes #10461 2018-06-27 18:59:28 +02:00
Sabe Jones
bd5c4a08e2 Merge branch 'release' into develop 2018-06-26 20:45:37 +00:00
Sabe Jones
34fb90455c 4.50.0 2018-06-26 20:44:53 +00:00
Sabe Jones
d038d9f9bb chore(i18n): update locales 2018-06-26 20:35:38 +00:00
SabreCat
420d7df4f5 chore(sprites): compile 2018-06-26 20:24:04 +00:00
SabreCat
3ee8072a6c feat(content): Summer Splash Potions and Seafoam 2018-06-26 20:19:16 +00:00
Matteo Pagliazzi
50ebdd1ece tasks hsitory migration: prevent it from running twice 2018-06-25 23:14:35 +02:00
Sabe Jones
4a80dcae2e 4.49.1 2018-06-25 20:27:08 +00:00
Sabe Jones
bc03c1d18a fix(sprites): correct armor and weapon sprites 2018-06-25 20:26:26 +00:00
Sabe Jones
e5d834b40a 4.49.0 2018-06-25 19:39:39 +00:00
Sabe Jones
7f847d322f chore(i18n): update locales 2018-06-25 19:37:38 +00:00
Phillip Thelen
ed27ac15c8 Fix documentation about rejecting group invitation (#10466) 2018-06-23 11:24:33 +02:00
Sabe Jones
88188e56d9 Merge branch 'release' into develop 2018-06-22 19:03:33 +00:00
Sabe Jones
7170cf05d0 4.48.1 2018-06-22 19:02:55 +00:00
Sabe Jones
22a8d5e94c chore(i18n): update locales 2018-06-22 19:02:25 +00:00
SabreCat
f37e5cde57 chore(news): Blog Bailey 2018-06-22 18:58:47 +00:00
Matteo Pagliazzi
592cfef6c6 Client: use api v4 (#10457)
* client: use api v4

* fix tests
2018-06-21 21:25:27 +02:00
Matteo Pagliazzi
c1bd7f5dc5 Habits: store one history entry per day (#10442)
* initial refactor

* add scoredUp and scoredDown values for habits history entries, one entry per habit per day

* fix lint and add initial migration

* update old test

* remove scoreNotes

* dry run for migration

* migration fixes

* update migration and remove old test

* fix

* add challenges migration (read only)

* fix challenges migration

* handle custom day start

* update tasks in migration

* scoring: support cds

* add new test
2018-06-21 21:25:19 +02:00
Sabe Jones
8437b916c4 4.48.0 2018-06-21 18:50:38 +00:00
Sabe Jones
52e53aa466 chore(i18n): update locales 2018-06-21 18:49:55 +00:00
SabreCat
4471186e09 chore(sprites): maintenance 2018-06-21 18:44:37 +00:00
SabreCat
6a2a844e04 feat(content): Mystery Items June 2018 2018-06-21 18:44:18 +00:00
Sabe Jones
122d147f07 Merge branch 'release' into develop 2018-06-19 23:33:19 +00:00
Sabe Jones
912f00a652 4.47.0 2018-06-19 23:32:57 +00:00
Sabe Jones
627d4330c8 chore(i18n): update locales 2018-06-19 23:31:53 +00:00
SabreCat
c7f6794dda chore(sprites): compile 2018-06-19 23:25:41 +00:00
SabreCat
00cb50a781 feat(event): Summer Splash 2018
Also gets rid of those Fairy Potions, FOR REAL this time, and fixes a couple of minor Market layout issues
2018-06-19 23:24:40 +00:00
Matteo Pagliazzi
8be9964483 API v4 (WIP) (#10453)
API v4
2018-06-18 14:40:25 +02:00
Sabe Jones
7ea6c911cb Better group plan member counts (#10449)
* fix(group-plans): improved member count accuracy

* fix(migration): don't leave server running after completion

* fix(migration): don't update Stripe for non-Stripe methods
Also fixes a linting issue.

* fix(lint): no comma dangle here

* fix(async): put async token in relevant spot

* fix(lint): still more linting

* fix(async): better handling for async and promises
Also adds additional logging where discrepancies are found.

* feat(migration): provide CSV output

* fix(promises): better pause/resume

* fix(migration): don't update already canceled subs

* fix(groups): also address quantity/memberCount discrepancies

* fix(migration): also log quantity issues

* fix(migration): equation was reversed

* refactor(migration): condense logic, add error catch

* fix(migration): fix root cause of failed quantity update??

* fix(lint): gratuitous parens

* fix(test): expect group to be updated db-side

* fix(migration): actually update quantities?

* fix(groups): roll back unneeded Stripe lib change, refactor migration
2018-06-15 14:49:18 -05:00
Isabelle Lavandero
97a069642d Add timestamp to moderator Slack messages (fixes #10441) (#10443)
* add timestamp to moderator Slack messages

* fix test errors

* import moment, condense formatting

* add timestamp to author_name variable

* update test to include timestamp, fix footer matching

* change ISODate to Date

* update test to include timestamp
2018-06-15 11:01:10 +02:00
Dexx Mandele
26e9827d39 Open correct group modal from header (#10430)
* Remove outdated server readme

* Open correct group modal from header

* Update party button after joining party

* Review: fix invite members not working without reload

* Remove invite-modal from group page to prevent duplicates

* Pass correct group to invite modal
2018-06-15 11:00:10 +02:00
Brian Fenton
88c625fe80 removing the 24px top margin on .modal-dialog .title (#10377)
* removing the 24px top margin on .modal-dialog .title so boss HP and difficulty line up

* stop overloading title so much and use a properly-scoped style

* removing unnecessary temp vars

* using SCSS color vars as per CR

* using relative font size measurements instead of absolute pixels, and adding popover override CSS to not break quest shop & invite notifications

* removing redundant font declarations
2018-06-15 10:58:28 +02:00
Travis
4044432fad Fixes asynchronous cron bug that allows cron to run twice for a user within seconds. (#10142)
* Fixes asynchronous cron bug that allows cron to run twice for a user within seconds of eachother.

fixes #8991

* Fixing tests.

* Updating assignment to keep user and res.locals.user in sync.
2018-06-14 13:47:44 -05:00
Sabe Jones
6a767ed70b 4.46.2 2018-06-14 17:08:55 +00:00
Sabe Jones
2c2ca4a9c8 chore(i18n): update locales 2018-06-14 17:06:55 +00:00
Sabe Jones
380ad8c9e5 Merge branch 'develop' into release 2018-06-14 17:04:50 +00:00
Sabe Jones
f53400f950 Report: Subscriber Task Histories (#10439)
* WIP(report): subscriber task histories

* fix(report): old object ref, return promise, pause/resume

* fix(report): handle 0 Habits 0 Dailies
Also add data for master total of history entries
2018-06-14 05:37:29 -05:00
Sabe Jones
53c719acbd 4.46.1 2018-06-12 22:13:15 +00:00
Sabe Jones
948a5d80c8 fix(news): broken link 2018-06-12 22:13:01 +00:00
Sabe Jones
e1f141ee91 Merge branch 'release' into develop 2018-06-12 20:25:32 +00:00
Sabe Jones
7a5a278dbb 4.46.0 2018-06-12 20:25:07 +00:00
Sabe Jones
08ab4d5900 chore(i18n): update locales 2018-06-12 20:24:24 +00:00
SabreCat
b8d5844d0f chore(sprites): compile 2018-06-12 20:19:39 +00:00
SabreCat
39b81aa685 feat(content): Aquatic Amigos quest bundle 2018-06-12 20:19:14 +00:00
Dominic Lee
44f196080c Match tavern sidebar UI to party and guild page changes (#10400)
* Match tavern sidebar UI to party and guild page changes

* Remove extra space at the top of guild's sidebar
2018-06-11 12:00:19 +02:00
Patricia Beier
65bfd74c93 more place for the donate button - for languages with more letters (#10417) 2018-06-11 11:59:16 +02:00
Michael Chenevey
5e60a05cac related to 10419 (#10438)
Added a 'habitica:update-challenge' call to mirror the 'clone-challenge' call. This properly sets the 'cloning' flag and makes things more consistent.
This fixes the a bug related to #10419 described in the #10419 thread
2018-06-11 11:58:18 +02:00
Yun Ha Seo
59af4a2d3b Modal width responsiveness (partial fix for #10267) (#10354)
* added responsive scss to allow modals to respond to changing window size

* Remove unecessary space

* moved scss around

* remove unnecessary space

* Adjust left and right panels to be more responsive + moved css for buyQuestModal into its respective vue file (startQuestModal css wasn't working in its vue file... I can't figure out why)

* removed important to get rid of extra scrollbar

* moved css all to one file
2018-06-11 11:57:11 +02:00
Sabe Jones
8d273fac5e 4.45.1 2018-06-07 22:31:35 +00:00
Sabe Jones
b58032d5fb chore(i18n): update locales 2018-06-07 22:27:49 +00:00
SabreCat
db4123610f fix(migration): connect string 2018-06-07 11:34:50 +00:00
SabreCat
e4c1d96b59 Merge branch 'release' into develop 2018-06-05 20:20:56 +00:00
Sabe Jones
fe1f0bf087 4.45.0 2018-06-05 19:17:44 +00:00
Sabe Jones
ccd0bec28c chore(i18n): update locales 2018-06-05 19:16:56 +00:00
SabreCat
eebf38b5ae chore(sprites): compile 2018-06-05 19:11:40 +00:00
SabreCat
30cd738635 feat(content): Armoire and BGs 2018-06 2018-06-05 19:10:54 +00:00
Matteo Pagliazzi
920b07ff12 Merge branch 'release' into develop 2018-06-04 14:19:24 +02:00
Matteo Pagliazzi
a7db15d768 fixes #10423 (#10426) 2018-06-04 14:18:23 +02:00
Matteo Pagliazzi
93768e70c5 fixes #10423 (#10425) 2018-06-04 14:16:30 +02:00
aszlig
3e29b958e3 tests/cron: Fix tests that involve mocked time (#10418)
* Revert commenting out some cron subscription tests

This reverts commit 47c488967c.

We're going to properly fix these tests, so let's start by reverting the
commit that temporarily disabled these tests in the first place.

Signed-off-by: aszlig <aszlig@nix.build>

* tests/cron: Fix restoring clock on test failure

Ah, the joys of global state... >:-(

Whenever some test failed which has mocked the time using
useFakeTimers(), other test that are run after that test would fail (or
even time out) as well, which is a bit confusing to debug.

Some of the tests even had a cleanup routine in afterEach() but most of
them didn't, so I rearranged them in a way so that we have a clock
variable for *all* of the subtests, which initially is null and then a
cleanup handler (also for *all* of the subtest) calls clock.restore() if
the value isn't null.

In order to avoid calling clock.restore() twice, I have removed all the
clock.restore() calls at the end of the tests setting the clock to a
specific value.

Signed-off-by: aszlig <aszlig@nix.build>

* tests/cron: Fix test for 3-month gift subscription

So this is the actual culprit of the test failures that emerge during
the first two days of a month:

The test group "for a 3-month gift subscription (non-recurring)" creates
a User object available for every test case, which has a subscription
for 3 months beginning at the current time/date.

During each test case the fake timer is set to the second day of the
month to be tested. For the first and second month it's unproblematic
because the subscription is still active, no matter whether the
dateTerminated is set to the first day or the last day of a month.

However, the third month is problematic here, because whenever the
subscription lasts until the first day of the third month it has already
ended after the second day and thus the test fails because the actual
implementation of cron sets plan.consecutive.count to zero (which is
what it's supposed to do).

In order to fix this, I've set dateTerminated for the User object to the
15th of the current month so the subscription lasts long enough to not
trigger the test failure (and also make time zones irrelevant, because
right now there is no TZ offset which is more than half of a month,
especially not while running the test suite).

Signed-off-by: aszlig <aszlig@nix.build>
2018-06-02 13:05:41 +02:00
Sabe Jones
ce1bcdeee0 Merge branch 'release' into develop 2018-06-01 19:41:40 +00:00
Sabe Jones
971145a72a 4.44.3 2018-06-01 19:41:11 +00:00
Sabe Jones
36595e0138 chore(i18n): update locales 2018-06-01 19:40:42 +00:00
SabreCat
a1de566c34 fix(gems): use Promise array when gifting 2018-06-01 19:07:17 +00:00
SabreCat
2b8415fad0 chore(news): Bailey announcements
Also removes Cuddle Buddies Bundle from featured items
2018-06-01 18:55:43 +00:00
Matteo Pagliazzi
2afbc23f6f Merge branch 'release' into develop 2018-06-01 15:58:32 +02:00
Matteo Pagliazzi
a35c1954af Remove parallel calls to save (#10416)
* remove parallel saves from the code

* fix more unit tests

* do not save users when sending message in buyGift (saved later)

* fix test

* reinstall

* fix tests

* fix tests
2018-06-01 15:56:01 +02:00
Alys
47c488967c temporarily comment-out cron subscription tests that fail in the first days of a month 2018-06-01 20:50:00 +10:00
Matteo Pagliazzi
ee4a05d7ec update packages 2018-06-01 11:53:56 +02:00
Sergey
308cd49e9c IE 11 task wrapping issue (#9754) (#10409)
- Added Flex property to force right behaviour
2018-06-01 10:12:47 +02:00
Andrew Gaffney
48e51a03d4 Added correct CSS classes to help menu dropdown items. (#10412) 2018-06-01 10:05:16 +02:00
James Robinson
96f7a192d7 fix landing page contribute and social media buttons misalignment in IE (#10408) 2018-06-01 10:01:35 +02:00
Sabe Jones
1e786412ba 4.44.2 2018-06-01 01:32:28 +00:00
SabreCat
1739b83609 chore(news): Bailey 2018-06-01 01:31:35 +00:00
Sabe Jones
3606b58a1d 4.44.1 2018-05-31 20:04:47 +00:00
Sabe Jones
202db599ae chore(i18n): update locales 2018-05-31 20:03:06 +00:00
Sabe Jones
3aca0343e8 Merge branch 'release' into develop 2018-05-29 22:54:31 +00:00
Sabe Jones
97b99c0550 fix(tests): correct for new content, fix lint 2018-05-29 22:54:00 +00:00
Sabe Jones
0e63f68ed6 Merge branch 'release' into develop 2018-05-29 22:24:41 +00:00
Sabe Jones
fa142e929f 4.44.0 2018-05-29 22:17:33 +00:00
Sabe Jones
c4867f1e8e chore(i18n): update locales 2018-05-29 22:16:30 +00:00
SabreCat
5f58fe66de chore(sprites): compile + add news 2018-05-29 22:11:02 +00:00
SabreCat
a0e2d6a05e feat(customize): earrings and headbands 2018-05-29 21:02:42 +00:00
Matteo Pagliazzi
b67522e92b fix(contact form): add it back, fixes #10401 2018-05-29 19:28:28 +02:00
Matteo Pagliazzi
0e3496395c fix(challenges): fix display issues, fixes #10397 2018-05-29 19:25:43 +02:00
Matteo Pagliazzi
6e7b9f1f93 fix(due date): update value correctly, fixes #10405 2018-05-28 13:54:13 +02:00
Matteo Pagliazzi
e6cf7564b8 fix(i18n): pass path to wrongItemPath string, fixes #10403 2018-05-28 13:40:49 +02:00
Matteo Pagliazzi
bf424573a4 Members: user .lean() to improve performances (#10399)
* perf(members): use lean where possible

* fix unit tests

* fix unit tests and update calls to old function

* simplify code and add tests
2018-05-28 13:38:59 +02:00
Brian Fenton
ac90a40be5 Api quest restrictions - no purchase/start without fulfilling eligibility requirements (#10387)
* removing duplicate translation key

* fixing typos

* extracting quest prerequisite check. adding check for previous quest completion, if required

* fixing (undoing) static change, adding tests

* more typos

* correcting test failures

* honoring quest prerequisites in quest invite API call. updating format of il8n string replacement arg

* no longer using apiError, use translate method instead (msg key was not defined)

* adding @apiError to docblock as requested in issue

* removing checks on quest invite method. small window of opportunity/low risk
2018-05-27 16:41:56 +02:00
Matteo Pagliazzi
821f84dbe8 fix(facebook): include email 2018-05-25 18:54:50 +02:00
Matteo Pagliazzi
8fb67e7944 only store necessary data for social login (continuation of 10352) (#10395)
* feat(gdpr) only store necessary data for social login

* feat(gdpr) also store email for social users

* fix(social auth): store emails array instead of single email

* fix(emails): do not get name from old facebook info

* add migration to remove extra data from social profiles

* update migration description

* fix tests

* fix typo in migration file
2018-05-25 18:16:30 +02:00
Matteo Pagliazzi
e81e458e9b Merge branch 'pengfluf-PM_opt-in-out' into develop 2018-05-25 12:44:53 +02:00
Matteo Pagliazzi
aec23d32f3 Merge branch 'PM_opt-in-out' of https://github.com/pengfluf/habitica into pengfluf-PM_opt-in-out 2018-05-25 12:44:44 +02:00
aszlig
4f2d066d66 client: Fix display of class bonus for other users (#10376)
Whenever one is hovering an item from another user, the bonuses of these
items are shown for the own user. So for example if you're a mage and
view a Royal Magus Robe of another mage, the class bonus is 6.

However if you're a warrior, the class bonus displays as 0 because the
attributes grid is always using the stats for the own user even if
viewing equipment of a different user.

I've fixed this by moving the user object to the properties in
attributesGrid and passing the current user from every other Vue file
that's using attributesGrid.

Not sure whether this is the right approach, as I'm no expert in Vue.js
but some testing with the client now shows the correct values.

Signed-off-by: aszlig <aszlig@nix.build>
2018-05-25 12:38:02 +02:00
Matteo Pagliazzi
eaf0c62e16 fix(errors): snackbars for 502 and notification not found errors should have timeout (#10394) 2018-05-25 12:30:43 +02:00
Matteo Pagliazzi
fc62db147f fix(emails): make sure quest invitations are sent to users that signed up with google, fixes #10389 (#10393) 2018-05-25 12:13:02 +02:00
Keith Holliday
6c9ff3e8ed Ensure leader is set (#10390) 2018-05-25 12:04:07 +02:00
Matteo Pagliazzi
6ef45a7fd2 Fix 9248: challenge creator should not automatically join their own challenge (#10383)
* fix(challenges): creator should not join challenge automatically

* change behavior on the client side as well

* update tests and fix membercount

* update tests

* fix tests
2018-05-25 12:03:39 +02:00
Sabe Jones
557212b549 4.43.1 2018-05-24 18:55:14 +00:00
Sabe Jones
f8bd116e54 chore(npm): update package lock 2018-05-24 18:54:59 +00:00
Sabe Jones
9194e8226d 4.43.0 2018-05-24 18:34:13 +00:00
Sabe Jones
e0140f67be chore(i18n): update locales 2018-05-24 18:33:40 +00:00
SabreCat
1e2fc14db9 Merge branch 'develop' into release 2018-05-24 18:26:21 +00:00
SabreCat
30082a3929 chore(sprites): compile 2018-05-24 18:25:56 +00:00
SabreCat
42d7744d12 feat(content): May Subscriber Items 2018-05-24 18:25:36 +00:00
Matteo Pagliazzi
2cbc41d02f fix(challenges); update category labels when filtering, fixes #10382 2018-05-21 20:58:01 +02:00
Alys
01ce7712e3 change "Advanced Options" to "Advanced Settings" in Settings screen to match change in wording on tasks page 2018-05-21 20:20:06 +10:00
Keith Holliday
c52e4a07d4 Removed redirect uri (#10380)
* Removed redirect uri

* Fixed lint
2018-05-20 13:02:37 -05:00
Matteo Pagliazzi
5212ac6394 fix(tests): do not use arrow function when using this 2018-05-19 21:10:36 +02:00
Matteo Pagliazzi
7b5d6b508d fix(tests): longer timeout for invites 2018-05-19 20:45:56 +02:00
Matteo Pagliazzi
c5a497ef91 fix(settings): when changing language, reload page entirely, fixes #9904 2018-05-19 20:22:30 +02:00
Matteo Pagliazzi
54bee67e03 fix(settings): language can be changed in firefox, fixes #9514 2018-05-19 20:17:48 +02:00
Matteo Pagliazzi
86ec68bedb Merge branch 'develop' of github.com:HabitRPG/habitica into develop 2018-05-19 20:03:36 +02:00
Matteo Pagliazzi
8223563e76 fix(audio): rename todo audio files with wrong casing, fixes #10294 2018-05-19 20:03:19 +02:00
Keith Holliday
37ab257f5b Added responsive fixes to home page (#10381) 2018-05-19 11:43:19 -05:00
Keith Holliday
04d7ff13de Refactored stripe checkout (#10345)
* Refactored stripe checkout

* Fixed dependency injection cache
2018-05-19 10:31:26 -05:00
Sabe Jones
25d07ac0ce fix(snackbars): don't timeout server error snacks (#10372)
Fixes #10031 and #9249.
2018-05-18 14:41:15 -05:00
SabreCat
724e1240a3 fix(content): update potion end date 2018-05-18 18:40:04 +00:00
Sabe Jones
026e1a5bca Merge branch 'release' into develop 2018-05-18 17:15:16 +00:00
Sabe Jones
6443918440 4.42.6 2018-05-18 17:14:39 +00:00
Matteo Pagliazzi
ac973ee753 fix(tests): do not error when test chat messages miss the timestamp attribute 2018-05-18 18:04:32 +02:00
Matteo Pagliazzi
c39b9dc320 fix(challenges): format summary with markdown and do not split words, fixes #10371 2018-05-18 17:33:38 +02:00
Matteo Pagliazzi
2132a3a242 fix(menu): correct padding 2018-05-18 17:30:15 +02:00
Dexx Mandele
ba52a90d93 Make nav drop-down cleaner/scrollable (#10138)
* Make nav drop-down cleaner/scrollable

* Stop overriding bootstrap navbar

* Move user menu to top bar in mobile

* Restructure/style first drop-down item

* Add ALL the pretty colors

* Apply menu drop-down re-structure to all menu drop-downs

* Replace curly brace lost during rebase
2018-05-18 17:11:25 +02:00
Brian Fenton
daa4994382 Change reward popunder (#10358)
* making add multiple tip reflect the task type

* removing duplicated key
2018-05-18 17:11:04 +02:00
Mateus Etto
12034161b7 Remove Ethereal Surge notification (#10368) 2018-05-18 17:09:00 +02:00
Ian Oxley
8438cf0578 Fix drawer text overlapping at smaller screen resolutions (#10360)
* Replace divs with semantic markup

Replace `<div>` tags with `<nav>`, `<aside>`, and a list for the nav items.

* Use grid layout

Replace flexbox with CSS grid layout. The right-hand side item is now in its own
grid cell, so the text wraps inside its cell at smaller screen widths.

Undo `<nav>` tag.

* Sort CSS

Sort the remaining CSS property declarations.

* Fix right alignment issue in Safari

Remove `justify-self: end` to fix the right alignment issue in Safari.

* Fix vertical alignment in Edge

Add `align-self: center` but only for MS Edge.

Also removed `position: relative` on the wrapper element for the tabs.
As the help item isn't using absolute positioning anymore we don't need
to set relative positioning on the parent element.
2018-05-18 17:07:42 +02:00
jerellmendoodoo
614848d60b added try catch, added snackbar notification for errors returned (#10370)
* added try catch, added snackbar notification for errors returned

* moved lines into try block
2018-05-18 17:04:18 +02:00
aszlig
79087b27d3 redirects: Fix parsing BASE_URL with port number (#10350)
The parsing in the redirects module was simply determining the base host
via trimming off everything up to //, so a BASE_URL like
"http://localhost:3000" will result in the host name "localhost:3000",
which isn't a valid host name.

So the problem here is that BASE_URL_HOST is used for determining
whether the client should be redirected and it's comparing the hostname
of the request object with BASE_URL_HOST.

For example if we have the aforementioned BASE_URL, we get to the
following comparison:

req.hostname !== BASE_URL_HOST

Which expands to:

"localhost" !== "localhost:3000"

So in order to get rid of the port number, we now use url.parse() to get
the right host name.

Signed-off-by: aszlig <aszlig@nix.build>
2018-05-18 17:02:36 +02:00
aszlig
5167f847d0 tests: Increase timeouts instead of disabling them (#10367)
Some tests were disabled in ba799c67f9 and
10567d81e2, because they tend to
frequently time out after 8 seconds.

Instead of disabling the tests (which IMHO is bad, because tests are
there for a reason), we're now increasing the timeout to 30 seconds just
for these tests.

As requested by @paglias, I've marked the timeout functions with a @TODO
comment, so that the slow tests or the functionality they're testing are
eventually refactored.

I also needed to change the arrow notation for the test cases to use the
function keyword, because otherwise we don't have this.timeout()
available.

Signed-off-by: aszlig <aszlig@nix.build>
Cc: @paglias
2018-05-18 17:02:20 +02:00
aszlig
d3a0348ac7 Avoid using media element with empty src attribute (#10364)
Whenever the client starts up, the following is emitted in the Firefox
console:

Invalid URI. Load of media resource  failed.
All candidate resources failed to load. Media load paused.

This happens because the <source/> tags are preinitialized with a src
attribute of "".

So what we're doing instead is initialize the <audio/> element without
any children and add the children as soon as the first audio file needs
to be played. This also has the advantage that we can determine at
runtime whether the browser supports Ogg/Vorbis or whether we should
fall back to MPEG layer 3 so only one source element is needed.

Signed-off-by: aszlig <aszlig@nix.build>
2018-05-18 17:01:27 +02:00
negue
de9883c3ac extract chat (#10362)
* extract chatTextarea from group/tavern - extract staffList array

* fix lint / rewrite condition

* clean up - part 1

* rename chatTextarea to chat

* refactor timestamp check
2018-05-18 17:01:05 +02:00
negue
3d39718048 Purchase API Refactoring: Spells [Gold] (#10305)
* convert buySpell operation

* remove purchaseWithSpell - change purchaseType 'special' to 'spells' - fix lint

* fix tests

* rollback 'spells' to 'special'
2018-05-18 17:00:39 +02:00
Matteo Pagliazzi
a0c51ee4ca fix(loading bar): always above other elements 2018-05-18 13:42:44 +02:00
Sabe Jones
b2edd1d932 4.42.5 2018-05-17 20:58:16 +00:00
Sabe Jones
6b5f46c5e1 chore(i18n): update locales 2018-05-17 20:57:57 +00:00
Alys
ad191c2c5c change apidoc to explain that the equip route also unequips 2018-05-16 20:45:35 +10:00
Sabe Jones
4a55d36831 Merge branch 'release' into develop 2018-05-15 21:11:54 +00:00
Sabe Jones
959adb05cf 4.42.4 2018-05-15 21:11:35 +00:00
Sabe Jones
d114b858fd chore(i18n): update locales 2018-05-15 21:06:11 +00:00
SabreCat
ae9db7aee3 feat(content): enable Fairy Potions 2018-05-15 21:00:49 +00:00
Matteo Pagliazzi
10567d81e2 fix(tests): remove tests that timeout 2018-05-15 18:03:00 +02:00
Matteo Pagliazzi
ba799c67f9 fix(tests): remove tests that timeout 2018-05-15 17:42:27 +02:00
Matteo Pagliazzi
37b890f282 fix(market): fixes #10316 2018-05-15 17:21:15 +02:00
Matteo Pagliazzi
196e5f5b95 upgrade deps 2018-05-15 17:00:17 +02:00
Matteo Pagliazzi
6db412f7e6 fix tags checkbox in bootstrap 4.1.1 2018-05-15 16:55:35 +02:00
Keith Holliday
fa60c9a232 Reset stats after allocation (#10363) 2018-05-14 22:18:23 -05:00
Matteo Pagliazzi
2c3d268a63 Merge branch 'develop' of github.com:HabitRPG/habitica into develop 2018-05-13 16:30:47 +02:00
Matteo Pagliazzi
d4a80a8561 Merge branch 'marvinrabe-fix-challenge-layout' into develop 2018-05-13 16:30:28 +02:00
Matteo Pagliazzi
388492e1e7 fix conflicts and remove extra dependency 2018-05-13 16:30:17 +02:00
aszlig
8cd695c397 locales/groups: Don't wrap task text in code block (#10349)
So far if a task contained Markdown, a system message like this would
have been posted to the group chat:

foo has claimed "Some [link](http://example.org/)"

Also, if the Markdown contained backticked code fragments, the whole
text would be displayed in red except the code part.

The reason for this is because the system message is already in Markdown
and a backticked task text would result in the following Markdown:

`foo has claimed "Foo `bar`"`

Here there are two code blocks, one with `foo has claimed "Foo ` and
another which only has `"`.

This is fixed by simply changing the userIsClamingTask translation
string to not wrap the task text inside a code block, as per @Alys
suggestion.

Signed-off-by: aszlig <aszlig@nix.build>
2018-05-13 16:14:17 +02:00
Brian Fenton
355f0fedfb disabling checking off a subtask if not assigned to a user (#10357) 2018-05-13 16:12:26 +02:00
Doğu Deniz Uğur
38d78de4b3 New method added to displaying locked quest popover message in shop (#10346)
isBuyingDependentOnPrevious () method checks if item.key of quest is in a list of quests whose unlock condition is not dependent on the completition of previous quest.
2018-05-13 16:07:20 +02:00
pengfluf
6c64a1cd8c Beard and mustache facial hairs now can be bought as a full set for 5 gems (#10338)
* Purchasing All Facial Hairs Fixed

* Notifications z-index fixed

* Notifications z-index fixed x2

* Z-indexes fixed, facial hairs buying corrected

* isPurchaseAllNeeded refactored

* isPurchaseAllNeeded is more generic now

* Linting Passed
2018-05-13 16:04:43 +02:00
Matteo Pagliazzi
128ec5a1b1 Update pull request template to mention issue number instead of url 2018-05-13 15:42:44 +02:00
siege918
d4d668f640 Prevent accidental submission of Tavern/Guild posts after pasting (#10226)
* Temporarily disable ctrl-enter to send Guild messages after paste

Disable Ctrl-Enter after pasting, because some users are experiencing issues with accidentally sending their messages after pasting.

* Code style fixes for "Temporarily disable ctrl-enter to send Guild messages after paste"

* Fix issues with variable location

* Fix variables for accidental chat submission features

Moving vatiables for the chat submit timeout to their own variable so they won't be overwritten

* Fix code formatting issues with accidental chat submission code

* Remove leading space from variables to fix lint issues
2018-05-11 15:18:41 -05:00
Marvin Rabe
41ccd58f8e Fixed tavern chat button. (#10342) 2018-05-11 15:15:50 -05:00
Sabe Jones
a33299a341 4.42.3 2018-05-11 20:12:54 +00:00
Sabe Jones
9129e22433 fix(event): disable seasonal potions 2018-05-11 20:12:41 +00:00
Sabe Jones
86d1bdaff1 4.42.2 2018-05-11 01:38:33 +00:00
SabreCat
206ed1f155 fix(tags): downgrade Bootstrap to restore tag checkboxes 2018-05-11 01:34:31 +00:00
Sabe Jones
eb66e9ec2e 4.42.1 2018-05-10 18:52:17 +00:00
Sabe Jones
8db99be017 chore(i18n): update locales 2018-05-10 18:51:37 +00:00
SabreCat
c62386e2e5 chore(news): Bailey 2018-05-10 18:42:44 +00:00
Matteo Pagliazzi
042ac6ac73 Fix notifications in user pre save hook (#10348) 2018-05-09 19:19:08 +02:00
Matteo Pagliazzi
a8655d923a Fix level up webhook (#10347)
* use user._tmp for level up webhook

* use post save hook to send webhook
2018-05-09 19:04:29 +02:00
Sabe Jones
bbbd1f9f73 Merge branch 'release' into develop 2018-05-08 18:41:13 +00:00
Sabe Jones
8fee5a9ba0 4.42.0 2018-05-08 18:40:48 +00:00
Sabe Jones
8df2b1e8c2 chore(i18n): update locales 2018-05-08 18:38:43 +00:00
SabreCat
24cceb1c91 feat(content): Cuddle Bundle 2018-05-08 18:28:33 +00:00
Keith Holliday
21eac3cc94 Fixed stat allocation issues (#10344) 2018-05-08 08:54:50 -05:00
Keith Holliday
e9ce968f88 4.41.8 2018-05-07 16:34:54 -05:00
Keith Holliday
8c283fdbe0 Removed hook updates (#10341)
* Removed hook updates

* Fixed lint error
2018-05-07 16:30:34 -05:00
Sabe Jones
69a782a1db Party header sort WIP (#10330)
* WIP(groups): improved sorting WIP

* WIP(groups): split sort option and direction

* WIP(party): header sort cont'd

* feat(party): header sorting
2018-05-07 16:19:00 -05:00
Keith Holliday
ccaf629228 4.41.7 2018-05-07 12:50:13 -05:00
Keith Holliday
147f2bb28e 4.41.6 2018-05-07 12:48:48 -05:00
Keith Holliday
54a4bba228 Removed update stats notification (#10339)
* Removed update stats notification

* Removed level up hook
2018-05-07 12:45:36 -05:00
Marvin Rabe
6ee21dcfa9 Added category tags tests. 2018-05-07 18:29:05 +02:00
Marvin Rabe
68353fb874 Added sidebar section test. 2018-05-07 18:21:32 +02:00
Marvin Rabe
68526c07ae Added missing comma. 2018-05-07 16:43:07 +02:00
Marvin Rabe
5f319ca4f6 My Challenges should include all Owned Challenges (#9286) 2018-05-07 16:23:49 +02:00
Marvin Rabe
0d84643961 Joined and Owned Challenges should still appear in Discover, but annotated with status (#9956) 2018-05-07 16:04:40 +02:00
Alys
8470f16f4f allow subscribers to buy their final monthly gem (#10331) 2018-05-07 15:20:28 +02:00
Marvin Rabe
1896a8fab0 Challenge task numbers get created from computed array. 2018-05-07 14:18:05 +02:00
Marvin Rabe
891b5566a9 Created reusable category tags component. 2018-05-07 13:56:54 +02:00
Marvin Rabe
c83499545c Added Tavern case 2018-05-07 13:35:53 +02:00
Marvin Rabe
4c837acf88 Use computed attribute for boss health bar width. 2018-05-07 13:30:33 +02:00
Marvin Rabe
11b223a81e Challenge item shadow and border radius matches guild item style. 2018-05-07 13:25:52 +02:00
Marvin Rabe
17001743e1 Changed last prop to :last-of-type 2018-05-07 13:24:48 +02:00
Keith Holliday
ac451bdb9b Removed spell queue (#10337) 2018-05-06 17:53:49 -05:00
Keith Holliday
6af50c9f2f Payment refactor (#10325)
* Rarranged payment index functions

* Moved gem function

* Increased buy gems test coverage

* Reduced length of functions. Reduced cognitive complexity
2018-05-06 15:12:00 -05:00
Marvin Rabe
5231cb03a8 Fixed columns when translation is too long. (#10315) 2018-05-04 16:10:02 -05:00
Marvin Rabe
f8739b6f37 Fixed learn more in user dropdown. (#10314) 2018-05-04 16:09:38 -05:00
Corey Gray
e31f62a818 Add responsive margins to pets and mounts. (#10311)
* Add responsive margins to pets and mounts.

* Move all margins to margin-right to make left edges flush.
2018-05-04 16:06:58 -05:00
pengfluf
d5d06c1d2d Header stuck naming fixed (#10309) 2018-05-04 16:05:38 -05:00
pengfluf
4fa2ef045d 10256 - The placeholder and the message row are fixed (#10307) 2018-05-04 16:04:06 -05:00
Matteo Pagliazzi
570a8bf0d5 do not load inbox in tasks routes (#10302) 2018-05-04 16:00:57 -05:00
Matteo Pagliazzi
b7dfe41e15 do not load inbox in some user routes (#10301) 2018-05-04 16:00:40 -05:00
negue
c26696a9eb moving developer-only strings to api/common messages (#10258)
* move translatable string to apiMessages

* use apiMessages instead of res.t for groupIdRequired / keepOrRemove

* move pageMustBeNumber to apiMessages

* change apimessages

* move missingKeyParam to apiMessages

* move more strings to apiMessages

* fix lint

* revert lodash imports to fix tests

* fix webhook test

* fix test

* rollback key change of `keepOrRemove`

* remove unneeded `req.language` param

*  extract more messages from i18n

* add missing `missingTypeParam` message

* Split api- and commonMessages

* fix test

* fix sanity

* merge messages to an object, rename commonMessage to errorMessage

* apiMessages -> apiError, commonMessages -> errorMessage, extract messages to separate objects

* fix test

* module.exports
2018-05-04 16:00:19 -05:00
Matteo Pagliazzi
f226b5da07 Revert #10324 and #10323 (#10329) 2018-05-04 20:57:18 +02:00
Keith Holliday
63cf5b6be7 4.41.5 2018-05-04 10:03:32 -05:00
Matteo Pagliazzi
f3a947339c fix quest completion modal: send only one request (#10327) 2018-05-04 17:00:11 +02:00
Keith Holliday
1bb8acad5d 4.41.4 2018-05-04 08:19:37 -05:00
Keith Holliday
8e04d6e284 Removed update stats notification (#10324) 2018-05-04 08:17:22 -05:00
Sabe Jones
f7415df6ba 4.41.3 2018-05-03 20:45:21 +00:00
Matteo Pagliazzi
f85e1c2dc4 Hotfix for webhooks bus in models/group (#10323)
* remove new webhooks code from group model

* disable chat webhooks as well
2018-05-03 22:40:42 +02:00
Sabe Jones
33628a0a6a 4.41.2 2018-05-03 18:02:49 +00:00
Keith Holliday
5e6541faa6 Logged users out if they were logged in with facebook (#10322) 2018-05-03 13:00:50 -05:00
Sabe Jones
c1ed02d383 4.41.1 2018-05-03 17:44:38 +00:00
Sabe Jones
3793e92b80 chore(i18n): update locales 2018-05-03 17:41:59 +00:00
Matteo Pagliazzi
e3ce1c5322 possible fix for facebook auth bug 2018-05-03 18:06:01 +02:00
Alys
84b16f28c2 remove statement about deletion feedback being anonymous 2018-05-03 21:31:06 +10:00
user
9c702505a9 Locales Changing, PM Disabled Caption Added 2018-05-02 19:08:43 +03:00
Sabe Jones
27c73e028a Merge branch 'release' into develop 2018-05-01 21:32:29 +00:00
Sabe Jones
d125b8d2f8 4.41.0 2018-05-01 21:32:06 +00:00
Sabe Jones
451e08ce1c chore(i18n): update locales 2018-05-01 21:31:34 +00:00
SabreCat
16b5b8b8c7 chore(sprites): compile 2018-05-01 21:25:58 +00:00
SabreCat
30a717148e chore(event): end Spring Fling 2018-05-01 21:25:45 +00:00
SabreCat
bcf9670dbe feat(content): Armoire and backgrounds May 2018 2018-05-01 20:55:16 +00:00
Marvin Rabe
129fccf646 Guild category tags and challenge category tags have now the same styling. 2018-05-01 21:36:23 +02:00
Marvin Rabe
9d755c5d5f Merge branch 'fix-german-translations' into fix-challenge-layout 2018-05-01 20:17:29 +02:00
Marvin Rabe
05c43d1f9d Use sidebar section component in tavern. 2018-05-01 20:13:46 +02:00
Marvin Rabe
45df73e4be Fixed challenges on 'Tavern' 2018-05-01 19:53:31 +02:00
Marvin Rabe
eaa00598d0 Improvements to Challenge Layout (#9619) 2018-05-01 19:47:04 +02:00
Marvin Rabe
88b14592c5 Make Challenge Owner's Name Clickable (#9283) 2018-05-01 17:23:02 +02:00
Marvin Rabe
85136675e9 Fixed group sidebar. 2018-05-01 16:13:16 +02:00
Marvin Rabe
4e4181a394 Improved challenge layout. 2018-05-01 16:10:57 +02:00
Marvin Rabe
f93822b0b3 Several hard coded strings fixed. 2018-05-01 14:34:58 +02:00
Matteo Pagliazzi
a864e69042 make unhandled promise rejections easier to find among logs 2018-05-01 12:09:30 +02:00
Matteo Pagliazzi
2ccd9eaa1e uprade deps 2018-05-01 12:01:47 +02:00
Alys
5faf00d489 replace loading screen tip about buff arrow
The buff arrow no longer appears on your avatar.
2018-05-01 18:32:44 +10:00
Alys
f211610f5d add swear word - TRIGGER / CONTENT WARNING: assault, slurs, swearwords, etc 2018-05-01 15:29:01 +10:00
Alys
332f285ea2 exempt The Rhyme Commando guild from the swearword blocker
This allows people to quote passes from literature containing words
that would otherwise be banned as religious oaths.
2018-05-01 13:31:56 +10:00
Sabe Jones
006159cc9c Merge branch 'release' into develop 2018-04-30 20:46:03 +00:00
Sabe Jones
3722452b51 4.40.1 2018-04-30 20:45:38 +00:00
Sabe Jones
d6b5d275da chore(i18n): update locales 2018-04-30 20:45:27 +00:00
SabreCat
72073386ec chore(news): Bailey 2018-04-30 20:41:31 +00:00
Matteo Pagliazzi
d34ec62901 Remove inbox from more routes (#10303)
* remove inbox from some auth routes

* remove inbox from quests routes

* remove inbox from groups routes
2018-04-30 20:36:31 +02:00
Matteo Pagliazzi
ca73b9af41 remove stackimpact 2018-04-30 19:07:46 +02:00
Matteo Pagliazzi
8b9bf88fa0 Remove inbox from more routes (#10300)
* remove inbox from user/stats routes

* remove inbox from news routes

* change signature for authWithHeaders

* do not load inbox in coupons routes

* do not load inbox in challenge routes

* do not load inbox in some members routes

* do not load inbox in chat routes
2018-04-30 17:36:41 +02:00
user
9133250a42 Useless CSS rule for the caption has deleted 2018-04-30 16:12:53 +03:00
user
e60177f14a Sending messages is allowed; PM related texts moved 2018-04-30 00:58:08 +03:00
Matteo Pagliazzi
5f0ef2d8f0 Webhooks v2 (and other fixes) (#10265)
* begin implementing global webhooks

* add checklist item scored webhook

* add pet hatched and mount raised webhooks (no tests)

* fix typo

* add lvl up webhooks, remove corrupt notifications and reorganize pre-save hook

* fix typo

* add some tests, globalActivity webhook

* fix bug in global activiy webhook and add more tests

* add tests and fix typo for petHatched and mountRaised webhooks

* fix errors and add tests for level up webhook

* wip: add default data to all webhooks, change signature for WebhookSender.send (missing tests)

* remove unused code

* fix unit tests

* fix chat webhooks

* remove console

* fix lint

* add and fix webhook tests

* add questStarted webhook and questActivity type

* add unit tests

* add finial tests and features
2018-04-29 20:07:14 +02:00
user
770285f10d Toggle-switch aligned with Messages Title 2018-04-29 19:08:09 +03:00
user
495dd2736c Locale Small Update 2018-04-29 17:03:51 +03:00
user
4467da980c POST request toggling opt deleted, changed to PUT /user 2018-04-29 16:48:10 +03:00
user
082539b982 Toggle-switch changed to the local one; 'en' locale edited 2018-04-29 16:31:23 +03:00
user
ef7719f91d PM opt-in opt-out intert internationalization 2018-04-29 04:32:33 +03:00
user
f98efd4eb9 PM opt-in opt-out is ready to use 2018-04-29 04:05:31 +03:00
user
4a0856c919 Client: opt-in / opt-out functionalitonality is ready 2018-04-29 03:07:03 +03:00
user
2adc5c13e4 Server: /toggle-private-messages-opt 2018-04-28 23:44:17 +03:00
Shadi Moustafa
cf274310a8 Changed Member List number in Guilds (#10268)
* Updated README.md

Added Team Name and Collaborators

* Updated README.md

* Changed Member List number in Guilds

Changed Member List number in Guilds

* remove habitica2.bat

* Updated README.md
2018-04-28 17:43:59 +02:00
Philip Karpiak
a2ee73a2e2 Use consistent elements in footer links (fix add-on/forum link colors) (#10208)
* Fix html element rendering of some footer links

* Unscope footer.expanded + children styles

Fixes link color cascading
2018-04-28 17:43:40 +02:00
Brian Fenton
c6c9503e22 Hiding popunder if challenge data is incomplete (#10284)
* removing file that only contained a reference to a missing folder

* fixing typo

* using full dates to avoid moment warning in tests

* more typos

* sending an empty string to vue bootstrap tooltip (disabling it) if no challenge short name is set
2018-04-28 17:38:38 +02:00
Asher Dale
403ac1ab7e Remove experience notification when leveling up (#10285) 2018-04-28 17:37:58 +02:00
Brian Fenton
63598f497b pinning mongodb container to recommended, supported version (#10270) 2018-04-28 17:37:19 +02:00
Asher Dale
9fcc953b18 Fix API challenges export CSV bug (Fixes #8350) (#10266)
* Fix challenges export CSV error by checking that users still belong to challenge

* Add test for challenge csv export fix

* Update fix for challenge export CSV bug

* Update tests for challenge export CSV to be more complete

* Refactor a test: change some 'let' variables to 'const'
2018-04-28 17:36:12 +02:00
Christos Maris
17408d01a9 Fix markdown (#10263)
The README.md file in the website/client/ directory had a flaw.
2018-04-28 17:35:14 +02:00
Tyler Nychka
ae786f28a2 Fix locked class-specific gear after death fixes #10025 (#10212)
* Fix locked class-specific gear after death fixes #10025

* Update to allow items next in tier but not owned

* Updated logic

* Added tests
2018-04-28 17:34:08 +02:00
Matteo Pagliazzi
1effa16b5b load memwatch-next only if installed 2018-04-27 20:52:33 +02:00
Matteo Pagliazzi
6b7333927a make memwatch-next optional, fixes #10291 2018-04-27 19:48:04 +02:00
Matteo Pagliazzi
31b439129d update deps 2018-04-27 19:38:42 +02:00
greenkeeper[bot]
2de85b937f fix(package): update bcrypt to version 2.0.0 (#10233) 2018-04-27 19:30:09 +02:00
negue
4f963e99dc Purchase API Refactoring: Gems [Gold] (#10271)
* remove `keyRequired` - change to `missingKeyParam` - i18n-string

* extract & convert buyGemsOperation

* fix lint
2018-04-27 19:29:26 +02:00
Keith Holliday
58ce3a9a42 Added bulk allocation (#10283) 2018-04-27 11:07:41 -05:00
Alys
e45d0c9b80 add website/raw_sprites/** to list of files ignored by nodemon (#10274) 2018-04-26 10:46:49 +02:00
Alys
84a20ef4f4 increase user count on home page from 2.5 to 3 million (#10257)
Uses a variable for the number instead of hard-coding it in the locales files.

Removes some old, unused locales strongs and an associated variable
from when we had a million users.
2018-04-25 10:48:04 -05:00
Alys
59a22805b9 changed message shown to muted users (after discussion with mods)
Adjusted apidocs comment to match.
Corrected the error type for that comment.
2018-04-25 20:40:21 +10:00
Alys
95865f5ec8 add a missing quote mark to the end of the Golden Knight's speech 2018-04-25 19:38:29 +10:00
Alys
79903d242f edit apidocs comment: CreateChallenge takes the parameter group not groupId 2018-04-25 16:23:07 +10:00
Sabe Jones
90959c18cd Merge branch 'release' into develop 2018-04-24 18:46:20 +00:00
Sabe Jones
8b2019c292 4.40.0 2018-04-24 18:45:51 +00:00
Sabe Jones
9ab70ca276 chore(i18n): update locales 2018-04-24 18:44:30 +00:00
SabreCat
d51aa25470 chore(sprites): compile 2018-04-24 18:38:56 +00:00
SabreCat
0b2c1e6d2e fix(pixels): Better alignment for Squirrel mounts
Also (1) updates an artist credit to a new usename, and (2) corrects a typo in migration comments
2018-04-24 18:37:33 +00:00
SabreCat
58ee6e9703 feat(content): Subscriber Mystery Items 2018/04 2018-04-24 18:36:19 +00:00
Keith Holliday
0044778497 4.39.2 2018-04-24 08:16:35 -05:00
Matteo Pagliazzi
46d6590fec chat: use _id instead of id when finding doc (#10278) 2018-04-24 15:10:20 +02:00
Keith Holliday
b10f056a73 4.39.1 2018-04-23 21:04:03 -05:00
Keith Holliday
eeb890466a Converted date to timestamp (#10276)
* Converted date to timestamp

* Added existence check

* Updated test to include timestamp
2018-04-23 21:03:24 -05:00
Keith Holliday
8d25a5d140 Added bulk spell queue (#10241)
* Added bulk spell queue

* Removed extra comment

* Moved queue to store
2018-04-23 20:30:55 -05:00
Sabe Jones
3b35a0a203 4.39.0 2018-04-23 17:21:12 +00:00
Sabe Jones
d787ad43d3 chore(i18n): update locales 2018-04-23 17:20:54 +00:00
Keith Holliday
7d7fe6047c Move Chat to Model (#9703)
* Began moving group chat to separate model

* Fixed lint issue

* Updated delete chat with new model

* Updated flag chat to support model

* Updated like chat to use model

* Fixed duplicate code and chat messages

* Added note about concat chat

* Updated clear flags to user new model

* Updated more chat checks when loading get group

* Fixed spell test and back save

* Moved get chat to json method

* Updated flagging with new chat model

* Added missing await

* Fixed chat user styles. Fixed spell group test

* Added new model to quest chat and group plan chat

* Removed extra timestamps. Added limit check for group plans

* Updated tests

* Synced id fields

* Fixed id creation

* Add meta and fixed tests

* Fixed group quest accept test

* Updated puppeteer

* Added migration

* Export vars

* Updated comments
2018-04-23 12:17:16 -05:00
Sabe Jones
0ec1a91774 4.38.0 2018-04-19 19:29:33 +00:00
Sabe Jones
adf3281bef chore(i18n): update locales 2018-04-19 19:28:43 +00:00
SabreCat
ea86b35833 chore(news): Bailey 2018-04-19 19:25:45 +00:00
Alys
ade14edcd7 add partial documentation for dueDate parameter in /api/v3/tasks/user and related code 2018-04-18 23:22:11 +10:00
Sabe Jones
3a1888739a Merge branch 'release' into develop 2018-04-17 20:07:12 +00:00
Sabe Jones
3b54ce4949 4.37.2 2018-04-17 20:06:46 +00:00
Sabe Jones
4a8aaf7389 chore(i18n): update locales 2018-04-17 19:55:10 +00:00
SabreCat
45eec47b7f chore(news): Bailey
Also disable some costly analytics
2018-04-17 19:52:36 +00:00
Keith Holliday
4b9af8aa86 Added analytics to front. Fixed group plan tracking (#10262) 2018-04-17 12:43:52 -05:00
SabreCat
631bbcb786 Merge branch 'fix-hippocrite' into develop 2018-04-17 01:37:20 +00:00
Matteo Pagliazzi
76a10d6cf9 start removing inbox from some routes (#10259) 2018-04-16 18:43:09 +02:00
Keith Holliday
a1c9ebd661 Prevent dropdown from closing when clicking search (#10252) 2018-04-15 19:18:40 -05:00
SabreCat
9f06d78db6 Revert "moving developer-only strings to api messages (#10188)"
This reverts commit a42cb0e3ab. Testing hypothesis that this was causing Staging to break.
2018-04-15 17:09:15 +00:00
Alys
ac98aa9271 replace Lemoness's email address with admin in sample config file
This is for consistency with the production server and to ensure
that contributors' screenshots in PRs match what will be seen
in production.
2018-04-15 13:34:42 +10:00
negue
455f7ac59b round priority on update too (#10186)
* round priority on update too

* move the fix to Task sanitizeTransform

* refactor the task.priority parsing
2018-04-14 16:16:25 +02:00
negue
a42cb0e3ab moving developer-only strings to api messages (#10188)
* move translatable string to apiMessages

* use apiMessages instead of res.t for groupIdRequired / keepOrRemove

* move pageMustBeNumber to apiMessages

* change apimessages

* move missingKeyParam to apiMessages

* move more strings to apiMessages

* fix lint

* revert lodash imports to fix tests

* fix webhook test

* fix test

* rollback key change of `keepOrRemove`

* remove unneeded `req.language` param

*  extract more messages from i18n

* add missing `missingTypeParam` message
2018-04-14 16:13:13 +02:00
Alys
d05d2fb9d7 removed a slur that has legit uses - TRIGGER / CONTENT WARNING: slurs, swearwords, assault, etc 2018-04-14 21:51:06 +10:00
negue
6c4c5b4697 always check for the quantity not (#10251) 2018-04-13 21:04:08 +02:00
Keith Holliday
5da87640e4 Apple pay tests (#10248)
* Added more tests for verifyGemPurchase

* Added more tests for subscribe

* Added user is subscribed check

* Reverted gulp task

* Added existence check
2018-04-13 12:41:41 -05:00
Kip Raske
fa044ffb44 Feature/sortable reward area (#9930)
* Client POC

We need to wrap each draggable region it its own div or else the
"draggable" element will conflict with each other. This screws up the
styling but that is totally fixable

* Ah that ref was being used after all, changing back

* Scaffold out a new callback for when we drag these things

Next is going to be the hard part: I need to save the sort order for
these to the database. I don't even know if there is a schema but hey
this is the best place to start

* Firefox caching is the problem: don't actually need the wrapper div

So I guess I should try this in chrome and see how it works then come
back to firefox and figure out what the heck is going on

* Scaffolding out our API call to save the sort order

The endpoint doesn't exist yet so we will need to add that

* Ok we are now calling our API endpoint to reorder these things

Of course it doesn't exist yet so you get a 404 when you try, but that
is ok

* Defining api endpoint, a work in progress

In particular I really had ought to use _id for these too, it appears
that the primary way we detect order doesn't even use "key" at all.

* Switching to using the pinned item UUID

This has much better results, but of course the server and client logic
don't match now. Will have to keep working on my splice to make sure
that they are the same

* I thought this would fix our server/client mismatch but it is not it

Something is really wrong with my logic somewhere, maybe I need to
update the db step?

* Moving this logic to the "user" rather than "tasks" and key off path

Path is unique and is less finiky than dealing with string comparisons
with ids. Unfortunately everything is still not working... I suppose
user.update() doesn't care about the position?

* This client code caused quite a lot of problems if you dragged fast

We don't really need it it seems, so off it goes

* Updating markup and CSS so it actually looks good.

Everything is working horray!!

I did just notice the following bug: the popover text sometimes makes it
very annoying to drag because you can't drop over it@

* Cleaning up my comments in the API section user.js

I had a lot of TODOS that are mostly done now

* Fixing a spacing code standards thing

* Turns out we never use type, so we should remove this from the API call

* Adding pinnedItemsOrder into the user schema

And disabling my call in the frontend before I do any more damage

* Halfway to using pinnedItemsOrder

This isn't working yet but it is not going to break it horribly like it
was before.

* Hooking up inAppRewards to always produce sorted information

It is suspicially working right now even though I have not added the
seasonal stuff logic yet...

* Updating the comments in user.js in movedPinnedItem

It turns out that my bandaid fix to just get the ball rolling perfectly
does what I need it to do when we have a length discrepancy. So we are
getting much closer to the final product, just need lots of testing

* Cleaning up code standards kinds of things

* Yay, this fixes the popover issue

I hope this is the right "vue" way to do things, because I tried a bunch
of other things that definately were not the right way to do it. And
this appears to work too

* ** Partial Work ** Starting tests on api call for draggable items

Doesn't work, doesn't compile so don't include in PR!

* Test failing still...

This is worth a save. The api call grabs the seasonal items too, so we
can't get away from using the common functions and calls here to get the
actual list of items

* Okay have the first test passing

Need to clean up my linter problems though

* Planning out the next two tests and fixing my format problems

* 2nd Test case written, this time with the "more" odd case

* Making sure that we didn't mess with pinned items

* Huh... this test doesn't give me the expected result

Drat, I guess I found a bug

* Throw an error when we put garbage in our api call.

Well, before we got user.pinnedItemsOrder filled with a bunch of "null"
entries which is not ideal. it still worked, but isn't this confusing
enough already?

* Cleaning up the multitude of linting problems thanks gulp :)

* Writing tests for inAppRewards.js, but something is wrong

* Fixing my linting errors in inAppRewards tests

These tests still do not run though, so they may fail and I would not
know

* Applying Negue's fixes to inAppRewards.js test

It never occured to me that we shouldn't try to reach the database while
in the common tests. Well, we shouldn't do that, we should use the
common.helpers instead. Thanks!
2018-04-13 15:22:06 +02:00
Tyler Nychka
5449652bd2 pinned items fixes #10012 (#10216)
* Don't unpin non-gear items

Assumes that multiple of bundles, quests, eggs, potions can be bought

* Added tests

* Changed type checking and made variables global

* Lint fix
2018-04-13 15:19:44 +02:00
Philip Karpiak
c12ae9ea25 Fix #10202 - Send DELETE request when detaching social auth (#10207) 2018-04-13 15:16:49 +02:00
greenkeeper[bot]
734a300b92 fix(package): update sass-loader to version 7.0.0 (#10250) 2018-04-13 15:15:08 +02:00
negue
1109ae308d convert buyQuest (gold) to the purchase refactoring / check quantity to be a number (#10244) 2018-04-13 15:14:51 +02:00
negue
8f1d241e83 if a pet is still hatchable show the hatchable - icon instead of the "you already own the mount"-icon (#10243) 2018-04-13 15:13:42 +02:00
Matteo Pagliazzi
acbca4d1dc upgrade deps 2018-04-12 21:55:24 +02:00
Matteo Pagliazzi
1ea9be8aa2 Preparatory Work for Smaller user doc (WIP) (#10245)
* protect all paths in user.pre(save using this.isDirectSelected to see if a field is available

* fix linting

* authWithHeaders: specify user fields to exclude instead of the ones to include, add comments, doc and improve test

* add more options to unit helper generateReq and add tests for excluding fields in authWithHeaders
2018-04-12 21:17:47 +02:00
Sabe Jones
ace02893e5 4.37.1 2018-04-12 18:38:18 +00:00
Sabe Jones
1c3e043fac chore(i18n): update locales 2018-04-12 18:37:36 +00:00
Matteo Pagliazzi
71c9e7a685 Tasks Modal: add setter for repeatsOn (#10247)
* fix for 10236, add setter to repeatsOn

* remove console.log
2018-04-12 13:30:56 -05:00
Sabe Jones
fa945c7689 Merge branch 'release' into develop 2018-04-11 01:38:10 +00:00
Sabe Jones
c54ce96033 4.37.0 2018-04-11 01:37:46 +00:00
Sabe Jones
85c4e93763 chore(i18n): update locales 2018-04-11 01:37:27 +00:00
SabreCat
25e5e78373 chore(sprites): compile 2018-04-11 01:33:15 +00:00
SabreCat
06181d0a1a feat(content): Squirrel Pet Quest 2018-04-11 01:32:49 +00:00
Matteo Pagliazzi
d5a8259fdb fix members modals (#10240) 2018-04-10 13:27:06 +02:00
Isaac Lim
9db7141853 Added meta image for social media sharing (#10193)
* Add meta image for social media sharing

* Meta Image in Images

* Update index.html
2018-04-09 08:34:12 +02:00
Brian Fenton
ec2a1927a0 adding name attribute to radio inputs so browser inforces selecting a single item from the named set (#10236) 2018-04-09 08:31:33 +02:00
Matteo Pagliazzi
1c1b0f00ad reorganize payments files (#10235) 2018-04-08 16:27:03 +02:00
Alys
fb4d3e44d3 improve code and tests for banned words and slurs (#10211)
* remove removePunctuationFromString function from test code

It's not needed now that the test banned words don't contain underscores.

* prevent tests accidentally throwing messageGroupChatSpam

This commit makes the user for most tests have contributor tiers so
that the user can't trigger the messageGroupChatSpam error message
(for posting messages too quickly).

This is useful when some of the tests fail due to broken code
because that makes more messages be posted than expected. If the user
doesn't have tiers, the messageGroupChatSpam error message would be
triggered, which gives misleading information about the test failure.

* add tests for banned swear and slur words posted in mixed case

* allow banned word error message to show bad words in the same case the user typed them

* stop using randomly-chosen real banned words in tests

The test modified in this commit had been using real banned words,
which meant that those words were being displayed to the contributors
when the test failed.

NB the 'check all banned words are matched' test also uses the real
banned words but the test failure messages don't show the words.

* improve translatability of bannedWordUsed error message
2018-04-08 15:31:37 +02:00
Alys
37fd062cf9 increase Hourglasses and gemCapExtra promptly when multi-month subscription renews - fixes #4819 (#10147)
* allow Hourglasses and gemCapExtra to increase promptly after a multi-month subscription has renewed

* fix existing Hourglass and Gem Cap tests that were wrong

The scenario originally used for these two tests was a six-month recurring
subscription (you can tell that from the starting offset having a non-zero value).
For recurring subscriptions, we do NOT want to increase the consecutive month
benefits as soon as the sixth month starts because the user has already been
given a full six months' benefits in advance and they might cancel the
subscription before it renews later in the sixth month.
Therefore we want to give the extra benefits at the beginning of the seventh
month (ideally we'd give them mid-month in the sixth month when the renewal
happens but we don't have support for tracking renewal dates).
So, the two changed tests were actually not correct for the case
where the offset started as non-zero.

These tests are correct for one-month recurring subscriptions (when the offset
is never set to anything above zero). The user isn't meant to get any consecutive
month benefits until a multiple of 3 months has been reached.

* add tests for one-month recurring subscription before 3x months are reached

* add tests for 3-, 6-, and 12-month recurring subscriptions

The 3-month tests are the most thorough, stepping through the
expected start and end values of consecutive data for a 7-month
range.

The 6-month tests are a bit less thorough since the same code is
used for all multi-month periods.
The discount Google subscription code is used to ensure we keep
support for it.

The 12-month tests are less thorough still, since again the same
code is used.

I'm about to try some more tests with `useFakeTimers`, which should
be a better way to test the code since they won't rely on me having
set the initial values correctly for each test. :) But I wanted to
work through these cases manually first to ensure my understanding
of how the values should change does actually match the code.

* add tests for 1-, 3-, 6-, and 12-month recurring subscriptions using clock changes to simulate passing months

Also fixed the clock call in an unrelated test because it was forming
the date incorrectly (`unix()` can't be used to create a date).

Also changed email@email.email to email@example.com because
email@email.email is potentially a real email address.

* add tests for 3-month gift subscriptions - no extra consecutive benefits given

* add tests for consecutive benefits for 6-month recurring subscription that has incorrect consecutive month data because it started before issue #4819 was fixed

* fix lint errors

* remove outdated subscription tests
2018-04-08 15:26:25 +02:00
Matteo Pagliazzi
485c3c5c46 disable failing test 2018-04-08 14:58:51 +02:00
negue
5007393f24 enable hair style edit during intro (#10227) 2018-04-08 14:52:26 +02:00
Alys
e111ac730c enable translated pet names in hatching success message (#10231) 2018-04-08 14:50:36 +02:00
Philip Karpiak
e7c78eabce Wrap creator icon + text in @click event (#10221)
Perviously only clicking the icon would activate tabs in the creator, which was confusing
2018-04-06 12:56:33 -05:00
Philip Karpiak
5da7699548 Add tooltip to character buff icon (#10156)
* Add tooltip to character buff icon

* Add tooltips for task streak, challenge and broken challenge

* Add tooltips for task menu and due date

* Challenge icon tooltip displays the challenge short name
2018-04-06 12:53:39 -05:00
Keith Holliday
f42955a0ba Added initial account banned modal (#9868)
* Added initial account banned modal

* Fixed check for non logged in user
2018-04-06 08:33:38 -05:00
Neel Mehta
558dd2e4bf fix hippo-crite scroll image size 2018-03-27 23:04:34 -04:00
1383 changed files with 55551 additions and 39975 deletions

View File

@@ -1,7 +1,7 @@
[//]: # (Note: See http://habitica.wikia.com/wiki/Using_Your_Local_Install_to_Modify_Habitica%27s_Website_and_API for more info)
[//]: # (Put Issue # or URL here, if applicable. This will automatically close the issue if your PR is merged in)
Fixes put_issue_url_here
[//]: # (Put Issue # here, if applicable. This will automatically close the issue if your PR is merged in)
Fixes put_#_and_issue_numer_here
### Changes
[//]: # (Describe the changes that were made in detail here. Include pictures if necessary)

View File

@@ -17,3 +17,4 @@ CHANGELOG.md
newrelic_agent.log
*.swp
*.swx
website/raw_sprites/**

View File

@@ -20,8 +20,9 @@ env:
- DISABLE_REQUEST_LOGGING=true
matrix:
- TEST="lint"
- TEST="test:api-v3:unit" REQUIRES_SERVER=true COVERAGE=true
- TEST="test:api:unit" REQUIRES_SERVER=true COVERAGE=true
- TEST="test:api-v3:integration" REQUIRES_SERVER=true COVERAGE=true
- TEST="test:api-v4:integration" REQUIRES_SERVER=true COVERAGE=true
- TEST="test:sanity"
- TEST="test:content" COVERAGE=true
- TEST="test:common" COVERAGE=true

View File

@@ -1,18 +1,29 @@
FROM node:8
# Install global packages
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 https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
RUN cp config.json.example config.json
RUN npm install
# Create Build dir
RUN mkdir -p ./website/build
# Start Habitica
EXPOSE 3000
CMD ["npm", "start"]
FROM node:8
ENV ADMIN_EMAIL admin@habitica.com
ENV AMAZON_PAYMENTS_CLIENT_ID amzn1.application-oa2-client.68ed9e6904ef438fbc1bf86bf494056e
ENV AMAZON_PAYMENTS_SELLER_ID AMQ3SB4SG5E91
ENV AMPLITUDE_KEY e8d4c24b3d6ef3ee73eeba715023dd43
ENV BASE_URL https://habitica.com
ENV FACEBOOK_KEY 128307497299777
ENV GA_ID UA-33510635-1
ENV GOOGLE_CLIENT_ID 1035232791481-32vtplgnjnd1aufv3mcu1lthf31795fq.apps.googleusercontent.com
ENV NODE_ENV production
ENV STRIPE_PUB_KEY pk_85fQ0yMECHNfHTSsZoxZXlPSwSNfA
# Install global packages
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 release https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
RUN npm install
RUN gulp build:prod --force
# Create Build dir
RUN mkdir -p ./website/build
# Start Habitica
EXPOSE 3000
CMD ["node", "./website/transpiled-babel/index.js"]

18
Dockerfile-Dev Normal file
View File

@@ -0,0 +1,18 @@
FROM node:8
# Install global packages
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 https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
RUN cp config.json.example config.json
RUN npm install
# Create Build dir
RUN mkdir -p ./website/build
# Start Habitica
EXPOSE 3000
CMD ["npm", "start"]

View File

@@ -1,29 +0,0 @@
FROM node:8
ENV ADMIN_EMAIL admin@habitica.com
ENV AMAZON_PAYMENTS_CLIENT_ID amzn1.application-oa2-client.68ed9e6904ef438fbc1bf86bf494056e
ENV AMAZON_PAYMENTS_SELLER_ID AMQ3SB4SG5E91
ENV AMPLITUDE_KEY e8d4c24b3d6ef3ee73eeba715023dd43
ENV BASE_URL https://habitica.com
ENV FACEBOOK_KEY 128307497299777
ENV GA_ID UA-33510635-1
ENV GOOGLE_CLIENT_ID 1035232791481-32vtplgnjnd1aufv3mcu1lthf31795fq.apps.googleusercontent.com
ENV NODE_ENV production
ENV STRIPE_PUB_KEY pk_85fQ0yMECHNfHTSsZoxZXlPSwSNfA
# Install global packages
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.35.1 https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
RUN npm install
RUN gulp build:prod --force
# Create Build dir
RUN mkdir -p ./website/build
# Start Habitica
EXPOSE 3000
CMD ["node", "./website/transpiled-babel/index.js"]

View File

@@ -10,4 +10,3 @@ We need more programmers! Your assistance will be greatly appreciated.
For an introduction to the technologies used and how the software is organized, refer to [Guidance for Blacksmiths](http://habitica.wikia.com/wiki/Guidance_for_Blacksmiths).
To set up a local install of Habitica for development and testing on various platforms, see [Setting up Habitica Locally](http://habitica.wikia.com/wiki/Setting_up_Habitica_Locally).

View File

@@ -98,9 +98,9 @@
},
"ITUNES_SHARED_SECRET": "aaaabbbbccccddddeeeeffff00001111",
"EMAILS" : {
"COMMUNITY_MANAGER_EMAIL" : "leslie@habitica.com",
"COMMUNITY_MANAGER_EMAIL" : "admin@habitica.com",
"TECH_ASSISTANCE_EMAIL" : "admin@habitica.com",
"PRESS_ENQUIRY_EMAIL" : "leslie@habitica.com"
"PRESS_ENQUIRY_EMAIL" : "admin@habitica.com"
},
"LOGGLY" : {
"TOKEN" : "example-token",
@@ -113,5 +113,5 @@
"CLOUDKARAFKA_PASSWORD": "",
"CLOUDKARAFKA_TOPIC_PREFIX": ""
},
"STACK_IMPACT_KEY": "aaaabbbbccccddddeeeeffffgggg111100002222"
"MIGRATION_CONNECT_STRING": "mongodb://localhost:27017/habitrpg?auto_reconnect=true"
}

View File

@@ -0,0 +1,100 @@
import max from 'lodash/max';
import mean from 'lodash/mean';
import monk from 'monk';
import round from 'lodash/round';
import sum from 'lodash/sum';
/*
* Output data on subscribers' task histories, formatted for CSV.
* User ID,Count of Dailies,Count of Habits,Total History Size,Max History Size,Mean History Size,Median History Size
*/
const connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
let dbUsers = monk(connectionString).get('users', { castIds: false });
let dbTasks = monk(connectionString).get('tasks', { castIds: false });
function usersReport () {
let allHistoryLengths = [];
console.info('User ID,Count of Dailies,Count of Habits,Total History Size,Max History Size,Mean History Size,Median History Size');
dbUsers.find(
{
$and:
[
{'purchased.plan.planId': {$ne:null}},
{'purchased.plan.planId': {$ne:''}},
],
$or:
[
{'purchased.plan.dateTerminated': null},
{'purchased.plan.dateTerminated': ''},
{'purchased.plan.dateTerminated': {$gt:new Date()}},
],
},
{
fields: {_id: 1},
}
).each((user, {close, pause, resume}) => {
let historyLengths = [];
let habitCount = 0;
let dailyCount = 0;
pause();
return dbTasks.find(
{
userId: user._id,
$or:
[
{type: 'habit'},
{type: 'daily'},
],
},
{
fields: {
type: 1,
history: 1,
},
}
).each((task) => {
if (task.type === 'habit') {
habitCount++;
}
if (task.type === 'daily') {
dailyCount++;
}
if (task.history.length > 0) {
allHistoryLengths.push(task.history.length);
historyLengths.push(task.history.length);
}
}).then(() => {
const totalHistory = sum(historyLengths);
const maxHistory = historyLengths.length > 0 ? max(historyLengths) : 0;
const meanHistory = historyLengths.length > 0 ? round(mean(historyLengths)) : 0;
const medianHistory = historyLengths.length > 0 ? median(historyLengths) : 0;
console.info(`${user._id},${dailyCount},${habitCount},${totalHistory},${maxHistory},${meanHistory},${medianHistory}`);
resume();
});
}).then(() => {
console.info(`Total Subscriber History Entries: ${sum(allHistoryLengths)}`);
console.info(`Largest History Size: ${max(allHistoryLengths)}`);
console.info(`Mean History Size: ${round(mean(allHistoryLengths))}`);
console.info(`Median History Size: ${median(allHistoryLengths)}`);
return process.exit(0);
});
}
function median(values) { // https://gist.github.com/caseyjustus/1166258
values.sort( function(a,b) {return a - b;} );
var half = Math.floor(values.length/2);
if (values.length % 2) {
return values[half];
}
else {
return (values[half-1] + values[half]) / 2.0;
}
}
module.exports = usersReport;

View File

@@ -25,7 +25,7 @@ services:
- mongo
mongo:
image: mongo
image: mongo:3.4
ports:
- "27017:27017"
networks:

View File

@@ -165,9 +165,9 @@ gulp.task('test:content:safe', gulp.series('test:prepare:build', (cb) => {
pipe(runner);
}));
gulp.task('test:api-v3:unit', (done) => {
gulp.task('test:api:unit', (done) => {
let runner = exec(
testBin('node_modules/.bin/istanbul cover --dir coverage/api-v3-unit --report lcovonly node_modules/mocha/bin/_mocha -- test/api/v3/unit --recursive --require ./test/helpers/start-server'),
testBin('node_modules/.bin/istanbul cover --dir coverage/api-unit node_modules/mocha/bin/_mocha -- test/api/unit --recursive --require ./test/helpers/start-server'),
(err) => {
if (err) {
process.exit(1);
@@ -179,8 +179,8 @@ gulp.task('test:api-v3:unit', (done) => {
pipe(runner);
});
gulp.task('test:api-v3:unit:watch', () => {
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:unit:watch', () => {
return gulp.watch(['website/server/libs/*', 'test/api/v3/unit/**/*', 'website/server/controllers/**/*'], gulp.series('test:api:unit', done => done()));
});
gulp.task('test:api-v3:integration', (done) => {
@@ -215,17 +215,43 @@ gulp.task('test:api-v3:integration:separate-server', (done) => {
pipe(runner);
});
gulp.task('test:api-v4:integration', (done) => {
let runner = exec(
testBin('node_modules/.bin/istanbul cover --dir coverage/api-v4-integration --report lcovonly node_modules/mocha/bin/_mocha -- test/api/v4 --recursive --require ./test/helpers/start-server'),
{maxBuffer: 500 * 1024},
(err) => {
if (err) {
process.exit(1);
}
done();
}
);
pipe(runner);
});
gulp.task('test:api-v4:integration:separate-server', (done) => {
let runner = exec(
testBin('mocha test/api/v4 --recursive --require ./test/helpers/start-server', 'LOAD_SERVER=0'),
{maxBuffer: 500 * 1024},
(err) => done(err)
);
pipe(runner);
});
gulp.task('test', gulp.series(
'test:sanity',
'test:content',
'test:common',
'test:api-v3:unit',
'test:api:unit',
'test:api-v3:integration',
'test:api-v4:integration',
done => done()
));
gulp.task('test:api-v3', gulp.series(
'test:api-v3:unit',
'test:api:unit',
'test:api-v3:integration',
done => done()
));
));

View File

@@ -0,0 +1,52 @@
// @migrationName = 'MigrateGroupChat';
// @authorName = 'TheHollidayInn'; // in case script author needs to know when their ...
// @authorUuid = ''; // ... own data is done
/*
* This migration moves chat off of groups and into their own model
*/
import { model as Group } from '../../website/server/models/group';
import { model as Chat } from '../../website/server/models/chat';
async function moveGroupChatToModel (skip = 0) {
const groups = await Group.find({})
.limit(50)
.skip(skip)
.sort({ _id: -1 })
.exec();
if (groups.length === 0) {
console.log('End of groups');
process.exit();
}
const promises = groups.map(group => {
const chatpromises = group.chat.map(message => {
const newChat = new Chat();
Object.assign(newChat, message);
newChat._id = message.id;
newChat.groupId = group._id;
return newChat.save();
});
group.chat = [];
chatpromises.push(group.save());
return chatpromises;
});
const reducedPromises = promises.reduce((acc, curr) => {
acc = acc.concat(curr);
return acc;
}, []);
console.log(reducedPromises);
await Promise.all(reducedPromises);
moveGroupChatToModel(skip + 50);
}
module.exports = moveGroupChatToModel;

View File

@@ -0,0 +1,107 @@
import monk from 'monk';
import nconf from 'nconf';
import stripePayments from '../../website/server/libs/payments/stripe';
/*
* Ensure that group plan billing is accurate by doing the following:
* 1. Correct the memberCount in all paid groups whose counts are wrong
* 2. Where the above uses Stripe, update their subscription counts in Stripe
*
* Provides output on what groups were fixed, which can be piped to CSV.
*/
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING');
let dbGroups = monk(CONNECTION_STRING).get('groups', { castIds: false });
let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false });
async function fixGroupPlanMembers () {
console.info('Group ID, Customer ID, Plan ID, Quantity, Recorded Member Count, Actual Member Count');
let groupPlanCount = 0;
let fixedGroupCount = 0;
dbGroups.find(
{
$and:
[
{'purchased.plan.planId': {$ne: null}},
{'purchased.plan.planId': {$ne: ''}},
{'purchased.plan.customerId': {$ne: 'cus_9f0DV4g7WHRzpM'}}, // Demo groups
{'purchased.plan.customerId': {$ne: 'cus_9maalqDOFTrvqx'}},
],
$or:
[
{'purchased.plan.dateTerminated': null},
{'purchased.plan.dateTerminated': ''},
],
},
{
fields: {
memberCount: 1,
'purchased.plan': 1,
},
}
).each(async (group, {close, pause, resume}) => { // eslint-disable-line no-unused-vars
pause();
groupPlanCount++;
const canonicalMemberCount = await dbUsers.count(
{
$or:
[
{'party._id': group._id},
{guilds: group._id},
],
}
);
const incorrectMemberCount = group.memberCount !== canonicalMemberCount;
const isMonthlyPlan = group.purchased.plan.planId === 'group_monthly';
const quantityMismatch = group.purchased.plan.quantity !== group.memberCount + 2;
const incorrectQuantity = isMonthlyPlan && quantityMismatch;
if (!incorrectMemberCount && !incorrectQuantity) {
resume();
return;
}
console.info(`${group._id}, ${group.purchased.plan.customerId}, ${group.purchased.plan.planId}, ${group.purchased.plan.quantity}, ${group.memberCount}, ${canonicalMemberCount}`);
const groupUpdate = await dbGroups.update(
{ _id: group._id },
{
$set: {
memberCount: canonicalMemberCount,
},
}
);
if (!groupUpdate) return;
fixedGroupCount++;
if (group.purchased.plan.paymentMethod === 'Stripe') {
await stripePayments.chargeForAdditionalGroupMember(group);
await dbGroups.update(
{_id: group._id},
{$set: {'purchased.plan.quantity': canonicalMemberCount + 2}}
);
}
if (incorrectQuantity) {
await dbGroups.update(
{_id: group._id},
{$set: {'purchased.plan.quantity': canonicalMemberCount + 2}}
);
}
resume();
}).then(() => {
console.info(`Fixed ${fixedGroupCount} out of ${groupPlanCount} active Group Plans`);
return process.exit(0);
}).catch((err) => {
console.log(err);
return process.exit(1);
});
}
module.exports = fixGroupPlanMembers;

View File

@@ -17,5 +17,5 @@ function setUpServer () {
setUpServer();
// Replace this with your migration
const processUsers = require('./20180125_clean_new_notifications.js');
const processUsers = require('./tasks/habits-one-history-entry-per-day-challenges.js');
processUsers();

View File

@@ -0,0 +1,138 @@
// const migrationName = 'habits-one-history-entry-per-day';
// const authorName = 'paglias'; // in case script author needs to know when their ...
// const authorUuid = 'ed4c688c-6652-4a92-9d03-a5a79844174a'; // ... own data is done
/*
* Iterates over all habits and condense multiple history entries for the same day into a single entry
*/
const monk = require('monk');
const _ = require('lodash');
const moment = require('moment');
const connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
const dbTasks = monk(connectionString).get('tasks', { castIds: false });
function processChallengeHabits (lastId) {
let query = {
'challenge.id': {$exists: true},
userId: {$exists: false},
type: 'habit',
};
if (lastId) {
query._id = {
$gt: lastId,
};
}
dbTasks.find(query, {
sort: {_id: 1},
limit: 500,
})
.then(updateChallengeHabits)
.catch((err) => {
console.log(err);
return exiting(1, `ERROR! ${ err}`);
});
}
let progressCount = 1000;
let count = 0;
function updateChallengeHabits (habits) {
if (!habits || habits.length === 0) {
console.warn('All appropriate challenge habits found and modified.');
displayData();
return;
}
let habitsPromises = habits.map(updateChallengeHabit);
let lastHabit = habits[habits.length - 1];
return Promise.all(habitsPromises)
.then(() => {
return processChallengeHabits(lastHabit._id);
});
}
function updateChallengeHabit (habit) {
count++;
if (habit && habit.history && habit.history.length > 0) {
// First remove missing entries
habit.history = habit.history.filter(entry => Boolean(entry));
habit.history = _.chain(habit.history)
// processes all entries to identify an up or down score
.forEach((entry, index) => {
if (index === 0) { // first entry doesn't have a previous one
// first value < 0 identifies a negative score as the first action
entry.scoreDirection = entry.value >= 0 ? 'up' : 'down';
} else {
// could be missing if the previous entry was null and thus excluded
const previousEntry = habit.history[index - 1];
const previousValue = previousEntry.value;
entry.scoreDirection = entry.value > previousValue ? 'up' : 'down';
}
})
.groupBy(entry => { // group entries by aggregateBy
return moment(entry.date).format('YYYYMMDD');
})
.toPairs() // [key, entry]
.sortBy(([key]) => key) // sort by date
.map(keyEntryPair => {
let entries = keyEntryPair[1]; // 1 is entry, 0 is key
let scoredUp = 0;
let scoredDown = 0;
entries.forEach(entry => {
if (entry.scoreDirection === 'up') {
scoredUp += 1;
} else {
scoredDown += 1;
}
// delete the unnecessary scoreDirection and scoreNotes prop
delete entry.scoreDirection;
delete entry.scoreNotes;
});
return {
date: Number(entries[entries.length - 1].date), // keep last value
value: entries[entries.length - 1].value, // keep last value,
scoredUp,
scoredDown,
};
})
.value();
return dbTasks.update({_id: habit._id}, {
$set: {history: habit.history},
});
}
if (count % progressCount === 0) console.warn(`${count } habits 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 = processChallengeHabits;

View File

@@ -0,0 +1,163 @@
const migrationName = 'habits-one-history-entry-per-day';
const authorName = 'paglias'; // in case script author needs to know when their ...
const authorUuid = 'ed4c688c-6652-4a92-9d03-a5a79844174a'; // ... own data is done
/*
* Iterates over all habits and condense multiple history entries for the same day into a single entry
*/
const monk = require('monk');
const _ = require('lodash');
const moment = require('moment');
const connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
const dbTasks = monk(connectionString).get('tasks', { castIds: false });
const dbUsers = monk(connectionString).get('users', { castIds: false });
function processUsers (lastId) {
let query = {
migration: {$ne: migrationName},
};
if (lastId) {
query._id = {
$gt: lastId,
};
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 50, // just 50 users per time since we have to process all their habits as well
fields: ['_id', 'preferences.timezoneOffset', 'preferences.dayStart'],
})
.then(updateUsers)
.catch((err) => {
console.log(err);
return exiting(1, `ERROR! ${ err}`);
});
}
let progressCount = 1000;
let count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users and their tasks found and modified.');
displayData();
return;
}
let usersPromises = users.map(updateUser);
let lastUser = users[users.length - 1];
return Promise.all(usersPromises)
.then(() => {
return processUsers(lastUser._id);
});
}
function updateHabit (habit, timezoneOffset, dayStart) {
if (habit && habit.history && habit.history.length > 0) {
// First remove missing entries
habit.history = habit.history.filter(entry => Boolean(entry));
habit.history = _.chain(habit.history)
// processes all entries to identify an up or down score
.forEach((entry, index) => {
if (index === 0) { // first entry doesn't have a previous one
// first value < 0 identifies a negative score as the first action
entry.scoreDirection = entry.value >= 0 ? 'up' : 'down';
} else {
// could be missing if the previous entry was null and thus excluded
const previousEntry = habit.history[index - 1];
const previousValue = previousEntry.value;
entry.scoreDirection = entry.value > previousValue ? 'up' : 'down';
}
})
.groupBy(entry => { // group entries by aggregateBy
const entryDate = moment(entry.date).zone(timezoneOffset || 0);
if (entryDate.hour() < dayStart) entryDate.subtract(1, 'day');
return entryDate.format('YYYYMMDD');
})
.toPairs() // [key, entry]
.sortBy(([key]) => key) // sort by date
.map(keyEntryPair => {
let entries = keyEntryPair[1]; // 1 is entry, 0 is key
let scoredUp = 0;
let scoredDown = 0;
entries.forEach(entry => {
if (entry.scoreDirection === 'up') {
scoredUp += 1;
} else {
scoredDown += 1;
}
// delete the unnecessary scoreDirection and scoreNotes prop
delete entry.scoreDirection;
delete entry.scoreNotes;
});
return {
date: Number(entries[entries.length - 1].date), // keep last value
value: entries[entries.length - 1].value, // keep last value,
scoredUp,
scoredDown,
};
})
.value();
return dbTasks.update({_id: habit._id}, {
$set: {history: habit.history},
});
}
}
function updateUser (user) {
count++;
const timezoneOffset = user.preferences.timezoneOffset;
const dayStart = user.preferences.dayStart;
if (count % progressCount === 0) console.warn(`${count } ${ user._id}`);
if (user._id === authorUuid) console.warn(`${authorName } being processed`);
return dbTasks.find({
type: 'habit',
userId: user._id,
})
.then(habits => {
return Promise.all(habits.map(habit => updateHabit(habit, timezoneOffset, dayStart)));
})
.then(() => {
return dbUsers.update({_id: user._id}, {
$set: {migration: migrationName},
});
})
.catch((err) => {
console.log(err);
return exiting(1, `ERROR! ${ err}`);
});
}
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 = processUsers;

View File

@@ -7,7 +7,7 @@ let authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
*/
let monk = require('monk');
let connectionString = 'mongodb://sabrecat:z8e8jyRA8CTofMQ@ds013393-a0.mlab.com:13393/habitica?auto_reconnect=true';
let connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true';
let dbTasks = monk(connectionString).get('tasks', { castIds: false });
function processTasks (lastId) {

View File

@@ -1,15 +1,17 @@
const migrationName = 'mystery-items-201802.js'; // Update per month
import monk from 'monk';
import nconf from 'nconf';
const migrationName = 'mystery-items-201806.js'; // Update per month
const authorName = 'Sabe'; // in case script author needs to know when their ...
const authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
/*
* Award this month's mystery items to subscribers
*/
const MYSTERY_ITEMS = ['back_mystery_201803', 'head_mystery_201803'];
const connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
const MYSTERY_ITEMS = ['armor_mystery_201806', 'head_mystery_201806'];
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING');
let monk = require('monk');
let dbUsers = monk(connectionString).get('users', { castIds: false });
let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false });
let UserNotification = require('../../website/server/models/userNotification').model;
function processUsers (lastId) {

View File

@@ -0,0 +1,109 @@
const migrationName = 'remove-social-users-extra-data.js';
const authorName = 'paglias'; // in case script author needs to know when their ...
const authorUuid = 'ed4c688c-6652-4a92-9d03-a5a79844174a'; // ... own data is done
/*
* Remove not needed data from social profiles
*/
const connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
const monk = require('monk');
const dbUsers = monk(connectionString).get('users', { castIds: false });
function processUsers (lastId) {
// specify a query to limit the affected users (empty for all users):
let query = {
migration: {$ne: migrationName},
$or: [
{ 'auth.facebook.id': { $exists: true } },
{ 'auth.google.id': { $exists: true } },
],
};
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}`);
});
}
let progressCount = 1000;
let count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
displayData();
return;
}
let userPromises = users.map(updateUser);
let lastUser = users[users.length - 1];
return Promise.all(userPromises)
.then(() => {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
const isFacebook = user.auth.facebook && user.auth.facebook.id;
const isGoogle = user.auth.google && user.auth.google.id;
const update = { $set: {} };
if (isFacebook) {
update.$set['auth.facebook'] = {
id: user.auth.facebook.id,
emails: user.auth.facebook.emails,
};
}
if (isGoogle) {
update.$set['auth.google'] = {
id: user.auth.google.id,
emails: user.auth.google.emails,
};
}
dbUsers.update({
_id: user._id,
}, update);
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

@@ -1,4 +1,4 @@
let migrationName = '20180102_takeThis.js'; // Update per month
let migrationName = '20180702_takeThis.js'; // Update per month
let authorName = 'Sabe'; // in case script author needs to know when their ...
let authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
@@ -6,15 +6,16 @@ let authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
* Award Take This ladder items to participants in this month's challenge
*/
let monk = require('monk');
let connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
let dbUsers = monk(connectionString).get('users', { castIds: false });
import monk from 'monk';
import nconf from 'nconf';
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING'); // FOR TEST DATABASE
let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false });
function processUsers (lastId) {
// specify a query to limit the affected users (empty for all users):
let query = {
migration: {$ne: migrationName},
challenges: {$in: ['5f70ce5b-2d82-4114-8e44-ca65615aae62']}, // Update per month
challenges: {$in: ['f0481f95-1dde-4ae7-a876-d19502a45d61']}, // Update per month
};
if (lastId) {

13063
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,48 +1,48 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "4.36.0",
"version": "4.51.2",
"main": "./website/server/index.js",
"dependencies": {
"@slack/client": "^3.8.1",
"accepts": "^1.3.5",
"amazon-payments": "^0.2.6",
"amazon-payments": "^0.2.7",
"amplitude": "^3.5.0",
"apidoc": "^0.17.5",
"autoprefixer": "^8.1.0",
"aws-sdk": "^2.211.0",
"autoprefixer": "^8.5.0",
"aws-sdk": "^2.239.1",
"axios": "^0.18.0",
"axios-progress-bar": "^1.1.8",
"babel-core": "^6.0.0",
"babel-eslint": "^8.2.2",
"axios-progress-bar": "^1.2.0",
"babel-core": "^6.26.3",
"babel-eslint": "^8.2.3",
"babel-loader": "^7.1.4",
"babel-plugin-syntax-async-functions": "^6.13.0",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
"babel-plugin-transform-object-rest-spread": "^6.16.0",
"babel-plugin-transform-regenerator": "^6.16.1",
"babel-polyfill": "^6.6.1",
"babel-preset-es2015": "^6.6.0",
"babel-register": "^6.6.0",
"babel-runtime": "^6.11.6",
"bcrypt": "^1.0.2",
"body-parser": "^1.15.0",
"bootstrap": "^4.0.0",
"bootstrap-vue": "^2.0.0-rc.2",
"bcrypt": "^2.0.0",
"body-parser": "^1.18.3",
"bootstrap": "^4.1.1",
"bootstrap-vue": "^2.0.0-rc.9",
"compression": "^1.7.2",
"cookie-session": "^1.2.0",
"coupon-code": "^0.4.5",
"cross-env": "^5.1.4",
"cross-env": "^5.1.5",
"css-loader": "^0.28.11",
"csv-stringify": "^2.0.4",
"csv-stringify": "^2.1.0",
"cwait": "^1.1.1",
"domain-middleware": "~0.1.0",
"express": "^4.16.3",
"express-basic-auth": "^1.1.4",
"express-validator": "^5.0.3",
"express-basic-auth": "^1.1.5",
"express-validator": "^5.2.0",
"extract-text-webpack-plugin": "^3.0.2",
"glob": "^7.1.2",
"got": "^8.3.0",
"got": "^8.3.1",
"gulp": "^4.0.0",
"gulp-babel": "^7.0.1",
"gulp-imagemin": "^4.1.0",
@@ -50,64 +50,64 @@
"gulp.spritesmith": "^6.9.0",
"habitica-markdown": "^1.3.0",
"hellojs": "^1.15.1",
"html-webpack-plugin": "^3.0.0",
"html-webpack-plugin": "^3.2.0",
"image-size": "^0.6.2",
"in-app-purchase": "^1.8.9",
"intro.js": "^2.6.0",
"in-app-purchase": "^1.9.4",
"intro.js": "^2.9.3",
"jquery": ">=3.0.0",
"js2xmlparser": "^3.0.0",
"lodash": "^4.17.4",
"memwatch-next": "^0.3.0",
"lodash": "^4.17.10",
"merge-stream": "^1.0.0",
"method-override": "^2.3.5",
"moment": "^2.21.0",
"moment": "^2.22.1",
"moment-recur": "^1.0.7",
"mongoose": "^5.0.10",
"mongoose": "^5.1.2",
"morgan": "^1.7.0",
"nconf": "^0.10.0",
"node-gcm": "^0.14.4",
"node-sass": "^4.8.2",
"nodemailer": "^4.6.3",
"ora": "^2.0.0",
"node-sass": "^4.9.0",
"nodemailer": "^4.6.4",
"ora": "^2.1.0",
"pageres": "^4.1.1",
"passport": "^0.4.0",
"passport-facebook": "^2.0.0",
"passport-google-oauth20": "1.0.0",
"paypal-ipn": "3.0.0",
"paypal-rest-sdk": "^1.8.1",
"popper.js": "^1.14.1",
"popper.js": "^1.14.3",
"postcss-easy-import": "^3.0.0",
"ps-tree": "^1.0.0",
"pug": "^2.0.1",
"pug": "^2.0.3",
"push-notify": "git://github.com/habitrpg/push-notify.git#6bc2b5fdb1bdc9649b9ec1964d79ca50187fc8a9",
"pusher": "^1.3.0",
"rimraf": "^2.4.3",
"sass-loader": "^6.0.7",
"shelljs": "^0.8.1",
"stackimpact": "^1.2.1",
"stripe": "^5.5.0",
"superagent": "^3.4.3",
"sass-loader": "^7.0.0",
"shelljs": "^0.8.2",
"stripe": "^5.9.0",
"superagent": "^3.8.3",
"svg-inline-loader": "^0.8.0",
"svg-url-loader": "^2.3.2",
"svgo": "^1.0.5",
"svgo-loader": "^2.1.0",
"universal-analytics": "^0.4.16",
"update": "^0.7.4",
"upgrade": "^1.1.0",
"url-loader": "^1.0.0",
"useragent": "^2.1.9",
"uuid": "^3.0.1",
"validator": "^9.4.1",
"vinyl-buffer": "^1.0.1",
"vue": "^2.5.16",
"vue-loader": "^14.2.1",
"vue-loader": "^14.2.2",
"vue-mugen-scroll": "^0.2.1",
"vue-router": "^3.0.0",
"vue-style-loader": "^4.0.2",
"vue-style-loader": "^4.1.0",
"vue-template-compiler": "^2.5.16",
"vuedraggable": "^2.15.0",
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#5d237615463a84a23dd6f3f77c6ab577d68593ec",
"webpack": "^3.11.0",
"webpack": "^3.12.0",
"webpack-merge": "^4.0.0",
"winston": "^2.4.1",
"winston": "^2.4.2",
"winston-loggly-bulk": "^2.0.2",
"xml2js": "^0.4.4"
},
@@ -121,9 +121,11 @@
"test": "npm run lint && gulp test && gulp apidoc",
"test:build": "gulp test:prepare:build",
"test:api-v3": "gulp test:api-v3",
"test:api-v3:unit": "gulp test:api-v3:unit",
"test:api:unit": "gulp test:api:unit",
"test:api-v3:integration": "gulp test:api-v3:integration",
"test:api-v3:integration:separate-server": "NODE_ENV=test gulp test:api-v3:integration:separate-server",
"test:api-v4:integration": "gulp test:api-v4:integration",
"test:api-v4:integration:separate-server": "NODE_ENV=test gulp test:api-v4:integration:separate-server",
"test:sanity": "istanbul cover --dir coverage/sanity --report lcovonly node_modules/mocha/bin/_mocha -- test/sanity --recursive",
"test:common": "istanbul cover --dir coverage/common --report lcovonly node_modules/mocha/bin/_mocha -- test/common --recursive",
"test:content": "istanbul cover --dir coverage/content --report lcovonly node_modules/mocha/bin/_mocha -- test/content --recursive",
@@ -141,53 +143,54 @@
"apidoc": "gulp apidoc"
},
"devDependencies": {
"@vue/test-utils": "^1.0.0-beta.12",
"@vue/test-utils": "^1.0.0-beta.16",
"babel-plugin-istanbul": "^4.1.6",
"babel-plugin-syntax-object-rest-spread": "^6.13.0",
"chai": "^4.1.2",
"chai-as-promised": "^7.1.1",
"chalk": "^2.3.2",
"chromedriver": "^2.36.0",
"chalk": "^2.4.1",
"chromedriver": "^2.38.3",
"connect-history-api-fallback": "^1.1.0",
"coveralls": "^3.0.0",
"coveralls": "^3.0.1",
"cross-spawn": "^6.0.5",
"eslint": "^4.19.0",
"eslint": "^4.19.1",
"eslint-config-habitrpg": "^4.0.0",
"eslint-friendly-formatter": "^4.0.0",
"eslint-friendly-formatter": "^4.0.1",
"eslint-loader": "^2.0.0",
"eslint-plugin-html": "^4.0.2",
"eslint-plugin-html": "^4.0.3",
"eslint-plugin-mocha": "^5.0.0",
"eventsource-polyfill": "^0.9.6",
"expect.js": "^0.3.1",
"http-proxy-middleware": "^0.18.0",
"istanbul": "^1.1.0-alpha.1",
"karma": "^2.0.0",
"karma": "^2.0.2",
"karma-babel-preprocessor": "^7.0.0",
"karma-chai-plugins": "^0.9.0",
"karma-chrome-launcher": "^2.2.0",
"karma-coverage": "^1.1.1",
"karma-coverage": "^1.1.2",
"karma-mocha": "^1.3.0",
"karma-mocha-reporter": "^2.2.5",
"karma-sinon-chai": "^1.3.3",
"karma-sinon-chai": "^1.3.4",
"karma-sinon-stub-promise": "^1.0.0",
"karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "0.0.32",
"karma-webpack": "^3.0.0",
"lcov-result-merger": "^2.0.0",
"mocha": "^5.0.4",
"monk": "^6.0.5",
"nightwatch": "^0.9.20",
"puppeteer": "^1.2.0",
"mocha": "^5.1.1",
"monk": "^6.0.6",
"nightwatch": "^0.9.21",
"puppeteer": "^1.4.0",
"require-again": "^2.0.0",
"selenium-server": "^3.11.0",
"sinon": "^4.4.5",
"selenium-server": "^3.12.0",
"sinon": "^4.5.0",
"sinon-chai": "^3.0.0",
"sinon-stub-promise": "^4.0.0",
"webpack-bundle-analyzer": "^2.11.1",
"webpack-bundle-analyzer": "^2.12.0",
"webpack-dev-middleware": "^2.0.5",
"webpack-hot-middleware": "^2.21.2"
"webpack-hot-middleware": "^2.22.2"
},
"optionalDependencies": {
"memwatch-next": "^0.3.0",
"node-rdkafka": "^2.3.0"
}
}

View File

@@ -1,5 +1,5 @@
/* eslint-disable camelcase */
import analyticsService from '../../../../../website/server/libs/analyticsService';
import analyticsService from '../../../../website/server/libs/analyticsService';
import Amplitude from 'amplitude';
import { Visitor } from 'universal-analytics';

View File

@@ -1,19 +1,19 @@
import apiMessages from '../../../../../website/server/libs/apiMessages';
import apiError from '../../../../website/server/libs/apiError';
describe('API Messages', () => {
const message = 'Only public guilds support pagination.';
it('returns an API message', () => {
expect(apiMessages('guildsOnlyPaginate')).to.equal(message);
expect(apiError('guildsOnlyPaginate')).to.equal(message);
});
it('throws if the API message does not exist', () => {
expect(() => apiMessages('iDoNotExist')).to.throw;
expect(() => apiError('iDoNotExist')).to.throw;
});
it('clones the passed variables', () => {
let vars = {a: 1};
sandbox.stub(_, 'clone').returns({});
apiMessages('guildsOnlyPaginate', vars);
apiError('guildsOnlyPaginate', vars);
expect(_.clone).to.have.been.calledOnce;
expect(_.clone).to.have.been.calledWith(vars);
});
@@ -22,7 +22,7 @@ describe('API Messages', () => {
let vars = {a: 1};
let stub = sinon.stub().returns('string');
sandbox.stub(_, 'template').returns(stub);
apiMessages('guildsOnlyPaginate', vars);
apiError('guildsOnlyPaginate', vars);
expect(_.template).to.have.been.calledOnce;
expect(_.template).to.have.been.calledWith(message);
expect(stub).to.have.been.calledOnce;

View File

@@ -1,4 +1,4 @@
import baseModel from '../../../../../website/server/libs/baseModel';
import baseModel from '../../../../website/server/libs/baseModel';
import mongoose from 'mongoose';
describe('Base model plugin', () => {

View File

@@ -1,7 +1,7 @@
import mongoose from 'mongoose';
import {
removeFromArray,
} from '../../../../../website/server/libs/collectionManipulators';
} from '../../../../website/server/libs/collectionManipulators';
describe('Collection Manipulators', () => {
describe('removeFromArray', () => {

View File

@@ -2,17 +2,18 @@
import moment from 'moment';
import nconf from 'nconf';
import requireAgain from 'require-again';
import { recoverCron, cron } from '../../../../../website/server/libs/cron';
import { model as User } from '../../../../../website/server/models/user';
import * as Tasks from '../../../../../website/server/models/task';
import common from '../../../../../website/common';
import analytics from '../../../../../website/server/libs/analyticsService';
import { recoverCron, cron } from '../../../../website/server/libs/cron';
import { model as User } from '../../../../website/server/models/user';
import * as Tasks from '../../../../website/server/models/task';
import common from '../../../../website/common';
import analytics from '../../../../website/server/libs/analyticsService';
// const scoreTask = common.ops.scoreTask;
let pathToCronLib = '../../../../../website/server/libs/cron';
let pathToCronLib = '../../../../website/server/libs/cron';
describe('cron', () => {
let clock = null;
let user;
let tasksByType = {habits: [], dailys: [], todos: [], rewards: []};
let daysMissed = 0;
@@ -23,7 +24,7 @@ describe('cron', () => {
local: {
username: 'username',
lowerCaseUsername: 'username',
email: 'email@email.email',
email: 'email@example.com',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},
@@ -34,6 +35,8 @@ describe('cron', () => {
});
afterEach(() => {
if (clock !== null)
clock.restore();
analytics.track.restore();
});
@@ -82,14 +85,12 @@ describe('cron', () => {
});
it('does not reset plan.gemsBought within the month', () => {
let clock = sinon.useFakeTimers(moment().startOf('month').add(2, 'days').unix());
clock = sinon.useFakeTimers(moment().startOf('month').add(2, 'days').toDate());
user.purchased.plan.dateUpdated = moment().startOf('month').toDate();
user.purchased.plan.gemsBought = 10;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.gemsBought).to.equal(10);
clock.restore();
});
it('resets plan.dateUpdated on a new month', () => {
@@ -117,21 +118,6 @@ describe('cron', () => {
expect(user.purchased.plan.consecutive.offset).to.equal(1);
});
it('increments plan.consecutive.trinkets when user has reached a month that is a multiple of 3', () => {
user.purchased.plan.consecutive.count = 5;
user.purchased.plan.consecutive.offset = 1;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user.purchased.plan.consecutive.offset).to.equal(0);
});
it('increments plan.consecutive.trinkets multiple times if user has been absent with continuous subscription', () => {
user.purchased.plan.dateUpdated = moment().subtract(6, 'months').toDate();
user.purchased.plan.consecutive.count = 5;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.trinkets).to.equal(2);
});
it('does not award unearned plan.consecutive.trinkets if subscription ended during an absence', () => {
user.purchased.plan.dateUpdated = moment().subtract(6, 'months').toDate();
user.purchased.plan.dateTerminated = moment().subtract(3, 'months').toDate();
@@ -143,21 +129,6 @@ describe('cron', () => {
expect(user.purchased.plan.consecutive.trinkets).to.equal(1);
});
it('increments plan.consecutive.gemCapExtra when user has reached a month that is a multiple of 3', () => {
user.purchased.plan.consecutive.count = 5;
user.purchased.plan.consecutive.offset = 1;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(5);
expect(user.purchased.plan.consecutive.offset).to.equal(0);
});
it('increments plan.consecutive.gemCapExtra multiple times if user has been absent with continuous subscription', () => {
user.purchased.plan.dateUpdated = moment().subtract(6, 'months').toDate();
user.purchased.plan.consecutive.count = 5;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(10);
});
it('does not increment plan.consecutive.gemCapExtra when user has reached the gemCap limit', () => {
user.purchased.plan.consecutive.gemCapExtra = 25;
user.purchased.plan.consecutive.count = 5;
@@ -184,6 +155,427 @@ describe('cron', () => {
expect(user.purchased.plan.consecutive.count).to.equal(0);
expect(user.purchased.plan.consecutive.offset).to.equal(0);
});
describe('for a 1-month recurring subscription', () => {
// create a user that will be used for all of these tests without a reset before each
let user1 = new User({
auth: {
local: {
username: 'username1',
lowerCaseUsername: 'username1',
email: 'email1@example.com',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},
},
});
// user1 has a 1-month recurring subscription starting today
user1.purchased.plan.customerId = 'subscribedId';
user1.purchased.plan.dateUpdated = moment().toDate();
user1.purchased.plan.planId = 'basic';
user1.purchased.plan.consecutive.count = 0;
user1.purchased.plan.consecutive.offset = 0;
user1.purchased.plan.consecutive.trinkets = 0;
user1.purchased.plan.consecutive.gemCapExtra = 0;
it('does not increment consecutive benefits after the first month', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months').add(2, 'days').toDate());
// Add 1 month to simulate what happens a month after the subscription was created.
// Add 2 days so that we're sure we're not affected by any start-of-month effects e.g., from time zone oddness.
cron({user: user1, tasksByType, daysMissed, analytics});
expect(user1.purchased.plan.consecutive.count).to.equal(1);
expect(user1.purchased.plan.consecutive.offset).to.equal(0);
expect(user1.purchased.plan.consecutive.trinkets).to.equal(0);
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(0);
});
it('does not increment consecutive benefits after the second month', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months').add(2, 'days').toDate());
// Add 1 month to simulate what happens a month after the subscription was created.
// Add 2 days so that we're sure we're not affected by any start-of-month effects e.g., from time zone oddness.
cron({user: user1, tasksByType, daysMissed, analytics});
expect(user1.purchased.plan.consecutive.count).to.equal(2);
expect(user1.purchased.plan.consecutive.offset).to.equal(0);
expect(user1.purchased.plan.consecutive.trinkets).to.equal(0);
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(0);
});
it('increments consecutive benefits after the third month', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months').add(2, 'days').toDate());
// Add 1 month to simulate what happens a month after the subscription was created.
// Add 2 days so that we're sure we're not affected by any start-of-month effects e.g., from time zone oddness.
cron({user: user1, tasksByType, daysMissed, analytics});
expect(user1.purchased.plan.consecutive.count).to.equal(3);
expect(user1.purchased.plan.consecutive.offset).to.equal(0);
expect(user1.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(5);
});
it('does not increment consecutive benefits after the fourth month', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(4, 'months').add(2, 'days').toDate());
// Add 1 month to simulate what happens a month after the subscription was created.
// Add 2 days so that we're sure we're not affected by any start-of-month effects e.g., from time zone oddness.
cron({user: user1, tasksByType, daysMissed, analytics});
expect(user1.purchased.plan.consecutive.count).to.equal(4);
expect(user1.purchased.plan.consecutive.offset).to.equal(0);
expect(user1.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(5);
});
it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(10, 'months').add(2, 'days').toDate());
cron({user: user1, tasksByType, daysMissed, analytics});
expect(user1.purchased.plan.consecutive.count).to.equal(10);
expect(user1.purchased.plan.consecutive.offset).to.equal(0);
expect(user1.purchased.plan.consecutive.trinkets).to.equal(3);
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(15);
});
});
describe('for a 3-month recurring subscription', () => {
let user3 = new User({
auth: {
local: {
username: 'username3',
lowerCaseUsername: 'username3',
email: 'email3@example.com',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},
},
});
// user3 has a 3-month recurring subscription starting today
user3.purchased.plan.customerId = 'subscribedId';
user3.purchased.plan.dateUpdated = moment().toDate();
user3.purchased.plan.planId = 'basic_3mo';
user3.purchased.plan.consecutive.count = 0;
user3.purchased.plan.consecutive.offset = 3;
user3.purchased.plan.consecutive.trinkets = 1;
user3.purchased.plan.consecutive.gemCapExtra = 5;
it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months').add(2, 'days').toDate());
cron({user: user3, tasksByType, daysMissed, analytics});
expect(user3.purchased.plan.consecutive.count).to.equal(1);
expect(user3.purchased.plan.consecutive.offset).to.equal(2);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(5);
});
it('does not increment consecutive benefits in the middle of the period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months').add(2, 'days').toDate());
cron({user: user3, tasksByType, daysMissed, analytics});
expect(user3.purchased.plan.consecutive.count).to.equal(2);
expect(user3.purchased.plan.consecutive.offset).to.equal(1);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(5);
});
it('does not increment consecutive benefits in the final month of the period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months').add(2, 'days').toDate());
cron({user: user3, tasksByType, daysMissed, analytics});
expect(user3.purchased.plan.consecutive.count).to.equal(3);
expect(user3.purchased.plan.consecutive.offset).to.equal(0);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(5);
});
it('increments consecutive benefits the month after the second paid period has started', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(4, 'months').add(2, 'days').toDate());
cron({user: user3, tasksByType, daysMissed, analytics});
expect(user3.purchased.plan.consecutive.count).to.equal(4);
expect(user3.purchased.plan.consecutive.offset).to.equal(2);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(2);
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(10);
});
it('does not increment consecutive benefits in the second month of the second period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(5, 'months').add(2, 'days').toDate());
cron({user: user3, tasksByType, daysMissed, analytics});
expect(user3.purchased.plan.consecutive.count).to.equal(5);
expect(user3.purchased.plan.consecutive.offset).to.equal(1);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(2);
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(10);
});
it('does not increment consecutive benefits in the final month of the second period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(6, 'months').add(2, 'days').toDate());
cron({user: user3, tasksByType, daysMissed, analytics});
expect(user3.purchased.plan.consecutive.count).to.equal(6);
expect(user3.purchased.plan.consecutive.offset).to.equal(0);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(2);
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(10);
});
it('increments consecutive benefits the month after the third paid period has started', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(7, 'months').add(2, 'days').toDate());
cron({user: user3, tasksByType, daysMissed, analytics});
expect(user3.purchased.plan.consecutive.count).to.equal(7);
expect(user3.purchased.plan.consecutive.offset).to.equal(2);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(3);
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(15);
});
it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(10, 'months').add(2, 'days').toDate());
cron({user: user3, tasksByType, daysMissed, analytics});
expect(user3.purchased.plan.consecutive.count).to.equal(10);
expect(user3.purchased.plan.consecutive.offset).to.equal(2);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(4);
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(20);
});
});
describe('for a 6-month recurring subscription', () => {
let user6 = new User({
auth: {
local: {
username: 'username6',
lowerCaseUsername: 'username6',
email: 'email6@example.com',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},
},
});
// user6 has a 6-month recurring subscription starting today
user6.purchased.plan.customerId = 'subscribedId';
user6.purchased.plan.dateUpdated = moment().toDate();
user6.purchased.plan.planId = 'google_6mo';
user6.purchased.plan.consecutive.count = 0;
user6.purchased.plan.consecutive.offset = 6;
user6.purchased.plan.consecutive.trinkets = 2;
user6.purchased.plan.consecutive.gemCapExtra = 10;
it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months').add(2, 'days').toDate());
cron({user: user6, tasksByType, daysMissed, analytics});
expect(user6.purchased.plan.consecutive.count).to.equal(1);
expect(user6.purchased.plan.consecutive.offset).to.equal(5);
expect(user6.purchased.plan.consecutive.trinkets).to.equal(2);
expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(10);
});
it('does not increment consecutive benefits in the final month of the period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(6, 'months').add(2, 'days').toDate());
cron({user: user6, tasksByType, daysMissed, analytics});
expect(user6.purchased.plan.consecutive.count).to.equal(6);
expect(user6.purchased.plan.consecutive.offset).to.equal(0);
expect(user6.purchased.plan.consecutive.trinkets).to.equal(2);
expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(10);
});
it('increments consecutive benefits the month after the second paid period has started', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(7, 'months').add(2, 'days').toDate());
cron({user: user6, tasksByType, daysMissed, analytics});
expect(user6.purchased.plan.consecutive.count).to.equal(7);
expect(user6.purchased.plan.consecutive.offset).to.equal(5);
expect(user6.purchased.plan.consecutive.trinkets).to.equal(4);
expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(20);
});
it('increments consecutive benefits the month after the third paid period has started', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(13, 'months').add(2, 'days').toDate());
cron({user: user6, tasksByType, daysMissed, analytics});
expect(user6.purchased.plan.consecutive.count).to.equal(13);
expect(user6.purchased.plan.consecutive.offset).to.equal(5);
expect(user6.purchased.plan.consecutive.trinkets).to.equal(6);
expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(25);
});
it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(19, 'months').add(2, 'days').toDate());
cron({user: user6, tasksByType, daysMissed, analytics});
expect(user6.purchased.plan.consecutive.count).to.equal(19);
expect(user6.purchased.plan.consecutive.offset).to.equal(5);
expect(user6.purchased.plan.consecutive.trinkets).to.equal(8);
expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(25);
});
});
describe('for a 12-month recurring subscription', () => {
let user12 = new User({
auth: {
local: {
username: 'username12',
lowerCaseUsername: 'username12',
email: 'email12@example.com',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},
},
});
// user12 has a 12-month recurring subscription starting today
user12.purchased.plan.customerId = 'subscribedId';
user12.purchased.plan.dateUpdated = moment().toDate();
user12.purchased.plan.planId = 'basic_12mo';
user12.purchased.plan.consecutive.count = 0;
user12.purchased.plan.consecutive.offset = 12;
user12.purchased.plan.consecutive.trinkets = 4;
user12.purchased.plan.consecutive.gemCapExtra = 20;
it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months').add(2, 'days').toDate());
cron({user: user12, tasksByType, daysMissed, analytics});
expect(user12.purchased.plan.consecutive.count).to.equal(1);
expect(user12.purchased.plan.consecutive.offset).to.equal(11);
expect(user12.purchased.plan.consecutive.trinkets).to.equal(4);
expect(user12.purchased.plan.consecutive.gemCapExtra).to.equal(20);
});
it('does not increment consecutive benefits in the final month of the period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(12, 'months').add(2, 'days').toDate());
cron({user: user12, tasksByType, daysMissed, analytics});
expect(user12.purchased.plan.consecutive.count).to.equal(12);
expect(user12.purchased.plan.consecutive.offset).to.equal(0);
expect(user12.purchased.plan.consecutive.trinkets).to.equal(4);
expect(user12.purchased.plan.consecutive.gemCapExtra).to.equal(20);
});
it('increments consecutive benefits the month after the second paid period has started', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(13, 'months').add(2, 'days').toDate());
cron({user: user12, tasksByType, daysMissed, analytics});
expect(user12.purchased.plan.consecutive.count).to.equal(13);
expect(user12.purchased.plan.consecutive.offset).to.equal(11);
expect(user12.purchased.plan.consecutive.trinkets).to.equal(8);
expect(user12.purchased.plan.consecutive.gemCapExtra).to.equal(25);
});
it('increments consecutive benefits the month after the third paid period has started', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(25, 'months').add(2, 'days').toDate());
cron({user: user12, tasksByType, daysMissed, analytics});
expect(user12.purchased.plan.consecutive.count).to.equal(25);
expect(user12.purchased.plan.consecutive.offset).to.equal(11);
expect(user12.purchased.plan.consecutive.trinkets).to.equal(12);
expect(user12.purchased.plan.consecutive.gemCapExtra).to.equal(25);
});
it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(37, 'months').add(2, 'days').toDate());
cron({user: user12, tasksByType, daysMissed, analytics});
expect(user12.purchased.plan.consecutive.count).to.equal(37);
expect(user12.purchased.plan.consecutive.offset).to.equal(11);
expect(user12.purchased.plan.consecutive.trinkets).to.equal(16);
expect(user12.purchased.plan.consecutive.gemCapExtra).to.equal(25);
});
});
describe('for a 3-month gift subscription (non-recurring)', () => {
let user3g = new User({
auth: {
local: {
username: 'username3g',
lowerCaseUsername: 'username3g',
email: 'email3g@example.com',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},
},
});
// user3g has a 3-month gift subscription starting today
user3g.purchased.plan.customerId = 'Gift';
user3g.purchased.plan.dateUpdated = moment().toDate();
user3g.purchased.plan.dateTerminated = moment().startOf('month').add(3, 'months').add(15, 'days').toDate();
user3g.purchased.plan.planId = null;
user3g.purchased.plan.consecutive.count = 0;
user3g.purchased.plan.consecutive.offset = 3;
user3g.purchased.plan.consecutive.trinkets = 1;
user3g.purchased.plan.consecutive.gemCapExtra = 5;
it('does not increment consecutive benefits in the first month of the gift subscription', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months').add(2, 'days').toDate());
cron({user: user3g, tasksByType, daysMissed, analytics});
expect(user3g.purchased.plan.consecutive.count).to.equal(1);
expect(user3g.purchased.plan.consecutive.offset).to.equal(2);
expect(user3g.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user3g.purchased.plan.consecutive.gemCapExtra).to.equal(5);
});
it('does not increment consecutive benefits in the second month of the gift subscription', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months').add(2, 'days').toDate());
cron({user: user3g, tasksByType, daysMissed, analytics});
expect(user3g.purchased.plan.consecutive.count).to.equal(2);
expect(user3g.purchased.plan.consecutive.offset).to.equal(1);
expect(user3g.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user3g.purchased.plan.consecutive.gemCapExtra).to.equal(5);
});
it('does not increment consecutive benefits in the third month of the gift subscription', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months').add(2, 'days').toDate());
cron({user: user3g, tasksByType, daysMissed, analytics});
expect(user3g.purchased.plan.consecutive.count).to.equal(3);
expect(user3g.purchased.plan.consecutive.offset).to.equal(0);
expect(user3g.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user3g.purchased.plan.consecutive.gemCapExtra).to.equal(5);
});
it('does not increment consecutive benefits in the month after the gift subscription has ended', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(4, 'months').add(2, 'days').toDate());
cron({user: user3g, tasksByType, daysMissed, analytics});
expect(user3g.purchased.plan.consecutive.count).to.equal(0); // subscription has been erased by now
expect(user3g.purchased.plan.consecutive.offset).to.equal(0);
expect(user3g.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user3g.purchased.plan.consecutive.gemCapExtra).to.equal(0); // erased
});
});
describe('for a 6-month recurring subscription where the user has incorrect consecutive month data from prior bugs', () => {
let user6x = new User({
auth: {
local: {
username: 'username6x',
lowerCaseUsername: 'username6x',
email: 'email6x@example.com',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},
},
});
// user6x has a 6-month recurring subscription starting 8 months in the past before issue #4819 was fixed
user6x.purchased.plan.customerId = 'subscribedId';
user6x.purchased.plan.dateUpdated = moment().toDate();
user6x.purchased.plan.planId = 'basic_6mo';
user6x.purchased.plan.consecutive.count = 8;
user6x.purchased.plan.consecutive.offset = 0;
user6x.purchased.plan.consecutive.trinkets = 3;
user6x.purchased.plan.consecutive.gemCapExtra = 15;
it('increments consecutive benefits in the first month since the fix for #4819 goes live', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months').add(2, 'days').toDate());
cron({user: user6x, tasksByType, daysMissed, analytics});
expect(user6x.purchased.plan.consecutive.count).to.equal(9);
expect(user6x.purchased.plan.consecutive.offset).to.equal(5);
expect(user6x.purchased.plan.consecutive.trinkets).to.equal(5);
expect(user6x.purchased.plan.consecutive.gemCapExtra).to.equal(25);
});
it('does not increment consecutive benefits in the second month after the fix goes live', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months').add(2, 'days').toDate());
cron({user: user6x, tasksByType, daysMissed, analytics});
expect(user6x.purchased.plan.consecutive.count).to.equal(10);
expect(user6x.purchased.plan.consecutive.offset).to.equal(4);
expect(user6x.purchased.plan.consecutive.trinkets).to.equal(5);
expect(user6x.purchased.plan.consecutive.gemCapExtra).to.equal(25);
});
it('does not increment consecutive benefits in the third month after the fix goes live', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months').add(2, 'days').toDate());
cron({user: user6x, tasksByType, daysMissed, analytics});
expect(user6x.purchased.plan.consecutive.count).to.equal(11);
expect(user6x.purchased.plan.consecutive.offset).to.equal(3);
expect(user6x.purchased.plan.consecutive.trinkets).to.equal(5);
expect(user6x.purchased.plan.consecutive.gemCapExtra).to.equal(25);
});
it('increments consecutive benefits in the seventh month after the fix goes live', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(7, 'months').add(2, 'days').toDate());
cron({user: user6x, tasksByType, daysMissed, analytics});
expect(user6x.purchased.plan.consecutive.count).to.equal(15);
expect(user6x.purchased.plan.consecutive.offset).to.equal(5);
expect(user6x.purchased.plan.consecutive.trinkets).to.equal(7);
expect(user6x.purchased.plan.consecutive.gemCapExtra).to.equal(25);
});
});
});
describe('end of the month perks when user is not subscribed', () => {
@@ -198,14 +590,12 @@ describe('cron', () => {
});
it('does not reset plan.gemsBought within the month', () => {
let clock = sinon.useFakeTimers(moment().startOf('month').add(2, 'days').unix());
clock = sinon.useFakeTimers(moment().startOf('month').add(2, 'days').unix());
user.purchased.plan.dateUpdated = moment().startOf('month').toDate();
user.purchased.plan.gemsBought = 10;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.gemsBought).to.equal(10);
clock.restore();
});
it('does not reset plan.dateUpdated on a new month', () => {
@@ -611,15 +1001,11 @@ describe('cron', () => {
describe('counters', () => {
let notStartOfWeekOrMonth = new Date(2016, 9, 28).getTime(); // a Friday
let clock;
beforeEach(() => {
// Replace system clocks so we can get predictable results
clock = sinon.useFakeTimers(notStartOfWeekOrMonth);
});
afterEach(() => {
return clock.restore();
});
it('should reset a daily habit counter each day', () => {
tasksByType.habits[0].counterUp = 1;
@@ -1348,7 +1734,7 @@ describe('recoverCron', () => {
local: {
username: 'username',
lowerCaseUsername: 'username',
email: 'email@email.email',
email: 'email@example.com',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},

View File

@@ -3,9 +3,9 @@ import got from 'got';
import nconf from 'nconf';
import nodemailer from 'nodemailer';
import requireAgain from 'require-again';
import logger from '../../../../../website/server/libs/logger';
import { TAVERN_ID } from '../../../../../website/server/models/group';
import { defer } from '../../../../helpers/api-unit.helper';
import logger from '../../../../website/server/libs/logger';
import { TAVERN_ID } from '../../../../website/server/models/group';
import { defer } from '../../../helpers/api-unit.helper';
function getUser () {
return {
@@ -19,7 +19,6 @@ function getUser () {
emails: [{
value: 'email@facebook',
}],
displayName: 'fb display name',
},
},
profile: {
@@ -34,7 +33,7 @@ function getUser () {
}
describe('emails', () => {
let pathToEmailLib = '../../../../../website/server/libs/email';
let pathToEmailLib = '../../../../website/server/libs/email';
describe('sendEmail', () => {
let sendMailSpy;
@@ -100,7 +99,7 @@ describe('emails', () => {
let data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
expect(data).to.have.property('name', user.auth.facebook.displayName);
expect(data).to.have.property('name', user.profile.name);
expect(data).to.have.property('email', user.auth.facebook.emails[0].value);
expect(data).to.have.property('_id', user._id);
expect(data).to.have.property('canSend', true);
@@ -110,13 +109,12 @@ describe('emails', () => {
let attachEmail = requireAgain(pathToEmailLib);
let getUserInfo = attachEmail.getUserInfo;
let user = getUser();
delete user.profile.name;
delete user.auth.local.email;
delete user.auth.facebook;
let data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
expect(data).to.have.property('name', user.auth.local.username);
expect(data).to.have.property('name', user.profile.name);
expect(data).not.to.have.property('email');
expect(data).to.have.property('_id', user._id);
expect(data).to.have.property('canSend', true);

View File

@@ -1,7 +1,7 @@
import {
encrypt,
decrypt,
} from '../../../../../website/server/libs/encryption';
} from '../../../../website/server/libs/encryption';
describe('encryption', () => {
it('can encrypt and decrypt', () => {

View File

@@ -5,7 +5,7 @@ import {
BadRequest,
InternalServerError,
NotFound,
} from '../../../../../website/server/libs/errors';
} from '../../../../website/server/libs/errors';
describe('Custom Errors', () => {
describe('CustomError', () => {

View File

@@ -2,7 +2,7 @@ import {
translations,
localePath,
langCodes,
} from '../../../../../website/server/libs/i18n';
} from '../../../../website/server/libs/i18n';
import fs from 'fs';
import path from 'path';

View File

@@ -1,8 +1,8 @@
import winston from 'winston';
import logger from '../../../../../website/server/libs/logger';
import logger from '../../../../website/server/libs/logger';
import {
NotFound,
} from '../../../../../website/server/libs//errors';
} from '../../../../website/server/libs//errors';
describe('logger', () => {
let logSpy;

View File

@@ -2,11 +2,11 @@
import {
encrypt,
} from '../../../../../website/server/libs/encryption';
} from '../../../../website/server/libs/encryption';
import moment from 'moment';
import {
generateUser,
} from '../../../../helpers/api-integration/v3';
} from '../../../helpers/api-integration/v3';
import {
sha1Encrypt as sha1EncryptPassword,
sha1MakeSalt,
@@ -15,7 +15,7 @@ import {
compare,
convertToBcrypt,
validatePasswordResetCodeAndFindUser,
} from '../../../../../website/server/libs/password';
} from '../../../../website/server/libs/password';
describe('Password Utilities', () => {
describe('compare', () => {

View File

@@ -2,11 +2,11 @@ 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';
} from '../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../website/server/models/user';
import amzLib from '../../../../../../website/server/libs/payments/amazon';
import payments from '../../../../../../website/server/libs/payments/payments';
import common from '../../../../../../website/common';
import { createNonLeaderGroupMember } from '../paymentHelpers';
const i18n = common.i18n;

View File

@@ -1,7 +1,7 @@
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 { model as User } from '../../../../../../website/server/models/user';
import amzLib from '../../../../../../website/server/libs/payments/amazon';
import payments from '../../../../../../website/server/libs/payments/payments';
import common from '../../../../../../website/common';
const i18n = common.i18n;

View File

@@ -2,12 +2,12 @@ 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';
} 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/payments/amazon';
import payments from '../../../../../../website/server/libs/payments/payments';
import common from '../../../../../../website/common';
const i18n = common.i18n;

View File

@@ -2,11 +2,11 @@ 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';
} 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/payments/amazon';
import payments from '../../../../../../website/server/libs/payments/payments';
describe('#upgradeGroupPlan', () => {
let spy, data, user, group, uuidString;

View File

@@ -1,7 +1,7 @@
/* eslint-disable camelcase */
import iapModule from '../../../../../website/server/libs/inAppPurchases';
import payments from '../../../../../website/server/libs/payments';
import applePayments from '../../../../../website/server/libs/applePayments';
import payments from '../../../../../website/server/libs/payments/payments';
import applePayments from '../../../../../website/server/libs/payments/apple';
import iap from '../../../../../website/server/libs/inAppPurchases';
import {model as User} from '../../../../../website/server/models/user';
import common from '../../../../../website/common';
@@ -57,6 +57,18 @@ describe('Apple Payments', () => {
});
});
it('should throw an error if getPurchaseData is invalid', async () => {
iapGetPurchaseDataStub.restore();
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData').returns([]);
await expect(applePayments.verifyGemPurchase(user, receipt, headers))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: applePayments.constants.RESPONSE_NO_ITEM_PURCHASED,
});
});
it('errors if the user cannot purchase gems', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
await expect(applePayments.verifyGemPurchase(user, receipt, headers))
@@ -69,27 +81,76 @@ describe('Apple Payments', () => {
user.canGetGems.restore();
});
it('purchases gems', async () => {
it('errors if amount does not exist', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
await applePayments.verifyGemPurchase(user, receipt, headers);
iapGetPurchaseDataStub.restore();
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
.returns([{productId: 'badProduct',
transactionId: token,
}]);
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt);
expect(iapIsValidatedStub).to.be.calledOnce;
expect(iapIsValidatedStub).to.be.calledWith({});
expect(iapGetPurchaseDataStub).to.be.calledOnce;
await expect(applePayments.verifyGemPurchase(user, receipt, headers))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: applePayments.constants.RESPONSE_INVALID_ITEM,
});
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
amount: 5.25,
headers,
});
expect(user.canGetGems).to.be.calledOnce;
user.canGetGems.restore();
});
const gemsCanPurchase = [
{
productId: 'com.habitrpg.ios.Habitica.4gems',
amount: 1,
},
{
productId: 'com.habitrpg.ios.Habitica.20gems',
amount: 5.25,
},
{
productId: 'com.habitrpg.ios.Habitica.21gems',
amount: 5.25,
},
{
productId: 'com.habitrpg.ios.Habitica.42gems',
amount: 10.5,
},
{
productId: 'com.habitrpg.ios.Habitica.84gems',
amount: 21,
},
];
gemsCanPurchase.forEach(gemTest => {
it(`purchases ${gemTest.productId} gems`, async () => {
iapGetPurchaseDataStub.restore();
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
.returns([{productId: gemTest.productId,
transactionId: token,
}]);
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
await applePayments.verifyGemPurchase(user, receipt, headers);
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt);
expect(iapIsValidatedStub).to.be.calledOnce;
expect(iapIsValidatedStub).to.be.calledWith({});
expect(iapGetPurchaseDataStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
amount: gemTest.amount,
headers,
});
expect(user.canGetGems).to.be.calledOnce;
user.canGetGems.restore();
});
});
});
describe('subscribe', () => {
@@ -133,7 +194,16 @@ describe('Apple Payments', () => {
iapModule.validate.restore();
iapModule.isValidated.restore();
iapModule.getPurchaseData.restore();
payments.createSubscription.restore();
if (payments.createSubscription.restore) payments.createSubscription.restore();
});
it('should throw an error if sku is empty', async () => {
await expect(applePayments.subscribe('', user, receipt, headers, nextPaymentProcessing))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
message: i18n.t('missingSubscriptionCode'),
});
});
it('should throw an error if receipt is invalid', async () => {
@@ -149,26 +219,69 @@ describe('Apple Payments', () => {
});
});
it('creates a user subscription', async () => {
const subOptions = [
{
sku: 'subscription1month',
subKey: 'basic_earned',
},
{
sku: 'com.habitrpg.ios.habitica.subscription.3month',
subKey: 'basic_3mo',
},
{
sku: 'com.habitrpg.ios.habitica.subscription.6month',
subKey: 'basic_6mo',
},
{
sku: 'com.habitrpg.ios.habitica.subscription.12month',
subKey: 'basic_12mo',
},
];
subOptions.forEach(option => {
it(`creates a user subscription for ${option.sku}`, async () => {
iapModule.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().add({day: 1}).toDate(),
productId: option.sku,
transactionId: token,
}]);
sub = common.content.subscriptionBlocks[option.subKey];
await applePayments.subscribe(option.sku, user, receipt, headers, nextPaymentProcessing);
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt);
expect(iapIsValidatedStub).to.be.calledOnce;
expect(iapIsValidatedStub).to.be.calledWith({});
expect(iapGetPurchaseDataStub).to.be.calledOnce;
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
expect(paymentsCreateSubscritionStub).to.be.calledWith({
user,
customerId: token,
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
sub,
headers,
additionalData: receipt,
nextPaymentProcessing,
});
});
});
it('errors when a user is already subscribed', async () => {
payments.createSubscription.restore();
user = new User();
await applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing);
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt);
expect(iapIsValidatedStub).to.be.calledOnce;
expect(iapIsValidatedStub).to.be.calledWith({});
expect(iapGetPurchaseDataStub).to.be.calledOnce;
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
expect(paymentsCreateSubscritionStub).to.be.calledWith({
user,
customerId: token,
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
sub,
headers,
additionalData: receipt,
nextPaymentProcessing,
});
await expect(applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: applePayments.constants.RESPONSE_ALREADY_USED,
});
});
});

View File

@@ -1,7 +1,7 @@
/* eslint-disable camelcase */
import iapModule from '../../../../../website/server/libs/inAppPurchases';
import payments from '../../../../../website/server/libs/payments';
import googlePayments from '../../../../../website/server/libs/googlePayments';
import payments from '../../../../../website/server/libs/payments/payments';
import googlePayments from '../../../../../website/server/libs/payments/google';
import iap from '../../../../../website/server/libs/inAppPurchases';
import {model as User} from '../../../../../website/server/models/user';
import common from '../../../../../website/common';

View File

@@ -1,13 +1,13 @@
import moment from 'moment';
import * as sender from '../../../../../../../website/server/libs/email';
import * as api from '../../../../../../../website/server/libs/payments';
import { model as User } from '../../../../../../../website/server/models/user';
import { model as Group } from '../../../../../../../website/server/models/group';
import * as sender from '../../../../../../website/server/libs/email';
import * as api from '../../../../../../website/server/libs/payments/payments';
import { model as User } from '../../../../../../website/server/models/user';
import { model as Group } from '../../../../../../website/server/models/group';
import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
import i18n from '../../../../../../../website/common/script/i18n';
} from '../../../../../helpers/api-unit.helper.js';
import i18n from '../../../../../../website/common/script/i18n';
describe('Canceling a subscription for group', () => {
let plan, group, user, data;

View File

@@ -2,16 +2,16 @@ import moment from 'moment';
import stripeModule from 'stripe';
import nconf from 'nconf';
import * as sender from '../../../../../../../website/server/libs/email';
import * as api from '../../../../../../../website/server/libs/payments';
import amzLib from '../../../../../../../website/server/libs/amazonPayments';
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
import { model as User } from '../../../../../../../website/server/models/user';
import { model as Group } from '../../../../../../../website/server/models/group';
import * as sender from '../../../../../../website/server/libs/email';
import * as api from '../../../../../../website/server/libs/payments/payments';
import amzLib from '../../../../../../website/server/libs/payments/amazon';
import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
import { model as User } from '../../../../../../website/server/models/user';
import { model as Group } from '../../../../../../website/server/models/group';
import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
} from '../../../../../helpers/api-unit.helper.js';
describe('Purchasing a group plan for group', () => {
const EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_GOOGLE = 'Google_subscription';
@@ -443,8 +443,7 @@ describe('Purchasing a group plan for group', () => {
await api.createSubscription(data);
let updatedUser = await User.findById(recipient._id).exec();
const updatedUser = await User.findById(recipient._id).exec();
expect(updatedUser.purchased.plan.extraMonths).to.within(2, 3);
});

View File

@@ -1,4 +1,4 @@
import { model as User } from '../../../../../../website/server/models/user';
import { model as User } from '../../../../../website/server/models/user';
export async function createNonLeaderGroupMember (group) {
let nonLeader = new User();

View File

@@ -1,11 +1,11 @@
import moment from 'moment';
import * as sender from '../../../../../website/server/libs/email';
import * as api from '../../../../../website/server/libs/payments';
import * as api from '../../../../../website/server/libs/payments/payments';
import analytics from '../../../../../website/server/libs/analyticsService';
import notifications from '../../../../../website/server/libs/pushNotifications';
import { model as User } from '../../../../../website/server/models/user';
import { translate as t } from '../../../../helpers/api-v3-integration.helper';
import { translate as t } from '../../../../helpers/api-integration/v3';
import {
generateGroup,
} from '../../../../helpers/api-unit.helper.js';
@@ -210,7 +210,7 @@ describe('payments/index', () => {
let msg = '\`Hello recipient, sender has sent you 3 months of subscription!\`';
expect(user.sendMessage).to.be.calledOnce;
expect(user.sendMessage).to.be.calledWith(recipient, { receiverMsg: msg, senderMsg: msg });
expect(user.sendMessage).to.be.calledWith(recipient, { receiverMsg: msg, senderMsg: msg, save: false });
});
it('sends an email about the gift', async () => {
@@ -629,7 +629,16 @@ describe('payments/index', () => {
await api.buyGems(data);
let msg = '\`Hello recipient, sender has sent you 4 gems!\`';
expect(user.sendMessage).to.be.calledWith(recipient, { receiverMsg: msg, senderMsg: msg });
expect(user.sendMessage).to.be.calledWith(recipient, { receiverMsg: msg, senderMsg: msg, save: false });
});
it('sends a message from purchaser to recipient wtih custom message', async () => {
data.gift.message = 'giftmessage';
await api.buyGems(data);
const msg = `\`Hello recipient, sender has sent you 4 gems!\` ${data.gift.message}`;
expect(user.sendMessage).to.be.calledWith(recipient, { receiverMsg: msg, senderMsg: msg, save: false });
});
it('sends a push notification if user did not gift to self', async () => {
@@ -658,7 +667,7 @@ describe('payments/index', () => {
return `\`${messageContent}\``;
});
expect(user.sendMessage).to.be.calledWith(recipient, { receiverMsg: recipientsMessageContent, senderMsg: sendersMessageContent });
expect(user.sendMessage).to.be.calledWith(recipient, { receiverMsg: recipientsMessageContent, senderMsg: sendersMessageContent, save: false });
});
});
});

View File

@@ -1,7 +1,7 @@
/* 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';
import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
import payments from '../../../../../../website/server/libs/payments/payments';
import { model as User } from '../../../../../../website/server/models/user';
describe('checkout success', () => {
const subKey = 'basic_3mo';

View File

@@ -1,9 +1,9 @@
/* 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';
import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
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;

View File

@@ -1,10 +1,10 @@
/* eslint-disable camelcase */
import payments from '../../../../../../../website/server/libs/payments';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
import payments from '../../../../../../website/server/libs/payments/payments';
import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
} from '../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../website/server/models/user';
describe('ipn', () => {
const subKey = 'basic_3mo';

View File

@@ -1,11 +1,11 @@
/* eslint-disable camelcase */
import payments from '../../../../../../../website/server/libs/payments';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
import payments from '../../../../../../website/server/libs/payments/payments';
import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
import common from '../../../../../../../website/common';
} 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;

View File

@@ -1,11 +1,11 @@
/* eslint-disable camelcase */
import payments from '../../../../../../../website/server/libs/payments';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
import payments from '../../../../../../website/server/libs/payments/payments';
import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
import common from '../../../../../../../website/common';
} 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';

View File

@@ -2,9 +2,9 @@
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';
import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
import { model as Coupon } from '../../../../../../website/server/models/coupon';
import common from '../../../../../../website/common';
const i18n = common.i18n;

View File

@@ -2,11 +2,11 @@ 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';
} from '../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../website/server/models/user';
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
import payments from '../../../../../../website/server/libs/payments/payments';
import common from '../../../../../../website/common';
const i18n = common.i18n;

View File

@@ -3,12 +3,12 @@ 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';
} 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/payments/stripe';
import payments from '../../../../../../website/server/libs/payments/payments';
import common from '../../../../../../website/common';
const i18n = common.i18n;

View File

@@ -1,9 +1,9 @@
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';
import { model as User } from '../../../../../../website/server/models/user';
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
import payments from '../../../../../../website/server/libs/payments/payments';
import common from '../../../../../../website/common';
const i18n = common.i18n;
@@ -37,6 +37,22 @@ describe('checkout', () => {
payments.createSubscription.restore();
});
it('should error if there is no token', async () => {
await expect(stripePayments.checkout({
user,
gift,
groupId,
email,
headers,
coupon,
}, stripe))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
message: 'Missing req.body.id',
name: 'BadRequest',
});
});
it('should error if gem amount is too low', async () => {
let receivingUser = new User();
receivingUser.save();
@@ -64,7 +80,6 @@ describe('checkout', () => {
});
});
it('should error if user cannot get gems', async () => {
gift = undefined;
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);

View File

@@ -2,10 +2,10 @@ 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';
} from '../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../website/server/models/user';
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
import common from '../../../../../../website/common';
const i18n = common.i18n;

View File

@@ -2,12 +2,12 @@ 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';
} from '../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../website/server/models/user';
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
import payments from '../../../../../../website/server/libs/payments/payments';
import common from '../../../../../../website/common';
import logger from '../../../../../../website/server/libs/logger';
import { v4 as uuid } from 'uuid';
import moment from 'moment';

View File

@@ -2,11 +2,11 @@ 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';
} 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/payments/stripe';
import payments from '../../../../../../website/server/libs/payments/payments';
describe('Stripe - Upgrade Group Plan', () => {
const stripe = stripeModule('test');

View File

@@ -1,7 +1,7 @@
import { preenHistory } from '../../../../../website/server/libs/preening';
import { preenHistory } from '../../../../website/server/libs/preening';
import moment from 'moment';
import sinon from 'sinon'; // eslint-disable-line no-shadow
import { generateHistory } from '../../../../helpers/api-unit.helper.js';
import { generateHistory } from '../../../helpers/api-unit.helper.js';
describe('preenHistory', () => {
let clock;

View File

@@ -1,4 +1,4 @@
import { model as User } from '../../../../../website/server/models/user';
import { model as User } from '../../../../website/server/models/user';
import requireAgain from 'require-again';
import pushNotify from 'push-notify';
import nconf from 'nconf';
@@ -7,7 +7,7 @@ import gcmLib from 'node-gcm'; // works with FCM notifications too
describe('pushNotifications', () => {
let user;
let sendPushNotification;
let pathToPushNotifications = '../../../../../website/server/libs/pushNotifications';
let pathToPushNotifications = '../../../../website/server/libs/pushNotifications';
let fcmSendSpy;
let apnSendSpy;

View File

@@ -1,4 +1,4 @@
import setupNconf from '../../../../../website/server/libs/setupNconf';
import setupNconf from '../../../../website/server/libs/setupNconf';
import path from 'path';
import nconf from 'nconf';

View File

@@ -1,10 +1,11 @@
/* eslint-disable camelcase */
import { IncomingWebhook } from '@slack/client';
import requireAgain from 'require-again';
import slack from '../../../../../website/server/libs/slack';
import logger from '../../../../../website/server/libs/logger';
import { TAVERN_ID } from '../../../../../website/server/models/group';
import slack from '../../../../website/server/libs/slack';
import logger from '../../../../website/server/libs/logger';
import { TAVERN_ID } from '../../../../website/server/models/group';
import nconf from 'nconf';
import moment from 'moment';
describe('slack', () => {
describe('sendFlagNotification', () => {
@@ -45,13 +46,15 @@ describe('slack', () => {
it('sends a slack webhook', () => {
slack.sendFlagNotification(data);
const timestamp = `${moment(data.message.timestamp).utc().format('YYYY-MM-DD HH:mm')} UTC`;
expect(IncomingWebhook.prototype.send).to.be.calledOnce;
expect(IncomingWebhook.prototype.send).to.be.calledWith({
text: 'flagger (flagger-id; language: flagger-lang) flagged a message',
attachments: [{
fallback: 'Flag Message',
color: 'danger',
author_name: 'Author - author@example.com - author-id',
author_name: `Author - author@example.com - author-id\n${timestamp}`,
title: 'Flag in Some group - (private guild)',
title_link: undefined,
text: 'some text',
@@ -97,9 +100,11 @@ describe('slack', () => {
slack.sendFlagNotification(data);
const timestamp = `${moment(data.message.timestamp).utc().format('YYYY-MM-DD HH:mm')} UTC`;
expect(IncomingWebhook.prototype.send).to.be.calledWithMatch({
attachments: [sandbox.match({
author_name: 'System Message',
author_name: `System Message\n${timestamp}`,
})],
});
});
@@ -107,7 +112,7 @@ describe('slack', () => {
it('noops if no flagging url is provided', () => {
sandbox.stub(nconf, 'get').withArgs('SLACK:FLAGGING_URL').returns('');
sandbox.stub(logger, 'error');
let reRequiredSlack = requireAgain('../../../../../website/server/libs/slack');
let reRequiredSlack = requireAgain('../../../../website/server/libs/slack');
expect(logger.error).to.be.calledOnce;

View File

@@ -3,13 +3,13 @@ import {
getTasks,
syncableAttrs,
moveTask,
} from '../../../../../website/server/libs/taskManager';
import i18n from '../../../../../website/common/script/i18n';
} from '../../../../website/server/libs/taskManager';
import i18n from '../../../../website/common/script/i18n';
import {
generateUser,
generateGroup,
generateChallenge,
} from '../../../../helpers/api-unit.helper.js';
} from '../../../helpers/api-unit.helper.js';
describe('taskManager', () => {
let user, group, challenge;

View File

@@ -4,11 +4,19 @@ import {
taskScoredWebhook,
groupChatReceivedWebhook,
taskActivityWebhook,
} from '../../../../../website/server/libs/webhook';
import { defer } from '../../../../helpers/api-unit.helper';
questActivityWebhook,
userActivityWebhook,
} from '../../../../website/server/libs/webhook';
import {
model as User,
} from '../../../../website/server/models/user';
import {
generateUser,
} from '../../../helpers/api-unit.helper.js';
import { defer } from '../../../helpers/api-unit.helper';
describe('webhooks', () => {
let webhooks;
let webhooks, user;
beforeEach(() => {
sandbox.stub(got, 'post').returns(defer().promise);
@@ -23,6 +31,26 @@ describe('webhooks', () => {
updated: true,
deleted: true,
scored: true,
checklistScored: true,
},
}, {
id: 'questActivity',
url: 'http://quest-activity.com',
enabled: true,
type: 'questActivity',
options: {
questStarted: true,
questFinised: true,
},
}, {
id: 'userActivity',
url: 'http://user-activity.com',
enabled: true,
type: 'userActivity',
options: {
petHatched: true,
mountRaised: true,
leveledUp: true,
},
}, {
id: 'groupChatReceived',
@@ -33,6 +61,9 @@ describe('webhooks', () => {
groupId: 'group-id',
},
}];
user = generateUser();
user.webhooks = webhooks;
});
afterEach(() => {
@@ -57,7 +88,8 @@ describe('webhooks', () => {
let body = { foo: 'bar' };
sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}], body);
user.webhooks = [{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}];
sendWebhook.send(user, body);
expect(WebhookSender.defaultTransformData).to.be.calledOnce;
expect(got.post).to.be.calledOnce;
@@ -67,6 +99,30 @@ describe('webhooks', () => {
});
});
it('adds default data (user and webhookType) to the body', () => {
let sendWebhook = new WebhookSender({
type: 'custom',
});
sandbox.spy(sendWebhook, 'attachDefaultData');
let body = { foo: 'bar' };
user.webhooks = [{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}];
sendWebhook.send(user, body);
expect(sendWebhook.attachDefaultData).to.be.calledOnce;
expect(got.post).to.be.calledOnce;
expect(got.post).to.be.calledWithMatch('http://custom-url.com', {
json: true,
});
expect(body).to.eql({
foo: 'bar',
user: {_id: user._id},
webhookType: 'custom',
});
});
it('can pass in a data transformation function', () => {
sandbox.spy(WebhookSender, 'defaultTransformData');
let sendWebhook = new WebhookSender({
@@ -80,7 +136,8 @@ describe('webhooks', () => {
let body = { foo: 'bar' };
sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}], body);
user.webhooks = [{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}];
sendWebhook.send(user, body);
expect(WebhookSender.defaultTransformData).to.not.be.called;
expect(got.post).to.be.calledOnce;
@@ -93,7 +150,7 @@ describe('webhooks', () => {
});
});
it('provieds a default filter function', () => {
it('provides a default filter function', () => {
sandbox.spy(WebhookSender, 'defaultWebhookFilter');
let sendWebhook = new WebhookSender({
type: 'custom',
@@ -101,7 +158,8 @@ describe('webhooks', () => {
let body = { foo: 'bar' };
sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}], body);
user.webhooks = [{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}];
sendWebhook.send(user, body);
expect(WebhookSender.defaultWebhookFilter).to.be.calledOnce;
});
@@ -117,7 +175,8 @@ describe('webhooks', () => {
let body = { foo: 'bar' };
sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}], body);
user.webhooks = [{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}];
sendWebhook.send(user, body);
expect(WebhookSender.defaultWebhookFilter).to.not.be.called;
expect(got.post).to.not.be.called;
@@ -134,10 +193,11 @@ describe('webhooks', () => {
let body = { foo: 'bar' };
sendWebhook.send([
user.webhooks = [
{ id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom', options: { foo: 'bar' }},
{ id: 'other-custom-webhook', url: 'http://other-custom-url.com', enabled: true, type: 'custom', options: { foo: 'foo' }},
], body);
];
sendWebhook.send(user, body);
expect(got.post).to.be.calledOnce;
expect(got.post).to.be.calledWithMatch('http://custom-url.com');
@@ -150,7 +210,8 @@ describe('webhooks', () => {
let body = { foo: 'bar' };
sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: false, type: 'custom'}], body);
user.webhooks = [{id: 'custom-webhook', url: 'http://custom-url.com', enabled: false, type: 'custom'}];
sendWebhook.send(user, body);
expect(got.post).to.not.be.called;
});
@@ -162,7 +223,8 @@ describe('webhooks', () => {
let body = { foo: 'bar' };
sendWebhook.send([{id: 'custom-webhook', url: 'httxp://custom-url!!', enabled: true, type: 'custom'}], body);
user.webhooks = [{id: 'custom-webhook', url: 'httxp://custom-url!!!', enabled: true, type: 'custom'}];
sendWebhook.send(user, body);
expect(got.post).to.not.be.called;
});
@@ -174,10 +236,30 @@ describe('webhooks', () => {
let body = { foo: 'bar' };
sendWebhook.send([
user.webhooks = [
{ id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'},
{ id: 'other-webhook', url: 'http://other-url.com', enabled: true, type: 'other'},
], body);
];
sendWebhook.send(user, body);
expect(got.post).to.be.calledOnce;
expect(got.post).to.be.calledWithMatch('http://custom-url.com', {
body,
json: true,
});
});
it('sends every type of activity to global webhooks', () => {
let sendWebhook = new WebhookSender({
type: 'custom',
});
let body = { foo: 'bar' };
user.webhooks = [
{ id: 'global-webhook', url: 'http://custom-url.com', enabled: true, type: 'globalActivity'},
];
sendWebhook.send(user, body);
expect(got.post).to.be.calledOnce;
expect(got.post).to.be.calledWithMatch('http://custom-url.com', {
@@ -193,10 +275,11 @@ describe('webhooks', () => {
let body = { foo: 'bar' };
sendWebhook.send([
user.webhooks = [
{ id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'},
{ id: 'other-custom-webhook', url: 'http://other-url.com', enabled: true, type: 'custom'},
], body);
];
sendWebhook.send(user, body);
expect(got.post).to.be.calledTwice;
expect(got.post).to.be.calledWithMatch('http://custom-url.com', {
@@ -216,7 +299,6 @@ describe('webhooks', () => {
beforeEach(() => {
data = {
user: {
_id: 'user-id',
_tmp: {foo: 'bar'},
stats: {
lvl: 5,
@@ -227,17 +309,6 @@ describe('webhooks', () => {
return this;
},
},
addComputedStatsToJSONObj () {
let mockStats = Object.assign({
maxHealth: 50,
maxMP: 103,
toNextLevel: 40,
}, this.stats);
delete mockStats.toJSON;
return mockStats;
},
},
task: {
text: 'text',
@@ -245,18 +316,66 @@ describe('webhooks', () => {
direction: 'up',
delta: 176,
};
let mockStats = Object.assign({
maxHealth: 50,
maxMP: 103,
toNextLevel: 40,
}, data.user.stats);
delete mockStats.toJSON;
sandbox.stub(User, 'addComputedStatsToJSONObj').returns(mockStats);
});
it('sends task and stats data', () => {
taskScoredWebhook.send(webhooks, data);
taskScoredWebhook.send(user, data);
expect(got.post).to.be.calledOnce;
expect(got.post).to.be.calledWithMatch(webhooks[0].url, {
json: true,
body: {
type: 'scored',
webhookType: 'taskActivity',
user: {
_id: 'user-id',
_id: user._id,
_tmp: {foo: 'bar'},
stats: {
lvl: 5,
int: 10,
str: 5,
exp: 423,
toNextLevel: 40,
maxHealth: 50,
maxMP: 103,
},
},
task: {
text: 'text',
},
direction: 'up',
delta: 176,
},
});
});
it('sends task and stats data to globalActivity webhookd', () => {
user.webhooks = [{
id: 'globalActivity',
url: 'http://global-activity.com',
enabled: true,
type: 'globalActivity',
}];
taskScoredWebhook.send(user, data);
expect(got.post).to.be.calledOnce;
expect(got.post).to.be.calledWithMatch('http://global-activity.com', {
json: true,
body: {
type: 'scored',
webhookType: 'taskActivity',
user: {
_id: user._id,
_tmp: {foo: 'bar'},
stats: {
lvl: 5,
@@ -280,7 +399,7 @@ describe('webhooks', () => {
it('does not send task scored data if scored option is not true', () => {
webhooks[0].options.scored = false;
taskScoredWebhook.send(webhooks, data);
taskScoredWebhook.send(user, data);
expect(got.post).to.not.be.called;
});
@@ -301,13 +420,17 @@ describe('webhooks', () => {
it(`sends ${type} tasks`, () => {
data.type = type;
taskActivityWebhook.send(webhooks, data);
taskActivityWebhook.send(user, data);
expect(got.post).to.be.calledOnce;
expect(got.post).to.be.calledWithMatch(webhooks[0].url, {
json: true,
body: {
type,
webhookType: 'taskActivity',
user: {
_id: user._id,
},
task: data.task,
},
});
@@ -317,7 +440,142 @@ describe('webhooks', () => {
data.type = type;
webhooks[0].options[type] = false;
taskActivityWebhook.send(webhooks, data);
taskActivityWebhook.send(user, data);
expect(got.post).to.not.be.called;
});
});
describe('checklistScored', () => {
beforeEach(() => {
data = {
task: {
text: 'text',
},
item: {
text: 'item-text',
},
};
});
it('sends \'checklistScored\' tasks', () => {
data.type = 'checklistScored';
taskActivityWebhook.send(user, data);
expect(got.post).to.be.calledOnce;
expect(got.post).to.be.calledWithMatch(webhooks[0].url, {
json: true,
body: {
webhookType: 'taskActivity',
user: {
_id: user._id,
},
type: data.type,
task: data.task,
item: data.item,
},
});
});
it('does not send task \'checklistScored\' data if \'checklistScored\' option is not true', () => {
data.type = 'checklistScored';
webhooks[0].options.checklistScored = false;
taskActivityWebhook.send(user, data);
expect(got.post).to.not.be.called;
});
});
});
describe('userActivityWebhook', () => {
let data;
beforeEach(() => {
data = {
something: true,
};
});
['petHatched', 'mountRaised', 'leveledUp'].forEach((type) => {
it(`sends ${type} webhooks`, () => {
data.type = type;
userActivityWebhook.send(user, data);
expect(got.post).to.be.calledOnce;
expect(got.post).to.be.calledWithMatch(webhooks[2].url, {
json: true,
body: {
type,
webhookType: 'userActivity',
user: {
_id: user._id,
},
something: true,
},
});
});
it(`does not send webhook ${type} data if ${type} option is not true`, () => {
data.type = type;
webhooks[2].options[type] = false;
userActivityWebhook.send(user, data);
expect(got.post).to.not.be.called;
});
});
});
describe('questActivityWebhook', () => {
let data;
beforeEach(() => {
data = {
group: {
id: 'group-id',
name: 'some group',
otherData: 'foo',
},
quest: {
key: 'some-key',
},
};
});
['questStarted', 'questFinised'].forEach((type) => {
it(`sends ${type} webhooks`, () => {
data.type = type;
questActivityWebhook.send(user, data);
expect(got.post).to.be.calledOnce;
expect(got.post).to.be.calledWithMatch(webhooks[1].url, {
json: true,
body: {
type,
webhookType: 'questActivity',
user: {
_id: user._id,
},
group: {
id: 'group-id',
name: 'some group',
},
quest: {
key: 'some-key',
},
},
});
});
it(`does not send webhook ${type} data if ${type} option is not true`, () => {
data.type = type;
webhooks[1].options[type] = false;
userActivityWebhook.send(user, data);
expect(got.post).to.not.be.called;
});
@@ -338,12 +596,16 @@ describe('webhooks', () => {
},
};
groupChatReceivedWebhook.send(webhooks, data);
groupChatReceivedWebhook.send(user, data);
expect(got.post).to.be.calledOnce;
expect(got.post).to.be.calledWithMatch(webhooks[webhooks.length - 1].url, {
json: true,
body: {
webhookType: 'groupChatReceived',
user: {
_id: user._id,
},
group: {
id: 'group-id',
name: 'some group',
@@ -369,7 +631,7 @@ describe('webhooks', () => {
},
};
groupChatReceivedWebhook.send(webhooks, data);
groupChatReceivedWebhook.send(user, data);
expect(got.post).to.not.be.called;
});

View File

@@ -3,14 +3,14 @@ import {
generateRes,
generateReq,
generateNext,
} from '../../../../helpers/api-unit.helper';
import analyticsService from '../../../../../website/server/libs/analyticsService';
} from '../../../helpers/api-unit.helper';
import analyticsService from '../../../../website/server/libs/analyticsService';
import nconf from 'nconf';
import requireAgain from 'require-again';
describe('analytics middleware', () => {
let res, req, next;
let pathToAnalyticsMiddleware = '../../../../../website/server/middlewares/analytics';
let pathToAnalyticsMiddleware = '../../../../website/server/middlewares/analytics';
beforeEach(() => {
res = generateRes();

View File

@@ -0,0 +1,40 @@
import {
generateRes,
generateReq,
} from '../../../helpers/api-unit.helper';
import { authWithHeaders as authWithHeadersFactory } from '../../../../website/server/middlewares/auth';
describe('auth middleware', () => {
let res, req, user;
beforeEach(async () => {
res = generateRes();
req = generateReq();
user = await res.locals.user.save();
});
describe('auth with headers', () => {
it('allows to specify a list of user field that we do not want to load', (done) => {
const authWithHeaders = authWithHeadersFactory({
userFieldsToExclude: ['items', 'flags', 'auth.timestamps'],
});
req.headers['x-api-user'] = user._id;
req.headers['x-api-key'] = user.apiToken;
authWithHeaders(req, res, (err) => {
if (err) return done(err);
const userToJSON = res.locals.user.toJSON();
expect(userToJSON.items).to.not.exist;
expect(userToJSON.flags).to.not.exist;
expect(userToJSON.auth.timestamps).to.not.exist;
expect(userToJSON.auth).to.exist;
expect(userToJSON.notifications).to.exist;
expect(userToJSON.preferences).to.exist;
done();
});
});
});
});

View File

@@ -3,8 +3,8 @@ import {
generateRes,
generateReq,
generateNext,
} from '../../../../helpers/api-unit.helper';
import cors from '../../../../../website/server/middlewares/cors';
} from '../../../helpers/api-unit.helper';
import cors from '../../../../website/server/middlewares/cors';
describe('cors middleware', () => {
let res, req, next;

View File

@@ -3,14 +3,14 @@ import {
generateReq,
generateTodo,
generateDaily,
} from '../../../../helpers/api-unit.helper';
import cronMiddleware from '../../../../../website/server/middlewares/cron';
} from '../../../helpers/api-unit.helper';
import cronMiddleware from '../../../../website/server/middlewares/cron';
import moment from 'moment';
import { model as User } from '../../../../../website/server/models/user';
import { model as Group } from '../../../../../website/server/models/group';
import * as Tasks from '../../../../../website/server/models/task';
import analyticsService from '../../../../../website/server/libs/analyticsService';
import * as cronLib from '../../../../../website/server/libs/cron';
import { model as User } from '../../../../website/server/models/user';
import { model as Group } from '../../../../website/server/models/group';
import * as Tasks from '../../../../website/server/models/task';
import analyticsService from '../../../../website/server/libs/analyticsService';
import * as cronLib from '../../../../website/server/libs/cron';
import { v4 as generateUUID } from 'uuid';
const CRON_TIMEOUT_WAIT = new Date(60 * 60 * 1000).getTime();
@@ -166,8 +166,11 @@ describe('cron middleware', () => {
await new Promise((resolve, reject) => {
cronMiddleware(req, res, (err) => {
if (err) return reject(err);
expect(user.stats.hp).to.be.lessThan(hpBefore);
resolve();
User.findOne({_id: user._id}, function (secondErr, updatedUser) {
if (secondErr) return reject(secondErr);
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
resolve();
});
});
});
});
@@ -176,7 +179,7 @@ describe('cron middleware', () => {
user.lastCron = moment(new Date()).subtract({days: 2});
let todo = generateTodo(user);
let todoValueBefore = todo.value;
await user.save();
await Promise.all([todo.save(), user.save()]);
await new Promise((resolve, reject) => {
cronMiddleware(req, res, (err) => {
@@ -217,8 +220,11 @@ describe('cron middleware', () => {
await new Promise((resolve, reject) => {
cronMiddleware(req, res, (err) => {
if (err) return reject(err);
expect(user.stats.hp).to.be.lessThan(hpBefore);
resolve();
User.findOne({_id: user._id}, function (secondErr, updatedUser) {
if (secondErr) return reject(secondErr);
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
resolve();
});
});
});
});

View File

@@ -3,11 +3,11 @@ import {
generateRes,
generateReq,
generateNext,
} from '../../../../helpers/api-unit.helper';
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';
} from '../../../helpers/api-unit.helper';
import i18n from '../../../../website/common/script/i18n';
import { ensureAdmin, ensureSudo } from '../../../../website/server/middlewares/ensureAccessRight';
import { NotAuthorized } from '../../../../website/server/libs/errors';
import apiError from '../../../../website/server/libs/apiError';
describe('ensure access middlewares', () => {
let res, req, next;
@@ -46,7 +46,7 @@ describe('ensure access middlewares', () => {
ensureSudo(req, res, next);
const calledWith = next.getCall(0).args;
expect(calledWith[0].message).to.equal(apiMessages('noSudoAccess'));
expect(calledWith[0].message).to.equal(apiError('noSudoAccess'));
expect(calledWith[0] instanceof NotAuthorized).to.equal(true);
});

View File

@@ -3,9 +3,9 @@ import {
generateRes,
generateReq,
generateNext,
} from '../../../../helpers/api-unit.helper';
import ensureDevelpmentMode from '../../../../../website/server/middlewares/ensureDevelpmentMode';
import { NotFound } from '../../../../../website/server/libs/errors';
} from '../../../helpers/api-unit.helper';
import ensureDevelpmentMode from '../../../../website/server/middlewares/ensureDevelpmentMode';
import { NotFound } from '../../../../website/server/libs/errors';
import nconf from 'nconf';
describe('developmentMode middleware', () => {

View File

@@ -2,17 +2,17 @@ import {
generateRes,
generateReq,
generateNext,
} from '../../../../helpers/api-unit.helper';
} from '../../../helpers/api-unit.helper';
import errorHandler from '../../../../../website/server/middlewares/errorHandler';
import responseMiddleware from '../../../../../website/server/middlewares/response';
import errorHandler from '../../../../website/server/middlewares/errorHandler';
import responseMiddleware from '../../../../website/server/middlewares/response';
import {
getUserLanguage,
attachTranslateFunction,
} from '../../../../../website/server/middlewares/language';
} from '../../../../website/server/middlewares/language';
import { BadRequest } from '../../../../../website/server/libs/errors';
import logger from '../../../../../website/server/libs/logger';
import { BadRequest } from '../../../../website/server/libs/errors';
import logger from '../../../../website/server/libs/logger';
describe('errorHandler', () => {
let res, req, next;

View File

@@ -2,13 +2,13 @@ import {
generateRes,
generateReq,
generateNext,
} from '../../../../helpers/api-unit.helper';
} from '../../../helpers/api-unit.helper';
import {
getUserLanguage,
attachTranslateFunction,
} from '../../../../../website/server/middlewares/language';
import common from '../../../../../website/common';
import { model as User } from '../../../../../website/server/models/user';
} from '../../../../website/server/middlewares/language';
import common from '../../../../website/common';
import { model as User } from '../../../../website/server/models/user';
const i18n = common.i18n;

View File

@@ -2,13 +2,13 @@ import {
generateRes,
generateReq,
generateNext,
} from '../../../../helpers/api-unit.helper';
} from '../../../helpers/api-unit.helper';
import nconf from 'nconf';
import requireAgain from 'require-again';
describe('maintenance mode middleware', () => {
let res, req, next;
let pathToMaintenanceModeMiddleware = '../../../../../website/server/middlewares/maintenanceMode';
let pathToMaintenanceModeMiddleware = '../../../../website/server/middlewares/maintenanceMode';
beforeEach(() => {
res = generateRes();

View File

@@ -2,13 +2,13 @@ import {
generateRes,
generateReq,
generateNext,
} from '../../../../helpers/api-unit.helper';
} from '../../../helpers/api-unit.helper';
import nconf from 'nconf';
import requireAgain from 'require-again';
describe('redirects middleware', () => {
let res, req, next;
let pathToRedirectsMiddleware = '../../../../../website/server/middlewares/redirects';
let pathToRedirectsMiddleware = '../../../../website/server/middlewares/redirects';
beforeEach(() => {
res = generateRes();

View File

@@ -2,9 +2,9 @@ import {
generateRes,
generateReq,
generateNext,
} from '../../../../helpers/api-unit.helper';
import responseMiddleware from '../../../../../website/server/middlewares/response';
import packageInfo from '../../../../../package.json';
} from '../../../helpers/api-unit.helper';
import responseMiddleware from '../../../../website/server/middlewares/response';
import packageInfo from '../../../../package.json';
describe('response middleware', () => {
let res, req, next;

View File

@@ -1,8 +1,8 @@
import { model as Challenge } from '../../../../../website/server/models/challenge';
import { model as Group } from '../../../../../website/server/models/group';
import { model as User } from '../../../../../website/server/models/user';
import * as Tasks from '../../../../../website/server/models/task';
import common from '../../../../../website/common/';
import { model as Challenge } from '../../../../website/server/models/challenge';
import { model as Group } from '../../../../website/server/models/group';
import { model as User } from '../../../../website/server/models/user';
import * as Tasks from '../../../../website/server/models/task';
import common from '../../../../website/common/';
import { each, find } from 'lodash';
describe('Challenge Model', () => {

View File

@@ -1,26 +1,30 @@
import moment from 'moment';
import { v4 as generateUUID } from 'uuid';
import validator from 'validator';
import { sleep } from '../../../../helpers/api-unit.helper';
import { sleep } from '../../../helpers/api-unit.helper';
import {
SPAM_MESSAGE_LIMIT,
SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
SPAM_WINDOW_LENGTH,
INVITES_LIMIT,
model as Group,
} from '../../../../../website/server/models/group';
import { model as User } from '../../../../../website/server/models/user';
import { quests as questScrolls } from '../../../../../website/common/script/content';
import { groupChatReceivedWebhook } from '../../../../../website/server/libs/webhook';
import * as email from '../../../../../website/server/libs/email';
import { TAVERN_ID } from '../../../../../website/common/script/';
import shared from '../../../../../website/common';
} from '../../../../website/server/models/group';
import { model as User } from '../../../../website/server/models/user';
import { quests as questScrolls } from '../../../../website/common/script/content';
import {
groupChatReceivedWebhook,
questActivityWebhook,
} from '../../../../website/server/libs/webhook';
import * as email from '../../../../website/server/libs/email';
import { TAVERN_ID } from '../../../../website/common/script/';
import shared from '../../../../website/common';
describe('Group Model', () => {
let party, questLeader, participatingMember, nonParticipatingMember, undecidedMember;
beforeEach(async () => {
sandbox.stub(email, 'sendTxn');
sandbox.stub(questActivityWebhook, 'send');
party = new Group({
name: 'test party',
@@ -182,7 +186,7 @@ describe('Group Model', () => {
await party.startQuest(questLeader);
await party.save();
sendChatStub = sandbox.stub(Group.prototype, 'sendChat');
sendChatStub = sandbox.spy(Group.prototype, 'sendChat');
});
afterEach(() => sendChatStub.restore());
@@ -378,7 +382,7 @@ describe('Group Model', () => {
await party.startQuest(questLeader);
await party.save();
sendChatStub = sandbox.stub(Group.prototype, 'sendChat');
sendChatStub = sandbox.spy(Group.prototype, 'sendChat');
});
afterEach(() => sendChatStub.restore());
@@ -918,21 +922,8 @@ describe('Group Model', () => {
sandbox.spy(User, 'update');
});
it('puts message at top of chat array', () => {
let oldMessage = {
text: 'a message',
};
party.chat.push(oldMessage, oldMessage, oldMessage);
party.sendChat('a new message', {_id: 'user-id', profile: { name: 'user name' }});
expect(party.chat).to.have.a.lengthOf(4);
expect(party.chat[0].text).to.eql('a new message');
expect(party.chat[0].uuid).to.eql('user-id');
});
it('formats message', () => {
party.sendChat('a new message', {
const chatMessage = party.sendChat('a new message', {
_id: 'user-id',
profile: { name: 'user name' },
contributor: {
@@ -947,11 +938,11 @@ describe('Group Model', () => {
},
});
let chat = party.chat[0];
const chat = chatMessage;
expect(chat.text).to.eql('a new message');
expect(validator.isUUID(chat.id)).to.eql(true);
expect(chat.timestamp).to.be.a('number');
expect(chat.timestamp).to.be.a('date');
expect(chat.likes).to.eql({});
expect(chat.flags).to.eql({});
expect(chat.flagCount).to.eql(0);
@@ -962,13 +953,11 @@ describe('Group Model', () => {
});
it('formats message as system if no user is passed in', () => {
party.sendChat('a system message');
let chat = party.chat[0];
const chat = party.sendChat('a system message');
expect(chat.text).to.eql('a system message');
expect(validator.isUUID(chat.id)).to.eql(true);
expect(chat.timestamp).to.be.a('number');
expect(chat.timestamp).to.be.a('date');
expect(chat.likes).to.eql({});
expect(chat.flags).to.eql({});
expect(chat.flagCount).to.eql(0);
@@ -1204,6 +1193,47 @@ describe('Group Model', () => {
expect(typeOfEmail).to.eql('quest-started');
});
it('sends webhook to participating members that quest has started', async () => {
// should receive webhook
participatingMember.webhooks = [{
type: 'questActivity',
url: 'http://someurl.com',
options: {
questStarted: true,
},
}];
questLeader.webhooks = [{
type: 'questActivity',
url: 'http://someurl.com',
options: {
questStarted: true,
},
}];
await Promise.all([participatingMember.save(), questLeader.save()]);
await party.startQuest(nonParticipatingMember);
await sleep(0.5);
expect(questActivityWebhook.send).to.be.calledTwice; // for 2 participating members
let args = questActivityWebhook.send.args[0];
let webhooks = args[0].webhooks;
let webhookOwner = args[0]._id;
let options = args[1];
expect(webhooks).to.have.a.lengthOf(1);
if (webhookOwner === questLeader._id) {
expect(webhooks[0].id).to.eql(questLeader.webhooks[0].id);
} else {
expect(webhooks[0].id).to.eql(participatingMember.webhooks[0].id);
}
expect(webhooks[0].type).to.eql('questActivity');
expect(options.group).to.eql(party);
expect(options.quest.key).to.eql('whale');
});
it('sends email only to members who have not opted out', async () => {
participatingMember.preferences.emailNotifications.questStarted = false;
questLeader.preferences.emailNotifications.questStarted = true;
@@ -1375,7 +1405,8 @@ describe('Group Model', () => {
expect(updatedParticipatingMember.achievements.quests[quest.key]).to.eql(1);
});
it('gives out super awesome Masterclasser achievement to the deserving', async () => {
// Disable test, it fails on TravisCI, but only there
xit('gives out super awesome Masterclasser achievement to the deserving', async () => {
quest = questScrolls.lostMasterclasser4;
party.quest.key = quest.key;
@@ -1584,6 +1615,42 @@ describe('Group Model', () => {
});
});
it('sends webhook to participating members that quest has finished', async () => {
// should receive webhook
participatingMember.webhooks = [{
type: 'questActivity',
url: 'http://someurl.com',
options: {
questFinished: true,
},
}];
questLeader.webhooks = [{
type: 'questActivity',
url: 'http://someurl.com',
options: {
questStarted: true, // will not receive the webhook
},
}];
await Promise.all([participatingMember.save(), questLeader.save()]);
await party.finishQuest(quest);
await sleep(0.5);
expect(questActivityWebhook.send).to.be.calledOnce;
let args = questActivityWebhook.send.args[0];
let webhooks = args[0].webhooks;
let options = args[1];
expect(webhooks).to.have.a.lengthOf(1);
expect(webhooks[0].id).to.eql(participatingMember.webhooks[0].id);
expect(webhooks[0].type).to.eql('questActivity');
expect(options.group).to.eql(party);
expect(options.quest.key).to.eql(quest.key);
});
context('World quests in Tavern', () => {
let tavernQuest;
@@ -1699,7 +1766,7 @@ describe('Group Model', () => {
expect(groupChatReceivedWebhook.send).to.be.calledOnce;
let args = groupChatReceivedWebhook.send.args[0];
let webhooks = args[0];
let webhooks = args[0].webhooks;
let options = args[1];
expect(webhooks).to.have.a.lengthOf(1);
@@ -1763,9 +1830,9 @@ describe('Group Model', () => {
expect(groupChatReceivedWebhook.send).to.be.calledThrice;
let args = groupChatReceivedWebhook.send.args;
expect(args.find(arg => arg[0][0].id === memberWithWebhook.webhooks[0].id)).to.be.exist;
expect(args.find(arg => arg[0][0].id === memberWithWebhook2.webhooks[0].id)).to.be.exist;
expect(args.find(arg => arg[0][0].id === memberWithWebhook3.webhooks[0].id)).to.be.exist;
expect(args.find(arg => arg[0].webhooks[0].id === memberWithWebhook.webhooks[0].id)).to.be.exist;
expect(args.find(arg => arg[0].webhooks[0].id === memberWithWebhook2.webhooks[0].id)).to.be.exist;
expect(args.find(arg => arg[0].webhooks[0].id === memberWithWebhook3.webhooks[0].id)).to.be.exist;
});
});

View File

@@ -1,7 +1,7 @@
import { model as Challenge } from '../../../../../website/server/models/challenge';
import { model as Group } from '../../../../../website/server/models/group';
import { model as User } from '../../../../../website/server/models/user';
import * as Tasks from '../../../../../website/server/models/task';
import { model as Challenge } from '../../../../website/server/models/challenge';
import { model as Group } from '../../../../website/server/models/group';
import { model as User } from '../../../../website/server/models/user';
import * as Tasks from '../../../../website/server/models/task';
import { each, find, findIndex } from 'lodash';
describe('Group Task Methods', () => {

View File

@@ -1,10 +1,10 @@
import { model as Challenge } from '../../../../../website/server/models/challenge';
import { model as Group } from '../../../../../website/server/models/group';
import { model as User } from '../../../../../website/server/models/user';
import * as Tasks from '../../../../../website/server/models/task';
import { InternalServerError } from '../../../../../website/server/libs/errors';
import { model as Challenge } from '../../../../website/server/models/challenge';
import { model as Group } from '../../../../website/server/models/group';
import { model as User } from '../../../../website/server/models/user';
import * as Tasks from '../../../../website/server/models/task';
import { InternalServerError } from '../../../../website/server/libs/errors';
import { each } from 'lodash';
import { generateHistory } from '../../../../helpers/api-unit.helper.js';
import { generateHistory } from '../../../helpers/api-unit.helper.js';
describe('Task Model', () => {
let guild, leader, challenge, task;

View File

@@ -1,7 +1,7 @@
import moment from 'moment';
import { model as User } from '../../../../../website/server/models/user';
import { model as Group } from '../../../../../website/server/models/group';
import common from '../../../../../website/common';
import { model as User } from '../../../../website/server/models/user';
import { model as Group } from '../../../../website/server/models/group';
import common from '../../../../website/common';
describe('User Model', () => {
it('keeps user._tmp when calling .toJSON', () => {
@@ -42,13 +42,48 @@ describe('User Model', () => {
expect(userToJSON.stats.maxHealth).to.not.exist;
expect(userToJSON.stats.toNextLevel).to.not.exist;
user.addComputedStatsToJSONObj(userToJSON.stats);
User.addComputedStatsToJSONObj(userToJSON.stats, userToJSON);
expect(userToJSON.stats.maxMP).to.exist;
expect(userToJSON.stats.maxHealth).to.equal(common.maxHealth);
expect(userToJSON.stats.toNextLevel).to.equal(common.tnl(user.stats.lvl));
});
it('can transform user object without mongoose helpers', async () => {
let user = new User();
await user.save();
let userToJSON = await User.findById(user._id).lean().exec();
expect(userToJSON.stats.maxMP).to.not.exist;
expect(userToJSON.stats.maxHealth).to.not.exist;
expect(userToJSON.stats.toNextLevel).to.not.exist;
expect(userToJSON.id).to.not.exist;
User.transformJSONUser(userToJSON);
expect(userToJSON.id).to.equal(userToJSON._id);
expect(userToJSON.stats.maxMP).to.not.exist;
expect(userToJSON.stats.maxHealth).to.not.exist;
expect(userToJSON.stats.toNextLevel).to.not.exist;
});
it('can transform user object without mongoose helpers (including computed stats)', async () => {
let user = new User();
await user.save();
let userToJSON = await User.findById(user._id).lean().exec();
expect(userToJSON.stats.maxMP).to.not.exist;
expect(userToJSON.stats.maxHealth).to.not.exist;
expect(userToJSON.stats.toNextLevel).to.not.exist;
User.transformJSONUser(userToJSON, true);
expect(userToJSON.id).to.equal(userToJSON._id);
expect(userToJSON.stats.maxMP).to.exist;
expect(userToJSON.stats.maxHealth).to.equal(common.maxHealth);
expect(userToJSON.stats.toNextLevel).to.equal(common.tnl(user.stats.lvl));
});
context('notifications', () => {
it('can add notifications without data', () => {
let user = new User();

View File

@@ -1,4 +1,4 @@
import { model as UserNotification } from '../../../../../website/server/models/userNotification';
import { model as UserNotification } from '../../../../website/server/models/userNotification';
describe('UserNotification Model', () => {
context('convertNotificationsToSafeJson', () => {

View File

@@ -0,0 +1,323 @@
import { model as Webhook } from '../../../../website/server/models/webhook';
import { BadRequest } from '../../../../website/server/libs/errors';
import { v4 as generateUUID } from 'uuid';
import apiError from '../../../../website/server/libs/apiError';
describe('Webhook Model', () => {
context('Instance Methods', () => {
describe('#formatOptions', () => {
let res;
beforeEach(() => {
res = {
t: sandbox.spy(),
};
});
context('type is taskActivity', () => {
let config;
beforeEach(() => {
config = {
type: 'taskActivity',
url: 'https//exmaple.com/endpoint',
options: {
created: true,
updated: true,
deleted: true,
scored: true,
checklistScored: true,
},
};
});
it('it provides default values for options', () => {
delete config.options;
let wh = new Webhook(config);
wh.formatOptions(res);
expect(wh.options).to.eql({
checklistScored: false,
created: false,
updated: false,
deleted: false,
scored: true,
});
});
it('provides missing task options', () => {
delete config.options.created;
let wh = new Webhook(config);
wh.formatOptions(res);
expect(wh.options).to.eql({
checklistScored: true,
created: false,
updated: true,
deleted: true,
scored: true,
});
});
it('discards additional options', () => {
config.options.foo = 'another option';
let wh = new Webhook(config);
wh.formatOptions(res);
expect(wh.options.foo).to.not.exist;
expect(wh.options).to.eql({
checklistScored: true,
created: true,
updated: true,
deleted: true,
scored: true,
});
});
['created', 'updated', 'deleted', 'scored', 'checklistScored'].forEach((option) => {
it(`validates that ${option} is a boolean`, (done) => {
config.options[option] = 'not a boolean';
try {
let wh = new Webhook(config);
wh.formatOptions(res);
} catch (err) {
expect(err).to.be.an.instanceOf(BadRequest);
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('webhookBooleanOption', { option });
done();
}
});
});
});
context('type is userActivity', () => {
let config;
beforeEach(() => {
config = {
type: 'userActivity',
url: 'https//exmaple.com/endpoint',
options: {
petHatched: true,
mountRaised: true,
leveledUp: true,
},
};
});
it('it provides default values for options', () => {
delete config.options;
let wh = new Webhook(config);
wh.formatOptions(res);
expect(wh.options).to.eql({
petHatched: false,
mountRaised: false,
leveledUp: false,
});
});
it('provides missing user options', () => {
delete config.options.petHatched;
let wh = new Webhook(config);
wh.formatOptions(res);
expect(wh.options).to.eql({
petHatched: false,
mountRaised: true,
leveledUp: true,
});
});
it('discards additional options', () => {
config.options.foo = 'another option';
let wh = new Webhook(config);
wh.formatOptions(res);
expect(wh.options.foo).to.not.exist;
expect(wh.options).to.eql({
petHatched: true,
mountRaised: true,
leveledUp: true,
});
});
['petHatched', 'petHatched', 'leveledUp'].forEach((option) => {
it(`validates that ${option} is a boolean`, (done) => {
config.options[option] = 'not a boolean';
try {
let wh = new Webhook(config);
wh.formatOptions(res);
} catch (err) {
expect(err).to.be.an.instanceOf(BadRequest);
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('webhookBooleanOption', { option });
done();
}
});
});
});
context('type is questActivity', () => {
let config;
beforeEach(() => {
config = {
type: 'questActivity',
url: 'https//exmaple.com/endpoint',
options: {
questStarted: true,
questFinished: true,
},
};
});
it('it provides default values for options', () => {
delete config.options;
let wh = new Webhook(config);
wh.formatOptions(res);
expect(wh.options).to.eql({
questStarted: false,
questFinished: false,
});
});
it('provides missing user options', () => {
delete config.options.questStarted;
let wh = new Webhook(config);
wh.formatOptions(res);
expect(wh.options).to.eql({
questStarted: false,
questFinished: true,
});
});
it('discards additional options', () => {
config.options.foo = 'another option';
let wh = new Webhook(config);
wh.formatOptions(res);
expect(wh.options.foo).to.not.exist;
expect(wh.options).to.eql({
questStarted: true,
questFinished: true,
});
});
['questStarted', 'questFinished'].forEach((option) => {
it(`validates that ${option} is a boolean`, (done) => {
config.options[option] = 'not a boolean';
try {
let wh = new Webhook(config);
wh.formatOptions(res);
} catch (err) {
expect(err).to.be.an.instanceOf(BadRequest);
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('webhookBooleanOption', { option });
done();
}
});
});
});
context('type is groupChatReceived', () => {
let config;
beforeEach(() => {
config = {
type: 'groupChatReceived',
url: 'https//exmaple.com/endpoint',
options: {
groupId: generateUUID(),
},
};
});
it('creates options', () => {
let wh = new Webhook(config);
wh.formatOptions(res);
expect(wh.options).to.eql(config.options);
});
it('discards additional objects', () => {
config.options.foo = 'another thing';
let wh = new Webhook(config);
wh.formatOptions(res);
expect(wh.options.foo).to.not.exist;
expect(wh.options).to.eql({
groupId: config.options.groupId,
});
});
it('requires groupId option to be a uuid', (done) => {
config.options.groupId = 'not a uuid';
try {
let wh = new Webhook(config);
wh.formatOptions(res);
} catch (err) {
expect(err).to.be.an.instanceOf(BadRequest);
expect(err.message).to.eql(apiError('groupIdRequired'));
done();
}
});
});
context('type is globalActivity', () => {
let config;
beforeEach(() => {
config = {
type: 'globalActivity',
url: 'https//exmaple.com/endpoint',
options: { },
};
});
it('discards additional objects', () => {
config.options.foo = 'another thing';
let wh = new Webhook(config);
wh.formatOptions(res);
expect(wh.options.foo).to.not.exist;
expect(wh.options).to.eql({});
});
});
});
});
});

View File

@@ -5,7 +5,7 @@ import {
sleep,
checkExistence,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
} from '../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
describe('DELETE /challenges/:challengeId', () => {
@@ -41,6 +41,7 @@ describe('DELETE /challenges/:challengeId', () => {
group = populatedGroup.group;
challenge = await generateChallenge(groupLeader, group);
await groupLeader.post(`/challenges/${challenge._id}/join`);
await groupLeader.post(`/tasks/challenge/${challenge._id}`, [
{type: 'habit', text: taskText},

View File

@@ -3,7 +3,7 @@ import {
createAndPopulateGroup,
generateChallenge,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
} from '../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
describe('GET /challenges/:challengeId', () => {
@@ -33,9 +33,11 @@ describe('GET /challenges/:challengeId', () => {
group = populatedGroup.group;
challenge = await generateChallenge(groupLeader, group);
await groupLeader.post(`/challenges/${challenge._id}/join`);
});
it('should return challenge data', async () => {
await challenge.sync();
let chal = await user.get(`/challenges/${challenge._id}`);
expect(chal.memberCount).to.equal(challenge.memberCount);
expect(chal.name).to.equal(challenge.name);
@@ -80,6 +82,7 @@ describe('GET /challenges/:challengeId', () => {
challenge = await generateChallenge(groupLeader, group);
await members[0].post(`/challenges/${challenge._id}/join`);
await groupLeader.post(`/challenges/${challenge._id}/join`);
});
it('fails if user doesn\'t have access to the challenge', async () => {
@@ -134,6 +137,7 @@ describe('GET /challenges/:challengeId', () => {
challenge = await generateChallenge(groupLeader, group);
await members[0].post(`/challenges/${challenge._id}/join`);
await groupLeader.post(`/challenges/${challenge._id}/join`);
});
it('fails if user doesn\'t have access to the challenge', async () => {

View File

@@ -4,7 +4,7 @@ import {
generateChallenge,
translate as t,
sleep,
} from '../../../../helpers/api-v3-integration.helper';
} from '../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
describe('GET /challenges/:challengeId/export/csv', () => {
@@ -24,6 +24,7 @@ describe('GET /challenges/:challengeId/export/csv', () => {
members = populatedGroup.members;
challenge = await generateChallenge(groupLeader, group);
await groupLeader.post(`/challenges/${challenge._id}/join`);
await members[0].post(`/challenges/${challenge._id}/join`);
await members[1].post(`/challenges/${challenge._id}/join`);
await members[2].post(`/challenges/${challenge._id}/join`);
@@ -60,9 +61,9 @@ describe('GET /challenges/:challengeId/export/csv', () => {
});
it('should return a valid CSV file with export data', async () => {
let res = await members[0].get(`/challenges/${challenge._id}/export/csv`);
let sortedMembers = _.sortBy([members[0], members[1], members[2], groupLeader], '_id');
let splitRes = res.split('\n');
const res = await members[0].get(`/challenges/${challenge._id}/export/csv`);
const sortedMembers = _.sortBy([members[0], members[1], members[2], groupLeader], '_id');
const splitRes = res.split('\n');
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`);
@@ -71,4 +72,16 @@ describe('GET /challenges/:challengeId/export/csv', () => {
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('');
});
it('should successfully return when it contains erroneous residue user data', async () => {
await members[0].update({challenges: []});
const res = await members[1].get(`/challenges/${challenge._id}/export/csv`);
const sortedMembers = _.sortBy([members[1], members[2], groupLeader], '_id');
const splitRes = res.split('\n');
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('');
});
});

View File

@@ -3,7 +3,7 @@ import {
generateGroup,
generateChallenge,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
} from '../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
describe('GET /challenges/:challengeId/members', () => {
@@ -45,6 +45,7 @@ describe('GET /challenges/:challengeId/members', () => {
let leader = await generateUser({balance: 4});
let group = await generateGroup(leader, {type: 'guild', privacy: 'public', name: generateUUID()});
let challenge = await generateChallenge(leader, group);
await leader.post(`/challenges/${challenge._id}/join`);
let res = await user.get(`/challenges/${challenge._id}/members`);
expect(res[0]).to.eql({
_id: leader._id,
@@ -59,6 +60,7 @@ describe('GET /challenges/:challengeId/members', () => {
let anotherUser = await generateUser({balance: 3});
let group = await generateGroup(anotherUser, {type: 'guild', privacy: 'public', name: generateUUID()});
let challenge = await generateChallenge(anotherUser, group);
await anotherUser.post(`/challenges/${challenge._id}/join`);
let res = await user.get(`/challenges/${challenge._id}/members`);
expect(res[0]).to.eql({
_id: anotherUser._id,
@@ -72,6 +74,7 @@ describe('GET /challenges/:challengeId/members', () => {
it('returns only first 30 members if req.query.includeAllMembers is not true', async () => {
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
let challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
let usersToGenerate = [];
for (let i = 0; i < 31; i++) {
@@ -90,6 +93,7 @@ describe('GET /challenges/:challengeId/members', () => {
it('returns only first 30 members if req.query.includeAllMembers is not defined', async () => {
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
let challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
let usersToGenerate = [];
for (let i = 0; i < 31; i++) {
@@ -108,6 +112,7 @@ describe('GET /challenges/:challengeId/members', () => {
it('returns all members if req.query.includeAllMembers is true', async () => {
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
let challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
let usersToGenerate = [];
for (let i = 0; i < 31; i++) {
@@ -123,9 +128,11 @@ describe('GET /challenges/:challengeId/members', () => {
});
});
it('supports using req.query.lastId to get more members', async () => {
it('supports using req.query.lastId to get more members', async function () {
this.timeout(30000); // @TODO: times out after 8 seconds
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
let challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
let usersToGenerate = [];
for (let i = 0; i < 57; i++) {
@@ -146,6 +153,7 @@ describe('GET /challenges/:challengeId/members', () => {
it('supports using req.query.search to get search members', async () => {
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
let challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
let usersToGenerate = [];
for (let i = 0; i < 3; i++) {

View File

@@ -3,7 +3,7 @@ import {
generateChallenge,
generateGroup,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
} from '../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
describe('GET /challenges/:challengeId/members/:memberId', () => {
@@ -50,6 +50,7 @@ describe('GET /challenges/:challengeId/members/:memberId', () => {
it('fails if user doesn\'t have access to the challenge', async () => {
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
let challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
let anotherUser = await generateUser();
let member = await generateUser();
await expect(anotherUser.get(`/challenges/${challenge._id}/members/${member._id}`)).to.eventually.be.rejected.and.eql({
@@ -62,6 +63,7 @@ describe('GET /challenges/:challengeId/members/:memberId', () => {
it('fails if member is not part of the challenge', async () => {
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
let challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
let member = await generateUser();
await expect(user.get(`/challenges/${challenge._id}/members/${member._id}`)).to.eventually.be.rejected.and.eql({
code: 404,
@@ -74,6 +76,7 @@ describe('GET /challenges/:challengeId/members/:memberId', () => {
let groupLeader = await generateUser({balance: 4});
let group = await generateGroup(groupLeader, {type: 'guild', privacy: 'public', name: generateUUID()});
let challenge = await generateChallenge(groupLeader, group);
await groupLeader.post(`/challenges/${challenge._id}/join`);
let taskText = 'Test Text';
await groupLeader.post(`/tasks/challenge/${challenge._id}`, [{type: 'habit', text: taskText}]);
@@ -86,6 +89,7 @@ describe('GET /challenges/:challengeId/members/:memberId', () => {
it('returns the member tasks for the challenges', async () => {
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
let challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
await user.post(`/tasks/challenge/${challenge._id}`, [{type: 'habit', text: 'Test Text'}]);
let memberProgress = await user.get(`/challenges/${challenge._id}/members/${user._id}`);
@@ -98,6 +102,7 @@ describe('GET /challenges/:challengeId/members/:memberId', () => {
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);
await user.post(`/challenges/${challenge._id}/join`);
let taskText = 'Test Text';
await user.post(`/tasks/challenge/${challenge._id}`, [{
type: 'todo',

View File

@@ -3,7 +3,7 @@ import {
generateChallenge,
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
} from '../../../../helpers/api-integration/v3';
import { TAVERN_ID } from '../../../../../website/common/script/constants';
describe('GET challenges/groups/:groupId', () => {
@@ -25,7 +25,9 @@ describe('GET challenges/groups/:groupId', () => {
nonMember = await generateUser();
challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
challenge2 = await generateChallenge(user, group);
await user.post(`/challenges/${challenge2._id}/join`);
});
it('should return group challenges for non member with populated leader', async () => {
@@ -73,6 +75,7 @@ describe('GET challenges/groups/:groupId', () => {
expect(foundChallengeIndex).to.eql(0);
let newChallenge = await generateChallenge(user, publicGuild);
await user.post(`/challenges/${newChallenge._id}/join`);
challenges = await user.get(`/challenges/groups/${publicGuild._id}`);
@@ -99,7 +102,9 @@ describe('GET challenges/groups/:groupId', () => {
nonMember = await generateUser();
challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
challenge2 = await generateChallenge(user, group);
await user.post(`/challenges/${challenge2._id}/join`);
});
it('should prevent non-member from seeing challenges', async () => {
@@ -156,9 +161,12 @@ describe('GET challenges/groups/:groupId', () => {
slug: 'habitica_official',
}],
});
await user.post(`/challenges/${officialChallenge._id}/join`);
challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
challenge2 = await generateChallenge(user, group);
await user.post(`/challenges/${challenge2._id}/join`);
});
it('should return official challenges first', async () => {
@@ -178,6 +186,7 @@ describe('GET challenges/groups/:groupId', () => {
expect(foundChallengeIndex).to.eql(1);
let newChallenge = await generateChallenge(user, publicGuild);
await user.post(`/challenges/${newChallenge._id}/join`);
challenges = await user.get(`/challenges/groups/${publicGuild._id}`);
@@ -203,7 +212,9 @@ describe('GET challenges/groups/:groupId', () => {
nonMember = await generateUser();
challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
challenge2 = await generateChallenge(user, group);
await user.post(`/challenges/${challenge2._id}/join`);
});
it('should prevent non-member from seeing challenges', async () => {
@@ -263,7 +274,9 @@ describe('GET challenges/groups/:groupId', () => {
tavern = await user.get(`/groups/${TAVERN_ID}`);
challenge = await generateChallenge(user, tavern, {prize: 1});
await user.post(`/challenges/${challenge._id}/join`);
challenge2 = await generateChallenge(user, tavern, {prize: 1});
await user.post(`/challenges/${challenge2._id}/join`);
});
it('should return tavern challenges with populated leader', async () => {

View File

@@ -2,7 +2,7 @@ import {
generateUser,
generateChallenge,
createAndPopulateGroup,
} from '../../../../helpers/api-v3-integration.helper';
} from '../../../../helpers/api-integration/v3';
describe('GET challenges/user', () => {
context('no official challenges', () => {
@@ -24,7 +24,9 @@ describe('GET challenges/user', () => {
nonMember = await generateUser();
challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
challenge2 = await generateChallenge(user, group);
await user.post(`/challenges/${challenge2._id}/join`);
});
it('should return challenges user has joined', async () => {
@@ -146,6 +148,7 @@ describe('GET challenges/user', () => {
expect(foundChallengeIndex).to.eql(0);
let newChallenge = await generateChallenge(user, publicGuild);
await user.post(`/challenges/${newChallenge._id}/join`);
challenges = await user.get('/challenges/user');
@@ -164,6 +167,7 @@ describe('GET challenges/user', () => {
});
let privateChallenge = await generateChallenge(groupLeader, group);
await groupLeader.post(`/challenges/${privateChallenge._id}/join`);
let challenges = await nonMember.get('/challenges/user');
@@ -198,9 +202,12 @@ describe('GET challenges/user', () => {
slug: 'habitica_official',
}],
});
await user.post(`/challenges/${officialChallenge._id}/join`);
challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
challenge2 = await generateChallenge(user, group);
await user.post(`/challenges/${challenge2._id}/join`);
});
it('should return official challenges first', async () => {
@@ -220,6 +227,7 @@ describe('GET challenges/user', () => {
expect(foundChallengeIndex).to.eql(1);
let newChallenge = await generateChallenge(user, publicGuild);
await user.post(`/challenges/${newChallenge._id}/join`);
challenges = await user.get('/challenges/user');
@@ -252,12 +260,14 @@ describe('GET challenges/user', () => {
await user.update({balance: 20});
for (let i = 0; i < 11; i += 1) {
await generateChallenge(user, group); // eslint-disable-line
let challenge = await generateChallenge(user, group); // eslint-disable-line
await user.post(`/challenges/${challenge._id}/join`); // eslint-disable-line
}
});
it('returns public guilds filtered by category', async () => {
const categoryChallenge = await generateChallenge(user, guild, {categories});
await user.post(`/challenges/${categoryChallenge._id}/join`);
const challenges = await user.get(`/challenges/user?categories=${categories[0].slug}`);
expect(challenges[0]._id).to.eql(categoryChallenge._id);

View File

@@ -2,7 +2,7 @@ import {
generateUser,
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
} from '../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
describe('POST /challenges', () => {
@@ -242,7 +242,6 @@ describe('POST /challenges', () => {
it('returns an error when challenge validation fails; doesn\'s save user or group', async () => {
let oldChallengeCount = group.challengeCount;
let oldUserBalance = groupLeader.balance;
let oldUserChallenges = groupLeader.challenges;
let oldGroupBalance = group.balance;
await expect(groupLeader.post('/challenges', {
@@ -260,7 +259,6 @@ describe('POST /challenges', () => {
expect(group.challengeCount).to.eql(oldChallengeCount);
expect(group.balance).to.eql(oldGroupBalance);
expect(groupLeader.balance).to.eql(oldUserBalance);
expect(groupLeader.challenges).to.eql(oldUserChallenges);
});
it('sets all properites of the challenge as passed', async () => {
@@ -291,18 +289,19 @@ describe('POST /challenges', () => {
name: group.name,
type: group.type,
});
expect(challenge.memberCount).to.eql(1);
expect(challenge.memberCount).to.eql(0);
expect(challenge.prize).to.eql(prize);
});
it('adds challenge to creator\'s challenges', async () => {
let challenge = await groupLeader.post('/challenges', {
it('does not add challenge to creator\'s challenges', async () => {
await groupLeader.post('/challenges', {
group: group._id,
name: 'Test Challenge',
shortName: 'TC Label',
});
await expect(groupLeader.sync()).to.eventually.have.property('challenges').to.include(challenge._id);
await groupLeader.sync();
expect(groupLeader.challenges.length).to.equal(0);
});
it('awards achievement if this is creator\'s first challenge', async () => {

View File

@@ -3,7 +3,7 @@ import {
generateChallenge,
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
} from '../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
describe('POST /challenges/:challengeId/join', () => {
@@ -43,6 +43,7 @@ describe('POST /challenges/:challengeId/join', () => {
authorizedUser = populatedGroup.members[0];
challenge = await generateChallenge(groupLeader, group);
await groupLeader.post(`/challenges/${challenge._id}/join`);
});
it('returns an error when user doesn\'t have permissions to access the challenge', async () => {
@@ -91,6 +92,7 @@ describe('POST /challenges/:challengeId/join', () => {
});
it('increases memberCount of challenge', async () => {
await challenge.sync();
let oldMemberCount = challenge.memberCount;
await authorizedUser.post(`/challenges/${challenge._id}/join`);

View File

@@ -3,7 +3,7 @@ import {
generateChallenge,
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
} from '../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
describe('POST /challenges/:challengeId/leave', () => {
@@ -48,6 +48,7 @@ describe('POST /challenges/:challengeId/leave', () => {
notInGroupLeavingUser = populatedGroup.members[2];
challenge = await generateChallenge(groupLeader, group);
await groupLeader.post(`/challenges/${challenge._id}/join`);
taskText = 'A challenge task text';

View File

@@ -5,7 +5,7 @@ import {
sleep,
checkExistence,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
} from '../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
describe('POST /challenges/:challengeId/winner/:winnerId', () => {
@@ -58,6 +58,7 @@ describe('POST /challenges/:challengeId/winner/:winnerId', () => {
challenge = await generateChallenge(groupLeader, group, {
prize: 1,
});
await groupLeader.post(`/challenges/${challenge._id}/join`);
await groupLeader.post(`/tasks/challenge/${challenge._id}`, [
{type: 'habit', text: taskText},

View File

@@ -1,7 +1,7 @@
import {
generateUser,
generateGroup,
} from '../../../../helpers/api-v3-integration.helper';
} from '../../../../helpers/api-integration/v3';
describe('POST /challenges/:challengeId/clone', () => {
it('clones a challenge', async () => {

View File

@@ -3,7 +3,7 @@ import {
generateChallenge,
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
} from '../../../../helpers/api-integration/v3';
describe('PUT /challenges/:challengeId', () => {
let privateGuild, user, nonMember, challenge, member;
@@ -25,6 +25,7 @@ describe('PUT /challenges/:challengeId', () => {
member = members[0];
challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
await member.post(`/challenges/${challenge._id}/join`);
});

View File

@@ -2,7 +2,7 @@ import {
createAndPopulateGroup,
generateUser,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
} from '../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
describe('DELETE /groups/:groupId/chat/:chatId', () => {
@@ -53,16 +53,26 @@ describe('DELETE /groups/:groupId/chat/:chatId', () => {
it('allows creator to delete a their message', async () => {
await user.del(`/groups/${groupWithChat._id}/chat/${nextMessage.id}`);
let messages = await user.get(`/groups/${groupWithChat._id}/chat/`);
expect(messages).is.an('array');
expect(messages).to.not.include(nextMessage);
const returnedMessages = await user.get(`/groups/${groupWithChat._id}/chat/`);
const messageFromUser = returnedMessages.find(returnedMessage => {
return returnedMessage.id === nextMessage.id;
});
expect(returnedMessages).is.an('array');
expect(messageFromUser).to.not.exist;
});
it('allows admin to delete another user\'s message', async () => {
await admin.del(`/groups/${groupWithChat._id}/chat/${nextMessage.id}`);
let messages = await user.get(`/groups/${groupWithChat._id}/chat/`);
expect(messages).is.an('array');
expect(messages).to.not.include(nextMessage);
const returnedMessages = await user.get(`/groups/${groupWithChat._id}/chat/`);
const messageFromUser = returnedMessages.find(returnedMessage => {
return returnedMessage.id === nextMessage.id;
});
expect(returnedMessages).is.an('array');
expect(messageFromUser).to.not.exist;
});
it('returns empty when previous message parameter is passed and the last message was deleted', async () => {
@@ -71,9 +81,9 @@ describe('DELETE /groups/:groupId/chat/:chatId', () => {
});
it('returns the update chat when previous message parameter is passed and the chat is updated', async () => {
let deleteResult = await user.del(`/groups/${groupWithChat._id}/chat/${nextMessage.id}?previousMsg=${message.id}`);
const updatedChat = await user.del(`/groups/${groupWithChat._id}/chat/${nextMessage.id}?previousMsg=${message.id}`);
expect(deleteResult[0].id).to.eql(message.id);
expect(updatedChat[0].id).to.eql(message.id);
});
});
});

View File

@@ -2,7 +2,7 @@ import {
generateUser,
generateGroup,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
} from '../../../../helpers/api-integration/v3';
describe('GET /groups/:groupId/chat', () => {
let user;
@@ -23,16 +23,17 @@ describe('GET /groups/:groupId/chat', () => {
privacy: 'public',
}, {
chat: [
{text: 'Hello', flags: {}},
{text: 'Welcome to the Guild', flags: {}},
{text: 'Hello', flags: {}, id: 1},
{text: 'Welcome to the Guild', flags: {}, id: 2},
],
});
});
it('returns Guild chat', async () => {
let chat = await user.get(`/groups/${group._id}/chat`);
const chat = await user.get(`/groups/${group._id}/chat`);
expect(chat).to.eql(group.chat);
expect(chat[0].id).to.eql(group.chat[0].id);
expect(chat[1].id).to.eql(group.chat[1].id);
});
});

View File

@@ -1,7 +1,7 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
} from '../../../../helpers/api-integration/v3';
import { find } from 'lodash';
describe('POST /chat/:chatId/flag', () => {

View File

@@ -1,7 +1,7 @@
import {
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
} from '../../../../helpers/api-integration/v3';
import { find } from 'lodash';
describe('POST /chat/:chatId/like', () => {

View File

@@ -4,14 +4,14 @@ import {
translate as t,
sleep,
server,
} from '../../../../helpers/api-v3-integration.helper';
} from '../../../../helpers/api-integration/v3';
import {
SPAM_MESSAGE_LIMIT,
SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
TAVERN_ID,
} from '../../../../../website/server/models/group';
import { v4 as generateUUID } from 'uuid';
import { getMatchesByWordArray, removePunctuationFromString } from '../../../../../website/server/libs/stringUtils';
import { getMatchesByWordArray } from '../../../../../website/server/libs/stringUtils';
import bannedWords from '../../../../../website/server/libs/bannedWords';
import guildsAllowingBannedWords from '../../../../../website/server/libs/guildsAllowingBannedWords';
import * as email from '../../../../../website/server/libs/email';
@@ -24,10 +24,10 @@ describe('POST /chat', () => {
let user, groupWithChat, member, additionalMember;
let testMessage = 'Test Message';
let testBannedWordMessage = 'TESTPLACEHOLDERSWEARWORDHERE';
let testBannedWordMessage1 = 'TESTPLACEHOLDERSWEARWORDHERE1';
let testSlurMessage = 'message with TESTPLACEHOLDERSLURWORDHERE';
let bannedWordErrorMessage = t('bannedWordUsed').split('.');
bannedWordErrorMessage[0] += ` (${removePunctuationFromString(testBannedWordMessage.toLowerCase())})`;
bannedWordErrorMessage = bannedWordErrorMessage.join('.');
let testSlurMessage1 = 'TESTPLACEHOLDERSLURWORDHERE1';
let bannedWordErrorMessage = t('bannedWordUsed', {swearWordsUsed: testBannedWordMessage});
before(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
@@ -39,6 +39,7 @@ describe('POST /chat', () => {
members: 2,
});
user = groupLeader;
await user.update({'contributor.level': SPAM_MIN_EXEMPT_CONTRIB_LEVEL}); // prevent tests accidentally throwing messageGroupChatSpam
groupWithChat = group;
member = members[0];
additionalMember = members[1];
@@ -136,9 +137,19 @@ describe('POST /chat', () => {
});
});
it('checks error message has the banned words used', async () => {
let randIndex = Math.floor(Math.random() * (bannedWords.length + 1));
let testBannedWords = bannedWords.slice(randIndex, randIndex + 2).map((w) => w.replace(/\\/g, ''));
it('errors when word is typed in mixed case', async () => {
let substrLength = Math.floor(testBannedWordMessage.length / 2);
let chatMessage = testBannedWordMessage.substring(0, substrLength).toLowerCase() + testBannedWordMessage.substring(substrLength).toUpperCase();
await expect(user.post('/groups/habitrpg/chat', { message: chatMessage }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedWordUsed', {swearWordsUsed: chatMessage}),
});
});
it('checks error message has all the banned words used, regardless of case', async () => {
let testBannedWords = [testBannedWordMessage.toUpperCase(), testBannedWordMessage1.toLowerCase()];
let chatMessage = `Mixing ${testBannedWords[0]} and ${testBannedWords[1]} is bad for you.`;
await expect(user.post('/groups/habitrpg/chat', { message: chatMessage}))
.to.eventually.be.rejected
@@ -320,6 +331,17 @@ describe('POST /chat', () => {
members[0].flags.chatRevoked = false;
await members[0].update({'flags.chatRevoked': false});
});
it('errors when slur is typed in mixed case', async () => {
let substrLength = Math.floor(testSlurMessage1.length / 2);
let chatMessage = testSlurMessage1.substring(0, substrLength).toLowerCase() + testSlurMessage1.substring(substrLength).toUpperCase();
await expect(user.post('/groups/habitrpg/chat', { message: chatMessage }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedSlurUsed'),
});
});
});
it('does not error when sending a message to a private guild with a user with revoked chat', async () => {
@@ -359,9 +381,11 @@ describe('POST /chat', () => {
});
it('creates a chat', async () => {
let message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage});
const newMessage = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage});
const groupMessages = await user.get(`/groups/${groupWithChat._id}/chat`);
expect(message.message.id).to.exist;
expect(newMessage.message.id).to.exist;
expect(groupMessages[0].id).to.exist;
});
it('creates a chat with user styles', async () => {

View File

@@ -1,7 +1,7 @@
import {
createAndPopulateGroup,
sleep,
} from '../../../../helpers/api-v3-integration.helper';
} from '../../../../helpers/api-integration/v3';
describe('POST /groups/:id/chat/seen', () => {
context('Guild', () => {

View File

@@ -2,7 +2,7 @@ import {
createAndPopulateGroup,
generateUser,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
} from '../../../../helpers/api-integration/v3';
import config from '../../../../../config.json';
import { v4 as generateUUID } from 'uuid';

View File

@@ -1,7 +1,7 @@
import {
requester,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
} from '../../../../helpers/api-integration/v3';
import i18n from '../../../../../website/common/script/i18n';
describe('GET /content', () => {

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