Compare commits

..

121 Commits

Author SHA1 Message Date
Matteo Pagliazzi
79c3efaf9c 3.71.0 2017-01-23 19:05:39 +01:00
Matteo Pagliazzi
74c6a891fc Revert "Revert Facebook Pixel" (#8449) 2017-01-23 16:38:56 +01:00
MathWhiz
9a5d17f538 attempt revert (#8406) 2017-01-23 16:03:23 +01:00
Matteo Pagliazzi
070c4a8fbd add auth.local.passwordHashMethod field 2017-01-23 10:38:41 +01:00
Keith Holliday
2bbc4f4f4d Paypal refactor lib (#8420)
* Abstracted paypal logic from controller. Added intial tests

* Added checkout tests

* Added checkout success test

* Added subscribe test

* Added subscribeSuccess tests

* Added subscribeCancel tests

* Added ipn tests

* Fixed broken test

* Added integration tests and fixed linting issues

* Added errors for paypal checkout success integration test

* Removed extra test

* Removed pending status from subscribe cancel test

* Added more error checking and tests

* Fixed lint issues
2017-01-22 09:59:47 -07:00
Alys
39c00ea433 change HTML <title> on static/old-news from "Old News" to "News"
That page now contains the most recent news as well as the old news.
2017-01-22 13:32:54 +10:00
Alys
dd6c1c764a change footer's "Add-ons & Extensions" link
The new link is a better wiki page and is already in use in the Settings > API screen.
2017-01-22 13:15:12 +10:00
Sabe Jones
9b456d1760 3.70.0 2017-01-21 03:13:36 +00:00
Sabe Jones
acf1031317 chore(i18n): update locales 2017-01-21 02:50:49 +00:00
Sabe Jones
5d45204d8b chore(sprites): compile 2017-01-21 02:41:31 +00:00
Sabe Jones
37a71924fe Incentives, Batch 2 (#8435)
* feat(content): Incentives batch 2 gear

* feat(incentives): incentives 55-100

* chore(migration): hand out missed incentive

* refactor(constant): export MAX_INCENTIVES

* fix(incentives): correct const import
and say "Royal Purple Potion" not "Royal Purple"
2017-01-20 20:36:38 -06:00
Alys
9cf2408988 change name of Newbies Guild to Habitica Help: Ask a Question guild (#8424)
* change name of Newbies Guild to Habitica Help: Ask a Question guild

* remove ": Ask a Question" from "Habitica Help: Ask a Question" in locations where we are linking to the guild or its wiki page
2017-01-21 09:20:39 +10:00
Keith Holliday
638525f8d8 Stripe refactor to lib (#8417)
* Moved stripe tests to folder

* Abstracted stripe payments logic to lib

* Added initial unit test for stripe payment

* Added subscription tests

* Added tests for regulare purchases

* Added tests for edit subscription

* Added cancel tests

* Added integration tests

* Fixed lint issues

* Fixed lint issue
2017-01-20 16:08:52 -07:00
Keith Holliday
2c37ba3cee Added migration runner that allows for migrations to use server code (#8436)
* Added migration runner that allows for migrations to use server code

* Replaced example script in migration runner
2017-01-20 16:08:01 -06:00
Sabe Jones
ad5b2fe540 3.69.1 2017-01-19 23:02:41 +00:00
Sabe Jones
bfb6daad51 fix(docs): separate def blocks 2017-01-19 23:02:22 +00:00
Sabe Jones
281b8e2b7c 3.69.0 2017-01-19 22:09:56 +00:00
Sabe Jones
3f88ea2378 fix(typo): extra brackets 2017-01-19 22:04:36 +00:00
Sabe Jones
9c6275d4ab chore(i18n): update locales 2017-01-19 20:31:23 +00:00
Sabe Jones
fd3c8ddc8b chore(sprites): compile 2017-01-19 20:22:32 +00:00
Sabe Jones
72f47ad4e6 chore(news): misc Bailey 2017-01-19 20:19:15 +00:00
ean
74c9a1b02d Add missing text to buttons on the party / guild invite modal - Fixes #8391 (#8402)
* Fix missing text in party / guild invite modal

* add tests and abstract logic

* use translation lib for test
2017-01-19 12:17:38 -07:00
Keith Holliday
ffa561473c [WIP] Amazon refactor to lib (#8403)
* Moved amazon tests to folder

* Abstracted amazon payment code and added initial test

* Abstracted cancel and subscribe logic to amazon payment lib

* Added arg checks to checkout

* Added constants. Added more subscription test

* Added with arg checks to cancel

* Fixed linting issues

* Added integration tests for amazon subscribe cancel

* Added integration test for amazon checkout

* Added integration test for amazon subscribe

* Added coupon unit test

* Fixed lint

* Fixed minor test issue and changed header expectations

* Fixed line endings
2017-01-19 08:30:30 -07:00
MathWhiz
080ffae4e1 apiDoc /chat Documentation (#8277)
* apiDoc /chat Documentation

* Add {NotFound} to FlagOwnMessage error

* Quick changes

* Update chat.js
2017-01-18 22:28:00 -07:00
Gerardo Saca
e395182c95 Fix User > Profile showing {getProgressDisplay()} - fixes #8412 (#8421)
* Fix User > Profile showing {getProgressDisplay()}

* Remove bad nextRewardAt check
2017-01-18 22:25:37 -07:00
Matteo Pagliazzi
68f4275c44 fix tests failures (#8422)
* fix tests failures

* make sure _meta is not public

* fix typo

* fix typo
2017-01-18 20:17:56 +01:00
Alys
4bf4c3a6c2 remove Windows line breaks 2017-01-19 05:00:54 +10:00
Sabe Jones
f00bb29192 fix(quest): correct pt 2 label 2017-01-18 16:25:52 +00:00
Alyssa Batula
016447ec77 Critical Hits now affect boss damage fixes #5429 (#8092)
* Moved critical hit calculation from _addPoints() to _calculateDelta(). Added user as an input argument to _calculateDelta() so for critical hit calculation

* Changed test to expect task value of 1.5 after critical hit

* Revert "Moved critical hit calculation from _addPoints() to _calculateDelta(). Added user as an input argument to _calculateDelta() so for critical hit calculation"

This reverts commit 51b8ab6498.

* Moved critical hit calculation to _changeTaskValue(). Use value stored in user._tmp.crit in _addPoints()

* Test is no longer affected by critical hits

* Removed unneeded comment

* Added WIP test of critical hits

* Want the crit function to return 2 to test critical hits

* Changed crit function to export as a function within an object so that it can be stubbed for testing. References to the crit() function were updated to call crit.crit() instead

* Added test for increased experience on critical hits
2017-01-18 08:11:39 -07:00
Dumindu Buddhika
fa024e071b added message saying quest is started (#8107)
* added chat message saying quest has started

* added info to message

* added information as meta data

* fixed failing test case

* added new tests
2017-01-18 08:08:47 -07:00
Keith Holliday
28fec237fe Group plans misc fixes (#8388)
* Added notification for approval request in the group leaders language

* Added test for group task meta actions. Added sync when user claims

* Added tests for group task actions. Ensured assigned members are synce when added or removed

* Fixed approval required toggle

* Added support for users with comma in their name

* Fixed sync issue when user is approved and reloads the website

* Added advance options for group rewards

* Added back ticks to group claim message

* Fixed disappearing tasks that need approval

* Up chat limit to 400 for subbed groups

* Fixed line endings

* Updated activie subscription check

* Added group isSubscribed function

* Changed to isAfter
2017-01-18 07:54:49 -07:00
Sabe Jones
e4bb82768c fix(sprites): add large-1 to repo 2017-01-18 01:52:14 +00:00
Sabe Jones
65eca22407 3.68.0 2017-01-18 01:26:58 +00:00
Sabe Jones
cea1597ee1 chore(i18n): update locales 2017-01-18 00:27:01 +00:00
Sabe Jones
3906952154 chore(sprites): compile 2017-01-18 00:16:01 +00:00
Sabe Jones
6169b9d0ae feat(content): new Gold quests (#8418)
and Wintery Skins, and fixes #8412
2017-01-17 16:03:10 -08:00
Declan Ramsay
69cac7e504 Add markdown formatting to task-edit title text (#8416) 2017-01-17 14:49:16 -07:00
mleah
febf3f0024 Adding "How to gift subscription" copy to Settings > Subscription Page: Fixes Issue 8341 (#8386)
* Issue 8341 updating subscriber json and subscription settings page with gifting a subscription directions

* Issue-8341 updating the layout of the settings - subscription page

* Issue-8341 removing extra comma from gift subscription copy

* Issue-8341 removing extra spans in subscription jade file
2017-01-17 14:48:21 -07:00
Matteo Pagliazzi
563f40e4b7 client: reorganize files, router and add inventory skeleton 2017-01-17 19:45:27 +01:00
Matteo Pagliazzi
e2b06161e1 client: add task component 2017-01-17 18:07:02 +01:00
Alys
e7de8b8e2f clarify two Notifications descriptions (onboarding, inactive account)
New words were supplied by Lemoness.
2017-01-17 07:32:35 +10:00
Matteo Pagliazzi
a0624d9507 fix build by not requiring optional package in prod 2017-01-16 18:31:29 +01:00
Matteo Pagliazzi
cddd0df4f2 chore(i18n) add overview and login incentives 2017-01-16 17:59:04 +01:00
Matteo Pagliazzi
220bfb3517 chore(i18n): update locales 2017-01-16 17:58:30 +01:00
Matteo Pagliazzi
2106a5ebd3 correct deps loading and shrinkwrap 2017-01-15 23:45:22 +01:00
Matteo Pagliazzi
bbffa9830b client: replace deprecated vue-resource with axios, lint more file 2017-01-15 20:49:15 +01:00
Matteo Pagliazzi
caa546eb62 client: add missing test files and use v-once 2017-01-15 20:08:36 +01:00
Alys
4e83059652 remove outdated information about changing your email address
This is being done to remove a redundant string that
contains an email address, in preparation for actioning
https://github.com/HabitRPG/habitica/issues/8385 ("Email addresses
should not probably be translatable")
2017-01-15 22:50:32 +10:00
Matteo Pagliazzi
903cdb36ef client: remove old code 2017-01-14 21:46:28 +01:00
Matteo Pagliazzi
d8128cc3db New Client: english translations and misc fixes (#8410)
* misc fixes and add english translations

* add tests
2017-01-14 21:12:11 +01:00
MathWhiz
f888e80b01 apiDoc: meta (#8167)
* apiDoc: meta

* Update modelsPaths.js

* Update error

* fix test

* Update GET-model_paths.test.js

* Fixed test
2017-01-13 14:06:46 -07:00
Matteo Pagliazzi
fd7aedbff2 chore(locales): add merch file 2017-01-13 19:09:47 +01:00
Matteo Pagliazzi
6c5234313d chore(i18n): update locales 2017-01-13 19:01:26 +01:00
Sabe Jones
08aa5758b4 3.67.1 2017-01-13 00:05:39 +00:00
Sabe Jones
1415e344c0 chore(i18n): update locales 2017-01-12 23:24:04 +00:00
Sabe Jones
2ce7915f06 fix(incentives): return 0 for upcoming at end of list
Also adds some new strings for upcoming content, adds a news announcement, and compiles a recent sprites fix.
2017-01-12 23:15:37 +00:00
MathWhiz
838c8b4e08 Update Royal Purple Flying PIg (#8401) 2017-01-12 15:06:25 -08:00
Keith Holliday
1590d955cd Group plans reorder tasks (#8358)
* Added move route for group tasks

* Added group task reorder to front end

* Added syncing with group task order

* Fixed linting issues

* Added missing exec and abstracted move code

* Added unit test for moveTask
2017-01-11 19:16:20 +01:00
MathWhiz
2690caed35 News translation link (#8393)
* Add translation link to news

* Add newsArchive string

* Translate news archive link

* Fix link?

* Add link to wiki
2017-01-11 09:16:10 -07:00
Sabe Jones
dc2d4fa10b fix(coveralls): update badge for /habitica 2017-01-10 22:25:40 -06:00
Sabe Jones
1540ec89ee 3.67.0 2017-01-10 20:49:54 +00:00
Sabe Jones
f304d4fe52 chore(migration): update Bailey to use monk 2017-01-10 20:47:58 +00:00
Sabe Jones
023e433a5c feat(content): Triceratops pet quest 2017-01-10 20:32:21 +00:00
Declan Ramsay
ef4aeb29ab Fixes task left in odd setting when cancelling editing with ESC / Escape Key; add click outside to close modal #8308 #8321 (#8382)
* Remove backdrop property from open modal call to allow click-outside to close, call cancelTaskEdit function on dismissal of edit modal via ESC or click-outside

* Remove commented out code, change catch function to normal (non-arrow)

* Modify task services test mock openModal function, to handle new use of subsequent promise resolution functionality

* Tidy old edit controls away
2017-01-10 12:04:34 -07:00
Jan Jorgensen
2b80931202 Fix home column style on medium screen widths fixes #8372 (#8387)
* handle medium screen width on home columns

* rename new-row helper used on home screen for todo column flow
2017-01-10 12:03:21 -07:00
myoshuGO
2950713712 Fixes #8227 (#8380)
* Fixes #8227

* Fixes #8227

* Fixes #8227
2017-01-10 12:00:53 -07:00
Jaka Kranjc
118f3bd1bb gulp/gulp-transifex-test.js: fixed variable name typo (#8389) 2017-01-10 10:07:35 +00:00
Alys
69f1343ea8 correct apidocs: /api/v3/user/... not /user/... 2017-01-09 19:57:26 +00:00
Keith Holliday
918ee02d64 Added hp back to party member query (#8378) 2017-01-07 20:59:17 -06:00
Sabe Jones
0cac34dd26 3.66.3 2017-01-07 22:42:50 +00:00
Sabe Jones
1c859fc91f chore(promo): end gift subs promo 2017-01-07 15:42:03 +00:00
Keith Holliday
857aa5827b Ensured group tasks are removed from places that challegnes tasks are (#8359)
* Ensured group tasks are removed from places that challegnes tasks are

* Added tests for user reset and class cast
2017-01-07 12:01:12 +01:00
Matteo Pagliazzi
28e8ec2d2c add BOSS_DAMAGE to valid notifications 2017-01-07 11:47:19 +01:00
Matteo Pagliazzi
856f13d213 chore(i18n): update locales 2017-01-07 11:43:53 +01:00
Sabe Jones
121fd38bd1 3.66.2 2017-01-06 21:28:35 +00:00
Keith Holliday
36d72f5f7a Claim task messages are now system messages (#8375) 2017-01-06 13:00:23 -08:00
Keith Holliday
f1b8bd80e7 Added fix to correctly check if object is sub group for all users (#8377) 2017-01-06 14:09:57 -06:00
Brian David
84d2ce6a3f Fixes issue #8315 (https://github.com/HabitRPG/habitica/issues/8315) (#8367) 2017-01-06 11:45:13 -06:00
Keith Holliday
76010e6c8f Added migration for profile restoration (#8355)
* Added migration for restoring profile data

* Updated migrations to use monk

* Fixed line endings

* Moved monk to dev dependencies
2017-01-06 11:07:21 -06:00
Keith Holliday
c707b6c99b Add check to ensure obj is not user (#8373) 2017-01-06 10:23:39 -06:00
Sabe Jones
e4bd466cc7 3.66.1 2017-01-06 02:37:50 +00:00
Sabe Jones
001b8eb894 chore(news): Bailey 2017-01-05 2017-01-06 02:09:59 +00:00
Keith Holliday
9abcfe8614 Added fixes for party group plans (#8366) 2017-01-05 16:52:05 -06:00
Matteo Pagliazzi
bc6102551d chore(i18n): update locales 2017-01-05 16:42:43 +01:00
Sabe Jones
959a3ff85b fix(menus): keep icon after group ack
Also corrects a TypeError in the menu closing function.
2017-01-04 22:19:57 +00:00
Matteo Pagliazzi
518b874f64 Always use .exec() for .find*() and .update() (#8361)
* add exec where missing in /models

* ix taskManager query

* fix top-level controllers

* fix api-v3 controllers
2017-01-04 16:49:43 +01:00
Travis
6cc359a935 Adding new user.addNotification method with Mongodb Update (#8272)
* Adding new method to user schema that pushes a new notification to the database with an update operation instead of a save. fixes #8264

* fixing test text

* changing the addUserNotificationUpdate method to a static method as requested.

* Renaming to push notification

* fixing comment documentation based on pr comments.

* Changed the update statement to do a multi update and added a validation step. Added tests for both these cases.

* Updating pushNotification method to allow a query to be passed instead of an array of userIds to make it more flexible.

* Removing createdAt field from tests as it's no longer used.

* Removing only from test suite
2017-01-04 15:27:54 +01:00
Sabe Jones
514d35c0be 3.66.0 2017-01-03 22:03:09 +00:00
Sabe Jones
13da92ea68 feat(content): Armoire and BGs 2017-01 2017-01-03 21:35:23 +00:00
Travis
03c4d82b7d fix: prevents blank messages from being posted to chat (#8257)
* fix: throws an error when the server receives a post chat request with a message containing only whitespace.

* Adding a test confirming behavior around messages that only contain newlines.

* Removing accidental only that was left on a test
2017-01-03 07:15:31 -07:00
Sabe Jones
d905ab7f86 3.65.2 2017-01-03 00:50:46 +00:00
Sabe Jones
c6560b6b1b chore(event): end New Year's bennies 2017-01-03 00:19:20 +00:00
Keith Holliday
c61f660255 Added field existence checks (#8356) 2017-01-02 16:31:04 -07:00
Matteo Pagliazzi
2f1b683ec9 Avoid setting profile name to not found (#8357)
* avoid setting profile name to not found

* only set profile name when empty

* profile.name is required

* set profile name before validation

* fix and add tests
2017-01-03 00:00:01 +01:00
chan_gami
47bb217068 Fixes pressing enter to confirm character choice in IME creates new checklist line (#8326) (#8334) 2017-01-02 10:03:03 -07:00
Declan Ramsay
f49fd05da1 Fixes on-hover notes on tasks do not update after you edit the notes, until a sync occurs (#8353)
* Shift taskPopover from one-time-binding to regular binding, allowing task note display to be updated

* Re-add Markdown filter to task popover
2017-01-02 10:00:35 -07:00
Matteo Pagliazzi
b0341aa06f chore(i18n): update locales 2017-01-02 15:54:04 +01:00
Kaitlin Hipkin
b07ec18e33 Correct party up/on achievement string names (#8313)
* Revert party up/on achievement string names

* fix missed references to party up/on strings
2017-01-01 10:54:49 -07:00
Alys
12930a2bac 3.65.1 2016-12-31 07:47:49 +00:00
Alys
91f5c47d9d Revert "feature: adding hp notification for boss damage" (#8340) 2016-12-31 17:31:25 +10:00
Sabe Jones
fe7850d10f 3.65.0 2016-12-30 23:30:42 +00:00
Sabe Jones
c5c2da75bf fix(shops): hardcode NYE card 2016-12-30 23:08:01 +00:00
Sabe Jones
969607cd3b feat(event): New Year's 2016 2016-12-30 22:19:29 +00:00
Travis
2a1f52a359 feature: adding hp notification for boss damage (#8249)
* feature: adding hp notification for boss damage. fixes #7749

* Updating boss damage text to 'Damage from Boss' to make it more clear
2016-12-30 13:29:20 -06:00
Keith Holliday
47d9594679 Group plans remove unlinked tasks (#8332)
* Added ability to delete tasks that are broken

* Added ability to delete group tasks after leaving group
2016-12-30 13:17:39 -06:00
Keith Holliday
97e40c81f3 Added error when nonleader attempts to invite to group plan (#8331) 2016-12-30 13:17:22 -06:00
Sabe Jones
c8b61a2f7d feat(content): Armoire and BG strings 2017-01 2016-12-29 20:43:15 +00:00
Cai Lu
e9543f0d28 Add 'balanceGemAmount' to Amplitude #8057 (#8323)
* Add balanceGemAmount property

* Add check for balanceGemAmount property

* Fix balanceGemAmount to be 4 times balance
2016-12-29 14:31:30 -06:00
Keith Holliday
77b88490e4 (WIP) Thehollidayin/front end updates (#8278)
* Added read me and Inbox Page

* Fixed inbox linting

* Added converstaion route

* Added temp data and style

* Added social page and nav

* Fixed Inbox routes

* Added basic layout for Tavern Page
2016-12-29 13:24:08 -06:00
Keith Holliday
7fc2500bfd Removed group plan option from gift modal (#8327) 2016-12-29 08:57:00 -06:00
Keith Holliday
fb229acb58 Added per user cost message if group has subscription (#8328)
* Added per user cost message if group has subscription

* Added user specification to cost
2016-12-29 08:56:40 -06:00
Sabe Jones
6ce83d1fa4 3.64.1 2016-12-28 22:03:35 +00:00
Sabe Jones
2be4815aea chore(news): misc Bailey 2016-12-28 21:59:20 +00:00
Khwunchai Jaengsawang
1dbc42f48a Fixes character appears too high in avatar on profile when no pet or mount is equipped (#7916) (#8318) 2016-12-28 09:36:56 -06:00
Matteo Pagliazzi
89279c8aed chore(i18n): update locales 2016-12-28 09:53:20 +01:00
Matteo Pagliazzi
faedb13598 add schema field to keep track of onboarding emails 2016-12-28 09:47:14 +01:00
Rick Kasten
c0c74659c5 Docker improvements (#8297)
* Improved docker, bower

* npm install missing mocha

* Fix for 'npm install -g npm@4' not resulting in a functional npm

* Improve speed of 'docker start' by withholding directories not used by image environment

* Reverting changes to bower.json
2016-12-28 09:33:24 +01:00
Travis
bf5ad2db1f Fixing Exponential Quest Reward Scrolls (#7800)
* adding quest owner specific rewards. closes #2715

* Updating model to prevent this from being a breaking change.

* Removing duplicate translatable string and readding accidentally deleted portion

* capitalizing according to pr.

* fixing according to comments on pr

* removing final mistakes

* fixing whitespace

* re-adding the onlyOwner field that got deleted when the index.js file was moved and fixed console errors.

* moving cleaning of empty obejct for quest owner updates into quest owner updates method

* Fixing so tests pass by updating variable name and removing unnecessary parameter definition.

* adding a new test and refactoring client side code to use controller method.
2016-12-28 01:38:52 -06:00
Megan Tiu
7d99873960 Add inline save-close buttons to top of task modal (#8319) 2016-12-28 01:24:58 -06:00
Keith Holliday
e02ef00397 Add leader check for challenge tasks on delete icon (#8325) 2016-12-28 01:08:37 -06:00
Alys
23c5c4211c decapitalise "A" in a title; Windows character removal; spacing fixes
- decapitaliseed "A" in "Create A Group"
- converted non-ascii apostrophes to single-quote for consistency with other strings
- removed a double space within a string
- removed Windows line endings on every line
- converted all leading four spaces to two spaces (normally wouldn't do that but since every line was already touched by the line ending change, why not)
2016-12-28 05:07:21 +00:00
934 changed files with 46057 additions and 36693 deletions

3
.dockerignore Normal file
View File

@@ -0,0 +1,3 @@
node_modules
.git
website

View File

@@ -20,8 +20,4 @@ website/common/browserify.js
test/content/**/*
Gruntfile.js
gulpfile.js
gulp
webpack
test/client/e2e
test/client/unit/index.js
test/client/unit/karma.conf.js
gulp

View File

@@ -20,17 +20,19 @@ RUN apt-get install -y \
RUN curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
RUN apt-get install -y nodejs
# Install npm@latest
RUN curl -sL https://www.npmjs.org/install.sh | sh
# Clean up package management
RUN apt-get clean
RUN rm -rf /var/lib/apt/lists/*
# Install global packages
RUN npm install -g npm@4
RUN npm install -g gulp grunt-cli bower
RUN npm install -g gulp grunt-cli bower mocha
# Clone Habitica repo and install dependencies
WORKDIR /habitrpg
RUN git clone https://github.com/HabitRPG/habitrpg.git /habitrpg
RUN git clone https://github.com/HabitRPG/habitica.git /habitrpg
RUN npm install
RUN bower install --allow-root

View File

@@ -1,4 +1,4 @@
Habitica [![Build Status](https://travis-ci.org/HabitRPG/habitica.svg?branch=develop)](https://travis-ci.org/HabitRPG/habitica) [![Code Climate](https://codeclimate.com/github/HabitRPG/habitrpg.svg)](https://codeclimate.com/github/HabitRPG/habitrpg) [![Coverage Status](https://coveralls.io/repos/HabitRPG/habitrpg/badge.svg?branch=develop)](https://coveralls.io/r/HabitRPG/habitrpg?branch=develop) [![Bountysource](https://api.bountysource.com/badge/tracker?tracker_id=68393)](https://www.bountysource.com/trackers/68393-habitrpg?utm_source=68393&utm_medium=shield&utm_campaign=TRACKER_BADGE)
Habitica [![Build Status](https://travis-ci.org/HabitRPG/habitica.svg?branch=develop)](https://travis-ci.org/HabitRPG/habitica) [![Code Climate](https://codeclimate.com/github/HabitRPG/habitrpg.svg)](https://codeclimate.com/github/HabitRPG/habitrpg) [![Coverage Status](https://coveralls.io/repos/github/HabitRPG/habitica/badge.svg?branch=develop)](https://coveralls.io/github/HabitRPG/habitica?branch=develop) [![Bountysource](https://api.bountysource.com/badge/tracker?tracker_id=68393)](https://www.bountysource.com/trackers/68393-habitrpg?utm_source=68393&utm_medium=shield&utm_campaign=TRACKER_BADGE)
===============
[Habitica](https://habitica.com) is an open source habit building program which treats your life like a Role Playing Game. Level up as you succeed, lose HP as you fail, earn money to buy weapons and armor.

View File

@@ -1,3 +1,3 @@
web:
volumes:
- '.:/habitrpg'
- '.:/habitrpg'

View File

@@ -1,13 +1,13 @@
web:
build: .
ports:
- "3000:3000"
- "3000:3000"
links:
- mongo
- mongo
environment:
- NODE_DB_URI=mongodb://mongo/habitrpg
- NODE_DB_URI=mongodb://mongo/habitrpg
mongo:
image: mongo
ports:
- "27017:27017"
- "27017:27017"

View File

@@ -84,8 +84,8 @@ gulp.task('transifex:malformedStrings', () => {
let malformedString = `${lang} - ${file} - ${key} - ${translationString}`;
stringsWithMalformedInterpolations.push(malformedString);
} else if (englishOccurences.length !== translationOccurences.length && !malformedStringExceptions[key]) {
let missingInterploationString = `${lang} - ${file} - ${key} - ${translationString}`;
stringsWithIncorrectNumberOfInterpolations.push(missingInterploationString);
let missingInterpolationString = `${lang} - ${file} - ${key} - ${translationString}`;
stringsWithIncorrectNumberOfInterpolations.push(missingInterpolationString);
}
});
});

View File

@@ -0,0 +1,73 @@
var migrationName = '20161230_nye_hats.js';
var authorName = 'Sabe'; // in case script author needs to know when their ...
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
/*
* Yearly New Year's party hat award
*/
var mongo = require('mongoskin');
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
var dbUsers = mongo.db(connectionString).collection('users');
// specify a query to limit the affected users (empty for all users):
var query = {
'migration':{$ne:migrationName},
'auth.timestamps.loggedin':{$gt:new Date('2016-11-30')} // Remove after first run
};
// specify fields we are interested in to limit retrieved data (empty if we're not reading data):
var fields = {
'items.gear.owned': 1,
};
console.warn('Updating users...');
var progressCount = 1000;
var count = 0;
dbUsers.findEach(query, fields, {batchSize:250}, function(err, user) {
if (err) { return exiting(1, 'ERROR! ' + err); }
if (!user) {
console.warn('All appropriate users found and modified.');
setTimeout(displayData, 300000);
return;
}
count++;
// specify user data to change:
var set = {};
if (typeof user.items.gear.owned.head_special_nye2015 !== 'undefined') {
set = {'migration':migrationName, 'items.gear.owned.head_special_nye2016':false};
} else if (typeof user.items.gear.owned.head_special_nye2014 !== 'undefined') {
set = {'migration':migrationName, 'items.gear.owned.head_special_nye2015':false};
} else if (typeof user.items.gear.owned.head_special_nye !== 'undefined') {
set = {'migration':migrationName, 'items.gear.owned.head_special_nye2014':false};
} else {
set = {'migration':migrationName, 'items.gear.owned.head_special_nye':false};
}
dbUsers.update({_id:user._id}, {$set:set});
if (count%progressCount == 0) console.warn(count + ' ' + user._id);
if (user._id == authorUuid) console.warn(authorName + ' processed');
});
function displayData() {
console.warn('\n' + count + ' users processed\n');
return exiting(0);
}
function exiting(code, msg) {
code = code || 0; // 0 = success
if (code && !msg) { msg = 'ERROR!'; }
if (msg) {
if (code) { console.error(msg); }
else { console.log( msg); }
}
process.exit(code);
}

View File

@@ -0,0 +1,113 @@
var migrationName = '20170120_missing_incentive.js';
var authorName = 'Sabe'; // in case script author needs to know when their ...
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
/*
* Award missing Royal Purple Hatching Potion to users with 55+ check-ins
* Reduce users with impossible check-in counts to a reasonable number
*/
import monk from 'monk';
import common from '../website/common';
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
var dbUsers = monk(connectionString).get('users', { castIds: false });
function processUsers(lastId) {
// specify a query to limit the affected users (empty for all users):
var query = {
'loginIncentives': {$gt:54},
'migration': {$ne: migrationName},
};
if (lastId) {
query._id = {
$gt: lastId
}
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 250,
fields: [] // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
})
.then(updateUsers)
.catch(function (err) {
console.log(err);
return exiting(1, 'ERROR! ' + err);
});
}
var progressCount = 1000;
var count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
displayData();
return;
}
var userPromises = users.map(updateUser);
var lastUser = users[users.length - 1];
return Promise.all(userPromises)
.then(function () {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
var language = user.preferences.language || 'en';
var set = {'migration': migrationName};
var inc = {'items.hatchingPotions.RoyalPurple': 1};
if (user.loginIncentives > 58) {
set = {'migration': migrationName, 'loginIncentives': 58};
}
var push = {
'notifications': {
'type': 'LOGIN_INCENTIVE',
'data': {
'nextRewardAt': 60,
'rewardKey': [
'Pet_HatchingPotion_Purple',
],
'rewardText': common.i18n.t('potion', {potionType: common.i18n.t('hatchingPotionRoyalPurple', language)}, language),
'reward': [
{
'premium': true,
'key': 'RoyalPurple',
'limited': true,
'value': 2,
}
],
'message': common.i18n.t('unlockedCheckInReward', language),
},
'id': common.uuid(),
}
};
dbUsers.update({_id: user._id}, {$set:set, $push:push, $inc:inc});
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
if (user._id == authorUuid) console.warn(authorName + ' processed');
}
function displayData() {
console.warn('\n' + count + ' users processed\n');
return exiting(0);
}
function exiting(code, msg) {
code = code || 0; // 0 = success
if (code && !msg) { msg = 'ERROR!'; }
if (msg) {
if (code) { console.error(msg); }
else { console.log( msg); }
}
process.exit(code);
}
module.exports = processUsers;

View File

@@ -0,0 +1,21 @@
require("babel-register");
require("babel-polyfill");
// This file must use ES5, everything required can be in ES6
function setUpServer () {
var nconf = require('nconf');
var mongoose = require('mongoose');
var Bluebird = require('bluebird');
var setupNconf = require('../website/server/libs/setupNconf');
setupNconf();
// We require src/server and npt src/index because
// 1. nconf is already setup
// 2. we don't need clustering
require('../website/server/server'); // eslint-disable-line global-require
}
setUpServer();
// Replace this with your migration
var processUsers = require('./new_stuff');
processUsers();

View File

@@ -6,49 +6,70 @@ var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
* set the newStuff flag in all user accounts so they see a Bailey message
*/
var mongo = require('mongoskin');
var monk = require('monk');
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
var dbUsers = monk(connectionString).get('users', { castIds: false });
var dbUsers = mongo.db(connectionString).collection('users');
// specify a query to limit the affected users (empty for all users):
var query = {
'flags.newStuff':{$ne:true}
};
function processUsers(lastId) {
// specify a query to limit the affected users (empty for all users):
var query = {
'flags.newStuff': {$ne:true},
};
// specify fields we are interested in to limit retrieved data (empty if we're not reading data):
var fields = {
};
if (lastId) {
query._id = {
$gt: lastId
}
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 250,
fields: [] // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
})
.then(updateUsers)
.catch(function (err) {
console.log(err);
return exiting(1, 'ERROR! ' + err);
});
}
console.warn('Updating users...');
var progressCount = 1000;
var count = 0;
dbUsers.findEach(query, fields, {batchSize:250}, function(err, user) {
if (err) { return exiting(1, 'ERROR! ' + err); }
if (!user) {
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
setTimeout(displayData, 300000);
displayData();
return;
}
var userPaymentPromises = users.map(updateUser);
var lastUser = users[users.length - 1];
return Promise.all(userPaymentPromises)
.then(function () {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
// specify user data to change:
var set = {'flags.newStuff':true};
var set = {'flags.newStuff': true};
dbUsers.update({_id:user._id}, {$set:set});
dbUsers.update({_id: user._id}, {$set:set});
if (count%progressCount == 0) console.warn(count + ' ' + user._id);
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!'; }
@@ -58,3 +79,5 @@ function exiting(code, msg) {
}
process.exit(code);
}
module.exports = processUsers;

View File

@@ -0,0 +1,115 @@
var migrationName = 'restore_profile_data.js';
var authorName = 'ThehollidayInn'; // in case script author needs to know when their ...
var authorUuid = ''; //... own data is done
/*
* Check if users have empty profile data in new database and update it with old database info
*/
var monk = require('monk');
var connectionString = ''; // FOR TEST DATABASE
var dbUsers = monk(connectionString).get('users', { castIds: false });
var monk2 = require('monk');
var oldDbConnectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
var olDbUsers = monk2(oldDbConnectionString).get('users', { castIds: false });
function processUsers(lastId)
{
// specify a query to limit the affected users (empty for all users):
var query = {
// 'profile.name': 'profile name not found',
'profile.blurb': null,
// 'auth.timestamps.loggedin': {$gt: new Date('11/30/2016')},
};
if (lastId) {
query._id = {
$gt: lastId
}
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 250,
fields: ['_id', 'profile', 'auth.timestamps.loggedin'] // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
})
.then(updateUsers)
.catch(function (err) {
console.log(err);
return exiting(1, 'ERROR! ' + err);
});
}
var progressCount = 1000;
var count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
setTimeout(displayData, 300000);
return;
}
var userPaymentPromises = users.map(updateUser);
var lastUser = users[users.length - 1];
return Promise.all(userPaymentPromises)
.then(function () {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
if (!user.profile.name || user.profile.name === 'profile name not found' || !user.profile.imageUrl || !user.profile.blurb) {
return olDbUsers.findOne({_id: user._id}, '_id profile')
.then((oldUserData) => {
if (!oldUserData) return;
// specify user data to change:
var set = {};
if (oldUserData.profile.name === 'profile name not found') return;
var userNeedsProfileName = !user.profile.name || user.profile.name === 'profile name not found';
if (userNeedsProfileName && oldUserData.profile.name) {
set['profile.name'] = oldUserData.profile.name;
}
if (!user.profile.imageUrl && oldUserData.profile.imageUrl) {
set['profile.imageUrl'] = oldUserData.profile.imageUrl;
}
if (!user.profile.blurb && oldUserData.profile.blurb) {
set['profile.blurb'] = oldUserData.profile.blurb;
}
if (Object.keys(set).length !== 0 && set.constructor === Object) {
console.log(set)
return dbUsers.update({_id: user._id}, {$set:set});
}
});
}
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
if (user._id == authorUuid) console.warn(authorName + ' processed');
}
function displayData() {
console.warn('\n' + count + ' users processed\n');
return exiting(0);
}
function exiting(code, msg) {
code = code || 0; // 0 = success
if (code && !msg) { msg = 'ERROR!'; }
if (msg) {
if (code) { console.error(msg); }
else { console.log( msg); }
}
process.exit(code);
}
processUsers()

View File

@@ -1,4 +1,4 @@
var migrationName = '20161201_takeThis.js';
var migrationName = '20170103_takeThis.js'; // Update per month
var authorName = 'Sabe'; // in case script author needs to know when their ...
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
@@ -38,7 +38,9 @@ dbUsers.findEach(query, fields, {batchSize:250}, function(err, user) {
// specify user data to change:
var set = {};
if (typeof user.items.gear.owned.body_special_takeThis !== 'undefined') {
if (typeof user.items.gear.owned.back_special_takeThis !== 'undefined') {
set = {'migration':migrationName};
{ else if (typeof user.items.gear.owned.body_special_takeThis !== 'undefined') {
set = {'migration':migrationName, 'items.gear.owned.back_special_takeThis':false};
} else if (typeof user.items.gear.owned.head_special_takeThis !== 'undefined') {
set = {'migration':migrationName, 'items.gear.owned.body_special_takeThis':false};

799
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "3.64.0",
"version": "3.71.0",
"main": "./website/server/index.js",
"dependencies": {
"@slack/client": "3.6.0",
@@ -13,6 +13,7 @@
"async": "^1.5.0",
"autoprefixer": "^6.4.0",
"aws-sdk": "^2.0.25",
"axios": "^0.15.3",
"babel-core": "^6.0.0",
"babel-loader": "^6.0.0",
"babel-plugin-syntax-async-functions": "^6.13.0",
@@ -118,7 +119,6 @@
"vue": "^2.1.0",
"vue-hot-reload-api": "^1.2.0",
"vue-loader": "^10.0.0",
"vue-resource": "^1.0.2",
"vue-router": "^2.0.0-rc.5",
"vue-template-compiler": "^2.1.0",
"webpack": "^1.12.2",
@@ -199,6 +199,7 @@
"mocha": "^2.3.3",
"mongodb": "^2.0.46",
"mongoskin": "~2.1.0",
"monk": "^3.1.3",
"nightwatch": "^0.8.18",
"phantomjs-prebuilt": "^2.1.12",
"protractor": "^3.1.1",

View File

@@ -35,6 +35,24 @@ describe('POST /chat', () => {
});
});
it('Returns an error when an empty message is provided', async () => {
await expect(user.post(`/groups/${groupWithChat._id}/chat`, { message: ' '}))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('Returns an error when an message containing only newlines is provided', async () => {
await expect(user.post(`/groups/${groupWithChat._id}/chat`, { message: '\n\n'}))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('Returns an error when group is not found', async () => {
await expect(user.post('/groups/invalidID/chat', { message: testMessage})).to.eventually.be.rejected.and.eql({
code: 404,

View File

@@ -300,6 +300,26 @@ describe('Post /groups/:groupId/invite', () => {
message: t('userAlreadyInGroup'),
});
});
// @TODO: Add this after we are able to mock the group plan route
xit('returns an error when a non-leader invites to a group plan', async () => {
let userToInvite = await generateUser();
let nonGroupLeader = await generateUser();
await inviter.post(`/groups/${group._id}/invite`, {
uuids: [nonGroupLeader._id],
});
await nonGroupLeader.post(`/groups/${group._id}/join`);
await expect(nonGroupLeader.post(`/groups/${group._id}/invite`, {
uuids: [userToInvite._id],
}))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlyGroupLeaderCanInviteToGroupPlan'),
});
});
});
describe('party invites', () => {

View File

@@ -10,7 +10,7 @@ describe('GET /models/:model/paths', () => {
user = await generateUser();
});
it('returns an error when model is not accessible or doesn\'t exists', async () => {
it('returns an error when model is not accessible or doesn\'t exist', async () => {
await expect(user.get('/models/1234/paths')).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',

View File

@@ -1,21 +0,0 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('payments : amazon #subscribeCancel', () => {
let endpoint = '/amazon/subscribe/cancel';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies subscription', async () => {
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('missingSubscription'),
});
});
});

View File

@@ -1,21 +0,0 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
xdescribe('payments : paypal #checkout', () => {
let endpoint = '/paypal/checkout';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies subscription', async () => {
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('missingSubscription'),
});
});
});

View File

@@ -1,21 +0,0 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
xdescribe('payments : paypal #checkoutSuccess', () => {
let endpoint = '/paypal/checkout/success';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies subscription', async () => {
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('missingSubscription'),
});
});
});

View File

@@ -1,21 +0,0 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
xdescribe('payments : paypal #subscribe', () => {
let endpoint = '/paypal/subscribe';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies credentials', async () => {
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('missingSubscription'),
});
});
});

View File

@@ -1,21 +0,0 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('payments : paypal #subscribeCancel', () => {
let endpoint = '/paypal/subscribe/cancel';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies credentials', async () => {
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('missingSubscription'),
});
});
});

View File

@@ -1,21 +0,0 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
xdescribe('payments : paypal #subscribeSuccess', () => {
let endpoint = '/paypal/subscribe/success';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies credentials', async () => {
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('missingSubscription'),
});
});
});

View File

@@ -1,21 +0,0 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('payments - stripe - #subscribeCancel', () => {
let endpoint = '/stripe/subscribe/cancel';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies credentials', async () => {
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('missingSubscription'),
});
});
});

View File

@@ -1,20 +0,0 @@
import {
generateUser,
} from '../../../../helpers/api-integration/v3';
describe('payments - amazon - #checkout', () => {
let endpoint = '/amazon/checkout';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies credentials', async () => {
await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Missing req.body.orderReferenceId',
});
});
});

View File

@@ -1,21 +0,0 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('payments - amazon - #subscribe', () => {
let endpoint = '/amazon/subscribe';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies subscription code', async () => {
await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingSubscriptionCode'),
});
});
});

View File

@@ -1,17 +0,0 @@
import {
generateUser,
} from '../../../../helpers/api-integration/v3';
describe('payments - paypal - #ipn', () => {
let endpoint = '/paypal/ipn';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies credentials', async () => {
let result = await user.post(endpoint);
expect(result).to.eql('OK');
});
});

View File

@@ -1,20 +0,0 @@
import {
generateUser,
} from '../../../../helpers/api-integration/v3';
describe('payments - stripe - #checkout', () => {
let endpoint = '/stripe/checkout';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies credentials', async () => {
await expect(user.post(endpoint, {id: 123})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'Error',
message: 'Invalid API Key provided: ****************************1111',
});
});
});

View File

@@ -1,21 +0,0 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('payments - stripe - #subscribeEdit', () => {
let endpoint = '/stripe/subscribe/edit';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies credentials', async () => {
await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('missingSubscription'),
});
});
});

View File

@@ -0,0 +1,78 @@
import {
generateUser,
generateGroup,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import amzLib from '../../../../../../website/server/libs/amazonPayments';
describe('payments : amazon #subscribeCancel', () => {
let endpoint = '/amazon/subscribe/cancel';
let user, group, amazonSubscribeCancelStub;
beforeEach(async () => {
user = await generateUser();
});
it('throws error when there users has no subscription', async () => {
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('missingSubscription'),
});
});
describe('success', () => {
beforeEach(async () => {
amazonSubscribeCancelStub = sinon.stub(amzLib, 'cancelSubscription').returnsPromise().resolves({});
});
afterEach(() => {
amzLib.cancelSubscription.restore();
});
it('cancels a user subscription', async () => {
user = await generateUser({
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
balance: 2,
});
await user.get(endpoint);
expect(amazonSubscribeCancelStub).to.be.calledOnce;
expect(amazonSubscribeCancelStub.args[0][0].user._id).to.eql(user._id);
expect(amazonSubscribeCancelStub.args[0][0].groupId).to.eql(undefined);
expect(amazonSubscribeCancelStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken);
expect(amazonSubscribeCancelStub.args[0][0].headers['x-api-user']).to.eql(user._id);
});
it('cancels a group subscription', async () => {
user = await generateUser({
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
balance: 2,
});
group = await generateGroup(user, {
name: 'test group',
type: 'guild',
privacy: 'public',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
});
await user.get(`${endpoint}?groupId=${group._id}`);
expect(amazonSubscribeCancelStub).to.be.calledOnce;
expect(amazonSubscribeCancelStub.args[0][0].user._id).to.eql(user._id);
expect(amazonSubscribeCancelStub.args[0][0].groupId).to.eql(group._id);
expect(amazonSubscribeCancelStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken);
expect(amazonSubscribeCancelStub.args[0][0].headers['x-api-user']).to.eql(user._id);
});
});
});

View File

@@ -0,0 +1,63 @@
import {
generateUser,
} from '../../../../../helpers/api-integration/v3';
import amzLib from '../../../../../../website/server/libs/amazonPayments';
describe('payments - amazon - #checkout', () => {
let endpoint = '/amazon/checkout';
let user, amazonCheckoutStub;
beforeEach(async () => {
user = await generateUser();
});
it('verifies credentials', async () => {
await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Missing req.body.orderReferenceId',
});
});
describe('success', () => {
beforeEach(async () => {
amazonCheckoutStub = sinon.stub(amzLib, 'checkout').returnsPromise().resolves({});
});
afterEach(() => {
amzLib.checkout.restore();
});
it('makes a purcahse with amazon checkout', async () => {
user = await generateUser({
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
balance: 2,
});
let gift = {
type: 'gems',
gems: {
amount: 16,
uuid: user._id,
},
};
let orderReferenceId = 'orderReferenceId-example';
await user.post(endpoint, {
gift,
orderReferenceId,
});
expect(amazonCheckoutStub).to.be.calledOnce;
expect(amazonCheckoutStub.args[0][0].user._id).to.eql(user._id);
expect(amazonCheckoutStub.args[0][0].gift).to.eql(gift);
expect(amazonCheckoutStub.args[0][0].orderReferenceId).to.eql(orderReferenceId);
expect(amazonCheckoutStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken);
expect(amazonCheckoutStub.args[0][0].headers['x-api-user']).to.eql(user._id);
});
});
});

View File

@@ -1,6 +1,6 @@
import {
generateUser,
} from '../../../../helpers/api-integration/v3';
} from '../../../../../helpers/api-integration/v3';
describe('payments - amazon - #createOrderReferenceId', () => {
let endpoint = '/amazon/createOrderReferenceId';

View File

@@ -0,0 +1,96 @@
import {
generateUser,
generateGroup,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import amzLib from '../../../../../../website/server/libs/amazonPayments';
describe('payments - amazon - #subscribe', () => {
let endpoint = '/amazon/subscribe';
let user, group, subscribeWithAmazonStub;
beforeEach(async () => {
user = await generateUser();
});
it('verifies subscription code', async () => {
await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingSubscriptionCode'),
});
});
describe('success', () => {
let billingAgreementId = 'billingAgreementId-example';
let subscription = 'basic_3mo';
let coupon;
beforeEach(async () => {
subscribeWithAmazonStub = sinon.stub(amzLib, 'subscribe').returnsPromise().resolves({});
});
afterEach(() => {
amzLib.subscribe.restore();
});
it('creates a user subscription', async () => {
user = await generateUser({
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
balance: 2,
});
await user.post(endpoint, {
billingAgreementId,
subscription,
coupon,
});
expect(subscribeWithAmazonStub).to.be.calledOnce;
expect(subscribeWithAmazonStub.args[0][0].billingAgreementId).to.eql(billingAgreementId);
expect(subscribeWithAmazonStub.args[0][0].sub).to.exist;
expect(subscribeWithAmazonStub.args[0][0].coupon).to.eql(coupon);
expect(subscribeWithAmazonStub.args[0][0].groupId).not.exist;
expect(subscribeWithAmazonStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken);
expect(subscribeWithAmazonStub.args[0][0].headers['x-api-user']).to.eql(user._id);
});
it('creates a group subscription', async () => {
user = await generateUser({
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
balance: 2,
});
group = await generateGroup(user, {
name: 'test group',
type: 'guild',
privacy: 'public',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
});
await user.post(endpoint, {
billingAgreementId,
subscription,
coupon,
groupId: group._id,
});
expect(subscribeWithAmazonStub).to.be.calledOnce;
expect(subscribeWithAmazonStub.args[0][0].billingAgreementId).to.eql(billingAgreementId);
expect(subscribeWithAmazonStub.args[0][0].sub).to.exist;
expect(subscribeWithAmazonStub.args[0][0].coupon).to.eql(coupon);
expect(subscribeWithAmazonStub.args[0][0].user._id).to.eql(user._id);
expect(subscribeWithAmazonStub.args[0][0].groupId).to.eql(group._id);
expect(subscribeWithAmazonStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken);
expect(subscribeWithAmazonStub.args[0][0].headers['x-api-user']).to.eql(user._id);
});
});
});

View File

@@ -1,6 +1,6 @@
import {
generateUser,
} from '../../../../helpers/api-integration/v3';
} from '../../../../../helpers/api-integration/v3';
describe('payments : amazon', () => {
let endpoint = '/amazon/verifyAccessToken';

View File

@@ -0,0 +1,40 @@
import {
generateUser,
} from '../../../../../helpers/api-integration/v3';
import paypalPayments from '../../../../../../website/server/libs/paypalPayments';
describe('payments : paypal #checkout', () => {
let endpoint = '/paypal/checkout';
let user;
beforeEach(async () => {
user = await generateUser();
});
describe('success', () => {
let checkoutStub;
beforeEach(async () => {
checkoutStub = sinon.stub(paypalPayments, 'checkout').returnsPromise().resolves('/');
});
afterEach(() => {
paypalPayments.checkout.restore();
});
it('creates a purchase link', async () => {
user = await generateUser({
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
balance: 2,
});
await user.get(endpoint);
expect(checkoutStub).to.be.calledOnce;
expect(checkoutStub.args[0][0].gift).to.eql(undefined);
});
});
});

View File

@@ -0,0 +1,66 @@
import {
generateUser,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import paypalPayments from '../../../../../../website/server/libs/paypalPayments';
describe('payments : paypal #checkoutSuccess', () => {
let endpoint = '/paypal/checkout/success';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies paymentId', async () => {
await expect(user.get(endpoint))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingPaymentId'),
});
});
it('verifies customerId', async () => {
await expect(user.get(`${endpoint}?paymentId=test-paymentid`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingCustomerId'),
});
});
describe('success', () => {
let checkoutSuccessStub;
beforeEach(async () => {
checkoutSuccessStub = sinon.stub(paypalPayments, 'checkoutSuccess').returnsPromise().resolves({});
});
afterEach(() => {
paypalPayments.checkoutSuccess.restore();
});
it('makes a purchase', async () => {
let paymentId = 'test-paymentid';
let customerId = 'test-customerId';
user = await generateUser({
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
balance: 2,
});
await user.get(`${endpoint}?PayerID=${customerId}&paymentId=${paymentId}`);
expect(checkoutSuccessStub).to.be.calledOnce;
expect(checkoutSuccessStub.args[0][0].user._id).to.eql(user._id);
expect(checkoutSuccessStub.args[0][0].gift).to.eql(undefined);
expect(checkoutSuccessStub.args[0][0].paymentId).to.eql(paymentId);
expect(checkoutSuccessStub.args[0][0].customerId).to.eql(customerId);
});
});
});

View File

@@ -0,0 +1,55 @@
import {
generateUser,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import paypalPayments from '../../../../../../website/server/libs/paypalPayments';
import shared from '../../../../../../website/common';
describe('payments : paypal #subscribe', () => {
let endpoint = '/paypal/subscribe';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies sub key', async () => {
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingSubKey'),
});
});
describe('success', () => {
let subscribeStub;
beforeEach(async () => {
subscribeStub = sinon.stub(paypalPayments, 'subscribe').returnsPromise().resolves('/');
});
afterEach(() => {
paypalPayments.subscribe.restore();
});
it('makes a purchase', async () => {
let subKey = 'basic_3mo';
let sub = shared.content.subscriptionBlocks[subKey];
user = await generateUser({
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
balance: 2,
});
await user.get(`${endpoint}?sub=${subKey}`);
expect(subscribeStub).to.be.calledOnce;
expect(subscribeStub.args[0][0].sub).to.eql(sub);
expect(subscribeStub.args[0][0].coupon).to.eql(undefined);
});
});
});

View File

@@ -0,0 +1,52 @@
import {
generateUser,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import paypalPayments from '../../../../../../website/server/libs/paypalPayments';
describe('payments : paypal #subscribeCancel', () => {
let endpoint = '/paypal/subscribe/cancel';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies credentials', async () => {
await expect(user.get(endpoint))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('missingSubscription'),
});
});
describe('success', () => {
let subscribeCancelStub;
beforeEach(async () => {
subscribeCancelStub = sinon.stub(paypalPayments, 'subscribeCancel').returnsPromise().resolves('/');
});
afterEach(() => {
paypalPayments.subscribeCancel.restore();
});
it('cancels a subscription', async () => {
user = await generateUser({
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
balance: 2,
});
await user.get(endpoint);
expect(subscribeCancelStub).to.be.calledOnce;
expect(subscribeCancelStub.args[0][0].user._id).to.eql(user._id);
expect(subscribeCancelStub.args[0][0].groupId).to.eql(undefined);
});
});
});

View File

@@ -0,0 +1,56 @@
import {
generateUser,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import paypalPayments from '../../../../../../website/server/libs/paypalPayments';
describe('payments : paypal #subscribeSuccess', () => {
let endpoint = '/paypal/subscribe/success';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies Paypal Block', async () => {
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingPaypalBlock'),
});
});
xdescribe('success', () => {
let subscribeSuccessStub;
beforeEach(async () => {
subscribeSuccessStub = sinon.stub(paypalPayments, 'subscribeSuccess').returnsPromise().resolves({});
});
afterEach(() => {
paypalPayments.subscribeSuccess.restore();
});
it('creates a subscription', async () => {
let token = 'test-token';
user = await generateUser({
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
balance: 2,
});
await user.get(`${endpoint}?token=${token}`);
expect(subscribeSuccessStub).to.be.calledOnce;
expect(subscribeSuccessStub.args[0][0].user._id).to.eql(user._id);
expect(subscribeSuccessStub.args[0][0].block).to.eql(undefined);
expect(subscribeSuccessStub.args[0][0].groupId).to.eql(undefined);
expect(subscribeSuccessStub.args[0][0].token).to.eql(token);
expect(subscribeSuccessStub.args[0][0].headers).to.exist;
});
});
});

View File

@@ -0,0 +1,44 @@
import {
generateUser,
} from '../../../../../helpers/api-integration/v3';
import paypalPayments from '../../../../../../website/server/libs/paypalPayments';
describe('payments - paypal - #ipn', () => {
let endpoint = '/paypal/ipn';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies credentials', async () => {
let result = await user.post(endpoint);
expect(result).to.eql('OK');
});
describe('success', () => {
let ipnStub;
beforeEach(async () => {
ipnStub = sinon.stub(paypalPayments, 'ipn').returnsPromise().resolves({});
});
afterEach(() => {
paypalPayments.ipn.restore();
});
it('makes a purchase', async () => {
user = await generateUser({
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
balance: 2,
});
await user.post(endpoint);
expect(ipnStub).to.be.calledOnce;
});
});
});

View File

@@ -0,0 +1,74 @@
import {
generateUser,
generateGroup,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import stripePayments from '../../../../../../website/server/libs/stripePayments';
describe('payments - stripe - #subscribeCancel', () => {
let endpoint = '/stripe/subscribe/cancel';
let user, group, stripeCancelSubscriptionStub;
beforeEach(async () => {
user = await generateUser();
});
it('verifies credentials', async () => {
await expect(user.get(endpoint)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('missingSubscription'),
});
});
describe('success', () => {
beforeEach(async () => {
stripeCancelSubscriptionStub = sinon.stub(stripePayments, 'cancelSubscription').returnsPromise().resolves({});
});
afterEach(() => {
stripePayments.cancelSubscription.restore();
});
it('cancels a user subscription', async () => {
user = await generateUser({
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
balance: 2,
});
await user.get(`${endpoint}?redirect=none`);
expect(stripeCancelSubscriptionStub).to.be.calledOnce;
expect(stripeCancelSubscriptionStub.args[0][0].user._id).to.eql(user._id);
expect(stripeCancelSubscriptionStub.args[0][0].groupId).to.eql(undefined);
});
it('cancels a group subscription', async () => {
user = await generateUser({
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
balance: 2,
});
group = await generateGroup(user, {
name: 'test group',
type: 'guild',
privacy: 'public',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
});
await user.get(`${endpoint}?groupId=${group._id}&redirect=none`);
expect(stripeCancelSubscriptionStub).to.be.calledOnce;
expect(stripeCancelSubscriptionStub.args[0][0].user._id).to.eql(user._id);
expect(stripeCancelSubscriptionStub.args[0][0].groupId).to.eql(group._id);
});
});
});

View File

@@ -0,0 +1,75 @@
import {
generateUser,
generateGroup,
} from '../../../../../helpers/api-integration/v3';
import stripePayments from '../../../../../../website/server/libs/stripePayments';
describe('payments - stripe - #checkout', () => {
let endpoint = '/stripe/checkout';
let user, group;
beforeEach(async () => {
user = await generateUser();
});
it('verifies credentials', async () => {
await expect(user.post(endpoint, {id: 123})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'Error',
message: 'Invalid API Key provided: ****************************1111',
});
});
describe('success', () => {
let stripeCheckoutSubscriptionStub;
beforeEach(async () => {
stripeCheckoutSubscriptionStub = sinon.stub(stripePayments, 'checkout').returnsPromise().resolves({});
});
afterEach(() => {
stripePayments.checkout.restore();
});
it('cancels a user subscription', async () => {
user = await generateUser({
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
balance: 2,
});
await user.post(endpoint);
expect(stripeCheckoutSubscriptionStub).to.be.calledOnce;
expect(stripeCheckoutSubscriptionStub.args[0][0].user._id).to.eql(user._id);
expect(stripeCheckoutSubscriptionStub.args[0][0].groupId).to.eql(undefined);
});
it('cancels a group subscription', async () => {
user = await generateUser({
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
balance: 2,
});
group = await generateGroup(user, {
name: 'test group',
type: 'guild',
privacy: 'public',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
});
await user.post(`${endpoint}?groupId=${group._id}`);
expect(stripeCheckoutSubscriptionStub).to.be.calledOnce;
expect(stripeCheckoutSubscriptionStub.args[0][0].user._id).to.eql(user._id);
expect(stripeCheckoutSubscriptionStub.args[0][0].groupId).to.eql(group._id);
});
});
});

View File

@@ -0,0 +1,78 @@
import {
generateUser,
generateGroup,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import stripePayments from '../../../../../../website/server/libs/stripePayments';
describe('payments - stripe - #subscribeEdit', () => {
let endpoint = '/stripe/subscribe/edit';
let user, group;
beforeEach(async () => {
user = await generateUser();
});
it('verifies credentials', async () => {
await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('missingSubscription'),
});
});
describe('success', () => {
let stripeEditSubscriptionStub;
beforeEach(async () => {
stripeEditSubscriptionStub = sinon.stub(stripePayments, 'editSubscription').returnsPromise().resolves({});
});
afterEach(() => {
stripePayments.editSubscription.restore();
});
it('cancels a user subscription', async () => {
user = await generateUser({
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
balance: 2,
});
await user.post(endpoint);
expect(stripeEditSubscriptionStub).to.be.calledOnce;
expect(stripeEditSubscriptionStub.args[0][0].user._id).to.eql(user._id);
expect(stripeEditSubscriptionStub.args[0][0].groupId).to.eql(undefined);
});
it('cancels a group subscription', async () => {
user = await generateUser({
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
balance: 2,
});
group = await generateGroup(user, {
name: 'test group',
type: 'guild',
privacy: 'public',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
});
await user.post(endpoint, {
groupId: group._id,
});
expect(stripeEditSubscriptionStub).to.be.calledOnce;
expect(stripeEditSubscriptionStub.args[0][0].user._id).to.eql(user._id);
expect(stripeEditSubscriptionStub.args[0][0].groupId).to.eql(group._id);
});
});
});

View File

@@ -148,5 +148,20 @@ describe('POST /groups/:groupId/quests/accept', () => {
expect(rejectingMember.party.quest.key).to.not.exist;
expect(rejectingMember.party.quest.completed).to.not.exist;
});
it('begins the quest if accepting the last pending invite and verifies chat', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
// quest will start after everyone has accepted
await partyMembers[1].post(`/groups/${questingGroup._id}/quests/accept`);
await questingGroup.sync();
expect(questingGroup.chat[0].text).to.exist;
expect(questingGroup.chat[0]._meta).to.exist;
expect(questingGroup.chat[0]._meta).to.have.all.keys(['participatingMembers']);
let returnedGroup = await leader.get(`/groups/${questingGroup._id}`);
expect(returnedGroup.chat[0]._meta).to.be.undefined;
});
});
});

View File

@@ -231,5 +231,22 @@ describe('POST /groups/:groupId/quests/force-start', () => {
expect(questingGroup.quest.members[partyMembers[0]._id]).to.exist;
expect(questingGroup.quest.members[leader._id]).to.exist;
});
it('allows group leader to force start quest and verifies chat', async () => {
let questLeader = partyMembers[0];
await questLeader.update({[`items.quests.${PET_QUEST}`]: 1});
await questLeader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await leader.post(`/groups/${questingGroup._id}/quests/force-start`);
await questingGroup.sync();
expect(questingGroup.chat[0].text).to.exist;
expect(questingGroup.chat[0]._meta).to.exist;
expect(questingGroup.chat[0]._meta).to.have.all.keys(['participatingMembers']);
let returnedGroup = await leader.get(`/groups/${questingGroup._id}`);
expect(returnedGroup.chat[0]._meta).to.be.undefined;
});
});
});

View File

@@ -188,5 +188,25 @@ describe('POST /groups/:groupId/quests/invite/:questKey', () => {
expect(group.quest.active).to.eql(true);
});
it('starts quest automatically if user is in a solo party and verifies chat', async () => {
let leaderDetails = { balance: 10 };
leaderDetails[`items.quests.${PET_QUEST}`] = 1;
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
leaderDetails,
});
await groupLeader.post(`/groups/${group._id}/quests/invite/${PET_QUEST}`);
await group.sync();
expect(group.chat[0].text).to.exist;
expect(group.chat[0]._meta).to.exist;
expect(group.chat[0]._meta).to.have.all.keys(['participatingMembers']);
let returnedGroup = await groupLeader.get(`/groups/${group._id}`);
expect(returnedGroup.chat[0]._meta).to.be.undefined;
});
});
});

View File

@@ -86,11 +86,12 @@ describe('POST /groups/:groupId/quests/abort', () => {
});
it('aborts a quest', async () => {
sandbox.stub(Group.prototype, 'sendChat');
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
await partyMembers[1].post(`/groups/${questingGroup._id}/quests/accept`);
let stub = sandbox.stub(Group.prototype, 'sendChat');
let res = await leader.post(`/groups/${questingGroup._id}/quests/abort`);
await Promise.all([
leader.sync(),
@@ -127,6 +128,7 @@ describe('POST /groups/:groupId/quests/abort', () => {
});
expect(Group.prototype.sendChat).to.be.calledOnce;
expect(Group.prototype.sendChat).to.be.calledWithMatch(/aborted the party quest Wail of the Whale.`/);
Group.prototype.sendChat.restore();
stub.restore();
});
});

View File

@@ -180,5 +180,19 @@ describe('POST /groups/:groupId/quests/reject', () => {
expect(rejectingMember.party.quest.key).to.not.exist;
expect(rejectingMember.party.quest.completed).to.not.exist;
});
it('starts the quest when the last user reject and verifies chat', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
await partyMembers[1].post(`/groups/${questingGroup._id}/quests/reject`);
await questingGroup.sync();
expect(questingGroup.chat[0].text).to.exist;
expect(questingGroup.chat[0]._meta).to.exist;
expect(questingGroup.chat[0]._meta).to.have.all.keys(['participatingMembers']);
let returnedGroup = await leader.get(`/groups/${questingGroup._id}`);
expect(returnedGroup.chat[0]._meta).to.be.undefined;
});
});
});

View File

@@ -5,7 +5,7 @@ import {
} from '../../../../helpers/api-integration/v3';
describe('POST /tasks/clearCompletedTodos', () => {
it('deletes all completed todos except the ones from a challenge', async () => {
it('deletes all completed todos except the ones from a challenge and group', async () => {
let user = await generateUser({balance: 1});
let guild = await generateGroup(user);
let challenge = await generateChallenge(user, guild);
@@ -24,8 +24,14 @@ describe('POST /tasks/clearCompletedTodos', () => {
type: 'todo',
});
let groupTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'todo 7',
type: 'todo',
});
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
let tasks = await user.get('/tasks/user?type=todos');
expect(tasks.length).to.equal(initialTodoCount + 6);
expect(tasks.length).to.equal(initialTodoCount + 7);
for (let task of tasks) {
if (['todo 2', 'todo 3', 'todo 6'].indexOf(task.text) !== -1) {
@@ -38,6 +44,6 @@ describe('POST /tasks/clearCompletedTodos', () => {
let todos = await user.get('/tasks/user?type=todos');
let allTodos = todos.concat(completedTodos);
expect(allTodos.length).to.equal(initialTodoCount + 4); // + 6 - 3 completed (but one is from challenge)
expect(allTodos[allTodos.length - 1].text).to.equal('todo 6');
expect(allTodos[allTodos.length - 1].text).to.equal('todo 7');
});
});

View File

@@ -81,4 +81,36 @@ describe('DELETE /tasks/:id', () => {
message: t('cantDeleteAssignedGroupTasks'),
});
});
it('allows a user to delete a broken task', async () => {
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await user.del(`/tasks/${task._id}`);
await member.del(`/tasks/${syncedTask._id}`);
await expect(member.get(`/tasks/${syncedTask._id}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: 'Task not found.',
});
});
it('allows a user to delete a task after leaving a group', async () => {
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/groups/${guild._id}/leave`);
await member.del(`/tasks/${syncedTask._id}`);
await expect(member.get(`/tasks/${syncedTask._id}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: 'Task not found.',
});
});
});

View File

@@ -34,6 +34,10 @@ describe('POST /tasks/:id/score/:direction', () => {
});
it('prevents user from scoring a task that needs to be approved', async () => {
await user.update({
'preferences.language': 'cs',
});
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
@@ -52,7 +56,7 @@ describe('POST /tasks/:id/score/:direction', () => {
expect(user.notifications[0].data.message).to.equal(t('userHasRequestedTaskApproval', {
user: member.auth.local.username,
taskName: updatedTask.text,
}));
}, 'cs')); // This test only works if we have the notification translated
expect(user.notifications[0].data.groupId).to.equal(guild._id);
expect(updatedTask.group.approval.requested).to.equal(true);

View File

@@ -99,6 +99,7 @@ describe('POST /tasks/:taskId', () => {
let updateGroup = await user.get(`/groups/${guild._id}`);
expect(updateGroup.chat[0].text).to.equal(t('userIsClamingTask', {username: member.profile.name, task: task.text}));
expect(updateGroup.chat[0].uuid).to.equal('system');
});
it('assigns a task to a user', async () => {

View File

@@ -0,0 +1,49 @@
import {
generateUser,
generateGroup,
} from '../../../../../helpers/api-v3-integration.helper';
describe('POST group-tasks/:taskId/move/to/:position', () => {
let user, guild;
beforeEach(async () => {
user = await generateUser({balance: 1});
guild = await generateGroup(user, {type: 'guild'});
});
it('can move task to new position', async () => {
let tasks = await user.post(`/tasks/group/${guild._id}`, [
{type: 'habit', text: 'habit 1'},
{type: 'habit', text: 'habit 2'},
{type: 'daily', text: 'daily 1'},
{type: 'habit', text: 'habit 3'},
{type: 'habit', text: 'habit 4'},
{type: 'todo', text: 'todo 1'},
{type: 'habit', text: 'habit 5'},
]);
let taskToMove = tasks[1];
expect(taskToMove.text).to.equal('habit 2');
let newOrder = await user.post(`/group-tasks/${tasks[1]._id}/move/to/3`);
expect(newOrder[3]).to.equal(taskToMove._id);
expect(newOrder.length).to.equal(5);
});
it('can push to bottom', async () => {
let tasks = await user.post(`/tasks/group/${guild._id}`, [
{type: 'habit', text: 'habit 1'},
{type: 'habit', text: 'habit 2'},
{type: 'daily', text: 'daily 1'},
{type: 'habit', text: 'habit 3'},
{type: 'habit', text: 'habit 4'},
{type: 'todo', text: 'todo 1'},
{type: 'habit', text: 'habit 5'},
]);
let taskToMove = tasks[1];
expect(taskToMove.text).to.equal('habit 2');
let newOrder = await user.post(`/group-tasks/${tasks[1]._id}/move/to/-1`);
expect(newOrder[4]).to.equal(taskToMove._id);
expect(newOrder.length).to.equal(5);
});
});

View File

@@ -62,15 +62,6 @@ describe('POST /user/buy/:key', () => {
await user.post(`/user/buy/${key}`);
await user.sync();
expect(user.items.gear.owned).to.eql({
armor_warrior_1: true,
eyewear_special_blackTopFrame: true,
eyewear_special_blueTopFrame: true,
eyewear_special_greenTopFrame: true,
eyewear_special_pinkTopFrame: true,
eyewear_special_redTopFrame: true,
eyewear_special_whiteTopFrame: true,
eyewear_special_yellowTopFrame: true,
});
expect(user.items.gear.owned.armor_warrior_1).to.eql(true);
});
});

View File

@@ -31,15 +31,6 @@ describe('POST /user/buy-gear/:key', () => {
await user.post(`/user/buy-gear/${key}`);
await user.sync();
expect(user.items.gear.owned).to.eql({
armor_warrior_1: true,
eyewear_special_blackTopFrame: true,
eyewear_special_blueTopFrame: true,
eyewear_special_greenTopFrame: true,
eyewear_special_pinkTopFrame: true,
eyewear_special_redTopFrame: true,
eyewear_special_whiteTopFrame: true,
eyewear_special_yellowTopFrame: true,
});
expect(user.items.gear.owned.armor_warrior_1).to.eql(true);
});
});

View File

@@ -7,6 +7,7 @@ import {
} from '../../../../helpers/api-integration/v3';
import { v4 as generateUUID } from 'uuid';
import { find } from 'lodash';
describe('POST /user/class/cast/:spellId', () => {
let user;
@@ -120,6 +121,31 @@ describe('POST /user/class/cast/:spellId', () => {
});
});
it('returns an error if a group task was targeted', async () => {
let {group, groupLeader} = await createAndPopulateGroup();
let groupTask = await groupLeader.post(`/tasks/group/${group._id}`, {
text: 'todo group',
type: 'todo',
});
await groupLeader.post(`/tasks/${groupTask._id}/assign/${groupLeader._id}`);
let memberTasks = await groupLeader.get('/tasks/user');
let syncedGroupTask = find(memberTasks, function findAssignedTask (memberTask) {
return memberTask.group.id === group._id;
});
await groupLeader.update({'stats.class': 'rogue', 'stats.lvl': 11});
await sleep(0.5);
await groupLeader.sync();
await expect(groupLeader.post(`/user/class/cast/pickPocket?targetId=${syncedGroupTask._id}`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('groupTasksNoCast'),
});
});
it('returns an error if targeted party member doesn\'t exist', async () => {
let {groupLeader} = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },

View File

@@ -4,6 +4,7 @@ import {
generateChallenge,
translate as t,
} from '../../../../helpers/api-integration/v3';
import { find } from 'lodash';
describe('POST /user/reset', () => {
let user;
@@ -86,19 +87,34 @@ describe('POST /user/reset', () => {
expect(user.tasksOrder.rewards).to.be.empty;
});
it('does not delete challenge tasks', async () => {
it('does not delete challenge or group tasks', async () => {
let guild = await generateGroup(user);
let challenge = await generateChallenge(user, guild);
let task = await user.post(`/tasks/challenge/${challenge._id}`, {
await user.post(`/tasks/challenge/${challenge._id}`, {
text: 'test challenge habit',
type: 'habit',
});
let groupTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'todo group',
type: 'todo',
});
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
await user.post('/user/reset');
await user.sync();
let userChallengeTask = await user.get(`/tasks/${task._id}`);
let memberTasks = await user.get('/tasks/user');
expect(userChallengeTask).to.eql(task);
let syncedGroupTask = find(memberTasks, function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
});
let userChallengeTask = find(memberTasks, function findAssignedTask (memberTask) {
return memberTask.challenge.id === challenge._id;
});
expect(userChallengeTask).to.exist;
expect(syncedGroupTask).to.exist;
});
});

View File

@@ -26,6 +26,32 @@ describe('PUT /user', () => {
expect(user.preferences.costume).to.eql(true);
expect(user.stats.hp).to.eql(14);
});
it('profile.name cannot be an empty string or null', async () => {
await expect(user.put('/user', {
'profile.name': ' ', // string should be trimmed
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'User validation failed',
});
await expect(user.put('/user', {
'profile.name': '',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'User validation failed',
});
await expect(user.put('/user', {
'profile.name': null,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'User validation failed',
});
});
});
context('Top Level Protected Operations', () => {

View File

@@ -32,6 +32,7 @@ describe('POST /user/auth/local/register', () => {
expect(user._id).to.exist;
expect(user.apiToken).to.exist;
expect(user.auth.local.username).to.eql(username);
expect(user.profile.name).to.eql(username);
});
it('provides default tags and tasks', async () => {

View File

@@ -33,7 +33,7 @@ describe('POST /user/auth/social', () => {
describe('facebook', () => {
before(async () => {
let expectedResult = {id: facebookId};
let expectedResult = {id: facebookId, displayName: 'a facebook user'};
sandbox.stub(passport._strategies.facebook, 'userProfile').yields(null, expectedResult);
network = 'facebook';
});
@@ -47,6 +47,7 @@ describe('POST /user/auth/social', () => {
expect(response.apiToken).to.exist;
expect(response.id).to.exist;
expect(response.newUser).to.be.true;
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a facebook user');
});
it('logs an existing user in', async () => {
@@ -88,7 +89,7 @@ describe('POST /user/auth/social', () => {
describe('google', () => {
before(async () => {
let expectedResult = {id: googleId};
let expectedResult = {id: googleId, displayName: 'a google user'};
sandbox.stub(passport._strategies.google, 'userProfile').yields(null, expectedResult);
network = 'google';
});
@@ -102,6 +103,7 @@ describe('POST /user/auth/social', () => {
expect(response.apiToken).to.exist;
expect(response.id).to.exist;
expect(response.newUser).to.be.true;
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a google user');
});
it('logs an existing user in', async () => {

View File

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

View File

@@ -303,6 +303,7 @@ describe('analyticsService', () => {
contributorLevel: 1,
subscription: 'foo-plan',
balance: 12,
balanceGemAmount: 48,
loginIncentives: 1,
},
});

View File

@@ -11,7 +11,6 @@ import {
generateGroup,
} from '../../../../helpers/api-unit.helper.js';
import i18n from '../../../../../website/common/script/i18n';
import amzLib from '../../../../../website/server/libs/amazonPayments';
describe('payments/index', () => {
let user, group, data, plan;
@@ -818,109 +817,4 @@ describe('payments/index', () => {
expect(updatedGroup.purchased.plan.quantity).to.eql(3);
});
});
describe('payWithStripe', () => {
let spy;
let stripeCreateCustomerSpy;
let createSubSpy;
beforeEach(function () {
spy = sinon.stub(stripe.subscriptions, 'update');
spy.returnsPromise().resolves;
stripeCreateCustomerSpy = sinon.stub(stripe.customers, 'create');
let stripCustomerResponse = {
subscriptions: {
data: [{id: 'test-id'}],
},
};
stripeCreateCustomerSpy.returnsPromise().resolves(stripCustomerResponse);
createSubSpy = sinon.stub(api, 'createSubscription');
createSubSpy.returnsPromise().resolves({});
data.groupId = group._id;
data.sub.quantity = 3;
});
afterEach(function () {
sinon.restore(stripe.subscriptions.update);
stripe.customers.create.restore();
api.createSubscription.restore();
});
it('subscribes with stripe', async () => {
let token = 'test-token';
let gift;
let sub = data.sub;
let groupId = group._id;
let email = 'test@test.com';
let headers = {};
let coupon;
await api.payWithStripe({
token,
user,
gift,
sub,
groupId,
email,
headers,
coupon,
}, stripe);
expect(stripeCreateCustomerSpy.calledOnce).to.be.true;
expect(createSubSpy.calledOnce).to.be.true;
});
});
describe('subscribeWithAmazon', () => {
let amazonSetBillingAgreementDetailsSpy;
let amazonConfirmBillingAgreementSpy;
let amazongAuthorizeOnBillingAgreementSpy;
let createSubSpy;
beforeEach(function () {
amazonSetBillingAgreementDetailsSpy = sinon.stub(amzLib, 'setBillingAgreementDetails');
amazonSetBillingAgreementDetailsSpy.returnsPromise().resolves({});
amazonConfirmBillingAgreementSpy = sinon.stub(amzLib, 'confirmBillingAgreement');
amazonConfirmBillingAgreementSpy.returnsPromise().resolves({});
amazongAuthorizeOnBillingAgreementSpy = sinon.stub(amzLib, 'authorizeOnBillingAgreement');
amazongAuthorizeOnBillingAgreementSpy.returnsPromise().resolves({});
createSubSpy = sinon.stub(api, 'createSubscription');
createSubSpy.returnsPromise().resolves({});
});
afterEach(function () {
amzLib.setBillingAgreementDetails.restore();
amzLib.confirmBillingAgreement.restore();
amzLib.authorizeOnBillingAgreement.restore();
api.createSubscription.restore();
});
it('subscribes with amazon', async () => {
let billingAgreementId = 'billingAgreementId';
let sub = data.sub;
let coupon;
let groupId = group._id;
let headers = {};
await api.subscribeWithAmazon({
billingAgreementId,
sub,
coupon,
user,
groupId,
headers,
});
expect(amazonSetBillingAgreementDetailsSpy.calledOnce).to.be.true;
expect(amazonConfirmBillingAgreementSpy.calledOnce).to.be.true;
expect(amazongAuthorizeOnBillingAgreementSpy.calledOnce).to.be.true;
expect(createSubSpy.calledOnce).to.be.true;
});
});
});

View File

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

View File

@@ -0,0 +1,661 @@
import stripeModule from 'stripe';
import cc from 'coupon-code';
import {
generateGroup,
} from '../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../website/server/models/user';
import { model as Coupon } from '../../../../../website/server/models/coupon';
import stripePayments from '../../../../../website/server/libs/stripePayments';
import payments from '../../../../../website/server/libs/payments';
import common from '../../../../../website/common';
const i18n = common.i18n;
describe('Stripe Payments', () => {
let subKey = 'basic_3mo';
let stripe = stripeModule('test');
describe('checkout', () => {
let stripeChargeStub, paymentBuyGemsStub, paymentCreateSubscritionStub;
let user, gift, groupId, email, headers, coupon, customerIdResponse, token;
beforeEach(() => {
user = new User();
user.profile.name = 'sender';
user.purchased.plan.customerId = 'customer-id';
user.purchased.plan.planId = subKey;
user.purchased.plan.lastBillingDate = new Date();
token = 'test-token';
customerIdResponse = 'example-customerIdResponse';
let stripCustomerResponse = {
id: customerIdResponse,
};
stripeChargeStub = sinon.stub(stripe.charges, 'create').returnsPromise().resolves(stripCustomerResponse);
paymentBuyGemsStub = sinon.stub(payments, 'buyGems').returnsPromise().resolves({});
paymentCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({});
});
afterEach(() => {
stripe.charges.create.restore();
payments.buyGems.restore();
payments.createSubscription.restore();
});
it('should purchase gems', async () => {
await stripePayments.checkout({
token,
user,
gift,
groupId,
email,
headers,
coupon,
}, stripe);
expect(stripeChargeStub).to.be.calledOnce;
expect(stripeChargeStub).to.be.calledWith({
amount: 500,
currency: 'usd',
card: token,
});
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,
customerId: customerIdResponse,
paymentMethod: 'Stripe',
gift,
});
});
it('should gift gems', async () => {
let receivingUser = new User();
receivingUser.save();
gift = {
type: 'gems',
gems: {
amount: 16,
uuid: receivingUser._id,
},
};
await stripePayments.checkout({
token,
user,
gift,
groupId,
email,
headers,
coupon,
}, stripe);
gift.member = receivingUser;
expect(stripeChargeStub).to.be.calledOnce;
expect(stripeChargeStub).to.be.calledWith({
amount: '400',
currency: 'usd',
card: token,
});
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,
customerId: customerIdResponse,
paymentMethod: 'Gift',
gift,
});
});
it('should gift a subscription', async () => {
let receivingUser = new User();
receivingUser.save();
gift = {
type: 'subscription',
subscription: {
key: subKey,
uuid: receivingUser._id,
},
};
await stripePayments.checkout({
token,
user,
gift,
groupId,
email,
headers,
coupon,
}, stripe);
gift.member = receivingUser;
expect(stripeChargeStub).to.be.calledOnce;
expect(stripeChargeStub).to.be.calledWith({
amount: '1500',
currency: 'usd',
card: token,
});
expect(paymentCreateSubscritionStub).to.be.calledOnce;
expect(paymentCreateSubscritionStub).to.be.calledWith({
user,
customerId: customerIdResponse,
paymentMethod: 'Gift',
gift,
});
});
});
describe('checkout with subscription', () => {
let user, group, data, gift, sub, groupId, email, headers, coupon, customerIdResponse, subscriptionId, token;
let spy;
let stripeCreateCustomerSpy;
let stripePaymentsCreateSubSpy;
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
user.purchased.plan.customerId = 'customer-id';
user.purchased.plan.planId = subKey;
user.purchased.plan.lastBillingDate = new Date();
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
group.purchased.plan.customerId = 'customer-id';
group.purchased.plan.planId = subKey;
await group.save();
sub = {
key: 'basic_3mo',
};
data = {
user,
sub,
customerId: 'customer-id',
paymentMethod: 'Payment Method',
};
email = 'example@example.com';
customerIdResponse = 'test-id';
subscriptionId = 'test-sub-id';
token = 'test-token';
spy = sinon.stub(stripe.subscriptions, 'update');
spy.returnsPromise().resolves;
stripeCreateCustomerSpy = sinon.stub(stripe.customers, 'create');
let stripCustomerResponse = {
id: customerIdResponse,
subscriptions: {
data: [{id: subscriptionId}],
},
};
stripeCreateCustomerSpy.returnsPromise().resolves(stripCustomerResponse);
stripePaymentsCreateSubSpy = sinon.stub(payments, 'createSubscription');
stripePaymentsCreateSubSpy.returnsPromise().resolves({});
data.groupId = group._id;
data.sub.quantity = 3;
});
afterEach(function () {
sinon.restore(stripe.subscriptions.update);
stripe.customers.create.restore();
payments.createSubscription.restore();
});
it('should throw an error if we are missing a token', async () => {
await expect(stripePayments.checkout({
user,
gift,
sub,
groupId,
email,
headers,
coupon,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
message: 'Missing req.body.id',
});
});
it('should throw an error when coupon code is missing', async () => {
sub.discount = 40;
await expect(stripePayments.checkout({
token,
user,
gift,
sub,
groupId,
email,
headers,
coupon,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
message: i18n.t('couponCodeRequired'),
});
});
it('should throw an error when coupon code is invalid', async () => {
sub.discount = 40;
sub.key = 'google_6mo';
coupon = 'example-coupon';
let couponModel = new Coupon();
couponModel.event = 'google_6mo';
await couponModel.save();
sinon.stub(cc, 'validate').returns('invalid');
await expect(stripePayments.checkout({
token,
user,
gift,
sub,
groupId,
email,
headers,
coupon,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
message: i18n.t('invalidCoupon'),
});
cc.validate.restore();
});
it('subscribes with amazon with a coupon', async () => {
sub.discount = 40;
sub.key = 'google_6mo';
coupon = 'example-coupon';
let couponModel = new Coupon();
couponModel.event = 'google_6mo';
let updatedCouponModel = await couponModel.save();
sinon.stub(cc, 'validate').returns(updatedCouponModel._id);
await stripePayments.checkout({
token,
user,
gift,
sub,
groupId,
email,
headers,
coupon,
}, stripe);
expect(stripeCreateCustomerSpy).to.be.calledOnce;
expect(stripeCreateCustomerSpy).to.be.calledWith({
email,
metadata: { uuid: user._id },
card: token,
plan: sub.key,
});
expect(stripePaymentsCreateSubSpy).to.be.calledOnce;
expect(stripePaymentsCreateSubSpy).to.be.calledWith({
user,
customerId: customerIdResponse,
paymentMethod: 'Stripe',
sub,
headers,
groupId: undefined,
subscriptionId: undefined,
});
cc.validate.restore();
});
it('subscribes a user', async () => {
sub = data.sub;
await stripePayments.checkout({
token,
user,
gift,
sub,
groupId,
email,
headers,
coupon,
}, stripe);
expect(stripeCreateCustomerSpy).to.be.calledOnce;
expect(stripeCreateCustomerSpy).to.be.calledWith({
email,
metadata: { uuid: user._id },
card: token,
plan: sub.key,
});
expect(stripePaymentsCreateSubSpy).to.be.calledOnce;
expect(stripePaymentsCreateSubSpy).to.be.calledWith({
user,
customerId: customerIdResponse,
paymentMethod: 'Stripe',
sub,
headers,
groupId: undefined,
subscriptionId: undefined,
});
});
it('subscribes a group', async () => {
token = 'test-token';
sub = data.sub;
groupId = group._id;
email = 'test@test.com';
headers = {};
await stripePayments.checkout({
token,
user,
gift,
sub,
groupId,
email,
headers,
coupon,
}, stripe);
expect(stripeCreateCustomerSpy).to.be.calledOnce;
expect(stripeCreateCustomerSpy).to.be.calledWith({
email,
metadata: { uuid: user._id },
card: token,
plan: sub.key,
quantity: 3,
});
expect(stripePaymentsCreateSubSpy).to.be.calledOnce;
expect(stripePaymentsCreateSubSpy).to.be.calledWith({
user,
customerId: customerIdResponse,
paymentMethod: 'Stripe',
sub,
headers,
groupId,
subscriptionId,
});
});
});
describe('edit subscription', () => {
let user, groupId, group, token;
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
user.purchased.plan.customerId = 'customer-id';
user.purchased.plan.planId = subKey;
user.purchased.plan.lastBillingDate = new Date();
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
group.purchased.plan.customerId = 'customer-id';
group.purchased.plan.planId = subKey;
await group.save();
groupId = group._id;
token = 'test-token';
});
it('throws an error if there is no customer id', async () => {
user.purchased.plan.customerId = undefined;
await expect(stripePayments.editSubscription({
user,
groupId: undefined,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('missingSubscription'),
});
});
it('throws an error if a token is not provided', async () => {
await expect(stripePayments.editSubscription({
user,
groupId: undefined,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
message: 'Missing req.body.id',
});
});
it('throws an error if the group is not found', async () => {
await expect(stripePayments.editSubscription({
token,
user,
groupId: 'fake-group',
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 404,
name: 'NotFound',
message: i18n.t('groupNotFound'),
});
});
it('throws an error if user is not the group leader', async () => {
let nonLeader = new User();
nonLeader.guilds.push(groupId);
await nonLeader.save();
await expect(stripePayments.editSubscription({
token,
user: nonLeader,
groupId,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
});
});
describe('success', () => {
let stripeListSubscriptionStub, stripeUpdateSubscriptionStub, subscriptionId;
beforeEach(() => {
subscriptionId = 'subId';
stripeListSubscriptionStub = sinon.stub(stripe.customers, 'listSubscriptions')
.returnsPromise().resolves({
data: [{id: subscriptionId}],
});
stripeUpdateSubscriptionStub = sinon.stub(stripe.customers, 'updateSubscription').returnsPromise().resolves({});
});
afterEach(() => {
stripe.customers.listSubscriptions.restore();
stripe.customers.updateSubscription.restore();
});
it('edits a user subscription', async () => {
await stripePayments.editSubscription({
token,
user,
groupId: undefined,
}, stripe);
expect(stripeListSubscriptionStub).to.be.calledOnce;
expect(stripeListSubscriptionStub).to.be.calledWith(user.purchased.plan.customerId);
expect(stripeUpdateSubscriptionStub).to.be.calledOnce;
expect(stripeUpdateSubscriptionStub).to.be.calledWith(
user.purchased.plan.customerId,
subscriptionId,
{ card: token }
);
});
it('edits a group subscription', async () => {
await stripePayments.editSubscription({
token,
user,
groupId,
}, stripe);
expect(stripeListSubscriptionStub).to.be.calledOnce;
expect(stripeListSubscriptionStub).to.be.calledWith(group.purchased.plan.customerId);
expect(stripeUpdateSubscriptionStub).to.be.calledOnce;
expect(stripeUpdateSubscriptionStub).to.be.calledWith(
group.purchased.plan.customerId,
subscriptionId,
{ card: token }
);
});
});
});
describe('cancel subscription', () => {
let user, groupId, group;
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
user.purchased.plan.customerId = 'customer-id';
user.purchased.plan.planId = subKey;
user.purchased.plan.lastBillingDate = new Date();
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
leader: user._id,
});
group.purchased.plan.customerId = 'customer-id';
group.purchased.plan.planId = subKey;
await group.save();
groupId = group._id;
});
it('throws an error if there is no customer id', async () => {
user.purchased.plan.customerId = undefined;
await expect(stripePayments.cancelSubscription({
user,
groupId: undefined,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('missingSubscription'),
});
});
it('throws an error if the group is not found', async () => {
await expect(stripePayments.cancelSubscription({
user,
groupId: 'fake-group',
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 404,
name: 'NotFound',
message: i18n.t('groupNotFound'),
});
});
it('throws an error if user is not the group leader', async () => {
let nonLeader = new User();
nonLeader.guilds.push(groupId);
await nonLeader.save();
await expect(stripePayments.cancelSubscription({
user: nonLeader,
groupId,
}))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
});
});
describe('success', () => {
let stripeDeleteCustomerStub, paymentsCancelSubStub, stripeRetrieveStub, subscriptionId, currentPeriodEndTimeStamp;
beforeEach(() => {
subscriptionId = 'subId';
stripeDeleteCustomerStub = sinon.stub(stripe.customers, 'del').returnsPromise().resolves({});
paymentsCancelSubStub = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
currentPeriodEndTimeStamp = (new Date()).getTime();
stripeRetrieveStub = sinon.stub(stripe.customers, 'retrieve')
.returnsPromise().resolves({
subscriptions: {
data: [{id: subscriptionId, current_period_end: currentPeriodEndTimeStamp}], // eslint-disable-line camelcase
},
});
});
afterEach(() => {
stripe.customers.del.restore();
stripe.customers.retrieve.restore();
payments.cancelSubscription.restore();
});
it('cancels a user subscription', async () => {
await stripePayments.cancelSubscription({
user,
groupId: undefined,
}, stripe);
expect(stripeDeleteCustomerStub).to.be.calledOnce;
expect(stripeDeleteCustomerStub).to.be.calledWith(user.purchased.plan.customerId);
expect(stripeRetrieveStub).to.be.calledOnce;
expect(stripeRetrieveStub).to.be.calledWith(user.purchased.plan.customerId);
expect(paymentsCancelSubStub).to.be.calledOnce;
expect(paymentsCancelSubStub).to.be.calledWith({
user,
groupId: undefined,
nextBill: currentPeriodEndTimeStamp * 1000, // timestamp in seconds
paymentMethod: 'Stripe',
});
});
it('cancels a group subscription', async () => {
await stripePayments.cancelSubscription({
user,
groupId,
}, stripe);
expect(stripeDeleteCustomerStub).to.be.calledOnce;
expect(stripeDeleteCustomerStub).to.be.calledWith(group.purchased.plan.customerId);
expect(stripeRetrieveStub).to.be.calledOnce;
expect(stripeRetrieveStub).to.be.calledWith(user.purchased.plan.customerId);
expect(paymentsCancelSubStub).to.be.calledOnce;
expect(paymentsCancelSubStub).to.be.calledWith({
user,
groupId,
nextBill: currentPeriodEndTimeStamp * 1000, // timestamp in seconds
paymentMethod: 'Stripe',
});
});
});
});
});

View File

@@ -2,6 +2,7 @@ import {
createTasks,
getTasks,
syncableAttrs,
moveTask,
} from '../../../../../website/server/libs/taskManager';
import i18n from '../../../../../website/common/script/i18n';
import {
@@ -169,4 +170,12 @@ describe('taskManager', () => {
expect(syncableTask.notes).to.not.exist;
expect(syncableTask.updatedAt).to.not.exist;
});
it('moves tasks to a specified position', async() => {
let order = ['task-id-1', 'task-id-2'];
moveTask(order, 'task-id-2', 0);
expect(order).to.eql(['task-id-2', 'task-id-1']);
});
});

View File

@@ -170,15 +170,19 @@ describe('Group Model', () => {
});
context('Boss Quests', () => {
let sendChatStub;
beforeEach(async () => {
party.quest.key = 'whale';
await party.startQuest(questLeader);
await party.save();
sandbox.stub(Group.prototype, 'sendChat');
sendChatStub = sandbox.stub(Group.prototype, 'sendChat');
});
afterEach(() => sendChatStub.restore());
it('applies user\'s progress to quest boss hp', async () => {
await Group.processQuestProgress(participatingMember, progress);
@@ -322,15 +326,19 @@ describe('Group Model', () => {
});
context('Collection Quests', () => {
let sendChatStub;
beforeEach(async () => {
party.quest.key = 'atom1';
await party.startQuest(questLeader);
await party.save();
sandbox.stub(Group.prototype, 'sendChat');
sendChatStub = sandbox.stub(Group.prototype, 'sendChat');
});
afterEach(() => sendChatStub.restore());
it('applies user\'s progress to found quest items', async () => {
await Group.processQuestProgress(participatingMember, progress);
@@ -365,6 +373,7 @@ describe('Group Model', () => {
party.quest.active = false;
await party.startQuest(questLeader);
Group.prototype.sendChat.reset();
await party.save();
await Group.processQuestProgress(participatingMember, progress);
@@ -383,6 +392,7 @@ describe('Group Model', () => {
party.quest.active = false;
await party.startQuest(questLeader);
Group.prototype.sendChat.reset();
await party.save();
await Group.processQuestProgress(participatingMember, progress);
@@ -809,6 +819,20 @@ describe('Group Model', () => {
expect(party.chat).to.have.a.lengthOf(200);
});
it('cuts down chat to 400 messages when group is subcribed', () => {
party.purchased.plan.customerId = 'test-customer-id';
for (let i = 0; i < 420; i++) {
party.chat.push({ text: 'a message' });
}
expect(party.chat).to.have.a.lengthOf(420);
party.sendChat('message');
expect(party.chat).to.have.a.lengthOf(400);
});
it('updates users about new messages in party', () => {
party.sendChat('message');
@@ -1259,14 +1283,34 @@ describe('Group Model', () => {
expect(updatedParticipatingMember.items.hatchingPotions.Shade).to.eql(2);
});
it('awards quests', async () => {
it('awards quest scrolls to owner', async () => {
let questAwardQuest = questScrolls.vice2;
await party.finishQuest(questAwardQuest);
let updatedLeader = await User.findById(questLeader._id);
expect(updatedLeader.items.quests.vice3).to.eql(1);
});
it('awards non quest leader rewards to quest leader', async () => {
let gearQuest = questScrolls.vice3;
await party.finishQuest(gearQuest);
let updatedLeader = await User.findById(questLeader._id);
expect(updatedLeader.items.gear.owned.weapon_special_2).to.eql(true);
});
it('doesn\'t award quest owner rewards to all participants', async () => {
let questAwardQuest = questScrolls.vice2;
await party.finishQuest(questAwardQuest);
let updatedParticipatingMember = await User.findById(participatingMember._id);
expect(updatedParticipatingMember.items.quests.vice3).to.eql(1);
expect(updatedParticipatingMember.items.quests.vice3).to.not.exist;
});
it('awards pets', async () => {

View File

@@ -1,5 +1,6 @@
import { model as User } from '../../../../../website/server/models/user';
import common from '../../../../../website/common';
import Bluebird from 'bluebird';
describe('User Model', () => {
it('keeps user._tmp when calling .toJSON', () => {
@@ -48,7 +49,7 @@ describe('User Model', () => {
});
context('notifications', () => {
it('can add notifications with data', () => {
it('can add notifications without data', () => {
let user = new User();
user.addNotification('CRON');
@@ -60,7 +61,7 @@ describe('User Model', () => {
expect(userToJSON.notifications[0].data).to.eql({});
});
it('can add notifications without data', () => {
it('can add notifications with data', () => {
let user = new User();
user.addNotification('CRON', {field: 1});
@@ -71,5 +72,77 @@ describe('User Model', () => {
expect(userToJSON.notifications[0].type).to.equal('CRON');
expect(userToJSON.notifications[0].data).to.eql({field: 1});
});
context('static push method', () => {
it('adds notifications for a single member via static method', async() => {
let user = new User();
await user.save();
await User.pushNotification({_id: user._id}, 'CRON');
user = await User.findOne({_id: user._id}).exec();
let userToJSON = user.toJSON();
expect(user.notifications.length).to.equal(1);
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type']);
expect(userToJSON.notifications[0].type).to.equal('CRON');
expect(userToJSON.notifications[0].data).to.eql({});
});
it('validates notifications via static method', async() => {
let user = new User();
await user.save();
expect(User.pushNotification({_id: user._id}, 'BAD_TYPE')).to.eventually.be.rejected;
});
it('adds notifications without data for all given users via static method', async() => {
let user = new User();
let otherUser = new User();
await Bluebird.all([user.save(), otherUser.save()]);
await User.pushNotification({_id: {$in: [user._id, otherUser._id]}}, 'CRON');
user = await User.findOne({_id: user._id}).exec();
let userToJSON = user.toJSON();
expect(user.notifications.length).to.equal(1);
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type']);
expect(userToJSON.notifications[0].type).to.equal('CRON');
expect(userToJSON.notifications[0].data).to.eql({});
user = await User.findOne({_id: otherUser._id}).exec();
userToJSON = user.toJSON();
expect(user.notifications.length).to.equal(1);
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type']);
expect(userToJSON.notifications[0].type).to.equal('CRON');
expect(userToJSON.notifications[0].data).to.eql({});
});
it('adds notifications with data for all given users via static method', async() => {
let user = new User();
let otherUser = new User();
await Bluebird.all([user.save(), otherUser.save()]);
await User.pushNotification({_id: {$in: [user._id, otherUser._id]}}, 'CRON', {field: 1});
user = await User.findOne({_id: user._id}).exec();
let userToJSON = user.toJSON();
expect(user.notifications.length).to.equal(1);
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type']);
expect(userToJSON.notifications[0].type).to.equal('CRON');
expect(userToJSON.notifications[0].data).to.eql({field: 1});
user = await User.findOne({_id: otherUser._id}).exec();
userToJSON = user.toJSON();
expect(user.notifications.length).to.equal(1);
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type']);
expect(userToJSON.notifications[0].type).to.equal('CRON');
expect(userToJSON.notifications[0].data).to.eql({field: 1});
});
});
});
});

View File

@@ -240,6 +240,5 @@ describe('Groups Controller', function() {
describe.skip("clickMember", function() { });
describe.skip("removeMember", function() { });
describe.skip("confirmRemoveMember", function() { });
describe.skip("openInviteModal", function() { });
describe.skip("quickReply", function() { });
});

View File

@@ -0,0 +1,63 @@
describe('Group Tasks Meta Actions Controller', () => {
let rootScope, scope, user, userSerivce;
beforeEach(() => {
module(function($provide) {
$provide.value('User', {});
});
inject(($rootScope, $controller) => {
rootScope = $rootScope;
user = specHelper.newUser();
user._id = "unique-user-id";
userSerivce = {user: user};
scope = $rootScope.$new();
scope.task = {
group: {
assignedUsers: [],
approval: {
required: false,
}
},
};
scope.task._edit = angular.copy(scope.task);
$controller('GroupTaskActionsCtrl', {$scope: scope, User: userSerivce});
});
});
describe('toggleTaskRequiresApproval', function () {
it('toggles task approval required field from false to true', function () {
scope.toggleTaskRequiresApproval();
expect(scope.task._edit.group.approval.required).to.be.true;
});
it('toggles task approval required field from true to false', function () {
scope.task._edit.group.approval.required = true;
scope.toggleTaskRequiresApproval();
expect(scope.task._edit.group.approval.required).to.be.false;
});
});
describe('assign events', function () {
it('adds a group member to assigned users on "addedGroupMember" event ', () => {
var testId = 'test-id';
rootScope.$broadcast('addedGroupMember', testId);
expect(scope.task.group.assignedUsers).to.contain(testId);
expect(scope.task._edit.group.assignedUsers).to.contain(testId);
});
it('removes a group member to assigned users on "addedGroupMember" event ', () => {
var testId = 'test-id';
scope.task.group.assignedUsers.push(testId);
scope.task._edit.group.assignedUsers.push(testId);
rootScope.$broadcast('removedGroupMember', testId);
expect(scope.task.group.assignedUsers).to.not.contain(testId);
expect(scope.task._edit.group.assignedUsers).to.not.contain(testId);
});
});
});

View File

@@ -0,0 +1,42 @@
describe('Group Task Actions Controller', () => {
let scope, user, userSerivce;
beforeEach(() => {
module(function($provide) {
$provide.value('User', {});
});
inject(($rootScope, $controller) => {
user = specHelper.newUser();
user._id = "unique-user-id";
userSerivce = {user: user};
userSerivce.sync = sandbox.stub();
scope = $rootScope.$new();
$controller('GroupTaskMetaActionsCtrl', {$scope: scope, User: userSerivce});
scope.task = {
group: {
assignedUsers: [],
},
};
});
});
describe('claim', () => {
beforeEach(() => {
sandbox.stub(window, 'confirm').returns(true);
});
it('adds user to assigned users of scope task ', () => {
scope.claim();
expect(scope.task.group.assignedUsers).to.contain(user._id);
});
it('syncs user tasks ', () => {
scope.claim();
expect(userSerivce.sync).to.be.calledOnce;
});
});
});

View File

@@ -44,6 +44,7 @@ module.exports = function karmaConfig (config) {
'../../../website/client-old/js/filters/**/*.js',
'../../../website/client-old/js/directives/**/*.js',
'../../../website/client-old/js/controllers/**/*.js',
'../../../website/client-old/js/components/**/*.js',
'../../../test/client-old/spec/specHelper.js',
'../../../test/client-old/spec/**/*.js',

View File

@@ -194,6 +194,7 @@ describe('Analytics Service', function () {
rewards: 1
};
expectedProperties.balance = 12;
expectedProperties.balanceGemAmount = 48;
beforeEach(function() {
user._id = 'unique-user-id';
@@ -243,7 +244,8 @@ describe('Analytics Service', function () {
habits: 1,
rewards: 1
},
balance: 12
balance: 12,
balanceGemAmount: 48
};
beforeEach(function() {

View File

@@ -1,7 +1,7 @@
'use strict';
describe('groupServices', function() {
var $httpBackend, $http, groups, user;
var $httpBackend, $http, groups, user, $rootScope;
var groupApiUrlPrefix = '/api/v3/groups';
beforeEach(function() {
@@ -13,8 +13,10 @@ describe('groupServices', function() {
$provide.value('User', {user: user});
});
inject(function(_$httpBackend_, Groups, User) {
inject(function(_$httpBackend_, _$rootScope_, Groups, User) {
$httpBackend = _$httpBackend_;
$rootScope = _$rootScope_;
$rootScope.openModal = function() {}
groups = Groups;
});
});
@@ -166,4 +168,33 @@ describe('groupServices', function() {
$httpBackend.flush()
});
it('sets a "sendInviteText" property on a party to "Send Invitations"', function() {
var sendInviteText = window.env.t('sendInvitations');
var party = {
type: 'party',
data: {
_id: '1234',
},
};
groups.inviteOrStartParty(party);
expect(party.sendInviteText).to.eql(sendInviteText);
});
it('sets a "sendInviteText" proptery on a guild to "Send Invitations +$3.00/month/user"', function() {
var sendInviteText = window.env.t('sendInvitations');
var guild = {
type: 'guild',
data: {
_id: '12345',
},
purchased: {
plan: {
customerId: '123',
},
},
};
groups.inviteOrStartParty(guild);
expect(guild.sendInviteText).to.eql(sendInviteText + window.env.t('groupAdditionalUserCost'));
});
});

View File

@@ -17,7 +17,14 @@ describe('Tasks Service', function() {
tasks = Tasks;
});
rootScope.openModal = function () {};
rootScope.openModal = function() {
return {
result: {
then: function() {},
catch: function() {},
},
};
};
});
it('calls get user tasks endpoint', function() {
@@ -83,6 +90,14 @@ describe('Tasks Service', function() {
$httpBackend.flush();
});
it('calls group move task endpoint', function() {
var taskId = 1;
var position = 0;
$httpBackend.expectPOST('/api/v3/group-tasks/' + taskId + '/move/to/' + position).respond({});
tasks.moveGroupTask(taskId, position);
$httpBackend.flush();
});
it('calls add check list item endpoint', function() {
var taskId = 1;
$httpBackend.expectPOST(apiV3Prefix + '/' + taskId + '/checklist').respond({});

6
test/client/.eslintrc Normal file
View File

@@ -0,0 +1,6 @@
{
"env": {
"node": true,
"browser": true,
}
}

View File

@@ -7,7 +7,7 @@
// for how to write custom assertions see
// http://nightwatchjs.org/guide#writing-custom-assertions
exports.assertion = function (selector, count) {
this.message = 'Testing if element <' + selector + '> has count: ' + count;
this.message = `Testing if element <${selector}> has count: ${count}`;
this.expected = count;
this.pass = function (val) {
return val === this.expected;
@@ -16,11 +16,10 @@ exports.assertion = function (selector, count) {
return res.value;
};
this.command = function (cb) {
var self = this;
return this.api.execute(function (selector) {
return document.querySelectorAll(selector).length;
}, [selector], function (res) {
cb.call(self, res);
return this.api.execute((el) => {
return document.querySelectorAll(el).length;
}, [selector], (res) => {
cb.call(this, res);
});
};
};

View File

@@ -1,45 +1,48 @@
/* eslint-disable camelcase */
require('babel-register');
var config = require('../../../webpack/config');
const config = require('../../../webpack/config');
const chromeDriverPath = require('chromedriver').path;
// http://nightwatchjs.org/guide#settings-file
module.exports = {
'src_folders': ['test/client/e2e/specs'],
'output_folder': 'test/client/e2e/reports',
'custom_assertions_path': ['test/client/e2e/custom-assertions'],
src_folders: ['test/client/e2e/specs'],
output_folder: 'test/client/e2e/reports',
custom_assertions_path: ['test/client/e2e/custom-assertions'],
'selenium': {
'start_process': true,
'server_path': 'node_modules/selenium-server/lib/runner/selenium-server-standalone-2.53.0.jar',
'host': '127.0.0.1',
'port': 4444,
'cli_args': {
'webdriver.chrome.driver': require('chromedriver').path,
selenium: {
start_process: true,
server_path: 'node_modules/selenium-server/lib/runner/selenium-server-standalone-2.53.0.jar',
host: '127.0.0.1',
port: 4444,
cli_args: {
'webdriver.chrome.driver': chromeDriverPath,
},
},
'test_settings': {
'default': {
'selenium_port': 4444,
'selenium_host': 'localhost',
'silent': true,
'globals': {
'devServerURL': 'http://localhost:' + (process.env.PORT || config.dev.port),
test_settings: {
default: {
selenium_port: 4444,
selenium_host: 'localhost',
silent: true,
globals: {
devServerURL: `http://localhost:${process.env.PORT || config.dev.port}`, // eslint-disable-line no-process-env
},
},
'chrome': {
'desiredCapabilities': {
'browserName': 'chrome',
'javascriptEnabled': true,
'acceptSslCerts': true,
chrome: {
desiredCapabilities: {
browserName: 'chrome',
javascriptEnabled: true,
acceptSslCerts: true,
},
},
'firefox': {
'desiredCapabilities': {
'browserName': 'firefox',
'javascriptEnabled': true,
'acceptSslCerts': true,
firefox: {
desiredCapabilities: {
browserName: 'firefox',
javascriptEnabled: true,
acceptSslCerts: true,
},
},
},

View File

@@ -1,6 +1,6 @@
// 1. start the dev server using production config
process.env.NODE_ENV = 'testing';
var server = require('../../../webpack/dev-server.js');
process.env.NODE_ENV = 'testing'; // eslint-disable-line no-process-env
const server = require('../../../webpack/dev-server.js');
// 2. run the nightwatch test suite against it
// to run in additional browsers:
@@ -9,7 +9,7 @@ var server = require('../../../webpack/dev-server.js');
// or override the environment flag, for example: `npm run e2e -- --env chrome,firefox`
// For more information on Nightwatch's config file, see
// http://nightwatchjs.org/guide#settings-file
var opts = process.argv.slice(2);
let opts = process.argv.slice(2);
if (opts.indexOf('--config') === -1) {
opts = opts.concat(['--config', 'test/client/e2e/nightwatch.conf.js']);
}
@@ -17,8 +17,8 @@ if (opts.indexOf('--env') === -1) {
opts = opts.concat(['--env', 'chrome']);
}
var spawn = require('cross-spawn');
var runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' });
const spawn = require('cross-spawn');
const runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' });
runner.on('exit', function (code) {
server.close();

View File

@@ -2,12 +2,11 @@
// http://nightwatchjs.org/guide#usage
module.exports = {
'default e2e tests': function (browser) {
'default e2e tests' (browser) {
// automatically uses dev Server port from /config.index.js
// default: http://localhost:8080
// see nightwatch.conf.js
var devServer = browser.globals.devServerURL;
const devServer = browser.globals.devServerURL;
browser
.url(devServer)

View File

@@ -1,11 +1,11 @@
// TODO verify if it's needed, added because Vuex require Promise in the global scope
// TODO verify if it's needed, added because Axios require Promise in the global scope
// and babel-runtime doesn't affect external libraries
require('babel-polyfill');
// require all test files (files that ends with .spec.js)
var testsContext = require.context('./specs', true, /\.spec$/);
let testsContext = require.context('./specs', true, /\.spec$/);
testsContext.keys().forEach(testsContext);
// require all .vue and .js files except main.js for coverage.
var srcContext = require.context('../../../website/client', true, /^\.\/(?=(?!main(\.js)?$))(?=(.*\.(vue|js)$))/);
let srcContext = require.context('../../../website/client', true, /^\.\/(?=(?!main(\.js)?$))(?=(.*\.(vue|js)$))/);
srcContext.keys().forEach(srcContext);

View File

@@ -3,14 +3,15 @@
// we are also using it with karma-webpack
// https://github.com/webpack/karma-webpack
var path = require('path');
var merge = require('webpack-merge');
var baseConfig = require('../../../webpack/webpack.base.conf');
var utils = require('../../../webpack/utils');
var webpack = require('webpack');
var projectRoot = path.resolve(__dirname, '../../../');
const path = require('path');
const merge = require('webpack-merge');
const baseConfig = require('../../../webpack/webpack.base.conf');
const utils = require('../../../webpack/utils');
const webpack = require('webpack');
const projectRoot = path.resolve(__dirname, '../../../');
const testEnv = require('../../../webpack/config/test.env');
var webpackConfig = merge(baseConfig, {
const webpackConfig = merge(baseConfig, {
// use inline sourcemap for karma-sourcemap-loader
module: {
loaders: utils.styleLoaders(),
@@ -23,7 +24,7 @@ var webpackConfig = merge(baseConfig, {
},
plugins: [
new webpack.DefinePlugin({
'process.env': require('../../../webpack/config/test.env'),
'process.env': testEnv,
}),
],
});
@@ -43,7 +44,7 @@ webpackConfig.module.preLoaders.unshift({
});
// only apply babel for test files when using isparta
webpackConfig.module.loaders.some(function (loader, i) {
webpackConfig.module.loaders.some((loader) => {
if (loader.loader === 'babel') {
loader.include = path.resolve(projectRoot, 'test/client/unit');
return true;

View File

@@ -0,0 +1,19 @@
import i18n from 'client/plugins/i18n';
import commoni18n from 'common/script/i18n';
import Vue from 'vue';
describe('i18n plugin', () => {
before(() => {
i18n.install(Vue);
});
it('adds $t to Vue.prototype', () => {
expect(Vue.prototype.$t).to.be.a.function;
});
it('$t is a proxy for common/i18n.t', () => {
const result = (new Vue()).$t('reportBug');
expect(result).to.equal(commoni18n.t('reportBug'));
expect(result).to.equal('Report a Bug');
});
});

View File

@@ -11,7 +11,7 @@ describe('crit', () => {
});
it('computes', () => {
let result = crit(user);
let result = crit.crit(user);
expect(result).to.eql(1);
});
});

View File

@@ -147,6 +147,15 @@ describe('shared.ops.purchase', () => {
expect(user.stats.gp).to.equal(goldPoints - planGemLimits.convRate);
});
it('purchases gems with a different language than the default', () => {
let [, message] = purchase(user, {params: {type: 'gems', key: 'gem'}, language: 'de'});
expect(message).to.equal(i18n.t('plusOneGem', 'de'));
expect(user.balance).to.equal(userGemAmount + 0.5);
expect(user.purchased.plan.gemsBought).to.equal(2);
expect(user.stats.gp).to.equal(goldPoints - planGemLimits.convRate * 2);
});
it('purchases eggs', () => {
let type = 'eggs';
let key = 'Wolf';

View File

@@ -1,4 +1,5 @@
import scoreTask from '../../../website/common/script/ops/scoreTask';
import {
generateUser,
generateDaily,
@@ -11,6 +12,7 @@ import i18n from '../../../website/common/script/i18n';
import {
NotAuthorized,
} from '../../../website/common/script/libs/errors';
import crit from '../../../website/common/script/fns/crit';
let EPSILON = 0.0001; // negligible distance between datapoints
@@ -142,6 +144,32 @@ describe('shared.ops.scoreTask', () => {
expect(ref.beforeUser._id).to.eql(ref.afterUser._id);
});
it('critical hits', () => {
let normalUser = ref.beforeUser;
expect(normalUser.party.quest.progress.up).to.eql(0);
normalUser.party.quest.key = 'gryphon';
let critUser = ref.afterUser;
expect(critUser.party.quest.progress.up).to.eql(0);
critUser.party.quest.key = 'gryphon';
let normalTask = todo;
let critTask = freshTodo;
scoreTask({ user: normalUser, task: normalTask, direction: 'up', cron: false });
let normalTaskDelta = normalUser.party.quest.progress.up;
sandbox.stub(crit, 'crit').returns(1.5);
scoreTask({ user: critUser, task: critTask, direction: 'up', cron: false });
let critTaskDelta = critUser.party.quest.progress.up;
crit.crit.restore();
expect(critUser.stats.hp).to.eql(normalUser.stats.hp);
expect(critUser.stats.gp).to.be.greaterThan(normalUser.stats.gp);
expect(critUser.stats.mp).to.be.greaterThan(normalUser.stats.mp);
expect(critUser.stats.exp).to.be.greaterThan(normalUser.stats.exp);
expect(critTask.value).to.eql(normalTask.value);
expect(critTaskDelta).to.be.greaterThan(normalTaskDelta);
});
it('and increments quest progress', () => {
expect(ref.afterUser.party.quest.progress.up).to.eql(0);
ref.afterUser.party.quest.key = 'gryphon';

View File

@@ -1,35 +1,37 @@
/* global env:true, rm:true, mkdir:true, cp:true */
// https://github.com/shelljs/shelljs
require('shelljs/global');
env.NODE_ENV = 'production';
var path = require('path');
var config = require('./config');
var ora = require('ora');
var webpack = require('webpack');
var webpackConfig = require('./webpack.prod.conf');
const path = require('path');
const config = require('./config');
const ora = require('ora');
const webpack = require('webpack');
const webpackConfig = require('./webpack.prod.conf');
console.log(
console.log( // eslint-disable-line no-console
' Tip:\n' +
' Built files are meant to be served over an HTTP server.\n' +
' Opening index.html over file:// won\'t work.\n'
);
var spinner = ora('building for production...');
const spinner = ora('building for production...');
spinner.start();
var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory);
const assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory);
rm('-rf', assetsPath);
mkdir('-p', assetsPath);
cp('-R', config.build.staticAssetsDirectory, assetsPath);
webpack(webpackConfig, function (err, stats) {
webpack(webpackConfig, (err, stats) => {
spinner.stop();
if (err) throw err;
process.stdout.write(stats.toString({
process.stdout.write(`${stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false,
}) + '\n');
})}\n`);
});

View File

@@ -1,5 +1,5 @@
var merge = require('webpack-merge');
var prodEnv = require('./prod.env');
const merge = require('webpack-merge');
const prodEnv = require('./prod.env');
module.exports = merge(prodEnv, {
NODE_ENV: '"development"',

View File

@@ -1,15 +1,17 @@
// see http://vuejs-templates.github.io/webpack for documentation.
var path = require('path');
const path = require('path');
const staticAssetsDirectory = './website/static/.'; // The folder where static files (not processed) live
const prodEnv = require('./prod.env');
const devEnv = require('./dev.env');
var staticAssetsDirectory = './website/static/.'; // The folder where static files (not processed) live
module.exports = {
build: {
env: require('./prod.env'),
env: prodEnv,
index: path.resolve(__dirname, '../../dist-client/index.html'),
assetsRoot: path.resolve(__dirname, '../../dist-client'),
assetsSubDirectory: 'static',
assetsPublicPath: '/new-app',
staticAssetsDirectory: staticAssetsDirectory,
staticAssetsDirectory,
productionSourceMap: true,
// Gzip off by default as many popular static hosts such as
// Surge or Netlify already gzip all static assets for you.
@@ -19,11 +21,11 @@ module.exports = {
productionGzipExtensions: ['js', 'css'],
},
dev: {
env: require('./dev.env'),
env: devEnv,
port: 8080,
assetsSubDirectory: 'static',
assetsPublicPath: '/',
staticAssetsDirectory: staticAssetsDirectory,
staticAssetsDirectory,
proxyTable: {
// proxy all requests starting with /api/v3 to localhost:3000
'/api/v3': {

View File

@@ -1,5 +1,5 @@
var merge = require('webpack-merge');
var devEnv = require('./dev.env');
const merge = require('webpack-merge');
const devEnv = require('./dev.env');
module.exports = merge(devEnv, {
NODE_ENV: '"testing"',

View File

@@ -1,9 +1,10 @@
/* eslint-disable */
require('eventsource-polyfill')
var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true&overlay=false')
/* global window:true */
hotClient.subscribe(function (event) {
require('eventsource-polyfill');
const hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true&overlay=false');
hotClient.subscribe(event => {
if (event.action === 'reload') {
window.location.reload()
window.location.reload();
}
})
});

View File

@@ -1,22 +1,24 @@
var path = require('path');
var express = require('express');
var webpack = require('webpack');
var config = require('./config');
var proxyMiddleware = require('http-proxy-middleware');
var webpackConfig = process.env.NODE_ENV === 'testing'
? require('./webpack.prod.conf')
: require('./webpack.dev.conf');
/* eslint-disable no-process-env, no-console */
const path = require('path');
const express = require('express');
const webpack = require('webpack');
const config = require('./config');
const proxyMiddleware = require('http-proxy-middleware');
const webpackConfig = process.env.NODE_ENV === 'testing' ?
require('./webpack.prod.conf') :
require('./webpack.dev.conf');
// default port where dev server listens for incoming traffic
var port = process.env.PORT || config.dev.port;
const port = process.env.PORT || config.dev.port;
// Define HTTP proxies to your custom API backend
// https://github.com/chimurai/http-proxy-middleware
var proxyTable = config.dev.proxyTable;
const proxyTable = config.dev.proxyTable;
var app = express();
var compiler = webpack(webpackConfig);
const app = express();
const compiler = webpack(webpackConfig);
var devMiddleware = require('webpack-dev-middleware')(compiler, {
const devMiddleware = require('webpack-dev-middleware')(compiler, {
publicPath: webpackConfig.output.publicPath,
stats: {
colors: true,
@@ -24,18 +26,18 @@ var devMiddleware = require('webpack-dev-middleware')(compiler, {
},
});
var hotMiddleware = require('webpack-hot-middleware')(compiler);
const hotMiddleware = require('webpack-hot-middleware')(compiler);
// force page reload when html-webpack-plugin template changes
compiler.plugin('compilation', function (compilation) {
compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
compiler.plugin('compilation', (compilation) => {
compilation.plugin('html-webpack-plugin-after-emit', (data, cb) => {
hotMiddleware.publish({ action: 'reload' });
cb();
});
});
// proxy api requests
Object.keys(proxyTable).forEach(function (context) {
var options = proxyTable[context];
Object.keys(proxyTable).forEach((context) => {
let options = proxyTable[context];
if (typeof options === 'string') {
options = { target: options };
}
@@ -53,13 +55,13 @@ app.use(devMiddleware);
app.use(hotMiddleware);
// serve pure static assets
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory);
const staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory);
app.use(staticPath, express.static(config.dev.staticAssetsDirectory));
module.exports = app.listen(port, function (err) {
module.exports = app.listen(port, (err) => {
if (err) {
console.log(err);
return;
}
console.log('Listening at http://localhost:' + port + '\n');
console.log(`Listening at http://localhost:${port}\n`);
});

View File

@@ -1,28 +1,30 @@
var path = require('path');
var config = require('./config');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
/* eslint-disable no-process-env, no-console */
exports.assetsPath = function (_path) {
var assetsSubDirectory = process.env.NODE_ENV === 'production'
? config.build.assetsSubDirectory
: config.dev.assetsSubDirectory;
const path = require('path');
const config = require('./config');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
exports.assetsPath = (_path) => {
const assetsSubDirectory = process.env.NODE_ENV === 'production' ?
config.build.assetsSubDirectory :
config.dev.assetsSubDirectory;
return path.posix.join(assetsSubDirectory, _path);
};
exports.cssLoaders = function (options) {
exports.cssLoaders = (options) => {
options = options || {};
// generate loader string to be used with extract text plugin
function generateLoaders (loaders) {
var sourceLoader = loaders.map(function (loader) {
var extraParamChar;
let sourceLoader = loaders.map((loader) => {
let extraParamChar;
if (/\?/.test(loader)) {
loader = loader.replace(/\?/, '-loader?');
extraParamChar = '&';
} else {
loader = loader + '-loader';
loader = `${loader}-loader`;
extraParamChar = '?';
}
return loader + (options.sourceMap ? extraParamChar + 'sourceMap' : '');
return `${loader}${(options.sourceMap ? extraParamChar + 'sourceMap' : '')}`; // eslint-disable-line prefer-template
}).join('!');
if (options.extract) {
@@ -45,14 +47,14 @@ exports.cssLoaders = function (options) {
};
// Generate loaders for standalone style files (outside of .vue)
exports.styleLoaders = function (options) {
var output = [];
var loaders = exports.cssLoaders(options);
for (var extension in loaders) {
var loader = loaders[extension];
exports.styleLoaders = (options) => {
const output = [];
const loaders = exports.cssLoaders(options);
for (let extension in loaders) {
const loader = loaders[extension];
output.push({
test: new RegExp('\\.' + extension + '$'),
loader: loader,
test: new RegExp(`\\.${extension}$`),
loader,
});
}
return output;

View File

@@ -1,11 +1,15 @@
var path = require('path');
var config = require('./config');
var utils = require('./utils');
var projectRoot = path.resolve(__dirname, '../');
var webpack = require('webpack');
/* eslint-disable no-process-env, no-console */
var IS_PROD = process.env.NODE_ENV === 'production';
var baseConfig = {
const path = require('path');
const config = require('./config');
const utils = require('./utils');
const projectRoot = path.resolve(__dirname, '../');
const webpack = require('webpack');
const autoprefixer = require('autoprefixer');
const postcssEasyImport = require('postcss-easy-import');
const IS_PROD = process.env.NODE_ENV === 'production';
const baseConfig = {
entry: {
app: './website/client/main.js',
},
@@ -19,6 +23,8 @@ var baseConfig = {
fallback: [path.join(__dirname, '../node_modules')],
alias: {
jquery: 'jquery/src/jquery',
website: path.resolve(__dirname, '../website'),
common: path.resolve(__dirname, '../website/common'),
client: path.resolve(__dirname, '../website/client'),
assets: path.resolve(__dirname, '../website/client/assets'),
components: path.resolve(__dirname, '../website/client/components'),
@@ -84,10 +90,10 @@ var baseConfig = {
vue: {
loaders: utils.cssLoaders(),
postcss: [
require('autoprefixer')({
autoprefixer({
browsers: ['last 2 versions'],
}),
require('postcss-easy-import')({
postcssEasyImport({
glob: true,
}),
],
@@ -95,8 +101,10 @@ var baseConfig = {
};
if (!IS_PROD) {
const eslintFriendlyFormatter = require('eslint-friendly-formatter'); // eslint-disable-line global-require
baseConfig.eslint = {
formatter: require('eslint-friendly-formatter'),
formatter: eslintFriendlyFormatter,
emitWarning: true,
};
}

View File

@@ -1,12 +1,12 @@
var config = require('./config');
var webpack = require('webpack');
var merge = require('webpack-merge');
var utils = require('./utils');
var baseWebpackConfig = require('./webpack.base.conf');
var HtmlWebpackPlugin = require('html-webpack-plugin');
const config = require('./config');
const webpack = require('webpack');
const merge = require('webpack-merge');
const utils = require('./utils');
const baseWebpackConfig = require('./webpack.base.conf');
const HtmlWebpackPlugin = require('html-webpack-plugin');
// add hot-reload related code to entry chunks
Object.keys(baseWebpackConfig.entry).forEach(function (name) {
Object.keys(baseWebpackConfig.entry).forEach((name) => {
baseWebpackConfig.entry[name] = ['./webpack/dev-client'].concat(baseWebpackConfig.entry[name]);
});

View File

@@ -1,16 +1,18 @@
var path = require('path');
var config = require('./config');
var utils = require('./utils');
var webpack = require('webpack');
var merge = require('webpack-merge');
var baseWebpackConfig = require('./webpack.base.conf');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var env = process.env.NODE_ENV === 'testing'
? require('./config/test.env')
: config.build.env;
/* eslint-disable no-process-env, no-console */
var webpackConfig = merge(baseWebpackConfig, {
const path = require('path');
const config = require('./config');
const utils = require('./utils');
const webpack = require('webpack');
const merge = require('webpack-merge');
const baseWebpackConfig = require('./webpack.base.conf');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const env = process.env.NODE_ENV === 'testing' ?
require('./config/test.env') :
config.build.env;
const webpackConfig = merge(baseWebpackConfig, {
module: {
loaders: utils.styleLoaders({ sourceMap: config.build.productionSourceMap, extract: true }),
},
@@ -43,9 +45,9 @@ var webpackConfig = merge(baseWebpackConfig, {
// you can customize output by editing /index.html
// see https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: process.env.NODE_ENV === 'testing'
? 'index.html'
: config.build.index,
filename: process.env.NODE_ENV === 'testing' ?
'index.html' :
config.build.index,
template: './website/client/index.html',
inject: true,
minify: {
@@ -61,12 +63,12 @@ var webpackConfig = merge(baseWebpackConfig, {
// split vendor js into its own file
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function (module, count) {
minChunks (scriptModule) {
// any required modules inside node_modules are extracted to vendor
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
scriptModule.resource &&
/\.js$/.test(scriptModule.resource) &&
scriptModule.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
);
@@ -82,17 +84,13 @@ var webpackConfig = merge(baseWebpackConfig, {
});
if (config.build.productionGzip) {
var CompressionWebpackPlugin = require('compression-webpack-plugin');
const CompressionWebpackPlugin = require('compression-webpack-plugin'); // eslint-disable-line global-require
webpackConfig.plugins.push(
new CompressionWebpackPlugin({
asset: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp(
'\\.(' +
config.build.productionGzipExtensions.join('|') +
')$'
),
test: new RegExp(`\\.(${config.build.productionGzipExtensions.join('|')})$`),
threshold: 10240,
minRatio: 0.8,
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

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