mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-13 12:47:28 +01:00
* Simplify cron code
use transactions for cron
remove only
bump mongoose to 8.x
remove deprecated config
fix race condition when users join a party
console debugging time
try calling transaction differently
add missing await
addditional console log
.
..
...
….
await
more debug log
mongoose logging
more logging
move session to encapsulate all of cron
delete old todos before fetching all tasks
changes
try waiting for mongoose connection
try adding timeout to time jump
cleanup and code refactoring
Translated using Weblate (Spanish)
Currently translated at 100.0% (821 of 821 strings)
Translated using Weblate (German)
Currently translated at 100.0% (821 of 821 strings)
Translated using Weblate (Japanese)
Currently translated at 87.0% (228 of 262 strings)
Translated using Weblate (Spanish (Latin America))
Currently translated at 93.8% (107 of 114 strings)
Translated using Weblate (Spanish (Latin America))
Currently translated at 18.1% (44 of 243 strings)
Translated using Weblate (Spanish (Latin America))
Currently translated at 11.9% (29 of 243 strings)
Translated using Weblate (Spanish (Latin America))
Currently translated at 88.1% (724 of 821 strings)
Translated using Weblate (Spanish (Latin America))
Currently translated at 100.0% (22 of 22 strings)
Translated using Weblate (Spanish (Latin America))
Currently translated at 91.2% (104 of 114 strings)
Translated using Weblate (Spanish (Latin America))
Currently translated at 7.4% (18 of 243 strings)
Translated using Weblate (Spanish)
Currently translated at 99.5% (817 of 821 strings)
Translated using Weblate (German)
Currently translated at 99.3% (816 of 821 strings)
Translated using Weblate (German)
Currently translated at 100.0% (3265 of 3265 strings)
Translated using Weblate (German)
Currently translated at 100.0% (3265 of 3265 strings)
Translated using Weblate (English (United Kingdom))
Currently translated at 98.2% (112 of 114 strings)
Translated using Weblate (English (United Kingdom))
Currently translated at 97.7% (131 of 134 strings)
Translated using Weblate (English (United Kingdom))
Currently translated at 69.1% (2257 of 3265 strings)
Translated using Weblate (English (United Kingdom))
Currently translated at 99.5% (239 of 240 strings)
Translated using Weblate (English (United Kingdom))
Currently translated at 16.4% (40 of 243 strings)
Translated using Weblate (German)
Currently translated at 99.9% (3264 of 3265 strings)
Translated using Weblate (Japanese)
Currently translated at 86.6% (227 of 262 strings)
Translated using Weblate (Japanese)
Currently translated at 100.0% (272 of 272 strings)
Translated using Weblate (Japanese)
Currently translated at 97.9% (423 of 432 strings)
Translated using Weblate (German)
Currently translated at 100.0% (262 of 262 strings)
Translated using Weblate (German)
Currently translated at 100.0% (432 of 432 strings)
Translated using Weblate (German)
Currently translated at 100.0% (240 of 240 strings)
Translated using Weblate (English (United Kingdom))
Currently translated at 14.8% (36 of 243 strings)
Translated using Weblate (German)
Currently translated at 99.1% (814 of 821 strings)
Translated using Weblate (German)
Currently translated at 100.0% (397 of 397 strings)
Translated using Weblate (Portuguese (Brazil))
Currently translated at 84.7% (222 of 262 strings)
Translated using Weblate (English (United Kingdom))
Currently translated at 84.3% (221 of 262 strings)
Translated using Weblate (German)
Currently translated at 100.0% (60 of 60 strings)
Translated using Weblate (English (United Kingdom))
Currently translated at 100.0% (22 of 22 strings)
Translated using Weblate (Portuguese (Brazil))
Currently translated at 96.0% (415 of 432 strings)
Translated using Weblate (Portuguese (Brazil))
Currently translated at 94.2% (3077 of 3265 strings)
Translated using Weblate (Portuguese (Brazil))
Currently translated at 63.7% (155 of 243 strings)
Translated using Weblate (German)
Currently translated at 99.0% (813 of 821 strings)
Translated using Weblate (English (United Kingdom))
Currently translated at 99.7% (396 of 397 strings)
Translated using Weblate (Portuguese (Brazil))
Currently translated at 98.7% (885 of 896 strings)
Translated using Weblate (English (United Kingdom))
Currently translated at 100.0% (896 of 896 strings)
Translated using Weblate (German)
Currently translated at 100.0% (896 of 896 strings)
Translated using Weblate (Japanese)
Currently translated at 97.4% (265 of 272 strings)
Translated using Weblate (Japanese)
Currently translated at 100.0% (114 of 114 strings)
Translated using Weblate (Japanese)
Currently translated at 100.0% (134 of 134 strings)
Translated using Weblate (Japanese)
Currently translated at 100.0% (260 of 260 strings)
Translated using Weblate (Japanese)
Currently translated at 100.0% (896 of 896 strings)
Translated using Weblate (Japanese)
Currently translated at 100.0% (397 of 397 strings)
Translated using Weblate (Japanese)
Currently translated at 100.0% (60 of 60 strings)
Update translation files
Updated by "Cleanup translation files" hook in Weblate.
Translated using Weblate (Japanese)
Currently translated at 98.7% (392 of 397 strings)
Translated using Weblate (Japanese)
Currently translated at 100.0% (240 of 240 strings)
Translated using Weblate (Spanish)
Currently translated at 100.0% (262 of 262 strings)
Translated using Weblate (Spanish)
Currently translated at 100.0% (114 of 114 strings)
Translated using Weblate (Spanish)
Currently translated at 100.0% (60 of 60 strings)
Translated using Weblate (Spanish)
Currently translated at 100.0% (432 of 432 strings)
Translated using Weblate (Spanish)
Currently translated at 100.0% (240 of 240 strings)
Translated using Weblate (Spanish)
Currently translated at 99.0% (813 of 821 strings)
Translated using Weblate (Spanish)
Currently translated at 100.0% (397 of 397 strings)
Translated using Weblate (Spanish)
Currently translated at 100.0% (896 of 896 strings)
Translated using Weblate (French)
Currently translated at 100.0% (262 of 262 strings)
Translated using Weblate (French)
Currently translated at 100.0% (60 of 60 strings)
Translated using Weblate (French)
Currently translated at 100.0% (432 of 432 strings)
Translated using Weblate (French)
Currently translated at 100.0% (3265 of 3265 strings)
Translated using Weblate (French)
Currently translated at 100.0% (240 of 240 strings)
Translated using Weblate (French)
Currently translated at 100.0% (821 of 821 strings)
Translated using Weblate (French)
Currently translated at 100.0% (397 of 397 strings)
Translated using Weblate (French)
Currently translated at 100.0% (896 of 896 strings)
Translated using Weblate (French)
Currently translated at 100.0% (3265 of 3265 strings)
Update translation files
Updated by "Cleanup translation files" hook in Weblate.
Update translation files
Updated by "Cleanup translation files" hook in Weblate.
Translated using Weblate (French)
Currently translated at 100.0% (3265 of 3265 strings)
Translated using Weblate (Spanish)
Currently translated at 100.0% (3255 of 3255 strings)
Translated using Weblate (Spanish)
Currently translated at 100.0% (15 of 15 strings)
Co-authored-by: Asier Gallego <agr2367789@gmail.com>
Co-authored-by: Asier Gallego Roca <asiernoide@users.noreply.translate.habitica.com>
Co-authored-by: Henrique Ferreira <pedroferreira217.ph@gmail.com>
Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
Co-authored-by: John Doe (Anonymous) <shyamjayeshduck@duck.com>
Co-authored-by: Katharina <katharinaanna.wilding@gmail.com>
Co-authored-by: Marie Blosse--Gilbin <mbgil@hotmail.fr>
Co-authored-by: Mauricio Pérez <mauriciodavidperez@gmail.com>
Co-authored-by: Raul Ernesto Ceron Lara <raztreuzz1234@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Willhelm Winter <carapax@posteo.de>
Co-authored-by: mattya 226 <worldworld1114@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/de/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/content/de/
Translate-URL: https://translate.habitica.com/projects/habitica/content/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/content/es/
Translate-URL: https://translate.habitica.com/projects/habitica/content/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/content/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/death/es/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/de/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/es/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/de/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/es/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/de/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/es/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/es/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/de/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/es/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/de/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/es/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/pt_BR/
Translation: Habitica/Backgrounds
Translation: Habitica/Content
Translation: Habitica/Death
Translation: Habitica/Faq
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Loginincentives
Translation: Habitica/Messages
Translation: Habitica/Npc
Translation: Habitica/Pets
Translation: Habitica/Questscontent
Translation: Habitica/Settings
Translation: Habitica/Subscriber
5.33.1
fix(links): next round of wiki revisions
Translated using Weblate (German)
Currently translated at 100.0% (896 of 896 strings)
Translated using Weblate (Portuguese)
Currently translated at 96.4% (864 of 896 strings)
Co-authored-by: Miya <baddybadges@gmail.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/de/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt/
Translation: Habitica/Backgrounds
5.33.2
Fix achievement display in admin panel (#15326)
Fix news related permission issues (#15287)
Support sprite version of armoire icon (#15354)
* Use sprite component for armoire sprite
* use gif version of armoire sprite
* fix(import): sprite component path
---------
Co-authored-by: Kalista Payne <sabrecat@gmail.com>
log slow requests to loggly (#15364)
Update .eslintrc.js (#15388)
Add `require-await` to eslint config
Translated using Weblate (Japanese)
Currently translated at 93.0% (764 of 821 strings)
Translated using Weblate (Hungarian)
Currently translated at 54.8% (1790 of 3265 strings)
Translated using Weblate (Hungarian)
Currently translated at 53.5% (1748 of 3265 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (896 of 896 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (262 of 262 strings)
Translated using Weblate (Hungarian)
Currently translated at 52.1% (1704 of 3265 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (896 of 896 strings)
Translated using Weblate (Hungarian)
Currently translated at 59.3% (532 of 896 strings)
Translated using Weblate (Hungarian)
Currently translated at 79.3% (208 of 262 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (13 of 13 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (432 of 432 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (245 of 245 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (91 of 91 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (896 of 896 strings)
Translated using Weblate (Spanish (Latin America))
Currently translated at 77.4% (2528 of 3265 strings)
Translated using Weblate (Spanish (Latin America))
Currently translated at 100.0% (167 of 167 strings)
Translated using Weblate (Spanish (Latin America))
Currently translated at 100.0% (167 of 167 strings)
Translated using Weblate (Spanish (Latin America))
Currently translated at 100.0% (167 of 167 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (272 of 272 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 93.0% (764 of 821 strings)
Translated using Weblate (French)
Currently translated at 100.0% (193 of 193 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (896 of 896 strings)
Translated using Weblate (French)
Currently translated at 100.0% (260 of 260 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (896 of 896 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (896 of 896 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (896 of 896 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (896 of 896 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (896 of 896 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (896 of 896 strings)
Translated using Weblate (Hungarian)
Currently translated at 94.8% (258 of 272 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (245 of 245 strings)
Translated using Weblate (Portuguese (Brazil))
Currently translated at 95.2% (378 of 397 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (896 of 896 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (896 of 896 strings)
Translated using Weblate (Hungarian)
Currently translated at 82.8% (203 of 245 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (821 of 821 strings)
Translated using Weblate (Portuguese (Brazil))
Currently translated at 94.9% (377 of 397 strings)
Translated using Weblate (Hungarian)
Currently translated at 52.1% (1704 of 3265 strings)
Translated using Weblate (Hungarian)
Currently translated at 49.7% (122 of 245 strings)
Translated using Weblate (Portuguese (Brazil))
Currently translated at 96.1% (789 of 821 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (821 of 821 strings)
Translated using Weblate (Hungarian)
Currently translated at 48.5% (119 of 245 strings)
Translated using Weblate (Hungarian)
Currently translated at 26.1% (64 of 245 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (2 of 2 strings)
Translated using Weblate (Hungarian)
Currently translated at 8.9% (22 of 245 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (15 of 15 strings)
Translated using Weblate (Hungarian)
Currently translated at 96.2% (790 of 821 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (896 of 896 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (896 of 896 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (896 of 896 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (896 of 896 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (22 of 22 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (432 of 432 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (240 of 240 strings)
Translated using Weblate (Portuguese (Brazil))
Currently translated at 95.4% (784 of 821 strings)
Translated using Weblate (Hungarian)
Currently translated at 91.5% (752 of 821 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (91 of 91 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (397 of 397 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (47 of 47 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (193 of 193 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (896 of 896 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (896 of 896 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (114 of 114 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (60 of 60 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (397 of 397 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (193 of 193 strings)
Translated using Weblate (Spanish)
Currently translated at 100.0% (193 of 193 strings)
Translated using Weblate (Spanish)
Currently translated at 100.0% (260 of 260 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (94 of 94 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (134 of 134 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (240 of 240 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (47 of 47 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (260 of 260 strings)
Translated using Weblate (German)
Currently translated at 99.2% (133 of 134 strings)
Translated using Weblate (German)
Currently translated at 99.2% (133 of 134 strings)
Translated using Weblate (Czech)
Currently translated at 95.2% (159 of 167 strings)
Translated using Weblate (Russian)
Currently translated at 91.2% (2978 of 3265 strings)
Translated using Weblate (Russian)
Currently translated at 99.3% (890 of 896 strings)
Translated using Weblate (German)
Currently translated at 100.0% (3265 of 3265 strings)
Translated using Weblate (German)
Currently translated at 100.0% (3265 of 3265 strings)
Translated using Weblate (Spanish)
Currently translated at 100.0% (134 of 134 strings)
Translated using Weblate (Spanish)
Currently translated at 100.0% (47 of 47 strings)
Translated using Weblate (French)
Currently translated at 100.0% (134 of 134 strings)
Translated using Weblate (French)
Currently translated at 100.0% (47 of 47 strings)
Translated using Weblate (Korean)
Currently translated at 100.0% (56 of 56 strings)
Translated using Weblate (Korean)
Currently translated at 98.5% (132 of 134 strings)
Translated using Weblate (Korean)
Currently translated at 6.9% (17 of 245 strings)
Translated using Weblate (Korean)
Currently translated at 71.9% (645 of 896 strings)
Translated using Weblate (Korean)
Currently translated at 49.2% (129 of 262 strings)
Translated using Weblate (Korean)
Currently translated at 100.0% (13 of 13 strings)
Translated using Weblate (Korean)
Currently translated at 81.9% (77 of 94 strings)
Translated using Weblate (Korean)
Currently translated at 91.6% (153 of 167 strings)
Translated using Weblate (Korean)
Currently translated at 67.3% (291 of 432 strings)
Translated using Weblate (Korean)
Currently translated at 79.5% (191 of 240 strings)
Translated using Weblate (Korean)
Currently translated at 54.6% (1785 of 3265 strings)
Translated using Weblate (Korean)
Currently translated at 88.8% (48 of 54 strings)
Translated using Weblate (Korean)
Currently translated at 89.3% (42 of 47 strings)
Translated using Weblate (Korean)
Currently translated at 93.9% (373 of 397 strings)
Translated using Weblate (Korean)
Currently translated at 54.9% (50 of 91 strings)
Translated using Weblate (German)
Currently translated at 100.0% (182 of 182 strings)
Translated using Weblate (German)
Currently translated at 100.0% (182 of 182 strings)
Translated using Weblate (Spanish (Latin America))
Currently translated at 73.0% (179 of 245 strings)
Translated using Weblate (French)
Currently translated at 100.0% (245 of 245 strings)
Translated using Weblate (German)
Currently translated at 99.1% (243 of 245 strings)
Translated using Weblate (French)
Currently translated at 99.5% (244 of 245 strings)
Translated using Weblate (Spanish (Latin America))
Currently translated at 62.0% (152 of 245 strings)
Translated using Weblate (Indonesian)
Currently translated at 73.4% (180 of 245 strings)
Translated using Weblate (Indonesian)
Currently translated at 96.0% (861 of 896 strings)
Translated using Weblate (Spanish (Latin America))
Currently translated at 62.0% (152 of 245 strings)
Translated using Weblate (German)
Currently translated at 98.7% (242 of 245 strings)
Translated using Weblate (Spanish)
Currently translated at 100.0% (245 of 245 strings)
Translated using Weblate (Spanish)
Currently translated at 99.5% (244 of 245 strings)
Translated using Weblate (Portuguese)
Currently translated at 33.7% (82 of 243 strings)
Translated using Weblate (Portuguese)
Currently translated at 73.3% (602 of 821 strings)
Translated using Weblate (Portuguese)
Currently translated at 56.0% (51 of 91 strings)
Translated using Weblate (German)
Currently translated at 100.0% (167 of 167 strings)
Translated using Weblate (German)
Currently translated at 100.0% (167 of 167 strings)
Translated using Weblate (Portuguese)
Currently translated at 97.2% (107 of 110 strings)
Translated using Weblate (Portuguese)
Currently translated at 100.0% (896 of 896 strings)
Translated using Weblate (Portuguese)
Currently translated at 100.0% (896 of 896 strings)
Translated using Weblate (Portuguese)
Currently translated at 100.0% (896 of 896 strings)
Co-authored-by: César Orlando Pallares Delgado <copdeb@gmail.com>
Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: Diego Benitez <diego.benitez@bigpond.com>
Co-authored-by: Finrod <963505255@qq.com>
Co-authored-by: ForbiddenFigs <sorautai@outlook.com>
Co-authored-by: Hexe des Windes (she/her) <krausanna1@gmail.com>
Co-authored-by: Icaro <icaro.mascarenhas@outlook.com>
Co-authored-by: Ikmal <ikmal.s.16@gmail.com>
Co-authored-by: Jackal <qwerty70244@gmail.com>
Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
Co-authored-by: Katharina <katharinaanna.wilding@gmail.com>
Co-authored-by: Leslie Munguía <moongeeuh@gmail.com>
Co-authored-by: Lio Zam <zerofux@web.de>
Co-authored-by: Marius <mariusschmid11@gmail.com>
Co-authored-by: Miya <baddybadges@gmail.com>
Co-authored-by: Natalie Luhrs <eilatan@gmail.com>
Co-authored-by: Raul Ernesto Ceron Lara <raztreuzz1234@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: Viktor Révész <rviktor@ivankapal.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: 小王 <963505255@qq.com>
Co-authored-by: 이채린 <cofls1256@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/cs/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/de/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/id/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/character/es/
Translate-URL: https://translate.habitica.com/projects/habitica/character/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/character/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/character/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/content/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/content/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/content/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/content/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/es/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/death/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/defaulttasks/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/de/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/es/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/id/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/front/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/noscript/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/de/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/es/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/es/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/spells/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ko/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Communityguidelines
Translation: Habitica/Content
Translation: Habitica/Contrib
Translation: Habitica/Death
Translation: Habitica/Defaulttasks
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Loginincentives
Translation: Habitica/Messages
Translation: Habitica/Noscript
Translation: Habitica/Npc
Translation: Habitica/Pets
Translation: Habitica/Quests
Translation: Habitica/Questscontent
Translation: Habitica/Rebirth
Translation: Habitica/Settings
Translation: Habitica/Spells
Translation: Habitica/Subscriber
5.33.3
March 2025 Content Build (#15392)
* build: March 2025 css, backgrounds, subscriber gear, armoire
* build: March 2025 quests, seasonal gear, various fixes
* fix: fix string
* fix: fixes to string errors
* fix: string fixes
wait for mongoose connection on timetravel
rework broken cron recovery
remove lodash from cron code
remove old cron notification
Simplify cron code
fix unit tests
Remove unnecessary user fetch
Further code simplification
fix test check
lint fix
disable world boss calculation during cron for now
prevent saving user twice in paralllel when leaving group plan
correctly call cron in api call
remove console
fix tests failing
mark cronSignature as modified
fix test
Translated using Weblate (Spanish)
Currently translated at 99.5% (3288 of 3303 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (836 of 836 strings)
Translated using Weblate (Spanish)
Currently translated at 100.0% (836 of 836 strings)
Translated using Weblate (Spanish)
Currently translated at 100.0% (899 of 899 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (836 of 836 strings)
Translated using Weblate (Spanish)
Currently translated at 99.5% (832 of 836 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (836 of 836 strings)
Translated using Weblate (German)
Currently translated at 98.8% (826 of 836 strings)
Translated using Weblate (Russian)
Currently translated at 40.8% (100 of 245 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (836 of 836 strings)
Translated using Weblate (French)
Currently translated at 100.0% (836 of 836 strings)
Translated using Weblate (Russian)
Currently translated at 40.4% (99 of 245 strings)
Translated using Weblate (Russian)
Currently translated at 40.0% (98 of 245 strings)
Translated using Weblate (Russian)
Currently translated at 40.0% (98 of 245 strings)
Translated using Weblate (Russian)
Currently translated at 39.1% (96 of 245 strings)
Translated using Weblate (Russian)
Currently translated at 91.2% (219 of 240 strings)
Translated using Weblate (Russian)
Currently translated at 38.7% (95 of 245 strings)
Translated using Weblate (Russian)
Currently translated at 38.7% (95 of 245 strings)
Translated using Weblate (Russian)
Currently translated at 38.7% (95 of 245 strings)
Translated using Weblate (Russian)
Currently translated at 37.5% (92 of 245 strings)
Translated using Weblate (Russian)
Currently translated at 37.1% (91 of 245 strings)
Translated using Weblate (Russian)
Currently translated at 36.7% (90 of 245 strings)
Translated using Weblate (Russian)
Currently translated at 100.0% (60 of 60 strings)
Translated using Weblate (Russian)
Currently translated at 90.8% (218 of 240 strings)
Translated using Weblate (Russian)
Currently translated at 90.8% (218 of 240 strings)
Translated using Weblate (Russian)
Currently translated at 36.3% (89 of 245 strings)
Translated using Weblate (Russian)
Currently translated at 36.3% (89 of 245 strings)
Translated using Weblate (Russian)
Currently translated at 100.0% (899 of 899 strings)
Translated using Weblate (Russian)
Currently translated at 99.3% (893 of 899 strings)
Translated using Weblate (Russian)
Currently translated at 99.2% (892 of 899 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (836 of 836 strings)
Translated using Weblate (Spanish)
Currently translated at 99.4% (831 of 836 strings)
Translated using Weblate (Hungarian)
Currently translated at 62.6% (2068 of 3303 strings)
Translated using Weblate (Portuguese (Brazil))
Currently translated at 97.9% (804 of 821 strings)
Translated using Weblate (Portuguese)
Currently translated at 72.0% (602 of 836 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (836 of 836 strings)
Translated using Weblate (Spanish)
Currently translated at 99.1% (829 of 836 strings)
Translated using Weblate (Portuguese (Brazil))
Currently translated at 98.4% (885 of 899 strings)
Translated using Weblate (Portuguese)
Currently translated at 99.6% (896 of 899 strings)
Translated using Weblate (Hungarian)
Currently translated at 57.9% (1915 of 3303 strings)
Translated using Weblate (Portuguese (Brazil))
Currently translated at 97.4% (800 of 821 strings)
Translated using Weblate (Hungarian)
Currently translated at 57.6% (1903 of 3303 strings)
Translated using Weblate (Hungarian)
Currently translated at 57.5% (1900 of 3303 strings)
Translated using Weblate (Portuguese (Brazil))
Currently translated at 97.0% (797 of 821 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (836 of 836 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (899 of 899 strings)
Translated using Weblate (Bulgarian)
Currently translated at 84.0% (79 of 94 strings)
Translated using Weblate (Bulgarian)
Currently translated at 84.0% (79 of 94 strings)
Translated using Weblate (Spanish)
Currently translated at 98.4% (823 of 836 strings)
Translated using Weblate (Spanish)
Currently translated at 98.7% (3263 of 3303 strings)
Translated using Weblate (Portuguese (Brazil))
Currently translated at 96.8% (795 of 821 strings)
Translated using Weblate (Spanish)
Currently translated at 98.3% (822 of 836 strings)
Translated using Weblate (Spanish)
Currently translated at 98.5% (3256 of 3303 strings)
Translated using Weblate (Portuguese (Brazil))
Currently translated at 96.8% (795 of 821 strings)
Translated using Weblate (Portuguese (Brazil))
Currently translated at 96.8% (795 of 821 strings)
Translated using Weblate (French)
Currently translated at 98.4% (823 of 836 strings)
Translated using Weblate (French)
Currently translated at 100.0% (3303 of 3303 strings)
Translated using Weblate (Portuguese (Brazil))
Currently translated at 96.5% (793 of 821 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (899 of 899 strings)
Translated using Weblate (French)
Currently translated at 99.8% (3297 of 3303 strings)
Translated using Weblate (Portuguese (Brazil))
Currently translated at 96.4% (792 of 821 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (899 of 899 strings)
Translated using Weblate (French)
Currently translated at 99.3% (3280 of 3303 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 99.7% (897 of 899 strings)
Translated using Weblate (French)
Currently translated at 99.1% (3275 of 3303 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 99.7% (897 of 899 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 99.7% (897 of 899 strings)
Translated using Weblate (German)
Currently translated at 100.0% (899 of 899 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 99.7% (897 of 899 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 97.6% (3187 of 3265 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 99.7% (897 of 899 strings)
Translated using Weblate (French)
Currently translated at 100.0% (899 of 899 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (896 of 896 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (896 of 896 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (896 of 896 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (896 of 896 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (262 of 262 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (272 of 272 strings)
Translated using Weblate (Hungarian)
Currently translated at 58.1% (1898 of 3265 strings)
Translated using Weblate (Portuguese (Brazil))
Currently translated at 62.8% (154 of 245 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (15 of 15 strings)
Translated using Weblate (Portuguese (Brazil))
Currently translated at 96.4% (792 of 821 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (91 of 91 strings)
Translated using Weblate (Portuguese (Brazil))
Currently translated at 95.2% (378 of 397 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (397 of 397 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (896 of 896 strings)
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (896 of 896 strings)
Translated using Weblate (Hungarian)
Currently translated at 100.0% (896 of 896 strings)
Co-authored-by: Anna <shiloanna007@gmail.com>
Co-authored-by: Besogon <victoria_murka@mail.ru>
Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: ForbiddenFigs <sorautai@outlook.com>
Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
Co-authored-by: Nell Chant <doubletailor@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: Viktor Révész <rviktor@ivankapal.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: razil <boss.razmarin@gmail.com>
Co-authored-by: 小王 <963505255@qq.com>
Co-authored-by: 海岛钓鱼佬 <963505255@qq.com>
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/de/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/content/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/content/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/death/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/bg/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/de/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/es/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/hu/
Translation: Habitica/Backgrounds
Translation: Habitica/Communityguidelines
Translation: Habitica/Content
Translation: Habitica/Death
Translation: Habitica/Faq
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Limited
Translation: Habitica/Messages
Translation: Habitica/Quests
Translation: Habitica/Questscontent
Translation: Habitica/Subscriber
5.34.0
Update test.yml (#15397)
combined messages restyling - next round (#15386)
* split component prepare new views / states
* extract empty and disabled state as components
* fix empty state mail icon
* first logic switching between modes, move page to /private-messages/index.vue
* extract autoCompleteHelper.js
* style header + start new message input
* style plus button + focus input
* state logic, types for sanity
* WIP PM new Message started
* add /members/username test
* first design changes to messageCard
* delete private message or chat - based on the mode
* copy as todo
* mention links to modal
* report chat or private message
* WIP likeButton
* likeButton styling
* hide like on private message cards
* fix unit test
* replace copy as todo - to just a copy to clipboard
* style changes
* menu position + like button width
* dropdown items background + like font
* fix like button padding
* move api endpoints and tests around to group inbox methods + like for inbox private messages
* restyle system messages
* Dropdown Radius and Padding
* WIP system messages
* fix lint
* copy delta commit of allowing liking own private messages
* enable liking private messages
* fix menu non hovered item icon color
* fix import path
* ignore background on system messages
* requested changes + migration
* update migration to update the unique id to some messages and delete the duplicates
* migration based on users pagination
* fix(migration): use Promise.all
* change to bulkWrites per User, and all messages in one run (of a user)
* check for array
* use rest operator ...
* skip sorting to get the users
* remove migration, disable like for private messages without uniqueMessageId
* lean+bulkWrite for likes, add time checks for like and auth for further debugging
* add a limit 2 get the messages by uniqueId
* Adding a simple server start script
* remove pinned nodemon dep
* fix inbox controller/tests
* fix / requested style changes
* fix empty state padding /
* hide avatar weapons on messages - fix avatar spacing on messages
* Hourglass Simplification (#15323)
* begin removing obsolete tests
* begin refactoring
* update cron tests
* cleanup
* finish basic implementation of new logic
* add more subscription tests
* subscription test improvements
* return nextHourglassDate again
* fix gem limit
* fix(test): short circuit this.
* fix(admin): correct logic and style for shrimple subs
* WIP(frontend): draft of main subs page view
* fix hourglass count
* Fix hourglass logic for upgrades
* fix admin panel display
* WIP(subs): extant Stripe state
* fix admin panel strings
* fix missing transaction type
* add new field for cumulative subscription count
* show date for hourglass bonus if it was received
* fix test
* feat(subscription): max Gems progress readout
* fix(css): correct and refactor heights and selection states
* fix(subs): correct border-radius and redirect
* fix(stripe): correct redirect after success
* Admin panel display fixes
* don’t give additional HG for new sub if they already got one this month
* fix issue with promo hourglasses
* fix(subscription): update layout when gifting
* fix(subscriptions): more gift layout revisions
* fix(subscriptions): minor visual updates
* fix(subs): pass autoRenews through Stripe
* fix(subs): gifts DON't renew
* fix(lint): unnecessary ternary
* fix(lint): do negate object ig
* fix(subs): try again on gifts
* fix(subs): unhovery and un-12-monthy
* fix bug with incorrectly giving HG bonus
* remove only
* fix test
* fix test
* fix(subs): also redirect to subs after gift sub
* fix(subs): fix typeError
* fix(g1g1): don't try to find Gems promo during bogo
---------
Co-authored-by: Phillip Thelen <phillip@habitica.com>
Co-authored-by: Kalista Payne <sabe@habitica.com>
* chore(sprites): update subproject
* fix(layout): tighten cancellation note
* fix(subs): Google wording and HG escape
* chore(testing): fake g1g1 dates
* fix(subs): don't hide HG preview entirely
* fix(subs): center next hourglass message
* working validatedTextInput.vue within start-new-conversation-input-header.vue 🎉
* fix(git): remove changes from old develop
* Revert "fix(git): remove changes from old develop"
This reverts commit 0e30f7df00.
* fix(git): no actually just this file i guesss
* adding an empty loading state, hiding
* fought the avatar arch nemesis again
* fix chatMessages (party chat) message spacing
* move disabled text back to above the input area - re-enable input area
* show disabled private messages top panel
* fix font color
* fixing uiStates - removing disabled - moving the own user check to the last
* fix(lint): add missing prop defaults
* fix(lint): object default should be fn
* fix(chat): correct grammar in error
* remove weapon position relative
* revert most of avatar.vue changes, add back weapons in chat message UI
* show date tooltip above system / skill messages
* fix toggle disable icon position
* trivial CSS cleanup
* fix(typo): English syntax in test
* chore(test): small style cleanup
* chore(logging): revert debug function
* chore(debug): remove timers from inbox like
---------
Co-authored-by: SabreCat <sabe@habitica.com>
Co-authored-by: Kalista Payne <sabrecat@gmail.com>
Co-authored-by: Phillip Thelen <phillip@habitica.com>
* improve method signature
* add fallback
* syntax fix
* fix merge error
* facepalm
---------
Co-authored-by: SabreCat <sabe@habitica.com>
Co-authored-by: Kalista Payne <sabrecat@gmail.com>
2015 lines
68 KiB
JavaScript
2015 lines
68 KiB
JavaScript
/* eslint-disable global-require */
|
|
import moment from 'moment';
|
|
import nconf from 'nconf';
|
|
import requireAgain from 'require-again';
|
|
import { v4 as generateUUID } from 'uuid';
|
|
import {
|
|
generateRes,
|
|
generateReq,
|
|
generateTodo,
|
|
generateDaily,
|
|
} from '../../../helpers/api-unit.helper';
|
|
import { cron, cronWrapper } from '../../../../website/server/libs/cron';
|
|
import { model as User } from '../../../../website/server/models/user';
|
|
import * as Tasks from '../../../../website/server/models/task';
|
|
import common from '../../../../website/common';
|
|
import * as analytics from '../../../../website/server/libs/analyticsService';
|
|
import { model as Group } from '../../../../website/server/models/group';
|
|
|
|
const CRON_TIMEOUT_WAIT = new Date(5 * 60 * 1000).getTime();
|
|
const CRON_TIMEOUT_UNIT = new Date(60 * 1000).getTime();
|
|
|
|
const pathToCronLib = '../../../../website/server/libs/cron';
|
|
|
|
describe('cron', async () => {
|
|
let clock = null;
|
|
let user;
|
|
const tasksByType = {
|
|
habits: [], dailys: [], todos: [], rewards: [],
|
|
};
|
|
let daysMissed = 0;
|
|
|
|
beforeEach(async () => {
|
|
user = new User({
|
|
auth: {
|
|
local: {
|
|
username: 'username',
|
|
lowerCaseUsername: 'username',
|
|
email: 'email@example.com',
|
|
salt: 'salt',
|
|
hashed_password: 'hashed_password', // eslint-disable-line camelcase
|
|
},
|
|
},
|
|
});
|
|
|
|
sinon.spy(analytics, 'track');
|
|
});
|
|
|
|
afterEach(async () => {
|
|
if (clock !== null) clock.restore();
|
|
analytics.track.restore();
|
|
});
|
|
|
|
it('updates user.preferences.timezoneOffsetAtLastCron', async () => {
|
|
const timezoneUtcOffsetFromUserPrefs = -1;
|
|
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics, timezoneUtcOffsetFromUserPrefs,
|
|
});
|
|
|
|
expect(user.preferences.timezoneOffsetAtLastCron).to.equal(1);
|
|
});
|
|
|
|
it('resets user.items.lastDrop.count', async () => {
|
|
user.items.lastDrop.count = 4;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.items.lastDrop.count).to.equal(0);
|
|
});
|
|
|
|
it('increments user cron count', async () => {
|
|
const cronCountBefore = user.flags.cronCount;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.flags.cronCount).to.be.greaterThan(cronCountBefore);
|
|
});
|
|
|
|
it('calls analytics', async () => {
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(analytics.track.callCount).to.equal(1);
|
|
});
|
|
|
|
it('calls analytics when user is sleeping', async () => {
|
|
user.preferences.sleep = true;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(analytics.track.callCount).to.equal(1);
|
|
});
|
|
|
|
describe('end of the month perks', async () => {
|
|
beforeEach(async () => {
|
|
user.purchased.plan.customerId = 'subscribedId';
|
|
user.purchased.plan.dateUpdated = moment().subtract(1, 'months').toDate();
|
|
});
|
|
|
|
it('awards current mystery items to subscriber', async () => {
|
|
user.purchased.plan.dateUpdated = new Date('2018-12-11');
|
|
clock = sinon.useFakeTimers(new Date('2019-01-29'));
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.purchased.plan.mysteryItems.length).to.eql(2);
|
|
const filteredNotifications = user.notifications.filter(n => n.type === 'NEW_MYSTERY_ITEMS');
|
|
expect(filteredNotifications.length).to.equal(1);
|
|
});
|
|
|
|
it('awards multiple mystery item sets if user skipped months between logins', async () => {
|
|
user.purchased.plan.dateUpdated = new Date('2018-11-11');
|
|
clock = sinon.useFakeTimers(new Date('2019-01-29'));
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.purchased.plan.mysteryItems.length).to.eql(4);
|
|
const filteredNotifications = user.notifications.filter(n => n.type === 'NEW_MYSTERY_ITEMS');
|
|
expect(filteredNotifications.length).to.equal(1);
|
|
});
|
|
|
|
it('resets plan.gemsBought on a new month', async () => {
|
|
user.purchased.plan.gemsBought = 10;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.purchased.plan.gemsBought).to.equal(0);
|
|
});
|
|
|
|
it('resets plan.gemsBought on a new month if user does not have purchased.plan.dateUpdated', async () => {
|
|
user.purchased.plan.gemsBought = 10;
|
|
user.purchased.plan.dateUpdated = undefined;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.purchased.plan.gemsBought).to.equal(0);
|
|
});
|
|
|
|
it('does not reset plan.gemsBought within the month', async () => {
|
|
clock = sinon.useFakeTimers(moment().startOf('month').add(2, 'days').toDate());
|
|
user.purchased.plan.dateUpdated = moment().startOf('month').toDate();
|
|
|
|
user.purchased.plan.gemsBought = 10;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.purchased.plan.gemsBought).to.equal(10);
|
|
});
|
|
|
|
it('resets plan.dateUpdated on a new month', async () => {
|
|
const currentMonth = moment().startOf('month');
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(moment(user.purchased.plan.dateUpdated).startOf('month').isSame(currentMonth)).to.eql(true);
|
|
});
|
|
|
|
it('increments plan.consecutive.count', async () => {
|
|
user.purchased.plan.consecutive.count = 0;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.purchased.plan.consecutive.count).to.equal(1);
|
|
});
|
|
|
|
it('increments plan.cumulativeCount', async () => {
|
|
user.purchased.plan.cumulativeCount = 0;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.purchased.plan.cumulativeCount).to.equal(1);
|
|
});
|
|
|
|
it('increments plan.consecutive.count by more than 1 if user skipped months between logins', async () => {
|
|
user.purchased.plan.dateUpdated = moment().subtract(2, 'months').toDate();
|
|
user.purchased.plan.consecutive.count = 0;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.purchased.plan.consecutive.count).to.equal(2);
|
|
});
|
|
|
|
it('increments plan.cumulativeCount by more than 1 if user skipped months between logins', async () => {
|
|
user.purchased.plan.dateUpdated = moment().subtract(3, 'months').toDate();
|
|
user.purchased.plan.cumulativeCount = 0;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.purchased.plan.cumulativeCount).to.equal(3);
|
|
});
|
|
|
|
it('does not award unearned plan.consecutive.trinkets if subscription ended during an absence', async () => {
|
|
user.purchased.plan.dateUpdated = moment().subtract(6, 'months').toDate();
|
|
user.purchased.plan.dateTerminated = moment().subtract(3, 'months').toDate();
|
|
user.purchased.plan.consecutive.count = 5;
|
|
user.purchased.plan.consecutive.trinkets = 1;
|
|
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
|
|
expect(user.purchased.plan.consecutive.trinkets).to.equal(1);
|
|
});
|
|
|
|
it('does not increment plan.consecutive.gemCapExtra when user has reached the gemCap limit', async () => {
|
|
user.purchased.plan.consecutive.gemCapExtra = 26;
|
|
user.purchased.plan.consecutive.count = 5;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(26);
|
|
});
|
|
|
|
it('does not reset plan stats if we are before the last day of the cancelled month', async () => {
|
|
user.purchased.plan.dateTerminated = moment(new Date()).add({ days: 1 });
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.purchased.plan.customerId).to.exist;
|
|
});
|
|
|
|
it('does reset plan stats if we are after the last day of the cancelled month', async () => {
|
|
user.purchased.plan.dateTerminated = moment(new Date()).subtract({ days: 1 });
|
|
user.purchased.plan.consecutive.gemCapExtra = 20;
|
|
user.purchased.plan.consecutive.count = 5;
|
|
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
|
|
expect(user.purchased.plan.customerId).to.not.exist;
|
|
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(20);
|
|
expect(user.purchased.plan.consecutive.count).to.equal(0);
|
|
});
|
|
|
|
describe('for a 1-month recurring subscription', async () => {
|
|
// create a user that will be used for all of these tests without a reset before each
|
|
const user1 = new User({
|
|
auth: {
|
|
local: {
|
|
username: 'username1',
|
|
lowerCaseUsername: 'username1',
|
|
email: 'email1@example.com',
|
|
salt: 'salt',
|
|
hashed_password: 'hashed_password', // eslint-disable-line camelcase
|
|
},
|
|
},
|
|
});
|
|
// user1 has a 1-month recurring subscription starting today
|
|
beforeEach(async () => {
|
|
user1.purchased.plan.customerId = 'subscribedId';
|
|
user1.purchased.plan.dateUpdated = moment().toDate();
|
|
user1.purchased.plan.planId = 'basic';
|
|
user1.purchased.plan.consecutive.count = 0;
|
|
user1.purchased.plan.consecutive.trinkets = 1;
|
|
user1.purchased.plan.consecutive.gemCapExtra = 0;
|
|
});
|
|
|
|
it('increments consecutive benefits', async () => {
|
|
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
|
|
.add(2, 'days')
|
|
.toDate());
|
|
// Add 1 month to simulate what happens a month after the subscription was created.
|
|
// Add 2 days so that we're sure we're not affected by any start-of-month effects
|
|
// e.g., from time zone oddness.
|
|
await cron({
|
|
user: user1, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user1.purchased.plan.consecutive.count).to.equal(1);
|
|
expect(user1.purchased.plan.consecutive.trinkets).to.equal(2);
|
|
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(2);
|
|
});
|
|
|
|
it('increments consecutive benefits correctly if user has been absent with continuous subscription', async () => {
|
|
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(10, 'months')
|
|
.add(2, 'days')
|
|
.toDate());
|
|
await cron({
|
|
user: user1, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user1.purchased.plan.consecutive.count).to.equal(10);
|
|
expect(user1.purchased.plan.consecutive.trinkets).to.equal(11);
|
|
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(20);
|
|
});
|
|
});
|
|
|
|
describe('for a 3-month recurring subscription', async () => {
|
|
const user3 = new User({
|
|
auth: {
|
|
local: {
|
|
username: 'username3',
|
|
lowerCaseUsername: 'username3',
|
|
email: 'email3@example.com',
|
|
salt: 'salt',
|
|
hashed_password: 'hashed_password', // eslint-disable-line camelcase
|
|
},
|
|
},
|
|
});
|
|
// user3 has a 3-month recurring subscription starting today
|
|
beforeEach(async () => {
|
|
user3.purchased.plan.customerId = 'subscribedId';
|
|
user3.purchased.plan.dateUpdated = moment().toDate();
|
|
user3.purchased.plan.planId = 'basic_3mo';
|
|
user3.purchased.plan.consecutive.count = 0;
|
|
user3.purchased.plan.consecutive.trinkets = 1;
|
|
user3.purchased.plan.consecutive.gemCapExtra = 0;
|
|
});
|
|
|
|
it('increments consecutive benefits', async () => {
|
|
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
|
|
.add(2, 'days')
|
|
.toDate());
|
|
await cron({
|
|
user: user3, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user3.purchased.plan.consecutive.count).to.equal(1);
|
|
expect(user3.purchased.plan.consecutive.trinkets).to.equal(2);
|
|
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(2);
|
|
});
|
|
|
|
it('increments consecutive benefits correctly if user has been absent with continuous subscription', async () => {
|
|
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(10, 'months')
|
|
.add(2, 'days')
|
|
.toDate());
|
|
await cron({
|
|
user: user3, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user3.purchased.plan.consecutive.count).to.equal(10);
|
|
expect(user3.purchased.plan.consecutive.trinkets).to.equal(11);
|
|
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(20);
|
|
});
|
|
});
|
|
|
|
describe('for a 6-month recurring subscription', async () => {
|
|
const user6 = new User({
|
|
auth: {
|
|
local: {
|
|
username: 'username6',
|
|
lowerCaseUsername: 'username6',
|
|
email: 'email6@example.com',
|
|
salt: 'salt',
|
|
hashed_password: 'hashed_password', // eslint-disable-line camelcase
|
|
},
|
|
},
|
|
});
|
|
// user6 has a 6-month recurring subscription starting today
|
|
beforeEach(async () => {
|
|
user6.purchased.plan.customerId = 'subscribedId';
|
|
user6.purchased.plan.dateUpdated = moment().toDate();
|
|
user6.purchased.plan.planId = 'google_6mo';
|
|
user6.purchased.plan.consecutive.count = 0;
|
|
user6.purchased.plan.consecutive.trinkets = 1;
|
|
user6.purchased.plan.consecutive.gemCapExtra = 0;
|
|
});
|
|
|
|
it('increments benefits', async () => {
|
|
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
|
|
.add(2, 'days')
|
|
.toDate());
|
|
await cron({
|
|
user: user6, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user6.purchased.plan.consecutive.count).to.equal(1);
|
|
expect(user6.purchased.plan.consecutive.trinkets).to.equal(2);
|
|
expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(2);
|
|
});
|
|
});
|
|
|
|
describe('for a 12-month recurring subscription', async () => {
|
|
const user12 = new User({
|
|
auth: {
|
|
local: {
|
|
username: 'username12',
|
|
lowerCaseUsername: 'username12',
|
|
email: 'email12@example.com',
|
|
salt: 'salt',
|
|
hashed_password: 'hashed_password', // eslint-disable-line camelcase
|
|
},
|
|
},
|
|
});
|
|
// user12 has a 12-month recurring subscription starting today
|
|
user12.purchased.plan.customerId = 'subscribedId';
|
|
user12.purchased.plan.dateUpdated = moment().toDate();
|
|
user12.purchased.plan.planId = 'basic_12mo';
|
|
user12.purchased.plan.consecutive.count = 0;
|
|
user12.purchased.plan.consecutive.trinkets = 1;
|
|
user12.purchased.plan.consecutive.gemCapExtra = 26;
|
|
|
|
it('increments consecutive benefits the month after the second paid period has started', async () => {
|
|
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
|
|
.add(2, 'days')
|
|
.toDate());
|
|
await cron({
|
|
user: user12, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user12.purchased.plan.consecutive.count).to.equal(1);
|
|
expect(user12.purchased.plan.consecutive.trinkets).to.equal(2);
|
|
expect(user12.purchased.plan.consecutive.gemCapExtra).to.equal(26);
|
|
});
|
|
|
|
it('increments consecutive benefits correctly if user has been absent with continuous subscription', async () => {
|
|
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(10, 'months')
|
|
.add(2, 'days')
|
|
.toDate());
|
|
await cron({
|
|
user: user12, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user12.purchased.plan.consecutive.count).to.equal(10);
|
|
expect(user12.purchased.plan.consecutive.trinkets).to.equal(11);
|
|
expect(user12.purchased.plan.consecutive.gemCapExtra).to.equal(26);
|
|
});
|
|
});
|
|
|
|
describe('for a 3-month gift subscription (non-recurring)', async () => {
|
|
const user3g = new User({
|
|
auth: {
|
|
local: {
|
|
username: 'username3g',
|
|
lowerCaseUsername: 'username3g',
|
|
email: 'email3g@example.com',
|
|
salt: 'salt',
|
|
hashed_password: 'hashed_password', // eslint-disable-line camelcase
|
|
},
|
|
},
|
|
});
|
|
// user3g has a 3-month gift subscription starting today
|
|
user3g.purchased.plan.customerId = 'Gift';
|
|
user3g.purchased.plan.dateUpdated = moment().toDate();
|
|
user3g.purchased.plan.dateTerminated = moment().startOf('month').add(3, 'months').add(15, 'days')
|
|
.toDate();
|
|
user3g.purchased.plan.planId = null;
|
|
user3g.purchased.plan.consecutive.count = 0;
|
|
user3g.purchased.plan.cumulativeCount = 0;
|
|
user3g.purchased.plan.consecutive.trinkets = 1;
|
|
user3g.purchased.plan.consecutive.gemCapExtra = 0;
|
|
|
|
it('increments benefits', async () => {
|
|
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
|
|
.add(2, 'days')
|
|
.toDate());
|
|
await cron({
|
|
user: user3g, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user3g.purchased.plan.consecutive.count).to.equal(1);
|
|
expect(user3g.purchased.plan.cumulativeCount).to.equal(1);
|
|
expect(user3g.purchased.plan.consecutive.trinkets).to.equal(2);
|
|
expect(user3g.purchased.plan.consecutive.gemCapExtra).to.equal(2);
|
|
});
|
|
|
|
it('does not increment consecutive benefits in the month after the gift subscription has ended', async () => {
|
|
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(4, 'months')
|
|
.add(2, 'days')
|
|
.toDate());
|
|
await cron({
|
|
user: user3g, tasksByType, daysMissed, analytics,
|
|
});
|
|
// subscription has been erased by now
|
|
expect(user3g.purchased.plan.consecutive.count).to.equal(0);
|
|
expect(user3g.purchased.plan.consecutive.trinkets).to.equal(2);
|
|
expect(user3g.purchased.plan.consecutive.gemCapExtra).to.equal(2);
|
|
expect(user3g.purchased.plan.cumulativeCount).to.equal(1);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('end of the month perks when user is not subscribed', async () => {
|
|
beforeEach(async () => {
|
|
user.purchased.plan.dateUpdated = moment().subtract(1, 'months').toDate();
|
|
});
|
|
|
|
it('resets plan.gemsBought on a new month', async () => {
|
|
user.purchased.plan.gemsBought = 10;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.purchased.plan.gemsBought).to.equal(0);
|
|
});
|
|
|
|
it('does not reset plan.gemsBought within the month', async () => {
|
|
clock = sinon.useFakeTimers(moment().startOf('month').add(2, 'days').unix());
|
|
user.purchased.plan.dateUpdated = moment().startOf('month').toDate();
|
|
|
|
user.purchased.plan.gemsBought = 10;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.purchased.plan.gemsBought).to.equal(10);
|
|
});
|
|
|
|
it('does not reset plan.dateUpdated on a new month', async () => {
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.purchased.plan.dateUpdated).to.be.empty;
|
|
});
|
|
|
|
it('does not increment plan.consecutive.count', async () => {
|
|
user.purchased.plan.consecutive.count = 0;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.purchased.plan.consecutive.count).to.equal(0);
|
|
});
|
|
|
|
it('does not increment plan.cumulativeCount', async () => {
|
|
user.purchased.plan.cumulativeCount = 0;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.purchased.plan.cumulativeCount).to.equal(0);
|
|
});
|
|
|
|
it('does not increment plan.consecutive.trinkets when user has reached a month that is a multiple of 3', async () => {
|
|
user.purchased.plan.consecutive.count = 5;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.purchased.plan.consecutive.trinkets).to.equal(0);
|
|
});
|
|
|
|
it('does not increment plan.consecutive.gemCapExtra when user has reached a month that is a multiple of 3', async () => {
|
|
user.purchased.plan.consecutive.count = 5;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(0);
|
|
});
|
|
|
|
it('does not increment plan.consecutive.gemCapExtra when user has reached the gemCap limit', async () => {
|
|
user.purchased.plan.consecutive.gemCapExtra = 26;
|
|
user.purchased.plan.consecutive.count = 5;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(26);
|
|
});
|
|
|
|
it('does nothing to plan stats if we are before the last day of the cancelled month', async () => {
|
|
user.purchased.plan.dateTerminated = moment(new Date()).add({ days: 1 });
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.purchased.plan.customerId).to.not.exist;
|
|
});
|
|
});
|
|
|
|
describe('todos', async () => {
|
|
beforeEach(async () => {
|
|
const todo = {
|
|
text: 'test todo',
|
|
type: 'todo',
|
|
value: 0,
|
|
};
|
|
|
|
const task = new Tasks.todo(Tasks.Task.sanitize(todo)); // eslint-disable-line new-cap
|
|
tasksByType.todos.push(task);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
tasksByType.todos = [];
|
|
user.tasksOrder.todos = [];
|
|
});
|
|
|
|
it('should make uncompleted todos redder', async () => {
|
|
const valueBefore = tasksByType.todos[0].value;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(tasksByType.todos[0].value).to.be.lessThan(valueBefore);
|
|
});
|
|
|
|
it('should not make completed todos redder', async () => {
|
|
tasksByType.todos[0].completed = true;
|
|
const valueBefore = tasksByType.todos[0].value;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(tasksByType.todos[0].value).to.equal(valueBefore);
|
|
});
|
|
|
|
it('should add history of completed todos to user history', async () => {
|
|
tasksByType.todos[0].completed = true;
|
|
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
|
|
expect(user.history.todos).to.be.lengthOf(1);
|
|
});
|
|
|
|
it('should remove completed todos from users taskOrder list', async () => {
|
|
const todo = {
|
|
text: 'test todo',
|
|
type: 'todo',
|
|
value: 0,
|
|
};
|
|
|
|
const task = new Tasks.todo(Tasks.Task.sanitize(todo)); // eslint-disable-line new-cap
|
|
tasksByType.todos.push(task);
|
|
tasksByType.todos[0].completed = true;
|
|
|
|
user.tasksOrder.todos = tasksByType.todos.map(taskTodo => taskTodo._id);
|
|
// Since ideally tasksByType should not contain completed todos,
|
|
// fake ids should be filtered too
|
|
user.tasksOrder.todos.push('00000000-0000-0000-0000-000000000000');
|
|
|
|
expect(tasksByType.todos).to.be.lengthOf(2);
|
|
expect(user.tasksOrder.todos).to.be.lengthOf(3);
|
|
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
|
|
// user.tasksOrder.todos should be filtered while tasks by type remains unchanged
|
|
expect(tasksByType.todos).to.be.lengthOf(2);
|
|
expect(user.tasksOrder.todos).to.be.lengthOf(1);
|
|
});
|
|
|
|
it('should preserve todos order in task list', async () => {
|
|
const todo = {
|
|
text: 'test todo',
|
|
type: 'todo',
|
|
value: 0,
|
|
};
|
|
|
|
let task = new Tasks.todo(Tasks.Task.sanitize(todo)); // eslint-disable-line new-cap
|
|
tasksByType.todos.push(task);
|
|
task = new Tasks.todo(Tasks.Task.sanitize(todo)); // eslint-disable-line new-cap
|
|
tasksByType.todos.push(task);
|
|
task = new Tasks.todo(Tasks.Task.sanitize(todo)); // eslint-disable-line new-cap
|
|
tasksByType.todos.push(task);
|
|
|
|
// Set up user.tasksOrder list in a specific order
|
|
user.tasksOrder.todos = tasksByType.todos.map(todoTask => todoTask._id).reverse();
|
|
const original = user.tasksOrder.todos; // Preserve the original order
|
|
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
|
|
let listsAreEqual = true;
|
|
user.tasksOrder.todos.forEach((taskId, index) => {
|
|
if (original[index]._id !== taskId) {
|
|
listsAreEqual = false;
|
|
}
|
|
});
|
|
|
|
expect(listsAreEqual);
|
|
expect(user.tasksOrder.todos).to.be.lengthOf(original.length);
|
|
});
|
|
});
|
|
|
|
describe('dailys', async () => {
|
|
beforeEach(async () => {
|
|
const daily = {
|
|
text: 'test daily',
|
|
type: 'daily',
|
|
};
|
|
|
|
const task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line new-cap
|
|
tasksByType.dailys = [];
|
|
tasksByType.dailys.push(task);
|
|
|
|
const statsComputedRes = common.statsComputed(user);
|
|
const stubbedStatsComputed = sinon.stub(common, 'statsComputed');
|
|
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { con: 1 }));
|
|
});
|
|
|
|
afterEach(async () => {
|
|
common.statsComputed.restore();
|
|
});
|
|
|
|
it('computes isDue', async () => {
|
|
tasksByType.dailys[0].frequency = 'daily';
|
|
tasksByType.dailys[0].everyX = 5;
|
|
tasksByType.dailys[0].startDate = moment().add(1, 'days').toDate();
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(tasksByType.dailys[0].isDue).to.be.false;
|
|
});
|
|
|
|
it('computes isDue when user is sleeping', async () => {
|
|
user.preferences.sleep = true;
|
|
tasksByType.dailys[0].frequency = 'daily';
|
|
tasksByType.dailys[0].everyX = 5;
|
|
tasksByType.dailys[0].startDate = moment().toDate();
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(tasksByType.dailys[0].isDue).to.exist;
|
|
});
|
|
|
|
it('computes nextDue', async () => {
|
|
tasksByType.dailys[0].frequency = 'daily';
|
|
tasksByType.dailys[0].everyX = 5;
|
|
tasksByType.dailys[0].startDate = moment().add(1, 'days').toDate();
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(tasksByType.dailys[0].nextDue.length).to.eql(6);
|
|
});
|
|
|
|
it('should add history', async () => {
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(tasksByType.dailys[0].history).to.be.lengthOf(1);
|
|
});
|
|
|
|
it('should set tasks completed to false', async () => {
|
|
tasksByType.dailys[0].completed = true;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(tasksByType.dailys[0].completed).to.be.false;
|
|
});
|
|
|
|
it('should set tasks completed to false when user is sleeping', async () => {
|
|
user.preferences.sleep = true;
|
|
tasksByType.dailys[0].completed = true;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(tasksByType.dailys[0].completed).to.be.false;
|
|
});
|
|
|
|
it('should reset task checklist for completed dailys', async () => {
|
|
tasksByType.dailys[0].checklist.push({ title: 'test', completed: false });
|
|
tasksByType.dailys[0].completed = true;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
|
|
});
|
|
|
|
it('should reset task checklist for completed dailys when user is sleeping', async () => {
|
|
user.preferences.sleep = true;
|
|
tasksByType.dailys[0].checklist.push({ title: 'test', completed: false });
|
|
tasksByType.dailys[0].completed = true;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
|
|
});
|
|
|
|
it('should reset task checklist for dailys with scheduled misses', async () => {
|
|
daysMissed = 10;
|
|
tasksByType.dailys[0].checklist.push({ title: 'test', completed: false });
|
|
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
|
|
});
|
|
|
|
it('should do damage for missing a daily', async () => {
|
|
daysMissed = 1;
|
|
const hpBefore = user.stats.hp;
|
|
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.stats.hp).to.be.lessThan(hpBefore);
|
|
});
|
|
|
|
it('should not do damage for missing a daily when user is sleeping', async () => {
|
|
user.preferences.sleep = true;
|
|
daysMissed = 1;
|
|
const hpBefore = user.stats.hp;
|
|
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.stats.hp).to.equal(hpBefore);
|
|
});
|
|
|
|
it('should not do damage for missing a daily when CRON_SAFE_MODE is set', async () => {
|
|
sandbox.stub(nconf, 'get').withArgs('CRON_SAFE_MODE').returns('true');
|
|
const cronOverride = requireAgain(pathToCronLib).cron;
|
|
|
|
daysMissed = 1;
|
|
const hpBefore = user.stats.hp;
|
|
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
|
|
|
cronOverride({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
|
|
expect(user.stats.hp).to.equal(hpBefore);
|
|
});
|
|
|
|
it('should not do damage for missing a daily if user stealth buff is greater than or equal to days missed', async () => {
|
|
daysMissed = 1;
|
|
const hpBefore = user.stats.hp;
|
|
user.stats.buffs.stealth = 2;
|
|
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
|
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
|
|
expect(user.stats.hp).to.equal(hpBefore);
|
|
});
|
|
|
|
it('should do less damage for missing a daily with partial completion', async () => {
|
|
daysMissed = 1;
|
|
let hpBefore = user.stats.hp;
|
|
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
const hpDifferenceOfFullyIncompleteDaily = hpBefore - user.stats.hp;
|
|
|
|
hpBefore = user.stats.hp;
|
|
tasksByType.dailys[0].checklist.push({ title: 'test', completed: true });
|
|
tasksByType.dailys[0].checklist.push({ title: 'test2', completed: false });
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
const hpDifferenceOfPartiallyIncompleteDaily = hpBefore - user.stats.hp;
|
|
|
|
expect(hpDifferenceOfPartiallyIncompleteDaily)
|
|
.to.be.lessThan(hpDifferenceOfFullyIncompleteDaily);
|
|
});
|
|
|
|
it('should decrement quest.progress.down for missing a daily', async () => {
|
|
daysMissed = 1;
|
|
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
|
|
|
const progress = await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
|
|
expect(progress.down).to.equal(-1);
|
|
});
|
|
|
|
it('should not decrement quest.progress.down for missing a daily when user is sleeping', async () => {
|
|
user.preferences.sleep = true;
|
|
daysMissed = 1;
|
|
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
|
|
|
const progress = await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
|
|
expect(progress.down).to.equal(0);
|
|
});
|
|
|
|
it('should do damage for only yesterday\'s dailies', async () => {
|
|
daysMissed = 3;
|
|
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
|
|
|
const daily = {
|
|
text: 'test daily',
|
|
type: 'daily',
|
|
};
|
|
const task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line new-cap
|
|
tasksByType.dailys.push(task);
|
|
tasksByType.dailys[1].startDate = moment(new Date()).subtract({ days: 2 });
|
|
tasksByType.dailys[1].everyX = 2;
|
|
tasksByType.dailys[1].frequency = 'daily';
|
|
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
|
|
expect(user.stats.hp).to.equal(48);
|
|
});
|
|
});
|
|
|
|
describe('habits', async () => {
|
|
beforeEach(async () => {
|
|
const habit = {
|
|
text: 'test habit',
|
|
type: 'habit',
|
|
};
|
|
|
|
const task = new Tasks.habit(Tasks.Task.sanitize(habit)); // eslint-disable-line new-cap
|
|
tasksByType.habits = [];
|
|
tasksByType.habits.push(task);
|
|
});
|
|
|
|
it('should decrement only up value', async () => {
|
|
tasksByType.habits[0].value = 1;
|
|
tasksByType.habits[0].down = false;
|
|
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
|
|
expect(tasksByType.habits[0].value).to.be.lessThan(1);
|
|
});
|
|
|
|
it('should decrement only down value', async () => {
|
|
tasksByType.habits[0].value = 1;
|
|
tasksByType.habits[0].up = false;
|
|
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
|
|
expect(tasksByType.habits[0].value).to.be.lessThan(1);
|
|
});
|
|
|
|
it('should do nothing to habits with both up and down', async () => {
|
|
tasksByType.habits[0].value = 1;
|
|
tasksByType.habits[0].up = true;
|
|
tasksByType.habits[0].down = true;
|
|
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
|
|
expect(tasksByType.habits[0].value).to.equal(1);
|
|
});
|
|
|
|
describe('counters', async () => {
|
|
const notStartOfWeekOrMonth = new Date(2016, 9, 28).getTime(); // a Friday
|
|
|
|
beforeEach(async () => {
|
|
// Replace system clocks so we can get predictable results
|
|
clock = sinon.useFakeTimers(notStartOfWeekOrMonth);
|
|
});
|
|
|
|
it('should reset a daily habit counter each day', async () => {
|
|
tasksByType.habits[0].counterUp = 1;
|
|
tasksByType.habits[0].counterDown = 1;
|
|
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
|
|
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
|
expect(tasksByType.habits[0].counterDown).to.equal(0);
|
|
});
|
|
|
|
it('should reset habit counters even if user is sleeping', async () => {
|
|
user.preferences.sleep = true;
|
|
tasksByType.habits[0].counterUp = 1;
|
|
tasksByType.habits[0].counterDown = 1;
|
|
|
|
await 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', async () => {
|
|
tasksByType.habits[0].frequency = 'weekly';
|
|
tasksByType.habits[0].counterUp = 1;
|
|
tasksByType.habits[0].counterDown = 1;
|
|
|
|
// should not reset
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
|
|
expect(tasksByType.habits[0].counterUp).to.equal(1);
|
|
expect(tasksByType.habits[0].counterDown).to.equal(1);
|
|
|
|
// should reset
|
|
daysMissed = 8;
|
|
await 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 with custom daily start', async () => {
|
|
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
|
|
await 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
|
|
await 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', async () => {
|
|
clock.restore();
|
|
|
|
// Server clock: Monday 11pm UTC
|
|
const 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
|
|
await 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;
|
|
await 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', async () => {
|
|
clock.restore();
|
|
|
|
// Server clock: Sunday 11pm UTC
|
|
const 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
|
|
await 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', async () => {
|
|
clock.restore();
|
|
|
|
// Server clock: Monday 2am UTC
|
|
const 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
|
|
await 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', async () => {
|
|
tasksByType.habits[0].frequency = 'monthly';
|
|
tasksByType.habits[0].counterUp = 1;
|
|
tasksByType.habits[0].counterDown = 1;
|
|
|
|
// should not reset
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
|
|
expect(tasksByType.habits[0].counterUp).to.equal(1);
|
|
expect(tasksByType.habits[0].counterDown).to.equal(1);
|
|
|
|
// should reset
|
|
daysMissed = 32;
|
|
await 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 monthly habit counter when server tz is last day of month but user tz is first day of the month', async () => {
|
|
clock.restore();
|
|
daysMissed = 0;
|
|
|
|
// Server clock: 4/30/17 11pm UTC
|
|
const 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
|
|
await 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', async () => {
|
|
clock.restore();
|
|
|
|
// Server clock: 5/1/17 11pm UTC
|
|
const 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
|
|
await 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;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
|
|
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
|
expect(tasksByType.habits[0].counterDown).to.equal(0);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('perfect day', async () => {
|
|
beforeEach(async () => {
|
|
const daily = {
|
|
text: 'test daily',
|
|
type: 'daily',
|
|
};
|
|
|
|
const task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line new-cap
|
|
tasksByType.dailys = [];
|
|
tasksByType.dailys.push(task);
|
|
|
|
const statsComputedRes = common.statsComputed(user);
|
|
const stubbedStatsComputed = sinon.stub(common, 'statsComputed');
|
|
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { con: 1 }));
|
|
});
|
|
|
|
afterEach(async () => {
|
|
common.statsComputed.restore();
|
|
});
|
|
|
|
it('stores a new entry in user.history.exp', async () => {
|
|
user.stats.lvl = 2;
|
|
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
|
|
expect(user.history.exp).to.have.lengthOf(1);
|
|
expect(user.history.exp[0].value).to.equal(25);
|
|
});
|
|
|
|
it('increments perfect day achievement if all (at least 1) due dailies were completed', async () => {
|
|
daysMissed = 1;
|
|
tasksByType.dailys[0].completed = true;
|
|
tasksByType.dailys[0].isDue = true;
|
|
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
|
|
expect(user.achievements.perfect).to.equal(1);
|
|
});
|
|
|
|
it('does not increment perfect day achievement if no due dailies', async () => {
|
|
daysMissed = 1;
|
|
tasksByType.dailys[0].completed = true;
|
|
tasksByType.dailys[0].isDue = false;
|
|
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
|
|
expect(user.achievements.perfect).to.equal(0);
|
|
});
|
|
|
|
it('gives perfect day buff if all (at least 1) due dailies were completed', async () => {
|
|
daysMissed = 1;
|
|
tasksByType.dailys[0].completed = true;
|
|
tasksByType.dailys[0].isDue = true;
|
|
|
|
const previousBuffs = user.stats.buffs.toObject();
|
|
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
|
|
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
|
|
expect(user.stats.buffs.int).to.be.greaterThan(previousBuffs.int);
|
|
expect(user.stats.buffs.per).to.be.greaterThan(previousBuffs.per);
|
|
expect(user.stats.buffs.con).to.be.greaterThan(previousBuffs.con);
|
|
});
|
|
|
|
it('gives perfect day buff if all (at least 1) due dailies were completed when user is sleeping', async () => {
|
|
user.preferences.sleep = true;
|
|
daysMissed = 1;
|
|
tasksByType.dailys[0].completed = true;
|
|
tasksByType.dailys[0].isDue = true;
|
|
|
|
const previousBuffs = user.stats.buffs.toObject();
|
|
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
|
|
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
|
|
expect(user.stats.buffs.int).to.be.greaterThan(previousBuffs.int);
|
|
expect(user.stats.buffs.per).to.be.greaterThan(previousBuffs.per);
|
|
expect(user.stats.buffs.con).to.be.greaterThan(previousBuffs.con);
|
|
});
|
|
|
|
it('clears buffs if user does not have a perfect day (no due dailys)', async () => {
|
|
daysMissed = 1;
|
|
tasksByType.dailys[0].completed = true;
|
|
tasksByType.dailys[0].isDue = false;
|
|
|
|
user.stats.buffs = {
|
|
str: 1,
|
|
int: 1,
|
|
per: 1,
|
|
con: 1,
|
|
stealth: 0,
|
|
streaks: true,
|
|
};
|
|
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
|
|
expect(user.stats.buffs.str).to.equal(0);
|
|
expect(user.stats.buffs.int).to.equal(0);
|
|
expect(user.stats.buffs.per).to.equal(0);
|
|
expect(user.stats.buffs.con).to.equal(0);
|
|
expect(user.stats.buffs.stealth).to.equal(0);
|
|
expect(user.stats.buffs.streaks).to.be.false;
|
|
});
|
|
|
|
it('clears buffs if user does not have a perfect day (no due dailys) when user is sleeping', async () => {
|
|
user.preferences.sleep = true;
|
|
daysMissed = 1;
|
|
tasksByType.dailys[0].completed = true;
|
|
tasksByType.dailys[0].startDate = moment(new Date()).add({ days: 1 });
|
|
|
|
user.stats.buffs = {
|
|
str: 1,
|
|
int: 1,
|
|
per: 1,
|
|
con: 1,
|
|
stealth: 0,
|
|
streaks: true,
|
|
};
|
|
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
|
|
expect(user.stats.buffs.str).to.equal(0);
|
|
expect(user.stats.buffs.int).to.equal(0);
|
|
expect(user.stats.buffs.per).to.equal(0);
|
|
expect(user.stats.buffs.con).to.equal(0);
|
|
expect(user.stats.buffs.stealth).to.equal(0);
|
|
expect(user.stats.buffs.streaks).to.be.false;
|
|
});
|
|
|
|
it('clears buffs if user does not have a perfect day (at least one due daily not completed)', async () => {
|
|
daysMissed = 1;
|
|
tasksByType.dailys[0].completed = false;
|
|
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
|
|
|
user.stats.buffs = {
|
|
str: 1,
|
|
int: 1,
|
|
per: 1,
|
|
con: 1,
|
|
stealth: 0,
|
|
streaks: true,
|
|
};
|
|
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
|
|
expect(user.stats.buffs.str).to.equal(0);
|
|
expect(user.stats.buffs.int).to.equal(0);
|
|
expect(user.stats.buffs.per).to.equal(0);
|
|
expect(user.stats.buffs.con).to.equal(0);
|
|
expect(user.stats.buffs.stealth).to.equal(0);
|
|
expect(user.stats.buffs.streaks).to.be.false;
|
|
});
|
|
|
|
it('clears buffs if user does not have a perfect day (at least one due daily not completed) when user is sleeping', async () => {
|
|
user.preferences.sleep = true;
|
|
daysMissed = 1;
|
|
tasksByType.dailys[0].completed = false;
|
|
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
|
|
|
user.stats.buffs = {
|
|
str: 1,
|
|
int: 1,
|
|
per: 1,
|
|
con: 1,
|
|
stealth: 0,
|
|
streaks: true,
|
|
};
|
|
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
|
|
expect(user.stats.buffs.str).to.equal(0);
|
|
expect(user.stats.buffs.int).to.equal(0);
|
|
expect(user.stats.buffs.per).to.equal(0);
|
|
expect(user.stats.buffs.con).to.equal(0);
|
|
expect(user.stats.buffs.stealth).to.equal(0);
|
|
expect(user.stats.buffs.streaks).to.be.false;
|
|
});
|
|
|
|
it('always grants a perfect day buff when CRON_SAFE_MODE is set', async () => {
|
|
sandbox.stub(nconf, 'get').withArgs('CRON_SAFE_MODE').returns('true');
|
|
const cronOverride = requireAgain(pathToCronLib).cron;
|
|
daysMissed = 1;
|
|
tasksByType.dailys[0].completed = false;
|
|
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
|
|
|
const previousBuffs = user.stats.buffs.toObject();
|
|
|
|
cronOverride({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
|
|
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
|
|
expect(user.stats.buffs.int).to.be.greaterThan(previousBuffs.int);
|
|
expect(user.stats.buffs.per).to.be.greaterThan(previousBuffs.per);
|
|
expect(user.stats.buffs.con).to.be.greaterThan(previousBuffs.con);
|
|
});
|
|
|
|
it('always grants a perfect day buff when CRON_SAFE_MODE is set when user is sleeping', async () => {
|
|
user.preferences.sleep = true;
|
|
sandbox.stub(nconf, 'get').withArgs('CRON_SAFE_MODE').returns('true');
|
|
const cronOverride = requireAgain(pathToCronLib).cron;
|
|
daysMissed = 1;
|
|
tasksByType.dailys[0].completed = false;
|
|
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
|
|
|
const previousBuffs = user.stats.buffs.toObject();
|
|
|
|
cronOverride({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
|
|
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
|
|
expect(user.stats.buffs.int).to.be.greaterThan(previousBuffs.int);
|
|
expect(user.stats.buffs.per).to.be.greaterThan(previousBuffs.per);
|
|
expect(user.stats.buffs.con).to.be.greaterThan(previousBuffs.con);
|
|
});
|
|
});
|
|
|
|
describe('adding mp', async () => {
|
|
it('should add mp to user', async () => {
|
|
const statsComputedRes = common.statsComputed(user);
|
|
const stubbedStatsComputed = sinon.stub(common, 'statsComputed');
|
|
|
|
const mpBefore = user.stats.mp;
|
|
tasksByType.dailys[0].completed = true;
|
|
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.stats.mp).to.be.greaterThan(mpBefore);
|
|
|
|
common.statsComputed.restore();
|
|
});
|
|
|
|
it('should not add mp to user when user is sleeping', async () => {
|
|
const statsComputedRes = common.statsComputed(user);
|
|
const stubbedStatsComputed = sinon.stub(common, 'statsComputed');
|
|
|
|
user.preferences.sleep = true;
|
|
const mpBefore = user.stats.mp;
|
|
tasksByType.dailys[0].completed = true;
|
|
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.stats.mp).to.equal(mpBefore);
|
|
|
|
common.statsComputed.restore();
|
|
});
|
|
|
|
it('set user\'s mp to statsComputed.maxMP when user.stats.mp is greater', async () => {
|
|
const statsComputedRes = common.statsComputed(user);
|
|
const stubbedStatsComputed = sinon.stub(common, 'statsComputed');
|
|
user.stats.mp = 120;
|
|
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.stats.mp).to.equal(common.statsComputed(user).maxMP);
|
|
|
|
common.statsComputed.restore();
|
|
});
|
|
});
|
|
|
|
describe('quest progress', async () => {
|
|
beforeEach(async () => {
|
|
const daily = {
|
|
text: 'test daily',
|
|
type: 'daily',
|
|
};
|
|
|
|
const task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line new-cap
|
|
tasksByType.dailys = [];
|
|
tasksByType.dailys.push(task);
|
|
|
|
const statsComputedRes = common.statsComputed(user);
|
|
const stubbedStatsComputed = sinon.stub(common, 'statsComputed');
|
|
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { con: 1 }));
|
|
|
|
daysMissed = 1;
|
|
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
|
});
|
|
|
|
afterEach(async () => {
|
|
common.statsComputed.restore();
|
|
});
|
|
|
|
it('resets user progress', async () => {
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.party.quest.progress.up).to.equal(0);
|
|
expect(user.party.quest.progress.down).to.equal(0);
|
|
expect(user.party.quest.progress.collectedItems).to.equal(0);
|
|
});
|
|
|
|
it('applies the user progress', async () => {
|
|
const progress = await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(progress.down).to.equal(-1);
|
|
});
|
|
});
|
|
|
|
describe('private messages', async () => {
|
|
let lastMessageId;
|
|
|
|
beforeEach(async () => {
|
|
const maxPMs = 200;
|
|
for (let index = 0; index < maxPMs - 1; index += 1) {
|
|
const messageId = common.uuid();
|
|
user.inbox.messages[messageId] = {
|
|
id: messageId,
|
|
text: `test ${index}`,
|
|
timestamp: Number(new Date()),
|
|
likes: {},
|
|
flags: {},
|
|
flagCount: 0,
|
|
};
|
|
}
|
|
|
|
lastMessageId = common.uuid();
|
|
user.inbox.messages[lastMessageId] = {
|
|
id: lastMessageId,
|
|
text: `test ${lastMessageId}`,
|
|
timestamp: Number(new Date()),
|
|
likes: {},
|
|
flags: {},
|
|
flagCount: 0,
|
|
};
|
|
});
|
|
});
|
|
|
|
describe('login incentives', async () => {
|
|
it('increments incentive counter each cron', async () => {
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.loginIncentives).to.eql(1);
|
|
user.lastCron = moment(new Date()).subtract({ days: 1 });
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.loginIncentives).to.eql(2);
|
|
});
|
|
|
|
it('pushes a notification of the day\'s incentive each cron', async () => {
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.notifications.length).to.eql(1);
|
|
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
|
});
|
|
|
|
it('replaces previous notifications', async () => {
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
|
|
const filteredNotifications = user.notifications.filter(n => n.type === 'LOGIN_INCENTIVE');
|
|
|
|
expect(filteredNotifications.length).to.equal(1);
|
|
});
|
|
|
|
it('increments loginIncentives by 1 even if days are skipped in between', async () => {
|
|
daysMissed = 3;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.loginIncentives).to.eql(1);
|
|
});
|
|
|
|
it('increments loginIncentives by 1 even if user is sleeping', async () => {
|
|
user.preferences.sleep = true;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.loginIncentives).to.eql(1);
|
|
});
|
|
|
|
it('awards user bard robes if login incentive is 1', async () => {
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.loginIncentives).to.eql(1);
|
|
expect(user.items.gear.owned.armor_special_bardRobes).to.eql(true);
|
|
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
|
});
|
|
|
|
it('awards user incentive backgrounds if login incentive is 2', async () => {
|
|
user.loginIncentives = 1;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.loginIncentives).to.eql(2);
|
|
expect(user.purchased.background.blue).to.eql(true);
|
|
expect(user.purchased.background.green).to.eql(true);
|
|
expect(user.purchased.background.purple).to.eql(true);
|
|
expect(user.purchased.background.red).to.eql(true);
|
|
expect(user.purchased.background.yellow).to.eql(true);
|
|
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
|
});
|
|
|
|
it('awards user Bard Hat if login incentive is 3', async () => {
|
|
user.loginIncentives = 2;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.loginIncentives).to.eql(3);
|
|
expect(user.items.gear.owned.head_special_bardHat).to.eql(true);
|
|
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
|
});
|
|
|
|
it('awards user RoyalPurple Hatching Potion if login incentive is 4', async () => {
|
|
user.loginIncentives = 3;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.loginIncentives).to.eql(4);
|
|
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
|
|
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
|
});
|
|
|
|
it('awards user a Chocolate, Meat and Pink Contton Candy if login incentive is 5', async () => {
|
|
user.loginIncentives = 4;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.loginIncentives).to.eql(5);
|
|
|
|
expect(user.items.food.Chocolate).to.eql(1);
|
|
expect(user.items.food.Meat).to.eql(1);
|
|
expect(user.items.food.CottonCandyPink).to.eql(1);
|
|
|
|
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
|
});
|
|
|
|
it('awards user moon quest if login incentive is 7', async () => {
|
|
user.loginIncentives = 6;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.loginIncentives).to.eql(7);
|
|
expect(user.items.quests.moon1).to.eql(1);
|
|
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
|
});
|
|
|
|
it('awards user RoyalPurple Hatching Potion if login incentive is 10', async () => {
|
|
user.loginIncentives = 9;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.loginIncentives).to.eql(10);
|
|
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
|
|
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
|
});
|
|
|
|
it('awards user a Strawberry, Patato and Blue Contton Candy if login incentive is 14', async () => {
|
|
user.loginIncentives = 13;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.loginIncentives).to.eql(14);
|
|
|
|
expect(user.items.food.Strawberry).to.eql(1);
|
|
expect(user.items.food.Potatoe).to.eql(1);
|
|
expect(user.items.food.CottonCandyBlue).to.eql(1);
|
|
|
|
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
|
});
|
|
|
|
it('awards user a bard instrument if login incentive is 18', async () => {
|
|
user.loginIncentives = 17;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.loginIncentives).to.eql(18);
|
|
expect(user.items.gear.owned.weapon_special_bardInstrument).to.eql(true);
|
|
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
|
});
|
|
|
|
it('awards user second moon quest if login incentive is 22', async () => {
|
|
user.loginIncentives = 21;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.loginIncentives).to.eql(22);
|
|
expect(user.items.quests.moon2).to.eql(1);
|
|
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
|
});
|
|
|
|
it('awards user a RoyalPurple hatching potion if login incentive is 26', async () => {
|
|
user.loginIncentives = 25;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.loginIncentives).to.eql(26);
|
|
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
|
|
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
|
});
|
|
|
|
it('awards user Fish, Milk, Rotten Meat and Honey if login incentive is 30', async () => {
|
|
user.loginIncentives = 29;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.loginIncentives).to.eql(30);
|
|
|
|
expect(user.items.food.Fish).to.eql(1);
|
|
expect(user.items.food.Milk).to.eql(1);
|
|
expect(user.items.food.RottenMeat).to.eql(1);
|
|
expect(user.items.food.Honey).to.eql(1);
|
|
|
|
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
|
});
|
|
|
|
it('awards user a RoyalPurple hatching potion if login incentive is 35', async () => {
|
|
user.loginIncentives = 34;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.loginIncentives).to.eql(35);
|
|
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
|
|
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
|
});
|
|
|
|
it('awards user the third moon quest if login incentive is 40', async () => {
|
|
user.loginIncentives = 39;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.loginIncentives).to.eql(40);
|
|
expect(user.items.quests.moon3).to.eql(1);
|
|
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
|
});
|
|
|
|
it('awards user a RoyalPurple hatching potion if login incentive is 45', async () => {
|
|
user.loginIncentives = 44;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.loginIncentives).to.eql(45);
|
|
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
|
|
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
|
});
|
|
|
|
it('awards user a saddle if login incentive is 50', async () => {
|
|
user.loginIncentives = 49;
|
|
await cron({
|
|
user, tasksByType, daysMissed, analytics,
|
|
});
|
|
expect(user.loginIncentives).to.eql(50);
|
|
expect(user.items.food.Saddle).to.eql(1);
|
|
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('cron wrapper', () => {
|
|
let res; let
|
|
req;
|
|
let user;
|
|
|
|
beforeEach(async () => {
|
|
res = generateRes();
|
|
req = generateReq();
|
|
user = await res.locals.user.save();
|
|
res.analytics = analytics;
|
|
});
|
|
|
|
afterEach(() => {
|
|
sandbox.restore();
|
|
});
|
|
|
|
it('calls next when user is not attached', async () => {
|
|
res.locals.user = null;
|
|
await cronWrapper(req, res);
|
|
});
|
|
|
|
it('calls next when days have not been missed', async () => {
|
|
await cronWrapper(req, res);
|
|
});
|
|
|
|
it('should clear todos older than 30 days for free users', async () => {
|
|
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
const task = generateTodo(user);
|
|
task.dateCompleted = moment(new Date()).subtract({ days: 31 });
|
|
task.completed = true;
|
|
await task.save();
|
|
await user.save();
|
|
|
|
await cronWrapper(req, res);
|
|
const taskRes = await Tasks.Task.findOne({ _id: task._id });
|
|
expect(taskRes).to.not.exist;
|
|
});
|
|
|
|
it('should not clear todos older than 30 days for subscribed users', async () => {
|
|
user.purchased.plan.customerId = 'subscribedId';
|
|
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
|
|
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
const task = generateTodo(user);
|
|
task.dateCompleted = moment(new Date()).subtract({ days: 31 });
|
|
task.completed = true;
|
|
await Promise.all([task.save(), user.save()]);
|
|
|
|
await cronWrapper(req, res);
|
|
const taskRes = await Tasks.Task.findOne({ _id: task._id });
|
|
expect(taskRes).to.exist;
|
|
});
|
|
|
|
it('should clear todos older than 90 days for subscribed users', async () => {
|
|
user.purchased.plan.customerId = 'subscribedId';
|
|
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
|
|
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
|
|
const task = generateTodo(user);
|
|
task.dateCompleted = moment(new Date()).subtract({ days: 91 });
|
|
task.completed = true;
|
|
await task.save();
|
|
await user.save();
|
|
|
|
await cronWrapper(req, res);
|
|
const taskRes = await Tasks.Task.findOne({ _id: task._id });
|
|
expect(taskRes).to.not.exist;
|
|
});
|
|
|
|
it('should call next if user was not modified after cron', async () => {
|
|
const hpBefore = user.stats.hp;
|
|
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
await user.save();
|
|
|
|
await cronWrapper(req, res);
|
|
expect(hpBefore).to.equal(user.stats.hp);
|
|
});
|
|
|
|
it('runs cron if previous cron was incomplete', async () => {
|
|
user.lastCron = moment(new Date()).subtract({ days: 1 });
|
|
user.auth.timestamps.loggedin = moment(new Date()).subtract({ days: 4 });
|
|
const now = new Date();
|
|
await user.save();
|
|
|
|
await cronWrapper(req, res);
|
|
expect(moment(now).isSame(user.lastCron, 'day'));
|
|
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
|
});
|
|
|
|
it('updates user.auth.timestamps.loggedin and lastCron', async () => {
|
|
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
const now = new Date();
|
|
await user.save();
|
|
|
|
await cronWrapper(req, res);
|
|
expect(moment(now).isSame(user.lastCron, 'day'));
|
|
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
|
});
|
|
|
|
it('does damage for missing dailies', async () => {
|
|
const hpBefore = user.stats.hp;
|
|
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
const daily = generateDaily(user);
|
|
daily.startDate = moment(new Date()).subtract({ days: 2 });
|
|
await daily.save();
|
|
await user.save();
|
|
|
|
await cronWrapper(req, res);
|
|
const updatedUser = await User.findOne({ _id: user._id });
|
|
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
|
|
});
|
|
|
|
it('updates tasks', async () => {
|
|
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
const todo = generateTodo(user);
|
|
const todoValueBefore = todo.value;
|
|
await Promise.all([todo.save(), user.save()]);
|
|
|
|
await cronWrapper(req, res);
|
|
const todoFound = await Tasks.Task.findOne({ _id: todo._id });
|
|
expect(todoFound.value).to.be.lessThan(todoValueBefore);
|
|
});
|
|
|
|
it('updates large number of tasks', async () => {
|
|
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
const todo = generateTodo(user);
|
|
const todoValueBefore = todo.value;
|
|
const start = new Date();
|
|
const saves = [todo.save(), user.save()];
|
|
for (let i = 0; i < 200; i += 1) {
|
|
const newTodo = generateTodo(user);
|
|
newTodo.value = i;
|
|
saves.push(newTodo.save());
|
|
}
|
|
await Promise.all(saves);
|
|
|
|
await cronWrapper(req, res);
|
|
const duration = new Date() - start;
|
|
expect(duration).to.be.lessThan(1000);
|
|
const todoFound = await Tasks.Task.findOne({ _id: todo._id });
|
|
expect(moment(start).isSame(user.lastCron, 'day'));
|
|
expect(moment(start).isSame(user.auth.timestamps.loggedin, 'day'));
|
|
expect(todoFound.value).to.be.lessThan(todoValueBefore);
|
|
});
|
|
|
|
it('fails entire cron if one task is failing', async () => {
|
|
const lastCron = moment(new Date()).subtract({ days: 2 });
|
|
user.lastCron = lastCron;
|
|
const todo = generateTodo(user);
|
|
const todoValueBefore = todo.value;
|
|
const badTodo = generateTodo(user);
|
|
badTodo.text = 'bad todo';
|
|
badTodo.attribute = 'bad';
|
|
await Promise.all([badTodo.save({ validateBeforeSave: false }), todo.save(), user.save()]);
|
|
|
|
try {
|
|
await cronWrapper(req, res);
|
|
} catch (err) {
|
|
expect(err).to.exist;
|
|
}
|
|
const todoFound = await Tasks.Task.findOne({ _id: todo._id });
|
|
expect(moment(lastCron).isSame(user.lastCron, 'day'));
|
|
expect(todoFound.value).to.be.equal(todoValueBefore);
|
|
});
|
|
|
|
it('applies quest progress', async () => {
|
|
const hpBefore = user.stats.hp;
|
|
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
const daily = generateDaily(user);
|
|
daily.startDate = moment(new Date()).subtract({ days: 2 });
|
|
await daily.save();
|
|
|
|
const questKey = 'dilatory';
|
|
user.party.quest.key = questKey;
|
|
|
|
const party = new Group({
|
|
type: 'party',
|
|
name: generateUUID(),
|
|
leader: user._id,
|
|
});
|
|
party.quest.members[user._id] = true;
|
|
party.quest.key = questKey;
|
|
await party.save();
|
|
|
|
user.party._id = party._id;
|
|
await user.save();
|
|
|
|
party.startQuest(user);
|
|
|
|
await cronWrapper(req, res);
|
|
const updatedUser = await User.findOne({ _id: user._id });
|
|
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
|
|
});
|
|
|
|
it('cronSignature less than 5 minutes ago should error', async () => {
|
|
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
const now = new Date();
|
|
await User.updateOne({
|
|
_id: user._id,
|
|
}, {
|
|
$set: {
|
|
_cronSignature: now.getTime() - CRON_TIMEOUT_WAIT + CRON_TIMEOUT_UNIT,
|
|
},
|
|
}).exec();
|
|
await user.save();
|
|
try {
|
|
await cronWrapper(req, res);
|
|
} catch (err) {
|
|
expect(err).to.exist;
|
|
}
|
|
});
|
|
|
|
it('cronSignature longer than an hour ago should allow cron', async () => {
|
|
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
const now = new Date();
|
|
await User.updateOne({
|
|
_id: user._id,
|
|
}, {
|
|
$set: {
|
|
_cronSignature: now.getTime() - CRON_TIMEOUT_WAIT - CRON_TIMEOUT_UNIT,
|
|
},
|
|
}).exec();
|
|
await user.save();
|
|
|
|
await cronWrapper(req, res);
|
|
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
|
expect(user._cronSignature).to.be.equal('NOT_RUNNING');
|
|
});
|
|
|
|
it('cron should not run more than once', async () => {
|
|
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
await user.save();
|
|
|
|
const result = await Promise.allSettled([
|
|
cronWrapper(req, res),
|
|
cronWrapper(req, res),
|
|
new Promise((resolve, reject) => {
|
|
setTimeout(async () => {
|
|
try {
|
|
const runResult = await cronWrapper(req, res);
|
|
if (runResult !== null) {
|
|
reject(new Error('cron ran more than once'));
|
|
} else {
|
|
resolve();
|
|
}
|
|
} catch (err) {
|
|
reject(err);
|
|
}
|
|
}, 200);
|
|
}),
|
|
]);
|
|
|
|
expect(result.filter(r => r.status === 'fulfilled')).to.have.lengthOf(2);
|
|
expect(result.filter(r => r.status === 'rejected')).to.have.lengthOf(1);
|
|
});
|
|
});
|