Compare commits

...

292 Commits

Author SHA1 Message Date
Sabe Jones
11cfb3920a 4.52.0 2018-07-17 19:10:41 +00:00
Sabe Jones
f5468d3771 chore(i18n): update locales 2018-07-17 19:09:37 +00:00
Sabe Jones
99882d09ab chore(sprites): compile 2018-07-17 14:05:24 -05:00
Sabe Jones
7034d135d5 feat(content): Sea Serpent Pet Quest 2018-07-17 14:05:09 -05:00
Sabe Jones
034c0c9bb5 4.51.4 2018-07-16 21:12:50 +00:00
Sabe Jones
e0711655f0 chore(i18n): update locales 2018-07-16 21:12:33 +00:00
Sabe Jones
71e162eed5 chore(news): Bailey 2018-07-16 16:10:04 -05:00
Sabe Jones
f810fff6fc 4.51.3 2018-07-12 19:10:02 +00:00
Sabe Jones
5bbe59c52d chore(i18n): update locales 2018-07-12 19:09:42 +00:00
Sabe Jones
3f52401384 chore(news): Bailey 2018-07-12 14:05:40 -05:00
Matteo Pagliazzi
e7944b3d98 iOS push notifications, use node-apn (#10517)
* fixing typos in comments. yes, I am that kind of nerd

* replacing push-notify with node-apn in deps and in pushNotifications.js

* updating calling code and tests to use node-apn

* updating APN configs to new format

* migrating team ID and key ID to config.json

* update code to use env variables and add correct topic
2018-07-12 12:56:15 +02:00
Sabe Jones
08e925e3da Merge branch 'release' into develop 2018-07-10 17:26:37 +00:00
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
Keith Holliday
1fcc0d8d3a Ensured redirect is using base (#10497) 2018-07-09 10:08:19 -05:00
Matteo Pagliazzi
8a4c4e10f1 remove sensitive info from logs 2018-07-08 10:43:28 +02:00
Maurício El Uri
18ed0fe446 Fixed the unnecessary whitespace to the right of landing page (#10435) (#10490) 2018-07-06 15:57:36 -05:00
Vinicius
1f9ebeb629 Set .resting-banner z-index to 1300, set #progress .bar z-index to 1600 (#10492) 2018-07-06 15:55:44 -05:00
Sabe Jones
b9d83122d1 fix(tasks): eliminate console error related to filtering
Also localize a group plans string
2018-07-06 15:43:27 -05:00
Sabe Jones
d1f7e64156 fix(groups): display correct messages in two places 2018-07-06 13:21:28 -05:00
Sabe Jones
0f8e7416f8 fix(groups): save group options on task create
Also, correct count of assigned members when viewing user is among 
assignments
2018-07-06 10:30:09 -05:00
Sabe Jones
1c8b0f92df Small Group Plan fixes (#10499)
* fix(groups): better margins, don't close whole modal on close assigned members list

* fix(groups): proper sticky toggle switch
2018-07-05 16:37:06 -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
user
9c702505a9 Locales Changing, PM Disabled Caption Added 2018-05-02 19:08:43 +03: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
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
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
1249 changed files with 55253 additions and 42290 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

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

@@ -78,6 +78,9 @@
"PUSH_CONFIGS": {
"GCM_SERVER_API_KEY": "",
"APN_ENABLED": "false",
"APN_KEY_ID": "xxxxxxxxxx",
"APN_KEY": "xxxxxxxxxx",
"APN_TEAM_ID": "aaabbbcccd",
"FCM_SERVER_API_KEY": ""
},
"SITE_HTTP_AUTH": {
@@ -112,5 +115,6 @@
"CLOUDKARAFKA_USERNAME": "",
"CLOUDKARAFKA_PASSWORD": "",
"CLOUDKARAFKA_TOPIC_PREFIX": ""
}
},
"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

@@ -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,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('./groups/migrate-chat.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_201804', 'headAccessory_mystery_201804'];
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) {

11529
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,17 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "4.41.3",
"version": "4.52.0",
"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.4.1",
"aws-sdk": "^2.230.1",
"autoprefixer": "^8.5.0",
"aws-sdk": "^2.239.1",
"apn": "^2.2.0",
"axios": "^0.18.0",
"axios-progress-bar": "^1.2.0",
"babel-core": "^6.26.3",
@@ -26,20 +27,20 @@
"babel-register": "^6.6.0",
"babel-runtime": "^6.11.6",
"bcrypt": "^2.0.0",
"body-parser": "^1.15.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.1.0",
"cwait": "^1.1.1",
"domain-middleware": "~0.1.0",
"express": "^4.16.3",
"express-basic-auth": "^1.1.5",
"express-validator": "^5.1.2",
"express-validator": "^5.2.0",
"extract-text-webpack-plugin": "^3.0.2",
"glob": "^7.1.2",
"got": "^8.3.1",
@@ -52,7 +53,7 @@
"hellojs": "^1.15.1",
"html-webpack-plugin": "^3.2.0",
"image-size": "^0.6.2",
"in-app-purchase": "^1.9.3",
"in-app-purchase": "^1.9.4",
"intro.js": "^2.9.3",
"jquery": ">=3.0.0",
"js2xmlparser": "^3.0.0",
@@ -61,7 +62,7 @@
"method-override": "^2.3.5",
"moment": "^2.22.1",
"moment-recur": "^1.0.7",
"mongoose": "^5.0.17",
"mongoose": "^5.1.2",
"morgan": "^1.7.0",
"nconf": "^0.10.0",
"node-gcm": "^0.14.4",
@@ -78,18 +79,19 @@
"postcss-easy-import": "^3.0.0",
"ps-tree": "^1.0.0",
"pug": "^2.0.3",
"push-notify": "git://github.com/habitrpg/push-notify.git#6bc2b5fdb1bdc9649b9ec1964d79ca50187fc8a9",
"pusher": "^1.3.0",
"rimraf": "^2.4.3",
"sass-loader": "^7.0.0",
"shelljs": "^0.8.1",
"stripe": "^5.8.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",
@@ -103,7 +105,7 @@
"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.2",
"winston-loggly-bulk": "^2.0.2",
@@ -119,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",
@@ -139,15 +143,15 @@
"apidoc": "gulp apidoc"
},
"devDependencies": {
"@vue/test-utils": "^1.0.0-beta.15",
"@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.4.1",
"chromedriver": "^2.38.2",
"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.1",
"eslint-config-habitrpg": "^4.0.0",
@@ -163,7 +167,7 @@
"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.4",
@@ -173,17 +177,17 @@
"karma-webpack": "^3.0.0",
"lcov-result-merger": "^2.0.0",
"mocha": "^5.1.1",
"monk": "^6.0.5",
"monk": "^6.0.6",
"nightwatch": "^0.9.21",
"puppeteer": "^1.3.0",
"puppeteer": "^1.4.0",
"require-again": "^2.0.0",
"selenium-server": "^3.11.0",
"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.22.1"
"webpack-hot-middleware": "^2.22.2"
},
"optionalDependencies": {
"memwatch-next": "^0.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;
@@ -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').toDate());
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', () => {
@@ -156,7 +157,6 @@ describe('cron', () => {
});
describe('for a 1-month recurring subscription', () => {
let clock;
// create a user that will be used for all of these tests without a reset before each
let user1 = new User({
auth: {
@@ -187,7 +187,6 @@ describe('cron', () => {
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);
clock.restore();
});
it('does not increment consecutive benefits after the second month', () => {
@@ -199,7 +198,6 @@ describe('cron', () => {
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);
clock.restore();
});
it('increments consecutive benefits after the third month', () => {
@@ -211,7 +209,6 @@ describe('cron', () => {
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);
clock.restore();
});
it('does not increment consecutive benefits after the fourth month', () => {
@@ -223,7 +220,6 @@ describe('cron', () => {
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);
clock.restore();
});
it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => {
@@ -233,12 +229,10 @@ describe('cron', () => {
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);
clock.restore();
});
});
describe('for a 3-month recurring subscription', () => {
let clock;
let user3 = new User({
auth: {
local: {
@@ -266,7 +260,6 @@ describe('cron', () => {
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);
clock.restore();
});
it('does not increment consecutive benefits in the middle of the period that they already have benefits for', () => {
@@ -276,7 +269,6 @@ describe('cron', () => {
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);
clock.restore();
});
it('does not increment consecutive benefits in the final month of the period that they already have benefits for', () => {
@@ -286,7 +278,6 @@ describe('cron', () => {
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);
clock.restore();
});
it('increments consecutive benefits the month after the second paid period has started', () => {
@@ -296,7 +287,6 @@ describe('cron', () => {
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);
clock.restore();
});
it('does not increment consecutive benefits in the second month of the second period that they already have benefits for', () => {
@@ -306,7 +296,6 @@ describe('cron', () => {
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);
clock.restore();
});
it('does not increment consecutive benefits in the final month of the second period that they already have benefits for', () => {
@@ -316,7 +305,6 @@ describe('cron', () => {
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);
clock.restore();
});
it('increments consecutive benefits the month after the third paid period has started', () => {
@@ -326,7 +314,6 @@ describe('cron', () => {
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);
clock.restore();
});
it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => {
@@ -336,12 +323,10 @@ describe('cron', () => {
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);
clock.restore();
});
});
describe('for a 6-month recurring subscription', () => {
let clock;
let user6 = new User({
auth: {
local: {
@@ -369,7 +354,6 @@ describe('cron', () => {
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);
clock.restore();
});
it('does not increment consecutive benefits in the final month of the period that they already have benefits for', () => {
@@ -379,7 +363,6 @@ describe('cron', () => {
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);
clock.restore();
});
it('increments consecutive benefits the month after the second paid period has started', () => {
@@ -389,7 +372,6 @@ describe('cron', () => {
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);
clock.restore();
});
it('increments consecutive benefits the month after the third paid period has started', () => {
@@ -399,7 +381,6 @@ describe('cron', () => {
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);
clock.restore();
});
it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => {
@@ -409,13 +390,10 @@ describe('cron', () => {
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);
clock.restore();
});
});
describe('for a 12-month recurring subscription', () => {
let clock;
let user12 = new User({
auth: {
local: {
@@ -443,7 +421,6 @@ describe('cron', () => {
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);
clock.restore();
});
it('does not increment consecutive benefits in the final month of the period that they already have benefits for', () => {
@@ -453,7 +430,6 @@ describe('cron', () => {
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);
clock.restore();
});
it('increments consecutive benefits the month after the second paid period has started', () => {
@@ -463,7 +439,6 @@ describe('cron', () => {
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);
clock.restore();
});
it('increments consecutive benefits the month after the third paid period has started', () => {
@@ -473,7 +448,6 @@ describe('cron', () => {
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);
clock.restore();
});
it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => {
@@ -483,12 +457,10 @@ describe('cron', () => {
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);
clock.restore();
});
});
describe('for a 3-month gift subscription (non-recurring)', () => {
let clock;
let user3g = new User({
auth: {
local: {
@@ -503,7 +475,7 @@ describe('cron', () => {
// 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().add(3, 'months').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;
@@ -517,7 +489,6 @@ describe('cron', () => {
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);
clock.restore();
});
it('does not increment consecutive benefits in the second month of the gift subscription', () => {
@@ -527,7 +498,6 @@ describe('cron', () => {
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);
clock.restore();
});
it('does not increment consecutive benefits in the third month of the gift subscription', () => {
@@ -537,7 +507,6 @@ describe('cron', () => {
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);
clock.restore();
});
it('does not increment consecutive benefits in the month after the gift subscription has ended', () => {
@@ -547,12 +516,10 @@ describe('cron', () => {
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
clock.restore();
});
});
describe('for a 6-month recurring subscription where the user has incorrect consecutive month data from prior bugs', () => {
let clock;
let user6x = new User({
auth: {
local: {
@@ -580,7 +547,6 @@ describe('cron', () => {
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);
clock.restore();
});
it('does not increment consecutive benefits in the second month after the fix goes live', () => {
@@ -590,7 +556,6 @@ describe('cron', () => {
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);
clock.restore();
});
it('does not increment consecutive benefits in the third month after the fix goes live', () => {
@@ -600,7 +565,6 @@ describe('cron', () => {
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);
clock.restore();
});
it('increments consecutive benefits in the seventh month after the fix goes live', () => {
@@ -610,7 +574,6 @@ describe('cron', () => {
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);
clock.restore();
});
});
});
@@ -627,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', () => {
@@ -1040,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;

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/payments/amazon';
import payments from '../../../../../../../website/server/libs/payments/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/payments/amazon';
import payments from '../../../../../../../website/server/libs/payments/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/payments/amazon';
import payments from '../../../../../../../website/server/libs/payments/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/payments/amazon';
import payments from '../../../../../../../website/server/libs/payments/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,10 +1,10 @@
/* eslint-disable camelcase */
import iapModule from '../../../../../../website/server/libs/inAppPurchases';
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';
import iapModule from '../../../../../website/server/libs/inAppPurchases';
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';
import moment from 'moment';
const i18n = common.i18n;

View File

@@ -1,10 +1,10 @@
/* eslint-disable camelcase */
import iapModule from '../../../../../../website/server/libs/inAppPurchases';
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';
import iapModule from '../../../../../website/server/libs/inAppPurchases';
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';
import moment from 'moment';
const i18n = common.i18n;

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/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/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 * 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,14 +1,14 @@
import moment from 'moment';
import * as sender from '../../../../../../website/server/libs/email';
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 * as sender from '../../../../../website/server/libs/email';
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-integration/v3';
import {
generateGroup,
} from '../../../../../helpers/api-unit.helper.js';
} from '../../../../helpers/api-unit.helper.js';
describe('payments/index', () => {
let user, group, data, plan;
@@ -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 paypalPayments from '../../../../../../../website/server/libs/payments/paypal';
import payments from '../../../../../../../website/server/libs/payments/payments';
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/payments/paypal';
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 paypalPayments from '../../../../../../../website/server/libs/payments/paypal';
import payments from '../../../../../../../website/server/libs/payments/payments';
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 paypalPayments from '../../../../../../../website/server/libs/payments/paypal';
import payments from '../../../../../../../website/server/libs/payments/payments';
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 paypalPayments from '../../../../../../../website/server/libs/payments/paypal';
import payments from '../../../../../../../website/server/libs/payments/payments';
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/payments/paypal';
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/payments/stripe';
import payments from '../../../../../../../website/server/libs/payments/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/payments/stripe';
import payments from '../../../../../../../website/server/libs/payments/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/payments/stripe';
import payments from '../../../../../../../website/server/libs/payments/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/payments/stripe';
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/payments/stripe';
import payments from '../../../../../../../website/server/libs/payments/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/payments/stripe';
import payments from '../../../../../../../website/server/libs/payments/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,13 +1,13 @@
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 apn from 'apn/mock';
import nconf from 'nconf';
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;
@@ -24,7 +24,7 @@ describe('pushNotifications', () => {
sandbox.stub(gcmLib.Sender.prototype, 'send').callsFake(fcmSendSpy);
sandbox.stub(pushNotify, 'apn').returns({
sandbox.stub(apn.Provider.prototype, 'send').returns({
on: () => null,
send: apnSendSpy,
});
@@ -104,10 +104,7 @@ describe('pushNotifications', () => {
},
};
sendPushNotification(user, details);
expect(apnSendSpy).to.have.been.calledOnce;
expect(apnSendSpy).to.have.been.calledWithMatch({
token: '123',
const expectedNotification = new apn.Notification({
alert: message,
sound: 'default',
category: 'fun',
@@ -117,6 +114,10 @@ describe('pushNotifications', () => {
b: true,
},
});
sendPushNotification(user, details);
expect(apnSendSpy).to.have.been.calledOnce;
expect(apnSendSpy).to.have.been.calledWithMatch(expectedNotification, '123');
expect(fcmSendSpy).to.not.have.been.called;
});
});

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

@@ -6,11 +6,14 @@ import {
taskActivityWebhook,
questActivityWebhook,
userActivityWebhook,
} from '../../../../../website/server/libs/webhook';
} 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';
} from '../../../helpers/api-unit.helper.js';
import { defer } from '../../../helpers/api-unit.helper';
describe('webhooks', () => {
let webhooks, user;
@@ -306,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',
@@ -324,6 +316,15 @@ 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', () => {

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

@@ -1,8 +1,8 @@
import {
generateRes,
generateReq,
} from '../../../../helpers/api-unit.helper';
import { authWithHeaders as authWithHeadersFactory } from '../../../../../website/server/middlewares/auth';
} from '../../../helpers/api-unit.helper';
import { authWithHeaders as authWithHeadersFactory } from '../../../../website/server/middlewares/auth';
describe('auth middleware', () => {
let res, req, user;

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,23 +1,23 @@
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';
} 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';
} 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;

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

@@ -1,6 +1,7 @@
import { model as Webhook } from '../../../../../website/server/models/webhook';
import { BadRequest } from '../../../../../website/server/libs/errors';
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', () => {
@@ -287,8 +288,8 @@ describe('Webhook Model', () => {
wh.formatOptions(res);
} catch (err) {
expect(err).to.be.an.instanceOf(BadRequest);
expect(res.t).to.be.calledOnce;
expect(res.t).to.be.calledWith('groupIdRequired');
expect(err.message).to.eql(apiError('groupIdRequired'));
done();
}
});

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`);

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', () => {

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;

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,7 +4,7 @@ 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,

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', () => {

View File

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

View File

@@ -2,7 +2,7 @@ import {
generateUser,
translate as t,
resetHabiticaDB,
} from '../../../../helpers/api-v3-integration.helper';
} from '../../../../helpers/api-integration/v3';
describe('POST /coupons/enter/:code', () => {
let user;

View File

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

View File

@@ -2,7 +2,7 @@ import {
generateUser,
requester,
resetHabiticaDB,
} from '../../../../helpers/api-v3-integration.helper';
} from '../../../../helpers/api-integration/v3';
describe('POST /coupons/validate/:code', () => {
let api = requester();

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