Compare commits

...

153 Commits

Author SHA1 Message Date
Sabe Jones
f269d381da 3.97.2 2017-06-21 04:00:53 +00:00
Sabe Jones
3483f69559 fix(sprites): realign healer fins 2017-06-21 03:59:51 +00:00
Sabe Jones
8bbe9ac36e 3.97.1 2017-06-21 01:52:08 +00:00
Sabe Jones
75b93e6ec4 fix(sprites): unbleach Matt 2017-06-21 01:51:50 +00:00
Sabe Jones
f7b298d506 3.97.0 2017-06-21 01:25:54 +00:00
Sabe Jones
4bd2932955 chore(i18n): update locales 2017-06-20 22:18:38 +00:00
SabreCat
7060f5941d chore(sprites): compile 2017-06-20 22:10:04 +00:00
SabreCat
21379ee357 feat(event): Summer Splash 2017 2017-06-20 22:08:38 +00:00
Sabe Jones
cb46cd8eeb 3.96.2 2017-06-19 22:43:34 +00:00
Sabe Jones
d9d02ca81d chore(i18n): update locales 2017-06-19 22:43:18 +00:00
Sabe Jones
50c216eb41 chore(news): Bailey announcement 2017-06-19 22:38:06 +00:00
SabreCat
6bdf8fdabc 3.96.1 2017-06-17 20:19:35 +00:00
SabreCat
3db304b6bf fix(potions): disable Florals
Also add sprite for Bars of Soap in Attack of the Mundane 1
2017-06-17 20:18:36 +00:00
Sabe Jones
ea85a8a3a8 3.96.0 2017-06-16 20:42:15 +00:00
Sabe Jones
e811a2700e feat(migration): update achievements (#8825) 2017-06-16 13:41:25 -07:00
Sabe Jones
83b573dcfc chore(i18n): update locales 2017-06-16 17:42:28 +00:00
Matteo Pagliazzi
c4fa9426b3 Client Tasks v1 / Bootstrap configurable (#8822)
* make bs4 configurable, change gutters to match zeplin\s

* correctly customize gutters
2017-06-16 18:58:34 +02:00
Matteo Pagliazzi
042e5a8d63 Client Fixes (#8821)
* new client: fix animation flickering

* fix transitions

* update copy
2017-06-16 18:07:24 +02:00
Keith Holliday
f7ce269f3c Add false return when repeats are empty (#8777)
* Add false return when repeats are empty

* Added front end check for repeats on monthly-daysOfWeek

* Fixed tests with static date
2017-06-14 11:27:50 -07:00
Sabe Jones
c084f8a2b9 Merge branch 'release' into develop 2017-06-13 21:25:41 +00:00
Sabe Jones
c1c42e17b8 3.95.0 2017-06-13 21:21:55 +00:00
Sabe Jones
306a782e7a chore(i18n): update locales 2017-06-13 21:20:48 +00:00
SabreCat
20178c0722 chore(sprites): compile 2017-06-13 21:10:02 +00:00
SabreCat
256e2e809c feat(content): Nudibranch Pets 2017-06-13 21:09:13 +00:00
Matteo Pagliazzi
592345e22c Party members in header v2 (#8815)
* update comemnt

* flyout on hover

* fix hasClass and isBuffed

* polish members in party header
2017-06-13 20:55:45 +02:00
Matteo Pagliazzi
292b2acb1e client: fix production path for chunks 2017-06-08 19:17:35 -07:00
Matteo Pagliazzi
977f9d5174 Client: party members in header (#8804)
* wip party members in header

* wip

* add inbox routes back

* polishing
2017-06-08 18:24:40 -07:00
Matteo Pagliazzi
85644fdc1b client: user -list-detail -> member-details 2017-06-08 14:36:56 -07:00
Matteo Pagliazzi
138b5c4bdb wip client/header-party-members (#8803) 2017-06-08 14:33:23 -07:00
Keith Holliday
52edb8a8da New client members (#8795)
* Began styling member modal

* Added store and updated modal styles

* Began converting angular

* Ported over angular routes

* Fixed lint issues
2017-06-08 14:27:22 -07:00
Keith Holliday
1999e1098e Allow guilds edit (#8800)
* test: test that admin users can update guilds

* test: test admin removeMember privileges

* fix: allow admins to edit guilds

* fix: add edit guild options for admins

* test: test that admin can't remove current leader

* Add error msg for removing current leader

* Taskwoods Quest Line (#8156)

* feat(content): Gold Quest 2016-10

* chore(news): Bailey

* chore(i18n): update locales

* chore(sprites): compile

* 3.49.0

* chore: update express

* Fix for the ReDOS vulnerability

habitica is currently affected by the high-severity [ReDOS vulnerability](https://snyk.io/vuln/npm:tough-cookie:20160722). 

Vulnerable module: `tough-cookie`
Introduced through: ` request`

This PR fixes the ReDOS vulnerability by upgrading ` request` to version 2.74.0

Check out the [Snyk test report](https://snyk.io/test/github/HabitRPG/habitica) to review other vulnerabilities that affect this repo. 

[Watch the repo](https://snyk.io/add) to 
* get alerts if newly disclosed vulnerabilities affect this repo in the future. 
* generate pull requests with the fixes you want, or let us do the work: when a newly disclosed vulnerability affects you, we'll submit a fix to you right away. 

Stay secure, 
The Snyk team

* Documentation - coupon

closes #8109

* fix(client): Allow member hp to be clickable

fixes #8016
closes #8155

* chore(npm): shrinkwrap

* test: test isAbleToEditGroup

* Add isAbleToEditGroup to groupsCtrl

* Remove unnecessary ternary

* Fix linting

* Move edit permission logic out to groupsCtrl

* fix: change ternary to boolean

* Fix linting

* Fixed merge issues
2017-06-08 13:45:24 -07:00
Keith Holliday
4d3a0c0571 Fixed issue with repeat settings turning false (#8773) 2017-06-08 12:18:49 -07:00
Matteo Pagliazzi
706de95458 Client: Header & Menu & Icons (#8770)
* header revamp - wip

* fix webpack fonts

* wip icons

* fix compilation errors

* implement icons loading without iconmoo

* new svg implementation

* wip

* fix issues with svgs

* fix issues with svgs

* fix bits svg

* fix displaying of pet in avatar

* avatar class icon

* no party header

* update navigation

* split code by route

* round gems and gp

* add string for faqs

* fix icons in css
2017-06-08 12:04:19 -07:00
Vince Campanale
e3c1eaa9d2 Preventing cardRead from notifying user. (#8473)
* added condition to prevent readCard operations from sending a notification

* created constant array to contain opNames for notifications we want to suppress and adjusted condition to accordingly

* replaced const with var to past karma test
2017-06-08 11:59:37 -07:00
Keith Holliday
17c0f795cc Began styling member modal 2017-06-08 11:03:06 -07:00
Sabe Jones
cb5ac9014e Include apidoc in test script (#8797)
* test(docs): include apidoc in script

* fix(test): also run apidoc on Travis
2017-06-08 10:40:10 -07:00
joe-salomon
0be681b7a2 Dailies performance fix - fixes #8756 (#8767)
* Changed recurring logic to not use moment-recur plugin for performance reasons

* change only nextDue calculations
add tests to make sure proper nextDue values are calculated
revert schedule.matches logic to original
revert shouldDo.test.js to original

* fix monthly nextDue logic
move tests to shouldDo.test.js

* typos

* revert to original logic. change not needed

* add failure cases
2017-06-08 10:34:05 -07:00
Sabe Jones
5360f9e587 Align doubled achievement popovers (#8798)
* bug(profile): align both achievement popups (hover vs. click)

* refactor(style): move to CSS/Stylus
2017-06-07 21:05:24 -07:00
Keith Holliday
4553a411f6 Paypal ipn options (#8713)
* Added more acceptable ipn cancelation options

* Fixed lint issue

* Fixed spelling issue
2017-06-07 10:31:44 -07:00
Alys
613f51b08d use new email template when joining a group plan for customisation of subscription cancellation information (#8637)
* use new email template when subscription is cancelled from joining a group plan

* use new email template when subscription is cancelled from joining a group plan - needs more code, tests

* change from sending new email as a cancel-subscription option to sending as a group plan join email

Uses a new group-member-join email template instead of old group-member-joining because new template includes mandril conditional merge tags.

Also adds tests and comments. Edits some comments for accuracy and typo fixes.

* adapt group-member-join email template for manual cancel message for iOS and Android subscriptions

* save test user so its profile name can be read by calls to sendTxn

* add documentation for the user model cancelSubscription function

* add constants for strings passed to mandrill email templates
2017-06-07 10:25:37 -07:00
joe-salomon
2292ba2694 Fix subscriptions ending early - fixes #8600 (#8746)
* Use “now” for calculation of the subscription end date instead of plan.dateUpdated

* add test to show previously incorrect logic does not affect sub end date.
2017-06-07 10:16:55 -07:00
joe-salomon
befacca457 Keep existing Mystery Items and Hourglasses when adding to group - fixes 8643 (#8745)
* Modified addSubToGroupUser to save existing mysteryItems and trinkets from an expired subscription
Added unit test

* fix eslint error
2017-06-07 09:59:09 -07:00
Airu
5cd30b430d Added Arashi's theme as a new audio theme (#8707)
* Add existing file

* Update menu.jade
2017-06-07 09:53:11 -07:00
Kevin Smith
c5d9ee1e0a Implemented new Achievement and Badge: Joined a Challenge (Fixes #8613) (#8761)
* Added image

* Added new achievement to user schema

* Added new achievement to content

* Added new achievement to libs

* Added achievement text to locale

* Added achievement to notification model and controller

* Grant achievement on joining or creating first challenge

* Added achievement to modal template

* Compiled new sprites

* Added integration tests

* Fix linting error
2017-06-07 09:43:16 -07:00
Sabe Jones
234328f2ba Reduce difficulty of collection quests (#8754)
* create script to insert message into party chat because collection quest is now easier

See https://github.com/HabitRPG/habitrpg/pull/7987 for more details.

* fix(quests): make collection less burdensome

* refactor(migration): return groups directly
2017-06-06 20:14:26 -07:00
SabreCat
029afa197e fix(achievements): move year-round cards out of seasonal 2017-06-07 02:41:13 +00:00
SabreCat
05b35c5147 fix(manifest): remove deleted files from manifest 2017-06-07 02:11:47 +00:00
MathWhiz
c9427ad34c New cards — Congratulations, Get Well (#8655)
* Add card and achievement sprite for Congrats card

* Add data regarding Congrats card

* Add Get Well card

* Add Get Well images

* Add schema

* Remove `if (!target.flags) target.flags = {};` code from cards

* Remove white backgrounds for congrats sprites

* add inital tests for cards

* Fix card tests

* Fix invalid urls in tests

* Update POST-user_class_cast_spellId.test.js

* Update POST-user_class_cast_spellId.test.js

* Update POST-user_class_cast_spellId.test.js

* Update congrats card sprite

* Fix card logic

* Fix user schema

* Change achievement values for new cards to Number

* Resize congrats and getwell cards

This will make them be sized properly

* Separate Market from Drops

* Extract cards to new section

* fix(sprites): revert spritesheet changes

* Add flags if target does not have them
2017-06-06 19:04:54 -07:00
madpink
d6c62262f1 Updating User API Doc (part 3) (#8720)
* Updating User API Doc (part 3)

* Updating User API Doc (part 3)

Fixed trailing spaces

* Updating User API Doc (part 3)

Made changes to @apiParamExample to make multi-line (which may have been cause of apiDoc failing)

* Updated quests to add questKey
2017-06-06 18:57:17 -07:00
MathWhiz
ec1d378504 Flagged chat messages are visible to the users that posted them (#8726)
* Allow users to see their chat messages that are hidden to others

* Fix lint

* Fix failing api test

* Add test
2017-06-06 18:55:12 -07:00
beatscribe
3bb88f450a New Beatscribe 8-bit sound theme (#8727) 2017-06-06 18:53:10 -07:00
Rick Kasten
97a38e68c5 Clean up references to repo as HabitRPG/habitrpg (#8742)
* Confirmed changes

* Removed bad link
This was apparantly missed in #8051

* Confirmed changes

* Fixed links to milestones
2017-06-06 18:51:54 -07:00
Alys
be948a1bf2 adjust postinstall command so that it works in Windows as well as *nix (#8744)
Semicolons in postinstall commands don't work in Windows.

'&&' works in *nix and in at least some versions of Windows.

This changes the meaning of the postinstall line slightly because
now the later commands won't run if the earlier ones failed but I
don't see that being a problem.
2017-06-06 18:49:31 -07:00
Sabe Jones
018976a723 Disallow interactions by blocked users; new "get objections" Members API route (#8755)
* Make flags.chatRevoked prevent sending private messages (issue #7971)

* Disallow sending gems when messages aren't allowed.

* Created function to check for objections to an interaction to user model and wired it into the API (issue #7971)

* Fixes for issues raised by reviewers.

* Added allowed values to apidoc for api.getObjectionsToInteraction.

* Refactoring of getObjectionsToInteraction and minor API changes.

* fix(objections): address PR comments

* fix(strings): use US English for base edits

* refactor(test): typos and phrasing
2017-06-06 18:49:05 -07:00
Grayson Gilmore
00e5896ac6 Add test for GET /shops/backgrounds (#8771) 2017-06-06 18:45:41 -07:00
Kevin Smith
36bc693545 Turtleheads (Fixes #8560) (#8776)
* Added new turtle head icons

* Recompiled spritesheets
2017-06-06 18:43:19 -07:00
Atte Kortesmaa
f27706cb4b Improved API documentation for hall #8087 (#8536)
* Improved API documentation for hall

* Fixes typos, removes apiHeader definitions and curl example

* Fixes @apiParam and capitalization errors. Moves @apiDefines to website/server/api-doc.js
2017-06-06 11:48:11 -07:00
MathWhiz
f6f99ec57e Require Dailies to have a Start Date (#8649)
* Require Dailies to have a Start Date

* Add preliminary test

* Fix lint errors
2017-06-06 10:05:17 -07:00
MathWhiz
c852d9d581 Add new favicon (#8732)
* Add new favicon

* Update 192x192 favicon image
2017-06-05 22:55:44 -07:00
Sabe Jones
4a78514308 3.94.1 2017-06-04 23:41:14 +00:00
Sabe Jones
265b48752d Merge branch 'develop' into release 2017-06-04 23:40:26 +00:00
Sabe Jones
db0b0d6b6d fix(migrations): return to generic state 2017-06-04 02:59:49 +00:00
Sabe Jones
20f1087552 fix(migrations): return to generic state 2017-06-04 02:58:42 +00:00
Keith Holliday
2e9bc2c31c New client guilds (#8736)
* add colors palette

* add secondary menu component and style it

* add box shadow to secondary menu

* misc css, fixes for secondary menu

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

* add typography

* more equipment

* stable: fix linting

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

* remove duplicate google fonts loading

* add dropdowns

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

* start adding drawer and selected indicator

* wip equipment

* fix equipment

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

* equipment: fix vue binding. version 2

* equipment: fix vue binding. version 3

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

* checkboxes and radio buttons

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

* add search

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

* add toggle switch, rename css

* correct offset

* upgrade deps

* upgrade deps

* drawer and lot of other work

* update equipping mechanism

* finish equipment

* fix compilation and upgrade deps

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

* v-show -> v-if

* Start of guild syyles

* fix linting in test/client

* fix es6 compilation in test/client

* fix babel compilation for tests

* fix groupsUtilities mixin tests

* More designs

* Added public guild state

* Added my guilds store

* client: buttons

* client: buttons: fix colors

* Added join and leave

* Began adding new guild form

* Create form updates

* Added search to local data

* Added filtering

* Added initial code for group create

* Added more create checks

* Added more guild routes

* Added styles to guild page

* Added more chat styles

* Began porting over angular functions

* Moved over group service functions

* Added paging

* Updated sidebar

* Updated join/leave and minor text

* Added new sidebar functions

* Updated paging

* Added some form updates

* Added more translations and styles

* Updated shrinkwrap

* Removed features config

* Lint cleanup

* Added member modal

* Added more member actions

* Updated nav

* Fixed filter toggling

* Updated create guild

* Added no guild page

* Added sort select

* Added more styles

* Added update guild form

* Removed extra css and other minor changes

* Many css and syntax fixes

* Fixed color and merge conflic

* Removed paging from my guilds

* Removed extra strings

* Many requests updates

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

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

* fix(feedback): address PR comments

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

* Removed only

* Changed numeric check to lodash isFinite

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

* Added every x to weekly

* Updated new recur logic to work with tests

* Added repeatable tests back

* Added custom day start support

* Moved back to zone function

* Added zone back

* Added nextDue field

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

* Removed extra codes

* Removed clone deep

* Added summary local

* Fixed every x weekly

* Prevented edit of repeats on

* Added next due date

* Fixed display of next due dates

* Fixed broken tests

* added next due date as today for weekly

* Fixed integration tests

* Updated common test

* Use user's format

* Allow user to deselect all days during week

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

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

Fixes #5490

* Remove expandMenu and closeMenu directives and tests

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

Changes Body to Query, changed example from  POST body

* Updated to remove trailing space

* Wording fix per Lady Alys

* Update user.js

Kicking off another test.

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

* i18n changes and fixes

* initial displaying of eggs, food and potions + sorting

* add missing files

* remove comment

* show food, eggs and potions

* add label to dropdowns acting as select menus

* popovers

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

* fix quantity ordering

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

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

* extract item-content as slot

* scoped context to pass the item

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

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

* fix(style): linting error

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

* fix(tests): update AB expectations

* fix(modal): remove extra includes

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

* fix(style): linting error from conflict
2017-05-19 14:45:11 -05:00
Keith Holliday
99a2013767 Fixed display of next due dates 2017-05-18 14:12:36 -06:00
Keith Holliday
a5e0e171cc Added next due date 2017-05-18 10:42:11 -06:00
Keith Holliday
1d93943458 Prevented edit of repeats on 2017-05-18 10:26:57 -06:00
Keith Holliday
7f2719a75c Fixed every x weekly 2017-05-18 10:11:29 -06:00
Sabe Jones
8a9ed04f5e Merge branch 'release' into develop 2017-05-18 02:13:14 +00:00
Sabe Jones
e6f605f23a Discount Bundled Quests (#8731)
* refactor(content): split quests file

* feat(purchases): sell bundled quests

* fix(style): address linting errors

* test(bundles): shop and purchase tests

* fix(test): remove only

* test(bundles): check balance deduction

* docs(content): comment bundle structure

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

* add secondary menu component and style it

* add box shadow to secondary menu

* misc css, fixes for secondary menu

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

* add typography

* more equipment

* stable: fix linting

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

* remove duplicate google fonts loading

* add dropdowns

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

* start adding drawer and selected indicator

* wip equipment

* fix equipment

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

* equipment: fix vue binding. version 2

* equipment: fix vue binding. version 3

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

* checkboxes and radio buttons

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

* add search

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

* add toggle switch, rename css

* correct offset

* upgrade deps

* upgrade deps

* drawer and lot of other work

* update equipping mechanism

* finish equipment

* fix compilation and upgrade deps

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

* v-show -> v-if

* fix linting in test/client

* fix es6 compilation in test/client

* fix babel compilation for tests

* fix groupsUtilities mixin tests

* client: buttons

* client: buttons: fix colors

* client: finish buttons and dropdowns

* upgrade bootstrap-vue, finish buttons and dropdowns

* fix tasks page layout

* misc fixes for buttons

* add textareas

* fix app menu

* add inputs

* fixes for toggleSwitch

* typography

* checkboxes and radio buttons

* add checkbox icon

* fix equip.js

* extract strings to newClient.json

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

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

* check errorMessage for null

* hide star if costume not enabled

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

* show minimize / expand icon - always centered by css

* drawer test

* drawer: fix centering on large screens

* fix show more button

* add margin when two dropdowns are next to each other

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

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

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

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

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

* correct typo in questVice3Completion: breath > breathe

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

* Added isDue update on update task

* Add isdue calc to score task

* Added isdue calc to cron

* Fixed lint issue

* Added isDue to no set and updated grammar
2017-05-10 07:40:45 -06:00
SabreCat
638c9dee89 Merge branch 'release' into develop 2017-05-09 19:36:42 +00:00
yugensoft
2adac35e31 Continuation of PR #8675, fix internationalization (#8698)
* Made uneditable habits show counter reset frequency
Added counter reset frequency to counter tooltip
Solves issue https://github.com/HabitRPG/habitica/issues/8571

* Internationalization fix, pursuant to https://github.com/HabitRPG/habitica/pull/8675#discussion_r112982947
2017-05-09 13:22:09 -05:00
SabreCat
15d4f7d6ab Revert "Add tests for shouldDo - fixes #8585 (#8660)"
This reverts commit 4bbebdd237. The change to moment.utcOffset() from moment.zone() broke cron code for determining today's date.
2017-05-08 22:08:24 +00:00
Keith Holliday
76222ac344 Added custom day start support 2017-05-08 10:20:47 -06:00
Keith Holliday
2659a4117b Added repeatable tests back 2017-05-08 09:52:15 -06:00
Keith Holliday
a0ee73e944 Updated new recur logic to work with tests 2017-05-08 09:39:50 -06:00
Keith Holliday
6174624b89 Merged develop 2017-05-08 08:32:03 -06:00
Keith Holliday
c8b6e8ea7c Updated docker file to use Node (#8704) 2017-05-08 07:40:55 -06:00
madpink
409b5d5965 Updating User API Doc (part 2) (#8602)
* Updating APIDOC for issue 8087

* Updating User API Doc (round 2)

cleaned up trailing sapces

* Updating User API Doc (round 2)

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

* Fix previously raised issues

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

* Created bulkScore using score callback

* Removed unnecessary code

* Added verification to run the code only for Approved Tasks

* Created scoreTasks on server and necessary code on client

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

This reverts commit b786c0e71a.

* Fixed gold/xp earn-lose-earnAgain problem

* Do not read already read notifications

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

* Fix #8585 - resolve feedback
2017-05-08 07:32:38 -06:00
TheHollidayInn
0fd85c0d60 Added every x to weekly 2017-03-27 09:52:32 -06:00
Keith Holliday
8c68f450c6 Enabled repeatables 2017-03-15 14:13:15 -06:00
1024 changed files with 56366 additions and 44725 deletions

View File

@@ -1,6 +1,6 @@
# Reporting Bugs
[Please see these instructions for reporting bugs](https://github.com/HabitRPG/habitrpg/issues/2760)
[Please see these instructions for reporting bugs](https://github.com/HabitRPG/habitica/issues/2760)
# Pull Request

View File

@@ -4,7 +4,7 @@
[//]: # (If you have a feature request, use "Help > Request a Feature", not GitHub or the Report a Bug guild.)
[//]: # (For more guidelines see https://github.com/HabitRPG/habitrpg/issues/2760)
[//]: # (For more guidelines see https://github.com/HabitRPG/habitica/issues/2760)
[//]: # (Fill out relevant information - UUID is found in Settings -> API)
### General Info

View File

@@ -12,6 +12,8 @@ before_install:
- $CXX --version
- npm install -g npm@4
- if [ $REQUIRES_SERVER ]; then sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10; echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list; sudo apt-get update; sudo apt-get install mongodb-org-server; fi
install:
- npm install &> npm.install.log || (cat npm.install.log; false)
before_script:
- npm run test:build
- cp config.json.example config.json
@@ -31,3 +33,4 @@ env:
- TEST="test:common" COVERAGE=true
- TEST="test:karma" COVERAGE=true
- TEST="client:unit" COVERAGE=true
- TEST="apidoc"

View File

@@ -1,43 +1,16 @@
FROM ubuntu:trusty
MAINTAINER Sabe Jones <sabe@habitica.com>
# Avoid ERROR: invoke-rc.d: policy-rc.d denied execution of start.
RUN echo -e '#!/bin/sh\nexit 0' > /usr/sbin/policy-rc.d
# Install prerequisites
RUN apt-get update
RUN apt-get install -y \
build-essential \
curl \
git \
libfontconfig1 \
libfreetype6 \
libkrb5-dev \
python
# Install NodeJS
RUN curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
RUN apt-get install -y nodejs
# Install npm@latest
RUN curl -sL https://www.npmjs.org/install.sh | sh
# Clean up package management
RUN apt-get clean
RUN rm -rf /var/lib/apt/lists/*
FROM node:boron
# Install global packages
RUN npm install -g gulp grunt-cli bower mocha
# Clone Habitica repo and install dependencies
WORKDIR /habitrpg
RUN git clone https://github.com/HabitRPG/habitica.git /habitrpg
RUN mkdir -p /usr/src/habitrpg
WORKDIR /usr/src/habitrpg
RUN git clone https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
RUN npm install
RUN bower install --allow-root
# Create environment config file and build directory
RUN cp config.json.example config.json
# Create Build dir
RUN mkdir -p ./website/build
# Start Habitica

View File

@@ -5,8 +5,7 @@ Habitica [![Build Status](https://travis-ci.org/HabitRPG/habitica.svg?branch=dev
We need more programmers! Your assistance will be greatly appreciated.
For an introduction to the technologies used and how the software is organized, refer to [Contributing to Habitica](http://habitica.wikia.com/wiki/Contributing_to_Habitica#Coders_.28Web_.26_Mobile.29) - "Coders (Web & Mobile)" section.
For an introduction to the technologies used and how the software is organized, refer to [Guidance for Blacksmiths](http://habitica.wikia.com/wiki/Guidance_for_Blacksmiths).
To set up a local install of Habitica for development and testing, see [Setting up Habitica Locally](http://habitica.wikia.com/wiki/Setting_up_Habitica_Locally), which contains instructions for Windows, *nix / Mac OS, and Vagrant.
To set up a local install of Habitica for development and testing on various platforms, see [Setting up Habitica Locally](http://habitica.wikia.com/wiki/Setting_up_Habitica_Locally).
Then read [Guidance for Blacksmiths](http://habitica.wikia.com/wiki/Guidance_for_Blacksmiths) for additional instructions and useful tips.

View File

@@ -3,7 +3,7 @@ var authorUuid = 'd904bd62-da08-416b-a816-ba797c9ee265'; //... own data is done
/**
* database_reports/count_users_who_own_specified_gear.js
* https://github.com/HabitRPG/habitrpg/pull/3884
* https://github.com/HabitRPG/habitica/pull/3884
*/
var thingsOfInterest = {

36
gulp/gulp-bootstrap.js Normal file
View File

@@ -0,0 +1,36 @@
import gulp from 'gulp';
import fs from 'fs';
// Copy Bootstrap 4 config variables from /website /node_modules so we can check
// them into Git
const BOOSTRAP_NEW_CONFIG_PATH = 'website/client/assets/scss/bootstrap_config.scss';
const BOOTSTRAP_ORIGINAL_CONFIG_PATH = 'node_modules/bootstrap/scss/_custom.scss';
// https://stackoverflow.com/a/14387791/969528
function copyFile(source, target, cb) {
let cbCalled = false;
function done(err) {
if (!cbCalled) {
cb(err);
cbCalled = true;
}
}
let rd = fs.createReadStream(source);
rd.on('error', done);
let wr = fs.createWriteStream(target);
wr.on('error', done);
wr.on('close', () => done());
rd.pipe(wr);
}
gulp.task('bootstrap', (done) => {
// use new config
copyFile(
BOOSTRAP_NEW_CONFIG_PATH,
BOOTSTRAP_ORIGINAL_CONFIG_PATH,
done,
);
});

View File

@@ -49,7 +49,7 @@ gulp.task('sprites:checkCompiledDimensions', ['sprites:main', 'sprites:largeSpri
});
if (numberOfSheetsThatAreTooBig > 0) {
console.error(`${numberOfSheetsThatAreTooBig} sheets might too big for mobile Safari to be able to handle them, but there is a margin of error in these calculations so it is probably okay. Mention this to an admin so they can test a staging site on mobile Safari after your PR is merged.`); // https://github.com/HabitRPG/habitrpg/pull/6683#issuecomment-185462180
console.error(`${numberOfSheetsThatAreTooBig} sheets might too big for mobile Safari to be able to handle them, but there is a margin of error in these calculations so it is probably okay. Mention this to an admin so they can test a staging site on mobile Safari after your PR is merged.`); // https://github.com/HabitRPG/habitica/pull/6683#issuecomment-185462180
} else {
console.log('All images are within the correct dimensions');
}

View File

@@ -0,0 +1,110 @@
'use strict';
/****************************************
* Author: @Alys
*
* Reason: Collection quests are being changed
* to require fewer items collected:
* https://github.com/HabitRPG/habitrpg/pull/7987
* This will cause existing quests to end sooner
* than the party is expecting.
* This script inserts an explanatory `system`
* message into the chat for affected parties.
***************************************/
global.Promise = require('bluebird');
const uuid = require('uuid');
const TaskQueue = require('cwait').TaskQueue;
const logger = require('./utils/logger');
const Timer = require('./utils/timer');
const connectToDb = require('./utils/connect').connectToDb;
const closeDb = require('./utils/connect').closeDb;
const message = '`This party\'s collection quest has been made easier! For details, refer to http://habitica.wikia.com/wiki/User_blog:LadyAlys/Collection_Quests_are_Now_Easier`';
const timer = new Timer();
// PROD: Enable prod db
// const DB_URI = 'mongodb://username:password@dsXXXXXX-a0.mlab.com:XXXXX,dsXXXXXX-a1.mlab.com:XXXXX/habitica?replicaSet=rs-dsXXXXXX';
const DB_URI = 'mongodb://localhost/habitrpg';
const COLLECTION_QUESTS = [
'vice2',
'egg',
'moonstone1',
'goldenknight1',
'dilatoryDistress1',
];
let Groups;
connectToDb(DB_URI).then((db) => {
Groups = db.collection('groups');
return Promise.resolve();
})
.then(findPartiesWithCollectionQuest)
// .then(displayGroups) // for testing only
.then(addMessageToGroups)
.then(() => {
timer.stop();
closeDb();
}).catch(reportError);
function reportError (err) {
logger.error('Uh oh, an error occurred');
closeDb();
timer.stop();
throw err;
}
function findPartiesWithCollectionQuest () {
logger.info('Looking up groups on collection quests...');
return Groups.find({'quest.key': {$in: COLLECTION_QUESTS}}, ['name','quest']).toArray().then((groups) => {
logger.success('Found', groups.length, 'parties on collection quests');
return Promise.resolve(groups);
})
}
function displayGroups (groups) { // for testing only
logger.info('Displaying parties...');
console.log(groups);
return Promise.resolve(groups);
}
function updateGroupById (group) {
var newMessage = {
'id' : uuid.v4(),
'text' : message,
'timestamp': Date.now(),
'likes': {},
'flags': {},
'flagCount': 0,
'uuid': 'system'
};
return Groups.findOneAndUpdate({_id: group._id}, {$push:{"chat" :{$each: [newMessage], $position:0}}}, {returnOriginal: false});
// Does not set the newMessage flag for all party members because I don't think it's essential and
// I don't want to run the extra code (extra database load, extra opportunity for bugs).
}
function addMessageToGroups (groups) {
let queue = new TaskQueue(Promise, 300);
logger.info('About to update', groups.length, 'parties...');
return Promise.map(groups, queue.wrap(updateGroupById)).then((result) => {
let updates = result.filter(res => res.lastErrorObject && res.lastErrorObject.updatedExisting)
let failures = result.filter(res => !(res.lastErrorObject && res.lastErrorObject.updatedExisting));
logger.success(updates.length, 'parties have been notified');
if (failures.length > 0) {
logger.error(failures.length, 'parties could not be notified');
}
return Promise.resolve();
});
}

View File

@@ -0,0 +1,114 @@
var migrationName = '20170616_achievements';
var authorName = 'Sabe'; // in case script author needs to know when their ...
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
/*
* Updates to achievements for June 16, 2017 biweekly merge
* 1. Multiply various collection quest achievements based on difficulty reduction
* 2. Award Joined Challenge achievement to those who should have it already
*/
import monk from 'monk';
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
var dbUsers = monk(connectionString).get('users', { castIds: false });
function processUsers(lastId) {
// specify a query to limit the affected users (empty for all users):
var query = {
$or: [
{'achievements.quests.dilatoryDistress1': {$gt:0}},
{'achievements.quests.egg': {$gt:0}},
{'achievements.quests.goldenknight1': {$gt:0}},
{'achievements.quests.moonstone1': {$gt:0}},
{'achievements.quests.vice2': {$gt:0}},
{'achievements.challenges': {$exists: true, $ne: []}},
{'challenges': {$exists: true, $ne: []}},
],
};
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):
'achievements',
'challenges',
],
})
.then(updateUsers)
.catch(function (err) {
console.log(err);
return exiting(1, 'ERROR! ' + err);
});
}
var progressCount = 1000;
var count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
displayData();
return;
}
var userPromises = users.map(updateUser);
var lastUser = users[users.length - 1];
return Promise.all(userPromises)
.then(function () {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
var set = {'migration': migrationName};
if (user.challenges.length > 0 || user.achievements.challenges.length > 0) {
set['achievements.joinedChallenge'] = true;
}
if (user.achievements.quests.dilatoryDistress1) {
set['achievements.quests.dilatoryDistress1'] = Math.ceil(user.achievements.quests.dilatoryDistress1 * 1.25);
}
if (user.achievements.quests.egg) {
set['achievements.quests.egg'] = Math.ceil(user.achievements.quests.egg * 2.5);
}
if (user.achievements.quests.goldenknight1) {
set['achievements.quests.goldenknight1'] = user.achievements.quests.goldenknight1 * 5;
}
if (user.achievements.quests.moonstone1) {
set['achievements.quests.moonstone1'] = user.achievements.quests.moonstone1 * 5;
}
if (user.achievements.quests.vice2) {
set['achievements.quests.vice2'] = Math.ceil(user.achievements.quests.vice2 * 1.5);
}
dbUsers.update({_id: user._id}, {$set:set});
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
if (user._id == authorUuid) console.warn(authorName + ' processed');
}
function displayData() {
console.warn('\n' + count + ' users processed\n');
return exiting(0);
}
function exiting(code, msg) {
code = code || 0; // 0 = success
if (code && !msg) { msg = 'ERROR!'; }
if (msg) {
if (code) { console.error(msg); }
else { console.log( msg); }
}
process.exit(code);
}
module.exports = processUsers;

View File

@@ -2,7 +2,7 @@ var _id = '';
var update = {
$addToSet: {
'purchased.plan.mysteryItems':{
$each:['back_mystery_201704','armor_mystery_201704']
$each:['body_mystery_201705','head_mystery_201705']
}
}
};

2421
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.91.0",
"version": "3.97.2",
"main": "./website/server/index.js",
"dependencies": {
"@slack/client": "^3.8.1",
@@ -13,10 +13,12 @@
"async": "^1.5.0",
"autoprefixer": "^6.4.0",
"aws-sdk": "^2.0.25",
"axios": "^0.15.3",
"axios": "^0.16.0",
"babel-core": "^6.0.0",
"babel-eslint": "^7.2.3",
"babel-loader": "^6.0.0",
"babel-plugin-syntax-async-functions": "^6.13.0",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-plugin-transform-async-to-module-method": "^6.8.0",
"babel-plugin-transform-object-rest-spread": "^6.16.0",
"babel-plugin-transform-regenerator": "^6.16.1",
@@ -29,13 +31,14 @@
"bluebird": "^3.3.5",
"body-parser": "^1.15.0",
"bootstrap": "^4.0.0-alpha.6",
"bootstrap-vue": "^0.15.8",
"bower": "~1.3.12",
"browserify": "~12.0.1",
"compression": "^1.6.1",
"connect-ratelimit": "0.0.7",
"cookie-session": "^1.2.0",
"coupon-code": "^0.4.5",
"css-loader": "^0.26.1",
"css-loader": "^0.28.0",
"csv-stringify": "^1.0.2",
"cwait": "^1.0.0",
"domain-middleware": "~0.1.0",
@@ -96,7 +99,7 @@
"postcss-easy-import": "^2.0.0",
"pretty-data": "^0.40.0",
"ps-tree": "^1.0.0",
"pug": "^2.0.0-beta11",
"pug": "^2.0.0-beta.12",
"push-notify": "habitrpg/push-notify#v1.2.0",
"pusher": "^1.3.0",
"request": "~2.74.0",
@@ -108,6 +111,9 @@
"shelljs": "^0.7.6",
"stripe": "^4.2.0",
"superagent": "^3.4.3",
"svg-inline-loader": "^0.7.1",
"svg-url-loader": "^2.0.2",
"svgo-loader": "^1.2.1",
"universal-analytics": "~0.3.2",
"url-loader": "^0.5.7",
"useragent": "^2.1.9",
@@ -119,10 +125,10 @@
"vue-loader": "^11.0.0",
"vue-mugen-scroll": "^0.2.1",
"vue-router": "^2.0.0-rc.5",
"vue-style-loader": "^2.0.0",
"vue-style-loader": "^3.0.0",
"vue-template-compiler": "^2.1.10",
"webpack": "^2.2.1",
"webpack-merge": "^2.6.1",
"webpack-merge": "^4.0.0",
"winston": "^2.1.0",
"winston-loggly-bulk": "^1.4.2",
"xml2js": "^0.4.4"
@@ -134,7 +140,7 @@
},
"scripts": {
"lint": "eslint --ext .js,.vue .",
"test": "npm run lint && gulp test && npm run client:unit",
"test": "npm run lint && gulp test && npm run client:unit && gulp apidoc",
"test:build": "gulp test:prepare:build",
"test:api-v3": "gulp test:api-v3",
"test:api-v3:unit": "gulp test:api-v3:unit",
@@ -151,14 +157,15 @@
"test:nodemon": "gulp test:nodemon",
"coverage": "COVERAGE=true mocha --require register-handlers.js --reporter html-cov > coverage.html; open coverage.html",
"sprites": "gulp sprites:compile",
"client:dev": "node webpack/dev-server.js",
"client:build": "node webpack/build.js",
"client:dev": "gulp bootstrap && node webpack/dev-server.js",
"client:build": "gulp bootstrap && node webpack/build.js",
"client:unit": "cross-env NODE_ENV=test karma start test/client/unit/karma.conf.js --single-run",
"client:unit:watch": "cross-env NODE_ENV=test karma start test/client/unit/karma.conf.js",
"client:e2e": "node test/client/e2e/runner.js",
"client:test": "npm run client:unit && npm run client:e2e",
"start": "gulp run:dev",
"postinstall": "bower --config.interactive=false install -f; gulp build; npm run client:build"
"postinstall": "bower --config.interactive=false install -f && gulp build && npm run client:build",
"apidoc": "gulp apidoc"
},
"devDependencies": {
"babel-plugin-istanbul": "^4.0.0",
@@ -168,7 +175,7 @@
"chromedriver": "^2.27.2",
"connect-history-api-fallback": "^1.1.0",
"coveralls": "^2.11.2",
"cross-env": "^3.1.4",
"cross-env": "^4.0.0",
"cross-spawn": "^5.0.1",
"csv": "~0.3.6",
"deep-diff": "~0.1.4",
@@ -192,7 +199,7 @@
"karma-mocha": "^0.2.0",
"karma-mocha-reporter": "^1.1.1",
"karma-phantomjs-launcher": "^1.0.0",
"karma-sinon-chai": "^1.2.0",
"karma-sinon-chai": "~1.2.0",
"karma-sinon-stub-promise": "^1.0.0",
"karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "0.0.24",
@@ -206,6 +213,7 @@
"nightwatch": "^0.9.12",
"phantomjs-prebuilt": "^2.1.12",
"protractor": "^3.1.1",
"raw-loader": "^0.5.1",
"require-again": "^2.0.0",
"rewire": "^2.3.3",
"selenium-server": "^3.0.1",

1
test/README.md Normal file
View File

@@ -0,0 +1 @@
For information about writing and running tests, see [Using Your Local Install to Modify Habitica's Website and API](http://habitica.wikia.com/wiki/Using_Your_Local_Install_to_Modify_Habitica%27s_Website_and_API).

View File

@@ -1,123 +0,0 @@
# So you want to write API integration tests?
@TODO rewrite
That's great! This README will serve as a quick primer for style conventions and practices for these tests.
## What is this?
These are integration tests for the Habitica API. They are performed by making HTTP requests to the API's endpoints and asserting on the data that is returned.
If the javascript looks weird to you, that's because it's written in [ES2015](http://www.ecma-international.org/ecma-262/6.0/) and transpiled by [Babel](https://babeljs.io/docs/learn-es2015/).
## How to run the tests
First, install gulp.
```bash
$ npm install -g gulp
```
To run the api tests, make sure the mongo db is up and running and then type this on the command line:
```bash
$ gulp test:api-v2
```
It may take a little while to get going, since it requires the web server to start up before the tests can run.
You can also run a watch command for the api tests. This will allow the tests to re-run automatically when you make changes in the `/test/api/v2/`.
```bash
$ gulp test:api-v2:watch
```
One caveat. If you have a severe syntax error in your files, the tests may fail to run, but they won't alert you that they are not running. If you ever find your test hanging for a while, run this to get the stackstrace. Once you've fixed the problem, you can resume running the watch comand.
```bash
$ gulp test:api:safe
```
If you'd like to run the tests individually and inspect the output from the server, in one pane you can run:
```bash
$ gulp test:nodemon
```
And run your tests in another pane:
```bash
$ mocha path/to/file.js
# Mark a test with the `.only` attribute
$ mocha
```
## Structure
Each top level route has it's own directory. So, all the routes that begin with `/groups/` live in `/test/api/groups/`.
Each test should:
* encompase a single route
* begin with the REST parameter for the route
* display the full name of the route, swapping out `/` for `_`
* end with test.js
So, for the `POST` route `/groups/:id/leave`, it would be
```bash
POST-groups_id_leave.test.js
```
## Promises
To mitigate [callback hell](http://callbackhell.com/) :imp:, we've written a helper method to generate a user object that can make http requests that [return promises](https://babeljs.io/docs/learn-es2015/#promises). This makes it very easy to chain together commands. All you need to do to make a subsequent request is return another promise and then call `.then((result) => {})` on the surrounding block, like so:
```js
it('does something', () => {
let user;
return generateUser().then((_user) => { // We return the initial promise so this test can be run asyncronously
user = _user;
return user.post('/groups', {
type: 'party',
});
}).then((party) => { // the result of the promise above is the argument of the function
return user.put(`/groups/${party._id}`, {
name: 'My party',
});
}).then((result) => {
return user.get('/groups/party');
}).then((party) => {
expect(party.name).to.eql('My party');
});
});
```
If the test is simple, you can use the [chai-as-promised](http://chaijs.com/plugins/chai-as-promised) `return expect(somePromise).to.eventually` syntax to make your assertion.
```js
it('makes the party creator the leader automatically', () => {
return expect(user.post('/groups', {
type: 'party',
})).to.eventually.have.deep.property('leader._id', user._id);
});
```
If the test is checking that the request returns an error, use the `.eventually.be.rejected.and.eql` syntax.
```js
it('returns an error', () => {
return expect(user.get('/groups/id-of-a-party-that-user-does-not-belong-to'))
.to.eventually.be.rejected.and.eql({
code: 404,
text: t('messageGroupNotFound'),
});
});
```
## Questions?
Ask in the [Aspiring Coder's Guild](https://habitica.com/#/options/groups/guilds/68d4a01e-db97-4786-8ee3-05d612c5af6f)!

View File

@@ -1,4 +0,0 @@
# How to run tests:
1. `npm test` is equivalent to `gulp test:api-v3` which will run, in order, `gulp lint`, `gulp test:api-v3:unit` and `gulp test:api-v3:integration`. If one of these fails, the whole `npm test` command blocks and fails. Each of these commands can also be run as a standalone command.
2. To run the server and the integrations tests in two different terminals (to better inspect the output in the server) run `npm start` in one and `npm test:api-v3:integration:separate-server` in the other

View File

@@ -304,5 +304,15 @@ describe('POST /challenges', () => {
await expect(groupLeader.sync()).to.eventually.have.property('challenges').to.include(challenge._id);
});
it('awards achievement if this is creator\'s first challenge', async () => {
await groupLeader.post('/challenges', {
group: group._id,
name: 'Test Challenge',
shortName: 'TC Label',
});
groupLeader = await groupLeader.sync();
expect(groupLeader.achievements.joinedChallenge).to.be.true;
});
});
});

View File

@@ -123,5 +123,12 @@ describe('POST /challenges/:challengeId/join', () => {
await expect(authorizedUser.get('/tags')).to.eventually.have.length(userTagsLength + 1);
});
it('awards achievement if this is user\'s first challenge', async () => {
await authorizedUser.post(`/challenges/${challenge._id}/join`);
await authorizedUser.sync();
expect(authorizedUser.achievements.joinedChallenge).to.be.true;
});
});
});

View File

@@ -100,8 +100,8 @@ describe('POST /challenges/:challengeId/winner/:winnerId', () => {
await sleep(0.5);
await expect(winningUser.sync()).to.eventually.have.deep.property('achievements.challenges').to.include(challenge.name);
expect(winningUser.notifications.length).to.equal(1);
expect(winningUser.notifications[0].type).to.equal('WON_CHALLENGE');
expect(winningUser.notifications.length).to.equal(2); // 2 because winningUser just joined the challenge, which now awards an achievement
expect(winningUser.notifications[1].type).to.equal('WON_CHALLENGE');
});
it('gives winner gems as reward', async () => {

View File

@@ -84,6 +84,10 @@ describe('POST /chat/:chatId/flag', () => {
type: 'party',
privacy: 'private',
});
await user.post(`/groups/${privateGroup._id}/invite`, {
uuids: [anotherUser._id],
});
await anotherUser.post(`/groups/${privateGroup._id}/join`);
let { message } = await user.post(`/groups/${privateGroup._id}/chat`, {message: TEST_MESSAGE});
let flagResult = await admin.post(`/groups/${privateGroup._id}/chat/${message.id}/flag`);
@@ -91,7 +95,7 @@ describe('POST /chat/:chatId/flag', () => {
expect(flagResult.flags[admin._id]).to.equal(true);
expect(flagResult.flagCount).to.equal(5);
let groupWithFlags = await user.get(`/groups/${privateGroup._id}`);
let groupWithFlags = await anotherUser.get(`/groups/${privateGroup._id}`);
let messageToCheck = find(groupWithFlags.chat, {id: message.id});
expect(messageToCheck).to.not.exist;
@@ -125,4 +129,20 @@ describe('POST /chat/:chatId/flag', () => {
message: t('messageGroupChatFlagAlreadyReported'),
});
});
it('shows a hidden message to the original poster', async () => {
let { message } = await user.post(`/groups/${group._id}/chat`, {message: TEST_MESSAGE});
await admin.post(`/groups/${group._id}/chat/${message.id}/flag`);
let groupWithFlags = await user.get(`/groups/${group._id}`);
let messageToCheck = find(groupWithFlags.chat, {id: message.id});
expect(messageToCheck).to.exist;
let auGroupWithFlags = await anotherUser.get(`/groups/${group._id}`);
let auMessageToCheck = find(auGroupWithFlags.chat, {id: message.id});
expect(auMessageToCheck).to.not.exist;
});
});

View File

@@ -70,9 +70,9 @@ describe('POST /chat', () => {
it('returns an error when chat privileges are revoked when sending a message to a public guild', async () => {
let userWithChatRevoked = await member.update({'flags.chatRevoked': true});
await expect(userWithChatRevoked.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage})).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: 'Your chat privileges have been revoked.',
code: 401,
error: 'NotAuthorized',
message: t('chatPrivilegesRevoked'),
});
});

View File

@@ -74,6 +74,18 @@ describe('POST /group', () => {
expect(updatedUser.guilds).to.include(guild._id);
});
it('awards the Joined Guild achievement', async () => {
await user.post('/groups', {
name: 'some guild',
type: 'guild',
privacy: 'public',
});
let updatedUser = await user.get('/user');
expect(updatedUser.achievements.joinedGuild).to.eql(true);
});
context('public guild', () => {
it('creates a group', async () => {
let groupName = 'Test Public Guild';

View File

@@ -68,6 +68,12 @@ describe('POST /group/:groupId/join', () => {
await expect(joiningUser.get(`/groups/${publicGuild._id}`)).to.eventually.have.property('memberCount', oldMemberCount + 1);
});
it('awards Joined Guild achievement', async () => {
await joiningUser.post(`/groups/${publicGuild._id}/join`);
await expect(joiningUser.get('/user')).to.eventually.have.deep.property('achievements.joinedGuild', true);
});
});
context('Joining a private guild', () => {
@@ -147,8 +153,14 @@ describe('POST /group/:groupId/join', () => {
}),
};
expect(inviter.notifications[0].type).to.eql('GROUP_INVITE_ACCEPTED');
expect(inviter.notifications[0].data).to.eql(expectedData);
expect(inviter.notifications[1].type).to.eql('GROUP_INVITE_ACCEPTED');
expect(inviter.notifications[1].data).to.eql(expectedData);
});
it('awards Joined Guild achievement', async () => {
await invitedUser.post(`/groups/${guild._id}/join`);
await expect(invitedUser.get('/user')).to.eventually.have.deep.property('achievements.joinedGuild', true);
});
});
});

View File

@@ -11,6 +11,7 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
let guild;
let member;
let member2;
let adminUser;
beforeEach(async () => {
let { group, groupLeader, invitees, members } = await createAndPopulateGroup({
@@ -28,6 +29,7 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
invitedUser = invitees[0];
member = members[0];
member2 = members[1];
adminUser = await generateUser({ 'contributor.admin': true });
});
context('All Groups', () => {
@@ -42,7 +44,7 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
});
});
it('returns an error when user is a non-leader member of a group', async () => {
it('returns an error when user is a non-leader member of a group and not an admin', async () => {
expect(member2.post(`/groups/${guild._id}/removeMember/${member._id}`))
.to.eventually.be.rejected.and.eql({
code: 401,
@@ -87,7 +89,30 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
let invitedUserWithoutInvite = await invitedUser.get('/user');
expect(_.findIndex(invitedUserWithoutInvite.invitations.guilds, {id: guild._id})).eql(-1);
expect(_.findIndex(invitedUserWithoutInvite.invitations.guilds, { id: guild._id })).eql(-1);
});
it('allows an admin to remove other members', async () => {
await adminUser.post(`/groups/${guild._id}/removeMember/${member._id}`);
let memberRemoved = await member.get('/user');
expect(memberRemoved.guilds.indexOf(guild._id)).eql(-1);
});
it('allows an admin to remove other invites', async () => {
await adminUser.post(`/groups/${guild._id}/removeMember/${invitedUser._id}`);
let invitedUserWithoutInvite = await invitedUser.get('/user');
expect(_.findIndex(invitedUserWithoutInvite.invitations.guilds, { id: guild._id })).eql(-1);
});
it('does not allow an admin to remove a leader', async () => {
expect(adminUser.post(`/groups/${guild._id}/removeMember/${leader._id}`))
.to.eventually.be.rejected.and.eql({
code: 401,
text: t('cannotRemoveCurrentLeader'),
});
});
it('sends email to user with rescinded invite', async () => {

View File

@@ -1,10 +1,11 @@
import {
createAndPopulateGroup,
generateUser,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
describe('PUT /group', () => {
let leader, nonLeader, groupToUpdate;
let leader, nonLeader, groupToUpdate, adminUser;
let groupName = 'Test Public Guild';
let groupType = 'guild';
let groupUpdatedName = 'Test Public Guild Updated';
@@ -18,13 +19,13 @@ describe('PUT /group', () => {
},
members: 1,
});
adminUser = await generateUser({ 'contributor.admin': true });
groupToUpdate = group;
leader = groupLeader;
nonLeader = members[0];
});
it('returns an error when a non group leader tries to update', async () => {
it('returns an error when a user that is not an admin or group leader tries to update', async () => {
await expect(nonLeader.put(`/groups/${groupToUpdate._id}`, {
name: groupUpdatedName,
})).to.eventually.be.rejected.and.eql({
@@ -44,6 +45,15 @@ describe('PUT /group', () => {
expect(updatedGroup.name).to.equal(groupUpdatedName);
});
it('allows an admin to update a guild', async () => {
let updatedGroup = await adminUser.put(`/groups/${groupToUpdate._id}`, {
name: groupUpdatedName,
});
expect(updatedGroup.leader._id).to.eql(leader._id);
expect(updatedGroup.leader.profile.name).to.eql(leader.profile.name);
expect(updatedGroup.name).to.equal(groupUpdatedName);
});
it('allows a leader to change leaders', async () => {
let updatedGroup = await leader.put(`/groups/${groupToUpdate._id}`, {
name: groupUpdatedName,

View File

@@ -0,0 +1,64 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
describe('GET /members/:toUserId/objections/:interaction', () => {
let user;
before(async () => {
user = await generateUser();
});
it('validates req.params.memberId', async () => {
await expect(
user.get('/members/invalidUUID/objections/send-private-message')
).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('handles non-existing members', async () => {
let dummyId = generateUUID();
await expect(
user.get(`/members/${dummyId}/objections/send-private-message`)
).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('userWithIDNotFound', {userId: dummyId}),
});
});
it('handles non-existing interactions', async () => {
let receiver = await generateUser();
await expect(
user.get(`/members/${receiver._id}/objections/hug-a-whole-forest-of-trees`)
).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('returns an empty array if there are no objections', async () => {
let receiver = await generateUser();
await expect(
user.get(`/members/${receiver._id}/objections/send-private-message`)
).to.eventually.be.fulfilled.and.eql([]);
});
it('returns an array of objections if any exist', async () => {
let receiver = await generateUser({'inbox.blocks': [user._id]});
await expect(
user.get(`/members/${receiver._id}/objections/send-private-message`)
).to.eventually.be.fulfilled.and.eql([
t('notAuthorizedToSendMessageToThisUser'),
]);
});
});

View File

@@ -82,6 +82,20 @@ describe('POST /members/send-private-message', () => {
});
});
it('returns an error when chat privileges are revoked', async () => {
let userWithChatRevoked = await generateUser({'flags.chatRevoked': true});
let receiver = await generateUser();
await expect(userWithChatRevoked.post('/members/send-private-message', {
message: messageToSend,
toUserId: receiver._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('chatPrivilegesRevoked'),
});
});
it('sends a private message to a user', async () => {
let receiver = await generateUser();

View File

@@ -43,7 +43,7 @@ describe('POST /members/transfer-gems', () => {
});
});
it('returns error when to user is not found', async () => {
it('returns error when recipient is not found', async () => {
await expect(userToSendMessage.post('/members/transfer-gems', {
message,
gemAmount,
@@ -55,7 +55,7 @@ describe('POST /members/transfer-gems', () => {
});
});
it('returns error when to user attempts to send gems to themselves', async () => {
it('returns error when user attempts to send gems to themselves', async () => {
await expect(userToSendMessage.post('/members/transfer-gems', {
message,
gemAmount,
@@ -67,6 +67,64 @@ describe('POST /members/transfer-gems', () => {
});
});
it('returns error when recipient has blocked the sender', async () => {
let receiverWhoBlocksUser = await generateUser({'inbox.blocks': [userToSendMessage._id]});
await expect(userToSendMessage.post('/members/transfer-gems', {
message,
gemAmount,
toUserId: receiverWhoBlocksUser._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('notAuthorizedToSendMessageToThisUser'),
});
});
it('returns error when sender has blocked recipient', async () => {
let sender = await generateUser({'inbox.blocks': [receiver._id]});
await expect(sender.post('/members/transfer-gems', {
message,
gemAmount,
toUserId: receiver._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('notAuthorizedToSendMessageToThisUser'),
});
});
it('returns an error when chat privileges are revoked', async () => {
let userWithChatRevoked = await generateUser({'flags.chatRevoked': true});
await expect(userWithChatRevoked.post('/members/transfer-gems', {
message,
gemAmount,
toUserId: receiver._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('chatPrivilegesRevoked'),
});
});
it('works when only the recipient\'s chat privileges are revoked', async () => {
let receiverWithChatRevoked = await generateUser({'flags.chatRevoked': true});
await expect(userToSendMessage.post('/members/transfer-gems', {
message,
gemAmount,
toUserId: receiverWithChatRevoked._id,
})).to.eventually.be.fulfilled;
let updatedReceiver = await receiverWithChatRevoked.get('/user');
let updatedSender = await userToSendMessage.get('/user');
expect(updatedReceiver.balance).to.equal(gemAmount / 4);
expect(updatedSender.balance).to.equal(0);
});
it('returns error when there is no gemAmount', async () => {
await expect(userToSendMessage.post('/members/transfer-gems', {
message,
@@ -144,7 +202,7 @@ describe('POST /members/transfer-gems', () => {
expect(updatedSender.balance).to.equal(0);
});
it('does not requrie a message', async () => {
it('does not require a message', async () => {
await userToSendMessage.post('/members/transfer-gems', {
gemAmount,
toUserId: receiver._id,

View File

@@ -0,0 +1,25 @@
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('GET /shops/backgrounds', () => {
let user;
beforeEach(async () => {
user = await generateUser();
});
it('returns a valid shop object', async () => {
let shop = await user.get('/shops/backgrounds');
expect(shop.identifier).to.equal('backgroundShop');
expect(shop.text).to.eql(t('backgroundShop'));
expect(shop.notes).to.eql(t('backgroundShopText'));
expect(shop.imageName).to.equal('background_shop');
expect(shop.sets).to.be.an('array');
let sets = shop.sets.map(set => set.identifier);
expect(sets).to.include('incentiveBackgrounds');
expect(sets).to.include('backgrounds062014');
});
});

View File

@@ -208,6 +208,20 @@ describe('POST /tasks/:id/score/:direction', () => {
expect(task.completed).to.equal(false);
});
it('computes isDue', async () => {
await user.post(`/tasks/${daily._id}/score/up`);
let task = await user.get(`/tasks/${daily._id}`);
expect(task.isDue).to.equal(true);
});
it('computes nextDue', async () => {
await user.post(`/tasks/${daily._id}/score/up`);
let task = await user.get(`/tasks/${daily._id}`);
expect(task.nextDue.length).to.eql(6);
});
it('scores up daily even if it is already completed'); // Yes?
it('scores down daily even if it is already uncompleted'); // Yes?

View File

@@ -509,6 +509,8 @@ describe('POST /tasks/user', () => {
expect(task.daysOfMonth).to.eql([15]);
expect(task.weeksOfMonth).to.eql([3]);
expect(new Date(task.startDate)).to.eql(now);
expect(task.isDue).to.be.true;
expect(task.nextDue.length).to.eql(6);
});
it('creates multiple dailys', async () => {
@@ -613,6 +615,18 @@ describe('POST /tasks/user', () => {
expect((new Date(task.startDate)).getDay()).to.eql(today);
});
it('returns an error if the start date is empty', async () => {
await expect(user.post('/tasks/user', {
text: 'test daily',
type: 'daily',
startDate: '',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'daily validation failed',
});
});
it('can create checklists', async () => {
let task = await user.post('/tasks/user', {
text: 'test daily',

View File

@@ -1,3 +1,4 @@
import moment from 'moment';
import {
generateUser,
generateGroup,
@@ -395,12 +396,15 @@ describe('PUT /tasks/:id', () => {
notes: 'some new notes',
frequency: 'daily',
everyX: 5,
startDate: moment().add(1, 'days').toDate(),
});
expect(savedDaily.text).to.eql('some new text');
expect(savedDaily.notes).to.eql('some new notes');
expect(savedDaily.frequency).to.eql('daily');
expect(savedDaily.everyX).to.eql(5);
expect(savedDaily.isDue).to.be.false;
expect(savedDaily.nextDue.length).to.eql(6);
});
it('can update checklists (replace it)', async () => {

View File

@@ -112,8 +112,8 @@ describe('POST /tasks/:id/approve/:userId', () => {
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(1);
expect(user.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
expect(user.notifications.length).to.equal(2);
expect(user.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
expect(member2.notifications.length).to.equal(1);
expect(member2.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
@@ -122,7 +122,7 @@ describe('POST /tasks/:id/approve/:userId', () => {
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(0);
expect(user.notifications.length).to.equal(1);
expect(member2.notifications.length).to.equal(0);
});

View File

@@ -52,14 +52,14 @@ describe('POST /tasks/:id/score/:direction', () => {
await user.sync();
expect(user.notifications.length).to.equal(1);
expect(user.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
expect(user.notifications[0].data.message).to.equal(t('userHasRequestedTaskApproval', {
expect(user.notifications.length).to.equal(2);
expect(user.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
expect(user.notifications[1].data.message).to.equal(t('userHasRequestedTaskApproval', {
user: member.auth.local.username,
taskName: updatedTask.text,
taskId: updatedTask._id,
}, 'cs')); // This test only works if we have the notification translated
expect(user.notifications[0].data.groupId).to.equal(guild._id);
expect(user.notifications[1].data.groupId).to.equal(guild._id);
expect(updatedTask.group.approval.requested).to.equal(true);
expect(updatedTask.group.approval.requestedDate).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
@@ -82,14 +82,14 @@ describe('POST /tasks/:id/score/:direction', () => {
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(1);
expect(user.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
expect(user.notifications[0].data.message).to.equal(t('userHasRequestedTaskApproval', {
expect(user.notifications.length).to.equal(2);
expect(user.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
expect(user.notifications[1].data.message).to.equal(t('userHasRequestedTaskApproval', {
user: member.auth.local.username,
taskName: updatedTask.text,
taskId: updatedTask._id,
}));
expect(user.notifications[0].data.groupId).to.equal(guild._id);
expect(user.notifications[1].data.groupId).to.equal(guild._id);
expect(member2.notifications.length).to.equal(1);
expect(member2.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');

View File

@@ -16,6 +16,7 @@ import {
sha1MakeSalt,
sha1Encrypt as sha1EncryptPassword,
} from '../../../../../website/server/libs/password';
import * as email from '../../../../../website/server/libs/email';
describe('DELETE /user', () => {
let user;
@@ -25,7 +26,7 @@ describe('DELETE /user', () => {
user = await generateUser({balance: 10});
});
it('returns an errors if password is wrong', async () => {
it('returns an error if password is wrong', async () => {
await expect(user.del('/user', {
password: 'wrong-password',
})).to.eventually.be.rejected.and.eql({
@@ -35,6 +36,33 @@ describe('DELETE /user', () => {
});
});
it('returns an error if password is not supplied', async () => {
await expect(user.del('/user', {
password: '',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingPassword'),
});
});
it('returns an error if excessive feedback is supplied', async () => {
let feedbackText = 'spam feedback ';
let feedback = feedbackText;
while (feedback.length < 10000) {
feedback = feedback + feedbackText;
}
await expect(user.del('/user', {
password,
feedback,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Account deletion feedback is limited to 10,000 characters. For lengthy feedback, email admin@habitica.com.',
});
});
it('returns an error if user has active subscription', async () => {
let userWithSubscription = await generateUser({'purchased.plan.customerId': 'fake-customer-id'});
@@ -96,6 +124,32 @@ describe('DELETE /user', () => {
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
});
it('sends feedback to the admin email', async () => {
sandbox.spy(email, 'sendTxn');
let feedback = 'Reasons for Deletion';
await user.del('/user', {
password,
feedback,
});
expect(email.sendTxn).to.be.calledOnce;
sandbox.restore();
});
it('does not send email if no feedback is supplied', async () => {
sandbox.spy(email, 'sendTxn');
await user.del('/user', {
password,
});
expect(email.sendTxn).to.not.be.called;
sandbox.restore();
});
it('deletes the user with a legacy sha1 password', async () => {
let textPassword = 'mySecretPassword';
let salt = sha1MakeSalt();

View File

@@ -221,6 +221,27 @@ describe('POST /user/class/cast/:spellId', () => {
expect(syncedGroupTask.value).to.equal(0);
});
it('increases both user\'s achievement values', async () => {
let party = await createAndPopulateGroup({
members: 1,
});
let leader = party.groupLeader;
let recipient = party.members[0];
await leader.update({'stats.gp': 10});
await leader.post(`/user/class/cast/birthday?targetId=${recipient._id}`);
await leader.sync();
await recipient.sync();
expect(leader.achievements.birthday).to.equal(1);
expect(recipient.achievements.birthday).to.equal(1);
});
it('only increases user\'s achievement one if target == caster', async () => {
await user.update({'stats.gp': 10});
await user.post(`/user/class/cast/birthday?targetId=${user._id}`);
await user.sync();
expect(user.achievements.birthday).to.equal(1);
});
// TODO find a way to have sinon working in integration tests
// it doesn't work when tests are running separately from server
it('passes correct target to spell when targetType === \'task\'');

View File

@@ -251,7 +251,6 @@ describe('POST /user/auth/local/register', () => {
confirmPassword: password,
});
await expect(getProperty('users', user._id, '_ABtest')).to.eventually.be.a('string');
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
});

View File

@@ -83,7 +83,7 @@ describe('POST /user/auth/social', () => {
network,
});
await expect(getProperty('users', user._id, '_ABtest')).to.eventually.be.a('string');
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
});
});
@@ -139,7 +139,7 @@ describe('POST /user/auth/social', () => {
network,
});
await expect(getProperty('users', user._id, '_ABtest')).to.eventually.be.a('string');
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
});
});
});

View File

@@ -525,6 +525,7 @@ describe('Amazon Payments', () => {
nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: amzLib.constants.PAYMENT_METHOD,
headers,
cancellationReason: undefined,
});
expectAmazonStubs();
});
@@ -555,6 +556,7 @@ describe('Amazon Payments', () => {
nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: amzLib.constants.PAYMENT_METHOD,
headers,
cancellationReason: undefined,
});
amzLib.closeBillingAgreement.restore();
});
@@ -593,6 +595,7 @@ describe('Amazon Payments', () => {
nextBill: moment(group.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: amzLib.constants.PAYMENT_METHOD,
headers,
cancellationReason: undefined,
});
expectAmazonStubs();
});
@@ -623,6 +626,7 @@ describe('Amazon Payments', () => {
nextBill: moment(group.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
paymentMethod: amzLib.constants.PAYMENT_METHOD,
headers,
cancellationReason: undefined,
});
amzLib.closeBillingAgreement.restore();
});

View File

@@ -313,6 +313,24 @@ describe('cron', () => {
expect(tasksByType.dailys[0].completed).to.be.false;
expect(user.stats.hp).to.equal(healthBefore);
});
it('sets isDue for daily', () => {
let daily = {
text: 'test daily',
type: 'daily',
frequency: 'daily',
everyX: 5,
startDate: new Date(),
};
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line new-cap
tasksByType.dailys.push(task);
tasksByType.dailys[0].completed = true;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].isDue).to.be.exist;
});
});
describe('todos', () => {
@@ -358,6 +376,22 @@ describe('cron', () => {
};
});
it('computes isDue', () => {
tasksByType.dailys[0].frequency = 'daily';
tasksByType.dailys[0].everyX = 5;
tasksByType.dailys[0].startDate = moment().add(1, 'days').toDate();
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].isDue).to.be.false;
});
it('computes nextDue', () => {
tasksByType.dailys[0].frequency = 'daily';
tasksByType.dailys[0].everyX = 5;
tasksByType.dailys[0].startDate = moment().add(1, 'days').toDate();
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].nextDue.length).to.eql(6);
});
it('should add history', () => {
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].history).to.be.lengthOf(1);

View File

@@ -16,6 +16,7 @@ describe('payments/index', () => {
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
await user.save();
group = generateGroup({
name: 'test group',
@@ -504,6 +505,18 @@ describe('payments/index', () => {
expect(daysTillTermination).to.be.within(13, 15);
});
it('terminates at next billing date even if dateUpdated is prior to now', async () => {
data.nextBill = moment().add({ days: 15 });
data.user.purchased.plan.dateUpdated = moment().subtract({ days: 10 });
await api.cancelSubscription(data);
let now = new Date();
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
expect(daysTillTermination).to.be.within(13, 15);
});
it('resets plan.extraMonths', async () => {
user.purchased.plan.extraMonths = 5;
@@ -653,5 +666,32 @@ describe('payments/index', () => {
expect(updatedUser.items.pets['Jackalope-RoyalPurple']).to.eql(5);
});
it('saves previously unused Mystery Items and Hourglasses for an expired subscription', async () => {
let planExpirationDate = new Date();
planExpirationDate.setDate(planExpirationDate.getDate() - 2);
let mysteryItem = 'item';
let mysteryItems = [mysteryItem];
let consecutive = {
trinkets: 3,
};
// set expired plan with unused items
plan.mysteryItems = mysteryItems;
plan.consecutive = consecutive;
plan.dateCreated = planExpirationDate;
plan.dateTerminated = planExpirationDate;
plan.customerId = null;
user.purchased.plan = plan;
await user.save();
await api.addSubToGroupUser(user, group);
let updatedUser = await User.findById(user._id).exec();
expect(updatedUser.purchased.plan.mysteryItems[0]).to.eql(mysteryItem);
expect(updatedUser.purchased.plan.consecutive.trinkets).to.equal(consecutive.trinkets);
});
});
});

View File

@@ -138,7 +138,7 @@ describe('Canceling a subscription for group', () => {
]);
});
it('prevents non group leader from manging subscription', async () => {
it('prevents non group leader from managing subscription', async () => {
let groupMember = new User();
data.user = groupMember;
data.groupId = group._id;

View File

@@ -1,5 +1,6 @@
import moment from 'moment';
import stripeModule from 'stripe';
import nconf from 'nconf';
import * as sender from '../../../../../../../website/server/libs/email';
import * as api from '../../../../../../../website/server/libs/payments';
@@ -12,17 +13,24 @@ import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
describe('Purchasing a subscription for group', () => {
describe('Purchasing a group plan for group', () => {
const EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_GOOGLE = 'Google_subscription';
const EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_IOS = 'iOS_subscription';
const EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NORMAL = 'normal_subscription';
const EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NONE = 'no_subscription';
let plan, group, user, data;
let stripe = stripeModule('test');
let groupLeaderName = 'sender';
let groupName = 'test group';
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
user.profile.name = groupLeaderName;
await user.save();
group = generateGroup({
name: 'test group',
name: groupName,
type: 'guild',
privacy: 'public',
leader: user._id,
@@ -81,7 +89,7 @@ describe('Purchasing a subscription for group', () => {
sender.sendTxn.restore();
});
it('creates a subscription', async () => {
it('creates a group plan', async () => {
expect(group.purchased.plan.planId).to.not.exist;
data.groupId = group._id;
@@ -157,7 +165,7 @@ describe('Purchasing a subscription for group', () => {
expect(updatedLeader.items.mounts['Jackalope-RoyalPurple']).to.be.true;
});
it('sends an email to members of group', async () => {
it('sends an email to member of group who was not a subscriber', async () => {
let recipient = new User();
recipient.profile.name = 'recipient';
recipient.guilds.push(group._id);
@@ -169,11 +177,181 @@ describe('Purchasing a subscription for group', () => {
expect(sender.sendTxn).to.be.calledTwice;
expect(sender.sendTxn.firstCall.args[0]._id).to.equal(recipient._id);
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-joining');
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-join');
expect(sender.sendTxn.firstCall.args[2]).to.eql([
{name: 'LEADER', content: user.profile.name},
{name: 'GROUP_NAME', content: group.name},
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NONE},
]);
// confirm that the other email sent is appropriate:
expect(sender.sendTxn.secondCall.args[0]._id).to.equal(group.leader);
expect(sender.sendTxn.secondCall.args[1]).to.equal('group-subscription-begins');
});
it('sends one email to subscribed member of group, stating subscription is cancelled (Stripe)', async () => {
let recipient = new User();
recipient.profile.name = 'recipient';
plan.key = 'basic_earned';
plan.paymentMethod = stripePayments.constants.PAYMENT_METHOD;
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
data.groupId = group._id;
await api.createSubscription(data);
expect(sender.sendTxn).to.be.calledTwice;
expect(sender.sendTxn.firstCall.args[0]._id).to.equal(recipient._id);
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-join');
expect(sender.sendTxn.firstCall.args[2]).to.eql([
{name: 'LEADER', content: user.profile.name},
{name: 'GROUP_NAME', content: group.name},
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NORMAL},
]);
// confirm that the other email sent is not a cancel-subscription email:
expect(sender.sendTxn.secondCall.args[0]._id).to.equal(group.leader);
expect(sender.sendTxn.secondCall.args[1]).to.equal('group-subscription-begins');
});
it('sends one email to subscribed member of group, stating subscription is cancelled (Amazon)', async () => {
sinon.stub(amzLib, 'getBillingAgreementDetails')
.returnsPromise()
.resolves({
BillingAgreementDetails: {
BillingAgreementStatus: {State: 'Closed'},
},
});
let recipient = new User();
recipient.profile.name = 'recipient';
plan.planId = 'basic_earned';
plan.paymentMethod = amzLib.constants.PAYMENT_METHOD;
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
data.groupId = group._id;
await api.createSubscription(data);
expect(sender.sendTxn).to.be.calledTwice;
expect(sender.sendTxn.firstCall.args[0]._id).to.equal(recipient._id);
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-join');
expect(sender.sendTxn.firstCall.args[2]).to.eql([
{name: 'LEADER', content: user.profile.name},
{name: 'GROUP_NAME', content: group.name},
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NORMAL},
]);
// confirm that the other email sent is not a cancel-subscription email:
expect(sender.sendTxn.secondCall.args[0]._id).to.equal(group.leader);
expect(sender.sendTxn.secondCall.args[1]).to.equal('group-subscription-begins');
amzLib.getBillingAgreementDetails.restore();
});
it('sends one email to subscribed member of group, stating subscription is cancelled (PayPal)', async () => {
sinon.stub(paypalPayments, 'paypalBillingAgreementCancel').returnsPromise().resolves({});
sinon.stub(paypalPayments, 'paypalBillingAgreementGet')
.returnsPromise().resolves({
agreement_details: { // eslint-disable-line camelcase
next_billing_date: moment().add(3, 'months').toDate(), // eslint-disable-line camelcase
cycles_completed: 1, // eslint-disable-line camelcase
},
});
let recipient = new User();
recipient.profile.name = 'recipient';
plan.planId = 'basic_earned';
plan.paymentMethod = paypalPayments.constants.PAYMENT_METHOD;
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
data.groupId = group._id;
await api.createSubscription(data);
expect(sender.sendTxn).to.be.calledTwice;
expect(sender.sendTxn.firstCall.args[0]._id).to.equal(recipient._id);
expect(sender.sendTxn.firstCall.args[1]).to.equal('group-member-join');
expect(sender.sendTxn.firstCall.args[2]).to.eql([
{name: 'LEADER', content: user.profile.name},
{name: 'GROUP_NAME', content: group.name},
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NORMAL},
]);
// confirm that the other email sent is not a cancel-subscription email:
expect(sender.sendTxn.secondCall.args[0]._id).to.equal(group.leader);
expect(sender.sendTxn.secondCall.args[1]).to.equal('group-subscription-begins');
paypalPayments.paypalBillingAgreementGet.restore();
paypalPayments.paypalBillingAgreementCancel.restore();
});
it('sends appropriate emails when subscribed member of group must manually cancel recurring Android subscription', async () => {
const TECH_ASSISTANCE_EMAIL = nconf.get('TECH_ASSISTANCE_EMAIL');
plan.customerId = 'random';
plan.paymentMethod = api.constants.GOOGLE_PAYMENT_METHOD;
let recipient = new User();
recipient.profile.name = 'recipient';
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
user.guilds.push(group._id);
await user.save();
data.groupId = group._id;
await api.createSubscription(data);
expect(sender.sendTxn).to.be.calledFourTimes;
expect(sender.sendTxn.args[0][0]._id).to.equal(TECH_ASSISTANCE_EMAIL);
expect(sender.sendTxn.args[0][1]).to.equal('admin-user-subscription-details');
expect(sender.sendTxn.args[1][0]._id).to.equal(recipient._id);
expect(sender.sendTxn.args[1][1]).to.equal('group-member-join');
expect(sender.sendTxn.args[1][2]).to.eql([
{name: 'LEADER', content: groupLeaderName},
{name: 'GROUP_NAME', content: groupName},
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_GOOGLE},
]);
expect(sender.sendTxn.args[2][0]._id).to.equal(group.leader);
expect(sender.sendTxn.args[2][1]).to.equal('group-member-join');
expect(sender.sendTxn.args[3][0]._id).to.equal(group.leader);
expect(sender.sendTxn.args[3][1]).to.equal('group-subscription-begins');
});
it('sends appropriate emails when subscribed member of group must manually cancel recurring iOS subscription', async () => {
const TECH_ASSISTANCE_EMAIL = nconf.get('TECH_ASSISTANCE_EMAIL');
plan.customerId = 'random';
plan.paymentMethod = api.constants.IOS_PAYMENT_METHOD;
let recipient = new User();
recipient.profile.name = 'recipient';
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
user.guilds.push(group._id);
await user.save();
data.groupId = group._id;
await api.createSubscription(data);
expect(sender.sendTxn).to.be.calledFourTimes;
expect(sender.sendTxn.args[0][0]._id).to.equal(TECH_ASSISTANCE_EMAIL);
expect(sender.sendTxn.args[0][1]).to.equal('admin-user-subscription-details');
expect(sender.sendTxn.args[1][0]._id).to.equal(recipient._id);
expect(sender.sendTxn.args[1][1]).to.equal('group-member-join');
expect(sender.sendTxn.args[1][2]).to.eql([
{name: 'LEADER', content: groupLeaderName},
{name: 'GROUP_NAME', content: groupName},
{name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_IOS},
]);
expect(sender.sendTxn.args[2][0]._id).to.equal(group.leader);
expect(sender.sendTxn.args[2][1]).to.equal('group-member-join');
expect(sender.sendTxn.args[3][0]._id).to.equal(group.leader);
expect(sender.sendTxn.args[3][1]).to.equal('group-subscription-begins');
});
it('adds months to members with existing gift subscription', async () => {
@@ -333,7 +511,7 @@ describe('Purchasing a subscription for group', () => {
});
it('adds months to members with existing recurring subscription (Android)');
it('adds months to members with existing recurring subscription (iOs)');
it('adds months to members with existing recurring subscription (iOS)');
it('adds months to members who already cancelled but not yet terminated recurring subscription', async () => {
let recipient = new User();
@@ -603,7 +781,7 @@ describe('Purchasing a subscription for group', () => {
expect(updatedUser.purchased.plan.dateCreated).to.exist;
});
it('does not modify a user with a Google subscription', async () => {
it('does not modify a user with an Android subscription', async () => {
plan.customerId = 'random';
plan.paymentMethod = api.constants.GOOGLE_PAYMENT_METHOD;

View File

@@ -447,6 +447,7 @@ describe('Paypal Payments', () => {
groupId,
paymentMethod: 'Paypal',
nextBill: nextBillingDate,
cancellationReason: undefined,
});
});
@@ -464,6 +465,7 @@ describe('Paypal Payments', () => {
groupId: group._id,
paymentMethod: 'Paypal',
nextBill: nextBillingDate,
cancellationReason: undefined,
});
});
});

View File

@@ -683,6 +683,7 @@ describe('Stripe Payments', () => {
groupId: undefined,
nextBill: currentPeriodEndTimeStamp * 1000, // timestamp in seconds
paymentMethod: 'Stripe',
cancellationReason: undefined,
});
});
@@ -702,6 +703,7 @@ describe('Stripe Payments', () => {
groupId,
nextBill: currentPeriodEndTimeStamp * 1000, // timestamp in seconds
paymentMethod: 'Stripe',
cancellationReason: undefined,
});
});
});

View File

@@ -228,7 +228,7 @@ describe('Group Model', () => {
});
it('applies damage only to participating members of party even under buggy conditions', async () => {
// stops unfair damage from mbugs like https://github.com/HabitRPG/habitrpg/issues/7653
// stops unfair damage from mbugs like https://github.com/HabitRPG/habitica/issues/7653
party.quest.members = {
[questLeader._id]: true,
[participatingMember._id]: true,

View File

@@ -112,6 +112,41 @@ describe('Groups Controller', function() {
});
});
describe('isAbleToEditGroup', () => {
var guild;
beforeEach(() => {
user.contributor = {};
guild = specHelper.newGroup({
_id: 'unique-guild-id',
type: 'guild',
members: ['not-user-id'],
$save: sandbox.spy(),
});
});
it('returns true if user is an admin', () => {
guild.leader = 'not-user-id';
user.contributor.admin = true;
expect(scope.isAbleToEditGroup(guild)).to.be.ok;
});
it('returns true if user is group leader', () => {
guild.leader = {_id: user._id}
expect(scope.isAbleToEditGroup(guild)).to.be.ok;
});
it('returns false is user is not a leader or admin', () => {
expect(scope.isAbleToEditGroup(guild)).to.not.be.ok;
});
it('returns false is user is an admin but group is a party', () => {
guild.type = 'party';
user.contributor.admin = true;
expect(scope.isAbleToEditGroup(guild)).to.not.be.ok;
});
});
describe('editGroup', () => {
var guild;

View File

@@ -1,35 +0,0 @@
'use strict';
describe('closeMenu Directive', function() {
var scope;
beforeEach(module('habitrpg'));
beforeEach(inject(function($rootScope) {
scope = $rootScope.$new();
scope.$digest();
}));
it('closes a connected menu when element is clicked', inject(function($compile) {
var menuElement = $compile('<a data-close-menu menu="mobile">')(scope);
scope._expandedMenu = { menu: 'mobile' };
menuElement.appendTo(document.body);
menuElement.triggerHandler('click');
expect(scope._expandedMenu.menu).to.eql(null)
}));
it('closes a connected menu when child element is clicked', inject(function($compile) {
var menuElementWithChild = $compile('<li></li>')(scope);
var menuElementChild = $compile('<a data-close-menu></a>')(scope);
scope._expandedMenu = { menu: 'mobile' };
menuElementWithChild.appendTo(document.body);
menuElementChild.appendTo(menuElementWithChild);
menuElementChild.triggerHandler('click');
expect(scope._expandedMenu.menu).to.eql(null)
}));
});

View File

@@ -1,35 +0,0 @@
'use strict';
describe('expandMenu Directive', function() {
var menuElement, scope;
beforeEach(module('habitrpg'));
beforeEach(inject(function($rootScope, $compile) {
scope = $rootScope.$new();
var element = '<a data-expand-menu menu="mobile"></a>';
menuElement = $compile(element)(scope);
scope.$digest();
}));
it('expands a connected menu when element is clicked', function() {
expect(scope._expandedMenu).to.not.exist;
menuElement.appendTo(document.body);
menuElement.triggerHandler('click');
expect(scope._expandedMenu.menu).to.eql('mobile')
});
it('closes a connected menu when it is already open', function() {
scope._expandedMenu = {};
scope._expandedMenu.menu = 'mobile';
menuElement.appendTo(document.body);
menuElement.triggerHandler('click');
expect(scope._expandedMenu.menu).to.eql(null)
});
});

View File

@@ -2,5 +2,10 @@
"env": {
"node": true,
"browser": true,
}
},
"extends": [
"habitrpg/browser",
"habitrpg/mocha",
"habitrpg/esnext",
],
}

View File

@@ -0,0 +1,19 @@
import Vue from 'vue';
import DrawerComponent from 'client/components/inventory/drawer.vue';
describe('DrawerComponent', () => {
it('sets the correct default data', () => {
expect(DrawerComponent.data).to.be.a('function');
const defaultData = DrawerComponent.data();
expect(defaultData.open).to.be.true;
});
it('renders the correct title', () => {
const Ctor = Vue.extend(DrawerComponent);
const vm = new Ctor({propsData: {
title: 'My title',
}}).$mount();
expect(vm.$el.textContent).to.be.equal('My title');
});
});

View File

@@ -0,0 +1,20 @@
import roundBigNumberFilter from 'client/filters/roundBigNumber';
describe('round big number filter', () => {
it('can round a decimal number', () => {
expect(roundBigNumberFilter(4.567)).to.equal(4.57);
expect(roundBigNumberFilter(4.562)).to.equal(4.56);
});
it('can round thousands', () => {
expect(roundBigNumberFilter(70065)).to.equal('70.1k');
});
it('can round milions', () => {
expect(roundBigNumberFilter(10000987)).to.equal('10.0m');
});
it('can round bilions', () => {
expect(roundBigNumberFilter(1000000000)).to.equal('1.0b');
});
});

View File

@@ -1,5 +1,6 @@
import groupsUtilities from 'client/mixins/groupsUtilities';
import { TAVERN_ID } from 'common/script/constants';
import generateStore from 'client/store';
import Vue from 'vue';
describe('Groups Utilities Mixin', () => {
@@ -7,6 +8,7 @@ describe('Groups Utilities Mixin', () => {
before(() => {
instance = new Vue({
store: generateStore(),
mixins: [groupsUtilities],
});

View File

@@ -121,6 +121,17 @@ describe('achievements', () => {
});
});
it('card achievements exist with counts', () => {
let cardTypes = ['greeting', 'thankyou', 'birthday', 'congrats', 'getwell'];
cardTypes.forEach((card) => {
let cardAchiev = basicAchievs[`${card}Cards`];
expect(cardAchiev).to.exist;
expect(cardAchiev).to.have.property('optionalCount')
.that.is.a('number');
});
});
it('rebirth achievement exists with no count', () => {
let rebirth = basicAchievs.rebirth;
@@ -174,7 +185,7 @@ describe('achievements', () => {
});
it('card achievements exist with counts', () => {
let cardTypes = ['greeting', 'thankyou', 'nye', 'valentine', 'birthday'];
let cardTypes = ['nye', 'valentine'];
cardTypes.forEach((card) => {
let cardAchiev = seasonalAchievs[`${card}Cards`];

View File

@@ -13,6 +13,12 @@ describe('shops', () => {
expect(shopCategories.length).to.be.greaterThan(2);
});
it('does not contain an empty category', () => {
_.each(shopCategories, (category) => {
expect(category.items.length).to.be.greaterThan(0);
});
});
it('does not duplicate identifiers', () => {
let identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));

File diff suppressed because it is too large Load Diff

View File

@@ -2,8 +2,8 @@
## Babel Paths for Production Environment
In development, we [transpile at server start](https://github.com/HabitRPG/habitrpg/blob/1ed7e21542519abe7a3c601f396e1a07f9b050ae/website/server/index.js#L6-L8). This allows us to work quickly while developing, but is not suitable for production. So, in production we transpile the server code before the app starts.
In development, we [transpile at server start](https://github.com/HabitRPG/habitica/blob/1ed7e21542519abe7a3c601f396e1a07f9b050ae/website/server/index.js#L6-L8). This allows us to work quickly while developing, but is not suitable for production. So, in production we transpile the server code before the app starts.
This system means that requiring any files from `common/script` in `website/server/**/*.js` must be done through the `common/index.js` module. In development, it'll pass through to the pre-transpiled files, but in production it'll point to the transpiled versions. If you try to require or import a file directly, it will error in production as the server doesn't know what to do with some es2015isms (such as the import statement).
This system means that requiring any files from `website/common/script` in `website/server/**/*.js` must be done through the `website/common/index.js` module. In development, it'll pass through to the pre-transpiled files, but in production it'll point to the transpiled versions. If you try to require or import a file directly, it will error in production as the server doesn't know what to do with some es2015isms (such as the import statement).
This test just verifies that none of the files in the server code are calling the common files directly.

View File

@@ -10,7 +10,7 @@ module.exports = {
index: path.resolve(__dirname, '../../dist-client/index.html'),
assetsRoot: path.resolve(__dirname, '../../dist-client'),
assetsSubDirectory: 'static',
assetsPublicPath: '/new-app',
assetsPublicPath: '/new-app/',
staticAssetsDirectory,
productionSourceMap: true,
// Gzip off by default as many popular static hosts such as

View File

@@ -17,21 +17,31 @@ const baseConfig = {
path: config.build.assetsRoot,
publicPath: IS_PROD ? config.build.assetsPublicPath : config.dev.assetsPublicPath,
filename: '[name].js',
devtoolModuleFilenameTemplate (info) {
// Fix source maps, code from
// https://github.com/Darkside73/bbsmile.com.ua/commit/3596d3c42ef91b69d8380359c3e8908edc08acdb
let filename = info.resourcePath;
if (info.resource.match(/\.vue$/) && !info.allLoaders.match(/type=script/)) {
filename = 'generated';
}
return filename;
},
},
resolve: {
extensions: ['*', '.js', '.vue', '.json'],
modules: [
path.join(__dirname, '..', 'website'),
path.join(__dirname, '..', 'test/client/unit'),
path.join(__dirname, '..', 'node_modules'),
path.join(projectRoot, 'website'),
path.join(projectRoot, 'test/client/unit'),
path.join(projectRoot, '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'),
website: path.resolve(projectRoot, 'website'),
common: path.resolve(projectRoot, 'website/common'),
client: path.resolve(projectRoot, 'website/client'),
assets: path.resolve(projectRoot, 'website/client/assets'),
components: path.resolve(projectRoot, 'website/client/components'),
},
},
plugins: [
@@ -63,11 +73,17 @@ const baseConfig = {
{
test: /\.js$/,
loader: 'babel-loader',
include: projectRoot,
exclude: /node_modules/,
include: [
path.join(projectRoot, 'test'),
path.join(projectRoot, 'website'),
path.join(projectRoot, 'node_modules', 'bootstrap-vue'),
],
options: {
cacheDirectory: true,
},
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
test: /\.(png|jpe?g|gif)(\?.*)?$/,
loader: 'url-loader',
query: {
limit: 10000,
@@ -82,6 +98,22 @@ const baseConfig = {
name: utils.assetsPath('fonts/[name].[hash:7].[ext]'),
},
},
{
test: /\.svg$/,
use: [
{ loader: 'svg-inline-loader' },
{ loader: 'svgo-loader' },
],
exclude: [path.resolve(projectRoot, 'website/client/assets/svg/for-css')],
},
{
test: /\.svg$/,
use: [
{ loader: 'svg-url-loader' },
{ loader: 'svgo-loader' },
],
include: [path.resolve(projectRoot, 'website/client/assets/svg/for-css')],
},
],
},
};

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -1,9 +1,9 @@
/* Comment out for holiday events */
.npc_ian {
/* .npc_ian {
background: url("/npc_ian.gif") no-repeat;
width: 78px;
height: 135px;
}
} */
.quest_burnout {
background: url("/quest_burnout.gif") no-repeat;
@@ -61,6 +61,10 @@
padding-right: 0.5em;
}
.achievement-container {
height: 52px;
}
[class*="Mount_Head_"],
[class*="Mount_Body_"] {
margin-top:18px; /* Sprite accommodates 105x123 box */

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