Compare commits

...

127 Commits

Author SHA1 Message Date
Sabe Jones
ecd55b6f5c 3.111.0 2017-08-17 20:39:31 +00:00
Sabe Jones
31e7882c51 chore(i18n): update locales 2017-08-17 20:37:05 +00:00
SabreCat
c831a05b9b feat(content): Mystery Items August 2017 2017-08-17 20:27:48 +00:00
Keith Holliday
4929a2dd79 Remove catch from cron (#8963)
* Removed extra logging

* Removed second extra logger

* Removed extra import
2017-08-17 13:42:41 -06:00
Sabe Jones
1a99380a53 3.110.3 2017-08-15 22:41:34 +00:00
Sabe Jones
3a28aba986 chore(i18n): update locales 2017-08-15 22:41:17 +00:00
SabreCat
42f86ff62b chore(news): Bailey 2017-08-15 22:33:54 +00:00
Sabe Jones
4ba97755a5 3.110.2 2017-08-11 20:03:14 +00:00
Sabe Jones
779ac3d4ab chore(i18n): update locales 2017-08-11 20:00:00 +00:00
Sabe Jones
32c3a3886f Merge branch 'develop' into release 2017-08-11 19:53:40 +00:00
SabreCat
99465d5995 chore(sprites): compile 2017-08-11 16:09:11 +00:00
Sabe Jones
df0dbaba63 feat(sprites): icons for backgrounds (#8939) 2017-08-11 10:07:17 -05:00
Sabe Jones
d1e9a6a74a fix(style): background different from color (#8942) 2017-08-11 15:08:24 +02:00
Sabe Jones
60a5599db4 fix(pets): better adjectives 2017-08-10 23:20:01 +00:00
Sabe Jones
68b19c931b 3.110.1 2017-08-10 23:18:37 +00:00
Sabe Jones
07d2699898 fix(quest): correct hippo egg unlock condition 2017-08-10 23:18:19 +00:00
Sabe Jones
f5ea24b4e6 fix(subscriptions): Wording change for Gold:Gems 2017-08-10 22:59:25 +00:00
Sabe Jones
9507f0758d Merge branch 'release' into develop 2017-08-10 19:59:24 +00:00
Sabe Jones
58694f46e0 3.110.0 2017-08-10 19:56:00 +00:00
Sabe Jones
a7351b0082 fix(Bailey): missing credits 2017-08-10 19:55:44 +00:00
Sabe Jones
2881566aed chore(i18n): update locales 2017-08-10 19:54:04 +00:00
SabreCat
6d350d5974 chore(sprites): compile 2017-08-10 19:46:17 +00:00
SabreCat
2e8da50b3e feat(content): Hippo Pet Quest 2017-08-10 19:45:30 +00:00
SabreCat
1985d04bcf fix(stable): missing label on standard pets 2017-08-09 21:12:08 +00:00
Sabe Jones
8d040873a1 fix(columns): task heading grammer (#8932) 2017-08-09 13:58:59 -07:00
Keith Holliday
5995dd235d New client more updates (#8934)
* Added api token to page

* Fixed wiki link

* Added categoires

* Removed extra create challenge button. Add prize model and user balance deduction

* Added pending filter

* Added member sort

* Added confirmation for leaving

* Filtered tavern

* Added redirect to newly created guild

* Made guild links routerlinks

* Fixed wiki link and added fetch recent messages

* Show backgrounds only on edit. Fixed glasses equip

* Added link to register page

* Added yesterdailies

* Added achievement footer

* Update guild badges

* Added avatar to achievement avatar component

* More guild crests updates

* Achievement footer and avatar added

* Added notification read

* Removed duplicate string
2017-08-09 10:56:48 -06:00
Matteo Pagliazzi
f57c647e21 Client Tasks v3 (#8926)
* tasks hover state

* hide column background if task too close

* wip edit tasks

* wip: replace tags

* upgrade bootstrap-vue and fix creare btn for tasks

* difficulty options colors and active label fixes

* fix tags
2017-08-09 17:13:40 +02:00
Sabe Jones
8d82566654 Merge branch 'release' into develop 2017-08-08 20:59:11 +00:00
Sabe Jones
7ff4a72d77 3.109.0 2017-08-08 20:58:38 +00:00
Sabe Jones
41d04b0f18 chore(i18n): update locales 2017-08-08 20:58:18 +00:00
SabreCat
c7c1ff816c chore(sprites): compile 2017-08-08 20:49:57 +00:00
SabreCat
b018f9cf90 feat(content): Ember Hatching Potions 2017-08-08 20:48:59 +00:00
negue
a380090013 [WIP] multiple fixes (#8916)
* change quest banner backgrond

* itemRows in inventory

* use itemRows in inventory/items - showLess/showMore as default labels - extend white space if theres no button available

* hide popover if dragging is active - show dragging info on first click (without to move)

* use itemRows in inventory/stable

* fix some strings

* highlight currently dragging item in inventory/items - auto attach info on click - z-index

* fix shopItem label color

* fix floating npcs in banner

* hatched-pet-dialog in items / stable

* change all ctx to context
2017-08-07 19:04:46 -06:00
Keith Holliday
0b076311df New client misc with some more misc (#8929)
* Added markdown

* Added styles and option for debug menu

* Added sm icons

* Began styling autocomplete

* Added autocomplete styles

* Added more challenge categories

* Updated challenge participants modal

* Fixed challenge list updating without reload

* Added close and delete challenge

* Fixed form placeholder, adjusted desc style and fixed create button style

* Fixed faq collapsing and style

* Fixed repeating ending

* Fixed delete account

* Fixed party fetch issue

* Fixed scope issue

* Added member count filters

* Fixed create button style

* Fixed badge color display

* Updated tavern styles

* Fixed some party styles

* Updated login styles

* Fixed login redirect

* Fixed initial login process

* Added done local
2017-08-07 14:26:17 -06:00
Alys
1896984777 adjust banned words. TRIGGER / CONTENT WARNING: assault, slurs, swearwords, etc 2017-08-05 17:19:15 +10:00
Matteo Pagliazzi
c3ba70f5d6 Tasks scoring and misc fixes (#8925)
* wip: add task scoring and persist checklist items

* remove unused files, fix checklist scoring and start adding support for groups tasks

* amke group and challenge tasks not scoreable
2017-08-04 23:27:11 +02:00
Sabe Jones
ac800a94f9 Merge branch 'release' into develop 2017-08-03 21:46:41 +00:00
Sabe Jones
9b2b9ef54d 3.108.0 2017-08-03 21:46:15 +00:00
Sabe Jones
3785a87221 chore(i18n): update locales 2017-08-03 21:43:16 +00:00
SabreCat
8c5d4ca190 chore(sprites): compile 2017-08-03 21:22:24 +00:00
SabreCat
ab6e77dd9a feat(content): Armoire and BGs 2017-08
Also ends the Summer Splash event.
2017-08-03 21:21:12 +00:00
Keith Holliday
75913842bc New client misc for days (#8924)
* Removed sticky header

* Fixed group desc and information

* Add flag modal

* Fixed chat sync errors

* Fixed balance display

* Fixed key and close issue

* Updated tavern placeholder

* Added and fixed links

* Removed open user modal from clicking menu

* Added better app loading check

* Removed banner from party

* Allowed for nav when clicking the card

* Fixed member display

* Updated create challenge modal to populate list and to create party/public

* Display members modal

* Added fetch recent messages
2017-08-03 14:04:03 -06:00
Keith Holliday
e61884ed08 New client tour (#8921)
* Linted tour. Added intro tour

* Added initial tours

* Fixed page number for intro

* Lint fix

* Updated shrinkwrap

* Removed bootstrap tour

* Lint fix
2017-08-02 20:15:00 -06:00
borisabramovich86
026014b8d6 lists banned words in the chat error message - fixes https://github.com/HabitRPG/habitica/issues/8812 (#8858)
* issue 8812 - added the list of bad words matched to the postChat error message.

* issue 8812 - added the list of bad words matched to the postChat error message.

* issue 8812 - some refactoring, fixed relevant tests, and lint rules refactor

* small fix for unnecessary empty array

* added test and did some small refactoring

* lint error fix

* issue 8812 - added the list of bad words matched to the postChat error message.

* issue 8812 - some refactoring, fixed relevant tests, and lint rules refactor

* small fix for unnecessary empty array

* added test and did some small refactoring

* lint error fix

* add test to check the error message contains the banned words used

* improve banned words test

* issue 8812 - added the list of bad words matched to the postChat error message.

* issue 8812 - some refactoring, fixed relevant tests, and lint rules refactor

* small fix for unnecessary empty array

* added test and did some small refactoring

* lint error fix

* issue 8812 - added the list of bad words matched to the postChat error message.

* issue 8812 - some refactoring, fixed relevant tests, and lint rules refactor

* add test to check the error message contains the banned words used

* improve banned words test

* merge with develop - aligned banned slurs check with banned words check
2017-08-02 12:43:22 -07:00
MathWhiz
014a7197f0 Update Website's Task Page image (#8791)
* Update Website's Task Page image

* update zip
2017-08-02 12:27:21 -07:00
Andrew Levenson
1ad3292f18 Cleaned up variable test/initialization (#8904) 2017-08-02 12:22:35 -07:00
Ben Harvill
add2743772 fix dockerfiles to not require entering container to run app (#8911) 2017-08-02 12:11:25 -07:00
Keith Holliday
cf0ce90968 New client challenge tasks (#8915)
* Added get and create challenge tasks

* Added challenge task edit
2017-08-02 10:57:57 -06:00
Imtiaz Ahmed
e3b10cdc2a Updated tip #27 (#8895)
* Updated tip #27

* fix(locales): leave non-base locales for Transifex
2017-08-02 08:41:10 -07:00
Sabe Jones
3ab4c4114b fix(groups): bail if group not passed to canEdit (#8897) 2017-08-02 08:38:30 -07:00
Sabe Jones
576285c004 fix(habits): reset counters when sleeping (#8898) 2017-08-02 08:35:20 -07:00
Keith Holliday
d3967d6567 Removed unneeded tests (#8912) 2017-08-01 20:19:51 -06:00
Keith Holliday
ea25a4bf04 Updated shrinkwrap (#8908)
* Updated shrinkwrap

* Updated shrinkwrap
2017-08-01 18:10:33 -06:00
Sabe Jones
4f26ac66ac Merge branch 'release' into develop 2017-08-01 22:06:57 +00:00
Sabe Jones
f01ca1f9be 3.107.1 2017-08-01 22:02:46 +00:00
Sabe Jones
b51f622c52 chore(event): end Aquatic Potions 2017-08-01 22:02:31 +00:00
Keith Holliday
0dba37008f New client popups profile andmore (#8907)
* Added more styles to user profile modal and replaced memberDetail

* Added notify library

* Added edit avator

* Added notification menu updates

* Fixed lint issues

* Added group invite functionality

* Added many achievement modals

* Added initial quest modals

* Added guild, drops, and rebirth modals

* Added the reset of the achievement modals and fixed lint
2017-08-01 12:52:49 -06:00
Matteo Pagliazzi
bca52cb6fa Client Tasks (#8894)
* fix filter button style

* display completed todos

* fix reward control position

* begin to add edit modal

* start adding settings to edit modal

* add task saving, creating and deleting

* fixes

* add tags and repeat frequency for habits

* clicking on links should not open the edit modal

* checklist editing

* repeatables and checklists

* delete checklist items

* add rewards price

* update shrinkwrap

* pin cwait
2017-08-01 14:30:17 +02:00
negue
ade6d9689f new client - quest / seasonal / time travelers shops (#8903)
* initial quests.vue - refactorings - add group to quests

* shows quests by quest-group

* buyQuestModal with rewards sidebar

* store / actions to load seasonal/time-travelers shop data

* buyModal buyPressed instead of buyAction - seasonal shop categories now with specialClass property - seasonal shop

* time travelers vue - show hourglass in shopItem / buyDialog - fix banners

* cleanup

* show amount of already owned quests

* show html notes in popovers / dialog

* extract purchase-api to common.ops.purchaseWithSpell to call the same in the store / update the UI on purchases

* add time-travelers sprites

* fix lint

* add last mystery set images

* remove unused Page

* remove equipment from newClient.json
2017-07-31 17:04:40 -06:00
Sabe Jones
90f7390f84 Merge branch 'release' into develop 2017-07-31 20:35:25 +00:00
Sabe Jones
5d6e8c8729 3.107.0 2017-07-31 20:34:57 +00:00
Sabe Jones
4659b1cc5c chore(i18n): update locales 2017-07-31 20:33:55 +00:00
SabreCat
06c58bfae2 chore(sprites): compile 2017-07-31 20:23:19 +00:00
SabreCat
568e8840ed feat(event): Naming Day 2017 2017-07-31 20:22:09 +00:00
Keith Holliday
ffe46c0f07 Many updates on our large list (#8905)
* Many updates on our large list

* Added footer debug functions
2017-07-31 13:54:52 -06:00
Sabe Jones
aad6130b21 3.106.1 2017-07-30 20:37:17 +00:00
Alys
88d48f1e5d adjust slurs / banned words. TRIGGER / CONTENT WARNING: assault, slurs, swearwords, etc 2017-07-31 06:26:16 +10:00
Sabe Jones
413626a971 3.106.0 2017-07-30 16:15:45 +00:00
Sabe Jones
26f39c9db6 chore(news): Last Chance Bailey 2017-07-30 16:13:58 +00:00
Keith Holliday
c5e0bcfb0e New client more misc (#8902)
* View party now opens member modal

* Clicking member in header opens member detail modal

* Began sticky header

* Added sleep

* Removed extra inbox and added name styles

* Lint fixes

* Added member filter

* Added task counts

* Updated quest start modal

* Updated members modal style

* Fixed editing party

* Updated tavern

* Updated my guilds

* More guild styles

* Many challenge styles and fixes

* Fixed notification menu display

* Added initial styles to groupplans

* Added syncing with inbox

* Fixed lint

* Added new edit profile layout

* Added initial achievement layout

* Began adding new stats layout

* Removed duplicate:

* fix(CI): attempt to address Travis Mongo connection issue

* fix(CI): don't strand us in Mongo shell

* Travis updates

* Try percise
2017-07-29 16:08:36 -06:00
Sabe Jones
c6c0e3660b Merge branch 'release' into develop 2017-07-27 21:13:43 +00:00
Sabe Jones
d00131fc4a 3.105.2 2017-07-27 21:12:40 +00:00
Sabe Jones
f7220e7e8c feat(locales): add Turkish language 2017-07-27 21:12:20 +00:00
Sabe Jones
bf6dde6e63 3.105.1 2017-07-27 21:08:46 +00:00
Sabe Jones
c3024e5e58 chore(i18n): update locales 2017-07-27 21:06:15 +00:00
SabreCat
ca8b7f6b67 fix(sprites): jellyfish arms
Also adds a Bailey announcement.
2017-07-27 20:58:49 +00:00
Sabe Jones
521077ed4f Explicitly configure HTTP request for PayPal (#8896)
* fix(payments): explicitly configure HTTP request

* refactor(req): return $http call

* fix(payments): inject and open $window
2017-07-27 11:31:58 -07:00
negue
f72f71fd32 [WIP] New Client - Shops/Market (#8884)
* initial market - routing - store - load market data

* move drawer/drawerSlider / count/star badge to components/ui

* filter market categories

* shopItem with gem / gold

* show count of purchable items

* show count of purchable itemsshow drawer with currently owned items + DrawerHeaderTabs-Component

* show featured gear

* show Gear - filter by class - sort by (type, price, stats) - sort market items

* Component: ItemRows - shows only the max items in one row (depending on the available width)

* Sell Dialog + Balance Component

* generic buy-dialog / attributes grid with highlight

* buyItem - hide already owned gear

* filter: hide locked/pinned - lock items if not enough gold

* API: Sell multiple items

* show avatar in buy-equipment-dialog with changed gear

* market banner

* misc fixes

* filter by text

* pin/unpin gear store actions

* Sell API: amount as query-parameter

* Update user.js

* fixes

* fix sell api amount test

* add back stroke/fill currentColor

* use scss variables
2017-07-27 19:41:23 +02:00
Matteo Pagliazzi
18b04e713e Revert "chore(i18n): update locales"
This reverts commit 6754c43317.
2017-07-27 17:41:23 +02:00
Matteo Pagliazzi
6754c43317 chore(i18n): update locales 2017-07-27 17:35:14 +02:00
Keith Holliday
0b13ba822e New client group finishes (#8899)
* Added challenges section

* Added public fields to guilds

* Added suggestion for habitica help guild

* Added categoires to group

* Added guild category filters

* Added guild filter by member count

* Removed console.log

* Updated group count in tests to account for newly created groups
2017-07-26 09:05:13 -06:00
Sabe Jones
9071fa0073 Merge branch 'release' into develop 2017-07-25 23:25:11 +00:00
Sabe Jones
7a5f01d516 3.105.0 2017-07-25 23:24:48 +00:00
Sabe Jones
0f3f54548d chore(i18n): update locales 2017-07-25 23:22:54 +00:00
SabreCat
c84dc40c7d feat(content): Subscriber items 2017-07 2017-07-25 23:15:06 +00:00
Keith Holliday
16b244d5c6 New client random catchup (#8891)
* Added initial challenge pages

* Added challenge item and find guilds page

* Added challenge detail

* Added challenge modals

* Ported over challenge service code

* Ported over challenge ctrl code

* Added styles and column

* Minor modal updates

* Removed duplicate keys

* Fixed casing

* Added initial chat component

* Added copy as todo modal

* Added sync

* Added chat to groups

* Fixed lint

* Added notification service

* Added tag services

* Added notifications

* Added hall

* Added analytics

* Added http interceptor

* Added initial autocomplete

* Added initial footer component

* Began coding and designing footer

* Added inital hall

* Ported over inital group plan ctrl code

* Added initial invite modal

* Added initial member detail modal

* Added initial notification menu

* Ported over inital notification code

* Fixed import line

* Fixed autocomplete import casing
2017-07-25 08:24:40 -06:00
Matteo Pagliazzi
86a07a4949 fix missing comma in json 2017-07-22 20:38:55 +02:00
Matteo Pagliazzi
31bbac1751 Client Tasks (#8889)
* tasks: markdown style, checkboxes. Misc fixes

* add filtering to tasks

* client tasks: complete filtering
2017-07-22 20:30:08 +02:00
Sabe Jones
e6dd0d5e82 Delete Account with Social Auth (#8796)
* feat(accounts): delete social accts

* test(integration): social auth delete
2017-07-21 10:55:53 -07:00
Alys
88fece1422 change Contact Us page to specify the Report a Bug guild instead of GitHub (#8877) 2017-07-21 10:49:00 -07:00
Keith Holliday
ecc18fc093 New client chat (#8890)
* Added initial challenge pages

* Added challenge item and find guilds page

* Added challenge detail

* Added challenge modals

* Ported over challenge service code

* Ported over challenge ctrl code

* Added styles and column

* Minor modal updates

* Removed duplicate keys

* Fixed casing

* Added initial chat component

* Added copy as todo modal

* Added sync

* Added chat to groups

* Fixed lint
2017-07-21 11:00:36 -06:00
Keith Holliday
0a59b8e85b [WIP] New client challenges (#8842)
* Added initial challenge pages

* Added challenge item and find guilds page

* Added challenge detail

* Added challenge modals

* Ported over challenge service code

* Ported over challenge ctrl code

* Added styles and column

* Minor modal updates

* Removed duplicate keys

* Fixed casing
2017-07-20 14:52:46 -06:00
Keith Holliday
d677f5cfc7 New client statics (#8885)
* Moved static files over to new client

* Added statics, fixed translations and update styles

* More style and vue fixes

* Fixed line endings

* Fixed new stuff converasion and help links
2017-07-20 12:20:53 -06:00
Keith Holliday
88f872ed50 New client settings (#8886)
* Added initial settings page

* Initial cleanup and translations

* Ported api settings

* Ported promocode settings

* POrted notifications code

* Fixed styles and translatins for site page

* Ported over rest of settings functions

* Ported payments over

* Initial lint clean up

* Added amazon modal

* Added stripe

* Added site settings
2017-07-20 12:01:00 -06:00
CJ
605391e4e7 Fixes #7958 - do not remove Battle Gear equipment when changing class (#8064)
* Changed files to fix Bug 7958:
 - website/client-old/.../userCtrl.js#38: removed to keep inventory constant
 - website/common/.../changeClass.js#33:  removed to stop 'classes' introduction

* Adjustments following Bug Review
 - Removed remaining 'foundKey' logic
 - Adjusted test logic to reflect feature change

* Reverting userCtrl.js to development version
 - Reintroduces "classes" Guide tour

* New version of Fixes #7958
 - Changed logic to only notify user the first time they choose a class
 - Changed message to represent this change in logic
 - #LINT: Cleaned interface for changing class
    - New method: enableClasses() -- because, really, should we be calling User.changeClass({}) from the UX?
    - New method: payForNewClass() -- handles prompting the user to confirm that they want to change class

* Remove new User Flag, use flags.tour.classes

* Whoopsie. Fix PR conflict.

* Changed files to fix Bug 7958:
 - website/client-old/.../userCtrl.js#38: removed to keep inventory constant
 - website/common/.../changeClass.js#33:  removed to stop 'classes' introduction

* Adjustments following Bug Review
 - Removed remaining 'foundKey' logic
 - Adjusted test logic to reflect feature change

* Reverting userCtrl.js to development version
 - Reintroduces "classes" Guide tour

* New version of Fixes #7958
 - Changed logic to only notify user the first time they choose a class
 - Changed message to represent this change in logic
 - #LINT: Cleaned interface for changing class
    - New method: enableClasses() -- because, really, should we be calling User.changeClass({}) from the UX?
    - New method: payForNewClass() -- handles prompting the user to confirm that they want to change class

* Remove new User Flag, use flags.tour.classes

* Whoopsie. Fix PR conflict.

* Removed Extraneous Flag

* Removed Extraneous Flag

* Changed files to fix Bug 7958:
 - website/client-old/.../userCtrl.js#38: removed to keep inventory constant
 - website/common/.../changeClass.js#33:  removed to stop 'classes' introduction

* New version of Fixes #7958
 - Changed logic to only notify user the first time they choose a class
 - Changed message to represent this change in logic
 - #LINT: Cleaned interface for changing class
    - New method: enableClasses() -- because, really, should we be calling User.changeClass({}) from the UX?
    - New method: payForNewClass() -- handles prompting the user to confirm that they want to change class

Remove new User Flag, use flags.tour.classes

Whoopsie. Fix PR conflict.

Removed Extraneous Flag

* Fixes handling architecture change

* Updates following Review 20170418-0602

* Remove cause of mocha/no-exclusive-tests lint failure
2017-07-20 10:28:53 -07:00
Sabe Jones
ca90d88289 Merge branch 'release' into develop 2017-07-20 16:10:12 +00:00
Sabe Jones
a4951c6478 3.104.1 2017-07-20 16:08:27 +00:00
Matteo Pagliazzi
95285cd85a do not send password to loggly (#8887) 2017-07-20 15:07:38 +02:00
Andrew Schultz
1a03f8d7ae 3 typos plus a question (#8866) 2017-07-19 18:51:39 -07:00
Andrew Schultz
5d0cbd2456 Stoikalm was missing umlaut in 2 game text places (#8867) 2017-07-19 18:50:31 -07:00
Mateus Etto
cdc8473f60 Allow Multiple Invites to Party (#8683)
* (server) Add parties array to store invites

* (server) Lint files

* Update joinGroup, rejectGroupInvite, _inviteByUUID, and remove clearPartyInvitation.js

* Update user schema: detailed 'invitations.parties' attributes

* Code improvement and do not let invite twice

* Check if the user is already invited earlier in the code

* Added message to invitation page, and show all invitations

* Added join party confirmation alert

* Small fixes

* Created test: allow inviting a user to 2 different parties

* Updated tests

* Update invitations.parties on more places

* Small adjustments

* Updates on invitations.party references

* Show all invitations when user is already in a party

* Fixed notifications counter

* Update both 'party' and 'parties' at _handleGroupInvitation

* Updated a test

* Fixed small mistake at _handleGroupInvitation

* More test update

* Update invitation.party when removing single invite and small adjust at view
2017-07-19 18:45:28 -07:00
Kevin Smith
11a4c1c95d Implemented new Achievement and Badge: Invited a Friend (Fixes #8615) (#8819)
* Added text to locale

* Added achievement to content and libs

* Added achievement modal

* Added achievement to notification model and controller

* Added achievement to user schema

* Grant achievement to inviter when user registers using emailed link

* Fix icon name

* Added integration test

* Fix linting

* Added sprite
2017-07-19 18:39:39 -07:00
SabreCat
625b159880 Merge branch 'sabrecat/travis-mongodb' into develop 2017-07-19 23:14:00 +00:00
SabreCat
bf8b2db6b3 fix(Travis): explicitly start Mongo 2017-07-19 22:50:57 +00:00
SabreCat
83a1b9c34e chore(words): reclassify some words as slurs
Also moves bannedSlurs.js to the same directory as bannedWords.js.
2017-07-19 21:41:08 +00:00
Alyssa Batula
c350665076 Automatically mute users who attempt to post a slur, fixes #8062 (#8177)
* Initial psuedo-code for checking for slurs in messages

* Initial working prototype for blocking posting of slurs. Moved check from group.js to the chat api. Still needs: to permanently revoke chat privileges, to notify the moderators, a better method for checking for the blacklisted words, and a way to get the real list of words to check.

* Permanently revoke chat privileges when attempting to post a slur.

* Removed console logs

* Fixing rebase

* Do not moderate private groups

* Moved slur check to a generic check for banned words function

* Moved list of slurs to a separate file, fixed misplacement of return in ContainsBannedWords() function

* Slurs are blocked in both public and private groups

* Added code to send a slack message for slurs

* Fixed formatting issues

* Incorporated tectContainsBannedWords() function from PR 8197, added an argument to specify the list of banned words to check

* Added initial tests for blocking slurs and revoking chat priviliges

* Uncommented line to save revoked privileges

* Check that privileges are revoked in private groups

* Moved code to email/slack mods to chat api file

* Switched to BadRequest instead of NotFound error

* Restore chat privileges after test

* Using official placeholder slur

* Fixed line to export sendSubscriptionNotification function for slack

* Replaced muteUser function in user methods with a single line in the chat controller file

* Reset chatRevoked flag to false in a single line

* Switched method of setting chatRevoked flag so that it is updated locally and in the database

* First attempt at the muteUser function: revokes user's chat privileges and notifies moderators

* Manual merge for cherry-pick

* Initial working prototype for blocking posting of slurs. Moved check from group.js to the chat api. Still needs: to permanently revoke chat privileges, to notify the moderators, a better method for checking for the blacklisted words, and a way to get the real list of words to check.

* Permanently revoke chat privileges when attempting to post a slur.

* Removed console logs

* Created report to be sent to moderators via email

* Do not moderate private groups

* Moved slur check to a generic check for banned words function

* Moved list of slurs to a separate file, fixed misplacement of return in ContainsBannedWords() function

* Slurs are blocked in both public and private groups

* Added code to send a slack message for slurs

* Fixed formatting issues

* Incorporated tectContainsBannedWords() function from PR 8197, added an argument to specify the list of banned words to check

* Added initial tests for blocking slurs and revoking chat priviliges

* Uncommented line to save revoked privileges

* Check that privileges are revoked in private groups

* Moved code to email/slack mods to chat api file

* Switched to BadRequest instead of NotFound error

* Restore chat privileges after test

* Using official placeholder slur

* Fixed line to export sendSubscriptionNotification function for slack

* Replaced muteUser function in user methods with a single line in the chat controller file

* Reset chatRevoked flag to false in a single line

* Switched method of setting chatRevoked flag so that it is updated locally and in the database

* Removed some code that got re-added after rebase

* Tests for automatic slur muting pass but are incomplete (do not check that chatRevoked flag is true)

* Moved list of banned slurs to server side

* Added warning to bannedSlurs file

* Test chat privileges revoked when posting slur in public chat

* Fix issues left over after rebase (I hope)

* Added code to test for revoked chat privileges after posting a slur in a private group

* Moved banned slur message into locales message

* Added new code to check for banned slurs (parallels banned words code)

* Fixed AUTHOR_MOTAL_URL in sendTxn for slur blocking

* Added tests that email sent on attempted slur in chat post

* Created context for slur-related-tests, fixed sandboxing of email. Successfully tests that email.sendTxn is called, but the email content test fails

* commented out slack (for now) and cleaned up tests of sending email

* Successfully tests that slur-report-to-mods email is sent

* Slack message is sent, and testing works, but some user variables seem to only work when found in chat.js and passed to slack

* Made some fixes for lint, but not sure what to do about the camel case requirement fail, since that's how they're defined in other slack calls

* Slack tests pass, skipped camelcase check around those code blocks

* Fixed InternalServerError caused by slack messaging

* Updated chat privileges revoked error

* fix(locale): typo correction
2017-07-19 14:06:15 -07:00
Matteo Pagliazzi
89ee8b1648 Client Tasks (#8883)
* client tasks: fix styles and add markdown rendering

* more style fixes

* client: tasks fixes
2017-07-19 21:02:40 +02:00
Sabe Jones
75680ab6aa Merge branch 'release' into develop 2017-07-19 18:41:13 +00:00
Sabe Jones
116ec0f9d9 3.104.0 2017-07-19 18:38:43 +00:00
Sabe Jones
a89633d17b chore(i18n): update locales 2017-07-19 18:37:11 +00:00
SabreCat
2e3dd27414 chore(sprites): compile 2017-07-19 18:25:14 +00:00
SabreCat
3af756a90d feat(cards): Good Luck card and achievement 2017-07-19 18:24:10 +00:00
jerellmendoodoo
a9195f0d96 Fixed release pets mounts (#8545)
* Fixed release pets/mounts achievements when fully earned and added unit tests for these changes

* Fixed release pets/mounts achievements to award only when fully earned and added unit tests for these changes, also fixed linting issues

* Updated variable assignments to make more readable

* Revised releaseBoth/Pets/Mounts to include null or undefined checks, also updated unit tests

* fixed integration tests
2017-07-18 13:34:54 -07:00
Oscar Rendón
ab777f7006 Restrict users from getting back from death (#8808)
* Add test to prevent death users recovering health

* Add check for buying potions with zero health

* Validate hp <= 0 to take boss damage into account
2017-07-18 13:26:10 -07:00
Grayson Gilmore
d918bc9f56 Add tests to increase coverage on /website/server/controllers/api-v3/auth.js (#8809)
* Add tests covering branches in _handleGroupInvitation

* Remove .only from latest test
2017-07-18 13:21:34 -07:00
Sabe Jones
4a89ca3e11 Merge branch 'develop' into fix-leave-challenges 2017-07-18 20:14:47 +00:00
joe-salomon
cdbbf93b74 Weekly/Monthly Habit reset counters resetting early - fixes #8570 (#8749)
* For habit reset logic, changed day check calculation to use user’s timezone instead of server time.
Added unit tests to check following cases:
- Weekly habit reset: Server tz is Sunday, User tz is Monday
- Weekly habit reset: Server tz is Monday, User tz is Sunday
- Monthly habit reset: Server tz is 1st of month, User tz is 2nd of month
- Monthly habit reset: Server tz is end of prev month, User tz is 1st of month

* use moment().zone() instead of utcOffset()

* typo

* Fixed check for daysMissed, added logic for CDS
Added test for CDS, fixed previous tests
2017-07-18 12:53:39 -07:00
Keith Holliday
d822843bbf [WIP] New client userpages (#8868)
* Added background page

* Added stats

* Added achievements

* Added profile
2017-07-17 22:18:17 -06:00
negue
ea3ed26f42 Stable minor fixes (#8861)
* fix dialog text + close after hatching

* Update newClient.json
2017-07-18 01:04:34 +02:00
Pavel Pletenev
0da1144635 Make responce codes uniform (#8865)
* Fix 201 responce wrong documentation

* Fix 201 in challenges

* Fix 201 in groups.js

* Fix 201 in tags.js

* Fix 201 in webhooks.js
2017-07-17 14:28:25 -07:00
Alys
5f3539da19 make Bailey correctly explain about Aquatic Friends achievement badge (#8853)
* make Bailey correctly explain about Aquatic Friends achievement badge

It's this error again: https://github.com/HabitRPG/habitica/issues/8658

From katzalina in the Report a Bug guild:
"not a priority: Re:Bailey's news 07/05/ it says: both you and you
friend get the aquatic friends badge - I splashed a party member
and did not get the badge (didn't expect to either, until reviewing
the news). Am I experiencing a bug or has there been a systematic
change and the news text doesn't reflect it, yet? Thanks"

* the incorrect message was in an earlier Bailey as well
2017-07-17 14:16:31 -07:00
Sabe Jones
e4d006e5cd 3.103.0 2017-07-16 16:25:07 +00:00
Sabe Jones
801b53857f fix(payments): short circuit for non-gift txns (#8881) 2017-07-16 09:24:17 -07:00
Keith Holliday
f8571ec5d5 Moved notifications to be read after yesterdailies (#8872)
* Moved notifications to be read after yesterdailies

* Prevent when user needs cron

* Updated tests
2017-07-16 09:24:09 -07:00
Matteo Pagliazzi
78ba596504 Groups can prevent members from getting gems (#8870)
* add possibility for group to block members from getting gems

* fixes

* fix tests

* adds some tests

* unit tests

* finish unit tests

* remove old code
2017-07-16 09:23:57 -07:00
Céline O'Neil
b1a4c1b4ff Update instructions for running select API tests, in line with wiki instructions 2017-05-01 10:04:37 -04:00
Céline O'Neil
fb80dd7c57 Allow leaving a challenge without having access to the challenge (e.g. after leaving a party or guild) 2017-05-01 10:04:37 -04:00
1233 changed files with 86122 additions and 46912 deletions

View File

@@ -1,6 +1,7 @@
{
"presets": ["es2015"],
"plugins": [
"transform-object-rest-spread",
["transform-async-to-module-method", {
"module": "bluebird",
"method": "coroutine"

View File

@@ -2,6 +2,9 @@ language: node_js
node_js:
- '6'
sudo: required
dist: precise
services:
- mongodb
addons:
apt:
sources:
@@ -17,7 +20,7 @@ install:
before_script:
- npm run test:build
- cp config.json.example config.json
- if [ $REQUIRES_SERVER ]; then until nc -z localhost 27017; do echo Waiting for MongoDB; sleep 1; done; export DISPLAY=:99; fi
- sleep 15
script:
- npm run $TEST
- if [ $COVERAGE ]; then ./node_modules/.bin/lcov-result-merger 'coverage/**/*.info' | ./node_modules/coveralls/bin/coveralls.js; fi

View File

@@ -7,6 +7,7 @@ RUN npm install -g gulp grunt-cli bower mocha
RUN mkdir -p /usr/src/habitrpg
WORKDIR /usr/src/habitrpg
RUN git clone https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
RUN cp config.json.example config.json
RUN npm install
RUN bower install --allow-root

View File

@@ -6,7 +6,7 @@ RUN npm install -g gulp grunt-cli bower mocha
# Clone Habitica repo and install dependencies
RUN mkdir -p /usr/src/habitrpg
WORKDIR /usr/src/habitrpg
RUN git clone --branch v3.102.2 https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
RUN git clone --branch v3.111.0 https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
RUN npm install
RUN bower install --allow-root
RUN gulp build:prod --force

View File

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

View File

@@ -0,0 +1,109 @@
var migrationName = '20170731_naming_day.js';
var authorName = 'Sabe'; // in case script author needs to know when their ...
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
/*
* Award Royal Purple Gryphon Helm to Royal Purple Gryphon pet owners,
* award Royal Purple Gryphon pet to Royal Purple Gryphon mount owners,
* award Royal Purple Gryphon mount to everyone else
*/
var monk = require('monk');
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
var dbUsers = monk(connectionString).get('users', { castIds: false });
function processUsers(lastId) {
// specify a query to limit the affected users (empty for all users):
var query = {
'migration':{$ne:migrationName},
'auth.timestamps.loggedin': {$gt: new Date('2017-01-01')},
};
if (lastId) {
query._id = {
$gt: lastId
}
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 250,
fields: [
'items.mounts',
'items.pets',
] // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
})
.then(updateUsers)
.catch(function (err) {
console.log(err);
return exiting(1, 'ERROR! ' + err);
});
}
var progressCount = 1000;
var count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
displayData();
return;
}
var userPromises = users.map(updateUser);
var lastUser = users[users.length - 1];
return Promise.all(userPromises)
.then(function () {
processUsers(lastUser._id);
});
}
function updateUser (user) {
count++;
var set = {};
var inc = {
'achievements.habiticaDays': 1,
'items.food.Cake_Skeleton': 1,
'items.food.Cake_Base': 1,
'items.food.Cake_CottonCandyBlue': 1,
'items.food.Cake_CottonCandyPink': 1,
'items.food.Cake_Shade': 1,
'items.food.Cake_White': 1,
'items.food.Cake_Golden': 1,
'items.food.Cake_Zombie': 1,
'items.food.Cake_Desert': 1,
'items.food.Cake_Red': 1
};
if (user.items.pets['Gryphon-RoyalPurple']) {
set = {'migration':migrationName, 'items.gear.owned.head_special_namingDay2017': false};
} else if (user.items.mounts['Gryphon-RoyalPurple']) {
set = {'migration':migrationName, 'items.pets.Gryphon-RoyalPurple': 5};
} else {
set = {'migration':migrationName, 'items.mounts.Gryphon-RoyalPurple': true};
}
dbUsers.update({_id: user._id}, {$set: set, $inc: inc});
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
if (user._id == authorUuid) console.warn(authorName + ' processed');
}
function displayData() {
console.warn('\n' + count + ' users processed\n');
return exiting(0);
}
function exiting(code, msg) {
code = code || 0; // 0 = success
if (code && !msg) { msg = 'ERROR!'; }
if (msg) {
if (code) { console.error(msg); }
else { console.log( msg); }
}
process.exit(code);
}
module.exports = processUsers;

View File

@@ -2,7 +2,7 @@ var _id = '';
var update = {
$addToSet: {
'purchased.plan.mysteryItems':{
$each:['body_mystery_201706','back_mystery_201706']
$each:['shield_mystery_201708','weapon_mystery_201708']
}
}
};

2636
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.102.3",
"version": "3.111.0",
"main": "./website/server/index.js",
"dependencies": {
"@slack/client": "^3.8.1",
@@ -31,7 +31,7 @@
"bluebird": "^3.3.5",
"body-parser": "^1.15.0",
"bootstrap": "^4.0.0-alpha.6",
"bootstrap-vue": "^0.16.1",
"bootstrap-vue": "^0.18.0",
"bower": "~1.3.12",
"browserify": "~12.0.1",
"compression": "^1.6.1",
@@ -40,7 +40,7 @@
"coupon-code": "^0.4.5",
"css-loader": "^0.28.0",
"csv-stringify": "^1.0.2",
"cwait": "^1.0.0",
"cwait": "~1.0.1",
"domain-middleware": "~0.1.0",
"estraverse": "^4.1.1",
"express": "~4.14.0",
@@ -73,6 +73,7 @@
"html-webpack-plugin": "^2.8.1",
"image-size": "~0.3.2",
"in-app-purchase": "^1.1.6",
"intro.js": "^2.6.0",
"jade": "~1.11.0",
"jquery": "^3.1.1",
"js2xmlparser": "~1.0.0",
@@ -81,7 +82,7 @@
"method-override": "^2.3.5",
"moment": "^2.13.0",
"moment-recur": "habitrpg/moment-recur#v1.0.6",
"mongoose": "^4.8.6",
"mongoose": "~4.8.6",
"mongoose-id-autoinc": "~2013.7.14-4",
"morgan": "^1.7.0",
"nconf": "~0.8.2",
@@ -125,9 +126,11 @@
"vue": "^2.1.0",
"vue-loader": "^11.0.0",
"vue-mugen-scroll": "^0.2.1",
"vue-notification": "^1.3.2",
"vue-router": "^2.0.0-rc.5",
"vue-style-loader": "^3.0.0",
"vue-template-compiler": "^2.1.10",
"vuejs-datepicker": "^0.9.4",
"webpack": "^2.2.1",
"webpack-merge": "^4.0.0",
"winston": "^2.1.0",

View File

@@ -32,18 +32,20 @@ describe('POST /challenges/:challengeId/leave', () => {
let group;
let challenge;
let notInChallengeUser;
let notInGroupLeavingUser;
let leavingUser;
let taskText;
beforeEach(async () => {
let populatedGroup = await createAndPopulateGroup({
members: 2,
members: 3,
});
groupLeader = populatedGroup.groupLeader;
group = populatedGroup.group;
leavingUser = populatedGroup.members[0];
notInChallengeUser = populatedGroup.members[1];
notInGroupLeavingUser = populatedGroup.members[2];
challenge = await generateChallenge(groupLeader, group);
@@ -55,17 +57,16 @@ describe('POST /challenges/:challengeId/leave', () => {
await leavingUser.post(`/challenges/${challenge._id}/join`);
await notInGroupLeavingUser.post(`/challenges/${challenge._id}/join`);
await notInGroupLeavingUser.post(`/groups/${group._id}/leave`, {
keepChallenges: 'remain-in-challenges',
});
await challenge.sync();
});
it('returns an error when user doesn\'t have permissions to view the challenge', async () => {
let unauthorizedUser = await generateUser();
await expect(unauthorizedUser.post(`/challenges/${challenge._id}/leave`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('challengeNotFound'),
});
it('lets user leave when not a member of the challenge group', async () => {
await expect(notInGroupLeavingUser.post(`/challenges/${challenge._id}/leave`)).to.eventually.be.ok;
});
it('returns an error when user isn\'t a member of the challenge', async () => {

View File

@@ -114,6 +114,26 @@ describe('POST /challenges/:challengeId/winner/:winnerId', () => {
await expect(winningUser.sync()).to.eventually.have.property('balance', oldBalance + challenge.prize / 4);
});
it('doesn\'t gives winner gems if group policy prevents it', async () => {
let oldBalance = winningUser.balance;
let oldLeaderBalance = (await groupLeader.sync()).balance;
await winningUser.update({
'purchased.plan.customerId': 'group-plan',
});
await group.update({
'leaderOnly.getGems': true,
'purchased.plan.customerId': 123,
});
await groupLeader.post(`/challenges/${challenge._id}/selectWinner/${winningUser._id}`);
await sleep(0.5);
await expect(winningUser.sync()).to.eventually.have.property('balance', oldBalance);
await expect(groupLeader.sync()).to.eventually.have.property('balance', oldLeaderBalance + challenge.prize / 4);
});
it('doesn\'t refund gems to group leader', async () => {
let oldBalance = (await groupLeader.sync()).balance;

View File

@@ -10,11 +10,22 @@ import {
TAVERN_ID,
} from '../../../../../website/server/models/group';
import { v4 as generateUUID } from 'uuid';
import { getMatchesByWordArray, removePunctuationFromString } from '../../../../../website/server/libs/stringUtils';
import bannedWords from '../../../../../website/server/libs/bannedWords';
import * as email from '../../../../../website/server/libs/email';
import { IncomingWebhook } from '@slack/client';
import nconf from 'nconf';
const BASE_URL = nconf.get('BASE_URL');
describe('POST /chat', () => {
let user, groupWithChat, member, additionalMember;
let testMessage = 'Test Message';
let testBannedWordMessage = 'TEST_PLACEHOLDER_SWEAR_WORD_HERE';
let testSlurMessage = 'message with TEST_PLACEHOLDER_SLUR_WORD_HERE';
let bannedWordErrorMessage = t('bannedWordUsed').split('.');
bannedWordErrorMessage[0] += ` (${removePunctuationFromString(testBannedWordMessage.toLowerCase())})`;
bannedWordErrorMessage = bannedWordErrorMessage.join('.');
before(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
@@ -25,7 +36,6 @@ describe('POST /chat', () => {
},
members: 2,
});
user = groupLeader;
groupWithChat = group;
member = members[0];
@@ -79,11 +89,11 @@ describe('POST /chat', () => {
context('banned word', () => {
it('returns an error when chat message contains a banned word in tavern', async () => {
await expect(user.post('/groups/habitrpg/chat', { message: testBannedWordMessage}))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedWordUsed'),
});
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: bannedWordErrorMessage,
});
});
it('errors when word is part of a phrase', async () => {
@@ -92,7 +102,7 @@ describe('POST /chat', () => {
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedWordUsed'),
message: bannedWordErrorMessage,
});
});
@@ -102,10 +112,26 @@ describe('POST /chat', () => {
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedWordUsed'),
message: bannedWordErrorMessage,
});
});
it('checks error message has the banned words used', async () => {
let randIndex = Math.floor(Math.random() * (bannedWords.length + 1));
let testBannedWords = bannedWords.slice(randIndex, randIndex + 2).map((w) => w.replace(/\\/g, ''));
let chatMessage = `Mixing ${testBannedWords[0]} and ${testBannedWords[1]} is bad for you.`;
await expect(user.post('/groups/habitrpg/chat', { message: chatMessage}))
.to.eventually.be.rejected
.and.have.property('message')
.that.includes(testBannedWords.join(', '));
});
it('check all banned words are matched', async () => {
let message = bannedWords.join(',').replace(/\\/g, '');
let matches = getMatchesByWordArray(message, bannedWords);
expect(matches.length).to.equal(bannedWords.length);
});
it('does not error when bad word is suffix of a word', async () => {
let wordAsSuffix = `prefix${testBannedWordMessage}`;
let message = await user.post('/groups/habitrpg/chat', { message: wordAsSuffix});
@@ -166,6 +192,114 @@ describe('POST /chat', () => {
});
});
context('banned slur', () => {
beforeEach(() => {
sandbox.spy(email, 'sendTxn');
sandbox.stub(IncomingWebhook.prototype, 'send');
});
afterEach(() => {
sandbox.restore();
});
it('errors and revokes privileges when chat message contains a banned slur', async () => {
await expect(user.post(`/groups/${groupWithChat._id}/chat`, { message: testSlurMessage})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedSlurUsed'),
});
// Email sent to mods
await sleep(0.5);
expect(email.sendTxn).to.be.calledOnce;
expect(email.sendTxn.args[0][1]).to.be.eql('slur-report-to-mods');
// Slack message to mods
expect(IncomingWebhook.prototype.send).to.be.calledOnce;
/* eslint-disable camelcase */
expect(IncomingWebhook.prototype.send).to.be.calledWith({
text: `${user.profile.name} (${user.id}) tried to post a slur`,
attachments: [{
fallback: 'Slur Message',
color: 'danger',
author_name: `${user.profile.name} - ${user.auth.local.email} - ${user._id}`,
title: 'Slur in Test Guild',
title_link: `${BASE_URL}/#/options/groups/guilds/${groupWithChat.id}`,
text: testSlurMessage,
// footer: sandbox.match(/<.*?groupId=group-id&chatId=chat-id\|Flag this message>/),
mrkdwn_in: [
'text',
],
}],
});
/* eslint-enable camelcase */
// Chat privileges are revoked
await expect(user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('chatPrivilegesRevoked'),
});
// Restore chat privileges to continue testing
user.flags.chatRevoked = false;
await user.update({'flags.chatRevoked': false});
});
it('does not allow slurs in private groups', async () => {
let { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'Party',
type: 'party',
privacy: 'private',
},
members: 1,
});
await expect(members[0].post(`/groups/${group._id}/chat`, { message: testSlurMessage})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedSlurUsed'),
});
// Email sent to mods
await sleep(0.5);
expect(email.sendTxn).to.be.calledThrice;
expect(email.sendTxn.args[2][1]).to.be.eql('slur-report-to-mods');
// Slack message to mods
expect(IncomingWebhook.prototype.send).to.be.calledOnce;
/* eslint-disable camelcase */
expect(IncomingWebhook.prototype.send).to.be.calledWith({
text: `${members[0].profile.name} (${members[0].id}) tried to post a slur`,
attachments: [{
fallback: 'Slur Message',
color: 'danger',
author_name: `${members[0].profile.name} - ${members[0].auth.local.email} - ${members[0]._id}`,
title: 'Slur in Party - (private party)',
title_link: undefined,
text: testSlurMessage,
// footer: sandbox.match(/<.*?groupId=group-id&chatId=chat-id\|Flag this message>/),
mrkdwn_in: [
'text',
],
}],
});
/* eslint-enable camelcase */
// Chat privileges are revoked
await expect(members[0].post(`/groups/${groupWithChat._id}/chat`, { message: testMessage})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('chatPrivilegesRevoked'),
});
// Restore chat privileges to continue testing
members[0].flags.chatRevoked = false;
await members[0].update({'flags.chatRevoked': false});
});
});
it('does not error when sending a message to a private guild with a user with revoked chat', async () => {
let { group, members } = await createAndPopulateGroup({
groupDetails: {

View File

@@ -16,6 +16,12 @@ describe('GET /groups', () => {
const NUMBER_OF_USERS_PRIVATE_GUILDS = 1;
const NUMBER_OF_GROUPS_USER_CAN_VIEW = 5;
const GUILD_PER_PAGE = 30;
let categories = [{
slug: 'newCat',
name: 'New Category',
}];
let publicGuildNotMember;
let privateGuildUserIsMemberOf;
before(async () => {
await resetHabiticaDB();
@@ -31,16 +37,18 @@ describe('GET /groups', () => {
await leader.post(`/groups/${publicGuildUserIsMemberOf._id}/invite`, { uuids: [user._id]});
await user.post(`/groups/${publicGuildUserIsMemberOf._id}/join`);
await generateGroup(leader, {
publicGuildNotMember = await generateGroup(leader, {
name: 'public guild - is not member',
type: 'guild',
privacy: 'public',
categories,
});
let privateGuildUserIsMemberOf = await generateGroup(leader, {
privateGuildUserIsMemberOf = await generateGroup(leader, {
name: 'private guild - is member',
type: 'guild',
privacy: 'private',
categories,
});
await leader.post(`/groups/${privateGuildUserIsMemberOf._id}/invite`, { uuids: [user._id]});
await user.post(`/groups/${privateGuildUserIsMemberOf._id}/join`);
@@ -100,6 +108,50 @@ describe('GET /groups', () => {
.to.eventually.have.a.lengthOf(NUMBER_OF_PUBLIC_GUILDS);
});
describe('filters', () => {
it('returns public guilds filtered by category', async () => {
let guilds = await user.get(`/groups?type=publicGuilds&categories=${categories[0].slug}`);
expect(guilds[0]._id).to.equal(publicGuildNotMember._id);
});
it('returns private guilds filtered by category', async () => {
let guilds = await user.get(`/groups?type=privateGuilds&categories=${categories[0].slug}`);
expect(guilds[0]._id).to.equal(privateGuildUserIsMemberOf._id);
});
it('filters public guilds by size', async () => {
await generateGroup(user, {
name: 'guild1',
type: 'guild',
privacy: 'public',
memberCount: 1,
});
// @TODO: anyway to set higher memberCount in tests right now?
let guilds = await user.get('/groups?type=publicGuilds&minMemberCount=3');
expect(guilds.length).to.equal(0);
});
it('filters private guilds by size', async () => {
await generateGroup(user, {
name: 'guild1',
type: 'guild',
privacy: 'private',
memberCount: 1,
});
// @TODO: anyway to set higher memberCount in tests right now?
let guilds = await user.get('/groups?type=privateGuilds&minMemberCount=3');
expect(guilds.length).to.equal(0);
});
});
describe('public guilds pagination', () => {
it('req.query.paginate must be a boolean string', async () => {
await expect(user.get('/groups?paginate=aString&type=publicGuilds'))
@@ -149,8 +201,8 @@ describe('GET /groups', () => {
await expect(user.get('/groups?type=publicGuilds&paginate=true&page=1'))
.to.eventually.have.a.lengthOf(GUILD_PER_PAGE);
let page2 = await expect(user.get('/groups?type=publicGuilds&paginate=true&page=2'))
.to.eventually.have.a.lengthOf(1 + 2); // 1 created now, 2 by other tests
expect(page2[2].name).to.equal('guild with less members');
.to.eventually.have.a.lengthOf(1 + 3); // 1 created now, 3 by other tests
expect(page2[3].name).to.equal('guild with less members');
});
});

View File

@@ -66,11 +66,25 @@ describe('GET /groups/:groupId/members', () => {
expect(res[0].profile).to.have.all.keys(['name']);
});
it('req.query.includeAllPublicFields === true only works with parties', async () => {
it('req.query.includeAllPublicFields === true works with guilds', async () => {
let group = await generateGroup(user, {type: 'guild', name: generateUUID()});
let res = await user.get(`/groups/${group._id}/members?includeAllPublicFields=true`);
expect(res[0]).to.have.all.keys(['_id', 'id', 'profile']);
expect(res[0].profile).to.have.all.keys(['name']);
let [memberRes] = await user.get(`/groups/${group._id}/members?includeAllPublicFields=true`);
expect(memberRes).to.have.all.keys([ // works as: object has all and only these keys
'_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party',
'backer', 'contributor', 'auth', 'items', 'inbox',
]);
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
expect(Object.keys(memberRes.preferences).sort()).to.eql([
'size', 'hair', 'skin', 'shirt',
'chair', 'costume', 'sleep', 'background', 'tasks',
].sort());
expect(memberRes.stats.maxMP).to.exist;
expect(memberRes.stats.maxHealth).to.equal(common.maxHealth);
expect(memberRes.stats.toNextLevel).to.equal(common.tnl(memberRes.stats.lvl));
expect(memberRes.inbox.optOut).to.exist;
expect(memberRes.inbox.messages).to.not.exist;
});
it('populates all public fields if req.query.includeAllPublicFields === true and it is a party', async () => {

View File

@@ -220,7 +220,7 @@ describe('POST /group/:groupId/join', () => {
it('clears invitation from user when joining party', async () => {
await invitedUser.post(`/groups/${party._id}/join`);
await expect(invitedUser.get('/user')).to.eventually.not.have.deep.property('invitations.party.id');
await expect(invitedUser.get('/user')).to.eventually.not.have.deep.property('invitations.parties[0].id');
});
it('increments memberCount when joining party', async () => {

View File

@@ -247,7 +247,7 @@ describe('POST /groups/:groupId/leave', () => {
let userWithoutInvitation = await invitedUser.get('/user');
expect(userWithoutInvitation.invitations.party).to.be.empty;
expect(userWithoutInvitation.invitations.parties[0]).to.be.empty;
});
});

View File

@@ -107,7 +107,7 @@ describe('POST /group/:groupId/reject-invite', () => {
it('clears invitation from user', async () => {
await invitedUser.post(`/groups/${party._id}/reject-invite`);
await expect(invitedUser.get('/user')).to.eventually.not.have.deep.property('invitations.party.id');
await expect(invitedUser.get('/user')).to.eventually.not.have.deep.property('invitations.parties[0].id');
});
});
});

View File

@@ -177,13 +177,13 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
});
it('can remove other invites', async () => {
expect(partyInvitedUser.invitations.party).to.not.be.empty;
expect(partyInvitedUser.invitations.parties[0]).to.not.be.empty;
await partyLeader.post(`/groups/${party._id}/removeMember/${partyInvitedUser._id}`);
let invitedUserWithoutInvite = await partyInvitedUser.get('/user');
expect(invitedUserWithoutInvite.invitations.party).to.be.empty;
expect(invitedUserWithoutInvite.invitations.parties[0]).to.be.empty;
});
it('removes new messages from a member who is removed', async () => {

View File

@@ -440,7 +440,38 @@ describe('Post /groups/:groupId/invite', () => {
await inviter.post(`/groups/${party._id}/invite`, {
uuids: [userToInvite._id],
});
expect((await userToInvite.get('/user')).invitations.party.id).to.equal(party._id);
expect((await userToInvite.get('/user')).invitations.parties[0].id).to.equal(party._id);
});
it('allow inviting a user to 2 different parties', async () => {
// Create another inviter
let inviter2 = await generateUser();
// Create user to invite
let userToInvite = await generateUser();
// Create second group
let party2 = await inviter2.post('/groups', {
name: 'Test Party 2',
type: 'party',
});
// Invite to first party
await inviter.post(`/groups/${party._id}/invite`, {
uuids: [userToInvite._id],
});
// Invite to second party
await inviter2.post(`/groups/${party2._id}/invite`, {
uuids: [userToInvite._id],
});
// Get updated user
let invitedUser = await userToInvite.get('/user');
expect(invitedUser.invitations.parties.length).to.equal(2);
expect(invitedUser.invitations.parties[0].id).to.equal(party._id);
expect(invitedUser.invitations.parties[1].id).to.equal(party2._id);
});
it('allow inviting a user if party id is not associated with a real party', async () => {
@@ -451,7 +482,7 @@ describe('Post /groups/:groupId/invite', () => {
await inviter.post(`/groups/${party._id}/invite`, {
uuids: [userToInvite._id],
});
expect((await userToInvite.get('/user')).invitations.party.id).to.equal(party._id);
expect((await userToInvite.get('/user')).invitations.parties[0].id).to.equal(party._id);
});
it('allows 30 members in a party', async () => {

View File

@@ -45,6 +45,20 @@ describe('PUT /group', () => {
expect(updatedGroup.name).to.equal(groupUpdatedName);
});
it('updates a group categories', async () => {
let categories = [{
slug: 'newCat',
name: 'New Category',
}];
let updatedGroup = await leader.put(`/groups/${groupToUpdate._id}`, {
categories,
});
expect(updatedGroup.categories[0].slug).to.eql(categories[0].slug);
expect(updatedGroup.categories[0].name).to.eql(categories[0].name);
});
it('allows an admin to update a guild', async () => {
let updatedGroup = await adminUser.put(`/groups/${groupToUpdate._id}`, {
name: groupUpdatedName,

View File

@@ -152,7 +152,7 @@ describe('GET /tasks/user', () => {
expect(dailys2[0].isDue).to.be.true;
});
it('returns dailies with isDue for the date specified and will add CDS offset if time is not supplied and assumes timezones', async () => {
xit('returns dailies with isDue for the date specified and will add CDS offset if time is not supplied and assumes timezones', async () => {
let timezone = 420;
await user.update({
'preferences.dayStart': 0,
@@ -179,7 +179,7 @@ describe('GET /tasks/user', () => {
});
it('returns dailies with isDue for the date specified and will add CDS offset if time is not supplied and assumes timezones', async () => {
xit('returns dailies with isDue for the date specified and will add CDS offset if time is not supplied and assumes timezones', async () => {
let timezone = 240;
await user.update({
'preferences.dayStart': 0,
@@ -205,7 +205,7 @@ describe('GET /tasks/user', () => {
expect(dailys2[0].isDue).to.be.false;
});
it('returns dailies with isDue for the date specified and will add CDS offset if time is not supplied and assumes timezones', async () => {
xit('returns dailies with isDue for the date specified and will add CDS offset if time is not supplied and assumes timezones', async () => {
let timezone = 540;
await user.update({
'preferences.dayStart': 0,

View File

@@ -18,266 +18,328 @@ import {
} from '../../../../../website/server/libs/password';
import * as email from '../../../../../website/server/libs/email';
const DELETE_CONFIRMATION = 'DELETE';
describe('DELETE /user', () => {
let user;
let password = 'password'; // from habitrpg/test/helpers/api-integration/v3/object-generators.js
beforeEach(async () => {
user = await generateUser({balance: 10});
});
it('returns an error if password is wrong', async () => {
await expect(user.del('/user', {
password: 'wrong-password',
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('wrongPassword'),
});
});
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'});
await expect(userWithSubscription.del('/user', {
password,
})).to.be.rejected.and.to.eventually.eql({
code: 401,
error: 'NotAuthorized',
message: t('cannotDeleteActiveAccount'),
});
});
it('deletes the user\'s tasks', async () => {
// gets the user's tasks ids
let ids = [];
each(user.tasksOrder, (idsForOrder) => {
ids.push(...idsForOrder);
});
expect(ids.length).to.be.above(0); // make sure the user has some task to delete
await user.del('/user', {
password,
});
await Bluebird.all(map(ids, id => {
return expect(checkExistence('tasks', id)).to.eventually.eql(false);
}));
});
it('reduces memberCount in challenges user is linked to', async () => {
let populatedGroup = await createAndPopulateGroup({
members: 2,
});
let group = populatedGroup.group;
let authorizedUser = populatedGroup.members[1];
let challenge = await generateChallenge(populatedGroup.groupLeader, group);
await authorizedUser.post(`/challenges/${challenge._id}/join`);
await challenge.sync();
expect(challenge.memberCount).to.eql(2);
await authorizedUser.del('/user', {
password,
});
await challenge.sync();
expect(challenge.memberCount).to.eql(1);
});
it('deletes the user', async () => {
await user.del('/user', {
password,
});
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();
let sha1HashedPassword = sha1EncryptPassword(textPassword, salt);
await user.update({
'auth.local.hashed_password': sha1HashedPassword,
'auth.local.passwordHashMethod': 'sha1',
'auth.local.salt': salt,
});
await user.sync();
expect(user.auth.local.passwordHashMethod).to.equal('sha1');
expect(user.auth.local.salt).to.equal(salt);
expect(user.auth.local.hashed_password).to.equal(sha1HashedPassword);
// delete the user
await user.del('/user', {
password: textPassword,
});
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
});
context('last member of a party', () => {
let party;
context('user with local auth', async () => {
beforeEach(async () => {
party = await generateGroup(user, {
type: 'party',
privacy: 'private',
user = await generateUser({balance: 10});
});
it('returns an error if password is wrong', async () => {
await expect(user.del('/user', {
password: 'wrong-password',
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('wrongPassword'),
});
});
it('deletes party when user is the only member', async () => {
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('deletes the user', async () => {
await user.del('/user', {
password,
});
await expect(checkExistence('party', party._id)).to.eventually.eql(false);
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
});
});
context('last member of a private guild', () => {
let privateGuild;
it('returns an error if excessive feedback is supplied', async () => {
let feedbackText = 'spam feedback ';
let feedback = feedbackText;
while (feedback.length < 10000) {
feedback = feedback + feedbackText;
}
beforeEach(async () => {
privateGuild = await generateGroup(user, {
type: 'guild',
privacy: 'private',
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('deletes guild when user is the only member', async () => {
it('returns an error if user has active subscription', async () => {
let userWithSubscription = await generateUser({'purchased.plan.customerId': 'fake-customer-id'});
await expect(userWithSubscription.del('/user', {
password,
})).to.be.rejected.and.to.eventually.eql({
code: 401,
error: 'NotAuthorized',
message: t('cannotDeleteActiveAccount'),
});
});
it('deletes the user\'s tasks', async () => {
// gets the user's tasks ids
let ids = [];
each(user.tasksOrder, (idsForOrder) => {
ids.push(...idsForOrder);
});
expect(ids.length).to.be.above(0); // make sure the user has some task to delete
await user.del('/user', {
password,
});
await expect(checkExistence('groups', privateGuild._id)).to.eventually.eql(false);
await Bluebird.all(map(ids, id => {
return expect(checkExistence('tasks', id)).to.eventually.eql(false);
}));
});
});
context('groups user is leader of', () => {
let guild, oldLeader, newLeader;
beforeEach(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'public',
},
members: 1,
it('reduces memberCount in challenges user is linked to', async () => {
let populatedGroup = await createAndPopulateGroup({
members: 2,
});
guild = group;
newLeader = members[0];
oldLeader = groupLeader;
});
let group = populatedGroup.group;
let authorizedUser = populatedGroup.members[1];
it('chooses new group leader for any group user was the leader of', async () => {
await oldLeader.del('/user', {
let challenge = await generateChallenge(populatedGroup.groupLeader, group);
await authorizedUser.post(`/challenges/${challenge._id}/join`);
await challenge.sync();
expect(challenge.memberCount).to.eql(2);
await authorizedUser.del('/user', {
password,
});
let updatedGuild = await newLeader.get(`/groups/${guild._id}`);
await challenge.sync();
expect(updatedGuild.leader).to.exist;
expect(updatedGuild.leader._id).to.not.eql(oldLeader._id);
});
});
context('groups user is a part of', () => {
let group1, group2, userToDelete, otherUser;
beforeEach(async () => {
userToDelete = await generateUser({balance: 10});
group1 = await generateGroup(userToDelete, {
type: 'guild',
privacy: 'public',
});
let {group, members} = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'public',
},
members: 3,
});
group2 = group;
otherUser = members[0];
await userToDelete.post(`/groups/${group2._id}/join`);
expect(challenge.memberCount).to.eql(1);
});
it('removes user from all groups user was a part of', async () => {
await userToDelete.del('/user', {
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,
});
let updatedGroup1Members = await otherUser.get(`/groups/${group1._id}/members`);
let updatedGroup2Members = await otherUser.get(`/groups/${group2._id}/members`);
let userInGroup = find(updatedGroup2Members, (member) => {
return member._id === userToDelete._id;
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();
let sha1HashedPassword = sha1EncryptPassword(textPassword, salt);
await user.update({
'auth.local.hashed_password': sha1HashedPassword,
'auth.local.passwordHashMethod': 'sha1',
'auth.local.salt': salt,
});
expect(updatedGroup1Members).to.be.empty;
expect(updatedGroup2Members).to.not.be.empty;
expect(userInGroup).to.not.exist;
await user.sync();
expect(user.auth.local.passwordHashMethod).to.equal('sha1');
expect(user.auth.local.salt).to.equal(salt);
expect(user.auth.local.hashed_password).to.equal(sha1HashedPassword);
// delete the user
await user.del('/user', {
password: textPassword,
});
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
});
context('last member of a party', () => {
let party;
beforeEach(async () => {
party = await generateGroup(user, {
type: 'party',
privacy: 'private',
});
});
it('deletes party when user is the only member', async () => {
await user.del('/user', {
password,
});
await expect(checkExistence('party', party._id)).to.eventually.eql(false);
});
});
context('last member of a private guild', () => {
let privateGuild;
beforeEach(async () => {
privateGuild = await generateGroup(user, {
type: 'guild',
privacy: 'private',
});
});
it('deletes guild when user is the only member', async () => {
await user.del('/user', {
password,
});
await expect(checkExistence('groups', privateGuild._id)).to.eventually.eql(false);
});
});
context('groups user is leader of', () => {
let guild, oldLeader, newLeader;
beforeEach(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'public',
},
members: 1,
});
guild = group;
newLeader = members[0];
oldLeader = groupLeader;
});
it('chooses new group leader for any group user was the leader of', async () => {
await oldLeader.del('/user', {
password,
});
let updatedGuild = await newLeader.get(`/groups/${guild._id}`);
expect(updatedGuild.leader).to.exist;
expect(updatedGuild.leader._id).to.not.eql(oldLeader._id);
});
});
context('groups user is a part of', () => {
let group1, group2, userToDelete, otherUser;
beforeEach(async () => {
userToDelete = await generateUser({balance: 10});
group1 = await generateGroup(userToDelete, {
type: 'guild',
privacy: 'public',
});
let {group, members} = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'public',
},
members: 3,
});
group2 = group;
otherUser = members[0];
await userToDelete.post(`/groups/${group2._id}/join`);
});
it('removes user from all groups user was a part of', async () => {
await userToDelete.del('/user', {
password,
});
let updatedGroup1Members = await otherUser.get(`/groups/${group1._id}/members`);
let updatedGroup2Members = await otherUser.get(`/groups/${group2._id}/members`);
let userInGroup = find(updatedGroup2Members, (member) => {
return member._id === userToDelete._id;
});
expect(updatedGroup1Members).to.be.empty;
expect(updatedGroup2Members).to.not.be.empty;
expect(userInGroup).to.not.exist;
});
});
});
context('user with Facebook auth', async () => {
beforeEach(async () => {
user = await generateUser({
auth: {
facebook: {
id: 'facebook-id',
},
},
});
});
it('returns an error if confirmation phrase is wrong', async () => {
await expect(user.del('/user', {
password: 'just-do-it',
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('incorrectDeletePhrase'),
});
});
it('returns an error if confirmation phrase is not supplied', async () => {
await expect(user.del('/user', {
password: '',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingPassword'),
});
});
it('deletes a Facebook user', async () => {
await user.del('/user', {
password: DELETE_CONFIRMATION,
});
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
});
});
context('user with Google auth', async () => {
beforeEach(async () => {
user = await generateUser({
auth: {
google: {
id: 'google-id',
},
},
});
});
it('deletes a Google user', async () => {
await user.del('/user', {
password: DELETE_CONFIRMATION,
});
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
});
});
});

View File

@@ -1,5 +1,6 @@
import {
generateUser,
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-integration/v3';
@@ -31,4 +32,70 @@ describe('POST /user/purchase/:type/:key', () => {
expect(user.items[type][key]).to.equal(1);
});
it('can convert gold to gems if subscribed', async () => {
let oldBalance = user.balance;
await user.update({
'purchased.plan.customerId': 'group-plan',
'stats.gp': 1000,
});
await user.post('/user/purchase/gems/gem');
await user.sync();
expect(user.balance).to.equal(oldBalance + 0.25);
});
it('leader can convert gold to gems even if the group plan prevents it', async () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'test',
type: 'guild',
privacy: 'private',
},
});
await group.update({
'leaderOnly.getGems': true,
'purchased.plan.customerId': 123,
});
await groupLeader.sync();
let oldBalance = groupLeader.balance;
await groupLeader.update({
'purchased.plan.customerId': 'group-plan',
'stats.gp': 1000,
});
await groupLeader.post('/user/purchase/gems/gem');
await groupLeader.sync();
expect(groupLeader.balance).to.equal(oldBalance + 0.25);
});
it('cannot convert gold to gems if the group plan prevents it', async () => {
let { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'test',
type: 'guild',
privacy: 'private',
},
members: 1,
});
await group.update({
'leaderOnly.getGems': true,
'purchased.plan.customerId': 123,
});
let oldBalance = members[0].balance;
await members[0].update({
'purchased.plan.customerId': 'group-plan',
'stats.gp': 1000,
});
await expect(members[0].post('/user/purchase/gems/gem'))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('groupPolicyCannotGetGems'),
});
await members[0].sync();
expect(members[0].balance).to.equal(oldBalance);
});
});

View File

@@ -2,17 +2,34 @@ import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
import content from '../../../../../website/common/script/content/index';
describe('POST /user/release-both', () => {
let user;
let animal = 'Wolf-Base';
const loadPets = () => {
let pets = {};
for (let p in content.pets) {
pets[p] = content.pets[p];
pets[p] = 5;
}
return pets;
};
const loadMounts = () => {
let mounts = {};
for (let m in content.pets) {
mounts[m] = content.pets[m];
mounts[m] = true;
}
return mounts;
};
beforeEach(async () => {
user = await generateUser({
'items.currentMount': animal,
'items.currentPet': animal,
'items.pets': {animal: 5},
'items.mounts': {animal: true},
'items.pets': loadPets(),
'items.mounts': loadMounts(),
});
});

View File

@@ -2,15 +2,25 @@ import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
import content from '../../../../../website/common/script/content/index';
describe('POST /user/release-mounts', () => {
let user;
let animal = 'Wolf-Base';
const loadMounts = () => {
let mounts = {};
for (let m in content.pets) {
mounts[m] = content.pets[m];
mounts[m] = true;
}
return mounts;
};
beforeEach(async () => {
user = await generateUser({
'items.currentMount': animal,
'items.mounts': {animal: true},
'items.mounts': loadMounts(),
});
});

View File

@@ -2,15 +2,25 @@ import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
import content from '../../../../../website/common/script/content/index';
describe('POST /user/release-pets', () => {
let user;
let animal = 'Wolf-Base';
const loadPets = () => {
let pets = {};
for (let p in content.pets) {
pets[p] = content.pets[p];
pets[p] = 5;
}
return pets;
};
beforeEach(async () => {
user = await generateUser({
'items.currentPet': animal,
'items.pets': {animal: 5},
'items.pets': loadPets(),
});
});

View File

@@ -509,7 +509,73 @@ describe('POST /user/auth/local/register', () => {
confirmPassword: password,
});
expect(user.invitations.party).to.eql({
expect(user.invitations.parties[0].id).to.eql(group._id);
expect(user.invitations.parties[0].name).to.eql(group.name);
expect(user.invitations.parties[0].inviter).to.eql(groupLeader._id);
});
it('awards achievement to inviter', async () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
});
let invite = encrypt(JSON.stringify({
id: group._id,
inviter: groupLeader._id,
sentAt: Date.now(),
}));
await api.post(`/user/auth/local/register?groupInvite=${invite}`, {
username,
email,
password,
confirmPassword: password,
});
await groupLeader.sync();
expect(groupLeader.achievements.invitedFriend).to.be.true;
});
it('user not added to a party on expired invite', async () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
});
let invite = encrypt(JSON.stringify({
id: group._id,
inviter: groupLeader._id,
sentAt: Date.now() - 6.912e8, // 8 days old
}));
let user = await api.post(`/user/auth/local/register?groupInvite=${invite}`, {
username,
email,
password,
confirmPassword: password,
});
expect(user.invitations.party).to.eql({});
});
it('adds a user to a guild on an invite of type other than party', async () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
});
let invite = encrypt(JSON.stringify({
id: group._id,
inviter: groupLeader._id,
sentAt: Date.now(),
}));
let user = await api.post(`/user/auth/local/register?groupInvite=${invite}`, {
username,
email,
password,
confirmPassword: password,
});
expect(user.invitations.guilds[0]).to.eql({
id: group._id,
name: group.name,
inviter: groupLeader._id,

View File

@@ -102,6 +102,7 @@ describe('Amazon Payments', () => {
});
it('should purchase gems', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
await amzLib.checkout({user, orderReferenceId, headers});
expect(paymentBuyGemsStub).to.be.calledOnce;
@@ -111,6 +112,8 @@ describe('Amazon Payments', () => {
headers,
});
expectAmazonStubs();
expect(user.canGetGems).to.be.calledOnce;
user.canGetGems.restore();
});
it('should error if gem amount is too low', async () => {
@@ -132,20 +135,29 @@ describe('Amazon Payments', () => {
});
});
it('should error if user cannot get gems gems', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
await expect(amzLib.checkout({user, orderReferenceId, headers})).to.eventually.be.rejected.and.to.eql({
httpCode: 401,
message: i18n.t('groupPolicyCannotGetGems'),
name: 'NotAuthorized',
});
user.canGetGems.restore();
});
it('should gift gems', async () => {
let receivingUser = new User();
receivingUser.save();
await receivingUser.save();
let gift = {
type: 'gems',
uuid: receivingUser._id,
gems: {
amount: 16,
uuid: receivingUser._id,
},
};
amount = 16 / 4;
await amzLib.checkout({gift, user, orderReferenceId, headers});
gift.member = receivingUser;
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,

View File

@@ -57,7 +57,20 @@ describe('Apple Payments', () => {
});
});
it('errors if the user cannot purchase gems', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
await expect(applePayments.verifyGemPurchase(user, receipt, headers))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('groupPolicyCannotGetGems'),
});
user.canGetGems.restore();
});
it('purchases gems', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
await applePayments.verifyGemPurchase(user, receipt, headers);
expect(iapSetupStub).to.be.calledOnce;
@@ -74,6 +87,8 @@ describe('Apple Payments', () => {
amount: 5.25,
headers,
});
expect(user.canGetGems).to.be.calledOnce;
user.canGetGems.restore();
});
});

View File

@@ -566,6 +566,17 @@ describe('cron', () => {
expect(tasksByType.habits[0].counterDown).to.equal(0);
});
it('should reset habit counters even if user is resting in the Inn', () => {
user.preferences.sleep = true;
tasksByType.habits[0].counterUp = 1;
tasksByType.habits[0].counterDown = 1;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.habits[0].counterUp).to.equal(0);
expect(tasksByType.habits[0].counterDown).to.equal(0);
});
it('should reset a weekly habit counter each Monday', () => {
tasksByType.habits[0].frequency = 'weekly';
tasksByType.habits[0].counterUp = 1;
@@ -585,6 +596,114 @@ describe('cron', () => {
expect(tasksByType.habits[0].counterDown).to.equal(0);
});
it('should reset a weekly habit counter with custom daily start', () => {
clock.restore();
// Server clock: Monday 12am UTC
let monday = new Date('May 22, 2017 00:00:00 GMT').getTime();
clock = sinon.useFakeTimers(monday);
// cron runs at 2am
user.preferences.dayStart = 2;
tasksByType.habits[0].frequency = 'weekly';
tasksByType.habits[0].counterUp = 1;
tasksByType.habits[0].counterDown = 1;
daysMissed = 1;
// should not reset
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.habits[0].counterUp).to.equal(1);
expect(tasksByType.habits[0].counterDown).to.equal(1);
clock.restore();
// Server clock: Monday 3am UTC
monday = new Date('May 22, 2017 03:00:00 GMT').getTime();
clock = sinon.useFakeTimers(monday);
// should reset after user CDS
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.habits[0].counterUp).to.equal(0);
expect(tasksByType.habits[0].counterDown).to.equal(0);
});
it('should not reset a weekly habit counter when server tz is Monday but user\'s tz is Tuesday', () => {
clock.restore();
// Server clock: Monday 11pm UTC
let monday = new Date('May 22, 2017 23:00:00 GMT').getTime();
clock = sinon.useFakeTimers(monday);
// User clock: Tuesday 1am UTC + 2
user.preferences.timezoneOffset = -120;
tasksByType.habits[0].frequency = 'weekly';
tasksByType.habits[0].counterUp = 1;
tasksByType.habits[0].counterDown = 1;
daysMissed = 1;
// should not reset
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.habits[0].counterUp).to.equal(1);
expect(tasksByType.habits[0].counterDown).to.equal(1);
// User missed one cron, which will subtract User clock back to Monday 1am UTC + 2
// should reset
daysMissed = 2;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.habits[0].counterUp).to.equal(0);
expect(tasksByType.habits[0].counterDown).to.equal(0);
});
it('should reset a weekly habit counter when server tz is Sunday but user\'s tz is Monday', () => {
clock.restore();
// Server clock: Sunday 11pm UTC
let sunday = new Date('May 21, 2017 23:00:00 GMT').getTime();
clock = sinon.useFakeTimers(sunday);
// User clock: Monday 2am UTC + 3
user.preferences.timezoneOffset = -180;
tasksByType.habits[0].frequency = 'weekly';
tasksByType.habits[0].counterUp = 1;
tasksByType.habits[0].counterDown = 1;
daysMissed = 1;
// should reset
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.habits[0].counterUp).to.equal(0);
expect(tasksByType.habits[0].counterDown).to.equal(0);
});
it('should not reset a weekly habit counter when server tz is Monday but user\'s tz is Sunday', () => {
clock.restore();
// Server clock: Monday 2am UTC
let monday = new Date('May 22, 2017 02:00:00 GMT').getTime();
clock = sinon.useFakeTimers(monday);
// User clock: Sunday 11pm UTC - 3
user.preferences.timezoneOffset = 180;
tasksByType.habits[0].frequency = 'weekly';
tasksByType.habits[0].counterUp = 1;
tasksByType.habits[0].counterDown = 1;
daysMissed = 1;
// should not reset
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.habits[0].counterUp).to.equal(1);
expect(tasksByType.habits[0].counterDown).to.equal(1);
});
it('should reset a monthly habit counter the first day of each month', () => {
tasksByType.habits[0].frequency = 'monthly';
tasksByType.habits[0].counterUp = 1;
@@ -603,6 +722,59 @@ describe('cron', () => {
expect(tasksByType.habits[0].counterUp).to.equal(0);
expect(tasksByType.habits[0].counterDown).to.equal(0);
});
it('should reset a monthly habit counter when server tz is last day of month but user tz is first day of the month', () => {
clock.restore();
daysMissed = 0;
// Server clock: 4/30/17 11pm UTC
let monday = new Date('April 30, 2017 23:00:00 GMT').getTime();
clock = sinon.useFakeTimers(monday);
// User clock: 5/1/17 2am UTC + 3
user.preferences.timezoneOffset = -180;
tasksByType.habits[0].frequency = 'monthly';
tasksByType.habits[0].counterUp = 1;
tasksByType.habits[0].counterDown = 1;
daysMissed = 1;
// should reset
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.habits[0].counterUp).to.equal(0);
expect(tasksByType.habits[0].counterDown).to.equal(0);
});
it('should not reset a monthly habit counter when server tz is first day of month but user tz is 2nd day of the month', () => {
clock.restore();
// Server clock: 5/1/17 11pm UTC
let monday = new Date('May 1, 2017 23:00:00 GMT').getTime();
clock = sinon.useFakeTimers(monday);
// User clock: 5/2/17 2am UTC + 3
user.preferences.timezoneOffset = -180;
tasksByType.habits[0].frequency = 'monthly';
tasksByType.habits[0].counterUp = 1;
tasksByType.habits[0].counterDown = 1;
daysMissed = 1;
// should not reset
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.habits[0].counterUp).to.equal(1);
expect(tasksByType.habits[0].counterDown).to.equal(1);
// User missed one day, which will subtract User clock back to 5/1/17 2am UTC + 3
// should reset
daysMissed = 2;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.habits[0].counterUp).to.equal(0);
expect(tasksByType.habits[0].counterDown).to.equal(0);
});
});
});

View File

@@ -63,7 +63,21 @@ describe('Google Payments', () => {
});
});
it('should throw an error if user cannot purchase gems', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
await expect(googlePayments.verifyGemPurchase(user, receipt, signature, headers))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('groupPolicyCannotGetGems'),
});
user.canGetGems.restore();
});
it('purchases gems', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
await googlePayments.verifyGemPurchase(user, receipt, signature, headers);
expect(iapSetupStub).to.be.calledOnce;
@@ -82,6 +96,8 @@ describe('Google Payments', () => {
amount: 5.25,
headers,
});
expect(user.canGetGems).to.be.calledOnce;
user.canGetGems.restore();
});
});

View File

@@ -61,7 +61,7 @@ describe('Paypal Payments', () => {
});
it('creates a link for gem purchases', async () => {
let link = await paypalPayments.checkout();
let link = await paypalPayments.checkout({user: new User()});
expect(paypalPaymentCreateStub).to.be.calledOnce;
expect(paypalPaymentCreateStub).to.be.calledWith(getPaypalCreateOptions('Habitica Gems', 5.00));
@@ -87,13 +87,25 @@ describe('Paypal Payments', () => {
});
});
it('should error if the user cannot get gems', async () => {
let user = new User();
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
await expect(paypalPayments.checkout({user})).to.eventually.be.rejected.and.to.eql({
httpCode: 401,
message: i18n.t('groupPolicyCannotGetGems'),
name: 'NotAuthorized',
});
});
it('creates a link for gifting gems', async () => {
let receivingUser = new User();
await receivingUser.save();
let gift = {
type: 'gems',
uuid: receivingUser._id,
gems: {
amount: 16,
uuid: receivingUser._id,
},
};

View File

@@ -75,8 +75,29 @@ describe('Stripe Payments', () => {
});
});
it('should error if user cannot get gems', async () => {
gift = undefined;
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
await expect(stripePayments.checkout({
token,
user,
gift,
groupId,
email,
headers,
coupon,
}, stripe)).to.eventually.be.rejected.and.to.eql({
httpCode: 401,
message: i18n.t('groupPolicyCannotGetGems'),
name: 'NotAuthorized',
});
});
it('should purchase gems', async () => {
gift = undefined;
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
await stripePayments.checkout({
token,
@@ -102,16 +123,18 @@ describe('Stripe Payments', () => {
paymentMethod: 'Stripe',
gift,
});
expect(user.canGetGems).to.be.calledOnce;
user.canGetGems.restore();
});
it('should gift gems', async () => {
let receivingUser = new User();
receivingUser.save();
await receivingUser.save();
gift = {
type: 'gems',
uuid: receivingUser._id,
gems: {
amount: 16,
uuid: receivingUser._id,
},
};
@@ -125,7 +148,6 @@ describe('Stripe Payments', () => {
coupon,
}, stripe);
gift.member = receivingUser;
expect(stripeChargeStub).to.be.calledOnce;
expect(stripeChargeStub).to.be.calledWith({
amount: '400',

View File

@@ -1,6 +1,7 @@
import Bluebird from 'bluebird';
import moment from 'moment';
import { model as User } from '../../../../../website/server/models/user';
import { model as Group } from '../../../../../website/server/models/group';
import common from '../../../../../website/common';
describe('User Model', () => {
@@ -179,6 +180,75 @@ describe('User Model', () => {
});
});
context('canGetGems', () => {
let user;
let group;
beforeEach(() => {
user = new User();
let leader = new User();
group = new Group({
name: 'test',
type: 'guild',
privacy: 'private',
leader: leader._id,
});
});
it('returns true if user is not subscribed', async () => {
expect(await user.canGetGems()).to.equal(true);
});
it('returns true if user is not subscribed with a group plan', async () => {
user.purchased.plan.customerId = 123;
expect(await user.canGetGems()).to.equal(true);
});
it('returns true if user is subscribed with a group plan', async () => {
user.purchased.plan.customerId = 'group-plan';
expect(await user.canGetGems()).to.equal(true);
});
it('returns true if user is part of a group', async () => {
user.guilds.push(group._id);
expect(await user.canGetGems()).to.equal(true);
});
it('returns true if user is part of a group with a subscription', async () => {
user.guilds.push(group._id);
user.purchased.plan.customerId = 'group-plan';
group.purchased.plan.customerId = 123;
await group.save();
expect(await user.canGetGems()).to.equal(true);
});
it('returns true if leader is part of a group with a subscription and canGetGems: false', async () => {
user.guilds.push(group._id);
user.purchased.plan.customerId = 'group-plan';
group.purchased.plan.customerId = 123;
group.leader = user._id;
group.leaderOnly.getGems = true;
await group.save();
expect(await user.canGetGems()).to.equal(true);
});
it('returns true if user is part of a group with no subscription but canGetGems: false', async () => {
user.guilds.push(group._id);
user.purchased.plan.customerId = 'group-plan';
group.leaderOnly.getGems = true;
await group.save();
expect(await user.canGetGems()).to.equal(true);
});
it('returns false if user is part of a group with a subscription and canGetGems: false', async () => {
user.guilds.push(group._id);
user.purchased.plan.customerId = 'group-plan';
group.purchased.plan.customerId = 123;
group.leaderOnly.getGems = true;
await group.save();
expect(await user.canGetGems()).to.equal(false);
});
});
context('hasNotCancelled', () => {
let user;
beforeEach(() => {

View File

@@ -6,6 +6,7 @@ describe('Notification Controller', function() {
beforeEach(function() {
user = specHelper.newUser();
user._id = "unique-user-id";
user.needsCron = false;
var userSync = sinon.stub().returns({
then: function then (f) { f(); }

View File

@@ -1,5 +1,5 @@
import Vue from 'vue';
import DrawerComponent from 'client/components/inventory/drawer.vue';
import DrawerComponent from 'client/components/ui/drawer.vue';
describe('DrawerComponent', () => {
it('sets the correct default data', () => {

View File

@@ -122,7 +122,7 @@ describe('achievements', () => {
});
it('card achievements exist with counts', () => {
let cardTypes = ['greeting', 'thankyou', 'birthday', 'congrats', 'getwell'];
let cardTypes = ['greeting', 'thankyou', 'birthday', 'congrats', 'getwell', 'goodluck'];
cardTypes.forEach((card) => {
let cardAchiev = basicAchievs[`${card}Cards`];

View File

@@ -76,5 +76,35 @@ describe('shared.ops.buyHealthPotion', () => {
done();
}
});
it('does not allow potion purchases when hp is zero', (done) => {
user.stats.hp = 0;
user.stats.gp = 40;
try {
buyHealthPotion(user);
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('messageHealthAlreadyMin'));
expect(user.stats.hp).to.eql(0);
expect(user.stats.gp).to.eql(40);
done();
}
});
it('does not allow potion purchases when hp is negative', (done) => {
user.stats.hp = -8;
user.stats.gp = 40;
try {
buyHealthPotion(user);
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('messageHealthAlreadyMin'));
expect(user.stats.hp).to.eql(-8);
expect(user.stats.gp).to.eql(40);
done();
}
});
});
});

View File

@@ -1,3 +1,5 @@
/* eslint-disable camelcase */
import changeClass from '../../../website/common/script/ops/changeClass';
import {
NotAuthorized,
@@ -58,7 +60,8 @@ describe('shared.ops.changeClass', () => {
it('changes class', () => {
user.stats.class = 'healer';
user.items.gear.owned.armor_rogue_1 = true; // eslint-disable-line camelcase
user.items.gear.owned.weapon_healer_3 = true;
user.items.gear.equipped.weapon = 'weapon_healer_3';
let [data] = changeClass(user, {query: {class: 'rogue'}});
expect(data).to.eql({
@@ -70,13 +73,10 @@ describe('shared.ops.changeClass', () => {
expect(user.stats.class).to.equal('rogue');
expect(user.flags.classSelected).to.be.true;
expect(user.items.gear.equipped.weapon).to.equal('weapon_rogue_0');
expect(user.items.gear.owned.weapon_rogue_0).to.be.true;
expect(user.items.gear.equipped.armor).to.equal('armor_rogue_1');
expect(user.items.gear.owned.armor_rogue_1).to.be.true;
expect(user.items.gear.equipped.shield).to.equal('shield_rogue_0');
expect(user.items.gear.owned.shield_rogue_0).to.be.true;
expect(user.items.gear.equipped.head).to.equal('head_base_0');
expect(user.items.gear.owned.weapon_healer_3).to.be.true;
expect(user.items.gear.equipped.weapon).to.equal('weapon_healer_3');
});
});

View File

@@ -14,10 +14,18 @@ describe('shared.ops.releaseBoth', () => {
beforeEach(() => {
user = generateUser();
for (let p in content.pets) {
user.items.pets[p] = content.pets[p];
user.items.pets[p] = 5;
}
for (let m in content.pets) {
user.items.mounts[m] = content.pets[m];
user.items.mounts[m] = true;
}
user.items.currentMount = animal;
user.items.currentPet = animal;
user.items.pets[animal] = 5;
user.items.mounts[animal] = true;
user.balance = 1.5;
});
@@ -34,7 +42,7 @@ describe('shared.ops.releaseBoth', () => {
});
it('grants triad bingo with gems', () => {
let [, message] = releaseBoth(user);
let message = releaseBoth(user)[1];
expect(message).to.equal(i18n.t('mountsAndPetsReleased'));
expect(user.achievements.triadBingoCount).to.equal(1);
@@ -45,27 +53,79 @@ describe('shared.ops.releaseBoth', () => {
user.achievements.triadBingo = 1;
user.achievements.triadBingoCount = 1;
let [, message] = releaseBoth(user);
let message = releaseBoth(user)[1];
expect(message).to.equal(i18n.t('mountsAndPetsReleased'));
expect(user.achievements.triadBingoCount).to.equal(2);
});
it('does not grant triad bingo if any pet has not been previously found', () => {
let triadBingoCountBeforeRelease = user.achievements.triadBingoCount;
user.items.pets[animal] = -1;
let message = releaseBoth(user)[1];
expect(message).to.equal(i18n.t('mountsAndPetsReleased'));
expect(user.achievements.triadBingoCount).to.equal(triadBingoCountBeforeRelease);
});
it('releases pets', () => {
let [, message] = releaseBoth(user);
let message = releaseBoth(user)[1];
expect(message).to.equal(i18n.t('mountsAndPetsReleased'));
expect(user.items.pets[animal]).to.be.empty;
expect(user.items.mounts[animal]).to.equal(null);
});
it('does not increment beastMasterCount if any pet is level 0 (released)', () => {
let beastMasterCountBeforeRelease = user.achievements.beastMasterCount;
user.items.pets[animal] = 0;
releaseBoth(user);
expect(user.achievements.beastMasterCount).to.equal(beastMasterCountBeforeRelease);
});
it('does not increment beastMasterCount if any pet is missing (null)', () => {
let beastMasterCountBeforeRelease = user.achievements.beastMasterCount;
user.items.pets[animal] = null;
releaseBoth(user);
expect(user.achievements.beastMasterCount).to.equal(beastMasterCountBeforeRelease);
});
it('does not increment beastMasterCount if any pet is missing (undefined)', () => {
let beastMasterCountBeforeRelease = user.achievements.beastMasterCount;
delete user.items.pets[animal];
releaseBoth(user);
expect(user.achievements.beastMasterCount).to.equal(beastMasterCountBeforeRelease);
});
it('releases mounts', () => {
let [, message] = releaseBoth(user);
let message = releaseBoth(user)[1];
expect(message).to.equal(i18n.t('mountsAndPetsReleased'));
expect(user.items.mounts[animal]).to.equal(null);
});
it('does not increase mountMasterCount achievement if mount is missing (null)', () => {
let mountMasterCountBeforeRelease = user.achievements.mountMasterCount;
user.items.mounts[animal] = null;
releaseBoth(user);
expect(user.achievements.mountMasterCount).to.equal(mountMasterCountBeforeRelease);
});
it('does not increase mountMasterCount achievement if mount is missing (undefined)', () => {
let mountMasterCountBeforeRelease = user.achievements.mountMasterCount;
delete user.items.mounts[animal];
releaseBoth(user);
expect(user.achievements.mountMasterCount).to.equal(mountMasterCountBeforeRelease);
});
it('removes drop currentPet', () => {
let petInfo = content.petInfo[user.items.currentPet];
expect(petInfo.type).to.equal('drop');

View File

@@ -14,8 +14,12 @@ describe('shared.ops.releaseMounts', () => {
beforeEach(() => {
user = generateUser();
for (let k in content.pets) {
user.items.mounts[k] = content.pets[k];
user.items.mounts[k] = true;
}
user.items.currentMount = animal;
user.items.mounts[animal] = true;
user.balance = 1;
});
@@ -32,7 +36,7 @@ describe('shared.ops.releaseMounts', () => {
});
it('releases mounts', () => {
let [, message] = releaseMounts(user);
let message = releaseMounts(user)[1];
expect(message).to.equal(i18n.t('mountsReleased'));
expect(user.items.mounts[animal]).to.equal(null);
@@ -60,10 +64,27 @@ describe('shared.ops.releaseMounts', () => {
it('increases mountMasterCount achievement', () => {
releaseMounts(user);
expect(user.achievements.mountMasterCount).to.equal(1);
});
it('does not increase mountMasterCount achievement if mount is missing (null)', () => {
let mountMasterCountBeforeRelease = user.achievements.mountMasterCount;
user.items.mounts[animal] = null;
releaseMounts(user);
expect(user.achievements.mountMasterCount).to.equal(mountMasterCountBeforeRelease);
});
it('does not increase mountMasterCount achievement if mount is missing (undefined)', () => {
let mountMasterCountBeforeRelease = user.achievements.mountMasterCount;
delete user.items.mounts[animal];
releaseMounts(user);
expect(user.achievements.mountMasterCount).to.equal(mountMasterCountBeforeRelease);
});
it('subtracts gems from balance', () => {
releaseMounts(user);

View File

@@ -14,8 +14,12 @@ describe('shared.ops.releasePets', () => {
beforeEach(() => {
user = generateUser();
for (let k in content.pets) {
user.items.pets[k] = content.pets[k];
user.items.pets[k] = 5;
}
user.items.currentPet = animal;
user.items.pets[animal] = 5;
user.balance = 1;
});
@@ -32,7 +36,7 @@ describe('shared.ops.releasePets', () => {
});
it('releases pets', () => {
let [, message] = releasePets(user);
let message = releasePets(user)[1];
expect(message).to.equal(i18n.t('petsReleased'));
expect(user.items.pets[animal]).to.equal(0);
@@ -69,4 +73,29 @@ describe('shared.ops.releasePets', () => {
expect(user.achievements.beastMasterCount).to.equal(1);
});
it('does not increment beastMasterCount if any pet is level 0 (released)', () => {
let beastMasterCountBeforeRelease = user.achievements.beastMasterCount;
user.items.pets[animal] = 0;
releasePets(user);
expect(user.achievements.beastMasterCount).to.equal(beastMasterCountBeforeRelease);
});
it('does not increment beastMasterCount if any pet is missing (null)', () => {
let beastMasterCountBeforeRelease = user.achievements.beastMasterCount;
user.items.pets[animal] = null;
releasePets(user);
expect(user.achievements.beastMasterCount).to.equal(beastMasterCountBeforeRelease);
});
it('does not increment beastMasterCount if any pet is missing (undefined)', () => {
let beastMasterCountBeforeRelease = user.achievements.beastMasterCount;
delete user.items.pets[animal];
releasePets(user);
expect(user.achievements.beastMasterCount).to.equal(beastMasterCountBeforeRelease);
});
});

View File

@@ -65,6 +65,16 @@ describe('shared.ops.sell', () => {
}
});
it('returns an error when the requested amount is above the available amount', (done) => {
try {
sell(user, {params: { type, key }, query: {amount: 2} });
} catch (err) {
expect(err).to.be.an.instanceof(NotFound);
expect(err.message).to.equal(i18n.t('userItemsNotEnough', {type}));
done();
}
});
it('reduces item count from user', () => {
sell(user, {params: { type, key } });

View File

@@ -38,6 +38,18 @@ module.exports = {
target: 'http://localhost:3000',
changeOrigin: true,
},
'/stripe': {
target: 'http://localhost:3000',
changeOrigin: true,
},
'/amazon': {
target: 'http://localhost:3000',
changeOrigin: true,
},
'/paypal': {
target: 'http://localhost:3000',
changeOrigin: true,
},
},
// CSS Sourcemaps off by default because relative paths are "buggy"
// with this option, according to the CSS-Loader README

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;

View File

@@ -1,6 +1,6 @@
.promo_android {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1748px 0px;
background-position: -139px -1483px;
width: 175px;
height: 175px;
}
@@ -12,25 +12,25 @@
}
.promo_backgrounds_armoire_201602 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1606px -295px;
background-position: -565px -894px;
width: 141px;
height: 294px;
}
.promo_backgrounds_armoire_201603 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1606px -590px;
background-position: -707px -894px;
width: 141px;
height: 294px;
}
.promo_backgrounds_armoire_201604 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1325px -441px;
background-position: -1608px -442px;
width: 140px;
height: 441px;
}
.promo_backgrounds_armoire_201605 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -987px -894px;
background-position: -1749px -442px;
width: 140px;
height: 441px;
}
@@ -48,67 +48,67 @@
}
.promo_backgrounds_armoire_201608 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1128px -894px;
background-position: -142px -894px;
width: 140px;
height: 439px;
}
.promo_backgrounds_armoire_201609 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1269px -894px;
background-position: -283px -894px;
width: 139px;
height: 438px;
}
.promo_backgrounds_armoire_201610 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -282px -894px;
background-position: -1467px -442px;
width: 140px;
height: 441px;
}
.promo_backgrounds_armoire_201611 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -141px -894px;
background-position: -1467px 0px;
width: 140px;
height: 441px;
}
.promo_backgrounds_armoire_201612 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: 0px -894px;
background-position: -1749px -884px;
width: 140px;
height: 441px;
}
.promo_backgrounds_armoire_201701 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -705px -894px;
background-position: -1749px 0px;
width: 140px;
height: 441px;
}
.promo_backgrounds_armoire_201702 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1041px 0px;
background-position: -1041px -442px;
width: 141px;
height: 441px;
}
.promo_backgrounds_armoire_201703 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1041px -442px;
background-position: -1183px 0px;
width: 141px;
height: 441px;
}
.promo_backgrounds_armoire_201704 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1183px 0px;
background-position: -1183px -442px;
width: 141px;
height: 441px;
}
.promo_backgrounds_armoire_201705 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -447px -148px;
background-position: -1325px 0px;
width: 141px;
height: 441px;
}
.promo_backgrounds_armoire_201706 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1183px -442px;
background-position: -1041px 0px;
width: 141px;
height: 441px;
}
@@ -118,18 +118,24 @@
width: 141px;
height: 441px;
}
.promo_bees {
.promo_backgrounds_armoire_201708 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -899px 0px;
width: 141px;
height: 441px;
}
.promo_bundle_feathered {
.promo_bees {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -757px -442px;
width: 141px;
height: 441px;
}
.promo_bundle_feathered {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1325px -442px;
width: 141px;
height: 441px;
}
.promo_bundle_splashy {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -757px 0px;
@@ -144,31 +150,31 @@
}
.promo_chairs_glasses {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1854px -352px;
background-position: -1994px -412px;
width: 51px;
height: 210px;
}
.promo_checkin_incentives {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1606px 0px;
background-position: -423px -894px;
width: 141px;
height: 294px;
}
.promo_classes_fall_2014 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: 0px -1484px;
background-position: -423px -1189px;
width: 321px;
height: 100px;
}
.promo_classes_fall_2015 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -823px -1336px;
background-position: -849px -1042px;
width: 377px;
height: 99px;
}
.promo_classes_fall_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1606px -885px;
background-position: -1890px 0px;
width: 103px;
height: 348px;
}
@@ -180,109 +186,115 @@
}
.promo_contrib_spotlight_Keith {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1467px -442px;
background-position: -1994px -1070px;
width: 87px;
height: 111px;
}
.promo_contrib_spotlight_alys {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1748px -1318px;
background-position: -1994px -847px;
width: 90px;
height: 110px;
}
.promo_contrib_spotlight_beffymaroo {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1748px -626px;
background-position: -491px -1483px;
width: 114px;
height: 147px;
}
.promo_contrib_spotlight_blade {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1748px -1429px;
background-position: -1994px -958px;
width: 89px;
height: 111px;
}
.promo_contrib_spotlight_cantras {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1467px -663px;
background-position: -1994px -1291px;
width: 87px;
height: 109px;
}
.promo_contrib_spotlight_dewines {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1467px -554px;
background-position: -1994px -1182px;
width: 89px;
height: 108px;
}
.promo_contrib_spotlight_megan {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1748px -1206px;
background-position: -1994px -623px;
width: 90px;
height: 111px;
}
.promo_contrib_spotlight_shanaqui {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1748px -1094px;
background-position: -1994px -735px;
width: 90px;
height: 111px;
}
.promo_cow {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -564px -894px;
background-position: -1608px 0px;
width: 140px;
height: 441px;
}
.promo_cupid_potions {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1467px 0px;
background-position: 0px -1335px;
width: 138px;
height: 441px;
}
.promo_dilatoryDistress {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1270px -1484px;
background-position: -956px -1335px;
width: 90px;
height: 90px;
}
.promo_egg_mounts {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: 0px -1336px;
background-position: -849px -894px;
width: 280px;
height: 147px;
}
.promo_ember_potions {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -447px -148px;
width: 141px;
height: 441px;
}
.promo_enchanted_armoire {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1201px -1336px;
background-position: -745px -1189px;
width: 374px;
height: 76px;
}
.promo_enchanted_armoire_201507 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -322px -1484px;
background-position: -1227px -1042px;
width: 217px;
height: 90px;
}
.promo_enchanted_armoire_201508 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -540px -1484px;
background-position: -654px -1335px;
width: 180px;
height: 90px;
}
.promo_enchanted_armoire_201509 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1088px -1484px;
background-position: -1890px -1532px;
width: 90px;
height: 90px;
}
.promo_enchanted_armoire_201511 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1748px -912px;
background-position: -140px -747px;
width: 122px;
height: 90px;
}
.promo_enchanted_armoire_201601 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -906px -1484px;
background-position: -1890px -895px;
width: 90px;
height: 90px;
}
@@ -294,19 +306,25 @@
}
.promo_floral_potions {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1748px -352px;
background-position: -1994px 0px;
width: 105px;
height: 273px;
}
.promo_ghost_potions {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -846px -894px;
background-position: -1467px -884px;
width: 140px;
height: 441px;
}
.promo_good_luck {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -526px -776px;
width: 110px;
height: 60px;
}
.promo_habitica {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1748px -176px;
background-position: -315px -1483px;
width: 175px;
height: 175px;
}
@@ -318,37 +336,37 @@
}
.promo_habitoween_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -423px -894px;
background-position: -1608px -884px;
width: 140px;
height: 441px;
}
.promo_haunted_hair {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1748px -774px;
background-position: -1994px -274px;
width: 100px;
height: 137px;
}
.promo_holly_potions {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1325px 0px;
background-position: 0px -894px;
width: 141px;
height: 440px;
}
.promo_item_notif {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: 0px -1585px;
background-position: -404px -1335px;
width: 249px;
height: 102px;
}
.promo_jackalope {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -281px -1336px;
background-position: -1130px -894px;
width: 276px;
height: 147px;
}
.promo_king_manta {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -558px -1336px;
background-position: -139px -1335px;
width: 264px;
height: 147px;
}
@@ -360,181 +378,163 @@
}
.promo_mystery_201405 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1543px -1484px;
background-position: -1890px -1077px;
width: 90px;
height: 90px;
}
.promo_mystery_201406 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1606px -1234px;
background-position: -1994px -1613px;
width: 90px;
height: 96px;
}
.promo_mystery_201407 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1854px -563px;
background-position: -1890px -1714px;
width: 42px;
height: 62px;
}
.promo_mystery_201408 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1839px -1206px;
background-position: -1120px -1189px;
width: 60px;
height: 71px;
}
.promo_mystery_201409 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1634px -1484px;
background-position: -1890px -713px;
width: 90px;
height: 90px;
}
.promo_mystery_201410 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1839px -1094px;
background-position: -1994px -1710px;
width: 72px;
height: 63px;
}
.promo_mystery_201411 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -614px -1585px;
background-position: -1890px -622px;
width: 90px;
height: 90px;
}
.promo_mystery_201412 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1869px -1003px;
background-position: -2046px -546px;
width: 42px;
height: 66px;
}
.promo_mystery_201501 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1863px -708px;
background-position: -2046px -412px;
width: 48px;
height: 63px;
}
.promo_mystery_201502 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -815px -1484px;
background-position: -1890px -1168px;
width: 90px;
height: 90px;
}
.promo_mystery_201503 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1179px -1484px;
background-position: -1890px -1259px;
width: 90px;
height: 90px;
}
.promo_mystery_201504 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1839px -1318px;
background-position: -1181px -1189px;
width: 60px;
height: 69px;
}
.promo_mystery_201505 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1361px -1484px;
background-position: -1890px -1441px;
width: 90px;
height: 90px;
}
.promo_mystery_201506 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1871px -912px;
background-position: -2046px -476px;
width: 42px;
height: 69px;
}
.promo_mystery_201507 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1467px -773px;
background-position: -1994px -1507px;
width: 90px;
height: 105px;
}
.promo_mystery_201508 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -140px -747px;
background-position: -1890px -531px;
width: 93px;
height: 90px;
}
.promo_mystery_201509 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -250px -1585px;
background-position: -1047px -1335px;
width: 90px;
height: 90px;
}
.promo_mystery_201510 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -721px -1484px;
background-position: -1890px -440px;
width: 93px;
height: 90px;
}
.promo_mystery_201511 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -432px -1585px;
background-position: -1890px -1350px;
width: 90px;
height: 90px;
}
.promo_mystery_201512 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1863px -626px;
background-position: -1229px -1335px;
width: 60px;
height: 81px;
}
.promo_mystery_201601 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1748px -1003px;
background-position: -835px -1335px;
width: 120px;
height: 90px;
}
.promo_mystery_201602 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -523px -1585px;
background-position: -1890px -986px;
width: 90px;
height: 90px;
}
.promo_mystery_201603 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1452px -1484px;
background-position: -1890px -804px;
width: 90px;
height: 90px;
}
.promo_mystery_201604 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1467px -1076px;
background-position: -1890px -349px;
width: 93px;
height: 90px;
}
.promo_mystery_201605 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -997px -1484px;
background-position: -1890px -1623px;
width: 90px;
height: 90px;
}
.promo_mystery_201606 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1467px -879px;
background-position: -1994px -1401px;
width: 90px;
height: 105px;
}
.promo_mystery_201607 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -341px -1585px;
background-position: -1138px -1335px;
width: 90px;
height: 90px;
}
.promo_mystery_201608 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1467px -985px;
width: 93px;
height: 90px;
}
.promo_mystery_201609 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1467px -1167px;
width: 93px;
height: 90px;
}
.promo_mystery_201610 {
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
background-position: -1849px -774px;
width: 63px;
height: 84px;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 928 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@@ -1,462 +1,486 @@
.promo_mystery_201608 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -949px -1619px;
width: 93px;
height: 90px;
}
.promo_mystery_201609 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -855px -1619px;
width: 93px;
height: 90px;
}
.promo_mystery_201610 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1845px -1219px;
width: 63px;
height: 84px;
}
.promo_mystery_201611 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1571px -1083px;
background-position: -120px -1770px;
width: 90px;
height: 99px;
}
.promo_mystery_201612 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -586px 0px;
background-position: 0px 0px;
width: 558px;
height: 294px;
}
.promo_mystery_201701 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1712px -300px;
background-position: -977px -515px;
width: 90px;
height: 105px;
}
.promo_mystery_201702 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -584px -1538px;
background-position: -1625px -742px;
width: 279px;
height: 147px;
}
.promo_mystery_201703 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1040px -1247px;
background-position: -1625px -299px;
width: 282px;
height: 147px;
}
.promo_mystery_201704 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1680px -992px;
background-position: -1043px -1619px;
width: 90px;
height: 90px;
}
.promo_mystery_201705 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -301px -1538px;
background-position: -1625px -151px;
width: 282px;
height: 147px;
}
.promo_mystery_201706 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1963px -934px;
background-position: -1084px -737px;
width: 111px;
height: 90px;
}
.promo_mystery_201707 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -749px -1619px;
width: 105px;
height: 90px;
}
.promo_mystery_201708 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1134px -1619px;
width: 90px;
height: 90px;
}
.promo_mystery_3014 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1812px -1381px;
background-position: -615px -1508px;
width: 217px;
height: 90px;
}
.promo_naming_day_2017 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1766px -595px;
width: 159px;
height: 141px;
}
.promo_new_hair_fall2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1429px -442px;
background-position: 0px -1066px;
width: 140px;
height: 441px;
}
.promo_orca {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1963px -783px;
background-position: -1377px -895px;
width: 105px;
height: 105px;
}
.promo_orcas {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1571px -844px;
background-position: -1625px -1219px;
width: 219px;
height: 147px;
}
.promo_partyhats {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1963px -1025px;
background-position: -1625px -1456px;
width: 115px;
height: 47px;
}
.promo_pastel_skin {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1110px -1422px;
background-position: -273px -792px;
width: 330px;
height: 83px;
}
.customize-option.promo_pastel_skin {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1135px -1437px;
background-position: -298px -807px;
width: 60px;
height: 60px;
}
.promo_pastel_skin_hair {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -824px -1096px;
background-position: -965px -1066px;
width: 354px;
height: 147px;
}
.customize-option.promo_pastel_skin_hair {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -849px -1111px;
background-position: -990px -1081px;
width: 60px;
height: 60px;
}
.promo_peppermint_flame {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1953px -1085px;
background-position: -1483px -884px;
width: 140px;
height: 147px;
}
.promo_pet_skins {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1812px -1085px;
background-position: -1084px -589px;
width: 140px;
height: 147px;
}
.customize-option.promo_pet_skins {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1837px -1100px;
background-position: -1109px -604px;
width: 60px;
height: 60px;
}
.promo_pyromancer {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1963px -632px;
background-position: -1368px -552px;
width: 113px;
height: 113px;
}
.promo_rainbow_armor {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1712px -196px;
background-position: -1833px -1038px;
width: 92px;
height: 103px;
}
.promo_redesign_header {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -849px -295px;
width: 166px;
height: 188px;
}
.promo_redesign_login {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -559px 0px;
width: 524px;
height: 274px;
}
.promo_redesign_tavern {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -601px -515px;
width: 375px;
height: 186px;
}
.promo_sdcc_2017 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: 0px -515px;
width: 272px;
height: 363px;
}
.promo_seafoam {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1287px 0px;
background-position: -1226px -257px;
width: 141px;
height: 441px;
}
.promo_seasonal_shop_fall_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -864px -1538px;
background-position: -1625px -890px;
width: 279px;
height: 147px;
}
.promo_shimmer_hair {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1144px -1538px;
background-position: -601px -702px;
width: 330px;
height: 83px;
}
.promo_shimmer_potions {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1429px 0px;
background-position: -1483px 0px;
width: 141px;
height: 441px;
}
.promo_shinySeeds {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1287px -442px;
background-position: -1483px -442px;
width: 141px;
height: 441px;
}
.promo_splashy_skins {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -725px -562px;
background-position: 0px -879px;
width: 375px;
height: 186px;
}
.customize-option.promo_splashy_skins {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -750px -577px;
background-position: -25px -894px;
width: 60px;
height: 60px;
}
.promo_splashyskins {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1812px -1472px;
background-position: -441px -1619px;
width: 198px;
height: 91px;
}
.customize-option.promo_splashyskins {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1837px -1487px;
background-position: -466px -1634px;
width: 60px;
height: 60px;
}
.promo_spooky_sparkles_fall_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1571px -196px;
background-position: -1625px -447px;
width: 140px;
height: 294px;
}
.promo_spring_classes_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -687px -984px;
background-position: -809px -879px;
width: 362px;
height: 102px;
}
.promo_spring_classes_2017 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -730px -1247px;
background-position: -564px -1313px;
width: 309px;
height: 147px;
}
.promo_springclasses2014 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1812px -91px;
background-position: -1320px -1066px;
width: 288px;
height: 90px;
}
.promo_springclasses2015 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1812px 0px;
background-position: -326px -1508px;
width: 288px;
height: 90px;
}
.promo_staff_spotlight_Lemoness {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1963px -330px;
background-position: -1368px -257px;
width: 102px;
height: 146px;
}
.promo_staff_spotlight_Viirus {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1969px -182px;
background-position: 0px -1770px;
width: 119px;
height: 147px;
}
.promo_staff_spotlight_paglias {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1963px -481px;
background-position: -1368px -404px;
width: 99px;
height: 147px;
}
.promo_steampunk_3017 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -282px -1096px;
background-position: -423px -1066px;
width: 140px;
height: 441px;
}
.promo_summer_classes_2014 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -257px -984px;
background-position: -1184px -1313px;
width: 429px;
height: 102px;
}
.promo_summer_classes_2015 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: 0px -1689px;
background-position: -1625px -1367px;
width: 300px;
height: 88px;
}
.promo_summer_classes_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -423px -1096px;
background-position: -564px -1066px;
width: 400px;
height: 150px;
}
.promo_summer_classes_2017 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1145px 0px;
background-position: -1084px 0px;
width: 141px;
height: 588px;
}
.promo_takeThis_gear {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1812px -1640px;
background-position: -833px -1508px;
width: 114px;
height: 87px;
}
.promo_takethis_armor {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1927px -1640px;
background-position: -1424px -1217px;
width: 114px;
height: 87px;
}
.promo_task_planning {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1571px 0px;
background-position: -1226px -699px;
width: 240px;
height: 195px;
}
.promo_turkey_day_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -141px -1096px;
background-position: -282px -1066px;
width: 140px;
height: 441px;
}
.promo_unconventional_armor {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1951px -1309px;
background-position: -1845px -1304px;
width: 60px;
height: 60px;
}
.promo_unconventional_armor2 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -2030px -1381px;
background-position: -1833px -1142px;
width: 70px;
height: 74px;
}
.promo_updos {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1812px -182px;
background-position: -1766px -447px;
width: 156px;
height: 147px;
}
.promo_veteran_pets {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1951px -1233px;
background-position: -932px -702px;
width: 146px;
height: 75px;
}
.promo_winter_classes_2016 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -749px -1422px;
background-position: -1063px -1217px;
width: 360px;
height: 90px;
}
.promo_winter_classes_2017 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -257px -839px;
background-position: -376px -879px;
width: 432px;
height: 144px;
}
.promo_winter_fireworks {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1812px -1233px;
background-position: -302px -1619px;
width: 138px;
height: 147px;
}
.promo_winterclasses2015 {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -423px -1422px;
background-position: 0px -1508px;
width: 325px;
height: 110px;
}
.promo_wintery_skins {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: 0px -1096px;
background-position: -141px -1066px;
width: 140px;
height: 441px;
}
.customize-option.promo_wintery_skins {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -25px -1111px;
background-position: -166px -1081px;
width: 60px;
height: 60px;
}
.promo_winteryhair {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1812px -1564px;
background-position: -604px -792px;
width: 152px;
height: 75px;
}
.avatar_variety {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -690px -839px;
background-position: -564px -1217px;
width: 498px;
height: 95px;
}
.npc_viirus {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1571px -992px;
background-position: -640px -1619px;
width: 108px;
height: 90px;
}
.party_preview {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -499px -312px;
background-position: 0px -295px;
width: 451px;
height: 219px;
}
.promo_backtoschool {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1812px -330px;
background-position: 0px -1619px;
width: 150px;
height: 150px;
}
.promo_cooking {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -328px -562px;
background-position: -452px -295px;
width: 396px;
height: 219px;
}
.promo_startingover {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1812px -783px;
background-position: -151px -1619px;
width: 150px;
height: 150px;
}
.promo_valentines {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1179px -1096px;
background-position: -874px -1313px;
width: 309px;
height: 147px;
}
.promo_working_out {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: 0px -1538px;
background-position: -1625px 0px;
width: 300px;
height: 150px;
}
.scene_arts_crafts {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: 0px -839px;
background-position: -1226px 0px;
width: 256px;
height: 256px;
}
.scene_buying_rewards {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1571px -663px;
background-position: -1625px -1038px;
width: 207px;
height: 180px;
}
.scene_coding {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1812px -632px;
background-position: -1226px -895px;
width: 150px;
height: 150px;
}
.scene_dailies {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: 0px -562px;
background-position: -273px -515px;
width: 327px;
height: 276px;
}
.scene_eco_friendly {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1571px -491px;
width: 222px;
height: 171px;
}
.scene_guilds {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: 0px -312px;
width: 498px;
height: 249px;
}
.scene_habitica_house {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: 0px 0px;
width: 585px;
height: 311px;
}
.scene_habits {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -423px -1247px;
width: 306px;
height: 174px;
}
.scene_meditation {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1812px -481px;
width: 150px;
height: 150px;
}
.scene_phone_peek {
background-image: url(/static/sprites/spritesmith-largeSprites-1.png);
background-position: -1812px -934px;
width: 150px;
height: 150px;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 996 KiB

After

Width:  |  Height:  |  Size: 873 KiB

View File

@@ -1,36 +1,90 @@
.scene_eco_friendly {
background-image: url(/static/sprites/spritesmith-largeSprites-2.png);
background-position: -518px -737px;
width: 222px;
height: 171px;
}
.scene_guilds {
background-image: url(/static/sprites/spritesmith-largeSprites-2.png);
background-position: 0px -312px;
width: 498px;
height: 249px;
}
.scene_habitica_house {
background-image: url(/static/sprites/spritesmith-largeSprites-2.png);
background-position: 0px 0px;
width: 585px;
height: 311px;
}
.scene_habits {
background-image: url(/static/sprites/spritesmith-largeSprites-2.png);
background-position: 0px -562px;
width: 306px;
height: 174px;
}
.scene_meditation {
background-image: url(/static/sprites/spritesmith-largeSprites-2.png);
background-position: -926px -384px;
width: 150px;
height: 150px;
}
.scene_pet_hatching {
background-image: url(/static/sprites/spritesmith-largeSprites-2.png);
background-position: -586px -343px;
width: 315px;
height: 195px;
}
.scene_phone_peek {
background-image: url(/static/sprites/spritesmith-largeSprites-2.png);
background-position: -926px -233px;
width: 150px;
height: 150px;
}
.scene_raking_leaves {
background-image: url(/static/sprites/spritesmith-largeSprites-2.png);
background-position: -340px 0px;
background-position: 0px -737px;
width: 246px;
height: 198px;
}
.scene_studying {
background-image: url(/static/sprites/spritesmith-largeSprites-2.png);
background-position: -926px 0px;
width: 220px;
height: 232px;
}
.scene_task_list {
background-image: url(/static/sprites/spritesmith-largeSprites-2.png);
background-position: 0px -936px;
width: 240px;
height: 195px;
}
.scene_todos {
background-image: url(/static/sprites/spritesmith-largeSprites-2.png);
background-position: -587px 0px;
background-position: -241px -936px;
width: 240px;
height: 195px;
}
.scene_video_games {
background-image: url(/static/sprites/spritesmith-largeSprites-2.png);
background-position: 0px 0px;
background-position: -586px 0px;
width: 339px;
height: 342px;
}
.welcome_basic_avatars {
background-image: url(/static/sprites/spritesmith-largeSprites-2.png);
background-position: -271px -343px;
background-position: -307px -562px;
width: 246px;
height: 165px;
}
.welcome_promo_party {
background-image: url(/static/sprites/spritesmith-largeSprites-2.png);
background-position: 0px -343px;
background-position: -247px -737px;
width: 270px;
height: 180px;
}
.welcome_sample_tasks {
background-image: url(/static/sprites/spritesmith-largeSprites-2.png);
background-position: 0px -524px;
background-position: -554px -562px;
width: 246px;
height: 165px;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 447 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 518 KiB

After

Width:  |  Height:  |  Size: 566 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 248 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 155 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 152 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

After

Width:  |  Height:  |  Size: 142 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 148 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

After

Width:  |  Height:  |  Size: 144 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 156 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

After

Width:  |  Height:  |  Size: 174 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 160 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 147 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 54 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 77 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 74 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 165 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 163 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 140 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 456 KiB

After

Width:  |  Height:  |  Size: 346 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 KiB

After

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

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