mirror of
				https://github.com/HabitRPG/habitica.git
				synced 2025-10-30 20:52:29 +01:00 
			
		
		
		
	Compare commits
	
		
			1175 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | b2d6a9474d | ||
|  | 5fac4a943c | ||
|  | 5d0be7bc72 | ||
|  | cc97935ffd | ||
|  | 6ea4d96830 | ||
|  | a63ba51497 | ||
|  | 04a7fd25a6 | ||
|  | 7cb045781b | ||
|  | f8d799d55c | ||
|  | 5bd9fcc99d | ||
|  | 84cafc4081 | ||
|  | d77a17112c | ||
|  | 4f9d97d38f | ||
|  | 40060e8ff5 | ||
|  | 508d97d374 | ||
|  | fd125352b7 | ||
|  | f8bd1be4a3 | ||
|  | ec37524164 | ||
|  | 87b9e72b56 | ||
|  | 962662fe7c | ||
|  | f63d2e47f0 | ||
|  | 349a1032b6 | ||
|  | 476131835d | ||
|  | 8237b7f2de | ||
|  | 6ee2b3690a | ||
|  | a08cca807a | ||
|  | 8c51f36784 | ||
|  | d35f81cdae | ||
|  | 1d1b25391f | ||
|  | ec81c02d72 | ||
|  | 166da3c2f8 | ||
|  | 23b72a673d | ||
|  | d22b4bb2f7 | ||
|  | aaebd4da77 | ||
|  | b128e7874e | ||
|  | 1e10e20a24 | ||
|  | b1aeb8ed87 | ||
|  | 2cb80e2275 | ||
|  | ee32e24ff2 | ||
|  | af40c437be | ||
|  | a99150c485 | ||
|  | 696b67204d | ||
|  | 4f3536e887 | ||
|  | 07e5bf1437 | ||
|  | d2ca738256 | ||
|  | f7983f39eb | ||
|  | 7c954f7073 | ||
|  | 82d0e737a6 | ||
|  | f8213aaf1b | ||
|  | 2335ad4167 | ||
|  | d84631255b | ||
|  | 0b352b9103 | ||
|  | 19b75c6257 | ||
|  | b66904a3a7 | ||
|  | cfbfec34aa | ||
|  | 88f28188a1 | ||
|  | fce5be2e8f | ||
|  | 1d68cbfaa2 | ||
|  | a44222a350 | ||
|  | 4d2510e322 | ||
|  | 3b5ed33e03 | ||
|  | 0a6bf92b6b | ||
|  | 05ae40bc2e | ||
|  | 57e96ea092 | ||
|  | ea49b5b8e0 | ||
|  | 11a5de714a | ||
|  | f74b4d3e73 | ||
|  | ab6fdb99af | ||
|  | e3efa557dd | ||
|  | a6cea47789 | ||
|  | 5a200a88bd | ||
|  | 545499ea0b | ||
|  | 3e7f4229ec | ||
|  | 56d5f77b6e | ||
|  | efdf5a2e16 | ||
|  | add3a2887f | ||
|  | 0ed7c7596a | ||
|  | 55a452694f | ||
|  | cd4d5f83ff | ||
|  | 8220199e49 | ||
|  | bd1f6918ba | ||
|  | 5b21e62647 | ||
|  | 5e76d6df21 | ||
|  | 7348145b7d | ||
|  | ba2832d21f | ||
|  | 688f5084a1 | ||
|  | d5955b8889 | ||
|  | 01fd17ee3f | ||
|  | 979497dd35 | ||
|  | 1d3db244ba | ||
|  | 80ca074352 | ||
|  | 6d4f9e0759 | ||
|  | d096695559 | ||
|  | a2c8b8b05c | ||
|  | 8750701c08 | ||
|  | 900676bf0a | ||
|  | e219daf44c | ||
|  | 1b12a6b51f | ||
|  | 8441b0a3d6 | ||
|  | 0667695390 | ||
|  | 4d322c1bf6 | ||
|  | a855ddacc7 | ||
|  | 9d48ef7322 | ||
|  | 2991f7acfb | ||
|  | 9dbfb565bb | ||
|  | f42e22b58f | ||
|  | 95f3315796 | ||
|  | 18ab57eb91 | ||
|  | 0dcbd8ccb8 | ||
|  | ed82b46f7b | ||
|  | dcc3044685 | ||
|  | 5cd62d7052 | ||
|  | 5e232d8c9f | ||
|  | 13793f8b3c | ||
|  | 55f875f95a | ||
|  | 14576be374 | ||
|  | 4cb2c26475 | ||
|  | b66a0b76ef | ||
|  | dd313b17b5 | ||
|  | bb01475e02 | ||
|  | 757959529b | ||
|  | 71ad1957b1 | ||
|  | 3ba5ea1d2d | ||
|  | 902da35f2b | ||
|  | aaa16a9527 | ||
|  | b98e95ee45 | ||
|  | 757160d6b7 | ||
|  | 4cf68eb018 | ||
|  | e91d5e5664 | ||
|  | b9e12aca3e | ||
|  | 53ca9475ee | ||
|  | e212842b50 | ||
|  | d2f7cba43d | ||
|  | 84558f79d6 | ||
|  | 178e59f287 | ||
|  | 7acccc0763 | ||
|  | 8ac21d2fd4 | ||
|  | 7873800f87 | ||
|  | 727041f020 | ||
|  | de0c62a37f | ||
|  | 68f420991e | ||
|  | f211ebeb0a | ||
|  | b91ef9f539 | ||
|  | ac0601630e | ||
|  | 3239491144 | ||
|  | c3b0a73507 | ||
|  | b218eb2c00 | ||
|  | 37d9f76fea | ||
|  | 7f2e12ba23 | ||
|  | 21b43287e3 | ||
|  | 357b48dc8f | ||
|  | 2cbd78b139 | ||
|  | ee5dd5842b | ||
|  | 074b8138de | ||
|  | cd0b9c0a96 | ||
|  | a09516944d | ||
|  | b82660823d | ||
|  | fde4402fbb | ||
|  | 5fe0776074 | ||
|  | d218f316d3 | ||
|  | f19e69948a | ||
|  | e6807d36b5 | ||
|  | 1ece230621 | ||
|  | d934d9d759 | ||
|  | bf7fabb20a | ||
|  | 88a2f317d8 | ||
|  | a30c4379a6 | ||
|  | b509c6631d | ||
|  | 5a725fa4b0 | ||
|  | dbe2143b7a | ||
|  | 7b687280d7 | ||
|  | 8db6c8bd4f | ||
|  | bf91dacb94 | ||
|  | 33e0892e95 | ||
|  | 42b146d5d0 | ||
|  | 2bebaf2cf8 | ||
|  | 6181328ac1 | ||
|  | c64d4b0914 | ||
|  | f94fd0d69d | ||
|  | 2cd66436bc | ||
|  | 81a17738b8 | ||
|  | b6b953ec46 | ||
|  | ab34c83a9d | ||
|  | 9cea86f4e0 | ||
|  | 1b7a705bf9 | ||
|  | e2c5b9058b | ||
|  | cc751960ac | ||
|  | 433c73c9d3 | ||
|  | 680c2162a7 | ||
|  | a2b38ffb02 | ||
|  | 6395870c00 | ||
|  | 9562ba432f | ||
|  | 8a3a83de37 | ||
|  | 745edd731d | ||
|  | 53b195931c | ||
|  | 37ae467fff | ||
|  | d7d7d64b45 | ||
|  | e76bdbd62d | ||
|  | 067e869141 | ||
|  | 51224a69d9 | ||
|  | f56018d46a | ||
|  | b846185f8a | ||
|  | ff81e55839 | ||
|  | 9a3cdb5deb | ||
|  | 47ad7305f5 | ||
|  | d9b5bbe2a9 | ||
|  | c2fe04367f | ||
|  | abcc77b7d6 | ||
|  | 07cbf45265 | ||
|  | c035435476 | ||
|  | 89fdd8a8bb | ||
|  | d406da4081 | ||
|  | d74786ef85 | ||
|  | 64a3d08ce3 | ||
|  | f635f178da | ||
|  | 1a7461a8a2 | ||
|  | cc13c4f28e | ||
|  | 239f78674b | ||
|  | d691dee2ca | ||
|  | 481bd6727d | ||
|  | d0fc1e0751 | ||
|  | b07dbb7752 | ||
|  | 34e7690c38 | ||
|  | eca7382545 | ||
|  | be95cd967a | ||
|  | ce03f837c7 | ||
|  | 808885425f | ||
|  | 39a35f44ef | ||
|  | 2b2e1d4b9a | ||
|  | 869411c0e9 | ||
|  | 7484ecf729 | ||
|  | 3265440bc4 | ||
|  | acf514e9cb | ||
|  | 2789d44dbf | ||
|  | b579f31e9e | ||
|  | 5e781017ab | ||
|  | b48f850eac | ||
|  | 5d6b6ed29a | ||
|  | 7fbc68511b | ||
|  | ee2858199b | ||
|  | b1dd79f75c | ||
|  | 1ac4dd8171 | ||
|  | 4f86abd6b2 | ||
|  | 23b0688abb | ||
|  | 38efe83cc7 | ||
|  | 2dadd74097 | ||
|  | 9f494360ef | ||
|  | 4122bbdecf | ||
|  | 256a3abc26 | ||
|  | b7f3c0f389 | ||
|  | dca00bf4b7 | ||
|  | d31c8913d3 | ||
|  | d839d57299 | ||
|  | ccc3b4d337 | ||
|  | 7195ac15b9 | ||
|  | a5ef6a129e | ||
|  | 38f5d63d29 | ||
|  | 43194b71ce | ||
|  | e4a347a3cb | ||
|  | 7eaf3e04ab | ||
|  | b6b03751c4 | ||
|  | 818d5e4eb6 | ||
|  | f871c7cf63 | ||
|  | e9eddec0c4 | ||
|  | a48a6a292d | ||
|  | 12aef475c8 | ||
|  | 112e4e1d76 | ||
|  | 90eebbcd70 | ||
|  | 1ad9ba4e71 | ||
|  | c42b72f8a8 | ||
|  | 830c8d3104 | ||
|  | 86ae5f3e44 | ||
|  | 3922415314 | ||
|  | 6c71abfac8 | ||
|  | 6ab08a7d52 | ||
|  | dc46127fc7 | ||
|  | b54f031acd | ||
|  | eafa2f8cdd | ||
|  | 1815d2b6d3 | ||
|  | 14cba76ba8 | ||
|  | fe45940d46 | ||
|  | 7dac53867b | ||
|  | 6f64cb7d9b | ||
|  | 7d989bcf50 | ||
|  | 7bd29c2dd7 | ||
|  | 9e10490102 | ||
|  | 5792bc0000 | ||
|  | 06812878b5 | ||
|  | 8714c7d162 | ||
|  | e66f4e7812 | ||
|  | c73f565f65 | ||
|  | 82e21df943 | ||
|  | 18ed148320 | ||
|  | a5fc909f0d | ||
|  | 30a5192e19 | ||
|  | 79c0499672 | ||
|  | dadb752087 | ||
|  | 37b29d3449 | ||
|  | bb2ed249b9 | ||
|  | bb90dde1b6 | ||
|  | 5299c8d406 | ||
|  | 8b81e38538 | ||
|  | 8e05a1b489 | ||
|  | aafcbe60a3 | ||
|  | 56d1b77215 | ||
|  | 61da558a5d | ||
|  | 490531cc76 | ||
|  | 8cd4c502bc | ||
|  | d8cacb653e | ||
|  | 59436a8bf7 | ||
|  | 6fade19f27 | ||
|  | 15d028a281 | ||
|  | 95b283676a | ||
|  | 6e7e81206a | ||
|  | af74cc7c64 | ||
|  | a9e2a17077 | ||
|  | 92057dbe17 | ||
|  | b4ab525be5 | ||
|  | d3c464d5ea | ||
|  | 804fe1c6d5 | ||
|  | 3d757c7814 | ||
|  | 3c5025a78e | ||
|  | e028232527 | ||
|  | e3b270a62e | ||
|  | fceeacec3b | ||
|  | f93c67e57c | ||
|  | 192dc26fbe | ||
|  | 1c1f270f64 | ||
|  | 483768f4a7 | ||
|  | 65031cef3a | ||
|  | 2fc1f46359 | ||
|  | 30fd530576 | ||
|  | f79999fde7 | ||
|  | 90d6e443ba | ||
|  | 4ed1082558 | ||
|  | 00717eda76 | ||
|  | d1b86e6c14 | ||
|  | c813afba44 | ||
|  | d49db6d367 | ||
|  | d6835aec56 | ||
|  | 960f7b5886 | ||
|  | cd9630332d | ||
|  | ed21a37e5a | ||
|  | 16256ee190 | ||
|  | ff57e31f4f | ||
|  | 6e21d154ae | ||
|  | fdecc8ce16 | ||
|  | 3cc49f6637 | ||
|  | 47f49f4256 | ||
|  | 4f4bb52360 | ||
|  | 3748b3046b | ||
|  | 5cd0f56811 | ||
|  | fe5beac91b | ||
|  | 52fd6a1451 | ||
|  | ae445555e9 | ||
|  | c4fc6671b4 | ||
|  | e7a096158e | ||
|  | 98473fcfaa | ||
|  | e4300fc714 | ||
|  | 456c5e57bc | ||
|  | ffba435923 | ||
|  | 1f44444a50 | ||
|  | 185b20995a | ||
|  | fdf2e590ea | ||
|  | 994123c387 | ||
|  | 273590716c | ||
|  | 6818a094ee | ||
|  | c99855cef4 | ||
|  | 6845943ed0 | ||
|  | 044fe17757 | ||
|  | ad0ede8d01 | ||
|  | 23815e89e1 | ||
|  | 061d990e39 | ||
|  | 71f4e6bc08 | ||
|  | 659f160e22 | ||
|  | 5f27bc5f90 | ||
|  | 074837b274 | ||
|  | cfd19ac694 | ||
|  | 0897ab5dc9 | ||
|  | 5c6e8a7331 | ||
|  | c576c5261e | ||
|  | bbd98517ff | ||
|  | 392b54aa7b | ||
|  | 60b26d4ec0 | ||
|  | aa517e0ad6 | ||
|  | 5ca489dee7 | ||
|  | fe39ef72ff | ||
|  | eee5f2f1df | ||
|  | fd8572c28a | ||
|  | f161987e1e | ||
|  | 2304d970a5 | ||
|  | 25ed05ab0a | ||
|  | fa1fef11d6 | ||
|  | 6f5b9ef119 | ||
|  | c64ea0a9a9 | ||
|  | 2e36b896d4 | ||
|  | 6fe73d431e | ||
|  | 998621cefe | ||
|  | 67bb179c25 | ||
|  | c875861dab | ||
|  | 418c18ddb2 | ||
|  | 0caab5c8d0 | ||
|  | 218e65b04b | ||
|  | fcd7ba77a7 | ||
|  | b0d177643c | ||
|  | c0e0b10a95 | ||
|  | 0bee2caf2e | ||
|  | e56d097b3a | ||
|  | 8c63a9e31f | ||
|  | 28ed9d8bcc | ||
|  | 36ead77e0c | ||
|  | e7969987ec | ||
|  | 97021e3422 | ||
|  | 218d47d64a | ||
|  | bdfc23717e | ||
|  | 464cd87736 | ||
|  | 67a8eebb96 | ||
|  | cfc0f6a3ac | ||
|  | 9f76db12bd | ||
|  | 70192e4935 | ||
|  | 5cd4ead9d1 | ||
|  | 87cd000bb8 | ||
|  | 0de5d8273b | ||
|  | 379898cc4d | ||
|  | adeaa6c754 | ||
|  | 539f0e33e2 | ||
|  | 405e053377 | ||
|  | 52fbb8f899 | ||
|  | c880596a77 | ||
|  | a35f04be46 | ||
|  | 8682cf1cf7 | ||
|  | 6e922cfb44 | ||
|  | cafabd93e1 | ||
|  | 1001d48eb7 | ||
|  | b5c4618d56 | ||
|  | 92a4ba93d2 | ||
|  | 90d35d2f1f | ||
|  | fead027cd2 | ||
|  | 5578426985 | ||
|  | 1c39fae127 | ||
|  | 45a757b589 | ||
|  | 8b610d771c | ||
|  | bd81d27145 | ||
|  | 8eb430cbcb | ||
|  | f218133d25 | ||
|  | 5f440d9097 | ||
|  | 0294868747 | ||
|  | 9c8d870d16 | ||
|  | a7acd863f3 | ||
|  | f32ef0a6ba | ||
|  | ebf3b4aa47 | ||
|  | 5a8366468b | ||
|  | df57518815 | ||
|  | 7d342b5115 | ||
|  | 388de9a97d | ||
|  | 28c79d9d20 | ||
|  | 85cf322b30 | ||
|  | 362ca73c94 | ||
|  | 5632031f16 | ||
|  | 90273362c4 | ||
|  | 7aadc10fab | ||
|  | bfd45596b5 | ||
|  | 2eed4d38ae | ||
|  | 29bbe8534b | ||
|  | 9e008890b2 | ||
|  | 5505bf1e45 | ||
|  | d40781ce07 | ||
|  | d9719cdc05 | ||
|  | 8cc6a96be0 | ||
|  | c5fb2d6506 | ||
|  | afc336461e | ||
|  | 31376c8461 | ||
|  | 3a849bac18 | ||
|  | 563f3e2012 | ||
|  | e24a024091 | ||
|  | dc7d3816fd | ||
|  | a094e13352 | ||
|  | 83376a38de | ||
|  | db9c13a05d | ||
|  | 8c8aa78a1a | ||
|  | 6e3f7c005a | ||
|  | 1395380dfe | ||
|  | 833ceb3bf3 | ||
|  | 0522aa1551 | ||
|  | 58a9e4a439 | ||
|  | 84e2b2f45e | ||
|  | 71c0939a15 | ||
|  | 26c8323e70 | ||
|  | bb7d447003 | ||
|  | 97ea510a34 | ||
|  | 99610b4916 | ||
|  | 9a43b85492 | ||
|  | ecbf39cee4 | ||
|  | 4394772ee3 | ||
|  | dd7fa73961 | ||
|  | 6ec23ce790 | ||
|  | b953519e2d | ||
|  | 33a8072d23 | ||
|  | 213316d807 | ||
|  | 44cd4d0708 | ||
|  | 063b7a9af0 | ||
|  | c08b5a4f1e | ||
|  | 90117625d7 | ||
|  | 1b3dad749e | ||
|  | a622a3ebe3 | ||
|  | 2c83c16644 | ||
|  | 1034675184 | ||
|  | a420876697 | ||
|  | dc265e26b3 | ||
|  | b5203dda61 | ||
|  | 80e92a8767 | ||
|  | 1c51e62e43 | ||
|  | a265bfac9d | ||
|  | 92e4d5cd68 | ||
|  | a18e9b3b18 | ||
|  | c1a6ba6242 | ||
|  | ed761a8b7b | ||
|  | 81d5971829 | ||
|  | eb2d320d1f | ||
|  | 67538a368e | ||
|  | d55b95834d | ||
|  | 9ff9cd3b35 | ||
|  | b1f24de3c4 | ||
|  | 2ce2100f89 | ||
|  | dbaae4183e | ||
|  | 2009bb97cb | ||
|  | 3fc9501bac | ||
|  | 2c2ded2b70 | ||
|  | d689010e38 | ||
|  | e173b7784c | ||
|  | c3db59aae8 | ||
|  | 44e063c035 | ||
|  | 4e2c08cfed | ||
|  | c845c337df | ||
|  | 418b57f9fb | ||
|  | 9725da258e | ||
|  | 4191ea1968 | ||
|  | a9340ee60f | ||
|  | c8d874d28a | ||
|  | 32a22f1545 | ||
|  | 8ffe302a49 | ||
|  | 5c50a40f39 | ||
|  | 84329e5fad | ||
|  | 64507a161e | ||
|  | 0f7fc27663 | ||
|  | 1545685a5b | ||
|  | 410355c3f1 | ||
|  | ac27cabf6a | ||
|  | 972631e7ac | ||
|  | d27ed7c406 | ||
|  | 031783b1d7 | ||
|  | 318aa7cbd9 | ||
|  | f802a41f75 | ||
|  | 1d597039ca | ||
|  | 07bc374078 | ||
|  | 8153674dc0 | ||
|  | 0002148326 | ||
|  | d198e23de6 | ||
|  | 4f4e141806 | ||
|  | 05e8d6f032 | ||
|  | 39847893d2 | ||
|  | e4dbf09dda | ||
|  | eb99b709e0 | ||
|  | 862b3453f8 | ||
|  | 7f48853d32 | ||
|  | 5c4f763bb1 | ||
|  | bc9401b2f7 | ||
|  | 6fb9030b96 | ||
|  | ba307af963 | ||
|  | cf4b920a67 | ||
|  | b0ff35a8f1 | ||
|  | 85b4c7825e | ||
|  | 5b7ea8ec5c | ||
|  | 5cfd0c863e | ||
|  | 10c6244c0c | ||
|  | 20e65be8bf | ||
|  | 8bac324ba7 | ||
|  | 2ee0288aaa | ||
|  | b7ef4c50b2 | ||
|  | 52be9c750f | ||
|  | b0200026aa | ||
|  | e6c8b977c8 | ||
|  | c78b5ecf7c | ||
|  | f27e9b02d8 | ||
|  | c06c19ca41 | ||
|  | d5d894b8a9 | ||
|  | 7bd4e6a5a9 | ||
|  | f13eed5663 | ||
|  | a9a2fe6314 | ||
|  | d6514bce8b | ||
|  | c862bdb76a | ||
|  | b596576c53 | ||
|  | 603fc8c4dd | ||
|  | 3c602351f9 | ||
|  | 29ed33461c | ||
|  | fbc1044100 | ||
|  | 35e02d2871 | ||
|  | 6aa204c3f5 | ||
|  | eaaa5ad7f3 | ||
|  | 54468ff499 | ||
|  | 53405aa586 | ||
|  | 7630c02e13 | ||
|  | ec444384f4 | ||
|  | cce9b33844 | ||
|  | b977d42402 | ||
|  | 2672cbd790 | ||
|  | f049d29d1b | ||
|  | b7ca5be6ee | ||
|  | 5ae89761b0 | ||
|  | 8b0101c74c | ||
|  | dbd295e35b | ||
|  | b5428f4ac9 | ||
|  | 5b213b4f94 | ||
|  | 0142e332e8 | ||
|  | b4f955333b | ||
|  | 696121fb24 | ||
|  | 2a7dfff88a | ||
|  | 2c921609c1 | ||
|  | 1f7dd421d4 | ||
|  | 45ca090105 | ||
|  | 02b22170e2 | ||
|  | 1134c7748b | ||
|  | 7019e32eed | ||
|  | 485c528b45 | ||
|  | f1c1ba8efa | ||
|  | b0e4c2cb11 | ||
|  | 0e346f7050 | ||
|  | 1eb1fe76a8 | ||
|  | 72a0e05804 | ||
|  | 5f37b9727a | ||
|  | bf17b49046 | ||
|  | 36edf5265f | ||
|  | 6da243e034 | ||
|  | b87ff03210 | ||
|  | 9d994f8a77 | ||
|  | 75c3f7214b | ||
|  | f3f8fa3a42 | ||
|  | 2e34dab9a6 | ||
|  | 0f9b274059 | ||
|  | 041bde0cba | ||
|  | 8888e63005 | ||
|  | 7aa2fac14a | ||
|  | 4493e1d98c | ||
|  | fcbc2acda7 | ||
|  | 729ba36ed3 | ||
|  | 896495cac5 | ||
|  | 3f89dae8c9 | ||
|  | 4ec5df170c | ||
|  | fdbcd99525 | ||
|  | 99726bdc2f | ||
|  | b00d1a067e | ||
|  | 0910ca7470 | ||
|  | 24f5e7c19f | ||
|  | 4f34443b84 | ||
|  | 714706f925 | ||
|  | 0899dddb42 | ||
|  | f123fcd1b3 | ||
|  | 6ade7b08c8 | ||
|  | c88b9b80b5 | ||
|  | 33149e1afa | ||
|  | c8becbccb5 | ||
|  | c9465cbfdd | ||
|  | 965b7a3be7 | ||
|  | 6b8784cf04 | ||
|  | 508d832d73 | ||
|  | 734e4a963f | ||
|  | 40495aaacb | ||
|  | 5566460541 | ||
|  | 774a1d9a96 | ||
|  | 60df912dcc | ||
|  | 7325bc0871 | ||
|  | 2fc233e70f | ||
|  | 9fd26a88ea | ||
|  | 76860fe3f8 | ||
|  | b16e700de5 | ||
|  | 9205ec10b3 | ||
|  | 9aa4cce3b9 | ||
|  | 42d823ab44 | ||
|  | f19331cfcc | ||
|  | b7de7335ed | ||
|  | 2e00ec5534 | ||
|  | f30d4b2cbf | ||
|  | a3af39ed25 | ||
|  | b57518732e | ||
|  | a6a6aac400 | ||
|  | 48a92e77be | ||
|  | a31f1f19fc | ||
|  | d2a39a5124 | ||
|  | 9b69b640c3 | ||
|  | 7bead74b49 | ||
|  | 91e91788ce | ||
|  | 62c60ce520 | ||
|  | 423eafbd4d | ||
|  | 004ab51c46 | ||
|  | f464403623 | ||
|  | 0fc66bef4e | ||
|  | 71636cd25e | ||
|  | d5efb50d9b | ||
|  | 510e01effd | ||
|  | b75e65f42d | ||
|  | 0e648d85a0 | ||
|  | 7b562c45cf | ||
|  | b7a46637d5 | ||
|  | 284b2cc413 | ||
|  | 8b69540e71 | ||
|  | 8d9a4e97a8 | ||
|  | f31a82c8f2 | ||
|  | 8bc02e82ee | ||
|  | 9040f9f04e | ||
|  | ff82c37d5f | ||
|  | 37364b0700 | ||
|  | 11cfb3920a | ||
|  | f5468d3771 | ||
|  | 99882d09ab | ||
|  | 7034d135d5 | ||
|  | 034c0c9bb5 | ||
|  | e0711655f0 | ||
|  | 71e162eed5 | ||
|  | 8eac8732c5 | ||
|  | 896a1b74b6 | ||
|  | 3b36046a6a | ||
|  | 07991817e7 | ||
|  | 47b75156fa | ||
|  | c630486fef | ||
|  | 0a070316b5 | ||
|  | f6b34e85df | ||
|  | 2946f0df15 | ||
|  | 614d9a920a | ||
|  | 454524fb5b | ||
|  | abc0777412 | ||
|  | 6972eb8f8f | ||
|  | 535ee2b2a7 | ||
|  | c9755bee7c | ||
|  | f810fff6fc | ||
|  | 5bbe59c52d | ||
|  | 3f52401384 | ||
|  | e7944b3d98 | ||
|  | 08e925e3da | ||
|  | 16c9e42ad8 | ||
|  | 0e1d00c95f | ||
|  | 166f4683ca | ||
|  | 1fcc0d8d3a | ||
|  | 8a4c4e10f1 | ||
|  | 18ed0fe446 | ||
|  | 1f9ebeb629 | ||
|  | b9d83122d1 | ||
|  | d1f7e64156 | ||
|  | 0f8e7416f8 | ||
|  | 1c8b0f92df | ||
|  | 75e5b20f93 | ||
|  | f9db432794 | ||
|  | 6cec7cbba2 | ||
|  | f76d097313 | ||
|  | 59af471438 | ||
|  | d0da303b7d | ||
|  | 596383e7a8 | ||
|  | accba7fc13 | ||
|  | dfc54f1600 | ||
|  | b76ab58e3e | ||
|  | 6f093a94c4 | ||
|  | a9a9e7a4ab | ||
|  | b2058ec23d | ||
|  | 2461f53cf5 | ||
|  | 73fc288f3b | ||
|  | ea78b6feb9 | ||
|  | 7c141614ed | ||
|  | c072935e80 | ||
|  | 54e49ca3b9 | ||
|  | b16a245d61 | ||
|  | 0a8109e496 | ||
|  | cdfcc6419f | ||
|  | 9c25c2452f | ||
|  | 573f2e4732 | ||
|  | 81ffcf9c1b | ||
|  | d8925a8811 | ||
|  | 217c16988b | ||
|  | de7f953b67 | ||
|  | 8ee2a02e73 | ||
|  | d9b573b430 | ||
|  | cbcf5a03e1 | ||
|  | 487523f64b | ||
|  | 74cfc2cf52 | ||
|  | 2d489e870f | ||
|  | 6659d4fa52 | ||
|  | 5471af74fa | ||
|  | 6eb484605a | ||
|  | 8969b755a6 | ||
|  | 0062e5b1f1 | ||
|  | 50b98d8d92 | ||
|  | 7ddf4b1f7b | ||
|  | c91da86b89 | ||
|  | d549fea4ed | ||
|  | a362914f93 | ||
|  | 61001d0e9a | ||
|  | fb95d001ab | ||
|  | bd5c4a08e2 | ||
|  | 34fb90455c | ||
|  | d038d9f9bb | ||
|  | 420d7df4f5 | ||
|  | 3ee8072a6c | ||
|  | 50ebdd1ece | ||
|  | 4a80dcae2e | ||
|  | bc03c1d18a | ||
|  | e5d834b40a | ||
|  | 7f847d322f | ||
|  | ed27ac15c8 | ||
|  | 88188e56d9 | ||
|  | 7170cf05d0 | ||
|  | 22a8d5e94c | ||
|  | f37e5cde57 | ||
|  | 592cfef6c6 | ||
|  | c1bd7f5dc5 | ||
|  | 8437b916c4 | ||
|  | 52e53aa466 | ||
|  | 4471186e09 | ||
|  | 6a2a844e04 | ||
|  | 122d147f07 | ||
|  | 912f00a652 | ||
|  | 627d4330c8 | ||
|  | c7f6794dda | ||
|  | 00cb50a781 | ||
|  | 8be9964483 | ||
|  | fd700f92ae | ||
|  | f41665f5a9 | ||
|  | 8b385c0b7b | ||
|  | 282f8db933 | ||
|  | 662b08c242 | ||
|  | 7ea6c911cb | ||
|  | 97a069642d | ||
|  | 26e9827d39 | ||
|  | 88c625fe80 | ||
|  | 4044432fad | ||
|  | 6a767ed70b | ||
|  | 2c2ca4a9c8 | ||
|  | 380ad8c9e5 | ||
|  | f53400f950 | ||
|  | 53c719acbd | ||
|  | 948a5d80c8 | ||
|  | e1f141ee91 | ||
|  | 7a5a278dbb | ||
|  | 08ab4d5900 | ||
|  | b8d5844d0f | ||
|  | 39b81aa685 | ||
|  | 44f196080c | ||
|  | 65bfd74c93 | ||
|  | 5e60a05cac | ||
|  | 59af4a2d3b | ||
|  | 8d273fac5e | ||
|  | b58032d5fb | ||
|  | db4123610f | ||
|  | e4c1d96b59 | ||
|  | fe1f0bf087 | ||
|  | ccd0bec28c | ||
|  | eebf38b5ae | ||
|  | 30cd738635 | ||
|  | 920b07ff12 | ||
|  | a7db15d768 | ||
|  | 93768e70c5 | ||
|  | 3e29b958e3 | ||
|  | ce1bcdeee0 | ||
|  | 971145a72a | ||
|  | 36595e0138 | ||
|  | a1de566c34 | ||
|  | 2b8415fad0 | ||
|  | 2afbc23f6f | ||
|  | a35c1954af | ||
|  | 47c488967c | ||
|  | ee4a05d7ec | ||
|  | 308cd49e9c | ||
|  | 48e51a03d4 | ||
|  | 96f7a192d7 | ||
|  | 1e786412ba | ||
|  | 1739b83609 | ||
|  | 3606b58a1d | ||
|  | 202db599ae | ||
|  | 3aca0343e8 | ||
|  | 97b99c0550 | ||
|  | 0e63f68ed6 | ||
|  | fa142e929f | ||
|  | c4867f1e8e | ||
|  | 5f58fe66de | ||
|  | a0e2d6a05e | ||
|  | b67522e92b | ||
|  | 0e3496395c | ||
|  | 6e7b9f1f93 | ||
|  | e6cf7564b8 | ||
|  | bf424573a4 | ||
|  | ac90a40be5 | ||
|  | 821f84dbe8 | ||
|  | 8fb67e7944 | ||
|  | e81e458e9b | ||
|  | aec23d32f3 | ||
|  | 4f2d066d66 | ||
|  | eaf0c62e16 | ||
|  | fc62db147f | ||
|  | 6c9ff3e8ed | ||
|  | 6ef45a7fd2 | ||
|  | 557212b549 | ||
|  | f8bd116e54 | ||
|  | 9194e8226d | ||
|  | e0140f67be | ||
|  | 1e2fc14db9 | ||
|  | 30082a3929 | ||
|  | 42d7744d12 | ||
|  | 2cbc41d02f | ||
|  | 01ce7712e3 | ||
|  | c52e4a07d4 | ||
|  | 5212ac6394 | ||
|  | 7b5d6b508d | ||
|  | c5a497ef91 | ||
|  | 54bee67e03 | ||
|  | 86ec68bedb | ||
|  | 8223563e76 | ||
|  | 37ab257f5b | ||
|  | 04d7ff13de | ||
|  | 25d07ac0ce | ||
|  | 724e1240a3 | ||
|  | 026e1a5bca | ||
|  | 6443918440 | ||
|  | ac973ee753 | ||
|  | c39b9dc320 | ||
|  | 2132a3a242 | ||
|  | ba52a90d93 | ||
|  | daa4994382 | ||
|  | 12034161b7 | ||
|  | 8438cf0578 | ||
|  | 614848d60b | ||
|  | 79087b27d3 | ||
|  | 5167f847d0 | ||
|  | d3a0348ac7 | ||
|  | de9883c3ac | ||
|  | 3d39718048 | ||
|  | a0c51ee4ca | ||
|  | b2edd1d932 | ||
|  | 6b5f46c5e1 | ||
|  | ad191c2c5c | ||
|  | 4a55d36831 | ||
|  | 959adb05cf | ||
|  | d114b858fd | ||
|  | ae9db7aee3 | ||
|  | 10567d81e2 | ||
|  | ba799c67f9 | ||
|  | 37b890f282 | ||
|  | 196e5f5b95 | ||
|  | 6db412f7e6 | ||
|  | fa60c9a232 | ||
|  | 2c3d268a63 | ||
|  | d4a80a8561 | ||
|  | 388492e1e7 | ||
|  | 8cd695c397 | ||
|  | 355f0fedfb | ||
|  | 38d78de4b3 | ||
|  | 6c64a1cd8c | ||
|  | 128ec5a1b1 | ||
|  | d4d668f640 | ||
|  | 41ccd58f8e | ||
|  | a33299a341 | ||
|  | 9129e22433 | ||
|  | 86d1bdaff1 | ||
|  | 206ed1f155 | ||
|  | eb66e9ec2e | ||
|  | 8db99be017 | ||
|  | c62386e2e5 | ||
|  | 042ac6ac73 | ||
|  | a8655d923a | ||
|  | bbbd1f9f73 | ||
|  | 8fee5a9ba0 | ||
|  | 8df2b1e8c2 | ||
|  | 24cceb1c91 | ||
|  | 21eac3cc94 | ||
|  | e9ce968f88 | ||
|  | 8c283fdbe0 | ||
|  | 69a782a1db | ||
|  | ccaf629228 | ||
|  | 147f2bb28e | ||
|  | 54a4bba228 | ||
|  | 6ee21dcfa9 | ||
|  | 68353fb874 | ||
|  | 68526c07ae | ||
|  | 5f319ca4f6 | ||
|  | 0d84643961 | ||
|  | 8470f16f4f | ||
|  | 1896a8fab0 | ||
|  | 891b5566a9 | ||
|  | c83499545c | ||
|  | 4c837acf88 | ||
|  | 11b223a81e | ||
|  | 17001743e1 | ||
|  | ac451bdb9b | ||
|  | 6af50c9f2f | ||
|  | 5231cb03a8 | ||
|  | f8739b6f37 | ||
|  | e31f62a818 | ||
|  | d5d06c1d2d | ||
|  | 4fa2ef045d | ||
|  | 570a8bf0d5 | ||
|  | b7dfe41e15 | ||
|  | c26696a9eb | ||
|  | f226b5da07 | ||
|  | 63cf5b6be7 | ||
|  | f3a947339c | ||
|  | 1bb8acad5d | ||
|  | 8e04d6e284 | ||
|  | f7415df6ba | ||
|  | f85e1c2dc4 | ||
|  | 33628a0a6a | ||
|  | 5e6541faa6 | ||
|  | c1ed02d383 | ||
|  | 3793e92b80 | ||
|  | e3ce1c5322 | ||
|  | 84b16f28c2 | ||
|  | 9c702505a9 | ||
|  | 27c73e028a | ||
|  | d125b8d2f8 | ||
|  | 451e08ce1c | ||
|  | 16b5b8b8c7 | ||
|  | 30a717148e | ||
|  | bcf9670dbe | ||
|  | 129fccf646 | ||
|  | 9d755c5d5f | ||
|  | 05c43d1f9d | ||
|  | 45df73e4be | ||
|  | eaa00598d0 | ||
|  | 88b14592c5 | ||
|  | 85136675e9 | ||
|  | 4e4181a394 | ||
|  | f93822b0b3 | ||
|  | a864e69042 | ||
|  | 2ccd9eaa1e | ||
|  | 5faf00d489 | ||
|  | f211610f5d | ||
|  | 332f285ea2 | ||
|  | 006159cc9c | ||
|  | 3722452b51 | ||
|  | d6b5d275da | ||
|  | 72073386ec | ||
|  | d34ec62901 | ||
|  | ca73b9af41 | ||
|  | 8b9bf88fa0 | ||
|  | 9133250a42 | ||
|  | e60177f14a | ||
|  | 5f0ef2d8f0 | ||
|  | 770285f10d | ||
|  | 495dd2736c | ||
|  | 4467da980c | ||
|  | 082539b982 | ||
|  | ef7719f91d | ||
|  | f98efd4eb9 | ||
|  | 4a0856c919 | ||
|  | 2adc5c13e4 | ||
|  | cf274310a8 | ||
|  | a2ee73a2e2 | ||
|  | c6c9503e22 | ||
|  | 403ac1ab7e | ||
|  | 63598f497b | ||
|  | 9fcc953b18 | ||
|  | 17408d01a9 | ||
|  | ae786f28a2 | ||
|  | 1effa16b5b | ||
|  | b7e601be16 | ||
|  | 6b7333927a | ||
|  | 31b439129d | ||
|  | 2de85b937f | ||
|  | 4f963e99dc | ||
|  | 58ce3a9a42 | ||
|  | 395676fcb1 | ||
|  | cc4df1c995 | ||
|  | 48eada2c37 | ||
|  | e45d0c9b80 | ||
|  | 84a20ef4f4 | ||
|  | 59a22805b9 | ||
|  | 95865f5ec8 | ||
|  | 79903d242f | ||
|  | 90959c18cd | ||
|  | 8b2019c292 | ||
|  | 9ab70ca276 | ||
|  | d51aa25470 | ||
|  | 0b2c1e6d2e | ||
|  | 58ee6e9703 | ||
|  | 0044778497 | ||
|  | 46d6590fec | ||
|  | b10f056a73 | ||
|  | eeb890466a | ||
|  | 8d25a5d140 | ||
|  | 3b35a0a203 | ||
|  | d787ad43d3 | ||
|  | 7d7fe6047c | ||
|  | 0ec1a91774 | ||
|  | adf3281bef | ||
|  | ea86b35833 | ||
|  | ade14edcd7 | ||
|  | 3a1888739a | ||
|  | 3b54ce4949 | ||
|  | 4a8aaf7389 | ||
|  | 45eec47b7f | ||
|  | 4b9af8aa86 | ||
|  | 631bbcb786 | ||
|  | 76a10d6cf9 | ||
|  | a1c9ebd661 | ||
|  | 9f06d78db6 | ||
|  | ac98aa9271 | ||
|  | 455f7ac59b | ||
|  | a42cb0e3ab | ||
|  | d05d2fb9d7 | ||
|  | 6c4c5b4697 | ||
|  | 5da87640e4 | ||
|  | fa044ffb44 | ||
|  | 5449652bd2 | ||
|  | c12ae9ea25 | ||
|  | 734a300b92 | ||
|  | 1109ae308d | ||
|  | 8f1d241e83 | ||
|  | acbca4d1dc | ||
|  | 1ea9be8aa2 | ||
|  | ace02893e5 | ||
|  | 1c3e043fac | ||
|  | 71c9e7a685 | ||
|  | fa945c7689 | ||
|  | c54ce96033 | ||
|  | 85c4e93763 | ||
|  | 25e5e78373 | ||
|  | 06181d0a1a | ||
|  | d5a8259fdb | ||
|  | 9db7141853 | ||
|  | ec2a1927a0 | ||
|  | 1c1b0f00ad | ||
|  | fb4d3e44d3 | ||
|  | 37fd062cf9 | ||
|  | 485c3c5c46 | ||
|  | 5007393f24 | ||
|  | e111ac730c | ||
|  | e7c78eabce | ||
|  | 5da7699548 | ||
|  | f42955a0ba | ||
|  | 4d67df4da6 | ||
|  | ab7459f4f3 | ||
|  | 469db7c0e2 | ||
|  | 952e813b30 | ||
|  | f04d05fee1 | ||
|  | 6d9aa43c07 | ||
|  | f527221079 | ||
|  | ab14312368 | ||
|  | 690d3e3fd2 | ||
|  | 63a04f36c9 | ||
|  | e58af6e3ea | ||
|  | 6ba28b5757 | ||
|  | ed607d2bae | ||
|  | 1f7fc594e5 | ||
|  | 45d0a4fac2 | ||
|  | e50bc189aa | ||
|  | 4623bcd877 | ||
|  | 4a368a1128 | ||
|  | bec8cb01e0 | ||
|  | f3c041a561 | ||
|  | c21726ec61 | ||
|  | df69208caa | ||
|  | 08d07cdd67 | ||
|  | a309e48183 | ||
|  | 70c539cc81 | ||
|  | 11f136ac89 | ||
|  | 567d5f74ba | ||
|  | 338781f57b | ||
|  | bd07f3cd38 | ||
|  | 0b735abd44 | ||
|  | a88cdaf1fc | ||
|  | 7cae5f1a37 | ||
|  | e453330535 | ||
|  | b1e5fcdeaf | ||
|  | 10e0848a5c | ||
|  | 64e86bad91 | ||
|  | 21cf5d2321 | ||
|  | a6106a801b | ||
|  | a0803796b2 | ||
|  | 10370ea1dc | ||
|  | 558dd2e4bf | 
							
								
								
									
										4
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							| @@ -1,7 +1,7 @@ | ||||
| [//]: # (Note: See http://habitica.wikia.com/wiki/Using_Your_Local_Install_to_Modify_Habitica%27s_Website_and_API for more info) | ||||
|  | ||||
| [//]: # (Put Issue # or URL here, if applicable. This will automatically close the issue if your PR is merged in) | ||||
| Fixes put_issue_url_here | ||||
| [//]: # (Put Issue # here, if applicable. This will automatically close the issue if your PR is merged in) | ||||
| Fixes put_#_and_issue_numer_here | ||||
|  | ||||
| ### Changes | ||||
| [//]: # (Describe the changes that were made in detail here. Include pictures if necessary) | ||||
|   | ||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -39,6 +39,7 @@ dist-client | ||||
| test/client/unit/coverage | ||||
| test/client/e2e/reports | ||||
| test/client-old/spec/mocks/translations.js | ||||
| yarn.lock | ||||
|  | ||||
| # Elastic Beanstalk Files | ||||
| .elasticbeanstalk/* | ||||
|   | ||||
| @@ -17,3 +17,4 @@ CHANGELOG.md | ||||
| newrelic_agent.log | ||||
| *.swp | ||||
| *.swx | ||||
| website/raw_sprites/** | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| language: node_js | ||||
| node_js: | ||||
|   - '8' | ||||
|   - '10' | ||||
| services: | ||||
|   - mongodb | ||||
| cache: | ||||
| @@ -20,8 +20,9 @@ env: | ||||
|     - DISABLE_REQUEST_LOGGING=true | ||||
|   matrix: | ||||
|     - TEST="lint" | ||||
|     - TEST="test:api-v3:unit" REQUIRES_SERVER=true COVERAGE=true | ||||
|     - TEST="test:api:unit" REQUIRES_SERVER=true COVERAGE=true | ||||
|     - TEST="test:api-v3:integration" REQUIRES_SERVER=true COVERAGE=true | ||||
|     - TEST="test:api-v4:integration" REQUIRES_SERVER=true COVERAGE=true | ||||
|     - TEST="test:sanity" | ||||
|     - TEST="test:content" COVERAGE=true | ||||
|     - TEST="test:common" COVERAGE=true | ||||
|   | ||||
							
								
								
									
										48
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										48
									
								
								Dockerfile
									
									
									
									
									
								
							| @@ -1,18 +1,30 @@ | ||||
| FROM node:8 | ||||
|  | ||||
| # Install global packages | ||||
| RUN npm install -g gulp-cli mocha | ||||
|  | ||||
| # Clone Habitica repo and install dependencies | ||||
| RUN mkdir -p /usr/src/habitrpg | ||||
| WORKDIR /usr/src/habitrpg | ||||
| RUN git clone https://github.com/HabitRPG/habitica.git /usr/src/habitrpg | ||||
| RUN cp config.json.example config.json | ||||
| RUN npm install | ||||
|  | ||||
| # Create Build dir | ||||
| RUN mkdir -p ./website/build | ||||
|  | ||||
| # Start Habitica | ||||
| EXPOSE 3000 | ||||
| CMD ["npm", "start"] | ||||
| FROM node:10 | ||||
|  | ||||
| ENV ADMIN_EMAIL admin@habitica.com | ||||
| ENV AMAZON_PAYMENTS_CLIENT_ID amzn1.application-oa2-client.68ed9e6904ef438fbc1bf86bf494056e | ||||
| ENV AMAZON_PAYMENTS_SELLER_ID AMQ3SB4SG5E91 | ||||
| ENV AMPLITUDE_KEY e8d4c24b3d6ef3ee73eeba715023dd43 | ||||
| ENV BASE_URL https://habitica.com | ||||
| ENV FACEBOOK_KEY 128307497299777 | ||||
| ENV GA_ID UA-33510635-1 | ||||
| ENV GOOGLE_CLIENT_ID 1035232791481-32vtplgnjnd1aufv3mcu1lthf31795fq.apps.googleusercontent.com | ||||
| ENV LOGGLY_CLIENT_TOKEN ab5663bf-241f-4d14-8783-7d80db77089a | ||||
| ENV NODE_ENV production | ||||
| ENV STRIPE_PUB_KEY pk_85fQ0yMECHNfHTSsZoxZXlPSwSNfA | ||||
|  | ||||
| # Install global packages | ||||
| RUN npm install -g gulp-cli mocha | ||||
|  | ||||
| # Clone Habitica repo and install dependencies | ||||
| RUN mkdir -p /usr/src/habitrpg | ||||
| WORKDIR /usr/src/habitrpg | ||||
| RUN git clone --branch release https://github.com/HabitRPG/habitica.git /usr/src/habitrpg | ||||
| RUN npm install | ||||
| RUN gulp build:prod --force | ||||
|  | ||||
| # Create Build dir | ||||
| RUN mkdir -p ./website/build | ||||
|  | ||||
| # Start Habitica | ||||
| EXPOSE 3000 | ||||
| CMD ["node", "./website/transpiled-babel/index.js"] | ||||
|   | ||||
							
								
								
									
										18
									
								
								Dockerfile-Dev
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								Dockerfile-Dev
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| FROM node:10 | ||||
|  | ||||
| # Install global packages | ||||
| RUN npm install -g gulp-cli mocha | ||||
|  | ||||
| # Clone Habitica repo and install dependencies | ||||
| RUN mkdir -p /usr/src/habitrpg | ||||
| WORKDIR /usr/src/habitrpg | ||||
| RUN git clone https://github.com/HabitRPG/habitica.git /usr/src/habitrpg | ||||
| RUN cp config.json.example config.json | ||||
| RUN npm install | ||||
|  | ||||
| # Create Build dir | ||||
| RUN mkdir -p ./website/build | ||||
|  | ||||
| # Start Habitica | ||||
| EXPOSE 3000 | ||||
| CMD ["npm", "start"] | ||||
| @@ -1,29 +0,0 @@ | ||||
| FROM node:8 | ||||
|  | ||||
| ENV ADMIN_EMAIL admin@habitica.com | ||||
| ENV AMAZON_PAYMENTS_CLIENT_ID amzn1.application-oa2-client.68ed9e6904ef438fbc1bf86bf494056e | ||||
| ENV AMAZON_PAYMENTS_SELLER_ID AMQ3SB4SG5E91 | ||||
| ENV AMPLITUDE_KEY e8d4c24b3d6ef3ee73eeba715023dd43 | ||||
| ENV BASE_URL https://habitica.com | ||||
| ENV FACEBOOK_KEY 128307497299777 | ||||
| ENV GA_ID UA-33510635-1 | ||||
| ENV GOOGLE_CLIENT_ID 1035232791481-32vtplgnjnd1aufv3mcu1lthf31795fq.apps.googleusercontent.com | ||||
| ENV NODE_ENV production | ||||
| ENV STRIPE_PUB_KEY pk_85fQ0yMECHNfHTSsZoxZXlPSwSNfA | ||||
|  | ||||
| # Install global packages | ||||
| RUN npm install -g gulp-cli mocha | ||||
|  | ||||
| # Clone Habitica repo and install dependencies | ||||
| RUN mkdir -p /usr/src/habitrpg | ||||
| WORKDIR /usr/src/habitrpg | ||||
| RUN git clone --branch v4.35.0 https://github.com/HabitRPG/habitica.git /usr/src/habitrpg | ||||
| RUN npm install | ||||
| RUN gulp build:prod --force | ||||
|  | ||||
| # Create Build dir | ||||
| RUN mkdir -p ./website/build | ||||
|  | ||||
| # Start Habitica | ||||
| EXPOSE 3000 | ||||
| CMD ["node", "./website/transpiled-babel/index.js"] | ||||
| @@ -10,4 +10,3 @@ We need more programmers! Your assistance will be greatly appreciated. | ||||
| For an introduction to the technologies used and how the software is organized, refer to [Guidance for Blacksmiths](http://habitica.wikia.com/wiki/Guidance_for_Blacksmiths). | ||||
|  | ||||
| To set up a local install of Habitica for development and testing on various platforms, see [Setting up Habitica Locally](http://habitica.wikia.com/wiki/Setting_up_Habitica_Locally). | ||||
|  | ||||
|   | ||||
| @@ -1,117 +1,82 @@ | ||||
| { | ||||
|     "PORT":3000, | ||||
|     "ENABLE_CONSOLE_LOGS_IN_PROD":"false", | ||||
|     "IP":"0.0.0.0", | ||||
|     "WEB_CONCURRENCY":1, | ||||
|     "BASE_URL":"http://localhost:3000", | ||||
|     "FACEBOOK_KEY":"123456789012345", | ||||
|     "FACEBOOK_SECRET":"aaaabbbbccccddddeeeeffff00001111", | ||||
|     "GOOGLE_CLIENT_ID":"123456789012345", | ||||
|     "GOOGLE_CLIENT_SECRET":"aaaabbbbccccddddeeeeffff00001111", | ||||
|     "PLAY_API": { | ||||
|       "CLIENT_ID": "aaaabbbbccccddddeeeeffff00001111", | ||||
|       "CLIENT_SECRET": "aaaabbbbccccddddeeeeffff00001111", | ||||
|       "ACCESS_TOKEN":"aaaabbbbccccddddeeeeffff00001111", | ||||
|       "REFRESH_TOKEN":"aaaabbbbccccddddeeeeffff00001111" | ||||
|     }, | ||||
|     "NODE_DB_URI":"mongodb://localhost/habitrpg", | ||||
|     "TEST_DB_URI":"mongodb://localhost/habitrpg_test", | ||||
|     "NODE_ENV":"development", | ||||
|     "ENABLE_CONSOLE_LOGS_IN_TEST": false, | ||||
|     "CRON_SAFE_MODE":"false", | ||||
|     "CRON_SEMI_SAFE_MODE":"false", | ||||
|     "MAINTENANCE_MODE": "false", | ||||
|     "SESSION_SECRET":"YOUR SECRET HERE", | ||||
|     "SESSION_SECRET_KEY": "1234567891234567891234567891234567891234567891234567891234567891", | ||||
|     "SESSION_SECRET_IV": "12345678912345678912345678912345", | ||||
|     "ADMIN_EMAIL": "you@example.com", | ||||
|     "SMTP_USER":"user@example.com", | ||||
|     "SMTP_PASS":"password", | ||||
|     "SMTP_SERVICE":"Gmail", | ||||
|     "SMTP_HOST":"example.com", | ||||
|     "SMTP_PORT": 587, | ||||
|     "SMTP_TLS": true, | ||||
|     "STRIPE_API_KEY":"aaaabbbbccccddddeeeeffff00001111", | ||||
|     "STRIPE_PUB_KEY":"22223333444455556666777788889999", | ||||
|     "NEW_RELIC_LICENSE_KEY":"NEW_RELIC_LICENSE_KEY", | ||||
|     "NEW_RELIC_NO_CONFIG_FILE":"true", | ||||
|     "NEW_RELIC_APPLICATION_ID":"NEW_RELIC_APPLICATION_ID", | ||||
|     "NEW_RELIC_API_KEY":"NEW_RELIC_API_KEY", | ||||
|     "GA_ID": "GA_ID", | ||||
|     "AMPLITUDE_KEY": "AMPLITUDE_KEY", | ||||
|     "AMAZON_PAYMENTS": { | ||||
|         "SELLER_ID": "SELLER_ID", | ||||
|         "CLIENT_ID": "CLIENT_ID", | ||||
|         "MWS_KEY": "", | ||||
|         "MWS_SECRET": "" | ||||
|     }, | ||||
|     "FLAG_REPORT_EMAIL": "email@mod.com,email2@mod.com", | ||||
|     "EMAIL_SERVER": { | ||||
|         "url": "http://example.com", | ||||
|         "authUser": "user", | ||||
|         "authPassword": "password" | ||||
|     }, | ||||
|     "S3":{ | ||||
|         "bucket":"bucket", | ||||
|         "accessKeyId":"accessKeyId", | ||||
|         "secretAccessKey":"secretAccessKey" | ||||
|     }, | ||||
|     "SLACK_URL": "https://hooks.slack.com/services/some-url", | ||||
|     "TRANSIFEX_SLACK_CHANNEL": "transifex", | ||||
|     "PAYPAL":{ | ||||
|         "billing_plans": { | ||||
|             "basic_earned":"basic_earned", | ||||
|             "basic_3mo":"basic_3mo", | ||||
|             "basic_6mo":"basic_6mo", | ||||
|             "google_6mo":"google_6mo", | ||||
|             "basic_12mo":"basic_12mo" | ||||
|         }, | ||||
|         "mode":"sandbox", | ||||
|         "client_id":"client_id", | ||||
|         "client_secret":"client_secret", | ||||
|         "experience_profile_id": "" | ||||
|     }, | ||||
|     "IAP_GOOGLE_KEYDIR": "/path/to/google/public/key/dir/", | ||||
|     "LOGGLY_TOKEN": "token", | ||||
|     "LOGGLY_CLIENT_TOKEN": "token", | ||||
|     "LOGGLY_ACCOUNT": "account", | ||||
|     "PUSH_CONFIGS": { | ||||
|         "GCM_SERVER_API_KEY": "", | ||||
|         "APN_ENABLED": "false", | ||||
|         "FCM_SERVER_API_KEY": "" | ||||
|     }, | ||||
|     "SITE_HTTP_AUTH": { | ||||
|         "ENABLED": "false", | ||||
|         "USERNAME": "admin", | ||||
|         "PASSWORD": "password" | ||||
|     }, | ||||
|     "PUSHER": { | ||||
|         "ENABLED": "false", | ||||
|         "APP_ID": "appId", | ||||
|         "KEY": "key", | ||||
|         "SECRET": "secret" | ||||
|     }, | ||||
|     "SLACK": { | ||||
|         "FLAGGING_URL": "https://hooks.slack.com/services/id/id/id", | ||||
|         "FLAGGING_FOOTER_LINK": "https://habitrpg.github.io/flag-o-rama/", | ||||
|         "SUBSCRIPTIONS_URL": "https://hooks.slack.com/services/id/id/id" | ||||
|     }, | ||||
|     "ITUNES_SHARED_SECRET": "aaaabbbbccccddddeeeeffff00001111", | ||||
|     "EMAILS" : { | ||||
|         "COMMUNITY_MANAGER_EMAIL" : "leslie@habitica.com", | ||||
|         "TECH_ASSISTANCE_EMAIL" : "admin@habitica.com", | ||||
|         "PRESS_ENQUIRY_EMAIL" : "leslie@habitica.com" | ||||
|     }, | ||||
|     "LOGGLY" : { | ||||
|         "TOKEN" : "example-token", | ||||
|         "SUBDOMAIN" : "exmaple-subdomain" | ||||
|     }, | ||||
|     "KAFKA": { | ||||
|       "GROUP_ID": "", | ||||
|       "CLOUDKARAFKA_BROKERS": "", | ||||
|       "CLOUDKARAFKA_USERNAME": "", | ||||
|       "CLOUDKARAFKA_PASSWORD": "", | ||||
|       "CLOUDKARAFKA_TOPIC_PREFIX": "" | ||||
|     }, | ||||
|     "STACK_IMPACT_KEY": "aaaabbbbccccddddeeeeffffgggg111100002222" | ||||
|   "ADMIN_EMAIL": "you@example.com", | ||||
|   "AMAZON_PAYMENTS_CLIENT_ID": "CLIENT_ID", | ||||
|   "AMAZON_PAYMENTS_MODE": "sandbox", | ||||
|   "AMAZON_PAYMENTS_MWS_KEY": "MWS_KEY", | ||||
|   "AMAZON_PAYMENTS_MWS_SECRET": "MWS_SECRET", | ||||
|   "AMAZON_PAYMENTS_SELLER_ID": "SELLER_ID", | ||||
|   "AMPLITUDE_KEY": "AMPLITUDE_KEY", | ||||
|   "AMPLITUDE_SECRET": "AMPLITUDE_SECRET", | ||||
|   "BASE_URL": "http://localhost:3000", | ||||
|   "CRON_SAFE_MODE": "false", | ||||
|   "CRON_SEMI_SAFE_MODE": "false", | ||||
|   "DISABLE_REQUEST_LOGGING": "true", | ||||
|   "EMAILS_COMMUNITY_MANAGER_EMAIL": "admin@habitica.com", | ||||
|   "EMAILS_PRESS_ENQUIRY_EMAIL": "admin@habitica.com", | ||||
|   "EMAILS_TECH_ASSISTANCE_EMAIL": "admin@habitica.com", | ||||
|   "EMAIL_SERVER_AUTH_PASSWORD": "password", | ||||
|   "EMAIL_SERVER_AUTH_USER": "user", | ||||
|   "EMAIL_SERVER_URL": "http://example.com", | ||||
|   "ENABLE_CONSOLE_LOGS_IN_PROD": "false", | ||||
|   "ENABLE_CONSOLE_LOGS_IN_TEST": "false", | ||||
|   "FACEBOOK_KEY": "123456789012345", | ||||
|   "FACEBOOK_SECRET": "aaaabbbbccccddddeeeeffff00001111", | ||||
|   "FLAG_REPORT_EMAIL": "email@example.com, email2@example.com", | ||||
|   "GA_ID": "GA_ID", | ||||
|   "GOOGLE_CLIENT_ID": "123456789012345", | ||||
|   "GOOGLE_CLIENT_SECRET": "aaaabbbbccccddddeeeeffff00001111", | ||||
|   "IAP_GOOGLE_KEYDIR": "/path/to/google/public/key/dir/", | ||||
|   "IGNORE_REDIRECT": "true", | ||||
|   "ITUNES_SHARED_SECRET": "aaaabbbbccccddddeeeeffff00001111", | ||||
|   "LOGGLY_CLIENT_TOKEN": "token", | ||||
|   "LOGGLY_SUBDOMAIN": "example-subdomain", | ||||
|   "LOGGLY_TOKEN": "example-token", | ||||
|   "MAINTENANCE_MODE": "false", | ||||
|   "NODE_DB_URI": "mongodb://localhost/habitrpg", | ||||
|   "NODE_ENV": "development", | ||||
|   "PATH": "bin:node_modules/.bin:/usr/local/bin:/usr/bin:/bin", | ||||
|   "PAYPAL_BILLING_PLANS_basic_12mo": "basic_12mo", | ||||
|   "PAYPAL_BILLING_PLANS_basic_3mo": "basic_3mo", | ||||
|   "PAYPAL_BILLING_PLANS_basic_6mo": "basic_6mo", | ||||
|   "PAYPAL_BILLING_PLANS_basic_earned": "basic_earned", | ||||
|   "PAYPAL_BILLING_PLANS_google_6mo": "google_6mo", | ||||
|   "PAYPAL_CLIENT_ID": "client_id", | ||||
|   "PAYPAL_CLIENT_SECRET": "client_secret", | ||||
|   "PAYPAL_EXPERIENCE_PROFILE_ID": "xp_profile_id", | ||||
|   "PAYPAL_MODE": "sandbox", | ||||
|   "PLAY_API_ACCESS_TOKEN": "aaaabbbbccccddddeeeeffff00001111", | ||||
|   "PLAY_API_CLIENT_ID": "aaaabbbbccccddddeeeeffff00001111", | ||||
|   "PLAY_API_CLIENT_SECRET": "aaaabbbbccccddddeeeeffff00001111", | ||||
|   "PLAY_API_REFRESH_TOKEN": "aaaabbbbccccddddeeeeffff00001111", | ||||
|   "PORT": 3000, | ||||
|   "PUSH_CONFIGS_APN_ENABLED": "false", | ||||
|   "PUSH_CONFIGS_APN_KEY": "xxxxxxxxxx", | ||||
|   "PUSH_CONFIGS_APN_KEY_ID": "xxxxxxxxxx", | ||||
|   "PUSH_CONFIGS_APN_TEAM_ID": "aaabbbcccd", | ||||
|   "PUSH_CONFIGS_FCM_SERVER_API_KEY": "aaabbbcccd", | ||||
|   "S3_ACCESS_KEY_ID": "accessKeyId", | ||||
|   "S3_BUCKET": "bucket", | ||||
|   "S3_SECRET_ACCESS_KEY": "secretAccessKey", | ||||
|   "SESSION_SECRET": "YOUR SECRET HERE", | ||||
|   "SESSION_SECRET_IV": "12345678912345678912345678912345", | ||||
|   "SESSION_SECRET_KEY": "1234567891234567891234567891234567891234567891234567891234567891", | ||||
|   "SITE_HTTP_AUTH_ENABLED": "false", | ||||
|   "SITE_HTTP_AUTH_PASSWORD": "password", | ||||
|   "SITE_HTTP_AUTH_USERNAME": "admin", | ||||
|   "SLACK_FLAGGING_FOOTER_LINK": "https://habitrpg.github.io/flag-o-rama/", | ||||
|   "SLACK_FLAGGING_URL": "https://hooks.slack.com/services/id/id/id", | ||||
|   "SLACK_SUBSCRIPTIONS_URL": "https://hooks.slack.com/services/id/id/id", | ||||
|   "SLACK_URL": "https://hooks.slack.com/services/some-url", | ||||
|   "SMTP_HOST": "example.com", | ||||
|   "SMTP_PASS": "password", | ||||
|   "SMTP_PORT": 587, | ||||
|   "SMTP_SERVICE": "Gmail", | ||||
|   "SMTP_TLS": "true", | ||||
|   "SMTP_USER": "user@example.com", | ||||
|   "STRIPE_API_KEY": "aaaabbbbccccddddeeeeffff00001111", | ||||
|   "STRIPE_PUB_KEY": "22223333444455556666777788889999", | ||||
|   "TEST_DB_URI": "mongodb://localhost/habitrpg_test", | ||||
|   "TRANSIFEX_SLACK_CHANNEL": "transifex", | ||||
|   "WEB_CONCURRENCY": 1, | ||||
|   "SKIP_SSL_CHECK_KEY": "key" | ||||
| } | ||||
|   | ||||
							
								
								
									
										100
									
								
								database_reports/20180607_subscriber_histories.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								database_reports/20180607_subscriber_histories.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,100 @@ | ||||
| import max from 'lodash/max'; | ||||
| import mean from 'lodash/mean'; | ||||
| import monk from 'monk'; | ||||
| import round from 'lodash/round'; | ||||
| import sum from 'lodash/sum'; | ||||
|  | ||||
| /* | ||||
|  * Output data on subscribers' task histories, formatted for CSV. | ||||
|  * User ID,Count of Dailies,Count of Habits,Total History Size,Max History Size,Mean History Size,Median History Size | ||||
|  */ | ||||
| const connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE | ||||
|  | ||||
| let dbUsers = monk(connectionString).get('users', { castIds: false }); | ||||
| let dbTasks = monk(connectionString).get('tasks', { castIds: false }); | ||||
|  | ||||
| function usersReport () { | ||||
|   let allHistoryLengths = []; | ||||
|  | ||||
|   console.info('User ID,Count of Dailies,Count of Habits,Total History Size,Max History Size,Mean History Size,Median History Size'); | ||||
|  | ||||
|   dbUsers.find( | ||||
|     { | ||||
|       $and: | ||||
|         [ | ||||
|           {'purchased.plan.planId': {$ne:null}}, | ||||
|           {'purchased.plan.planId': {$ne:''}}, | ||||
|         ], | ||||
|       $or: | ||||
|         [ | ||||
|           {'purchased.plan.dateTerminated': null}, | ||||
|           {'purchased.plan.dateTerminated': ''}, | ||||
|           {'purchased.plan.dateTerminated': {$gt:new Date()}}, | ||||
|         ], | ||||
|     }, | ||||
|     { | ||||
|       fields: {_id: 1}, | ||||
|     } | ||||
|   ).each((user, {close, pause, resume}) => { | ||||
|     let historyLengths = []; | ||||
|     let habitCount = 0; | ||||
|     let dailyCount = 0; | ||||
|  | ||||
|     pause(); | ||||
|     return dbTasks.find( | ||||
|       { | ||||
|         userId: user._id, | ||||
|         $or: | ||||
|           [ | ||||
|             {type: 'habit'}, | ||||
|             {type: 'daily'}, | ||||
|           ], | ||||
|       }, | ||||
|       { | ||||
|         fields: { | ||||
|           type: 1, | ||||
|           history: 1, | ||||
|         }, | ||||
|       } | ||||
|     ).each((task) => { | ||||
|       if (task.type === 'habit') { | ||||
|         habitCount++; | ||||
|       } | ||||
|       if (task.type === 'daily') { | ||||
|         dailyCount++; | ||||
|       } | ||||
|       if (task.history.length > 0) { | ||||
|         allHistoryLengths.push(task.history.length); | ||||
|         historyLengths.push(task.history.length); | ||||
|       } | ||||
|     }).then(() => { | ||||
|       const totalHistory = sum(historyLengths); | ||||
|       const maxHistory = historyLengths.length > 0 ? max(historyLengths) : 0; | ||||
|       const meanHistory = historyLengths.length > 0 ? round(mean(historyLengths)) : 0; | ||||
|       const medianHistory = historyLengths.length > 0 ? median(historyLengths) : 0; | ||||
|       console.info(`${user._id},${dailyCount},${habitCount},${totalHistory},${maxHistory},${meanHistory},${medianHistory}`); | ||||
|       resume(); | ||||
|     }); | ||||
|   }).then(() => { | ||||
|     console.info(`Total Subscriber History Entries: ${sum(allHistoryLengths)}`); | ||||
|     console.info(`Largest History Size: ${max(allHistoryLengths)}`); | ||||
|     console.info(`Mean History Size: ${round(mean(allHistoryLengths))}`); | ||||
|     console.info(`Median History Size: ${median(allHistoryLengths)}`); | ||||
|     return process.exit(0); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| function median(values) { // https://gist.github.com/caseyjustus/1166258 | ||||
|   values.sort( function(a,b) {return a - b;} ); | ||||
|  | ||||
|   var half = Math.floor(values.length/2); | ||||
|  | ||||
|   if (values.length % 2) { | ||||
|     return values[half]; | ||||
|   } | ||||
|   else { | ||||
|     return (values[half-1] + values[half]) / 2.0; | ||||
|   } | ||||
| } | ||||
|  | ||||
| module.exports = usersReport; | ||||
							
								
								
									
										48
									
								
								database_reports/20181001_backtoschool_challenge.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								database_reports/20181001_backtoschool_challenge.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| import monk from 'monk'; | ||||
| import nconf from 'nconf'; | ||||
|  | ||||
| /* | ||||
|  * Output data on users who completed all the To-Do tasks in the 2018 Back-to-School Challenge. | ||||
|  * User ID,Profile Name | ||||
|  */ | ||||
| const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING'); | ||||
| const CHALLENGE_ID = '0acb1d56-1660-41a4-af80-9259f080b62b'; | ||||
|  | ||||
| let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false }); | ||||
| let dbTasks = monk(CONNECTION_STRING).get('tasks', { castIds: false }); | ||||
|  | ||||
| function usersReport() { | ||||
|   console.info('User ID,Profile Name'); | ||||
|   let userCount = 0; | ||||
|  | ||||
|   dbUsers.find( | ||||
|     {challenges: CHALLENGE_ID}, | ||||
|     {fields: | ||||
|       {_id: 1, 'profile.name': 1} | ||||
|     }, | ||||
|   ).each((user, {close, pause, resume}) => { | ||||
|     pause(); | ||||
|     userCount++; | ||||
|     let completedTodos = 0; | ||||
|     return dbTasks.find( | ||||
|       { | ||||
|         userId: user._id, | ||||
|         'challenge.id': CHALLENGE_ID, | ||||
|         type: 'todo', | ||||
|       }, | ||||
|       {fields: {completed: 1}} | ||||
|     ).each((task) => { | ||||
|       if (task.completed) completedTodos++; | ||||
|     }).then(() => { | ||||
|       if (completedTodos >= 7) { | ||||
|         console.info(`${user._id},${user.profile.name}`); | ||||
|       } | ||||
|       resume(); | ||||
|     }); | ||||
|   }).then(() => { | ||||
|     console.info(`${userCount} users reviewed`); | ||||
|     return process.exit(0); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| module.exports = usersReport; | ||||
| @@ -2,9 +2,13 @@ version: "3" | ||||
| services: | ||||
|  | ||||
|   client: | ||||
|     environment: | ||||
|       - NODE_ENV=development | ||||
|     volumes: | ||||
|       - '.:/usr/src/habitrpg' | ||||
|  | ||||
|   server: | ||||
|     environment: | ||||
|       - NODE_ENV=development | ||||
|     volumes: | ||||
|       - '.:/usr/src/habitrpg' | ||||
|   | ||||
| @@ -25,7 +25,7 @@ services: | ||||
|       - mongo | ||||
|  | ||||
|   mongo: | ||||
|     image: mongo | ||||
|     image: mongo:3.4 | ||||
|     ports: | ||||
|       - "27017:27017" | ||||
|     networks: | ||||
|   | ||||
| @@ -165,9 +165,9 @@ gulp.task('test:content:safe', gulp.series('test:prepare:build', (cb) => { | ||||
|   pipe(runner); | ||||
| })); | ||||
|  | ||||
| gulp.task('test:api-v3:unit', (done) => { | ||||
| gulp.task('test:api:unit', (done) => { | ||||
|   let runner = exec( | ||||
|     testBin('node_modules/.bin/istanbul cover --dir coverage/api-v3-unit --report lcovonly node_modules/mocha/bin/_mocha -- test/api/v3/unit --recursive --require ./test/helpers/start-server'), | ||||
|     testBin('istanbul cover --dir coverage/api-unit node_modules/mocha/bin/_mocha -- test/api/unit --recursive --require ./test/helpers/start-server'), | ||||
|     (err) => { | ||||
|       if (err) { | ||||
|         process.exit(1); | ||||
| @@ -179,13 +179,13 @@ gulp.task('test:api-v3:unit', (done) => { | ||||
|   pipe(runner); | ||||
| }); | ||||
|  | ||||
| gulp.task('test:api-v3:unit:watch', () => { | ||||
|   return gulp.watch(['website/server/libs/*', 'test/api/v3/unit/**/*', 'website/server/controllers/**/*'], gulp.series('test:api-v3:unit', done => done())); | ||||
| gulp.task('test:api:unit:watch', () => { | ||||
|   return gulp.watch(['website/server/libs/*', 'test/api/unit/**/*', 'website/server/controllers/**/*'], gulp.series('test:api:unit', done => done())); | ||||
| }); | ||||
|  | ||||
| gulp.task('test:api-v3:integration', (done) => { | ||||
|   let runner = exec( | ||||
|     testBin('node_modules/.bin/istanbul cover --dir coverage/api-v3-integration --report lcovonly node_modules/mocha/bin/_mocha -- test/api/v3/integration --recursive --require ./test/helpers/start-server'), | ||||
|     testBin('istanbul cover --dir coverage/api-v3-integration --report lcovonly node_modules/mocha/bin/_mocha -- test/api/v3/integration --recursive --require ./test/helpers/start-server'), | ||||
|     {maxBuffer: 500 * 1024}, | ||||
|     (err) => { | ||||
|       if (err) { | ||||
| @@ -215,17 +215,43 @@ gulp.task('test:api-v3:integration:separate-server', (done) => { | ||||
|   pipe(runner); | ||||
| }); | ||||
|  | ||||
| gulp.task('test:api-v4:integration', (done) => { | ||||
|   let runner = exec( | ||||
|     testBin('istanbul cover --dir coverage/api-v4-integration --report lcovonly node_modules/mocha/bin/_mocha -- test/api/v4 --recursive --require ./test/helpers/start-server'), | ||||
|     {maxBuffer: 500 * 1024}, | ||||
|     (err) => { | ||||
|       if (err) { | ||||
|         process.exit(1); | ||||
|       } | ||||
|       done(); | ||||
|     } | ||||
|   ); | ||||
|  | ||||
|   pipe(runner); | ||||
| }); | ||||
|  | ||||
| gulp.task('test:api-v4:integration:separate-server', (done) => { | ||||
|   let runner = exec( | ||||
|     testBin('mocha test/api/v4 --recursive --require ./test/helpers/start-server', 'LOAD_SERVER=0'), | ||||
|     {maxBuffer: 500 * 1024}, | ||||
|     (err) => done(err) | ||||
|   ); | ||||
|  | ||||
|   pipe(runner); | ||||
| }); | ||||
|  | ||||
| gulp.task('test', gulp.series( | ||||
|   'test:sanity', | ||||
|   'test:content', | ||||
|   'test:common', | ||||
|   'test:api-v3:unit', | ||||
|   'test:api:unit', | ||||
|   'test:api-v3:integration', | ||||
|   'test:api-v4:integration', | ||||
|   done => done() | ||||
| )); | ||||
|  | ||||
| gulp.task('test:api-v3', gulp.series( | ||||
|   'test:api-v3:unit', | ||||
|   'test:api:unit', | ||||
|   'test:api-v3:integration', | ||||
|   done => done() | ||||
| )); | ||||
|   | ||||
							
								
								
									
										99
									
								
								migrations/archive/2018/20180724_summer_splash_orcas.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								migrations/archive/2018/20180724_summer_splash_orcas.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,99 @@ | ||||
| let migrationName = '20180724_summer-splash-orcas.js'; // Update per month | ||||
| let authorName = 'Sabe'; // in case script author needs to know when their ... | ||||
| let authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done | ||||
|  | ||||
| /* | ||||
|  * Award ladder items to participants in this year's Summer Splash festivities | ||||
|  */ | ||||
|  | ||||
| import monk from 'monk'; | ||||
| import nconf from 'nconf'; | ||||
| const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING'); | ||||
| let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false }); | ||||
|  | ||||
| function processUsers (lastId) { | ||||
|   // specify a query to limit the affected users (empty for all users): | ||||
|   let query = { | ||||
|     migration: {$ne: migrationName}, | ||||
|     'auth.timestamps.loggedin': {$gt: new Date('2018-07-01')}, // rerun without date restriction after initial run | ||||
|   }; | ||||
|  | ||||
|   if (lastId) { | ||||
|     query._id = { | ||||
|       $gt: lastId, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   dbUsers.find(query, { | ||||
|     sort: {_id: 1}, | ||||
|     limit: 250, | ||||
|     fields: [ | ||||
|       'items.mounts', | ||||
|     ], // specify fields we are interested in to limit retrieved data (empty if we're not reading data): | ||||
|   }) | ||||
|     .then(updateUsers) | ||||
|     .catch((err) => { | ||||
|       console.log(err); | ||||
|       return exiting(1, `ERROR! ${  err}`); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| let progressCount = 1000; | ||||
| let count = 0; | ||||
|  | ||||
| function updateUsers (users) { | ||||
|   if (!users || users.length === 0) { | ||||
|     console.warn('All appropriate users found and modified.'); | ||||
|     displayData(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   let userPromises = users.map(updateUser); | ||||
|   let lastUser = users[users.length - 1]; | ||||
|  | ||||
|   return Promise.all(userPromises) | ||||
|     .then(() => { | ||||
|       processUsers(lastUser._id); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function updateUser (user) { | ||||
|   count++; | ||||
|  | ||||
|   let set = {}; | ||||
|  | ||||
|   if (user && user.items && user.items.pets && typeof user.items.pets['Orca-Base'] !== 'undefined') { | ||||
|     set = {migration: migrationName}; | ||||
|   } else if (user && user.items && user.items.mounts && typeof user.items.mounts['Orca-Base'] !== 'undefined') { | ||||
|     set = {migration: migrationName, 'items.pets.Orca-Base': 5}; | ||||
|   } else { | ||||
|     set = {migration: migrationName, 'items.mounts.Orca-Base': true}; | ||||
|   } | ||||
|  | ||||
|   dbUsers.update({_id: user._id}, {$set: set}); | ||||
|  | ||||
|   if (count % progressCount === 0) console.warn(`${count  } ${  user._id}`); | ||||
|   if (user._id === authorUuid) console.warn(`${authorName  } processed`); | ||||
| } | ||||
|  | ||||
| function displayData () { | ||||
|   console.warn(`\n${  count  } users processed\n`); | ||||
|   return exiting(0); | ||||
| } | ||||
|  | ||||
| function exiting (code, msg) { | ||||
|   code = code || 0; // 0 = success | ||||
|   if (code && !msg) { | ||||
|     msg = 'ERROR!'; | ||||
|   } | ||||
|   if (msg) { | ||||
|     if (code) { | ||||
|       console.error(msg); | ||||
|     } else      { | ||||
|       console.log(msg); | ||||
|     } | ||||
|   } | ||||
|   process.exit(code); | ||||
| } | ||||
|  | ||||
| module.exports = processUsers; | ||||
							
								
								
									
										123
									
								
								migrations/archive/2018/20180731_naming_day.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								migrations/archive/2018/20180731_naming_day.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,123 @@ | ||||
| let migrationName = '20180731_naming-day.js'; // Update when running in future years | ||||
| let authorName = 'Sabe'; // in case script author needs to know when their ... | ||||
| let authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done | ||||
|  | ||||
| /* | ||||
|  * Award Naming Day ladder items to participants in this month's Naming Day festivities | ||||
|  */ | ||||
|  | ||||
| import monk from 'monk'; | ||||
| import nconf from 'nconf'; | ||||
| const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING'); // FOR TEST DATABASE | ||||
| let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false }); | ||||
|  | ||||
| function processUsers (lastId) { | ||||
|   // specify a query to limit the affected users (empty for all users): | ||||
|   let query = { | ||||
|     migration: {$ne: migrationName}, | ||||
|   }; | ||||
|  | ||||
|   if (lastId) { | ||||
|     query._id = { | ||||
|       $gt: lastId, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   dbUsers.find(query, { | ||||
|     sort: {_id: 1}, | ||||
|     limit: 250, | ||||
|     fields: [ | ||||
|       'items.gear.owned', | ||||
|       'items.mounts', | ||||
|       'items.pets', | ||||
|     ], // specify fields we are interested in to limit retrieved data (empty if we're not reading data): | ||||
|   }) | ||||
|     .then(updateUsers) | ||||
|     .catch((err) => { | ||||
|       console.log(err); | ||||
|       return exiting(1, `ERROR! ${  err}`); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| let progressCount = 1000; | ||||
| let count = 0; | ||||
|  | ||||
| function updateUsers (users) { | ||||
|   if (!users || users.length === 0) { | ||||
|     console.warn('All appropriate users found and modified.'); | ||||
|     displayData(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   let userPromises = users.map(updateUser); | ||||
|   let lastUser = users[users.length - 1]; | ||||
|  | ||||
|   return Promise.all(userPromises) | ||||
|     .then(() => { | ||||
|       processUsers(lastUser._id); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function updateUser (user) { | ||||
|   count++; | ||||
|  | ||||
|   let set = {}; | ||||
|   let push; | ||||
|  | ||||
|   const inc = { | ||||
|     'items.food.Cake_Base': 1, | ||||
|     'items.food.Cake_CottonCandyBlue': 1, | ||||
|     'items.food.Cake_CottonCandyPink': 1, | ||||
|     'items.food.Cake_Desert': 1, | ||||
|     'items.food.Cake_Golden': 1, | ||||
|     'items.food.Cake_Red': 1, | ||||
|     'items.food.Cake_Shade': 1, | ||||
|     'items.food.Cake_Skeleton': 1, | ||||
|     'items.food.Cake_White': 1, | ||||
|     'items.food.Cake_Zombie': 1, | ||||
|     'achievements.habiticaDays': 1, | ||||
|   }; | ||||
|  | ||||
|   if (user && user.items && user.items.gear && user.items.gear.owned && typeof user.items.gear.owned.head_special_namingDay2017 !== 'undefined') { | ||||
|     set = {migration: migrationName, 'items.gear.owned.body_special_namingDay2018': false}; | ||||
|     push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.body_special_namingDay2018', _id: monk.id()}}; | ||||
|   } else if (user && user.items && user.items.pets && typeof user.items.pets['Gryphon-RoyalPurple'] !== 'undefined') { | ||||
|     set = {migration: migrationName, 'items.gear.owned.head_special_namingDay2017': false}; | ||||
|     push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.head_special_namingDay2017', _id: monk.id()}}; | ||||
|   } else if (user && user.items && user.items.mounts && typeof user.items.mounts['Gryphon-RoyalPurple'] !== 'undefined') { | ||||
|     set = {migration: migrationName, 'items.pets.Gryphon-RoyalPurple': 5}; | ||||
|   } else { | ||||
|     set = {migration: migrationName, 'items.mounts.Gryphon-RoyalPurple': true}; | ||||
|   } | ||||
|  | ||||
|   if (push) { | ||||
|     dbUsers.update({_id: user._id}, {$set: set, $push: push, $inc: inc}); | ||||
|   } else { | ||||
|     dbUsers.update({_id: user._id}, {$set: set, $inc: inc}); | ||||
|   } | ||||
|  | ||||
|   if (count % progressCount === 0) console.warn(`${count  } ${  user._id}`); | ||||
|   if (user._id === authorUuid) console.warn(`${authorName  } processed`); | ||||
| } | ||||
|  | ||||
| function displayData () { | ||||
|   console.warn(`\n${  count  } users processed\n`); | ||||
|   return exiting(0); | ||||
| } | ||||
|  | ||||
| function exiting (code, msg) { | ||||
|   code = code || 0; // 0 = success | ||||
|   if (code && !msg) { | ||||
|     msg = 'ERROR!'; | ||||
|   } | ||||
|   if (msg) { | ||||
|     if (code) { | ||||
|       console.error(msg); | ||||
|     } else      { | ||||
|       console.log(msg); | ||||
|     } | ||||
|   } | ||||
|   process.exit(code); | ||||
| } | ||||
|  | ||||
| module.exports = processUsers; | ||||
							
								
								
									
										147
									
								
								migrations/archive/2018/20180811_inboxOutsideUser.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								migrations/archive/2018/20180811_inboxOutsideUser.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,147 @@ | ||||
| const migrationName = '20180811_inboxOutsideUser.js'; | ||||
| const authorName = 'paglias'; // in case script author needs to know when their ... | ||||
| const authorUuid = 'ed4c688c-6652-4a92-9d03-a5a79844174a'; // ... own data is done | ||||
|  | ||||
| /* | ||||
|  * Move inbox messages from the user model to their own collection | ||||
|  */ | ||||
|  | ||||
| const monk = require('monk'); | ||||
| const nconf = require('nconf'); | ||||
| const uuid = require('uuid').v4; | ||||
|  | ||||
| const Inbox = require('../website/server/models/message').inboxModel; | ||||
| const connectionString = nconf.get('MIGRATION_CONNECT_STRING'); // FOR TEST DATABASE | ||||
| const dbInboxes = monk(connectionString).get('inboxes', { castIds: false }); | ||||
| const dbUsers = monk(connectionString).get('users', { castIds: false }); | ||||
|  | ||||
| function processUsers (lastId) { | ||||
|   let query = { | ||||
|     migration: {$ne: migrationName}, | ||||
|   }; | ||||
|  | ||||
|   if (lastId) { | ||||
|     query._id = { | ||||
|       $gt: lastId, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   dbUsers.find(query, { | ||||
|     sort: {_id: 1}, | ||||
|     limit: 1000, | ||||
|     fields: ['_id', 'inbox'], | ||||
|   }) | ||||
|     .then(updateUsers) | ||||
|     .catch((err) => { | ||||
|       console.log(err); | ||||
|       return exiting(1, `ERROR! ${  err}`); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| let progressCount = 1000; | ||||
| let count = 0; | ||||
| let msgCount = 0; | ||||
|  | ||||
| function updateUsers (users) { | ||||
|   if (!users || users.length === 0) { | ||||
|     console.warn('All appropriate users and their tasks found and modified.'); | ||||
|     displayData(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   let usersPromises = users.map(updateUser); | ||||
|   let lastUser = users[users.length - 1]; | ||||
|  | ||||
|   return Promise.all(usersPromises) | ||||
|     .then(() => { | ||||
|       return processUsers(lastUser._id); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function updateUser (user) { | ||||
|   count++; | ||||
|  | ||||
|   if (count % progressCount === 0) console.warn(`${count  } ${  user._id}`); | ||||
|   if (msgCount % progressCount === 0) console.warn(`${msgCount  } messages processed`); | ||||
|   if (user._id === authorUuid) console.warn(`${authorName  } being processed`); | ||||
|  | ||||
|   const oldInboxMessages = user.inbox.messages || {}; | ||||
|   const oldInboxMessagesIds = Object.keys(oldInboxMessages); | ||||
|  | ||||
|   msgCount += oldInboxMessagesIds.length; | ||||
|  | ||||
|   const newInboxMessages = oldInboxMessagesIds.map(msgId => { | ||||
|     const msg = oldInboxMessages[msgId]; | ||||
|     if (!msg || (!msg.id && !msg._id)) { // eslint-disable-line no-extra-parens | ||||
|       console.log('missing message or message _id and id', msg); | ||||
|       throw new Error('error!'); | ||||
|     } | ||||
|  | ||||
|     if (msg.id && !msg._id) msg._id = msg.id; | ||||
|     if (msg._id && !msg.id) msg.id = msg._id; | ||||
|  | ||||
|     const newMsg = new Inbox(msg); | ||||
|     newMsg.ownerId = user._id; | ||||
|     return newMsg.toJSON(); | ||||
|   }); | ||||
|  | ||||
|   const promises = newInboxMessages.map(newMsg => { | ||||
|     return (async function fn () { | ||||
|       const existing = await dbInboxes.find({_id: newMsg._id}); | ||||
|  | ||||
|       if (existing.length > 0) { | ||||
|         if ( | ||||
|           existing[0].ownerId === newMsg.ownerId && | ||||
|           existing[0].text === newMsg.text && | ||||
|           existing[0].uuid === newMsg.uuid && | ||||
|           existing[0].sent === newMsg.sent | ||||
|         ) { | ||||
|           return null; | ||||
|         } | ||||
|  | ||||
|         newMsg.id = newMsg._id = uuid(); | ||||
|       } | ||||
|  | ||||
|       return newMsg; | ||||
|     })(); | ||||
|   }); | ||||
|  | ||||
|   return Promise.all(promises) | ||||
|     .then((filteredNewMsg) => { | ||||
|       filteredNewMsg = filteredNewMsg.filter(m => Boolean(m && m.id && m._id && m.id == m._id)); | ||||
|       return dbInboxes.insert(filteredNewMsg); | ||||
|     }).then(() => { | ||||
|       return dbUsers.update({_id: user._id}, { | ||||
|         $set: { | ||||
|           migration: migrationName, | ||||
|           'inbox.messages': {}, | ||||
|         }, | ||||
|       }); | ||||
|     }).catch((err) => { | ||||
|       console.log(err); | ||||
|       return exiting(1, `ERROR! ${  err}`); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function displayData () { | ||||
|   console.warn(`\n${  count  } users processed\n`); | ||||
|   console.warn(`\n${  msgCount  } messages processed\n`); | ||||
|   return exiting(0); | ||||
| } | ||||
|  | ||||
| function exiting (code, msg) { | ||||
|   code = code || 0; // 0 = success | ||||
|   if (code && !msg) { | ||||
|     msg = 'ERROR!'; | ||||
|   } | ||||
|   if (msg) { | ||||
|     if (code) { | ||||
|       console.error(msg); | ||||
|     } else      { | ||||
|       console.log(msg); | ||||
|     } | ||||
|   } | ||||
|   process.exit(code); | ||||
| } | ||||
|  | ||||
| module.exports = processUsers; | ||||
							
								
								
									
										99
									
								
								migrations/archive/2018/20181001_generate_usernames.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								migrations/archive/2018/20181001_generate_usernames.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,99 @@ | ||||
| let authorName = 'Sabe'; // in case script author needs to know when their ... | ||||
| let authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done | ||||
|  | ||||
| /* | ||||
|  * Generate usernames for users who lack them | ||||
|  */ | ||||
|  | ||||
| import monk from 'monk'; | ||||
| import nconf from 'nconf'; | ||||
| import { generateUsername } from '../../website/server/libs/auth/utils'; | ||||
| const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING'); // FOR TEST DATABASE | ||||
| let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false }); | ||||
|  | ||||
| function processUsers (lastId) { | ||||
|   // specify a query to limit the affected users (empty for all users): | ||||
|   let query = { | ||||
|     'auth.local.username': {$exists: false}, | ||||
|     'auth.timestamps.loggedin': {$gt: new Date('2018-04-01')}, // Initial coverage for users active within last 6 months | ||||
|   }; | ||||
|  | ||||
|   if (lastId) { | ||||
|     query._id = { | ||||
|       $gt: lastId, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   dbUsers.find(query, { | ||||
|     sort: {_id: 1}, | ||||
|     limit: 250, | ||||
|     fields: [ | ||||
|       'auth', | ||||
|     ], // specify fields we are interested in to limit retrieved data (empty if we're not reading data): | ||||
|   }) | ||||
|     .then(updateUsers) | ||||
|     .catch((err) => { | ||||
|       console.log(err); | ||||
|       return exiting(1, `ERROR! ${  err}`); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| let progressCount = 1000; | ||||
| let count = 0; | ||||
|  | ||||
| function updateUsers (users) { | ||||
|   if (!users || users.length === 0) { | ||||
|     console.warn('All appropriate users found and modified.'); | ||||
|     displayData(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   let userPromises = users.map(updateUser); | ||||
|   let lastUser = users[users.length - 1]; | ||||
|  | ||||
|   return Promise.all(userPromises) | ||||
|     .then(() => { | ||||
|       processUsers(lastUser._id); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function updateUser (user) { | ||||
|   count++; | ||||
|  | ||||
|   if (!user.auth.local.username) { | ||||
|     const newName = generateUsername(); | ||||
|     dbUsers.update( | ||||
|       {_id: user._id}, | ||||
|       {$set: | ||||
|         { | ||||
|           'auth.local.username': newName, | ||||
|           'auth.local.lowerCaseUsername': newName, | ||||
|         }, | ||||
|       } | ||||
|     ); | ||||
|   } | ||||
|   if (count % progressCount === 0) console.warn(`${count  } ${  user._id}`); | ||||
|   if (user._id === authorUuid) console.warn(`${authorName  } processed`); | ||||
| } | ||||
|  | ||||
| function displayData () { | ||||
|   console.warn(`\n${  count  } users processed\n`); | ||||
|   return exiting(0); | ||||
| } | ||||
|  | ||||
| function exiting (code, msg) { | ||||
|   code = code || 0; // 0 = success | ||||
|   if (code && !msg) { | ||||
|     msg = 'ERROR!'; | ||||
|   } | ||||
|   if (msg) { | ||||
|     if (code) { | ||||
|       console.error(msg); | ||||
|     } else      { | ||||
|       console.log(msg); | ||||
|     } | ||||
|   } | ||||
|   process.exit(code); | ||||
| } | ||||
|  | ||||
| module.exports = processUsers; | ||||
							
								
								
									
										107
									
								
								migrations/archive/2018/20181002_username_email.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								migrations/archive/2018/20181002_username_email.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,107 @@ | ||||
| const MIGRATION_NAME = '20181003_username_email.js'; | ||||
| let authorName = 'Sabe'; // in case script author needs to know when their ... | ||||
| let authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done | ||||
|  | ||||
| /* | ||||
|  * Send emails to eligible users announcing upcoming username changes | ||||
|  */ | ||||
|  | ||||
| import monk from 'monk'; | ||||
| import nconf from 'nconf'; | ||||
| import { sendTxn } from '../../website/server/libs/email'; | ||||
| const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING'); | ||||
| let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false }); | ||||
|  | ||||
| function processUsers (lastId) { | ||||
|   // specify a query to limit the affected users (empty for all users): | ||||
|   let query = { | ||||
|     migration: {$ne: MIGRATION_NAME}, | ||||
|     'auth.timestamps.loggedin': {$gt: new Date('2018-04-01')}, | ||||
|   }; | ||||
|  | ||||
|   if (lastId) { | ||||
|     query._id = { | ||||
|       $gt: lastId, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   dbUsers.find(query, { | ||||
|     sort: {_id: 1}, | ||||
|     limit: 100, | ||||
|     fields: [ | ||||
|       '_id', | ||||
|       'auth', | ||||
|       'preferences', | ||||
|       'profile', | ||||
|     ], // specify fields we are interested in to limit retrieved data (empty if we're not reading data): | ||||
|   }) | ||||
|     .then(updateUsers) | ||||
|     .catch((err) => { | ||||
|       console.log(err); | ||||
|       return exiting(1, `ERROR! ${  err}`); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| let progressCount = 1000; | ||||
| let count = 0; | ||||
|  | ||||
| function updateUsers (users) { | ||||
|   if (!users || users.length === 0) { | ||||
|     console.warn('All appropriate users found and modified.'); | ||||
|     displayData(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   let userPromises = users.map(updateUser); | ||||
|   let lastUser = users[users.length - 1]; | ||||
|  | ||||
|   return Promise.all(userPromises) | ||||
|     .then(() => delay(7000)) | ||||
|     .then(() => { | ||||
|       processUsers(lastUser._id); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function updateUser (user) { | ||||
|   count++; | ||||
|  | ||||
|   dbUsers.update({_id: user._id}, {$set: {migration: MIGRATION_NAME}}); | ||||
|  | ||||
|   sendTxn( | ||||
|     user, | ||||
|     'username-change', | ||||
|     [{name: 'UNSUB_EMAIL_TYPE_URL', content: '/user/settings/notifications?unsubFrom=importantAnnouncements'}, | ||||
|      {name: 'LOGIN_NAME', content: user.auth.local.username}] | ||||
|   ); | ||||
|  | ||||
|   if (count % progressCount === 0) console.warn(`${count} ${user._id}`); | ||||
|   if (user._id === authorUuid) console.warn(`${authorName} processed`); | ||||
| } | ||||
|  | ||||
| function displayData () { | ||||
|   console.warn(`\n${count} users processed\n`); | ||||
|   return exiting(0); | ||||
| } | ||||
|  | ||||
| function delay (t, v) { | ||||
|   return new Promise(function batchPause (resolve) { | ||||
|     setTimeout(resolve.bind(null, v), t); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| function exiting (code, msg) { | ||||
|   code = code || 0; // 0 = success | ||||
|   if (code && !msg) { | ||||
|     msg = 'ERROR!'; | ||||
|   } | ||||
|   if (msg) { | ||||
|     if (code) { | ||||
|       console.error(msg); | ||||
|     } else      { | ||||
|       console.log(msg); | ||||
|     } | ||||
|   } | ||||
|   process.exit(code); | ||||
| } | ||||
|  | ||||
| module.exports = processUsers; | ||||
							
								
								
									
										66
									
								
								migrations/archive/2018/20181023_veteran_pet_ladder.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								migrations/archive/2018/20181023_veteran_pet_ladder.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| /* eslint-disable no-console */ | ||||
| const MIGRATION_NAME = '20181023_veteran_pet_ladder'; | ||||
| import { model as User } from '../../website/server/models/user'; | ||||
|  | ||||
| const progressCount = 1000; | ||||
| let count = 0; | ||||
|  | ||||
| async function updateUser (user) { | ||||
|   count++; | ||||
|  | ||||
|   const set = {}; | ||||
|  | ||||
|   set.migration = MIGRATION_NAME; | ||||
|  | ||||
|   if (user.items.pets['Bear-Veteran']) { | ||||
|     set['items.pets.Fox-Veteran'] = 5; | ||||
|   } else if (user.items.pets['Lion-Veteran']) { | ||||
|     set['items.pets.Bear-Veteran'] = 5; | ||||
|   } else if (user.items.pets['Tiger-Veteran']) { | ||||
|     set['items.pets.Lion-Veteran'] = 5; | ||||
|   } else if (user.items.pets['Wolf-Veteran']) { | ||||
|     set['items.pets.Tiger-Veteran'] = 5; | ||||
|   } else { | ||||
|     set['items.pets.Wolf-Veteran'] = 5; | ||||
|   } | ||||
|  | ||||
|   if (count % progressCount === 0) console.warn(`${count} ${user._id}`); | ||||
|  | ||||
|   return await User.update({_id: user._id}, {$set: set}).exec(); | ||||
| } | ||||
|  | ||||
| module.exports = async function processUsers () { | ||||
|   let query = { | ||||
|     migration: {$ne: MIGRATION_NAME}, | ||||
|     'flags.verifiedUsername': true, | ||||
|   }; | ||||
|  | ||||
|   const fields = { | ||||
|     _id: 1, | ||||
|     items: 1, | ||||
|     migration: 1, | ||||
|     flags: 1, | ||||
|   }; | ||||
|  | ||||
|   while (true) { // eslint-disable-line no-constant-condition | ||||
|     const users = await User // eslint-disable-line no-await-in-loop | ||||
|       .find(query) | ||||
|       .limit(250) | ||||
|       .sort({_id: 1}) | ||||
|       .select(fields) | ||||
|       .lean() | ||||
|       .exec(); | ||||
|  | ||||
|     if (users.length === 0) { | ||||
|       console.warn('All appropriate users found and modified.'); | ||||
|       console.warn(`\n${count} users processed\n`); | ||||
|       break; | ||||
|     } else { | ||||
|       query._id = { | ||||
|         $gt: users[users.length - 1], | ||||
|       }; | ||||
|     } | ||||
|  | ||||
|     await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop | ||||
|   } | ||||
| }; | ||||
							
								
								
									
										116
									
								
								migrations/archive/2018/20181030_habitoween_ladder.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								migrations/archive/2018/20181030_habitoween_ladder.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,116 @@ | ||||
| /* | ||||
|  * Award Habitoween ladder items to participants in this month's Habitoween festivities | ||||
|  */ | ||||
|  | ||||
| import monk from 'monk'; | ||||
| import nconf from 'nconf'; | ||||
| const MIGRATION_NAME = '20181030_habitoween_ladder.js'; // Update when running in future years | ||||
| const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING'); | ||||
| const AUTHOR_NAME = 'Sabe'; // in case script author needs to know when their ... | ||||
| const AUTHOR_UUID = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done | ||||
|  | ||||
| let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false }); | ||||
|  | ||||
| function processUsers (lastId) { | ||||
|   // specify a query to limit the affected users (empty for all users): | ||||
|   let query = { | ||||
|     migration: {$ne: MIGRATION_NAME}, | ||||
|     'auth.timestamps.loggedin': {$gt: new Date('2018-10-01')}, | ||||
|   }; | ||||
|  | ||||
|   if (lastId) { | ||||
|     query._id = { | ||||
|       $gt: lastId, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   dbUsers.find(query, { | ||||
|     sort: {_id: 1}, | ||||
|     limit: 250, | ||||
|     fields: [ | ||||
|       'items.mounts', | ||||
|       'items.pets', | ||||
|     ], // specify fields we are interested in to limit retrieved data (empty if we're not reading data): | ||||
|   }) | ||||
|     .then(updateUsers) | ||||
|     .catch((err) => { | ||||
|       console.log(err); | ||||
|       return exiting(1, `ERROR! ${err}`); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| const PROGRESS_COUNT = 1000; | ||||
| let count = 0; | ||||
|  | ||||
| function updateUsers (users) { | ||||
|   if (!users || users.length === 0) { | ||||
|     console.warn('All appropriate users found and modified.'); | ||||
|     displayData(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   let userPromises = users.map(updateUser); | ||||
|   let lastUser = users[users.length - 1]; | ||||
|  | ||||
|   return Promise.all(userPromises) | ||||
|     .then(() => { | ||||
|       processUsers(lastUser._id); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function updateUser (user) { | ||||
|   count++; | ||||
|  | ||||
|   let set = {}; | ||||
|   let inc = { | ||||
|     'items.food.Candy_Skeleton': 1, | ||||
|     'items.food.Candy_Base': 1, | ||||
|     'items.food.Candy_CottonCandyBlue': 1, | ||||
|     'items.food.Candy_CottonCandyPink': 1, | ||||
|     'items.food.Candy_Shade': 1, | ||||
|     'items.food.Candy_White': 1, | ||||
|     'items.food.Candy_Golden': 1, | ||||
|     'items.food.Candy_Zombie': 1, | ||||
|     'items.food.Candy_Desert': 1, | ||||
|     'items.food.Candy_Red': 1, | ||||
|   }; | ||||
|  | ||||
|   if (user && user.items && user.items.pets && user.items.mounts['JackOLantern-Ghost']) { | ||||
|     set['items.pets.JackOLantern-Glow'] = 5; | ||||
|   } else if (user && user.items && user.items.pets && user.items.pets['JackOLantern-Ghost']) { | ||||
|     set['items.mounts.JackOLantern-Ghost'] = true; | ||||
|   } else if (user && user.items && user.items.mounts && user.items.mounts['JackOLantern-Base']) { | ||||
|     set['items.pets.JackOLantern-Ghost'] = 5; | ||||
|   } else if (user && user.items && user.items.pets && user.items.pets['JackOLantern-Base']) { | ||||
|     set['items.mounts.JackOLantern-Base'] = true; | ||||
|   } else { | ||||
|     set['items.pets.JackOLantern-Base'] = 5; | ||||
|   } | ||||
|  | ||||
|   dbUsers.update({_id: user._id}, {$set: set, $inc: inc}); | ||||
|  | ||||
|   if (count % PROGRESS_COUNT === 0) console.warn(`${count} ${user._id}`); | ||||
|   if (user._id === AUTHOR_UUID) console.warn(`${AUTHOR_NAME} processed`); | ||||
| } | ||||
|  | ||||
| function displayData () { | ||||
|   console.warn(`\n${count} users processed\n`); | ||||
|   return exiting(0); | ||||
| } | ||||
|  | ||||
| function exiting (code, msg) { | ||||
|   code = code || 0; // 0 = success | ||||
|   if (code && !msg) { | ||||
|     msg = 'ERROR!'; | ||||
|   } | ||||
|   if (msg) { | ||||
|     if (code) { | ||||
|       console.error(msg); | ||||
|     } else      { | ||||
|       console.log(msg); | ||||
|     } | ||||
|   } | ||||
|   process.exit(code); | ||||
| } | ||||
|  | ||||
| module.exports = processUsers; | ||||
							
								
								
									
										109
									
								
								migrations/archive/2018/20181108_username_email.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								migrations/archive/2018/20181108_username_email.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,109 @@ | ||||
| const MIGRATION_NAME = '20181108_username_email.js'; | ||||
| const AUTHOR_NAME = 'Sabe'; // in case script author needs to know when their ... | ||||
| const AUTHOR_UUID = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done | ||||
|  | ||||
| /* | ||||
|  * Send emails to eligible users announcing upcoming username changes | ||||
|  */ | ||||
|  | ||||
| import monk from 'monk'; | ||||
| import nconf from 'nconf'; | ||||
| import { sendTxn } from '../../../website/server/libs/email'; | ||||
| const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING'); | ||||
| const BASE_URL = nconf.get('BASE_URL'); | ||||
| let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false }); | ||||
|  | ||||
| function processUsers (lastId) { | ||||
|   // specify a query to limit the affected users (empty for all users): | ||||
|   let query = { | ||||
|     migration: {$ne: MIGRATION_NAME}, | ||||
|     'flags.verifiedUsername': {$ne: true}, | ||||
|     'auth.timestamps.loggedin': {$gt: new Date('2018-10-25')}, | ||||
|   }; | ||||
|  | ||||
|   if (lastId) { | ||||
|     query._id = { | ||||
|       $gt: lastId, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   dbUsers.find(query, { | ||||
|     sort: {_id: 1}, | ||||
|     limit: 100, | ||||
|     fields: [ | ||||
|       '_id', | ||||
|       'auth', | ||||
|       'preferences', | ||||
|       'profile', | ||||
|     ], // specify fields we are interested in to limit retrieved data (empty if we're not reading data): | ||||
|   }) | ||||
|     .then(updateUsers) | ||||
|     .catch((err) => { | ||||
|       console.log(err); | ||||
|       return exiting(1, `ERROR! ${  err}`); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| let progressCount = 1000; | ||||
| let count = 0; | ||||
|  | ||||
| function updateUsers (users) { | ||||
|   if (!users || users.length === 0) { | ||||
|     console.warn('All appropriate users found and modified.'); | ||||
|     displayData(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   let userPromises = users.map(updateUser); | ||||
|   let lastUser = users[users.length - 1]; | ||||
|  | ||||
|   return Promise.all(userPromises) | ||||
|     .then(() => delay(7000)) | ||||
|     .then(() => { | ||||
|       processUsers(lastUser._id); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function updateUser (user) { | ||||
|   count++; | ||||
|  | ||||
|   dbUsers.update({_id: user._id}, {$set: {migration: MIGRATION_NAME}}); | ||||
|  | ||||
|   sendTxn( | ||||
|     user, | ||||
|     'username-change-follow-up', | ||||
|     [{name: 'LOGIN_NAME', content: user.auth.local.username}, | ||||
|      {name: 'BASE_URL', content: BASE_URL}] | ||||
|   ); | ||||
|  | ||||
|   if (count % progressCount === 0) console.warn(`${count} ${user._id}`); | ||||
|   if (user._id === AUTHOR_UUID) console.warn(`${AUTHOR_NAME} processed`); | ||||
| } | ||||
|  | ||||
| function displayData () { | ||||
|   console.warn(`\n${count} users processed\n`); | ||||
|   return exiting(0); | ||||
| } | ||||
|  | ||||
| function delay (t, v) { | ||||
|   return new Promise(function batchPause (resolve) { | ||||
|     setTimeout(resolve.bind(null, v), t); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| function exiting (code, msg) { | ||||
|   code = code || 0; // 0 = success | ||||
|   if (code && !msg) { | ||||
|     msg = 'ERROR!'; | ||||
|   } | ||||
|   if (msg) { | ||||
|     if (code) { | ||||
|       console.error(msg); | ||||
|     } else      { | ||||
|       console.log(msg); | ||||
|     } | ||||
|   } | ||||
|   process.exit(code); | ||||
| } | ||||
|  | ||||
| module.exports = processUsers; | ||||
							
								
								
									
										109
									
								
								migrations/archive/2018/20181122_turkey_day.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								migrations/archive/2018/20181122_turkey_day.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,109 @@ | ||||
| /* eslint-disable no-console */ | ||||
| const MIGRATION_NAME = '20181122_turkey_day'; | ||||
| import mongoose from 'mongoose'; | ||||
| import { model as User } from '../../website/server/models/user'; | ||||
|  | ||||
| const progressCount = 1000; | ||||
| let count = 0; | ||||
|  | ||||
| async function updateUser (user) { | ||||
|   count++; | ||||
|  | ||||
|   const set = {}; | ||||
|   let push; | ||||
|  | ||||
|   set.migration = MIGRATION_NAME; | ||||
|  | ||||
|   if (typeof user.items.gear.owned.armor_special_turkeyArmorBase !== 'undefined') { | ||||
|     set['items.gear.owned.head_special_turkeyHelmGilded'] = false; | ||||
|     set['items.gear.owned.armor_special_turkeyArmorGilded'] = false; | ||||
|     set['items.gear.owned.back_special_turkeyTailGilded'] = false; | ||||
|     push = [ | ||||
|       { | ||||
|         type: 'marketGear', | ||||
|         path: 'gear.flat.head_special_turkeyHelmGilded', | ||||
|         _id: new mongoose.Types.ObjectId(), | ||||
|       }, | ||||
|       { | ||||
|         type: 'marketGear', | ||||
|         path: 'gear.flat.armor_special_turkeyArmorGilded', | ||||
|         _id: new mongoose.Types.ObjectId(), | ||||
|       }, | ||||
|       { | ||||
|         type: 'marketGear', | ||||
|         path: 'gear.flat.back_special_turkeyTailGilded', | ||||
|         _id: new mongoose.Types.ObjectId(), | ||||
|       }, | ||||
|     ]; | ||||
|   } else if (user.items && user.items.mounts && user.items.mounts['Turkey-Gilded']) { | ||||
|     set['items.gear.owned.head_special_turkeyHelmBase'] = false; | ||||
|     set['items.gear.owned.armor_special_turkeyArmorBase'] = false; | ||||
|     set['items.gear.owned.back_special_turkeyTailBase'] = false; | ||||
|     push = [ | ||||
|       { | ||||
|         type: 'marketGear', | ||||
|         path: 'gear.flat.head_special_turkeyHelmBase', | ||||
|         _id: new mongoose.Types.ObjectId(), | ||||
|       }, | ||||
|       { | ||||
|         type: 'marketGear', | ||||
|         path: 'gear.flat.armor_special_turkeyArmorBase', | ||||
|         _id: new mongoose.Types.ObjectId(), | ||||
|       }, | ||||
|       { | ||||
|         type: 'marketGear', | ||||
|         path: 'gear.flat.back_special_turkeyTailBase', | ||||
|         _id: new mongoose.Types.ObjectId(), | ||||
|       }, | ||||
|     ]; | ||||
|   } else if (user.items && user.items.pets && user.items.pets['Turkey-Gilded']) { | ||||
|     set['items.mounts.Turkey-Gilded'] = true; | ||||
|   } else if (user.items && user.items.mounts && user.items.mounts['Turkey-Base']) { | ||||
|     set['items.pets.Turkey-Gilded'] = 5; | ||||
|   } else if (user.items && user.items.pets && user.items.pets['Turkey-Base']) { | ||||
|     set['items.mounts.Turkey-Base'] = true; | ||||
|   } else { | ||||
|     set['items.pets.Turkey-Base'] = 5; | ||||
|   } | ||||
|  | ||||
|   if (count % progressCount === 0) console.warn(`${count} ${user._id}`); | ||||
|  | ||||
|   if (push) { | ||||
|     return await User.update({_id: user._id}, {$set: set, $push: {pinnedItems: {$each: push}}}).exec(); | ||||
|   } else { | ||||
|     return await User.update({_id: user._id}, {$set: set}).exec(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| module.exports = async function processUsers () { | ||||
|   let query = { | ||||
|     migration: {$ne: MIGRATION_NAME}, | ||||
|   }; | ||||
|  | ||||
|   const fields = { | ||||
|     _id: 1, | ||||
|     items: 1, | ||||
|   }; | ||||
|  | ||||
|   while (true) { // eslint-disable-line no-constant-condition | ||||
|     const users = await User // eslint-disable-line no-await-in-loop | ||||
|       .find(query) | ||||
|       .limit(250) | ||||
|       .sort({_id: 1}) | ||||
|       .select(fields) | ||||
|       .lean() | ||||
|       .exec(); | ||||
|  | ||||
|     if (users.length === 0) { | ||||
|       console.warn('All appropriate users found and modified.'); | ||||
|       console.warn(`\n${count} users processed\n`); | ||||
|       break; | ||||
|     } else { | ||||
|       query._id = { | ||||
|         $gt: users[users.length - 1], | ||||
|       }; | ||||
|     } | ||||
|  | ||||
|     await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop | ||||
|   } | ||||
| }; | ||||
							
								
								
									
										110
									
								
								migrations/archive/2018/20181231_nye.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								migrations/archive/2018/20181231_nye.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | ||||
| /* eslint-disable no-console */ | ||||
| const MIGRATION_NAME = '20181231_nye'; | ||||
| import { model as User } from '../../../website/server/models/user'; | ||||
| import mongoose from 'mongoose'; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
|  | ||||
| const progressCount = 1000; | ||||
| let count = 0; | ||||
|  | ||||
| async function updateUser (user) { | ||||
|   count++; | ||||
|  | ||||
|   const set = {'flags.newStuff': true}; | ||||
|   let push; | ||||
|  | ||||
|   set.migration = MIGRATION_NAME; | ||||
|  | ||||
|   if (typeof user.items.gear.owned.head_special_nye2017 !== 'undefined') { | ||||
|     set['items.gear.owned.head_special_nye2018'] = false; | ||||
|     push = [ | ||||
|       { | ||||
|         type: 'marketGear', | ||||
|         path: 'gear.flat.head_special_nye2018', | ||||
|         _id: uuid(), | ||||
|       }, | ||||
|     ]; | ||||
|   } else if (typeof user.items.gear.owned.head_special_nye2016 !== 'undefined') { | ||||
|     set['items.gear.owned.head_special_nye2017'] = false; | ||||
|     push = [ | ||||
|       { | ||||
|         type: 'marketGear', | ||||
|         path: 'gear.flat.head_special_nye2017', | ||||
|         _id: uuid(), | ||||
|       }, | ||||
|     ]; | ||||
|   } else if (typeof user.items.gear.owned.head_special_nye2015 !== 'undefined') { | ||||
|     set['items.gear.owned.head_special_nye2016'] = false; | ||||
|     push = [ | ||||
|       { | ||||
|         type: 'marketGear', | ||||
|         path: 'gear.flat.head_special_nye2016', | ||||
|         _id: uuid(), | ||||
|       }, | ||||
|     ]; | ||||
|   } else if (typeof user.items.gear.owned.head_special_nye2014 !== 'undefined') { | ||||
|     set['items.gear.owned.head_special_nye2015'] = false; | ||||
|     push = [ | ||||
|       { | ||||
|         type: 'marketGear', | ||||
|         path: 'gear.flat.head_special_nye2015', | ||||
|         _id: uuid(), | ||||
|       }, | ||||
|     ]; | ||||
|   } else if (typeof user.items.gear.owned.head_special_nye !== 'undefined') { | ||||
|     set['items.gear.owned.head_special_nye2014'] = false; | ||||
|     push = [ | ||||
|       { | ||||
|         type: 'marketGear', | ||||
|         path: 'gear.flat.head_special_nye2014', | ||||
|         _id: uuid(), | ||||
|       }, | ||||
|     ]; | ||||
|   } else { | ||||
|     set['items.gear.owned.head_special_nye'] = false; | ||||
|     push = [ | ||||
|       { | ||||
|         type: 'marketGear', | ||||
|         path: 'gear.flat.head_special_nye', | ||||
|         _id: uuid(), | ||||
|       }, | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|   if (count % progressCount === 0) console.warn(`${count} ${user._id}`); | ||||
|  | ||||
|   return await User.update({_id: user._id}, {$set: set, $push: {pinnedItems: {$each: push}}}).exec(); | ||||
| } | ||||
|  | ||||
| module.exports = async function processUsers () { | ||||
|   let query = { | ||||
|     migration: {$ne: MIGRATION_NAME}, | ||||
|   }; | ||||
|  | ||||
|   const fields = { | ||||
|     _id: 1, | ||||
|     items: 1, | ||||
|   }; | ||||
|  | ||||
|   while (true) { // eslint-disable-line no-constant-condition | ||||
|     const users = await User // eslint-disable-line no-await-in-loop | ||||
|       .find(query) | ||||
|       .limit(250) | ||||
|       .sort({_id: 1}) | ||||
|       .select(fields) | ||||
|       .lean() | ||||
|       .exec(); | ||||
|  | ||||
|     if (users.length === 0) { | ||||
|       console.warn('All appropriate users found and modified.'); | ||||
|       console.warn(`\n${count} users processed\n`); | ||||
|       break; | ||||
|     } else { | ||||
|       query._id = { | ||||
|         $gt: users[users.length - 1], | ||||
|       }; | ||||
|     } | ||||
|  | ||||
|     await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop | ||||
|   } | ||||
| }; | ||||
							
								
								
									
										110
									
								
								migrations/archive/mystery-items-old.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								migrations/archive/mystery-items-old.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | ||||
| import monk from 'monk'; | ||||
| import nconf from 'nconf'; | ||||
|  | ||||
| const migrationName = 'mystery-items-201808.js'; // Update per month | ||||
| const authorName = 'Sabe'; // in case script author needs to know when their ... | ||||
| const authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done | ||||
|  | ||||
| /* | ||||
|  * Award this month's mystery items to subscribers | ||||
|  */ | ||||
| const MYSTERY_ITEMS = ['armor_mystery_201810', 'head_mystery_201810']; | ||||
| const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING'); | ||||
|  | ||||
| let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false }); | ||||
| let UserNotification = require('../../website/server/models/userNotification').model; | ||||
|  | ||||
| function processUsers (lastId) { | ||||
|   // specify a query to limit the affected users (empty for all users): | ||||
|   let query = { | ||||
|     migration: {$ne: migrationName}, | ||||
|     'purchased.plan.customerId': { $ne: null }, | ||||
|     $or: [ | ||||
|       { 'purchased.plan.dateTerminated': { $gte: new Date() } }, | ||||
|       { 'purchased.plan.dateTerminated': { $exists: false } }, | ||||
|       { 'purchased.plan.dateTerminated': { $eq: null } }, | ||||
|     ], | ||||
|   }; | ||||
|  | ||||
|   if (lastId) { | ||||
|     query._id = { | ||||
|       $gt: lastId, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   dbUsers.find(query, { | ||||
|     sort: {_id: 1}, | ||||
|     limit: 250, | ||||
|     fields: [ | ||||
|     ], // specify fields we are interested in to limit retrieved data (empty if we're not reading data): | ||||
|   }) | ||||
|     .then(updateUsers) | ||||
|     .catch((err) => { | ||||
|       console.log(err); | ||||
|       return exiting(1, `ERROR! ${  err}`); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| let progressCount = 1000; | ||||
| let count = 0; | ||||
|  | ||||
| function updateUsers (users) { | ||||
|   if (!users || users.length === 0) { | ||||
|     console.warn('All appropriate users found and modified.'); | ||||
|     displayData(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   let userPromises = users.map(updateUser); | ||||
|   let lastUser = users[users.length - 1]; | ||||
|  | ||||
|   return Promise.all(userPromises) | ||||
|     .then(() => { | ||||
|       processUsers(lastUser._id); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function updateUser (user) { | ||||
|   count++; | ||||
|  | ||||
|   const addToSet = { | ||||
|     'purchased.plan.mysteryItems': { | ||||
|       $each: MYSTERY_ITEMS, | ||||
|     }, | ||||
|   }; | ||||
|   const push = { | ||||
|     notifications: (new UserNotification({ | ||||
|       type: 'NEW_MYSTERY_ITEMS', | ||||
|       data: { | ||||
|         MYSTERY_ITEMS, | ||||
|       }, | ||||
|     })).toJSON(), | ||||
|   }; | ||||
|  | ||||
|   dbUsers.update({_id: user._id}, {$addToSet: addToSet, $push: push}); | ||||
|  | ||||
|   if (count % progressCount === 0) console.warn(`${count  } ${  user._id}`); | ||||
|   if (user._id === authorUuid) console.warn(`${authorName  } processed`); | ||||
| } | ||||
|  | ||||
| function displayData () { | ||||
|   console.warn(`\n${  count  } users processed\n`); | ||||
|   return exiting(0); | ||||
| } | ||||
|  | ||||
| function exiting (code, msg) { | ||||
|   code = code || 0; // 0 = success | ||||
|   if (code && !msg) { | ||||
|     msg = 'ERROR!'; | ||||
|   } | ||||
|   if (msg) { | ||||
|     if (code) { | ||||
|       console.error(msg); | ||||
|     } else      { | ||||
|       console.log(msg); | ||||
|     } | ||||
|   } | ||||
|   process.exit(code); | ||||
| } | ||||
|  | ||||
| module.exports = processUsers; | ||||
| @@ -1,4 +1,4 @@ | ||||
| let migrationName = '20180102_takeThis.js'; // Update per month
 | ||||
| let migrationName = '20180904_takeThis.js'; // Update per month
 | ||||
| let authorName = 'Sabe'; // in case script author needs to know when their ...
 | ||||
| let authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
 | ||||
| 
 | ||||
| @@ -6,15 +6,16 @@ let authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done | ||||
|  * Award Take This ladder items to participants in this month's challenge | ||||
|  */ | ||||
| 
 | ||||
| let monk = require('monk'); | ||||
| let connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
 | ||||
| let dbUsers = monk(connectionString).get('users', { castIds: false }); | ||||
| import monk from 'monk'; | ||||
| import nconf from 'nconf'; | ||||
| const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING'); // FOR TEST DATABASE
 | ||||
| let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false }); | ||||
| 
 | ||||
| function processUsers (lastId) { | ||||
|   // specify a query to limit the affected users (empty for all users):
 | ||||
|   let query = { | ||||
|     migration: {$ne: migrationName}, | ||||
|     challenges: {$in: ['5f70ce5b-2d82-4114-8e44-ca65615aae62']}, // Update per month
 | ||||
|     challenges: {$in: ['1044ec0c-4a85-48c5-9f36-d51c0c62c7d3']}, // Update per month
 | ||||
|   }; | ||||
| 
 | ||||
|   if (lastId) { | ||||
							
								
								
									
										52
									
								
								migrations/groups/migrate-chat.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								migrations/groups/migrate-chat.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| // @migrationName = 'MigrateGroupChat'; | ||||
| // @authorName = 'TheHollidayInn'; // in case script author needs to know when their ... | ||||
| // @authorUuid = ''; // ... own data is done | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * This migration moves chat off of groups and into their own model | ||||
|  */ | ||||
|  | ||||
| import { model as Group } from '../../website/server/models/group'; | ||||
| import { model as Chat } from '../../website/server/models/chat'; | ||||
|  | ||||
| async function moveGroupChatToModel (skip = 0) { | ||||
|   const groups = await Group.find({}) | ||||
|     .limit(50) | ||||
|     .skip(skip) | ||||
|     .sort({ _id: -1 }) | ||||
|     .exec(); | ||||
|  | ||||
|   if (groups.length === 0) { | ||||
|     console.log('End of groups'); | ||||
|     process.exit(); | ||||
|   } | ||||
|  | ||||
|   const promises = groups.map(group => { | ||||
|     const chatpromises = group.chat.map(message => { | ||||
|       const newChat = new Chat(); | ||||
|       Object.assign(newChat, message); | ||||
|       newChat._id = message.id; | ||||
|       newChat.groupId = group._id; | ||||
|  | ||||
|       return newChat.save(); | ||||
|     }); | ||||
|  | ||||
|     group.chat = []; | ||||
|     chatpromises.push(group.save()); | ||||
|  | ||||
|     return chatpromises; | ||||
|   }); | ||||
|  | ||||
|  | ||||
|   const reducedPromises = promises.reduce((acc, curr) => { | ||||
|     acc = acc.concat(curr); | ||||
|     return acc; | ||||
|   }, []); | ||||
|  | ||||
|   console.log(reducedPromises); | ||||
|   await Promise.all(reducedPromises); | ||||
|   moveGroupChatToModel(skip + 50); | ||||
| } | ||||
|  | ||||
| module.exports = moveGroupChatToModel; | ||||
							
								
								
									
										107
									
								
								migrations/groups/reconcile-group-plan-members.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								migrations/groups/reconcile-group-plan-members.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,107 @@ | ||||
| import monk from 'monk'; | ||||
| import nconf from 'nconf'; | ||||
| import stripePayments from '../../website/server/libs/payments/stripe'; | ||||
|  | ||||
| /* | ||||
|  * Ensure that group plan billing is accurate by doing the following: | ||||
|  * 1. Correct the memberCount in all paid groups whose counts are wrong | ||||
|  * 2. Where the above uses Stripe, update their subscription counts in Stripe | ||||
|  * | ||||
|  * Provides output on what groups were fixed, which can be piped to CSV. | ||||
|  */ | ||||
|  | ||||
| const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING'); | ||||
|  | ||||
| let dbGroups = monk(CONNECTION_STRING).get('groups', { castIds: false }); | ||||
| let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false }); | ||||
|  | ||||
| async function fixGroupPlanMembers () { | ||||
|   console.info('Group ID, Customer ID, Plan ID, Quantity, Recorded Member Count, Actual Member Count'); | ||||
|   let groupPlanCount = 0; | ||||
|   let fixedGroupCount = 0; | ||||
|  | ||||
|   dbGroups.find( | ||||
|     { | ||||
|       $and: | ||||
|         [ | ||||
|           {'purchased.plan.planId': {$ne: null}}, | ||||
|           {'purchased.plan.planId': {$ne: ''}}, | ||||
|           {'purchased.plan.customerId': {$ne: 'cus_9f0DV4g7WHRzpM'}}, // Demo groups | ||||
|           {'purchased.plan.customerId': {$ne: 'cus_9maalqDOFTrvqx'}}, | ||||
|         ], | ||||
|       $or: | ||||
|         [ | ||||
|           {'purchased.plan.dateTerminated': null}, | ||||
|           {'purchased.plan.dateTerminated': ''}, | ||||
|         ], | ||||
|     }, | ||||
|     { | ||||
|       fields: { | ||||
|         memberCount: 1, | ||||
|         'purchased.plan': 1, | ||||
|       }, | ||||
|     } | ||||
|   ).each(async (group, {close, pause, resume}) => { // eslint-disable-line no-unused-vars | ||||
|     pause(); | ||||
|     groupPlanCount++; | ||||
|  | ||||
|     const canonicalMemberCount = await dbUsers.count( | ||||
|       { | ||||
|         $or: | ||||
|           [ | ||||
|             {'party._id': group._id}, | ||||
|             {guilds: group._id}, | ||||
|           ], | ||||
|       } | ||||
|     ); | ||||
|     const incorrectMemberCount = group.memberCount !== canonicalMemberCount; | ||||
|  | ||||
|     const isMonthlyPlan = group.purchased.plan.planId === 'group_monthly'; | ||||
|     const quantityMismatch = group.purchased.plan.quantity !== group.memberCount + 2; | ||||
|     const incorrectQuantity = isMonthlyPlan && quantityMismatch; | ||||
|  | ||||
|     if (!incorrectMemberCount && !incorrectQuantity) { | ||||
|       resume(); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     console.info(`${group._id}, ${group.purchased.plan.customerId}, ${group.purchased.plan.planId}, ${group.purchased.plan.quantity}, ${group.memberCount}, ${canonicalMemberCount}`); | ||||
|  | ||||
|     const groupUpdate = await dbGroups.update( | ||||
|       { _id: group._id }, | ||||
|       { | ||||
|         $set: { | ||||
|           memberCount: canonicalMemberCount, | ||||
|         }, | ||||
|       } | ||||
|     ); | ||||
|  | ||||
|     if (!groupUpdate) return; | ||||
|  | ||||
|     fixedGroupCount++; | ||||
|     if (group.purchased.plan.paymentMethod === 'Stripe') { | ||||
|       await stripePayments.chargeForAdditionalGroupMember(group); | ||||
|       await dbGroups.update( | ||||
|         {_id: group._id}, | ||||
|         {$set: {'purchased.plan.quantity': canonicalMemberCount + 2}} | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     if (incorrectQuantity) { | ||||
|       await dbGroups.update( | ||||
|         {_id: group._id}, | ||||
|         {$set: {'purchased.plan.quantity': canonicalMemberCount + 2}} | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     resume(); | ||||
|   }).then(() => { | ||||
|     console.info(`Fixed ${fixedGroupCount} out of ${groupPlanCount} active Group Plans`); | ||||
|     return process.exit(0); | ||||
|   }).catch((err) => { | ||||
|     console.log(err); | ||||
|     return process.exit(1); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| module.exports = fixGroupPlanMembers; | ||||
| @@ -17,5 +17,12 @@ function setUpServer () { | ||||
| setUpServer(); | ||||
|  | ||||
| // Replace this with your migration | ||||
| const processUsers = require('./20180125_clean_new_notifications.js'); | ||||
| processUsers(); | ||||
| const processUsers = require('./archive/2018/20181231_nye.js'); | ||||
| processUsers() | ||||
|   .then(function success () { | ||||
|     process.exit(0); | ||||
|   }) | ||||
|   .catch(function failure (err) { | ||||
|     console.log(err); | ||||
|     process.exit(1); | ||||
|   }); | ||||
|   | ||||
							
								
								
									
										138
									
								
								migrations/tasks/habits-one-history-entry-per-day-challenges.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								migrations/tasks/habits-one-history-entry-per-day-challenges.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,138 @@ | ||||
| // const migrationName = 'habits-one-history-entry-per-day'; | ||||
| // const authorName = 'paglias'; // in case script author needs to know when their ... | ||||
| // const authorUuid = 'ed4c688c-6652-4a92-9d03-a5a79844174a'; // ... own data is done | ||||
|  | ||||
| /* | ||||
|  * Iterates over all habits and condense multiple history entries for the same day into a single entry | ||||
|  */ | ||||
|  | ||||
| const monk = require('monk'); | ||||
| const _ = require('lodash'); | ||||
| const moment = require('moment'); | ||||
| const connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE | ||||
| const dbTasks = monk(connectionString).get('tasks', { castIds: false }); | ||||
|  | ||||
| function processChallengeHabits (lastId) { | ||||
|   let query = { | ||||
|     'challenge.id': {$exists: true}, | ||||
|     userId: {$exists: false}, | ||||
|     type: 'habit', | ||||
|   }; | ||||
|  | ||||
|   if (lastId) { | ||||
|     query._id = { | ||||
|       $gt: lastId, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   dbTasks.find(query, { | ||||
|     sort: {_id: 1}, | ||||
|     limit: 500, | ||||
|   }) | ||||
|     .then(updateChallengeHabits) | ||||
|     .catch((err) => { | ||||
|       console.log(err); | ||||
|       return exiting(1, `ERROR! ${  err}`); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| let progressCount = 1000; | ||||
| let count = 0; | ||||
|  | ||||
| function updateChallengeHabits (habits) { | ||||
|   if (!habits || habits.length === 0) { | ||||
|     console.warn('All appropriate challenge habits found and modified.'); | ||||
|     displayData(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   let habitsPromises = habits.map(updateChallengeHabit); | ||||
|   let lastHabit = habits[habits.length - 1]; | ||||
|  | ||||
|   return Promise.all(habitsPromises) | ||||
|     .then(() => { | ||||
|       return processChallengeHabits(lastHabit._id); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function updateChallengeHabit (habit) { | ||||
|   count++; | ||||
|  | ||||
|   if (habit && habit.history && habit.history.length > 0) { | ||||
|     // First remove missing entries | ||||
|     habit.history = habit.history.filter(entry => Boolean(entry)); | ||||
|  | ||||
|     habit.history = _.chain(habit.history) | ||||
|       // processes all entries to identify an up or down score | ||||
|       .forEach((entry, index) => { | ||||
|         if (index === 0) { // first entry doesn't have a previous one | ||||
|           // first value < 0 identifies a negative score as the first action | ||||
|           entry.scoreDirection = entry.value >= 0 ? 'up' : 'down'; | ||||
|         } else { | ||||
|           // could be missing if the previous entry was null and thus excluded | ||||
|           const previousEntry = habit.history[index - 1]; | ||||
|           const previousValue = previousEntry.value; | ||||
|  | ||||
|           entry.scoreDirection = entry.value > previousValue ? 'up' : 'down'; | ||||
|         } | ||||
|       }) | ||||
|       .groupBy(entry => { // group entries by aggregateBy | ||||
|         return moment(entry.date).format('YYYYMMDD'); | ||||
|       }) | ||||
|       .toPairs() // [key, entry] | ||||
|       .sortBy(([key]) => key) // sort by date | ||||
|       .map(keyEntryPair => { | ||||
|         let entries = keyEntryPair[1]; // 1 is entry, 0 is key | ||||
|         let scoredUp = 0; | ||||
|         let scoredDown = 0; | ||||
|  | ||||
|         entries.forEach(entry => { | ||||
|           if (entry.scoreDirection === 'up') { | ||||
|             scoredUp += 1; | ||||
|           } else { | ||||
|             scoredDown += 1; | ||||
|           } | ||||
|  | ||||
|           // delete the unnecessary scoreDirection and scoreNotes prop | ||||
|           delete entry.scoreDirection; | ||||
|           delete entry.scoreNotes; | ||||
|         }); | ||||
|  | ||||
|         return { | ||||
|           date: Number(entries[entries.length - 1].date), // keep last value | ||||
|           value: entries[entries.length - 1].value, // keep last value, | ||||
|           scoredUp, | ||||
|           scoredDown, | ||||
|         }; | ||||
|       }) | ||||
|       .value(); | ||||
|  | ||||
|     return dbTasks.update({_id: habit._id}, { | ||||
|       $set: {history: habit.history}, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   if (count % progressCount === 0) console.warn(`${count  } habits processed`); | ||||
| } | ||||
|  | ||||
| function displayData () { | ||||
|   console.warn(`\n${  count  } tasks processed\n`); | ||||
|   return exiting(0); | ||||
| } | ||||
|  | ||||
| function exiting (code, msg) { | ||||
|   code = code || 0; // 0 = success | ||||
|   if (code && !msg) { | ||||
|     msg = 'ERROR!'; | ||||
|   } | ||||
|   if (msg) { | ||||
|     if (code) { | ||||
|       console.error(msg); | ||||
|     } else      { | ||||
|       console.log(msg); | ||||
|     } | ||||
|   } | ||||
|   process.exit(code); | ||||
| } | ||||
|  | ||||
| module.exports = processChallengeHabits; | ||||
							
								
								
									
										163
									
								
								migrations/tasks/habits-one-history-entry-per-day-users.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								migrations/tasks/habits-one-history-entry-per-day-users.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,163 @@ | ||||
| const migrationName = 'habits-one-history-entry-per-day'; | ||||
| const authorName = 'paglias'; // in case script author needs to know when their ... | ||||
| const authorUuid = 'ed4c688c-6652-4a92-9d03-a5a79844174a'; // ... own data is done | ||||
|  | ||||
| /* | ||||
|  * Iterates over all habits and condense multiple history entries for the same day into a single entry | ||||
|  */ | ||||
|  | ||||
| const monk = require('monk'); | ||||
| const _ = require('lodash'); | ||||
| const moment = require('moment'); | ||||
| const connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE | ||||
| const dbTasks = monk(connectionString).get('tasks', { castIds: false }); | ||||
| const dbUsers = monk(connectionString).get('users', { castIds: false }); | ||||
|  | ||||
| function processUsers (lastId) { | ||||
|   let query = { | ||||
|     migration: {$ne: migrationName}, | ||||
|   }; | ||||
|  | ||||
|   if (lastId) { | ||||
|     query._id = { | ||||
|       $gt: lastId, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   dbUsers.find(query, { | ||||
|     sort: {_id: 1}, | ||||
|     limit: 50, // just 50 users per time since we have to process all their habits as well | ||||
|     fields: ['_id', 'preferences.timezoneOffset', 'preferences.dayStart'], | ||||
|   }) | ||||
|     .then(updateUsers) | ||||
|     .catch((err) => { | ||||
|       console.log(err); | ||||
|       return exiting(1, `ERROR! ${  err}`); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| let progressCount = 1000; | ||||
| let count = 0; | ||||
|  | ||||
| function updateUsers (users) { | ||||
|   if (!users || users.length === 0) { | ||||
|     console.warn('All appropriate users and their tasks found and modified.'); | ||||
|     displayData(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   let usersPromises = users.map(updateUser); | ||||
|   let lastUser = users[users.length - 1]; | ||||
|  | ||||
|   return Promise.all(usersPromises) | ||||
|     .then(() => { | ||||
|       return processUsers(lastUser._id); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function updateHabit (habit, timezoneOffset, dayStart) { | ||||
|   if (habit && habit.history && habit.history.length > 0) { | ||||
|     // First remove missing entries | ||||
|     habit.history = habit.history.filter(entry => Boolean(entry)); | ||||
|  | ||||
|     habit.history = _.chain(habit.history) | ||||
|       // processes all entries to identify an up or down score | ||||
|       .forEach((entry, index) => { | ||||
|         if (index === 0) { // first entry doesn't have a previous one | ||||
|           // first value < 0 identifies a negative score as the first action | ||||
|           entry.scoreDirection = entry.value >= 0 ? 'up' : 'down'; | ||||
|         } else { | ||||
|           // could be missing if the previous entry was null and thus excluded | ||||
|           const previousEntry = habit.history[index - 1]; | ||||
|           const previousValue = previousEntry.value; | ||||
|  | ||||
|           entry.scoreDirection = entry.value > previousValue ? 'up' : 'down'; | ||||
|         } | ||||
|       }) | ||||
|       .groupBy(entry => { // group entries by aggregateBy | ||||
|         const entryDate = moment(entry.date).zone(timezoneOffset || 0); | ||||
|         if (entryDate.hour() < dayStart) entryDate.subtract(1, 'day'); | ||||
|         return entryDate.format('YYYYMMDD'); | ||||
|       }) | ||||
|       .toPairs() // [key, entry] | ||||
|       .sortBy(([key]) => key) // sort by date | ||||
|       .map(keyEntryPair => { | ||||
|         let entries = keyEntryPair[1]; // 1 is entry, 0 is key | ||||
|         let scoredUp = 0; | ||||
|         let scoredDown = 0; | ||||
|  | ||||
|         entries.forEach(entry => { | ||||
|           if (entry.scoreDirection === 'up') { | ||||
|             scoredUp += 1; | ||||
|           } else { | ||||
|             scoredDown += 1; | ||||
|           } | ||||
|  | ||||
|           // delete the unnecessary scoreDirection and scoreNotes prop | ||||
|           delete entry.scoreDirection; | ||||
|           delete entry.scoreNotes; | ||||
|         }); | ||||
|  | ||||
|         return { | ||||
|           date: Number(entries[entries.length - 1].date), // keep last value | ||||
|           value: entries[entries.length - 1].value, // keep last value, | ||||
|           scoredUp, | ||||
|           scoredDown, | ||||
|         }; | ||||
|       }) | ||||
|       .value(); | ||||
|  | ||||
|     return dbTasks.update({_id: habit._id}, { | ||||
|       $set: {history: habit.history}, | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| function updateUser (user) { | ||||
|   count++; | ||||
|  | ||||
|   const timezoneOffset = user.preferences.timezoneOffset; | ||||
|   const dayStart = user.preferences.dayStart; | ||||
|  | ||||
|   if (count % progressCount === 0) console.warn(`${count  } ${  user._id}`); | ||||
|   if (user._id === authorUuid) console.warn(`${authorName  } being processed`); | ||||
|  | ||||
|   return dbTasks.find({ | ||||
|     type: 'habit', | ||||
|     userId: user._id, | ||||
|   }) | ||||
|     .then(habits => { | ||||
|       return Promise.all(habits.map(habit => updateHabit(habit, timezoneOffset, dayStart))); | ||||
|     }) | ||||
|     .then(() => { | ||||
|       return dbUsers.update({_id: user._id}, { | ||||
|         $set: {migration: migrationName}, | ||||
|       }); | ||||
|     }) | ||||
|     .catch((err) => { | ||||
|       console.log(err); | ||||
|       return exiting(1, `ERROR! ${  err}`); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function displayData () { | ||||
|   console.warn(`\n${  count  } tasks processed\n`); | ||||
|   return exiting(0); | ||||
| } | ||||
|  | ||||
| function exiting (code, msg) { | ||||
|   code = code || 0; // 0 = success | ||||
|   if (code && !msg) { | ||||
|     msg = 'ERROR!'; | ||||
|   } | ||||
|   if (msg) { | ||||
|     if (code) { | ||||
|       console.error(msg); | ||||
|     } else      { | ||||
|       console.log(msg); | ||||
|     } | ||||
|   } | ||||
|   process.exit(code); | ||||
| } | ||||
|  | ||||
| module.exports = processUsers; | ||||
| @@ -7,7 +7,7 @@ let authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done | ||||
|  */ | ||||
|  | ||||
| let monk = require('monk'); | ||||
| let connectionString = 'mongodb://sabrecat:z8e8jyRA8CTofMQ@ds013393-a0.mlab.com:13393/habitica?auto_reconnect=true'; | ||||
| let connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; | ||||
| let dbTasks = monk(connectionString).get('tasks', { castIds: false }); | ||||
|  | ||||
| function processTasks (lastId) { | ||||
|   | ||||
							
								
								
									
										61
									
								
								migrations/users/bulk-email.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								migrations/users/bulk-email.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | ||||
| /* eslint-disable no-console */ | ||||
| import { sendTxn } from '../../../website/server/libs/email'; | ||||
| import { model as User } from '../../website/server/models/user'; | ||||
| import moment from 'moment'; | ||||
| import nconf from 'nconf'; | ||||
| const BASE_URL = nconf.get('BASE_URL'); | ||||
| const EMAIL_SLUG = 'mandrill-email-slug'; // Set email template to send | ||||
| const MIGRATION_NAME = 'bulk-email'; | ||||
|  | ||||
| const progressCount = 1000; | ||||
| let count = 0; | ||||
|  | ||||
| async function updateUser (user) { | ||||
|   count++; | ||||
|  | ||||
|   if (count % progressCount === 0) console.warn(`${count} ${user._id}`); | ||||
|  | ||||
|   sendTxn( | ||||
|     user, | ||||
|     EMAIL_SLUG, | ||||
|     [{name: 'BASE_URL', content: BASE_URL}] // Add variables from template | ||||
|   ); | ||||
|  | ||||
|   return await User.update({_id: user._id}, {$set: {migration: MIGRATION_NAME}}).exec(); | ||||
| } | ||||
|  | ||||
| module.exports = async function processUsers () { | ||||
|   let query = { | ||||
|     migration: {$ne: MIGRATION_NAME}, | ||||
|     'auth.timestamps.loggedin': {$gt: moment().subtract(2, 'weeks').toDate()}, // customize or remove to target different populations | ||||
|   }; | ||||
|  | ||||
|   const fields = { | ||||
|     _id: 1, | ||||
|     auth: 1, | ||||
|     preferences: 1, | ||||
|     profile: 1, | ||||
|   }; | ||||
|  | ||||
|   while (true) { // eslint-disable-line no-constant-condition | ||||
|     const users = await User // eslint-disable-line no-await-in-loop | ||||
|       .find(query) | ||||
|       .limit(250) | ||||
|       .sort({_id: 1}) | ||||
|       .select(fields) | ||||
|       .lean() | ||||
|       .exec(); | ||||
|  | ||||
|     if (users.length === 0) { | ||||
|       console.warn('All appropriate users found and modified.'); | ||||
|       console.warn(`\n${count} users processed\n`); | ||||
|       break; | ||||
|     } else { | ||||
|       query._id = { | ||||
|         $gt: users[users.length - 1], | ||||
|       }; | ||||
|     } | ||||
|  | ||||
|     await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop | ||||
|   } | ||||
| }; | ||||
| @@ -1,68 +1,13 @@ | ||||
| const migrationName = 'mystery-items-201802.js'; // Update per month | ||||
| const authorName = 'Sabe'; // in case script author needs to know when their ... | ||||
| const authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done | ||||
| /* eslint-disable no-console */ | ||||
| const MIGRATION_NAME = 'mystery_items_201812'; | ||||
| const MYSTERY_ITEMS = ['headAccessory_mystery_201812', 'back_mystery_201812']; | ||||
| import { model as User } from '../../website/server/models/user'; | ||||
| import { model as UserNotification } from '../../website/server/models/userNotification'; | ||||
|  | ||||
| /* | ||||
|  * Award this month's mystery items to subscribers | ||||
|  */ | ||||
| const MYSTERY_ITEMS = ['back_mystery_201803', 'head_mystery_201803']; | ||||
| const connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE | ||||
|  | ||||
| let monk = require('monk'); | ||||
| let dbUsers = monk(connectionString).get('users', { castIds: false }); | ||||
| let UserNotification = require('../../website/server/models/userNotification').model; | ||||
|  | ||||
| function processUsers (lastId) { | ||||
|   // specify a query to limit the affected users (empty for all users): | ||||
|   let query = { | ||||
|     migration: {$ne: migrationName}, | ||||
|     'purchased.plan.customerId': { $ne: null }, | ||||
|     $or: [ | ||||
|       { 'purchased.plan.dateTerminated': { $gte: new Date() } }, | ||||
|       { 'purchased.plan.dateTerminated': { $exists: false } }, | ||||
|       { 'purchased.plan.dateTerminated': { $eq: null } }, | ||||
|     ], | ||||
|   }; | ||||
|  | ||||
|   if (lastId) { | ||||
|     query._id = { | ||||
|       $gt: lastId, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   dbUsers.find(query, { | ||||
|     sort: {_id: 1}, | ||||
|     limit: 250, | ||||
|     fields: [ | ||||
|     ], // specify fields we are interested in to limit retrieved data (empty if we're not reading data): | ||||
|   }) | ||||
|     .then(updateUsers) | ||||
|     .catch((err) => { | ||||
|       console.log(err); | ||||
|       return exiting(1, `ERROR! ${  err}`); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| let progressCount = 1000; | ||||
| const progressCount = 1000; | ||||
| let count = 0; | ||||
|  | ||||
| function updateUsers (users) { | ||||
|   if (!users || users.length === 0) { | ||||
|     console.warn('All appropriate users found and modified.'); | ||||
|     displayData(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   let userPromises = users.map(updateUser); | ||||
|   let lastUser = users[users.length - 1]; | ||||
|  | ||||
|   return Promise.all(userPromises) | ||||
|     .then(() => { | ||||
|       processUsers(lastUser._id); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function updateUser (user) { | ||||
| async function updateUser (user) { | ||||
|   count++; | ||||
|  | ||||
|   const addToSet = { | ||||
| @@ -78,31 +23,49 @@ function updateUser (user) { | ||||
|       }, | ||||
|     })).toJSON(), | ||||
|   }; | ||||
|   const set = { | ||||
|     migration: MIGRATION_NAME, | ||||
|   }; | ||||
|  | ||||
|   dbUsers.update({_id: user._id}, {$addToSet: addToSet, $push: push}); | ||||
|   if (count % progressCount === 0) console.warn(`${count} ${user._id}`); | ||||
|  | ||||
|   if (count % progressCount === 0) console.warn(`${count  } ${  user._id}`); | ||||
|   if (user._id === authorUuid) console.warn(`${authorName  } processed`); | ||||
|   return await User.update({_id: user._id}, {$set: set, $push: push, $addToSet: addToSet}).exec(); | ||||
| } | ||||
|  | ||||
| function displayData () { | ||||
|   console.warn(`\n${  count  } users processed\n`); | ||||
|   return exiting(0); | ||||
| } | ||||
| module.exports = async function processUsers () { | ||||
|   let query = { | ||||
|     migration: {$ne: MIGRATION_NAME}, | ||||
|     'purchased.plan.customerId': { $ne: null }, | ||||
|     $or: [ | ||||
|       { 'purchased.plan.dateTerminated': { $gte: new Date() } }, | ||||
|       { 'purchased.plan.dateTerminated': { $exists: false } }, | ||||
|       { 'purchased.plan.dateTerminated': { $eq: null } }, | ||||
|     ], | ||||
|   }; | ||||
|  | ||||
| function exiting (code, msg) { | ||||
|   code = code || 0; // 0 = success | ||||
|   if (code && !msg) { | ||||
|     msg = 'ERROR!'; | ||||
|   } | ||||
|   if (msg) { | ||||
|     if (code) { | ||||
|       console.error(msg); | ||||
|     } else      { | ||||
|       console.log(msg); | ||||
|   const fields = { | ||||
|     _id: 1, | ||||
|   }; | ||||
|  | ||||
|   while (true) { // eslint-disable-line no-constant-condition | ||||
|     const users = await User // eslint-disable-line no-await-in-loop | ||||
|       .find(query) | ||||
|       .limit(250) | ||||
|       .sort({_id: 1}) | ||||
|       .select(fields) | ||||
|       .lean() | ||||
|       .exec(); | ||||
|  | ||||
|     if (users.length === 0) { | ||||
|       console.warn('All appropriate users found and modified.'); | ||||
|       console.warn(`\n${count} users processed\n`); | ||||
|       break; | ||||
|     } else { | ||||
|       query._id = { | ||||
|         $gt: users[users.length - 1], | ||||
|       }; | ||||
|     } | ||||
|   } | ||||
|   process.exit(code); | ||||
| } | ||||
|  | ||||
| module.exports = processUsers; | ||||
|     await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop | ||||
|   } | ||||
| }; | ||||
|   | ||||
							
								
								
									
										109
									
								
								migrations/users/remove-social-users-extra-data.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								migrations/users/remove-social-users-extra-data.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,109 @@ | ||||
| const migrationName = 'remove-social-users-extra-data.js'; | ||||
| const authorName = 'paglias'; // in case script author needs to know when their ... | ||||
| const authorUuid = 'ed4c688c-6652-4a92-9d03-a5a79844174a'; // ... own data is done | ||||
|  | ||||
| /* | ||||
|  * Remove not needed data from social profiles | ||||
|  */ | ||||
| const connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE | ||||
|  | ||||
| const monk = require('monk'); | ||||
| const dbUsers = monk(connectionString).get('users', { castIds: false }); | ||||
|  | ||||
| function processUsers (lastId) { | ||||
|   // specify a query to limit the affected users (empty for all users): | ||||
|   let query = { | ||||
|     migration: {$ne: migrationName}, | ||||
|     $or: [ | ||||
|       { 'auth.facebook.id': { $exists: true } }, | ||||
|       { 'auth.google.id': { $exists: true } }, | ||||
|     ], | ||||
|   }; | ||||
|  | ||||
|   if (lastId) { | ||||
|     query._id = { | ||||
|       $gt: lastId, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   dbUsers.find(query, { | ||||
|     sort: {_id: 1}, | ||||
|     limit: 250, | ||||
|   }) | ||||
|     .then(updateUsers) | ||||
|     .catch((err) => { | ||||
|       console.log(err); | ||||
|       return exiting(1, `ERROR! ${  err}`); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| let progressCount = 1000; | ||||
| let count = 0; | ||||
|  | ||||
| function updateUsers (users) { | ||||
|   if (!users || users.length === 0) { | ||||
|     console.warn('All appropriate users found and modified.'); | ||||
|     displayData(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   let userPromises = users.map(updateUser); | ||||
|   let lastUser = users[users.length - 1]; | ||||
|  | ||||
|   return Promise.all(userPromises) | ||||
|     .then(() => { | ||||
|       processUsers(lastUser._id); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function updateUser (user) { | ||||
|   count++; | ||||
|  | ||||
|   const isFacebook = user.auth.facebook && user.auth.facebook.id; | ||||
|   const isGoogle = user.auth.google && user.auth.google.id; | ||||
|  | ||||
|   const update = { $set: {} }; | ||||
|  | ||||
|   if (isFacebook) { | ||||
|     update.$set['auth.facebook'] = { | ||||
|       id: user.auth.facebook.id, | ||||
|       emails: user.auth.facebook.emails, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   if (isGoogle) { | ||||
|     update.$set['auth.google'] = { | ||||
|       id: user.auth.google.id, | ||||
|       emails: user.auth.google.emails, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   dbUsers.update({ | ||||
|     _id: user._id, | ||||
|   }, update); | ||||
|  | ||||
|   if (count % progressCount === 0) console.warn(`${count  } ${  user._id}`); | ||||
|   if (user._id === authorUuid) console.warn(`${authorName  } processed`); | ||||
| } | ||||
|  | ||||
| function displayData () { | ||||
|   console.warn(`\n${  count  } users processed\n`); | ||||
|   return exiting(0); | ||||
| } | ||||
|  | ||||
| function exiting (code, msg) { | ||||
|   code = code || 0; // 0 = success | ||||
|   if (code && !msg) { | ||||
|     msg = 'ERROR!'; | ||||
|   } | ||||
|   if (msg) { | ||||
|     if (code) { | ||||
|       console.error(msg); | ||||
|     } else      { | ||||
|       console.log(msg); | ||||
|     } | ||||
|   } | ||||
|   process.exit(code); | ||||
| } | ||||
|  | ||||
| module.exports = processUsers; | ||||
							
								
								
									
										81
									
								
								migrations/users/take-this.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								migrations/users/take-this.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | ||||
| /* eslint-disable no-console */ | ||||
| const MIGRATION_NAME = '20181203_take_this'; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
|  | ||||
| import { model as User } from '../../website/server/models/user'; | ||||
|  | ||||
| const progressCount = 1000; | ||||
| let count = 0; | ||||
|  | ||||
| async function updateUser (user) { | ||||
|   count++; | ||||
|  | ||||
|   const set = {}; | ||||
|   let push; | ||||
|  | ||||
|   set.migration = MIGRATION_NAME; | ||||
|  | ||||
|   if (typeof user.items.gear.owned.back_special_takeThis !== 'undefined') { | ||||
|     push = false; | ||||
|   } else if (typeof user.items.gear.owned.body_special_takeThis !== 'undefined') { | ||||
|     set['items.gear.owned.back_special_takeThis'] = false; | ||||
|     push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.back_special_takeThis', _id: uuid()}}; | ||||
|   } else if (typeof user.items.gear.owned.head_special_takeThis !== 'undefined') { | ||||
|     set['items.gear.owned.body_special_takeThis'] = false; | ||||
|     push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.body_special_takeThis', _id: uuid()}}; | ||||
|   } else if (typeof user.items.gear.owned.armor_special_takeThis !== 'undefined') { | ||||
|     set['items.gear.owned.head_special_takeThis'] = false; | ||||
|     push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.head_special_takeThis', _id: uuid()}}; | ||||
|   } else if (typeof user.items.gear.owned.weapon_special_takeThis !== 'undefined') { | ||||
|     set['items.gear.owned.armor_special_takeThis'] = false; | ||||
|     push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.armor_special_takeThis', _id: uuid()}}; | ||||
|   } else if (typeof user.items.gear.owned.shield_special_takeThis !== 'undefined') { | ||||
|     set['items.gear.owned.weapon_special_takeThis'] = false; | ||||
|     push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.weapon_special_takeThis', _id: uuid()}}; | ||||
|   } else { | ||||
|     set['items.gear.owned.shield_special_takeThis'] = false; | ||||
|     push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.shield_special_takeThis', _id: uuid()}}; | ||||
|   } | ||||
|  | ||||
|   if (count % progressCount === 0) console.warn(`${count} ${user._id}`); | ||||
|  | ||||
|   if (push) { | ||||
|     return await User.update({_id: user._id}, {$set: set, $push: push}).exec(); | ||||
|   } else { | ||||
|     return await User.update({_id: user._id}, {$set: set}).exec(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| module.exports = async function processUsers () { | ||||
|   let query = { | ||||
|     migration: {$ne: MIGRATION_NAME}, | ||||
|     challenges: '00708425-d477-41a5-bf27-6270466e7976', | ||||
|   }; | ||||
|  | ||||
|   const fields = { | ||||
|     _id: 1, | ||||
|     items: 1, | ||||
|   }; | ||||
|  | ||||
|   while (true) { // eslint-disable-line no-constant-condition | ||||
|     const users = await User // eslint-disable-line no-await-in-loop | ||||
|       .find(query) | ||||
|       .limit(250) | ||||
|       .sort({_id: 1}) | ||||
|       .select(fields) | ||||
|       .lean() | ||||
|       .exec(); | ||||
|  | ||||
|     if (users.length === 0) { | ||||
|       console.warn('All appropriate users found and modified.'); | ||||
|       console.warn(`\n${count} users processed\n`); | ||||
|       break; | ||||
|     } else { | ||||
|       query._id = { | ||||
|         $gt: users[users.length - 1], | ||||
|       }; | ||||
|     } | ||||
|  | ||||
|     await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop | ||||
|   } | ||||
| }; | ||||
							
								
								
									
										25327
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										25327
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										147
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										147
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,129 +1,132 @@ | ||||
| { | ||||
|   "name": "habitica", | ||||
|   "description": "A habit tracker app which treats your goals like a Role Playing Game.", | ||||
|   "version": "4.35.1", | ||||
|   "version": "4.80.9", | ||||
|   "main": "./website/server/index.js", | ||||
|   "dependencies": { | ||||
|     "@slack/client": "^3.8.1", | ||||
|     "accepts": "^1.3.5", | ||||
|     "amazon-payments": "^0.2.6", | ||||
|     "amazon-payments": "^0.2.7", | ||||
|     "amplitude": "^3.5.0", | ||||
|     "apidoc": "^0.17.5", | ||||
|     "autoprefixer": "^8.1.0", | ||||
|     "aws-sdk": "^2.211.0", | ||||
|     "apn": "^2.2.0", | ||||
|     "autoprefixer": "^8.5.0", | ||||
|     "aws-sdk": "^2.329.0", | ||||
|     "axios": "^0.18.0", | ||||
|     "axios-progress-bar": "^1.1.8", | ||||
|     "babel-core": "^6.0.0", | ||||
|     "babel-eslint": "^8.2.2", | ||||
|     "axios-progress-bar": "^1.2.0", | ||||
|     "babel-core": "^6.26.3", | ||||
|     "babel-eslint": "^8.2.3", | ||||
|     "babel-loader": "^7.1.4", | ||||
|     "babel-plugin-syntax-async-functions": "^6.13.0", | ||||
|     "babel-plugin-syntax-dynamic-import": "^6.18.0", | ||||
|     "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0", | ||||
|     "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", | ||||
|     "babel-plugin-transform-object-rest-spread": "^6.16.0", | ||||
|     "babel-plugin-transform-regenerator": "^6.16.1", | ||||
|     "babel-polyfill": "^6.6.1", | ||||
|     "babel-preset-es2015": "^6.6.0", | ||||
|     "babel-register": "^6.6.0", | ||||
|     "babel-runtime": "^6.11.6", | ||||
|     "bcrypt": "^1.0.2", | ||||
|     "body-parser": "^1.15.0", | ||||
|     "bootstrap": "^4.0.0", | ||||
|     "bootstrap-vue": "^2.0.0-rc.2", | ||||
|     "bcrypt": "^3.0.1", | ||||
|     "body-parser": "^1.18.3", | ||||
|     "bootstrap": "^4.1.1", | ||||
|     "bootstrap-vue": "^2.0.0-rc.9", | ||||
|     "compression": "^1.7.2", | ||||
|     "cookie-session": "^1.2.0", | ||||
|     "coupon-code": "^0.4.5", | ||||
|     "cross-env": "^5.1.4", | ||||
|     "cross-env": "^5.2.0", | ||||
|     "css-loader": "^0.28.11", | ||||
|     "csv-stringify": "^2.0.4", | ||||
|     "csv-stringify": "^4.3.1", | ||||
|     "cwait": "^1.1.1", | ||||
|     "domain-middleware": "~0.1.0", | ||||
|     "express": "^4.16.3", | ||||
|     "express-basic-auth": "^1.1.4", | ||||
|     "express-validator": "^5.0.3", | ||||
|     "express-basic-auth": "^1.1.5", | ||||
|     "express-validator": "^5.2.0", | ||||
|     "extract-text-webpack-plugin": "^3.0.2", | ||||
|     "glob": "^7.1.2", | ||||
|     "got": "^8.3.0", | ||||
|     "got": "^9.0.0", | ||||
|     "gulp": "^4.0.0", | ||||
|     "gulp-babel": "^7.0.1", | ||||
|     "gulp-imagemin": "^4.1.0", | ||||
|     "gulp-nodemon": "^2.2.1", | ||||
|     "gulp-imagemin": "^5.0.3", | ||||
|     "gulp-nodemon": "^2.4.1", | ||||
|     "gulp.spritesmith": "^6.9.0", | ||||
|     "habitica-markdown": "^1.3.0", | ||||
|     "hellojs": "^1.15.1", | ||||
|     "html-webpack-plugin": "^3.0.0", | ||||
|     "html-webpack-plugin": "^3.2.0", | ||||
|     "image-size": "^0.6.2", | ||||
|     "in-app-purchase": "^1.8.9", | ||||
|     "intro.js": "^2.6.0", | ||||
|     "in-app-purchase": "^1.10.2", | ||||
|     "intro.js": "^2.9.3", | ||||
|     "jquery": ">=3.0.0", | ||||
|     "js2xmlparser": "^3.0.0", | ||||
|     "lodash": "^4.17.4", | ||||
|     "memwatch-next": "^0.3.0", | ||||
|     "lodash": "^4.17.10", | ||||
|     "merge-stream": "^1.0.0", | ||||
|     "method-override": "^2.3.5", | ||||
|     "moment": "^2.21.0", | ||||
|     "method-override": "^3.0.0", | ||||
|     "moment": "^2.22.1", | ||||
|     "moment-recur": "^1.0.7", | ||||
|     "mongoose": "^5.0.10", | ||||
|     "mongoose": "^5.3.4", | ||||
|     "morgan": "^1.7.0", | ||||
|     "nconf": "^0.10.0", | ||||
|     "node-gcm": "^0.14.4", | ||||
|     "node-sass": "^4.8.2", | ||||
|     "nodemailer": "^4.6.3", | ||||
|     "ora": "^2.0.0", | ||||
|     "node-gcm": "^1.0.2", | ||||
|     "node-sass": "^4.9.0", | ||||
|     "nodemailer": "^4.6.4", | ||||
|     "ora": "^3.0.0", | ||||
|     "pageres": "^4.1.1", | ||||
|     "passport": "^0.4.0", | ||||
|     "passport-facebook": "^2.0.0", | ||||
|     "passport-google-oauth20": "1.0.0", | ||||
|     "paypal-ipn": "3.0.0", | ||||
|     "paypal-rest-sdk": "^1.8.1", | ||||
|     "popper.js": "^1.14.1", | ||||
|     "popper.js": "^1.14.3", | ||||
|     "postcss-easy-import": "^3.0.0", | ||||
|     "ps-tree": "^1.0.0", | ||||
|     "pug": "^2.0.1", | ||||
|     "push-notify": "git://github.com/habitrpg/push-notify.git#6bc2b5fdb1bdc9649b9ec1964d79ca50187fc8a9", | ||||
|     "pusher": "^1.3.0", | ||||
|     "pug": "^2.0.3", | ||||
|     "rimraf": "^2.4.3", | ||||
|     "sass-loader": "^6.0.7", | ||||
|     "shelljs": "^0.8.1", | ||||
|     "stackimpact": "^1.2.1", | ||||
|     "stripe": "^5.5.0", | ||||
|     "superagent": "^3.4.3", | ||||
|     "sass-loader": "^7.0.3", | ||||
|     "shelljs": "^0.8.2", | ||||
|     "short-uuid": "^3.0.0", | ||||
|     "smartbanner.js": "^1.9.1", | ||||
|     "stripe": "^5.9.0", | ||||
|     "superagent": "^4.0.0", | ||||
|     "svg-inline-loader": "^0.8.0", | ||||
|     "svg-url-loader": "^2.3.2", | ||||
|     "svgo": "^1.0.5", | ||||
|     "svgo-loader": "^2.1.0", | ||||
|     "universal-analytics": "^0.4.16", | ||||
|     "universal-analytics": "^0.4.17", | ||||
|     "update": "^0.7.4", | ||||
|     "upgrade": "^1.1.0", | ||||
|     "url-loader": "^1.0.0", | ||||
|     "useragent": "^2.1.9", | ||||
|     "uuid": "^3.0.1", | ||||
|     "validator": "^9.4.1", | ||||
|     "validator": "^10.5.0", | ||||
|     "vinyl-buffer": "^1.0.1", | ||||
|     "vue": "^2.5.16", | ||||
|     "vue-loader": "^14.2.1", | ||||
|     "vue-loader": "^14.2.2", | ||||
|     "vue-mugen-scroll": "^0.2.1", | ||||
|     "vue-router": "^3.0.0", | ||||
|     "vue-style-loader": "^4.0.2", | ||||
|     "vue-style-loader": "^4.1.0", | ||||
|     "vue-template-compiler": "^2.5.16", | ||||
|     "vuedraggable": "^2.15.0", | ||||
|     "vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#5d237615463a84a23dd6f3f77c6ab577d68593ec", | ||||
|     "webpack": "^3.11.0", | ||||
|     "webpack-merge": "^4.0.0", | ||||
|     "winston": "^2.4.1", | ||||
|     "webpack": "^3.12.0", | ||||
|     "webpack-merge": "^4.1.3", | ||||
|     "winston": "^2.4.3", | ||||
|     "winston-loggly-bulk": "^2.0.2", | ||||
|     "xml2js": "^0.4.4" | ||||
|   }, | ||||
|   "private": true, | ||||
|   "engines": { | ||||
|     "node": "^8.9.4", | ||||
|     "npm": "^5.6.0" | ||||
|     "node": "^10", | ||||
|     "npm": "^6" | ||||
|   }, | ||||
|   "scripts": { | ||||
|     "lint": "eslint --ext .js,.vue .", | ||||
|     "test": "npm run lint && gulp test && gulp apidoc", | ||||
|     "test:build": "gulp test:prepare:build", | ||||
|     "test:api-v3": "gulp test:api-v3", | ||||
|     "test:api-v3:unit": "gulp test:api-v3:unit", | ||||
|     "test:api:unit": "gulp test:api:unit", | ||||
|     "test:api-v3:integration": "gulp test:api-v3:integration", | ||||
|     "test:api-v3:integration:separate-server": "NODE_ENV=test gulp test:api-v3:integration:separate-server", | ||||
|     "test:api-v4:integration": "gulp test:api-v4:integration", | ||||
|     "test:api-v4:integration:separate-server": "NODE_ENV=test gulp test:api-v4:integration:separate-server", | ||||
|     "test:sanity": "istanbul cover --dir coverage/sanity --report lcovonly node_modules/mocha/bin/_mocha -- test/sanity --recursive", | ||||
|     "test:common": "istanbul cover --dir coverage/common --report lcovonly node_modules/mocha/bin/_mocha -- test/common --recursive", | ||||
|     "test:content": "istanbul cover --dir coverage/content --report lcovonly node_modules/mocha/bin/_mocha -- test/content --recursive", | ||||
| @@ -141,53 +144,51 @@ | ||||
|     "apidoc": "gulp apidoc" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@vue/test-utils": "^1.0.0-beta.12", | ||||
|     "@vue/test-utils": "^1.0.0-beta.19", | ||||
|     "babel-plugin-istanbul": "^4.1.6", | ||||
|     "babel-plugin-syntax-object-rest-spread": "^6.13.0", | ||||
|     "chai": "^4.1.2", | ||||
|     "chai-as-promised": "^7.1.1", | ||||
|     "chalk": "^2.3.2", | ||||
|     "chromedriver": "^2.36.0", | ||||
|     "chalk": "^2.4.1", | ||||
|     "chromedriver": "^2.40.0", | ||||
|     "connect-history-api-fallback": "^1.1.0", | ||||
|     "coveralls": "^3.0.0", | ||||
|     "coveralls": "^3.0.1", | ||||
|     "cross-spawn": "^6.0.5", | ||||
|     "eslint": "^4.19.0", | ||||
|     "eslint": "^4.19.1", | ||||
|     "eslint-config-habitrpg": "^4.0.0", | ||||
|     "eslint-friendly-formatter": "^4.0.0", | ||||
|     "eslint-friendly-formatter": "^4.0.1", | ||||
|     "eslint-loader": "^2.0.0", | ||||
|     "eslint-plugin-html": "^4.0.2", | ||||
|     "eslint-plugin-html": "^4.0.3", | ||||
|     "eslint-plugin-mocha": "^5.0.0", | ||||
|     "eventsource-polyfill": "^0.9.6", | ||||
|     "expect.js": "^0.3.1", | ||||
|     "http-proxy-middleware": "^0.18.0", | ||||
|     "http-proxy-middleware": "^0.19.0", | ||||
|     "istanbul": "^1.1.0-alpha.1", | ||||
|     "karma": "^2.0.0", | ||||
|     "karma": "^3.1.3", | ||||
|     "karma-babel-preprocessor": "^7.0.0", | ||||
|     "karma-chai-plugins": "^0.9.0", | ||||
|     "karma-chrome-launcher": "^2.2.0", | ||||
|     "karma-coverage": "^1.1.1", | ||||
|     "karma-coverage": "^1.1.2", | ||||
|     "karma-mocha": "^1.3.0", | ||||
|     "karma-mocha-reporter": "^2.2.5", | ||||
|     "karma-sinon-chai": "^1.3.3", | ||||
|     "karma-sinon-chai": "^2.0.0", | ||||
|     "karma-sinon-stub-promise": "^1.0.0", | ||||
|     "karma-sourcemap-loader": "^0.3.7", | ||||
|     "karma-spec-reporter": "0.0.32", | ||||
|     "karma-webpack": "^3.0.0", | ||||
|     "lcov-result-merger": "^2.0.0", | ||||
|     "mocha": "^5.0.4", | ||||
|     "monk": "^6.0.5", | ||||
|     "nightwatch": "^0.9.20", | ||||
|     "puppeteer": "^1.2.0", | ||||
|     "lcov-result-merger": "^3.0.0", | ||||
|     "mocha": "^5.1.1", | ||||
|     "monk": "^6.0.6", | ||||
|     "nightwatch": "^0.9.21", | ||||
|     "puppeteer": "^1.5.0", | ||||
|     "require-again": "^2.0.0", | ||||
|     "selenium-server": "^3.11.0", | ||||
|     "sinon": "^4.4.5", | ||||
|     "selenium-server": "^3.12.0", | ||||
|     "sinon": "^6.3.5", | ||||
|     "sinon-chai": "^3.0.0", | ||||
|     "sinon-stub-promise": "^4.0.0", | ||||
|     "webpack-bundle-analyzer": "^2.11.1", | ||||
|     "webpack-bundle-analyzer": "^2.12.0", | ||||
|     "webpack-dev-middleware": "^2.0.5", | ||||
|     "webpack-hot-middleware": "^2.21.2" | ||||
|     "webpack-hot-middleware": "^2.22.2" | ||||
|   }, | ||||
|   "optionalDependencies": { | ||||
|     "node-rdkafka": "^2.3.0" | ||||
|   } | ||||
|   "optionalDependencies": {} | ||||
| } | ||||
|   | ||||
							
								
								
									
										89
									
								
								scripts/gdpr-delete-users.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								scripts/gdpr-delete-users.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,89 @@ | ||||
| /* eslint-disable no-console */ | ||||
| import axios from 'axios'; | ||||
| import { model as User } from '../website/server/models/user'; | ||||
| import nconf from 'nconf'; | ||||
|  | ||||
| const AMPLITUDE_KEY = nconf.get('AMPLITUDE_KEY'); | ||||
| const AMPLITUDE_SECRET = nconf.get('AMPLITUDE_SECRET'); | ||||
| const BASE_URL = nconf.get('BASE_URL'); | ||||
|  | ||||
| async function _deleteAmplitudeData (userId, email) { | ||||
|   const response = await axios.post( | ||||
|     'https://amplitude.com/api/2/deletions/users', | ||||
|     { | ||||
|       user_ids: userId, // eslint-disable-line camelcase | ||||
|       requester: email, | ||||
|     }, | ||||
|     { | ||||
|       auth: { | ||||
|         username: AMPLITUDE_KEY, | ||||
|         password: AMPLITUDE_SECRET, | ||||
|       }, | ||||
|     } | ||||
|   ).catch((err) => { | ||||
|     console.log(err.response.data); | ||||
|   }); | ||||
|  | ||||
|   if (response) console.log(`${response.status} ${response.statusText}`); | ||||
| } | ||||
|  | ||||
| async function _deleteHabiticaData (user, email) { | ||||
|   await User.update( | ||||
|     {_id: user._id}, | ||||
|     {$set: { | ||||
|       'auth.local.email': email, | ||||
|       'auth.local.hashed_password': '$2a$10$QDnNh1j1yMPnTXDEOV38xOePEWFd4X8DSYwAM8XTmqmacG5X0DKjW', | ||||
|       'auth.local.passwordHashMethod': 'bcrypt', | ||||
|     }} | ||||
|   ); | ||||
|   const response = await axios.delete( | ||||
|     `${BASE_URL}/api/v3/user`, | ||||
|     { | ||||
|       data: { | ||||
|         password: 'test', | ||||
|       }, | ||||
|       headers: { | ||||
|         'x-api-user': user._id, | ||||
|         'x-api-key': user.apiToken, | ||||
|       }, | ||||
|     } | ||||
|   ).catch((err) => { | ||||
|     console.log(err.response.data); | ||||
|   }); | ||||
|  | ||||
|   if (response) { | ||||
|     console.log(`${response.status} ${response.statusText}`); | ||||
|     if (response.status === 200) console.log(`${user._id} removed. Last login: ${user.auth.timestamps.loggedin}`); | ||||
|   } | ||||
| } | ||||
|  | ||||
| async function _processEmailAddress (email) { | ||||
|   const emailRegex = new RegExp(`^${email}`, 'i'); | ||||
|   const users = await User.find({ | ||||
|     $or: [ | ||||
|       {'auth.local.email': emailRegex}, | ||||
|       {'auth.facebook.emails.value': emailRegex}, | ||||
|       {'auth.google.emails.value': emailRegex}, | ||||
|     ]}, | ||||
|   { | ||||
|     _id: 1, | ||||
|     apiToken: 1, | ||||
|     auth: 1, | ||||
|   }).exec(); | ||||
|  | ||||
|   if (users.length < 1) { | ||||
|     console.log(`No users found with email address ${email}`); | ||||
|   } else { | ||||
|     for (const user of users) { | ||||
|       await _deleteAmplitudeData(user._id, email); // eslint-disable-line no-await-in-loop | ||||
|       await _deleteHabiticaData(user, email); // eslint-disable-line no-await-in-loop | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| function deleteUserData (emails) { | ||||
|   const emailPromises = emails.map(_processEmailAddress); | ||||
|   return Promise.all(emailPromises); | ||||
| } | ||||
|  | ||||
| module.exports = deleteUserData; | ||||
| @@ -12,28 +12,27 @@ const nconf = require('nconf'); | ||||
| const _ = require('lodash'); | ||||
| const paypal = require('paypal-rest-sdk'); | ||||
| const blocks = require('../website/common').content.subscriptionBlocks; | ||||
| const live = nconf.get('PAYPAL:mode') === 'live'; | ||||
| const live = nconf.get('PAYPAL_MODE') === 'live'; | ||||
|  | ||||
| nconf.argv().env().file('user', path.join(path.resolve(__dirname, '../config.json'))); | ||||
|  | ||||
| let OP = 'create'; // list create update remove | ||||
| let OP = 'create'; // list get update create create-webprofile | ||||
|  | ||||
| paypal.configure({ | ||||
|   mode: nconf.get('PAYPAL:mode'), // sandbox or live | ||||
|   client_id: nconf.get('PAYPAL:client_id'), | ||||
|   client_secret: nconf.get('PAYPAL:client_secret'), | ||||
|   mode: nconf.get('PAYPAL_MODE'), // sandbox or live | ||||
|   client_id: nconf.get('PAYPAL_CLIENT_ID'), | ||||
|   client_secret: nconf.get('PAYPAL_CLIENT_SECRET'), | ||||
| }); | ||||
|  | ||||
| // https://developer.paypal.com/docs/api/#billing-plans-and-agreements | ||||
| let billingPlanTitle = 'Habitica Subscription'; | ||||
| let billingPlanAttributes = { | ||||
|   name: billingPlanTitle, | ||||
|   description: billingPlanTitle, | ||||
|   type: 'INFINITE', | ||||
|   merchant_preferences: { | ||||
|     auto_bill_amount: 'yes', | ||||
|     cancel_url: live ? 'https://habitica.com' : 'http://localhost:3000', | ||||
|     return_url: `${live ? 'https://habitica.com' : 'http://localhost:3000'  }/paypal/subscribe/success`, | ||||
|     return_url: `${live ? 'https://habitica.com' : 'http://localhost:3000'}/paypal/subscribe/success`, | ||||
|   }, | ||||
|   payment_definitions: [{ | ||||
|     type: 'REGULAR', | ||||
| @@ -45,7 +44,7 @@ let billingPlanAttributes = { | ||||
| _.each(blocks, (block) => { | ||||
|   block.definition = _.cloneDeep(billingPlanAttributes); | ||||
|   _.merge(block.definition.payment_definitions[0], { | ||||
|     name: `${billingPlanTitle  } ($${block.price} every ${block.months} months, recurring)`, | ||||
|     name: `${billingPlanTitle} ($${block.price} every ${block.months} months, recurring)`, | ||||
|     frequency_interval: `${block.months}`, | ||||
|     amount: { | ||||
|       currency: 'USD', | ||||
| @@ -63,7 +62,7 @@ switch (OP) { | ||||
|     }); | ||||
|     break; | ||||
|   case 'get': | ||||
|     paypal.billingPlan.get(nconf.get('PAYPAL:billing_plans:12'), (err, plan) => { | ||||
|     paypal.billingPlan.get(nconf.get('PAYPAL_BILLING_PLANS_basic_12mo'), (err, plan) => { | ||||
|       console.log({err, plan}); | ||||
|     }); | ||||
|     break; | ||||
| @@ -75,7 +74,7 @@ switch (OP) { | ||||
|         cancel_url: 'https://habitica.com', | ||||
|       }, | ||||
|     }; | ||||
|     paypal.billingPlan.update(nconf.get('PAYPAL:billing_plans:12'), updatePayload, (err, res) => { | ||||
|     paypal.billingPlan.update(nconf.get('PAYPAL_BILLING_PLANS_basic_12mo'), updatePayload, (err, res) => { | ||||
|       console.log({err, plan: res}); | ||||
|     }); | ||||
|     break; | ||||
| @@ -101,9 +100,6 @@ switch (OP) { | ||||
|       }); | ||||
|     }); | ||||
|     break; | ||||
|  | ||||
|   case 'remove': break; | ||||
|  | ||||
|   case 'create-webprofile': | ||||
|     let webexpinfo = { | ||||
|       name: 'HabiticaProfile', | ||||
| @@ -116,4 +112,4 @@ switch (OP) { | ||||
|       console.log(error, result); | ||||
|     }); | ||||
|     break; | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| /* eslint-disable camelcase */ | ||||
| import analyticsService from '../../../../../website/server/libs/analyticsService'; | ||||
| import analyticsService from '../../../../website/server/libs/analyticsService'; | ||||
| import Amplitude from 'amplitude'; | ||||
| import { Visitor } from 'universal-analytics'; | ||||
| 
 | ||||
| @@ -1,19 +1,19 @@ | ||||
| import apiMessages from '../../../../../website/server/libs/apiMessages'; | ||||
| import apiError from '../../../../website/server/libs/apiError'; | ||||
| 
 | ||||
| describe('API Messages', () => { | ||||
|   const message = 'Only public guilds support pagination.'; | ||||
|   it('returns an API message', () => { | ||||
|     expect(apiMessages('guildsOnlyPaginate')).to.equal(message); | ||||
|     expect(apiError('guildsOnlyPaginate')).to.equal(message); | ||||
|   }); | ||||
| 
 | ||||
|   it('throws if the API message does not exist', () => { | ||||
|     expect(() => apiMessages('iDoNotExist')).to.throw; | ||||
|     expect(() => apiError('iDoNotExist')).to.throw; | ||||
|   }); | ||||
| 
 | ||||
|   it('clones the passed variables', () => { | ||||
|     let vars = {a: 1}; | ||||
|     sandbox.stub(_, 'clone').returns({}); | ||||
|     apiMessages('guildsOnlyPaginate', vars); | ||||
|     apiError('guildsOnlyPaginate', vars); | ||||
|     expect(_.clone).to.have.been.calledOnce; | ||||
|     expect(_.clone).to.have.been.calledWith(vars); | ||||
|   }); | ||||
| @@ -22,7 +22,7 @@ describe('API Messages', () => { | ||||
|     let vars = {a: 1}; | ||||
|     let stub = sinon.stub().returns('string'); | ||||
|     sandbox.stub(_, 'template').returns(stub); | ||||
|     apiMessages('guildsOnlyPaginate', vars); | ||||
|     apiError('guildsOnlyPaginate', vars); | ||||
|     expect(_.template).to.have.been.calledOnce; | ||||
|     expect(_.template).to.have.been.calledWith(message); | ||||
|     expect(stub).to.have.been.calledOnce; | ||||
| @@ -1,14 +1,21 @@ | ||||
| import baseModel from '../../../../../website/server/libs/baseModel'; | ||||
| import baseModel from '../../../../website/server/libs/baseModel'; | ||||
| import mongoose from 'mongoose'; | ||||
| 
 | ||||
| describe('Base model plugin', () => { | ||||
|   let schema; | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     schema = new mongoose.Schema(); | ||||
|     schema = new mongoose.Schema({}, { | ||||
|       typeKey: '$type', | ||||
|     }); | ||||
|     sandbox.stub(schema, 'add'); | ||||
|   }); | ||||
| 
 | ||||
|   it('throws if "typeKey" is not set to $type', () => { | ||||
|     const schemaWithoutTypeKey = new mongoose.Schema(); | ||||
|     expect(() => schemaWithoutTypeKey.plugin(baseModel)).to.throw; | ||||
|   }); | ||||
| 
 | ||||
|   it('adds a _id field to the schema', () => { | ||||
|     schema.plugin(baseModel); | ||||
| 
 | ||||
| @@ -1,7 +1,7 @@ | ||||
| import mongoose from 'mongoose'; | ||||
| import { | ||||
|   removeFromArray, | ||||
| } from '../../../../../website/server/libs/collectionManipulators'; | ||||
| } from '../../../../website/server/libs/collectionManipulators'; | ||||
| 
 | ||||
| describe('Collection Manipulators', () => { | ||||
|   describe('removeFromArray', () => { | ||||
| @@ -2,17 +2,18 @@ | ||||
| import moment from 'moment'; | ||||
| import nconf from 'nconf'; | ||||
| import requireAgain from 'require-again'; | ||||
| import { recoverCron, cron } from '../../../../../website/server/libs/cron'; | ||||
| import { model as User } from '../../../../../website/server/models/user'; | ||||
| import * as Tasks from '../../../../../website/server/models/task'; | ||||
| import common from '../../../../../website/common'; | ||||
| import analytics from '../../../../../website/server/libs/analyticsService'; | ||||
| import { recoverCron, cron } from '../../../../website/server/libs/cron'; | ||||
| import { model as User } from '../../../../website/server/models/user'; | ||||
| import * as Tasks from '../../../../website/server/models/task'; | ||||
| import common from '../../../../website/common'; | ||||
| import analytics from '../../../../website/server/libs/analyticsService'; | ||||
| 
 | ||||
| // const scoreTask = common.ops.scoreTask;
 | ||||
| 
 | ||||
| let pathToCronLib = '../../../../../website/server/libs/cron'; | ||||
| let pathToCronLib = '../../../../website/server/libs/cron'; | ||||
| 
 | ||||
| describe('cron', () => { | ||||
|   let clock = null; | ||||
|   let user; | ||||
|   let tasksByType = {habits: [], dailys: [], todos: [], rewards: []}; | ||||
|   let daysMissed = 0; | ||||
| @@ -23,7 +24,7 @@ describe('cron', () => { | ||||
|         local: { | ||||
|           username: 'username', | ||||
|           lowerCaseUsername: 'username', | ||||
|           email: 'email@email.email', | ||||
|           email: 'email@example.com', | ||||
|           salt: 'salt', | ||||
|           hashed_password: 'hashed_password', // eslint-disable-line camelcase
 | ||||
|         }, | ||||
| @@ -34,6 +35,8 @@ describe('cron', () => { | ||||
|   }); | ||||
| 
 | ||||
|   afterEach(() => { | ||||
|     if (clock !== null) | ||||
|       clock.restore(); | ||||
|     analytics.track.restore(); | ||||
|   }); | ||||
| 
 | ||||
| @@ -62,6 +65,12 @@ describe('cron', () => { | ||||
|     expect(analytics.track.callCount).to.equal(1); | ||||
|   }); | ||||
| 
 | ||||
|   it('calls analytics when user is sleeping', () => { | ||||
|     user.preferences.sleep = true; | ||||
|     cron({user, tasksByType, daysMissed, analytics}); | ||||
|     expect(analytics.track.callCount).to.equal(1); | ||||
|   }); | ||||
| 
 | ||||
|   describe('end of the month perks', () => { | ||||
|     beforeEach(() => { | ||||
|       user.purchased.plan.customerId = 'subscribedId'; | ||||
| @@ -82,14 +91,12 @@ describe('cron', () => { | ||||
|     }); | ||||
| 
 | ||||
|     it('does not reset plan.gemsBought within the month', () => { | ||||
|       let clock = sinon.useFakeTimers(moment().startOf('month').add(2, 'days').unix()); | ||||
|       clock = sinon.useFakeTimers(moment().startOf('month').add(2, 'days').toDate()); | ||||
|       user.purchased.plan.dateUpdated = moment().startOf('month').toDate(); | ||||
| 
 | ||||
|       user.purchased.plan.gemsBought = 10; | ||||
|       cron({user, tasksByType, daysMissed, analytics}); | ||||
|       expect(user.purchased.plan.gemsBought).to.equal(10); | ||||
| 
 | ||||
|       clock.restore(); | ||||
|     }); | ||||
| 
 | ||||
|     it('resets plan.dateUpdated on a new month', () => { | ||||
| @@ -117,21 +124,6 @@ describe('cron', () => { | ||||
|       expect(user.purchased.plan.consecutive.offset).to.equal(1); | ||||
|     }); | ||||
| 
 | ||||
|     it('increments plan.consecutive.trinkets when user has reached a month that is a multiple of 3', () => { | ||||
|       user.purchased.plan.consecutive.count = 5; | ||||
|       user.purchased.plan.consecutive.offset = 1; | ||||
|       cron({user, tasksByType, daysMissed, analytics}); | ||||
|       expect(user.purchased.plan.consecutive.trinkets).to.equal(1); | ||||
|       expect(user.purchased.plan.consecutive.offset).to.equal(0); | ||||
|     }); | ||||
| 
 | ||||
|     it('increments plan.consecutive.trinkets multiple times if user has been absent with continuous subscription', () => { | ||||
|       user.purchased.plan.dateUpdated = moment().subtract(6, 'months').toDate(); | ||||
|       user.purchased.plan.consecutive.count = 5; | ||||
|       cron({user, tasksByType, daysMissed, analytics}); | ||||
|       expect(user.purchased.plan.consecutive.trinkets).to.equal(2); | ||||
|     }); | ||||
| 
 | ||||
|     it('does not award unearned plan.consecutive.trinkets if subscription ended during an absence', () => { | ||||
|       user.purchased.plan.dateUpdated = moment().subtract(6, 'months').toDate(); | ||||
|       user.purchased.plan.dateTerminated = moment().subtract(3, 'months').toDate(); | ||||
| @@ -143,21 +135,6 @@ describe('cron', () => { | ||||
|       expect(user.purchased.plan.consecutive.trinkets).to.equal(1); | ||||
|     }); | ||||
| 
 | ||||
|     it('increments plan.consecutive.gemCapExtra when user has reached a month that is a multiple of 3', () => { | ||||
|       user.purchased.plan.consecutive.count = 5; | ||||
|       user.purchased.plan.consecutive.offset = 1; | ||||
|       cron({user, tasksByType, daysMissed, analytics}); | ||||
|       expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(5); | ||||
|       expect(user.purchased.plan.consecutive.offset).to.equal(0); | ||||
|     }); | ||||
| 
 | ||||
|     it('increments plan.consecutive.gemCapExtra multiple times if user has been absent with continuous subscription', () => { | ||||
|       user.purchased.plan.dateUpdated = moment().subtract(6, 'months').toDate(); | ||||
|       user.purchased.plan.consecutive.count = 5; | ||||
|       cron({user, tasksByType, daysMissed, analytics}); | ||||
|       expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(10); | ||||
|     }); | ||||
| 
 | ||||
|     it('does not increment plan.consecutive.gemCapExtra when user has reached the gemCap limit', () => { | ||||
|       user.purchased.plan.consecutive.gemCapExtra = 25; | ||||
|       user.purchased.plan.consecutive.count = 5; | ||||
| @@ -184,6 +161,427 @@ describe('cron', () => { | ||||
|       expect(user.purchased.plan.consecutive.count).to.equal(0); | ||||
|       expect(user.purchased.plan.consecutive.offset).to.equal(0); | ||||
|     }); | ||||
| 
 | ||||
|     describe('for a 1-month recurring subscription', () => { | ||||
|       // create a user that will be used for all of these tests without a reset before each
 | ||||
|       let 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
 | ||||
|       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.offset = 0; | ||||
|       user1.purchased.plan.consecutive.trinkets = 0; | ||||
|       user1.purchased.plan.consecutive.gemCapExtra = 0; | ||||
| 
 | ||||
|       it('does not increment consecutive benefits after the first month', () => { | ||||
|         clock = sinon.useFakeTimers(moment().zone(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.
 | ||||
|         cron({user: user1, tasksByType, daysMissed, analytics}); | ||||
|         expect(user1.purchased.plan.consecutive.count).to.equal(1); | ||||
|         expect(user1.purchased.plan.consecutive.offset).to.equal(0); | ||||
|         expect(user1.purchased.plan.consecutive.trinkets).to.equal(0); | ||||
|         expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(0); | ||||
|       }); | ||||
| 
 | ||||
|       it('does not increment consecutive benefits after the second month', () => { | ||||
|         clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, '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.
 | ||||
|         cron({user: user1, tasksByType, daysMissed, analytics}); | ||||
|         expect(user1.purchased.plan.consecutive.count).to.equal(2); | ||||
|         expect(user1.purchased.plan.consecutive.offset).to.equal(0); | ||||
|         expect(user1.purchased.plan.consecutive.trinkets).to.equal(0); | ||||
|         expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(0); | ||||
|       }); | ||||
| 
 | ||||
|       it('increments consecutive benefits after the third month', () => { | ||||
|         clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, '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.
 | ||||
|         cron({user: user1, tasksByType, daysMissed, analytics}); | ||||
|         expect(user1.purchased.plan.consecutive.count).to.equal(3); | ||||
|         expect(user1.purchased.plan.consecutive.offset).to.equal(0); | ||||
|         expect(user1.purchased.plan.consecutive.trinkets).to.equal(1); | ||||
|         expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(5); | ||||
|       }); | ||||
| 
 | ||||
|       it('does not increment consecutive benefits after the fourth month', () => { | ||||
|         clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(4, '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.
 | ||||
|         cron({user: user1, tasksByType, daysMissed, analytics}); | ||||
|         expect(user1.purchased.plan.consecutive.count).to.equal(4); | ||||
|         expect(user1.purchased.plan.consecutive.offset).to.equal(0); | ||||
|         expect(user1.purchased.plan.consecutive.trinkets).to.equal(1); | ||||
|         expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(5); | ||||
|       }); | ||||
| 
 | ||||
|       it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => { | ||||
|         clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(10, 'months').add(2, 'days').toDate()); | ||||
|         cron({user: user1, tasksByType, daysMissed, analytics}); | ||||
|         expect(user1.purchased.plan.consecutive.count).to.equal(10); | ||||
|         expect(user1.purchased.plan.consecutive.offset).to.equal(0); | ||||
|         expect(user1.purchased.plan.consecutive.trinkets).to.equal(3); | ||||
|         expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(15); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('for a 3-month recurring subscription', () => { | ||||
|       let 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
 | ||||
|       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.offset = 3; | ||||
|       user3.purchased.plan.consecutive.trinkets = 1; | ||||
|       user3.purchased.plan.consecutive.gemCapExtra = 5; | ||||
| 
 | ||||
|       it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', () => { | ||||
|         clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months').add(2, 'days').toDate()); | ||||
|         cron({user: user3, tasksByType, daysMissed, analytics}); | ||||
|         expect(user3.purchased.plan.consecutive.count).to.equal(1); | ||||
|         expect(user3.purchased.plan.consecutive.offset).to.equal(2); | ||||
|         expect(user3.purchased.plan.consecutive.trinkets).to.equal(1); | ||||
|         expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(5); | ||||
|       }); | ||||
| 
 | ||||
|       it('does not increment consecutive benefits in the middle of the period that they already have benefits for', () => { | ||||
|         clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months').add(2, 'days').toDate()); | ||||
|         cron({user: user3, tasksByType, daysMissed, analytics}); | ||||
|         expect(user3.purchased.plan.consecutive.count).to.equal(2); | ||||
|         expect(user3.purchased.plan.consecutive.offset).to.equal(1); | ||||
|         expect(user3.purchased.plan.consecutive.trinkets).to.equal(1); | ||||
|         expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(5); | ||||
|       }); | ||||
| 
 | ||||
|       it('does not increment consecutive benefits in the final month of the period that they already have benefits for', () => { | ||||
|         clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months').add(2, 'days').toDate()); | ||||
|         cron({user: user3, tasksByType, daysMissed, analytics}); | ||||
|         expect(user3.purchased.plan.consecutive.count).to.equal(3); | ||||
|         expect(user3.purchased.plan.consecutive.offset).to.equal(0); | ||||
|         expect(user3.purchased.plan.consecutive.trinkets).to.equal(1); | ||||
|         expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(5); | ||||
|       }); | ||||
| 
 | ||||
|       it('increments consecutive benefits the month after the second paid period has started', () => { | ||||
|         clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(4, 'months').add(2, 'days').toDate()); | ||||
|         cron({user: user3, tasksByType, daysMissed, analytics}); | ||||
|         expect(user3.purchased.plan.consecutive.count).to.equal(4); | ||||
|         expect(user3.purchased.plan.consecutive.offset).to.equal(2); | ||||
|         expect(user3.purchased.plan.consecutive.trinkets).to.equal(2); | ||||
|         expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(10); | ||||
|       }); | ||||
| 
 | ||||
|       it('does not increment consecutive benefits in the second month of the second period that they already have benefits for', () => { | ||||
|         clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(5, 'months').add(2, 'days').toDate()); | ||||
|         cron({user: user3, tasksByType, daysMissed, analytics}); | ||||
|         expect(user3.purchased.plan.consecutive.count).to.equal(5); | ||||
|         expect(user3.purchased.plan.consecutive.offset).to.equal(1); | ||||
|         expect(user3.purchased.plan.consecutive.trinkets).to.equal(2); | ||||
|         expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(10); | ||||
|       }); | ||||
| 
 | ||||
|       it('does not increment consecutive benefits in the final month of the second period that they already have benefits for', () => { | ||||
|         clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(6, 'months').add(2, 'days').toDate()); | ||||
|         cron({user: user3, tasksByType, daysMissed, analytics}); | ||||
|         expect(user3.purchased.plan.consecutive.count).to.equal(6); | ||||
|         expect(user3.purchased.plan.consecutive.offset).to.equal(0); | ||||
|         expect(user3.purchased.plan.consecutive.trinkets).to.equal(2); | ||||
|         expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(10); | ||||
|       }); | ||||
| 
 | ||||
|       it('increments consecutive benefits the month after the third paid period has started', () => { | ||||
|         clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(7, 'months').add(2, 'days').toDate()); | ||||
|         cron({user: user3, tasksByType, daysMissed, analytics}); | ||||
|         expect(user3.purchased.plan.consecutive.count).to.equal(7); | ||||
|         expect(user3.purchased.plan.consecutive.offset).to.equal(2); | ||||
|         expect(user3.purchased.plan.consecutive.trinkets).to.equal(3); | ||||
|         expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(15); | ||||
|       }); | ||||
| 
 | ||||
|       it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => { | ||||
|         clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(10, 'months').add(2, 'days').toDate()); | ||||
|         cron({user: user3, tasksByType, daysMissed, analytics}); | ||||
|         expect(user3.purchased.plan.consecutive.count).to.equal(10); | ||||
|         expect(user3.purchased.plan.consecutive.offset).to.equal(2); | ||||
|         expect(user3.purchased.plan.consecutive.trinkets).to.equal(4); | ||||
|         expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(20); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('for a 6-month recurring subscription', () => { | ||||
|       let 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
 | ||||
|       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.offset = 6; | ||||
|       user6.purchased.plan.consecutive.trinkets = 2; | ||||
|       user6.purchased.plan.consecutive.gemCapExtra = 10; | ||||
| 
 | ||||
|       it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', () => { | ||||
|         clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months').add(2, 'days').toDate()); | ||||
|         cron({user: user6, tasksByType, daysMissed, analytics}); | ||||
|         expect(user6.purchased.plan.consecutive.count).to.equal(1); | ||||
|         expect(user6.purchased.plan.consecutive.offset).to.equal(5); | ||||
|         expect(user6.purchased.plan.consecutive.trinkets).to.equal(2); | ||||
|         expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(10); | ||||
|       }); | ||||
| 
 | ||||
|       it('does not increment consecutive benefits in the final month of the period that they already have benefits for', () => { | ||||
|         clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(6, 'months').add(2, 'days').toDate()); | ||||
|         cron({user: user6, tasksByType, daysMissed, analytics}); | ||||
|         expect(user6.purchased.plan.consecutive.count).to.equal(6); | ||||
|         expect(user6.purchased.plan.consecutive.offset).to.equal(0); | ||||
|         expect(user6.purchased.plan.consecutive.trinkets).to.equal(2); | ||||
|         expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(10); | ||||
|       }); | ||||
| 
 | ||||
|       it('increments consecutive benefits the month after the second paid period has started', () => { | ||||
|         clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(7, 'months').add(2, 'days').toDate()); | ||||
|         cron({user: user6, tasksByType, daysMissed, analytics}); | ||||
|         expect(user6.purchased.plan.consecutive.count).to.equal(7); | ||||
|         expect(user6.purchased.plan.consecutive.offset).to.equal(5); | ||||
|         expect(user6.purchased.plan.consecutive.trinkets).to.equal(4); | ||||
|         expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(20); | ||||
|       }); | ||||
| 
 | ||||
|       it('increments consecutive benefits the month after the third paid period has started', () => { | ||||
|         clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(13, 'months').add(2, 'days').toDate()); | ||||
|         cron({user: user6, tasksByType, daysMissed, analytics}); | ||||
|         expect(user6.purchased.plan.consecutive.count).to.equal(13); | ||||
|         expect(user6.purchased.plan.consecutive.offset).to.equal(5); | ||||
|         expect(user6.purchased.plan.consecutive.trinkets).to.equal(6); | ||||
|         expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(25); | ||||
|       }); | ||||
| 
 | ||||
|       it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => { | ||||
|         clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(19, 'months').add(2, 'days').toDate()); | ||||
|         cron({user: user6, tasksByType, daysMissed, analytics}); | ||||
|         expect(user6.purchased.plan.consecutive.count).to.equal(19); | ||||
|         expect(user6.purchased.plan.consecutive.offset).to.equal(5); | ||||
|         expect(user6.purchased.plan.consecutive.trinkets).to.equal(8); | ||||
|         expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(25); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('for a 12-month recurring subscription', () => { | ||||
|       let 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.offset = 12; | ||||
|       user12.purchased.plan.consecutive.trinkets = 4; | ||||
|       user12.purchased.plan.consecutive.gemCapExtra = 20; | ||||
| 
 | ||||
|       it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', () => { | ||||
|         clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months').add(2, 'days').toDate()); | ||||
|         cron({user: user12, tasksByType, daysMissed, analytics}); | ||||
|         expect(user12.purchased.plan.consecutive.count).to.equal(1); | ||||
|         expect(user12.purchased.plan.consecutive.offset).to.equal(11); | ||||
|         expect(user12.purchased.plan.consecutive.trinkets).to.equal(4); | ||||
|         expect(user12.purchased.plan.consecutive.gemCapExtra).to.equal(20); | ||||
|       }); | ||||
| 
 | ||||
|       it('does not increment consecutive benefits in the final month of the period that they already have benefits for', () => { | ||||
|         clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(12, 'months').add(2, 'days').toDate()); | ||||
|         cron({user: user12, tasksByType, daysMissed, analytics}); | ||||
|         expect(user12.purchased.plan.consecutive.count).to.equal(12); | ||||
|         expect(user12.purchased.plan.consecutive.offset).to.equal(0); | ||||
|         expect(user12.purchased.plan.consecutive.trinkets).to.equal(4); | ||||
|         expect(user12.purchased.plan.consecutive.gemCapExtra).to.equal(20); | ||||
|       }); | ||||
| 
 | ||||
|       it('increments consecutive benefits the month after the second paid period has started', () => { | ||||
|         clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(13, 'months').add(2, 'days').toDate()); | ||||
|         cron({user: user12, tasksByType, daysMissed, analytics}); | ||||
|         expect(user12.purchased.plan.consecutive.count).to.equal(13); | ||||
|         expect(user12.purchased.plan.consecutive.offset).to.equal(11); | ||||
|         expect(user12.purchased.plan.consecutive.trinkets).to.equal(8); | ||||
|         expect(user12.purchased.plan.consecutive.gemCapExtra).to.equal(25); | ||||
|       }); | ||||
| 
 | ||||
|       it('increments consecutive benefits the month after the third paid period has started', () => { | ||||
|         clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(25, 'months').add(2, 'days').toDate()); | ||||
|         cron({user: user12, tasksByType, daysMissed, analytics}); | ||||
|         expect(user12.purchased.plan.consecutive.count).to.equal(25); | ||||
|         expect(user12.purchased.plan.consecutive.offset).to.equal(11); | ||||
|         expect(user12.purchased.plan.consecutive.trinkets).to.equal(12); | ||||
|         expect(user12.purchased.plan.consecutive.gemCapExtra).to.equal(25); | ||||
|       }); | ||||
| 
 | ||||
|       it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => { | ||||
|         clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(37, 'months').add(2, 'days').toDate()); | ||||
|         cron({user: user12, tasksByType, daysMissed, analytics}); | ||||
|         expect(user12.purchased.plan.consecutive.count).to.equal(37); | ||||
|         expect(user12.purchased.plan.consecutive.offset).to.equal(11); | ||||
|         expect(user12.purchased.plan.consecutive.trinkets).to.equal(16); | ||||
|         expect(user12.purchased.plan.consecutive.gemCapExtra).to.equal(25); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('for a 3-month gift subscription (non-recurring)', () => { | ||||
|       let 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.consecutive.offset = 3; | ||||
|       user3g.purchased.plan.consecutive.trinkets = 1; | ||||
|       user3g.purchased.plan.consecutive.gemCapExtra = 5; | ||||
| 
 | ||||
|       it('does not increment consecutive benefits in the first month of the gift subscription', () => { | ||||
|         clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months').add(2, 'days').toDate()); | ||||
|         cron({user: user3g, tasksByType, daysMissed, analytics}); | ||||
|         expect(user3g.purchased.plan.consecutive.count).to.equal(1); | ||||
|         expect(user3g.purchased.plan.consecutive.offset).to.equal(2); | ||||
|         expect(user3g.purchased.plan.consecutive.trinkets).to.equal(1); | ||||
|         expect(user3g.purchased.plan.consecutive.gemCapExtra).to.equal(5); | ||||
|       }); | ||||
| 
 | ||||
|       it('does not increment consecutive benefits in the second month of the gift subscription', () => { | ||||
|         clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months').add(2, 'days').toDate()); | ||||
|         cron({user: user3g, tasksByType, daysMissed, analytics}); | ||||
|         expect(user3g.purchased.plan.consecutive.count).to.equal(2); | ||||
|         expect(user3g.purchased.plan.consecutive.offset).to.equal(1); | ||||
|         expect(user3g.purchased.plan.consecutive.trinkets).to.equal(1); | ||||
|         expect(user3g.purchased.plan.consecutive.gemCapExtra).to.equal(5); | ||||
|       }); | ||||
| 
 | ||||
|       it('does not increment consecutive benefits in the third month of the gift subscription', () => { | ||||
|         clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months').add(2, 'days').toDate()); | ||||
|         cron({user: user3g, tasksByType, daysMissed, analytics}); | ||||
|         expect(user3g.purchased.plan.consecutive.count).to.equal(3); | ||||
|         expect(user3g.purchased.plan.consecutive.offset).to.equal(0); | ||||
|         expect(user3g.purchased.plan.consecutive.trinkets).to.equal(1); | ||||
|         expect(user3g.purchased.plan.consecutive.gemCapExtra).to.equal(5); | ||||
|       }); | ||||
| 
 | ||||
|       it('does not increment consecutive benefits in the month after the gift subscription has ended', () => { | ||||
|         clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(4, 'months').add(2, 'days').toDate()); | ||||
|         cron({user: user3g, tasksByType, daysMissed, analytics}); | ||||
|         expect(user3g.purchased.plan.consecutive.count).to.equal(0); // subscription has been erased by now
 | ||||
|         expect(user3g.purchased.plan.consecutive.offset).to.equal(0); | ||||
|         expect(user3g.purchased.plan.consecutive.trinkets).to.equal(1); | ||||
|         expect(user3g.purchased.plan.consecutive.gemCapExtra).to.equal(0); // erased
 | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('for a 6-month recurring subscription where the user has incorrect consecutive month data from prior bugs', () => { | ||||
|       let user6x = new User({ | ||||
|         auth: { | ||||
|           local: { | ||||
|             username: 'username6x', | ||||
|             lowerCaseUsername: 'username6x', | ||||
|             email: 'email6x@example.com', | ||||
|             salt: 'salt', | ||||
|             hashed_password: 'hashed_password', // eslint-disable-line camelcase
 | ||||
|           }, | ||||
|         }, | ||||
|       }); | ||||
|       // user6x has a 6-month recurring subscription starting 8 months in the past before issue #4819 was fixed
 | ||||
|       user6x.purchased.plan.customerId = 'subscribedId'; | ||||
|       user6x.purchased.plan.dateUpdated = moment().toDate(); | ||||
|       user6x.purchased.plan.planId = 'basic_6mo'; | ||||
|       user6x.purchased.plan.consecutive.count = 8; | ||||
|       user6x.purchased.plan.consecutive.offset = 0; | ||||
|       user6x.purchased.plan.consecutive.trinkets = 3; | ||||
|       user6x.purchased.plan.consecutive.gemCapExtra = 15; | ||||
| 
 | ||||
|       it('increments consecutive benefits in the first month since the fix for #4819 goes live', () => { | ||||
|         clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months').add(2, 'days').toDate()); | ||||
|         cron({user: user6x, tasksByType, daysMissed, analytics}); | ||||
|         expect(user6x.purchased.plan.consecutive.count).to.equal(9); | ||||
|         expect(user6x.purchased.plan.consecutive.offset).to.equal(5); | ||||
|         expect(user6x.purchased.plan.consecutive.trinkets).to.equal(5); | ||||
|         expect(user6x.purchased.plan.consecutive.gemCapExtra).to.equal(25); | ||||
|       }); | ||||
| 
 | ||||
|       it('does not increment consecutive benefits in the second month after the fix goes live', () => { | ||||
|         clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months').add(2, 'days').toDate()); | ||||
|         cron({user: user6x, tasksByType, daysMissed, analytics}); | ||||
|         expect(user6x.purchased.plan.consecutive.count).to.equal(10); | ||||
|         expect(user6x.purchased.plan.consecutive.offset).to.equal(4); | ||||
|         expect(user6x.purchased.plan.consecutive.trinkets).to.equal(5); | ||||
|         expect(user6x.purchased.plan.consecutive.gemCapExtra).to.equal(25); | ||||
|       }); | ||||
| 
 | ||||
|       it('does not increment consecutive benefits in the third month after the fix goes live', () => { | ||||
|         clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months').add(2, 'days').toDate()); | ||||
|         cron({user: user6x, tasksByType, daysMissed, analytics}); | ||||
|         expect(user6x.purchased.plan.consecutive.count).to.equal(11); | ||||
|         expect(user6x.purchased.plan.consecutive.offset).to.equal(3); | ||||
|         expect(user6x.purchased.plan.consecutive.trinkets).to.equal(5); | ||||
|         expect(user6x.purchased.plan.consecutive.gemCapExtra).to.equal(25); | ||||
|       }); | ||||
| 
 | ||||
|       it('increments consecutive benefits in the seventh month after the fix goes live', () => { | ||||
|         clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(7, 'months').add(2, 'days').toDate()); | ||||
|         cron({user: user6x, tasksByType, daysMissed, analytics}); | ||||
|         expect(user6x.purchased.plan.consecutive.count).to.equal(15); | ||||
|         expect(user6x.purchased.plan.consecutive.offset).to.equal(5); | ||||
|         expect(user6x.purchased.plan.consecutive.trinkets).to.equal(7); | ||||
|         expect(user6x.purchased.plan.consecutive.gemCapExtra).to.equal(25); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('end of the month perks when user is not subscribed', () => { | ||||
| @@ -198,14 +596,12 @@ describe('cron', () => { | ||||
|     }); | ||||
| 
 | ||||
|     it('does not reset plan.gemsBought within the month', () => { | ||||
|       let clock = sinon.useFakeTimers(moment().startOf('month').add(2, 'days').unix()); | ||||
|       clock = sinon.useFakeTimers(moment().startOf('month').add(2, 'days').unix()); | ||||
|       user.purchased.plan.dateUpdated = moment().startOf('month').toDate(); | ||||
| 
 | ||||
|       user.purchased.plan.gemsBought = 10; | ||||
|       cron({user, tasksByType, daysMissed, analytics}); | ||||
|       expect(user.purchased.plan.gemsBought).to.equal(10); | ||||
| 
 | ||||
|       clock.restore(); | ||||
|     }); | ||||
| 
 | ||||
|     it('does not reset plan.dateUpdated on a new month', () => { | ||||
| @@ -265,76 +661,6 @@ describe('cron', () => { | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('user is sleeping', () => { | ||||
|     beforeEach(() => { | ||||
|       user.preferences.sleep = true; | ||||
|     }); | ||||
| 
 | ||||
|     it('calls analytics', () => { | ||||
|       cron({user, tasksByType, daysMissed, analytics}); | ||||
|       expect(analytics.track.callCount).to.equal(1); | ||||
|     }); | ||||
| 
 | ||||
|     it('clears user buffs', () => { | ||||
|       user.stats.buffs = { | ||||
|         str: 1, | ||||
|         int: 1, | ||||
|         per: 1, | ||||
|         con: 1, | ||||
|         stealth: 1, | ||||
|         streaks: true, | ||||
|       }; | ||||
| 
 | ||||
|       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('resets all dailies without damaging user', () => { | ||||
|       let daily = { | ||||
|         text: 'test daily', | ||||
|         type: 'daily', | ||||
|         frequency: 'daily', | ||||
|         everyX: 5, | ||||
|         startDate: new Date(), | ||||
|       }; | ||||
| 
 | ||||
|       let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line new-cap
 | ||||
|       tasksByType.dailys.push(task); | ||||
|       tasksByType.dailys[0].completed = true; | ||||
| 
 | ||||
|       let healthBefore = user.stats.hp; | ||||
| 
 | ||||
|       cron({user, tasksByType, daysMissed, analytics}); | ||||
| 
 | ||||
|       expect(tasksByType.dailys[0].completed).to.be.false; | ||||
|       expect(user.stats.hp).to.equal(healthBefore); | ||||
|     }); | ||||
| 
 | ||||
|     it('sets isDue for daily', () => { | ||||
|       let daily = { | ||||
|         text: 'test daily', | ||||
|         type: 'daily', | ||||
|         frequency: 'daily', | ||||
|         everyX: 5, | ||||
|         startDate: new Date(), | ||||
|       }; | ||||
| 
 | ||||
|       let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line new-cap
 | ||||
|       tasksByType.dailys.push(task); | ||||
|       tasksByType.dailys[0].completed = true; | ||||
| 
 | ||||
|       cron({user, tasksByType, daysMissed, analytics}); | ||||
| 
 | ||||
|       expect(tasksByType.dailys[0].isDue).to.be.exist; | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('todos', () => { | ||||
|     beforeEach(() => { | ||||
|       let todo = { | ||||
| @@ -456,6 +782,15 @@ describe('cron', () => { | ||||
|       expect(tasksByType.dailys[0].isDue).to.be.false; | ||||
|     }); | ||||
| 
 | ||||
|     it('computes isDue when user is sleeping', () => { | ||||
|       user.preferences.sleep = true; | ||||
|       tasksByType.dailys[0].frequency = 'daily'; | ||||
|       tasksByType.dailys[0].everyX = 5; | ||||
|       tasksByType.dailys[0].startDate = moment().toDate(); | ||||
|       cron({user, tasksByType, daysMissed, analytics}); | ||||
|       expect(tasksByType.dailys[0].isDue).to.exist; | ||||
|     }); | ||||
| 
 | ||||
|     it('computes nextDue', () => { | ||||
|       tasksByType.dailys[0].frequency = 'daily'; | ||||
|       tasksByType.dailys[0].everyX = 5; | ||||
| @@ -475,6 +810,13 @@ describe('cron', () => { | ||||
|       expect(tasksByType.dailys[0].completed).to.be.false; | ||||
|     }); | ||||
| 
 | ||||
|     it('should set tasks completed to false when user is sleeping', () => { | ||||
|       user.preferences.sleep = true; | ||||
|       tasksByType.dailys[0].completed = true; | ||||
|       cron({user, tasksByType, daysMissed, analytics}); | ||||
|       expect(tasksByType.dailys[0].completed).to.be.false; | ||||
|     }); | ||||
| 
 | ||||
|     it('should reset task checklist for completed dailys', () => { | ||||
|       tasksByType.dailys[0].checklist.push({title: 'test', completed: false}); | ||||
|       tasksByType.dailys[0].completed = true; | ||||
| @@ -482,6 +824,14 @@ describe('cron', () => { | ||||
|       expect(tasksByType.dailys[0].checklist[0].completed).to.be.false; | ||||
|     }); | ||||
| 
 | ||||
|     it('should reset task checklist for completed dailys when user is sleeping', () => { | ||||
|       user.preferences.sleep = true; | ||||
|       tasksByType.dailys[0].checklist.push({title: 'test', completed: false}); | ||||
|       tasksByType.dailys[0].completed = true; | ||||
|       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', () => { | ||||
|       daysMissed = 10; | ||||
|       tasksByType.dailys[0].checklist.push({title: 'test', completed: false}); | ||||
| @@ -494,12 +844,19 @@ describe('cron', () => { | ||||
|       daysMissed = 1; | ||||
|       let hpBefore = user.stats.hp; | ||||
|       tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1}); | ||||
| 
 | ||||
|       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', () => { | ||||
|       user.preferences.sleep = true; | ||||
|       daysMissed = 1; | ||||
|       let hpBefore = user.stats.hp; | ||||
|       tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1}); | ||||
|       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', () => { | ||||
|       sandbox.stub(nconf, 'get').withArgs('CRON_SAFE_MODE').returns('true'); | ||||
|       let cronOverride = requireAgain(pathToCronLib).cron; | ||||
| @@ -540,7 +897,7 @@ describe('cron', () => { | ||||
|       expect(hpDifferenceOfPartiallyIncompleteDaily).to.be.lessThan(hpDifferenceOfFullyIncompleteDaily); | ||||
|     }); | ||||
| 
 | ||||
|     it('should decrement quest progress down for missing a daily', () => { | ||||
|     it('should decrement quest.progress.down for missing a daily', () => { | ||||
|       daysMissed = 1; | ||||
|       tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1}); | ||||
| 
 | ||||
| @@ -549,6 +906,16 @@ describe('cron', () => { | ||||
|       expect(progress.down).to.equal(-1); | ||||
|     }); | ||||
| 
 | ||||
|     it('should not decrement quest.progress.down for missing a daily when user is sleeping', () => { | ||||
|       user.preferences.sleep = true; | ||||
|       daysMissed = 1; | ||||
|       tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1}); | ||||
| 
 | ||||
|       let progress = cron({user, tasksByType, daysMissed, analytics}); | ||||
| 
 | ||||
|       expect(progress.down).to.equal(0); | ||||
|     }); | ||||
| 
 | ||||
|     it('should do damage for only yesterday\'s dailies', () => { | ||||
|       daysMissed = 3; | ||||
|       tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1}); | ||||
| @@ -611,15 +978,11 @@ describe('cron', () => { | ||||
| 
 | ||||
|     describe('counters', () => { | ||||
|       let notStartOfWeekOrMonth = new Date(2016, 9, 28).getTime(); // a Friday
 | ||||
|       let clock; | ||||
| 
 | ||||
|       beforeEach(() => { | ||||
|         // Replace system clocks so we can get predictable results
 | ||||
|         clock = sinon.useFakeTimers(notStartOfWeekOrMonth); | ||||
|       }); | ||||
|       afterEach(() => { | ||||
|         return clock.restore(); | ||||
|       }); | ||||
| 
 | ||||
|       it('should reset a daily habit counter each day', () => { | ||||
|         tasksByType.habits[0].counterUp = 1; | ||||
| @@ -631,7 +994,7 @@ describe('cron', () => { | ||||
|         expect(tasksByType.habits[0].counterDown).to.equal(0); | ||||
|       }); | ||||
| 
 | ||||
|       it('should reset habit counters even if user is resting in the Inn', () => { | ||||
|       it('should reset habit counters even if user is sleeping', () => { | ||||
|         user.preferences.sleep = true; | ||||
|         tasksByType.habits[0].counterUp = 1; | ||||
|         tasksByType.habits[0].counterDown = 1; | ||||
| @@ -892,7 +1255,23 @@ describe('cron', () => { | ||||
|       expect(user.achievements.perfect).to.equal(0); | ||||
|     }); | ||||
| 
 | ||||
|     it('increments user buffs if all (at least 1) due dailies were completed', () => { | ||||
|     it('gives perfect day buff if all (at least 1) due dailies were completed', () => { | ||||
|       daysMissed = 1; | ||||
|       tasksByType.dailys[0].completed = true; | ||||
|       tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1}); | ||||
| 
 | ||||
|       let previousBuffs = user.stats.buffs.toObject(); | ||||
| 
 | ||||
|       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', () => { | ||||
|       user.preferences.sleep = true; | ||||
|       daysMissed = 1; | ||||
|       tasksByType.dailys[0].completed = true; | ||||
|       tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1}); | ||||
| @@ -931,6 +1310,31 @@ describe('cron', () => { | ||||
|       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', () => { | ||||
|       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, | ||||
|       }; | ||||
| 
 | ||||
|       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)', () => { | ||||
|       daysMissed = 1; | ||||
|       tasksByType.dailys[0].completed = false; | ||||
| @@ -955,7 +1359,50 @@ describe('cron', () => { | ||||
|       expect(user.stats.buffs.streaks).to.be.false; | ||||
|     }); | ||||
| 
 | ||||
|     it('still grants a perfect day when CRON_SAFE_MODE is set', () => { | ||||
|     it('clears buffs if user does not have a perfect day (at least one due daily not completed) when user is sleeping', () => { | ||||
|       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, | ||||
|       }; | ||||
| 
 | ||||
|       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', () => { | ||||
|       sandbox.stub(nconf, 'get').withArgs('CRON_SAFE_MODE').returns('true'); | ||||
|       let cronOverride = requireAgain(pathToCronLib).cron; | ||||
|       daysMissed = 1; | ||||
|       tasksByType.dailys[0].completed = false; | ||||
|       tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1}); | ||||
| 
 | ||||
|       let 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', () => { | ||||
|       user.preferences.sleep = true; | ||||
|       sandbox.stub(nconf, 'get').withArgs('CRON_SAFE_MODE').returns('true'); | ||||
|       let cronOverride = requireAgain(pathToCronLib).cron; | ||||
|       daysMissed = 1; | ||||
| @@ -987,6 +1434,20 @@ describe('cron', () => { | ||||
|       common.statsComputed.restore(); | ||||
|     }); | ||||
| 
 | ||||
|     it('should not add mp to user when user is sleeping', () => { | ||||
|       const statsComputedRes = common.statsComputed(user); | ||||
|       const stubbedStatsComputed = sinon.stub(common, 'statsComputed'); | ||||
| 
 | ||||
|       user.preferences.sleep = true; | ||||
|       let mpBefore = user.stats.mp; | ||||
|       tasksByType.dailys[0].completed = true; | ||||
|       stubbedStatsComputed.returns(Object.assign(statsComputedRes, {maxMP: 100})); | ||||
|       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', () => { | ||||
|       const statsComputedRes = common.statsComputed(user); | ||||
|       const stubbedStatsComputed = sinon.stub(common, 'statsComputed'); | ||||
| @@ -1128,27 +1589,6 @@ describe('cron', () => { | ||||
|         flagCount: 0, | ||||
|       }; | ||||
|     }); | ||||
| 
 | ||||
|     xit('does not clear pms under 200', () => { | ||||
|       cron({user, tasksByType, daysMissed, analytics}); | ||||
|       expect(user.inbox.messages[lastMessageId]).to.exist; | ||||
|     }); | ||||
| 
 | ||||
|     xit('clears pms over 200', () => { | ||||
|       let messageId = common.uuid(); | ||||
|       user.inbox.messages[messageId] = { | ||||
|         id: messageId, | ||||
|         text: `test ${messageId}`, | ||||
|         timestamp: Number(new Date()), | ||||
|         likes: {}, | ||||
|         flags: {}, | ||||
|         flagCount: 0, | ||||
|       }; | ||||
| 
 | ||||
|       cron({user, tasksByType, daysMissed, analytics}); | ||||
| 
 | ||||
|       expect(user.inbox.messages[messageId]).to.not.exist; | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('login incentives', () => { | ||||
| @@ -1182,7 +1622,7 @@ describe('cron', () => { | ||||
|       expect(user.loginIncentives).to.eql(1); | ||||
|     }); | ||||
| 
 | ||||
|     it('increments loginIncentives by 1 even if user has Dailies paused', () => { | ||||
|     it('increments loginIncentives by 1 even if user is sleeping', () => { | ||||
|       user.preferences.sleep = true; | ||||
|       cron({user, tasksByType, daysMissed, analytics}); | ||||
|       expect(user.loginIncentives).to.eql(1); | ||||
| @@ -1348,7 +1788,7 @@ describe('recoverCron', () => { | ||||
|           local: { | ||||
|             username: 'username', | ||||
|             lowerCaseUsername: 'username', | ||||
|             email: 'email@email.email', | ||||
|             email: 'email@example.com', | ||||
|             salt: 'salt', | ||||
|             hashed_password: 'hashed_password', // eslint-disable-line camelcase
 | ||||
|           }, | ||||
| @@ -3,9 +3,9 @@ import got from 'got'; | ||||
| import nconf from 'nconf'; | ||||
| import nodemailer from 'nodemailer'; | ||||
| import requireAgain from 'require-again'; | ||||
| import logger from '../../../../../website/server/libs/logger'; | ||||
| import { TAVERN_ID } from '../../../../../website/server/models/group'; | ||||
| import { defer } from '../../../../helpers/api-unit.helper'; | ||||
| import logger from '../../../../website/server/libs/logger'; | ||||
| import { TAVERN_ID } from '../../../../website/server/models/group'; | ||||
| import { defer } from '../../../helpers/api-unit.helper'; | ||||
| 
 | ||||
| function getUser () { | ||||
|   return { | ||||
| @@ -19,7 +19,6 @@ function getUser () { | ||||
|         emails: [{ | ||||
|           value: 'email@facebook', | ||||
|         }], | ||||
|         displayName: 'fb display name', | ||||
|       }, | ||||
|     }, | ||||
|     profile: { | ||||
| @@ -34,7 +33,7 @@ function getUser () { | ||||
| } | ||||
| 
 | ||||
| describe('emails', () => { | ||||
|   let pathToEmailLib = '../../../../../website/server/libs/email'; | ||||
|   let pathToEmailLib = '../../../../website/server/libs/email'; | ||||
| 
 | ||||
|   describe('sendEmail', () => { | ||||
|     let sendMailSpy; | ||||
| @@ -100,7 +99,7 @@ describe('emails', () => { | ||||
| 
 | ||||
|       let data = getUserInfo(user, ['name', 'email', '_id', 'canSend']); | ||||
| 
 | ||||
|       expect(data).to.have.property('name', user.auth.facebook.displayName); | ||||
|       expect(data).to.have.property('name', user.profile.name); | ||||
|       expect(data).to.have.property('email', user.auth.facebook.emails[0].value); | ||||
|       expect(data).to.have.property('_id', user._id); | ||||
|       expect(data).to.have.property('canSend', true); | ||||
| @@ -110,13 +109,12 @@ describe('emails', () => { | ||||
|       let attachEmail = requireAgain(pathToEmailLib); | ||||
|       let getUserInfo = attachEmail.getUserInfo; | ||||
|       let user = getUser(); | ||||
|       delete user.profile.name; | ||||
|       delete user.auth.local.email; | ||||
|       delete user.auth.facebook; | ||||
| 
 | ||||
|       let data = getUserInfo(user, ['name', 'email', '_id', 'canSend']); | ||||
| 
 | ||||
|       expect(data).to.have.property('name', user.auth.local.username); | ||||
|       expect(data).to.have.property('name', user.profile.name); | ||||
|       expect(data).not.to.have.property('email'); | ||||
|       expect(data).to.have.property('_id', user._id); | ||||
|       expect(data).to.have.property('canSend', true); | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { | ||||
|   encrypt, | ||||
|   decrypt, | ||||
| } from '../../../../../website/server/libs/encryption'; | ||||
| } from '../../../../website/server/libs/encryption'; | ||||
| 
 | ||||
| describe('encryption', () => { | ||||
|   it('can encrypt and decrypt', () => { | ||||
| @@ -5,7 +5,9 @@ import { | ||||
|   BadRequest, | ||||
|   InternalServerError, | ||||
|   NotFound, | ||||
| } from '../../../../../website/server/libs/errors'; | ||||
|   NotificationNotFound, | ||||
| } from '../../../../website/server/libs/errors'; | ||||
| import i18n from '../../../../website/common/script/i18n'; | ||||
| 
 | ||||
| describe('Custom Errors', () => { | ||||
|   describe('CustomError', () => { | ||||
| @@ -66,6 +68,23 @@ describe('Custom Errors', () => { | ||||
| 
 | ||||
|       expect(notAuthorizedError.message).to.eql('Custom Error Message'); | ||||
|     }); | ||||
| 
 | ||||
|     describe('NotificationNotFound', () => { | ||||
|       it('is an instance of NotFound', () => { | ||||
|         const notificationNotFoundErr = new NotificationNotFound(); | ||||
|         expect(notificationNotFoundErr).to.be.an.instanceOf(NotFound); | ||||
|       }); | ||||
| 
 | ||||
|       it('it returns an http code of 404', () => { | ||||
|         const notificationNotFoundErr = new NotificationNotFound(); | ||||
|         expect(notificationNotFoundErr.httpCode).to.eql(404); | ||||
|       }); | ||||
| 
 | ||||
|       it('returns a standard message', () => { | ||||
|         const notificationNotFoundErr = new NotificationNotFound(); | ||||
|         expect(notificationNotFoundErr.message).to.eql(i18n.t('messageNotificationNotFound')); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('BadRequest', () => { | ||||
| @@ -2,7 +2,7 @@ import { | ||||
|   translations, | ||||
|   localePath, | ||||
|   langCodes, | ||||
| } from '../../../../../website/server/libs/i18n'; | ||||
| } from '../../../../website/server/libs/i18n'; | ||||
| import fs from 'fs'; | ||||
| import path from 'path'; | ||||
| 
 | ||||
| @@ -1,8 +1,8 @@ | ||||
| import winston from 'winston'; | ||||
| import logger from '../../../../../website/server/libs/logger'; | ||||
| import logger from '../../../../website/server/libs/logger'; | ||||
| import { | ||||
|   NotFound, | ||||
| } from '../../../../../website/server/libs//errors'; | ||||
| } from '../../../../website/server/libs//errors'; | ||||
| 
 | ||||
| describe('logger', () => { | ||||
|   let logSpy; | ||||
| @@ -2,11 +2,11 @@ | ||||
| 
 | ||||
| import { | ||||
|   encrypt, | ||||
| } from '../../../../../website/server/libs/encryption'; | ||||
| } from '../../../../website/server/libs/encryption'; | ||||
| import moment from 'moment'; | ||||
| import { | ||||
|   generateUser, | ||||
| } from '../../../../helpers/api-integration/v3'; | ||||
| } from '../../../helpers/api-integration/v3'; | ||||
| import { | ||||
|   sha1Encrypt as sha1EncryptPassword, | ||||
|   sha1MakeSalt, | ||||
| @@ -15,7 +15,7 @@ import { | ||||
|   compare, | ||||
|   convertToBcrypt, | ||||
|   validatePasswordResetCodeAndFindUser, | ||||
| } from '../../../../../website/server/libs/password'; | ||||
| } from '../../../../website/server/libs/password'; | ||||
| 
 | ||||
| describe('Password Utilities', () => { | ||||
|   describe('compare', () => { | ||||
| @@ -107,6 +107,25 @@ describe('Password Utilities', () => { | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     it('defaults to SHA1 encryption if salt is provided', async () => { | ||||
|       let textPassword = 'mySecretPassword'; | ||||
|       let salt = sha1MakeSalt(); | ||||
|       let hashedPassword = sha1EncryptPassword(textPassword, salt); | ||||
| 
 | ||||
|       let user = { | ||||
|         auth: { | ||||
|           local: { | ||||
|             hashed_password: hashedPassword, | ||||
|             salt, | ||||
|             passwordHashMethod: '', | ||||
|           }, | ||||
|         }, | ||||
|       }; | ||||
| 
 | ||||
|       let isValidPassword = await compare(user, textPassword); | ||||
|       expect(isValidPassword).to.eql(true); | ||||
|     }); | ||||
| 
 | ||||
|     it('throws an error if an invalid hashing method is used', async () => { | ||||
|       try { | ||||
|         await compare({ | ||||
| @@ -226,7 +245,9 @@ describe('Password Utilities', () => { | ||||
| 
 | ||||
|     it('returns false if the user has no local auth', async () => { | ||||
|       let user = await generateUser({ | ||||
|         auth: 'not an object with valid fields', | ||||
|         auth: { | ||||
|           facebook: {}, | ||||
|         }, | ||||
|       }); | ||||
|       let res = await validatePasswordResetCodeAndFindUser(encrypt(JSON.stringify({ | ||||
|         userId: user._id, | ||||
| @@ -2,11 +2,11 @@ import moment from 'moment'; | ||||
| 
 | ||||
| import { | ||||
|   generateGroup, | ||||
| } from '../../../../../../helpers/api-unit.helper.js'; | ||||
| import { model as User } from '../../../../../../../website/server/models/user'; | ||||
| import amzLib from '../../../../../../../website/server/libs/amazonPayments'; | ||||
| import payments from '../../../../../../../website/server/libs/payments'; | ||||
| import common from '../../../../../../../website/common'; | ||||
| } from '../../../../../helpers/api-unit.helper.js'; | ||||
| import { model as User } from '../../../../../../website/server/models/user'; | ||||
| import amzLib from '../../../../../../website/server/libs/payments/amazon'; | ||||
| import payments from '../../../../../../website/server/libs/payments/payments'; | ||||
| import common from '../../../../../../website/common'; | ||||
| import { createNonLeaderGroupMember } from '../paymentHelpers'; | ||||
| 
 | ||||
| const i18n = common.i18n; | ||||
| @@ -48,7 +48,6 @@ describe('Amazon Payments - Cancel Subscription', () => { | ||||
| 
 | ||||
|   function expectBillingAggreementDetailSpy () { | ||||
|     getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails') | ||||
|       .returnsPromise() | ||||
|       .resolves({ | ||||
|         BillingAgreementDetails: { | ||||
|           BillingAgreementStatus: {State: 'Open'}, | ||||
| @@ -80,14 +79,14 @@ describe('Amazon Payments - Cancel Subscription', () => { | ||||
|     headers = {}; | ||||
| 
 | ||||
|     getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails'); | ||||
|     getBillingAgreementDetailsSpy.returnsPromise().resolves({ | ||||
|     getBillingAgreementDetailsSpy.resolves({ | ||||
|       BillingAgreementDetails: { | ||||
|         BillingAgreementStatus: {State: 'Closed'}, | ||||
|       }, | ||||
|     }); | ||||
| 
 | ||||
|     paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription'); | ||||
|     paymentCancelSubscriptionSpy.returnsPromise().resolves({}); | ||||
|     paymentCancelSubscriptionSpy.resolves({}); | ||||
|   }); | ||||
| 
 | ||||
|   afterEach(function () { | ||||
| @@ -118,7 +117,7 @@ describe('Amazon Payments - Cancel Subscription', () => { | ||||
|   it('should close a user subscription if amazon not closed', async () => { | ||||
|     amzLib.getBillingAgreementDetails.restore(); | ||||
|     expectBillingAggreementDetailSpy(); | ||||
|     let closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').returnsPromise().resolves({}); | ||||
|     let closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').resolves({}); | ||||
|     billingAgreementId = user.purchased.plan.customerId; | ||||
| 
 | ||||
|     await amzLib.cancelSubscription({user, headers}); | ||||
| @@ -164,7 +163,7 @@ describe('Amazon Payments - Cancel Subscription', () => { | ||||
|   it('should close a group subscription if amazon not closed', async () => { | ||||
|     amzLib.getBillingAgreementDetails.restore(); | ||||
|     expectBillingAggreementDetailSpy(); | ||||
|     let closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').returnsPromise().resolves({}); | ||||
|     let closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').resolves({}); | ||||
|     billingAgreementId = group.purchased.plan.customerId; | ||||
| 
 | ||||
|     await amzLib.cancelSubscription({user, groupId: group._id, headers}); | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { model as User } from '../../../../../../../website/server/models/user'; | ||||
| import amzLib from '../../../../../../../website/server/libs/amazonPayments'; | ||||
| import payments from '../../../../../../../website/server/libs/payments'; | ||||
| import common from '../../../../../../../website/common'; | ||||
| import { model as User } from '../../../../../../website/server/models/user'; | ||||
| import amzLib from '../../../../../../website/server/libs/payments/amazon'; | ||||
| import payments from '../../../../../../website/server/libs/payments/payments'; | ||||
| import common from '../../../../../../website/common'; | ||||
| 
 | ||||
| const i18n = common.i18n; | ||||
| 
 | ||||
| @@ -68,22 +68,22 @@ describe('Amazon Payments - Checkout', () => { | ||||
|     orderReferenceId = 'orderReferenceId'; | ||||
| 
 | ||||
|     setOrderReferenceDetailsSpy = sinon.stub(amzLib, 'setOrderReferenceDetails'); | ||||
|     setOrderReferenceDetailsSpy.returnsPromise().resolves({}); | ||||
|     setOrderReferenceDetailsSpy.resolves({}); | ||||
| 
 | ||||
|     confirmOrderReferenceSpy = sinon.stub(amzLib, 'confirmOrderReference'); | ||||
|     confirmOrderReferenceSpy.returnsPromise().resolves({}); | ||||
|     confirmOrderReferenceSpy.resolves({}); | ||||
| 
 | ||||
|     authorizeSpy = sinon.stub(amzLib, 'authorize'); | ||||
|     authorizeSpy.returnsPromise().resolves({}); | ||||
|     authorizeSpy.resolves({}); | ||||
| 
 | ||||
|     closeOrderReferenceSpy = sinon.stub(amzLib, 'closeOrderReference'); | ||||
|     closeOrderReferenceSpy.returnsPromise().resolves({}); | ||||
|     closeOrderReferenceSpy.resolves({}); | ||||
| 
 | ||||
|     paymentBuyGemsStub = sinon.stub(payments, 'buyGems'); | ||||
|     paymentBuyGemsStub.returnsPromise().resolves({}); | ||||
|     paymentBuyGemsStub.resolves({}); | ||||
| 
 | ||||
|     paymentCreateSubscritionStub = sinon.stub(payments, 'createSubscription'); | ||||
|     paymentCreateSubscritionStub.returnsPromise().resolves({}); | ||||
|     paymentCreateSubscritionStub.resolves({}); | ||||
| 
 | ||||
|     sinon.stub(common, 'uuid').returns('uuid-generated'); | ||||
|   }); | ||||
| @@ -111,7 +111,7 @@ describe('Amazon Payments - Checkout', () => { | ||||
|   } | ||||
| 
 | ||||
|   it('should purchase gems', async () => { | ||||
|     sinon.stub(user, 'canGetGems').returnsPromise().resolves(true); | ||||
|     sinon.stub(user, 'canGetGems').resolves(true); | ||||
|     await amzLib.checkout({user, orderReferenceId, headers}); | ||||
| 
 | ||||
|     expectBuyGemsStub(amzLib.constants.PAYMENT_METHOD); | ||||
| @@ -140,7 +140,7 @@ describe('Amazon Payments - Checkout', () => { | ||||
|   }); | ||||
| 
 | ||||
|   it('should error if user cannot get gems gems', async () => { | ||||
|     sinon.stub(user, 'canGetGems').returnsPromise().resolves(false); | ||||
|     sinon.stub(user, 'canGetGems').resolves(false); | ||||
|     await expect(amzLib.checkout({user, orderReferenceId, headers})).to.eventually.be.rejected.and.to.eql({ | ||||
|       httpCode: 401, | ||||
|       message: i18n.t('groupPolicyCannotGetGems'), | ||||
| @@ -2,12 +2,12 @@ import cc from 'coupon-code'; | ||||
| 
 | ||||
| import { | ||||
|   generateGroup, | ||||
| } from '../../../../../../helpers/api-unit.helper.js'; | ||||
| import { model as User } from '../../../../../../../website/server/models/user'; | ||||
| import { model as Coupon } from '../../../../../../../website/server/models/coupon'; | ||||
| import amzLib from '../../../../../../../website/server/libs/amazonPayments'; | ||||
| import payments from '../../../../../../../website/server/libs/payments'; | ||||
| import common from '../../../../../../../website/common'; | ||||
| } from '../../../../../helpers/api-unit.helper.js'; | ||||
| import { model as User } from '../../../../../../website/server/models/user'; | ||||
| import { model as Coupon } from '../../../../../../website/server/models/coupon'; | ||||
| import amzLib from '../../../../../../website/server/libs/payments/amazon'; | ||||
| import payments from '../../../../../../website/server/libs/payments/payments'; | ||||
| import common from '../../../../../../website/common'; | ||||
| 
 | ||||
| const i18n = common.i18n; | ||||
| 
 | ||||
| @@ -46,16 +46,16 @@ describe('Amazon Payments - Subscribe', () => { | ||||
|     headers = {}; | ||||
| 
 | ||||
|     amazonSetBillingAgreementDetailsSpy = sinon.stub(amzLib, 'setBillingAgreementDetails'); | ||||
|     amazonSetBillingAgreementDetailsSpy.returnsPromise().resolves({}); | ||||
|     amazonSetBillingAgreementDetailsSpy.resolves({}); | ||||
| 
 | ||||
|     amazonConfirmBillingAgreementSpy = sinon.stub(amzLib, 'confirmBillingAgreement'); | ||||
|     amazonConfirmBillingAgreementSpy.returnsPromise().resolves({}); | ||||
|     amazonConfirmBillingAgreementSpy.resolves({}); | ||||
| 
 | ||||
|     amazonAuthorizeOnBillingAgreementSpy = sinon.stub(amzLib, 'authorizeOnBillingAgreement'); | ||||
|     amazonAuthorizeOnBillingAgreementSpy.returnsPromise().resolves({}); | ||||
|     amazonAuthorizeOnBillingAgreementSpy.resolves({}); | ||||
| 
 | ||||
|     createSubSpy = sinon.stub(payments, 'createSubscription'); | ||||
|     createSubSpy.returnsPromise().resolves({}); | ||||
|     createSubSpy.resolves({}); | ||||
| 
 | ||||
|     sinon.stub(common, 'uuid').returns('uuid-generated'); | ||||
|   }); | ||||
| @@ -2,11 +2,11 @@ import uuid from 'uuid'; | ||||
| 
 | ||||
| import { | ||||
|   generateGroup, | ||||
| } from '../../../../../../helpers/api-unit.helper.js'; | ||||
| import { model as User } from '../../../../../../../website/server/models/user'; | ||||
| import { model as Group } from '../../../../../../../website/server/models/group'; | ||||
| import amzLib from '../../../../../../../website/server/libs/amazonPayments'; | ||||
| import payments from '../../../../../../../website/server/libs/payments'; | ||||
| } from '../../../../../helpers/api-unit.helper.js'; | ||||
| import { model as User } from '../../../../../../website/server/models/user'; | ||||
| import { model as Group } from '../../../../../../website/server/models/group'; | ||||
| import amzLib from '../../../../../../website/server/libs/payments/amazon'; | ||||
| import payments from '../../../../../../website/server/libs/payments/payments'; | ||||
| 
 | ||||
| describe('#upgradeGroupPlan', () => { | ||||
|   let spy, data, user, group, uuidString; | ||||
| @@ -37,7 +37,7 @@ describe('#upgradeGroupPlan', () => { | ||||
|     await group.save(); | ||||
| 
 | ||||
|     spy = sinon.stub(amzLib, 'authorizeOnBillingAgreement'); | ||||
|     spy.returnsPromise().resolves([]); | ||||
|     spy.resolves([]); | ||||
| 
 | ||||
|     uuidString = 'uuid-v4'; | ||||
|     sinon.stub(uuid, 'v4').returns(uuidString); | ||||
| @@ -1,11 +1,12 @@ | ||||
| /* eslint-disable camelcase */ | ||||
| import iapModule from '../../../../../website/server/libs/inAppPurchases'; | ||||
| import payments from '../../../../../website/server/libs/payments'; | ||||
| import applePayments from '../../../../../website/server/libs/applePayments'; | ||||
| import payments from '../../../../../website/server/libs/payments/payments'; | ||||
| import applePayments from '../../../../../website/server/libs/payments/apple'; | ||||
| import iap from '../../../../../website/server/libs/inAppPurchases'; | ||||
| import {model as User} from '../../../../../website/server/models/user'; | ||||
| import common from '../../../../../website/common'; | ||||
| import moment from 'moment'; | ||||
| import {mockFindById, restoreFindById} from '../../../../helpers/mongoose.helper'; | ||||
| 
 | ||||
| const i18n = common.i18n; | ||||
| 
 | ||||
| @@ -24,16 +25,16 @@ describe('Apple Payments', ()  => { | ||||
|       headers = {}; | ||||
| 
 | ||||
|       iapSetupStub = sinon.stub(iapModule, 'setup') | ||||
|         .returnsPromise().resolves(); | ||||
|         .resolves(); | ||||
|       iapValidateStub = sinon.stub(iapModule, 'validate') | ||||
|         .returnsPromise().resolves({}); | ||||
|         .resolves({}); | ||||
|       iapIsValidatedStub = sinon.stub(iapModule, 'isValidated') | ||||
|         .returns(true); | ||||
|       iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData') | ||||
|         .returns([{productId: 'com.habitrpg.ios.Habitica.21gems', | ||||
|                    transactionId: token, | ||||
|         }]); | ||||
|       paymentBuyGemsStub = sinon.stub(payments, 'buyGems').returnsPromise().resolves({}); | ||||
|       paymentBuyGemsStub = sinon.stub(payments, 'buyGems').resolves({}); | ||||
|     }); | ||||
| 
 | ||||
|     afterEach(() => { | ||||
| @@ -49,7 +50,7 @@ describe('Apple Payments', ()  => { | ||||
|       iapIsValidatedStub = sinon.stub(iapModule, 'isValidated') | ||||
|         .returns(false); | ||||
| 
 | ||||
|       await expect(applePayments.verifyGemPurchase(user, receipt, headers)) | ||||
|       await expect(applePayments.verifyGemPurchase({user, receipt, headers})) | ||||
|         .to.eventually.be.rejected.and.to.eql({ | ||||
|           httpCode: 401, | ||||
|           name: 'NotAuthorized', | ||||
| @@ -57,9 +58,21 @@ describe('Apple Payments', ()  => { | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     it('should throw an error if getPurchaseData is invalid', async () => { | ||||
|       iapGetPurchaseDataStub.restore(); | ||||
|       iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData').returns([]); | ||||
| 
 | ||||
|       await expect(applePayments.verifyGemPurchase({user, receipt, headers})) | ||||
|         .to.eventually.be.rejected.and.to.eql({ | ||||
|           httpCode: 401, | ||||
|           name: 'NotAuthorized', | ||||
|           message: applePayments.constants.RESPONSE_NO_ITEM_PURCHASED, | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     it('errors if the user cannot purchase gems', async () => { | ||||
|       sinon.stub(user, 'canGetGems').returnsPromise().resolves(false); | ||||
|       await expect(applePayments.verifyGemPurchase(user, receipt, headers)) | ||||
|       sinon.stub(user, 'canGetGems').resolves(false); | ||||
|       await expect(applePayments.verifyGemPurchase({user, receipt, headers})) | ||||
|         .to.eventually.be.rejected.and.to.eql({ | ||||
|           httpCode: 401, | ||||
|           name: 'NotAuthorized', | ||||
| @@ -69,9 +82,91 @@ describe('Apple Payments', ()  => { | ||||
|       user.canGetGems.restore(); | ||||
|     }); | ||||
| 
 | ||||
|     it('purchases gems', async () => { | ||||
|       sinon.stub(user, 'canGetGems').returnsPromise().resolves(true); | ||||
|       await applePayments.verifyGemPurchase(user, receipt, headers); | ||||
|     it('errors if amount does not exist', async () => { | ||||
|       sinon.stub(user, 'canGetGems').resolves(true); | ||||
|       iapGetPurchaseDataStub.restore(); | ||||
|       iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData') | ||||
|         .returns([{productId: 'badProduct', | ||||
|                    transactionId: token, | ||||
|         }]); | ||||
| 
 | ||||
|       await expect(applePayments.verifyGemPurchase({user, receipt, headers})) | ||||
|         .to.eventually.be.rejected.and.to.eql({ | ||||
|           httpCode: 401, | ||||
|           name: 'NotAuthorized', | ||||
|           message: applePayments.constants.RESPONSE_INVALID_ITEM, | ||||
|         }); | ||||
| 
 | ||||
|       user.canGetGems.restore(); | ||||
|     }); | ||||
| 
 | ||||
|     const gemsCanPurchase = [ | ||||
|       { | ||||
|         productId: 'com.habitrpg.ios.Habitica.4gems', | ||||
|         amount: 1, | ||||
|       }, | ||||
|       { | ||||
|         productId: 'com.habitrpg.ios.Habitica.20gems', | ||||
|         amount: 5.25, | ||||
|       }, | ||||
|       { | ||||
|         productId: 'com.habitrpg.ios.Habitica.21gems', | ||||
|         amount: 5.25, | ||||
|       }, | ||||
|       { | ||||
|         productId: 'com.habitrpg.ios.Habitica.42gems', | ||||
|         amount: 10.5, | ||||
|       }, | ||||
|       { | ||||
|         productId: 'com.habitrpg.ios.Habitica.84gems', | ||||
|         amount: 21, | ||||
|       }, | ||||
|     ]; | ||||
| 
 | ||||
|     gemsCanPurchase.forEach(gemTest => { | ||||
|       it(`purchases ${gemTest.productId} gems`, async () => { | ||||
|         iapGetPurchaseDataStub.restore(); | ||||
|         iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData') | ||||
|           .returns([{productId: gemTest.productId, | ||||
|                      transactionId: token, | ||||
|           }]); | ||||
| 
 | ||||
|         sinon.stub(user, 'canGetGems').resolves(true); | ||||
|         await applePayments.verifyGemPurchase({user, receipt, headers}); | ||||
| 
 | ||||
|         expect(iapSetupStub).to.be.calledOnce; | ||||
|         expect(iapValidateStub).to.be.calledOnce; | ||||
|         expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt); | ||||
|         expect(iapIsValidatedStub).to.be.calledOnce; | ||||
|         expect(iapIsValidatedStub).to.be.calledWith({}); | ||||
|         expect(iapGetPurchaseDataStub).to.be.calledOnce; | ||||
| 
 | ||||
|         expect(paymentBuyGemsStub).to.be.calledOnce; | ||||
|         expect(paymentBuyGemsStub).to.be.calledWith({ | ||||
|           user, | ||||
|           paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE, | ||||
|           amount: gemTest.amount, | ||||
|           headers, | ||||
|         }); | ||||
|         expect(user.canGetGems).to.be.calledOnce; | ||||
|         user.canGetGems.restore(); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it('gifts gems', async () => { | ||||
|       const receivingUser = new User(); | ||||
|       await receivingUser.save(); | ||||
| 
 | ||||
|       mockFindById(receivingUser); | ||||
| 
 | ||||
|       iapGetPurchaseDataStub.restore(); | ||||
|       iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData') | ||||
|         .returns([{productId: gemsCanPurchase[0].productId, | ||||
|                    transactionId: token, | ||||
|         }]); | ||||
| 
 | ||||
|       const gift = {uuid: receivingUser._id}; | ||||
|       await applePayments.verifyGemPurchase({user, gift, receipt, headers}); | ||||
| 
 | ||||
|       expect(iapSetupStub).to.be.calledOnce; | ||||
|       expect(iapValidateStub).to.be.calledOnce; | ||||
| @@ -82,13 +177,12 @@ describe('Apple Payments', ()  => { | ||||
| 
 | ||||
|       expect(paymentBuyGemsStub).to.be.calledOnce; | ||||
|       expect(paymentBuyGemsStub).to.be.calledWith({ | ||||
|         user, | ||||
|         user: receivingUser, | ||||
|         paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE, | ||||
|         amount: 5.25, | ||||
|         amount: gemsCanPurchase[0].amount, | ||||
|         headers, | ||||
|       }); | ||||
|       expect(user.canGetGems).to.be.calledOnce; | ||||
|       user.canGetGems.restore(); | ||||
|       restoreFindById(); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
| @@ -106,9 +200,9 @@ describe('Apple Payments', ()  => { | ||||
|       nextPaymentProcessing = moment.utc().add({days: 2}); | ||||
| 
 | ||||
|       iapSetupStub = sinon.stub(iapModule, 'setup') | ||||
|         .returnsPromise().resolves(); | ||||
|         .resolves(); | ||||
|       iapValidateStub = sinon.stub(iapModule, 'validate') | ||||
|         .returnsPromise().resolves({}); | ||||
|         .resolves({}); | ||||
|       iapIsValidatedStub = sinon.stub(iapModule, 'isValidated') | ||||
|         .returns(true); | ||||
|       iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData') | ||||
| @@ -125,7 +219,7 @@ describe('Apple Payments', ()  => { | ||||
|           productId: sku, | ||||
|           transactionId: token, | ||||
|         }]); | ||||
|       paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({}); | ||||
|       paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').resolves({}); | ||||
|     }); | ||||
| 
 | ||||
|     afterEach(() => { | ||||
| @@ -133,7 +227,16 @@ describe('Apple Payments', ()  => { | ||||
|       iapModule.validate.restore(); | ||||
|       iapModule.isValidated.restore(); | ||||
|       iapModule.getPurchaseData.restore(); | ||||
|       payments.createSubscription.restore(); | ||||
|       if (payments.createSubscription.restore) payments.createSubscription.restore(); | ||||
|     }); | ||||
| 
 | ||||
|     it('should throw an error if sku is empty', async () => { | ||||
|       await expect(applePayments.subscribe('', user, receipt, headers, nextPaymentProcessing)) | ||||
|         .to.eventually.be.rejected.and.to.eql({ | ||||
|           httpCode: 400, | ||||
|           name: 'BadRequest', | ||||
|           message: i18n.t('missingSubscriptionCode'), | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     it('should throw an error if receipt is invalid', async () => { | ||||
| @@ -149,26 +252,69 @@ describe('Apple Payments', ()  => { | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     it('creates a user subscription', async () => { | ||||
|     const subOptions = [ | ||||
|       { | ||||
|         sku: 'subscription1month', | ||||
|         subKey: 'basic_earned', | ||||
|       }, | ||||
|       { | ||||
|         sku: 'com.habitrpg.ios.habitica.subscription.3month', | ||||
|         subKey: 'basic_3mo', | ||||
|       }, | ||||
|       { | ||||
|         sku: 'com.habitrpg.ios.habitica.subscription.6month', | ||||
|         subKey: 'basic_6mo', | ||||
|       }, | ||||
|       { | ||||
|         sku: 'com.habitrpg.ios.habitica.subscription.12month', | ||||
|         subKey: 'basic_12mo', | ||||
|       }, | ||||
|     ]; | ||||
|     subOptions.forEach(option => { | ||||
|       it(`creates a user subscription for ${option.sku}`, async () => { | ||||
|         iapModule.getPurchaseData.restore(); | ||||
|         iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData') | ||||
|           .returns([{ | ||||
|             expirationDate: moment.utc().add({day: 1}).toDate(), | ||||
|             productId: option.sku, | ||||
|             transactionId: token, | ||||
|           }]); | ||||
|         sub = common.content.subscriptionBlocks[option.subKey]; | ||||
| 
 | ||||
|         await applePayments.subscribe(option.sku, user, receipt, headers, nextPaymentProcessing); | ||||
| 
 | ||||
|         expect(iapSetupStub).to.be.calledOnce; | ||||
|         expect(iapValidateStub).to.be.calledOnce; | ||||
|         expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt); | ||||
|         expect(iapIsValidatedStub).to.be.calledOnce; | ||||
|         expect(iapIsValidatedStub).to.be.calledWith({}); | ||||
|         expect(iapGetPurchaseDataStub).to.be.calledOnce; | ||||
| 
 | ||||
|         expect(paymentsCreateSubscritionStub).to.be.calledOnce; | ||||
|         expect(paymentsCreateSubscritionStub).to.be.calledWith({ | ||||
|           user, | ||||
|           customerId: token, | ||||
|           paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE, | ||||
|           sub, | ||||
|           headers, | ||||
|           additionalData: receipt, | ||||
|           nextPaymentProcessing, | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it('errors when a user is already subscribed', async () => { | ||||
|       payments.createSubscription.restore(); | ||||
|       user = new User(); | ||||
| 
 | ||||
|       await applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing); | ||||
| 
 | ||||
|       expect(iapSetupStub).to.be.calledOnce; | ||||
|       expect(iapValidateStub).to.be.calledOnce; | ||||
|       expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt); | ||||
|       expect(iapIsValidatedStub).to.be.calledOnce; | ||||
|       expect(iapIsValidatedStub).to.be.calledWith({}); | ||||
|       expect(iapGetPurchaseDataStub).to.be.calledOnce; | ||||
| 
 | ||||
|       expect(paymentsCreateSubscritionStub).to.be.calledOnce; | ||||
|       expect(paymentsCreateSubscritionStub).to.be.calledWith({ | ||||
|         user, | ||||
|         customerId: token, | ||||
|         paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE, | ||||
|         sub, | ||||
|         headers, | ||||
|         additionalData: receipt, | ||||
|         nextPaymentProcessing, | ||||
|       }); | ||||
|       await expect(applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing)) | ||||
|         .to.eventually.be.rejected.and.to.eql({ | ||||
|           httpCode: 401, | ||||
|           name: 'NotAuthorized', | ||||
|           message: applePayments.constants.RESPONSE_ALREADY_USED, | ||||
|         }); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
| @@ -184,9 +330,9 @@ describe('Apple Payments', ()  => { | ||||
|       expirationDate = moment.utc(); | ||||
| 
 | ||||
|       iapSetupStub = sinon.stub(iapModule, 'setup') | ||||
|         .returnsPromise().resolves(); | ||||
|         .resolves(); | ||||
|       iapValidateStub = sinon.stub(iapModule, 'validate') | ||||
|         .returnsPromise().resolves({ | ||||
|         .resolves({ | ||||
|           expirationDate, | ||||
|         }); | ||||
|       iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData') | ||||
| @@ -201,7 +347,7 @@ describe('Apple Payments', ()  => { | ||||
|       user.purchased.plan.planId = subKey; | ||||
|       user.purchased.plan.additionalData = receipt; | ||||
| 
 | ||||
|       paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({}); | ||||
|       paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').resolves({}); | ||||
|     }); | ||||
| 
 | ||||
|     afterEach(function () { | ||||
| @@ -1,11 +1,12 @@ | ||||
| /* eslint-disable camelcase */ | ||||
| import iapModule from '../../../../../website/server/libs/inAppPurchases'; | ||||
| import payments from '../../../../../website/server/libs/payments'; | ||||
| import googlePayments from '../../../../../website/server/libs/googlePayments'; | ||||
| import payments from '../../../../../website/server/libs/payments/payments'; | ||||
| import googlePayments from '../../../../../website/server/libs/payments/google'; | ||||
| import iap from '../../../../../website/server/libs/inAppPurchases'; | ||||
| import {model as User} from '../../../../../website/server/models/user'; | ||||
| import common from '../../../../../website/common'; | ||||
| import moment from 'moment'; | ||||
| import {mockFindById, restoreFindById} from '../../../../helpers/mongoose.helper'; | ||||
| 
 | ||||
| const i18n = common.i18n; | ||||
| 
 | ||||
| @@ -24,12 +25,12 @@ describe('Google Payments', ()  => { | ||||
|       headers = {}; | ||||
| 
 | ||||
|       iapSetupStub = sinon.stub(iapModule, 'setup') | ||||
|         .returnsPromise().resolves(); | ||||
|         .resolves(); | ||||
|       iapValidateStub = sinon.stub(iapModule, 'validate') | ||||
|         .returnsPromise().resolves({}); | ||||
|         .resolves({}); | ||||
|       iapIsValidatedStub = sinon.stub(iapModule, 'isValidated') | ||||
|         .returns(true); | ||||
|       paymentBuyGemsStub = sinon.stub(payments, 'buyGems').returnsPromise().resolves({}); | ||||
|       paymentBuyGemsStub = sinon.stub(payments, 'buyGems').resolves({}); | ||||
|     }); | ||||
| 
 | ||||
|     afterEach(() => { | ||||
| @@ -44,7 +45,7 @@ describe('Google Payments', ()  => { | ||||
|       iapIsValidatedStub = sinon.stub(iapModule, 'isValidated') | ||||
|         .returns(false); | ||||
| 
 | ||||
|       await expect(googlePayments.verifyGemPurchase(user, receipt, signature, headers)) | ||||
|       await expect(googlePayments.verifyGemPurchase({user, receipt, signature, headers})) | ||||
|         .to.eventually.be.rejected.and.to.eql({ | ||||
|           httpCode: 401, | ||||
|           name: 'NotAuthorized', | ||||
| @@ -55,7 +56,7 @@ describe('Google Payments', ()  => { | ||||
|     it('should throw an error if productId is invalid', async () => { | ||||
|       receipt = `{"token": "${token}", "productId": "invalid"}`; | ||||
| 
 | ||||
|       await expect(googlePayments.verifyGemPurchase(user, receipt, signature, headers)) | ||||
|       await expect(googlePayments.verifyGemPurchase({user, receipt, signature, headers})) | ||||
|         .to.eventually.be.rejected.and.to.eql({ | ||||
|           httpCode: 401, | ||||
|           name: 'NotAuthorized', | ||||
| @@ -64,9 +65,9 @@ describe('Google Payments', ()  => { | ||||
|     }); | ||||
| 
 | ||||
|     it('should throw an error if user cannot purchase gems', async () => { | ||||
|       sinon.stub(user, 'canGetGems').returnsPromise().resolves(false); | ||||
|       sinon.stub(user, 'canGetGems').resolves(false); | ||||
| 
 | ||||
|       await expect(googlePayments.verifyGemPurchase(user, receipt, signature, headers)) | ||||
|       await expect(googlePayments.verifyGemPurchase({user, receipt, signature, headers})) | ||||
|         .to.eventually.be.rejected.and.to.eql({ | ||||
|           httpCode: 401, | ||||
|           name: 'NotAuthorized', | ||||
| @@ -77,8 +78,8 @@ describe('Google Payments', ()  => { | ||||
|     }); | ||||
| 
 | ||||
|     it('purchases gems', async () => { | ||||
|       sinon.stub(user, 'canGetGems').returnsPromise().resolves(true); | ||||
|       await googlePayments.verifyGemPurchase(user, receipt, signature, headers); | ||||
|       sinon.stub(user, 'canGetGems').resolves(true); | ||||
|       await googlePayments.verifyGemPurchase({user, receipt, signature, headers}); | ||||
| 
 | ||||
|       expect(iapSetupStub).to.be.calledOnce; | ||||
|       expect(iapValidateStub).to.be.calledOnce; | ||||
| @@ -99,6 +100,34 @@ describe('Google Payments', ()  => { | ||||
|       expect(user.canGetGems).to.be.calledOnce; | ||||
|       user.canGetGems.restore(); | ||||
|     }); | ||||
| 
 | ||||
|     it('gifts gems', async () => { | ||||
|       const receivingUser = new User(); | ||||
|       await receivingUser.save(); | ||||
| 
 | ||||
|       mockFindById(receivingUser); | ||||
| 
 | ||||
|       const gift = {uuid: receivingUser._id}; | ||||
|       await googlePayments.verifyGemPurchase({user, gift, receipt, signature, headers}); | ||||
| 
 | ||||
|       expect(iapSetupStub).to.be.calledOnce; | ||||
|       expect(iapValidateStub).to.be.calledOnce; | ||||
|       expect(iapValidateStub).to.be.calledWith(iap.GOOGLE, { | ||||
|         data: receipt, | ||||
|         signature, | ||||
|       }); | ||||
|       expect(iapIsValidatedStub).to.be.calledOnce; | ||||
|       expect(iapIsValidatedStub).to.be.calledWith({}); | ||||
| 
 | ||||
|       expect(paymentBuyGemsStub).to.be.calledOnce; | ||||
|       expect(paymentBuyGemsStub).to.be.calledWith({ | ||||
|         user: receivingUser, | ||||
|         paymentMethod: googlePayments.constants.PAYMENT_METHOD_GOOGLE, | ||||
|         amount: 5.25, | ||||
|         headers, | ||||
|       }); | ||||
|       restoreFindById(); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('subscribe', () => { | ||||
| @@ -116,12 +145,12 @@ describe('Google Payments', ()  => { | ||||
|       nextPaymentProcessing = moment.utc().add({days: 2}); | ||||
| 
 | ||||
|       iapSetupStub = sinon.stub(iapModule, 'setup') | ||||
|         .returnsPromise().resolves(); | ||||
|         .resolves(); | ||||
|       iapValidateStub = sinon.stub(iapModule, 'validate') | ||||
|         .returnsPromise().resolves({}); | ||||
|         .resolves({}); | ||||
|       iapIsValidatedStub = sinon.stub(iapModule, 'isValidated') | ||||
|         .returns(true); | ||||
|       paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({}); | ||||
|       paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').resolves({}); | ||||
|     }); | ||||
| 
 | ||||
|     afterEach(() => { | ||||
| @@ -193,9 +222,9 @@ describe('Google Payments', ()  => { | ||||
|       expirationDate = moment.utc(); | ||||
| 
 | ||||
|       iapSetupStub = sinon.stub(iapModule, 'setup') | ||||
|         .returnsPromise().resolves(); | ||||
|         .resolves(); | ||||
|       iapValidateStub = sinon.stub(iapModule, 'validate') | ||||
|         .returnsPromise().resolves({ | ||||
|         .resolves({ | ||||
|           expirationDate, | ||||
|         }); | ||||
|       iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData') | ||||
| @@ -210,7 +239,7 @@ describe('Google Payments', ()  => { | ||||
|       user.purchased.plan.planId = subKey; | ||||
|       user.purchased.plan.additionalData = {data: receipt, signature}; | ||||
| 
 | ||||
|       paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({}); | ||||
|       paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').resolves({}); | ||||
|     }); | ||||
| 
 | ||||
|     afterEach(function () { | ||||
| @@ -1,13 +1,13 @@ | ||||
| import moment from 'moment'; | ||||
| 
 | ||||
| import * as sender from '../../../../../../../website/server/libs/email'; | ||||
| import * as api from '../../../../../../../website/server/libs/payments'; | ||||
| import { model as User } from '../../../../../../../website/server/models/user'; | ||||
| import { model as Group } from '../../../../../../../website/server/models/group'; | ||||
| import * as sender from '../../../../../../website/server/libs/email'; | ||||
| import * as api from '../../../../../../website/server/libs/payments/payments'; | ||||
| import { model as User } from '../../../../../../website/server/models/user'; | ||||
| import { model as Group } from '../../../../../../website/server/models/group'; | ||||
| import { | ||||
|   generateGroup, | ||||
| } from '../../../../../../helpers/api-unit.helper.js'; | ||||
| import i18n from '../../../../../../../website/common/script/i18n'; | ||||
| } from '../../../../../helpers/api-unit.helper.js'; | ||||
| import i18n from '../../../../../../website/common/script/i18n'; | ||||
| 
 | ||||
| describe('Canceling a subscription for group', () => { | ||||
|   let plan, group, user, data; | ||||
| @@ -2,16 +2,16 @@ import moment from 'moment'; | ||||
| import stripeModule from 'stripe'; | ||||
| import nconf from 'nconf'; | ||||
| 
 | ||||
| import * as sender from '../../../../../../../website/server/libs/email'; | ||||
| import * as api from '../../../../../../../website/server/libs/payments'; | ||||
| import amzLib from '../../../../../../../website/server/libs/amazonPayments'; | ||||
| import stripePayments from '../../../../../../../website/server/libs/stripePayments'; | ||||
| import paypalPayments from '../../../../../../../website/server/libs/paypalPayments'; | ||||
| import { model as User } from '../../../../../../../website/server/models/user'; | ||||
| import { model as Group } from '../../../../../../../website/server/models/group'; | ||||
| import * as sender from '../../../../../../website/server/libs/email'; | ||||
| import * as api from '../../../../../../website/server/libs/payments/payments'; | ||||
| import amzLib from '../../../../../../website/server/libs/payments/amazon'; | ||||
| import paypalPayments from '../../../../../../website/server/libs/payments/paypal'; | ||||
| import stripePayments from '../../../../../../website/server/libs/payments/stripe'; | ||||
| import { model as User } from '../../../../../../website/server/models/user'; | ||||
| import { model as Group } from '../../../../../../website/server/models/group'; | ||||
| import { | ||||
|   generateGroup, | ||||
| } from '../../../../../../helpers/api-unit.helper.js'; | ||||
| } from '../../../../../helpers/api-unit.helper.js'; | ||||
| 
 | ||||
| describe('Purchasing a group plan for group', () => { | ||||
|   const EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_GOOGLE = 'Google_subscription'; | ||||
| @@ -69,11 +69,11 @@ describe('Purchasing a group plan for group', () => { | ||||
|     }; | ||||
| 
 | ||||
|     let subscriptionId = 'subId'; | ||||
|     sinon.stub(stripe.customers, 'del').returnsPromise().resolves({}); | ||||
|     sinon.stub(stripe.customers, 'del').resolves({}); | ||||
| 
 | ||||
|     let currentPeriodEndTimeStamp = moment().add(3, 'months').unix(); | ||||
|     sinon.stub(stripe.customers, 'retrieve') | ||||
|       .returnsPromise().resolves({ | ||||
|       .resolves({ | ||||
|         subscriptions: { | ||||
|           data: [{id: subscriptionId, current_period_end: currentPeriodEndTimeStamp}], // eslint-disable-line camelcase
 | ||||
|         }, | ||||
| @@ -216,7 +216,6 @@ describe('Purchasing a group plan for group', () => { | ||||
| 
 | ||||
|   it('sends one email to subscribed member of group, stating subscription is cancelled (Amazon)', async () => { | ||||
|     sinon.stub(amzLib, 'getBillingAgreementDetails') | ||||
|       .returnsPromise() | ||||
|       .resolves({ | ||||
|         BillingAgreementDetails: { | ||||
|           BillingAgreementStatus: {State: 'Closed'}, | ||||
| @@ -251,9 +250,9 @@ describe('Purchasing a group plan for group', () => { | ||||
|   }); | ||||
| 
 | ||||
|   it('sends one email to subscribed member of group, stating subscription is cancelled (PayPal)', async () => { | ||||
|     sinon.stub(paypalPayments, 'paypalBillingAgreementCancel').returnsPromise().resolves({}); | ||||
|     sinon.stub(paypalPayments, 'paypalBillingAgreementCancel').resolves({}); | ||||
|     sinon.stub(paypalPayments, 'paypalBillingAgreementGet') | ||||
|       .returnsPromise().resolves({ | ||||
|       .resolves({ | ||||
|         agreement_details: { // eslint-disable-line camelcase
 | ||||
|           next_billing_date: moment().add(3, 'months').toDate(), // eslint-disable-line camelcase
 | ||||
|           cycles_completed: 1, // eslint-disable-line camelcase
 | ||||
| @@ -443,14 +442,12 @@ describe('Purchasing a group plan for group', () => { | ||||
| 
 | ||||
|     await api.createSubscription(data); | ||||
| 
 | ||||
|     let updatedUser = await User.findById(recipient._id).exec(); | ||||
| 
 | ||||
|     const updatedUser = await User.findById(recipient._id).exec(); | ||||
|     expect(updatedUser.purchased.plan.extraMonths).to.within(2, 3); | ||||
|   }); | ||||
| 
 | ||||
|   it('adds months to members with existing recurring subscription (Amazon)', async () => { | ||||
|     sinon.stub(amzLib, 'getBillingAgreementDetails') | ||||
|       .returnsPromise() | ||||
|       .resolves({ | ||||
|         BillingAgreementDetails: { | ||||
|           BillingAgreementStatus: {State: 'Closed'}, | ||||
| @@ -479,9 +476,9 @@ describe('Purchasing a group plan for group', () => { | ||||
|   }); | ||||
| 
 | ||||
|   it('adds months to members with existing recurring subscription (Paypal)', async () => { | ||||
|     sinon.stub(paypalPayments, 'paypalBillingAgreementCancel').returnsPromise().resolves({}); | ||||
|     sinon.stub(paypalPayments, 'paypalBillingAgreementCancel').resolves({}); | ||||
|     sinon.stub(paypalPayments, 'paypalBillingAgreementGet') | ||||
|       .returnsPromise().resolves({ | ||||
|       .resolves({ | ||||
|         agreement_details: { // eslint-disable-line camelcase
 | ||||
|           next_billing_date: moment().add(3, 'months').toDate(), // eslint-disable-line camelcase
 | ||||
|           cycles_completed: 1, // eslint-disable-line camelcase
 | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { model as User } from '../../../../../../website/server/models/user'; | ||||
| import { model as User } from '../../../../../website/server/models/user'; | ||||
| 
 | ||||
| export async function createNonLeaderGroupMember (group) { | ||||
|   let nonLeader = new User(); | ||||
| @@ -1,11 +1,11 @@ | ||||
| import moment from 'moment'; | ||||
| 
 | ||||
| import * as sender from '../../../../../website/server/libs/email'; | ||||
| import * as api from '../../../../../website/server/libs/payments'; | ||||
| import * as api from '../../../../../website/server/libs/payments/payments'; | ||||
| import analytics from '../../../../../website/server/libs/analyticsService'; | ||||
| import notifications from '../../../../../website/server/libs/pushNotifications'; | ||||
| import { model as User } from '../../../../../website/server/models/user'; | ||||
| import { translate as t } from '../../../../helpers/api-v3-integration.helper'; | ||||
| import { translate as t } from '../../../../helpers/api-integration/v3'; | ||||
| import { | ||||
|   generateGroup, | ||||
| } from '../../../../helpers/api-unit.helper.js'; | ||||
| @@ -210,7 +210,7 @@ describe('payments/index', () => { | ||||
|         let msg = '\`Hello recipient, sender has sent you 3 months of subscription!\`'; | ||||
| 
 | ||||
|         expect(user.sendMessage).to.be.calledOnce; | ||||
|         expect(user.sendMessage).to.be.calledWith(recipient, { receiverMsg: msg, senderMsg: msg }); | ||||
|         expect(user.sendMessage).to.be.calledWith(recipient, { receiverMsg: msg, senderMsg: msg, save: false }); | ||||
|       }); | ||||
| 
 | ||||
|       it('sends an email about the gift', async () => { | ||||
| @@ -446,6 +446,19 @@ describe('payments/index', () => { | ||||
|         fakeClock.restore(); | ||||
|       }); | ||||
| 
 | ||||
|       it('does not add a notification for mystery items if none was awarded', async () => { | ||||
|         const noMysteryItemTimeframe = 1462183920000; // May 2nd 2016
 | ||||
|         let fakeClock = sinon.useFakeTimers(noMysteryItemTimeframe); | ||||
|         data = { paymentMethod: 'PaymentMethod', user, sub: { key: 'basic_3mo' } }; | ||||
| 
 | ||||
|         await api.createSubscription(data); | ||||
| 
 | ||||
|         expect(user.purchased.plan.mysteryItems).to.have.a.lengthOf(0); | ||||
|         expect(user.notifications.find(n => n.type === 'NEW_MYSTERY_ITEMS')).to.be.undefined; | ||||
| 
 | ||||
|         fakeClock.restore(); | ||||
|       }); | ||||
| 
 | ||||
|       it('does not award mystery item when user already owns the item', async () => { | ||||
|         let mayMysteryItemTimeframe = 1464725113000; // May 31st 2016
 | ||||
|         let fakeClock = sinon.useFakeTimers(mayMysteryItemTimeframe); | ||||
| @@ -629,7 +642,16 @@ describe('payments/index', () => { | ||||
|         await api.buyGems(data); | ||||
|         let msg = '\`Hello recipient, sender has sent you 4 gems!\`'; | ||||
| 
 | ||||
|         expect(user.sendMessage).to.be.calledWith(recipient, { receiverMsg: msg, senderMsg: msg }); | ||||
|         expect(user.sendMessage).to.be.calledWith(recipient, { receiverMsg: msg, senderMsg: msg, save: false }); | ||||
|       }); | ||||
| 
 | ||||
|       it('sends a message from purchaser to recipient wtih custom message', async () => { | ||||
|         data.gift.message = 'giftmessage'; | ||||
| 
 | ||||
|         await api.buyGems(data); | ||||
| 
 | ||||
|         const msg = `\`Hello recipient, sender has sent you 4 gems!\` ${data.gift.message}`; | ||||
|         expect(user.sendMessage).to.be.calledWith(recipient, { receiverMsg: msg, senderMsg: msg, save: false }); | ||||
|       }); | ||||
| 
 | ||||
|       it('sends a push notification if user did not gift to self', async () => { | ||||
| @@ -658,7 +680,7 @@ describe('payments/index', () => { | ||||
|           return `\`${messageContent}\``; | ||||
|         }); | ||||
| 
 | ||||
|         expect(user.sendMessage).to.be.calledWith(recipient, { receiverMsg: recipientsMessageContent, senderMsg: sendersMessageContent }); | ||||
|         expect(user.sendMessage).to.be.calledWith(recipient, { receiverMsg: recipientsMessageContent, senderMsg: sendersMessageContent, save: false }); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| @@ -1,7 +1,7 @@ | ||||
| /* eslint-disable camelcase */ | ||||
| import payments from '../../../../../../../website/server/libs/payments'; | ||||
| import paypalPayments from '../../../../../../../website/server/libs/paypalPayments'; | ||||
| import { model as User } from '../../../../../../../website/server/models/user'; | ||||
| import paypalPayments from '../../../../../../website/server/libs/payments/paypal'; | ||||
| import payments from '../../../../../../website/server/libs/payments/payments'; | ||||
| import { model as User } from '../../../../../../website/server/models/user'; | ||||
| 
 | ||||
| describe('checkout success', () => { | ||||
|   const subKey = 'basic_3mo'; | ||||
| @@ -13,9 +13,9 @@ describe('checkout success', () => { | ||||
|     customerId = 'customerId-test'; | ||||
|     paymentId = 'paymentId-test'; | ||||
| 
 | ||||
|     paypalPaymentExecuteStub = sinon.stub(paypalPayments, 'paypalPaymentExecute').returnsPromise().resolves({}); | ||||
|     paymentBuyGemsStub = sinon.stub(payments, 'buyGems').returnsPromise().resolves({}); | ||||
|     paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({}); | ||||
|     paypalPaymentExecuteStub = sinon.stub(paypalPayments, 'paypalPaymentExecute').resolves({}); | ||||
|     paymentBuyGemsStub = sinon.stub(payments, 'buyGems').resolves({}); | ||||
|     paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').resolves({}); | ||||
|   }); | ||||
| 
 | ||||
|   afterEach(() => { | ||||
| @@ -1,9 +1,9 @@ | ||||
| /* eslint-disable camelcase */ | ||||
| import nconf from 'nconf'; | ||||
| 
 | ||||
| import paypalPayments from '../../../../../../../website/server/libs/paypalPayments'; | ||||
| import { model as User } from '../../../../../../../website/server/models/user'; | ||||
| import common from '../../../../../../../website/common'; | ||||
| import paypalPayments from '../../../../../../website/server/libs/payments/paypal'; | ||||
| import { model as User } from '../../../../../../website/server/models/user'; | ||||
| import common from '../../../../../../website/common'; | ||||
| 
 | ||||
| const BASE_URL = nconf.get('BASE_URL'); | ||||
| const i18n = common.i18n; | ||||
| @@ -15,6 +15,7 @@ describe('checkout', () => { | ||||
| 
 | ||||
|   function getPaypalCreateOptions (description, amount) { | ||||
|     return { | ||||
|       experience_profile_id: 'xp_profile_id', | ||||
|       intent: 'sale', | ||||
|       payer: { payment_method: 'Paypal' }, | ||||
|       redirect_urls: { | ||||
| @@ -42,7 +43,7 @@ describe('checkout', () => { | ||||
|   beforeEach(() => { | ||||
|     approvalHerf = 'approval_href'; | ||||
|     paypalPaymentCreateStub = sinon.stub(paypalPayments, 'paypalPaymentCreate') | ||||
|       .returnsPromise().resolves({ | ||||
|       .resolves({ | ||||
|         links: [{ rel: 'approval_url', href: approvalHerf }], | ||||
|       }); | ||||
|   }); | ||||
| @@ -80,7 +81,7 @@ describe('checkout', () => { | ||||
| 
 | ||||
|   it('should error if the user cannot get gems', async () => { | ||||
|     let user = new User(); | ||||
|     sinon.stub(user, 'canGetGems').returnsPromise().resolves(false); | ||||
|     sinon.stub(user, 'canGetGems').resolves(false); | ||||
| 
 | ||||
|     await expect(paypalPayments.checkout({user})).to.eventually.be.rejected.and.to.eql({ | ||||
|       httpCode: 401, | ||||
| @@ -1,10 +1,10 @@ | ||||
| /* eslint-disable camelcase */ | ||||
| import payments from '../../../../../../../website/server/libs/payments'; | ||||
| import paypalPayments from '../../../../../../../website/server/libs/paypalPayments'; | ||||
| import paypalPayments from '../../../../../../website/server/libs/payments/paypal'; | ||||
| import payments from '../../../../../../website/server/libs/payments/payments'; | ||||
| import { | ||||
|   generateGroup, | ||||
| } from '../../../../../../helpers/api-unit.helper.js'; | ||||
| import { model as User } from '../../../../../../../website/server/models/user'; | ||||
| } from '../../../../../helpers/api-unit.helper.js'; | ||||
| import { model as User } from '../../../../../../website/server/models/user'; | ||||
| 
 | ||||
| describe('ipn', () => { | ||||
|   const subKey = 'basic_3mo'; | ||||
| @@ -34,8 +34,8 @@ describe('ipn', () => { | ||||
|     group.purchased.plan.lastBillingDate = new Date(); | ||||
|     await group.save(); | ||||
| 
 | ||||
|     ipnVerifyAsyncStub = sinon.stub(paypalPayments, 'ipnVerifyAsync').returnsPromise().resolves({}); | ||||
|     paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({}); | ||||
|     ipnVerifyAsyncStub = sinon.stub(paypalPayments, 'ipnVerifyAsync').resolves({}); | ||||
|     paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').resolves({}); | ||||
|   }); | ||||
| 
 | ||||
|   afterEach(function () { | ||||
| @@ -1,11 +1,11 @@ | ||||
| /* eslint-disable camelcase */ | ||||
| import payments from '../../../../../../../website/server/libs/payments'; | ||||
| import paypalPayments from '../../../../../../../website/server/libs/paypalPayments'; | ||||
| import paypalPayments from '../../../../../../website/server/libs/payments/paypal'; | ||||
| import payments from '../../../../../../website/server/libs/payments/payments'; | ||||
| import { | ||||
|   generateGroup, | ||||
| } from '../../../../../../helpers/api-unit.helper.js'; | ||||
| import { model as User } from '../../../../../../../website/server/models/user'; | ||||
| import common from '../../../../../../../website/common'; | ||||
| } from '../../../../../helpers/api-unit.helper.js'; | ||||
| import { model as User } from '../../../../../../website/server/models/user'; | ||||
| import common from '../../../../../../website/common'; | ||||
| import { createNonLeaderGroupMember } from '../paymentHelpers'; | ||||
| 
 | ||||
| const i18n = common.i18n; | ||||
| @@ -38,15 +38,15 @@ describe('subscribeCancel', () => { | ||||
| 
 | ||||
|     nextBillingDate = new Date(); | ||||
| 
 | ||||
|     paypalBillingAgreementCancelStub = sinon.stub(paypalPayments, 'paypalBillingAgreementCancel').returnsPromise().resolves({}); | ||||
|     paypalBillingAgreementCancelStub = sinon.stub(paypalPayments, 'paypalBillingAgreementCancel').resolves({}); | ||||
|     paypalBillingAgreementGetStub = sinon.stub(paypalPayments, 'paypalBillingAgreementGet') | ||||
|       .returnsPromise().resolves({ | ||||
|       .resolves({ | ||||
|         agreement_details: { | ||||
|           next_billing_date: nextBillingDate, | ||||
|           cycles_completed: 1, | ||||
|         }, | ||||
|       }); | ||||
|     paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({}); | ||||
|     paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').resolves({}); | ||||
|   }); | ||||
| 
 | ||||
|   afterEach(function () { | ||||
| @@ -1,11 +1,11 @@ | ||||
| /* eslint-disable camelcase */ | ||||
| import payments from '../../../../../../../website/server/libs/payments'; | ||||
| import paypalPayments from '../../../../../../../website/server/libs/paypalPayments'; | ||||
| import paypalPayments from '../../../../../../website/server/libs/payments/paypal'; | ||||
| import payments from '../../../../../../website/server/libs/payments/payments'; | ||||
| import { | ||||
|   generateGroup, | ||||
| } from '../../../../../../helpers/api-unit.helper.js'; | ||||
| import { model as User } from '../../../../../../../website/server/models/user'; | ||||
| import common from '../../../../../../../website/common'; | ||||
| } from '../../../../../helpers/api-unit.helper.js'; | ||||
| import { model as User } from '../../../../../../website/server/models/user'; | ||||
| import common from '../../../../../../website/common'; | ||||
| 
 | ||||
| describe('subscribeSuccess', () => { | ||||
|   const subKey = 'basic_3mo'; | ||||
| @@ -28,10 +28,10 @@ describe('subscribeSuccess', () => { | ||||
|     customerId = 'test-customerId'; | ||||
| 
 | ||||
|     paypalBillingAgreementExecuteStub = sinon.stub(paypalPayments, 'paypalBillingAgreementExecute') | ||||
|       .returnsPromise({}).resolves({ | ||||
|       .resolves({ | ||||
|         id: customerId, | ||||
|       }); | ||||
|     paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({}); | ||||
|     paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').resolves({}); | ||||
|   }); | ||||
| 
 | ||||
|   afterEach(() => { | ||||
| @@ -2,9 +2,9 @@ | ||||
| import moment from 'moment'; | ||||
| import cc from 'coupon-code'; | ||||
| 
 | ||||
| import paypalPayments from '../../../../../../../website/server/libs/paypalPayments'; | ||||
| import { model as Coupon } from '../../../../../../../website/server/models/coupon'; | ||||
| import common from '../../../../../../../website/common'; | ||||
| import paypalPayments from '../../../../../../website/server/libs/payments/paypal'; | ||||
| import { model as Coupon } from '../../../../../../website/server/models/coupon'; | ||||
| import common from '../../../../../../website/common'; | ||||
| 
 | ||||
| const i18n = common.i18n; | ||||
| 
 | ||||
| @@ -18,7 +18,7 @@ describe('subscribe', () => { | ||||
|     sub = Object.assign({}, common.content.subscriptionBlocks[subKey]); | ||||
| 
 | ||||
|     paypalBillingAgreementCreateStub = sinon.stub(paypalPayments, 'paypalBillingAgreementCreate') | ||||
|       .returnsPromise().resolves({ | ||||
|       .resolves({ | ||||
|         links: [{ rel: 'approval_url', href: approvalHerf }], | ||||
|       }); | ||||
|   }); | ||||
| @@ -2,11 +2,11 @@ import stripeModule from 'stripe'; | ||||
| 
 | ||||
| import { | ||||
|   generateGroup, | ||||
| } from '../../../../../../helpers/api-unit.helper.js'; | ||||
| import { model as User } from '../../../../../../../website/server/models/user'; | ||||
| import stripePayments from '../../../../../../../website/server/libs/stripePayments'; | ||||
| import payments from '../../../../../../../website/server/libs/payments'; | ||||
| import common from '../../../../../../../website/common'; | ||||
| } from '../../../../../helpers/api-unit.helper.js'; | ||||
| import { model as User } from '../../../../../../website/server/models/user'; | ||||
| import stripePayments from '../../../../../../website/server/libs/payments/stripe'; | ||||
| import payments from '../../../../../../website/server/libs/payments/payments'; | ||||
| import common from '../../../../../../website/common'; | ||||
| 
 | ||||
| const i18n = common.i18n; | ||||
| 
 | ||||
| @@ -82,12 +82,12 @@ describe('cancel subscription', () => { | ||||
| 
 | ||||
|     beforeEach(() => { | ||||
|       subscriptionId = 'subId'; | ||||
|       stripeDeleteCustomerStub = sinon.stub(stripe.customers, 'del').returnsPromise().resolves({}); | ||||
|       paymentsCancelSubStub = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({}); | ||||
|       stripeDeleteCustomerStub = sinon.stub(stripe.customers, 'del').resolves({}); | ||||
|       paymentsCancelSubStub = sinon.stub(payments, 'cancelSubscription').resolves({}); | ||||
| 
 | ||||
|       currentPeriodEndTimeStamp = (new Date()).getTime(); | ||||
|       stripeRetrieveStub = sinon.stub(stripe.customers, 'retrieve') | ||||
|         .returnsPromise().resolves({ | ||||
|         .resolves({ | ||||
|           subscriptions: { | ||||
|             data: [{id: subscriptionId, current_period_end: currentPeriodEndTimeStamp}], // eslint-disable-line camelcase
 | ||||
|           }, | ||||
| @@ -3,12 +3,12 @@ import cc from 'coupon-code'; | ||||
| 
 | ||||
| import { | ||||
|   generateGroup, | ||||
| } from '../../../../../../helpers/api-unit.helper.js'; | ||||
| import { model as User } from '../../../../../../../website/server/models/user'; | ||||
| import { model as Coupon } from '../../../../../../../website/server/models/coupon'; | ||||
| import stripePayments from '../../../../../../../website/server/libs/stripePayments'; | ||||
| import payments from '../../../../../../../website/server/libs/payments'; | ||||
| import common from '../../../../../../../website/common'; | ||||
| } from '../../../../../helpers/api-unit.helper.js'; | ||||
| import { model as User } from '../../../../../../website/server/models/user'; | ||||
| import { model as Coupon } from '../../../../../../website/server/models/coupon'; | ||||
| import stripePayments from '../../../../../../website/server/libs/payments/stripe'; | ||||
| import payments from '../../../../../../website/server/libs/payments/payments'; | ||||
| import common from '../../../../../../website/common'; | ||||
| 
 | ||||
| const i18n = common.i18n; | ||||
| 
 | ||||
| @@ -54,7 +54,7 @@ describe('checkout with subscription', () => { | ||||
|     token = 'test-token'; | ||||
| 
 | ||||
|     spy = sinon.stub(stripe.subscriptions, 'update'); | ||||
|     spy.returnsPromise().resolves; | ||||
|     spy.resolves; | ||||
| 
 | ||||
|     stripeCreateCustomerSpy = sinon.stub(stripe.customers, 'create'); | ||||
|     let stripCustomerResponse = { | ||||
| @@ -63,10 +63,10 @@ describe('checkout with subscription', () => { | ||||
|         data: [{id: subscriptionId}], | ||||
|       }, | ||||
|     }; | ||||
|     stripeCreateCustomerSpy.returnsPromise().resolves(stripCustomerResponse); | ||||
|     stripeCreateCustomerSpy.resolves(stripCustomerResponse); | ||||
| 
 | ||||
|     stripePaymentsCreateSubSpy = sinon.stub(payments, 'createSubscription'); | ||||
|     stripePaymentsCreateSubSpy.returnsPromise().resolves({}); | ||||
|     stripePaymentsCreateSubSpy.resolves({}); | ||||
| 
 | ||||
|     data.groupId = group._id; | ||||
|     data.sub.quantity = 3; | ||||
| @@ -1,9 +1,9 @@ | ||||
| import stripeModule from 'stripe'; | ||||
| 
 | ||||
| import { model as User } from '../../../../../../../website/server/models/user'; | ||||
| import stripePayments from '../../../../../../../website/server/libs/stripePayments'; | ||||
| import payments from '../../../../../../../website/server/libs/payments'; | ||||
| import common from '../../../../../../../website/common'; | ||||
| import { model as User } from '../../../../../../website/server/models/user'; | ||||
| import stripePayments from '../../../../../../website/server/libs/payments/stripe'; | ||||
| import payments from '../../../../../../website/server/libs/payments/payments'; | ||||
| import common from '../../../../../../website/common'; | ||||
| 
 | ||||
| const i18n = common.i18n; | ||||
| 
 | ||||
| @@ -26,9 +26,9 @@ describe('checkout', () => { | ||||
|     let stripCustomerResponse = { | ||||
|       id: customerIdResponse, | ||||
|     }; | ||||
|     stripeChargeStub = sinon.stub(stripe.charges, 'create').returnsPromise().resolves(stripCustomerResponse); | ||||
|     paymentBuyGemsStub = sinon.stub(payments, 'buyGems').returnsPromise().resolves({}); | ||||
|     paymentCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({}); | ||||
|     stripeChargeStub = sinon.stub(stripe.charges, 'create').resolves(stripCustomerResponse); | ||||
|     paymentBuyGemsStub = sinon.stub(payments, 'buyGems').resolves({}); | ||||
|     paymentCreateSubscritionStub = sinon.stub(payments, 'createSubscription').resolves({}); | ||||
|   }); | ||||
| 
 | ||||
|   afterEach(() => { | ||||
| @@ -37,6 +37,22 @@ describe('checkout', () => { | ||||
|     payments.createSubscription.restore(); | ||||
|   }); | ||||
| 
 | ||||
|   it('should error if there is no token', async () => { | ||||
|     await expect(stripePayments.checkout({ | ||||
|       user, | ||||
|       gift, | ||||
|       groupId, | ||||
|       email, | ||||
|       headers, | ||||
|       coupon, | ||||
|     }, stripe)) | ||||
|       .to.eventually.be.rejected.and.to.eql({ | ||||
|         httpCode: 400, | ||||
|         message: 'Missing req.body.id', | ||||
|         name: 'BadRequest', | ||||
|       }); | ||||
|   }); | ||||
| 
 | ||||
|   it('should error if gem amount is too low', async () => { | ||||
|     let receivingUser = new User(); | ||||
|     receivingUser.save(); | ||||
| @@ -64,10 +80,9 @@ describe('checkout', () => { | ||||
|       }); | ||||
|   }); | ||||
| 
 | ||||
| 
 | ||||
|   it('should error if user cannot get gems', async () => { | ||||
|     gift = undefined; | ||||
|     sinon.stub(user, 'canGetGems').returnsPromise().resolves(false); | ||||
|     sinon.stub(user, 'canGetGems').resolves(false); | ||||
| 
 | ||||
|     await expect(stripePayments.checkout({ | ||||
|       token, | ||||
| @@ -86,7 +101,7 @@ describe('checkout', () => { | ||||
| 
 | ||||
|   it('should purchase gems', async () => { | ||||
|     gift = undefined; | ||||
|     sinon.stub(user, 'canGetGems').returnsPromise().resolves(true); | ||||
|     sinon.stub(user, 'canGetGems').resolves(true); | ||||
| 
 | ||||
|     await stripePayments.checkout({ | ||||
|       token, | ||||
| @@ -2,10 +2,10 @@ import stripeModule from 'stripe'; | ||||
| 
 | ||||
| import { | ||||
|   generateGroup, | ||||
| } from '../../../../../../helpers/api-unit.helper.js'; | ||||
| import { model as User } from '../../../../../../../website/server/models/user'; | ||||
| import stripePayments from '../../../../../../../website/server/libs/stripePayments'; | ||||
| import common from '../../../../../../../website/common'; | ||||
| } from '../../../../../helpers/api-unit.helper.js'; | ||||
| import { model as User } from '../../../../../../website/server/models/user'; | ||||
| import stripePayments from '../../../../../../website/server/libs/payments/stripe'; | ||||
| import common from '../../../../../../website/common'; | ||||
| 
 | ||||
| const i18n = common.i18n; | ||||
| 
 | ||||
| @@ -98,11 +98,11 @@ describe('edit subscription', () => { | ||||
|     beforeEach(() => { | ||||
|       subscriptionId = 'subId'; | ||||
|       stripeListSubscriptionStub = sinon.stub(stripe.customers, 'listSubscriptions') | ||||
|         .returnsPromise().resolves({ | ||||
|         .resolves({ | ||||
|           data: [{id: subscriptionId}], | ||||
|         }); | ||||
| 
 | ||||
|       stripeUpdateSubscriptionStub = sinon.stub(stripe.customers, 'updateSubscription').returnsPromise().resolves({}); | ||||
|       stripeUpdateSubscriptionStub = sinon.stub(stripe.customers, 'updateSubscription').resolves({}); | ||||
|     }); | ||||
| 
 | ||||
|     afterEach(() => { | ||||
| @@ -2,12 +2,12 @@ import stripeModule from 'stripe'; | ||||
| 
 | ||||
| import { | ||||
|   generateGroup, | ||||
| } from '../../../../../../helpers/api-unit.helper.js'; | ||||
| import { model as User } from '../../../../../../../website/server/models/user'; | ||||
| import stripePayments from '../../../../../../../website/server/libs/stripePayments'; | ||||
| import payments from '../../../../../../../website/server/libs/payments'; | ||||
| import common from '../../../../../../../website/common'; | ||||
| import logger from '../../../../../../../website/server/libs/logger'; | ||||
| } from '../../../../../helpers/api-unit.helper.js'; | ||||
| import { model as User } from '../../../../../../website/server/models/user'; | ||||
| import stripePayments from '../../../../../../website/server/libs/payments/stripe'; | ||||
| import payments from '../../../../../../website/server/libs/payments/payments'; | ||||
| import common from '../../../../../../website/common'; | ||||
| import logger from '../../../../../../website/server/libs/logger'; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
| import moment from 'moment'; | ||||
| 
 | ||||
| @@ -22,7 +22,7 @@ describe('Stripe - Webhooks', () => { | ||||
|     const eventRetrieved = {type: eventType}; | ||||
| 
 | ||||
|     beforeEach(() => { | ||||
|       sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves(eventRetrieved); | ||||
|       sinon.stub(stripe.events, 'retrieve').resolves(eventRetrieved); | ||||
|       sinon.stub(logger, 'error'); | ||||
|     }); | ||||
| 
 | ||||
| @@ -52,8 +52,8 @@ describe('Stripe - Webhooks', () => { | ||||
|     const eventType = 'customer.subscription.deleted'; | ||||
| 
 | ||||
|     beforeEach(() => { | ||||
|       sinon.stub(stripe.customers, 'del').returnsPromise().resolves({}); | ||||
|       sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({}); | ||||
|       sinon.stub(stripe.customers, 'del').resolves({}); | ||||
|       sinon.stub(payments, 'cancelSubscription').resolves({}); | ||||
|     }); | ||||
| 
 | ||||
|     afterEach(() => { | ||||
| @@ -62,7 +62,7 @@ describe('Stripe - Webhooks', () => { | ||||
|     }); | ||||
| 
 | ||||
|     it('does not do anything if event.request is null (subscription cancelled manually)', async () => { | ||||
|       sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({ | ||||
|       sinon.stub(stripe.events, 'retrieve').resolves({ | ||||
|         id: 123, | ||||
|         type: eventType, | ||||
|         request: 123, | ||||
| @@ -79,7 +79,7 @@ describe('Stripe - Webhooks', () => { | ||||
|     describe('user subscription', () => { | ||||
|       it('throws an error if the user is not found', async () => { | ||||
|         const customerId = 456; | ||||
|         sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({ | ||||
|         sinon.stub(stripe.events, 'retrieve').resolves({ | ||||
|           id: 123, | ||||
|           type: eventType, | ||||
|           data: { | ||||
| @@ -113,7 +113,7 @@ describe('Stripe - Webhooks', () => { | ||||
|         subscriber.purchased.plan.paymentMethod = 'Stripe'; | ||||
|         await subscriber.save(); | ||||
| 
 | ||||
|         sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({ | ||||
|         sinon.stub(stripe.events, 'retrieve').resolves({ | ||||
|           id: 123, | ||||
|           type: eventType, | ||||
|           data: { | ||||
| @@ -146,7 +146,7 @@ describe('Stripe - Webhooks', () => { | ||||
|     describe('group plan subscription', () => { | ||||
|       it('throws an error if the group is not found', async () => { | ||||
|         const customerId = 456; | ||||
|         sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({ | ||||
|         sinon.stub(stripe.events, 'retrieve').resolves({ | ||||
|           id: 123, | ||||
|           type: eventType, | ||||
|           data: { | ||||
| @@ -185,7 +185,7 @@ describe('Stripe - Webhooks', () => { | ||||
|         subscriber.purchased.plan.paymentMethod = 'Stripe'; | ||||
|         await subscriber.save(); | ||||
| 
 | ||||
|         sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({ | ||||
|         sinon.stub(stripe.events, 'retrieve').resolves({ | ||||
|           id: 123, | ||||
|           type: eventType, | ||||
|           data: { | ||||
| @@ -227,7 +227,7 @@ describe('Stripe - Webhooks', () => { | ||||
|         subscriber.purchased.plan.paymentMethod = 'Stripe'; | ||||
|         await subscriber.save(); | ||||
| 
 | ||||
|         sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({ | ||||
|         sinon.stub(stripe.events, 'retrieve').resolves({ | ||||
|           id: 123, | ||||
|           type: eventType, | ||||
|           data: { | ||||
| @@ -2,11 +2,11 @@ import stripeModule from 'stripe'; | ||||
| 
 | ||||
| import { | ||||
|   generateGroup, | ||||
| } from '../../../../../../helpers/api-unit.helper.js'; | ||||
| import { model as User } from '../../../../../../../website/server/models/user'; | ||||
| import { model as Group } from '../../../../../../../website/server/models/group'; | ||||
| import stripePayments from '../../../../../../../website/server/libs/stripePayments'; | ||||
| import payments from '../../../../../../../website/server/libs/payments'; | ||||
| } from '../../../../../helpers/api-unit.helper.js'; | ||||
| import { model as User } from '../../../../../../website/server/models/user'; | ||||
| import { model as Group } from '../../../../../../website/server/models/group'; | ||||
| import stripePayments from '../../../../../../website/server/libs/payments/stripe'; | ||||
| import payments from '../../../../../../website/server/libs/payments/payments'; | ||||
| 
 | ||||
| describe('Stripe - Upgrade Group Plan', () => { | ||||
|   const stripe = stripeModule('test'); | ||||
| @@ -38,7 +38,7 @@ describe('Stripe - Upgrade Group Plan', () => { | ||||
|     await group.save(); | ||||
| 
 | ||||
|     spy = sinon.stub(stripe.subscriptions, 'update'); | ||||
|     spy.returnsPromise().resolves([]); | ||||
|     spy.resolves([]); | ||||
|     data.groupId = group._id; | ||||
|     data.sub.quantity = 3; | ||||
|     stripePayments.setStripeApi(stripe); | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { preenHistory } from '../../../../../website/server/libs/preening'; | ||||
| import { preenHistory } from '../../../../website/server/libs/preening'; | ||||
| import moment from 'moment'; | ||||
| import sinon from 'sinon'; // eslint-disable-line no-shadow
 | ||||
| import { generateHistory } from '../../../../helpers/api-unit.helper.js'; | ||||
| import { generateHistory } from '../../../helpers/api-unit.helper.js'; | ||||
| 
 | ||||
| describe('preenHistory', () => { | ||||
|   let clock; | ||||
| @@ -1,13 +1,13 @@ | ||||
| import { model as User } from '../../../../../website/server/models/user'; | ||||
| import { model as User } from '../../../../website/server/models/user'; | ||||
| import requireAgain from 'require-again'; | ||||
| import pushNotify from 'push-notify'; | ||||
| import apn from 'apn/mock'; | ||||
| import nconf from 'nconf'; | ||||
| import gcmLib from 'node-gcm'; // works with FCM notifications too
 | ||||
| 
 | ||||
| describe('pushNotifications', () => { | ||||
|   let user; | ||||
|   let sendPushNotification; | ||||
|   let pathToPushNotifications = '../../../../../website/server/libs/pushNotifications'; | ||||
|   let pathToPushNotifications = '../../../../website/server/libs/pushNotifications'; | ||||
|   let fcmSendSpy; | ||||
|   let apnSendSpy; | ||||
| 
 | ||||
| @@ -24,7 +24,7 @@ describe('pushNotifications', () => { | ||||
| 
 | ||||
|     sandbox.stub(gcmLib.Sender.prototype, 'send').callsFake(fcmSendSpy); | ||||
| 
 | ||||
|     sandbox.stub(pushNotify, 'apn').returns({ | ||||
|     sandbox.stub(apn.Provider.prototype, 'send').returns({ | ||||
|       on: () => null, | ||||
|       send: apnSendSpy, | ||||
|     }); | ||||
| @@ -104,10 +104,7 @@ describe('pushNotifications', () => { | ||||
|       }, | ||||
|     }; | ||||
| 
 | ||||
|     sendPushNotification(user, details); | ||||
|     expect(apnSendSpy).to.have.been.calledOnce; | ||||
|     expect(apnSendSpy).to.have.been.calledWithMatch({ | ||||
|       token: '123', | ||||
|     const expectedNotification = new apn.Notification({ | ||||
|       alert: message, | ||||
|       sound: 'default', | ||||
|       category: 'fun', | ||||
| @@ -117,6 +114,10 @@ describe('pushNotifications', () => { | ||||
|         b: true, | ||||
|       }, | ||||
|     }); | ||||
| 
 | ||||
|     sendPushNotification(user, details); | ||||
|     expect(apnSendSpy).to.have.been.calledOnce; | ||||
|     expect(apnSendSpy).to.have.been.calledWithMatch(expectedNotification, '123'); | ||||
|     expect(fcmSendSpy).to.not.have.been.called; | ||||
|   }); | ||||
| }); | ||||
| @@ -1,4 +1,4 @@ | ||||
| import setupNconf from '../../../../../website/server/libs/setupNconf'; | ||||
| import setupNconf from '../../../../website/server/libs/setupNconf'; | ||||
| 
 | ||||
| import path from 'path'; | ||||
| import nconf from 'nconf'; | ||||
| @@ -1,10 +1,11 @@ | ||||
| /* eslint-disable camelcase */ | ||||
| import { IncomingWebhook } from '@slack/client'; | ||||
| import requireAgain from 'require-again'; | ||||
| import slack from '../../../../../website/server/libs/slack'; | ||||
| import logger from '../../../../../website/server/libs/logger'; | ||||
| import { TAVERN_ID } from '../../../../../website/server/models/group'; | ||||
| import slack from '../../../../website/server/libs/slack'; | ||||
| import logger from '../../../../website/server/libs/logger'; | ||||
| import { TAVERN_ID } from '../../../../website/server/models/group'; | ||||
| import nconf from 'nconf'; | ||||
| import moment from 'moment'; | ||||
| 
 | ||||
| describe('slack', () => { | ||||
|   describe('sendFlagNotification', () => { | ||||
| @@ -45,17 +46,19 @@ describe('slack', () => { | ||||
|     it('sends a slack webhook', () => { | ||||
|       slack.sendFlagNotification(data); | ||||
| 
 | ||||
|       const timestamp = `${moment(data.message.timestamp).utc().format('YYYY-MM-DD HH:mm')} UTC`; | ||||
| 
 | ||||
|       expect(IncomingWebhook.prototype.send).to.be.calledOnce; | ||||
|       expect(IncomingWebhook.prototype.send).to.be.calledWith({ | ||||
|         text: 'flagger (flagger-id; language: flagger-lang) flagged a message', | ||||
|         attachments: [{ | ||||
|           fallback: 'Flag Message', | ||||
|           color: 'danger', | ||||
|           author_name: 'Author - author@example.com - author-id', | ||||
|           author_name: `Author - author@example.com - author-id\n${timestamp}`, | ||||
|           title: 'Flag in Some group - (private guild)', | ||||
|           title_link: undefined, | ||||
|           text: 'some text', | ||||
|           footer: sandbox.match(/<.*?groupId=group-id&chatId=chat-id\|Flag this message>/), | ||||
|           footer: sandbox.match(/<.*?groupId=group-id&chatId=chat-id\|Flag this message.>/), | ||||
|           mrkdwn_in: [ | ||||
|             'text', | ||||
|           ], | ||||
| @@ -97,17 +100,19 @@ describe('slack', () => { | ||||
| 
 | ||||
|       slack.sendFlagNotification(data); | ||||
| 
 | ||||
|       const timestamp = `${moment(data.message.timestamp).utc().format('YYYY-MM-DD HH:mm')} UTC`; | ||||
| 
 | ||||
|       expect(IncomingWebhook.prototype.send).to.be.calledWithMatch({ | ||||
|         attachments: [sandbox.match({ | ||||
|           author_name: 'System Message', | ||||
|           author_name: `System Message\n${timestamp}`, | ||||
|         })], | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it('noops if no flagging url is provided', () => { | ||||
|       sandbox.stub(nconf, 'get').withArgs('SLACK:FLAGGING_URL').returns(''); | ||||
|       sandbox.stub(nconf, 'get').withArgs('SLACK_FLAGGING_URL').returns(''); | ||||
|       sandbox.stub(logger, 'error'); | ||||
|       let reRequiredSlack = requireAgain('../../../../../website/server/libs/slack'); | ||||
|       let reRequiredSlack = requireAgain('../../../../website/server/libs/slack'); | ||||
| 
 | ||||
|       expect(logger.error).to.be.calledOnce; | ||||
| 
 | ||||
| @@ -3,13 +3,13 @@ import { | ||||
|   getTasks, | ||||
|   syncableAttrs, | ||||
|   moveTask, | ||||
| } from '../../../../../website/server/libs/taskManager'; | ||||
| import i18n from '../../../../../website/common/script/i18n'; | ||||
| } from '../../../../website/server/libs/taskManager'; | ||||
| import i18n from '../../../../website/common/script/i18n'; | ||||
| import { | ||||
|   generateUser, | ||||
|   generateGroup, | ||||
|   generateChallenge, | ||||
| } from '../../../../helpers/api-unit.helper.js'; | ||||
| } from '../../../helpers/api-unit.helper.js'; | ||||
| 
 | ||||
| describe('taskManager', () => { | ||||
|   let user, group, challenge; | ||||
| @@ -178,4 +178,12 @@ describe('taskManager', () => { | ||||
| 
 | ||||
|     expect(order).to.eql(['task-id-2', 'task-id-1']); | ||||
|   }); | ||||
| 
 | ||||
|   it('moves tasks to a specified position out of length', async () => { | ||||
|     let order = ['task-id-1']; | ||||
| 
 | ||||
|     moveTask(order, 'task-id-2', 2); | ||||
| 
 | ||||
|     expect(order).to.eql(['task-id-1', 'task-id-2']); | ||||
|   }); | ||||
| }); | ||||
| @@ -4,11 +4,19 @@ import { | ||||
|   taskScoredWebhook, | ||||
|   groupChatReceivedWebhook, | ||||
|   taskActivityWebhook, | ||||
| } from '../../../../../website/server/libs/webhook'; | ||||
| import { defer } from '../../../../helpers/api-unit.helper'; | ||||
|   questActivityWebhook, | ||||
|   userActivityWebhook, | ||||
| } from '../../../../website/server/libs/webhook'; | ||||
| import { | ||||
|   model as User, | ||||
| } from '../../../../website/server/models/user'; | ||||
| import { | ||||
|   generateUser, | ||||
| } from '../../../helpers/api-unit.helper.js'; | ||||
| import { defer } from '../../../helpers/api-unit.helper'; | ||||
| 
 | ||||
| describe('webhooks', () => { | ||||
|   let webhooks; | ||||
|   let webhooks, user; | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     sandbox.stub(got, 'post').returns(defer().promise); | ||||
| @@ -23,6 +31,26 @@ describe('webhooks', () => { | ||||
|         updated: true, | ||||
|         deleted: true, | ||||
|         scored: true, | ||||
|         checklistScored: true, | ||||
|       }, | ||||
|     }, { | ||||
|       id: 'questActivity', | ||||
|       url: 'http://quest-activity.com', | ||||
|       enabled: true, | ||||
|       type: 'questActivity', | ||||
|       options: { | ||||
|         questStarted: true, | ||||
|         questFinised: true, | ||||
|       }, | ||||
|     }, { | ||||
|       id: 'userActivity', | ||||
|       url: 'http://user-activity.com', | ||||
|       enabled: true, | ||||
|       type: 'userActivity', | ||||
|       options: { | ||||
|         petHatched: true, | ||||
|         mountRaised: true, | ||||
|         leveledUp: true, | ||||
|       }, | ||||
|     }, { | ||||
|       id: 'groupChatReceived', | ||||
| @@ -33,6 +61,9 @@ describe('webhooks', () => { | ||||
|         groupId: 'group-id', | ||||
|       }, | ||||
|     }]; | ||||
| 
 | ||||
|     user = generateUser(); | ||||
|     user.webhooks = webhooks; | ||||
|   }); | ||||
| 
 | ||||
|   afterEach(() => { | ||||
| @@ -57,7 +88,8 @@ describe('webhooks', () => { | ||||
| 
 | ||||
|       let body = { foo: 'bar' }; | ||||
| 
 | ||||
|       sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}], body); | ||||
|       user.webhooks = [{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}]; | ||||
|       sendWebhook.send(user, body); | ||||
| 
 | ||||
|       expect(WebhookSender.defaultTransformData).to.be.calledOnce; | ||||
|       expect(got.post).to.be.calledOnce; | ||||
| @@ -67,6 +99,30 @@ describe('webhooks', () => { | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it('adds default data (user and webhookType) to the body', () => { | ||||
|       let sendWebhook = new WebhookSender({ | ||||
|         type: 'custom', | ||||
|       }); | ||||
|       sandbox.spy(sendWebhook, 'attachDefaultData'); | ||||
| 
 | ||||
|       let body = { foo: 'bar' }; | ||||
| 
 | ||||
|       user.webhooks = [{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}]; | ||||
|       sendWebhook.send(user, body); | ||||
| 
 | ||||
|       expect(sendWebhook.attachDefaultData).to.be.calledOnce; | ||||
|       expect(got.post).to.be.calledOnce; | ||||
|       expect(got.post).to.be.calledWithMatch('http://custom-url.com', { | ||||
|         json: true, | ||||
|       }); | ||||
| 
 | ||||
|       expect(body).to.eql({ | ||||
|         foo: 'bar', | ||||
|         user: {_id: user._id}, | ||||
|         webhookType: 'custom', | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it('can pass in a data transformation function', () => { | ||||
|       sandbox.spy(WebhookSender, 'defaultTransformData'); | ||||
|       let sendWebhook = new WebhookSender({ | ||||
| @@ -80,7 +136,8 @@ describe('webhooks', () => { | ||||
| 
 | ||||
|       let body = { foo: 'bar' }; | ||||
| 
 | ||||
|       sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}], body); | ||||
|       user.webhooks = [{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}]; | ||||
|       sendWebhook.send(user, body); | ||||
| 
 | ||||
|       expect(WebhookSender.defaultTransformData).to.not.be.called; | ||||
|       expect(got.post).to.be.calledOnce; | ||||
| @@ -93,7 +150,7 @@ describe('webhooks', () => { | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it('provieds a default filter function', () => { | ||||
|     it('provides a default filter function', () => { | ||||
|       sandbox.spy(WebhookSender, 'defaultWebhookFilter'); | ||||
|       let sendWebhook = new WebhookSender({ | ||||
|         type: 'custom', | ||||
| @@ -101,7 +158,8 @@ describe('webhooks', () => { | ||||
| 
 | ||||
|       let body = { foo: 'bar' }; | ||||
| 
 | ||||
|       sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}], body); | ||||
|       user.webhooks = [{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}]; | ||||
|       sendWebhook.send(user, body); | ||||
| 
 | ||||
|       expect(WebhookSender.defaultWebhookFilter).to.be.calledOnce; | ||||
|     }); | ||||
| @@ -117,7 +175,8 @@ describe('webhooks', () => { | ||||
| 
 | ||||
|       let body = { foo: 'bar' }; | ||||
| 
 | ||||
|       sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}], body); | ||||
|       user.webhooks = [{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}]; | ||||
|       sendWebhook.send(user, body); | ||||
| 
 | ||||
|       expect(WebhookSender.defaultWebhookFilter).to.not.be.called; | ||||
|       expect(got.post).to.not.be.called; | ||||
| @@ -134,10 +193,11 @@ describe('webhooks', () => { | ||||
| 
 | ||||
|       let body = { foo: 'bar' }; | ||||
| 
 | ||||
|       sendWebhook.send([ | ||||
|       user.webhooks = [ | ||||
|         { id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom', options: { foo: 'bar' }}, | ||||
|         { id: 'other-custom-webhook', url: 'http://other-custom-url.com', enabled: true, type: 'custom', options: { foo: 'foo' }}, | ||||
|       ], body); | ||||
|       ]; | ||||
|       sendWebhook.send(user, body); | ||||
| 
 | ||||
|       expect(got.post).to.be.calledOnce; | ||||
|       expect(got.post).to.be.calledWithMatch('http://custom-url.com'); | ||||
| @@ -150,7 +210,8 @@ describe('webhooks', () => { | ||||
| 
 | ||||
|       let body = { foo: 'bar' }; | ||||
| 
 | ||||
|       sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: false, type: 'custom'}], body); | ||||
|       user.webhooks = [{id: 'custom-webhook', url: 'http://custom-url.com', enabled: false, type: 'custom'}]; | ||||
|       sendWebhook.send(user, body); | ||||
| 
 | ||||
|       expect(got.post).to.not.be.called; | ||||
|     }); | ||||
| @@ -162,7 +223,8 @@ describe('webhooks', () => { | ||||
| 
 | ||||
|       let body = { foo: 'bar' }; | ||||
| 
 | ||||
|       sendWebhook.send([{id: 'custom-webhook', url: 'httxp://custom-url!!', enabled: true, type: 'custom'}], body); | ||||
|       user.webhooks = [{id: 'custom-webhook', url: 'httxp://custom-url!!!', enabled: true, type: 'custom'}]; | ||||
|       sendWebhook.send(user, body); | ||||
| 
 | ||||
|       expect(got.post).to.not.be.called; | ||||
|     }); | ||||
| @@ -174,10 +236,30 @@ describe('webhooks', () => { | ||||
| 
 | ||||
|       let body = { foo: 'bar' }; | ||||
| 
 | ||||
|       sendWebhook.send([ | ||||
|       user.webhooks = [ | ||||
|         { id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}, | ||||
|         { id: 'other-webhook', url: 'http://other-url.com', enabled: true, type: 'other'}, | ||||
|       ], body); | ||||
|       ]; | ||||
|       sendWebhook.send(user, body); | ||||
| 
 | ||||
|       expect(got.post).to.be.calledOnce; | ||||
|       expect(got.post).to.be.calledWithMatch('http://custom-url.com', { | ||||
|         body, | ||||
|         json: true, | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it('sends every type of activity to global webhooks', () => { | ||||
|       let sendWebhook = new WebhookSender({ | ||||
|         type: 'custom', | ||||
|       }); | ||||
| 
 | ||||
|       let body = { foo: 'bar' }; | ||||
| 
 | ||||
|       user.webhooks = [ | ||||
|         { id: 'global-webhook', url: 'http://custom-url.com', enabled: true, type: 'globalActivity'}, | ||||
|       ]; | ||||
|       sendWebhook.send(user, body); | ||||
| 
 | ||||
|       expect(got.post).to.be.calledOnce; | ||||
|       expect(got.post).to.be.calledWithMatch('http://custom-url.com', { | ||||
| @@ -193,10 +275,11 @@ describe('webhooks', () => { | ||||
| 
 | ||||
|       let body = { foo: 'bar' }; | ||||
| 
 | ||||
|       sendWebhook.send([ | ||||
|       user.webhooks = [ | ||||
|         { id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}, | ||||
|         { id: 'other-custom-webhook', url: 'http://other-url.com', enabled: true, type: 'custom'}, | ||||
|       ], body); | ||||
|       ]; | ||||
|       sendWebhook.send(user, body); | ||||
| 
 | ||||
|       expect(got.post).to.be.calledTwice; | ||||
|       expect(got.post).to.be.calledWithMatch('http://custom-url.com', { | ||||
| @@ -216,7 +299,6 @@ describe('webhooks', () => { | ||||
|     beforeEach(() => { | ||||
|       data = { | ||||
|         user: { | ||||
|           _id: 'user-id', | ||||
|           _tmp: {foo: 'bar'}, | ||||
|           stats: { | ||||
|             lvl: 5, | ||||
| @@ -227,17 +309,6 @@ describe('webhooks', () => { | ||||
|               return this; | ||||
|             }, | ||||
|           }, | ||||
|           addComputedStatsToJSONObj () { | ||||
|             let mockStats = Object.assign({ | ||||
|               maxHealth: 50, | ||||
|               maxMP: 103, | ||||
|               toNextLevel: 40, | ||||
|             }, this.stats); | ||||
| 
 | ||||
|             delete mockStats.toJSON; | ||||
| 
 | ||||
|             return mockStats; | ||||
|           }, | ||||
|         }, | ||||
|         task: { | ||||
|           text: 'text', | ||||
| @@ -245,18 +316,66 @@ describe('webhooks', () => { | ||||
|         direction: 'up', | ||||
|         delta: 176, | ||||
|       }; | ||||
| 
 | ||||
|       let mockStats = Object.assign({ | ||||
|         maxHealth: 50, | ||||
|         maxMP: 103, | ||||
|         toNextLevel: 40, | ||||
|       }, data.user.stats); | ||||
|       delete mockStats.toJSON; | ||||
| 
 | ||||
|       sandbox.stub(User, 'addComputedStatsToJSONObj').returns(mockStats); | ||||
|     }); | ||||
| 
 | ||||
|     it('sends task and stats data', () => { | ||||
|       taskScoredWebhook.send(webhooks, data); | ||||
|       taskScoredWebhook.send(user, data); | ||||
| 
 | ||||
|       expect(got.post).to.be.calledOnce; | ||||
|       expect(got.post).to.be.calledWithMatch(webhooks[0].url, { | ||||
|         json: true, | ||||
|         body: { | ||||
|           type: 'scored', | ||||
|           webhookType: 'taskActivity', | ||||
|           user: { | ||||
|             _id: 'user-id', | ||||
|             _id: user._id, | ||||
|             _tmp: {foo: 'bar'}, | ||||
|             stats: { | ||||
|               lvl: 5, | ||||
|               int: 10, | ||||
|               str: 5, | ||||
|               exp: 423, | ||||
|               toNextLevel: 40, | ||||
|               maxHealth: 50, | ||||
|               maxMP: 103, | ||||
|             }, | ||||
|           }, | ||||
|           task: { | ||||
|             text: 'text', | ||||
|           }, | ||||
|           direction: 'up', | ||||
|           delta: 176, | ||||
|         }, | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it('sends task and stats data to globalActivity webhookd', () => { | ||||
|       user.webhooks = [{ | ||||
|         id: 'globalActivity', | ||||
|         url: 'http://global-activity.com', | ||||
|         enabled: true, | ||||
|         type: 'globalActivity', | ||||
|       }]; | ||||
| 
 | ||||
|       taskScoredWebhook.send(user, data); | ||||
| 
 | ||||
|       expect(got.post).to.be.calledOnce; | ||||
|       expect(got.post).to.be.calledWithMatch('http://global-activity.com', { | ||||
|         json: true, | ||||
|         body: { | ||||
|           type: 'scored', | ||||
|           webhookType: 'taskActivity', | ||||
|           user: { | ||||
|             _id: user._id, | ||||
|             _tmp: {foo: 'bar'}, | ||||
|             stats: { | ||||
|               lvl: 5, | ||||
| @@ -280,7 +399,7 @@ describe('webhooks', () => { | ||||
|     it('does not send task scored data if scored option is not true', () => { | ||||
|       webhooks[0].options.scored = false; | ||||
| 
 | ||||
|       taskScoredWebhook.send(webhooks, data); | ||||
|       taskScoredWebhook.send(user, data); | ||||
| 
 | ||||
|       expect(got.post).to.not.be.called; | ||||
|     }); | ||||
| @@ -301,13 +420,17 @@ describe('webhooks', () => { | ||||
|       it(`sends ${type} tasks`, () => { | ||||
|         data.type = type; | ||||
| 
 | ||||
|         taskActivityWebhook.send(webhooks, data); | ||||
|         taskActivityWebhook.send(user, data); | ||||
| 
 | ||||
|         expect(got.post).to.be.calledOnce; | ||||
|         expect(got.post).to.be.calledWithMatch(webhooks[0].url, { | ||||
|           json: true, | ||||
|           body: { | ||||
|             type, | ||||
|             webhookType: 'taskActivity', | ||||
|             user: { | ||||
|               _id: user._id, | ||||
|             }, | ||||
|             task: data.task, | ||||
|           }, | ||||
|         }); | ||||
| @@ -317,7 +440,142 @@ describe('webhooks', () => { | ||||
|         data.type = type; | ||||
|         webhooks[0].options[type] = false; | ||||
| 
 | ||||
|         taskActivityWebhook.send(webhooks, data); | ||||
|         taskActivityWebhook.send(user, data); | ||||
| 
 | ||||
|         expect(got.post).to.not.be.called; | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('checklistScored', () => { | ||||
|       beforeEach(() => { | ||||
|         data = { | ||||
|           task: { | ||||
|             text: 'text', | ||||
|           }, | ||||
|           item: { | ||||
|             text: 'item-text', | ||||
|           }, | ||||
|         }; | ||||
|       }); | ||||
| 
 | ||||
|       it('sends \'checklistScored\' tasks', () => { | ||||
|         data.type = 'checklistScored'; | ||||
| 
 | ||||
|         taskActivityWebhook.send(user, data); | ||||
| 
 | ||||
|         expect(got.post).to.be.calledOnce; | ||||
|         expect(got.post).to.be.calledWithMatch(webhooks[0].url, { | ||||
|           json: true, | ||||
|           body: { | ||||
|             webhookType: 'taskActivity', | ||||
|             user: { | ||||
|               _id: user._id, | ||||
|             }, | ||||
|             type: data.type, | ||||
|             task: data.task, | ||||
|             item: data.item, | ||||
|           }, | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|       it('does not send task \'checklistScored\' data if \'checklistScored\' option is not true', () => { | ||||
|         data.type = 'checklistScored'; | ||||
|         webhooks[0].options.checklistScored = false; | ||||
| 
 | ||||
|         taskActivityWebhook.send(user, data); | ||||
| 
 | ||||
|         expect(got.post).to.not.be.called; | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('userActivityWebhook', () => { | ||||
|     let data; | ||||
| 
 | ||||
|     beforeEach(() => { | ||||
|       data = { | ||||
|         something: true, | ||||
|       }; | ||||
|     }); | ||||
| 
 | ||||
|     ['petHatched', 'mountRaised', 'leveledUp'].forEach((type) => { | ||||
|       it(`sends ${type} webhooks`, () => { | ||||
|         data.type = type; | ||||
| 
 | ||||
|         userActivityWebhook.send(user, data); | ||||
| 
 | ||||
|         expect(got.post).to.be.calledOnce; | ||||
|         expect(got.post).to.be.calledWithMatch(webhooks[2].url, { | ||||
|           json: true, | ||||
|           body: { | ||||
|             type, | ||||
|             webhookType: 'userActivity', | ||||
|             user: { | ||||
|               _id: user._id, | ||||
|             }, | ||||
|             something: true, | ||||
|           }, | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|       it(`does not send webhook ${type} data if ${type} option is not true`, () => { | ||||
|         data.type = type; | ||||
|         webhooks[2].options[type] = false; | ||||
| 
 | ||||
|         userActivityWebhook.send(user, data); | ||||
| 
 | ||||
|         expect(got.post).to.not.be.called; | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('questActivityWebhook', () => { | ||||
|     let data; | ||||
| 
 | ||||
|     beforeEach(() => { | ||||
|       data = { | ||||
|         group: { | ||||
|           id: 'group-id', | ||||
|           name: 'some group', | ||||
|           otherData: 'foo', | ||||
|         }, | ||||
|         quest: { | ||||
|           key: 'some-key', | ||||
|         }, | ||||
|       }; | ||||
|     }); | ||||
| 
 | ||||
|     ['questStarted', 'questFinised'].forEach((type) => { | ||||
|       it(`sends ${type} webhooks`, () => { | ||||
|         data.type = type; | ||||
| 
 | ||||
|         questActivityWebhook.send(user, data); | ||||
| 
 | ||||
|         expect(got.post).to.be.calledOnce; | ||||
|         expect(got.post).to.be.calledWithMatch(webhooks[1].url, { | ||||
|           json: true, | ||||
|           body: { | ||||
|             type, | ||||
|             webhookType: 'questActivity', | ||||
|             user: { | ||||
|               _id: user._id, | ||||
|             }, | ||||
|             group: { | ||||
|               id: 'group-id', | ||||
|               name: 'some group', | ||||
|             }, | ||||
|             quest: { | ||||
|               key: 'some-key', | ||||
|             }, | ||||
|           }, | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|       it(`does not send webhook ${type} data if ${type} option is not true`, () => { | ||||
|         data.type = type; | ||||
|         webhooks[1].options[type] = false; | ||||
| 
 | ||||
|         userActivityWebhook.send(user, data); | ||||
| 
 | ||||
|         expect(got.post).to.not.be.called; | ||||
|       }); | ||||
| @@ -338,12 +596,16 @@ describe('webhooks', () => { | ||||
|         }, | ||||
|       }; | ||||
| 
 | ||||
|       groupChatReceivedWebhook.send(webhooks, data); | ||||
|       groupChatReceivedWebhook.send(user, data); | ||||
| 
 | ||||
|       expect(got.post).to.be.calledOnce; | ||||
|       expect(got.post).to.be.calledWithMatch(webhooks[webhooks.length - 1].url, { | ||||
|         json: true, | ||||
|         body: { | ||||
|           webhookType: 'groupChatReceived', | ||||
|           user: { | ||||
|             _id: user._id, | ||||
|           }, | ||||
|           group: { | ||||
|             id: 'group-id', | ||||
|             name: 'some group', | ||||
| @@ -369,7 +631,7 @@ describe('webhooks', () => { | ||||
|         }, | ||||
|       }; | ||||
| 
 | ||||
|       groupChatReceivedWebhook.send(webhooks, data); | ||||
|       groupChatReceivedWebhook.send(user, data); | ||||
| 
 | ||||
|       expect(got.post).to.not.be.called; | ||||
|     }); | ||||
| @@ -3,14 +3,14 @@ import { | ||||
|   generateRes, | ||||
|   generateReq, | ||||
|   generateNext, | ||||
| } from '../../../../helpers/api-unit.helper'; | ||||
| import analyticsService from '../../../../../website/server/libs/analyticsService'; | ||||
| } from '../../../helpers/api-unit.helper'; | ||||
| import analyticsService from '../../../../website/server/libs/analyticsService'; | ||||
| import nconf from 'nconf'; | ||||
| import requireAgain from 'require-again'; | ||||
| 
 | ||||
| describe('analytics middleware', () => { | ||||
|   let res, req, next; | ||||
|   let pathToAnalyticsMiddleware = '../../../../../website/server/middlewares/analytics'; | ||||
|   let pathToAnalyticsMiddleware = '../../../../website/server/middlewares/analytics'; | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     res = generateRes(); | ||||
							
								
								
									
										40
									
								
								test/api/unit/middlewares/auth.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								test/api/unit/middlewares/auth.test.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| import { | ||||
|   generateRes, | ||||
|   generateReq, | ||||
| } from '../../../helpers/api-unit.helper'; | ||||
| import { authWithHeaders as authWithHeadersFactory } from '../../../../website/server/middlewares/auth'; | ||||
|  | ||||
| describe('auth middleware', () => { | ||||
|   let res, req, user; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     res = generateRes(); | ||||
|     req = generateReq(); | ||||
|     user = await res.locals.user.save(); | ||||
|   }); | ||||
|  | ||||
|   describe('auth with headers', () => { | ||||
|     it('allows to specify a list of user field that we do not want to load', (done) => { | ||||
|       const authWithHeaders = authWithHeadersFactory({ | ||||
|         userFieldsToExclude: ['items', 'flags', 'auth.timestamps'], | ||||
|       }); | ||||
|  | ||||
|       req.headers['x-api-user'] = user._id; | ||||
|       req.headers['x-api-key'] = user.apiToken; | ||||
|  | ||||
|       authWithHeaders(req, res, (err) => { | ||||
|         if (err) return done(err); | ||||
|  | ||||
|         const userToJSON = res.locals.user.toJSON(); | ||||
|         expect(userToJSON.items).to.not.exist; | ||||
|         expect(userToJSON.flags).to.not.exist; | ||||
|         expect(userToJSON.auth.timestamps).to.not.exist; | ||||
|         expect(userToJSON.auth).to.exist; | ||||
|         expect(userToJSON.notifications).to.exist; | ||||
|         expect(userToJSON.preferences).to.exist; | ||||
|  | ||||
|         done(); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -3,8 +3,8 @@ import { | ||||
|   generateRes, | ||||
|   generateReq, | ||||
|   generateNext, | ||||
| } from '../../../../helpers/api-unit.helper'; | ||||
| import cors from '../../../../../website/server/middlewares/cors'; | ||||
| } from '../../../helpers/api-unit.helper'; | ||||
| import cors from '../../../../website/server/middlewares/cors'; | ||||
| 
 | ||||
| describe('cors middleware', () => { | ||||
|   let res, req, next; | ||||
| @@ -3,14 +3,14 @@ import { | ||||
|   generateReq, | ||||
|   generateTodo, | ||||
|   generateDaily, | ||||
| } from '../../../../helpers/api-unit.helper'; | ||||
| import cronMiddleware from '../../../../../website/server/middlewares/cron'; | ||||
| } from '../../../helpers/api-unit.helper'; | ||||
| import cronMiddleware from '../../../../website/server/middlewares/cron'; | ||||
| import moment from 'moment'; | ||||
| import { model as User } from '../../../../../website/server/models/user'; | ||||
| import { model as Group } from '../../../../../website/server/models/group'; | ||||
| import * as Tasks from '../../../../../website/server/models/task'; | ||||
| import analyticsService from '../../../../../website/server/libs/analyticsService'; | ||||
| import * as cronLib from '../../../../../website/server/libs/cron'; | ||||
| import { model as User } from '../../../../website/server/models/user'; | ||||
| import { model as Group } from '../../../../website/server/models/group'; | ||||
| import * as Tasks from '../../../../website/server/models/task'; | ||||
| import analyticsService from '../../../../website/server/libs/analyticsService'; | ||||
| import * as cronLib from '../../../../website/server/libs/cron'; | ||||
| import { v4 as generateUUID } from 'uuid'; | ||||
| 
 | ||||
| const CRON_TIMEOUT_WAIT = new Date(60 * 60 * 1000).getTime(); | ||||
| @@ -166,8 +166,11 @@ describe('cron middleware', () => { | ||||
|     await new Promise((resolve, reject) => { | ||||
|       cronMiddleware(req, res, (err) => { | ||||
|         if (err) return reject(err); | ||||
|         expect(user.stats.hp).to.be.lessThan(hpBefore); | ||||
|         resolve(); | ||||
|         User.findOne({_id: user._id}, function (secondErr, updatedUser) { | ||||
|           if (secondErr) return reject(secondErr); | ||||
|           expect(updatedUser.stats.hp).to.be.lessThan(hpBefore); | ||||
|           resolve(); | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| @@ -176,7 +179,7 @@ describe('cron middleware', () => { | ||||
|     user.lastCron = moment(new Date()).subtract({days: 2}); | ||||
|     let todo = generateTodo(user); | ||||
|     let todoValueBefore = todo.value; | ||||
|     await user.save(); | ||||
|     await Promise.all([todo.save(), user.save()]); | ||||
| 
 | ||||
|     await new Promise((resolve, reject) => { | ||||
|       cronMiddleware(req, res, (err) => { | ||||
| @@ -217,8 +220,11 @@ describe('cron middleware', () => { | ||||
|     await new Promise((resolve, reject) => { | ||||
|       cronMiddleware(req, res, (err) => { | ||||
|         if (err) return reject(err); | ||||
|         expect(user.stats.hp).to.be.lessThan(hpBefore); | ||||
|         resolve(); | ||||
|         User.findOne({_id: user._id}, function (secondErr, updatedUser) { | ||||
|           if (secondErr) return reject(secondErr); | ||||
|           expect(updatedUser.stats.hp).to.be.lessThan(hpBefore); | ||||
|           resolve(); | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| @@ -3,11 +3,11 @@ import { | ||||
|   generateRes, | ||||
|   generateReq, | ||||
|   generateNext, | ||||
| } from '../../../../helpers/api-unit.helper'; | ||||
| import i18n from '../../../../../website/common/script/i18n'; | ||||
| import { ensureAdmin, ensureSudo } from '../../../../../website/server/middlewares/ensureAccessRight'; | ||||
| import { NotAuthorized } from '../../../../../website/server/libs/errors'; | ||||
| import apiMessages from '../../../../../website/server/libs/apiMessages'; | ||||
| } from '../../../helpers/api-unit.helper'; | ||||
| import i18n from '../../../../website/common/script/i18n'; | ||||
| import { ensureAdmin, ensureSudo } from '../../../../website/server/middlewares/ensureAccessRight'; | ||||
| import { NotAuthorized } from '../../../../website/server/libs/errors'; | ||||
| import apiError from '../../../../website/server/libs/apiError'; | ||||
| 
 | ||||
| describe('ensure access middlewares', () => { | ||||
|   let res, req, next; | ||||
| @@ -46,7 +46,7 @@ describe('ensure access middlewares', () => { | ||||
|       ensureSudo(req, res, next); | ||||
| 
 | ||||
|       const calledWith = next.getCall(0).args; | ||||
|       expect(calledWith[0].message).to.equal(apiMessages('noSudoAccess')); | ||||
|       expect(calledWith[0].message).to.equal(apiError('noSudoAccess')); | ||||
|       expect(calledWith[0] instanceof NotAuthorized).to.equal(true); | ||||
|     }); | ||||
| 
 | ||||
| @@ -3,9 +3,9 @@ import { | ||||
|   generateRes, | ||||
|   generateReq, | ||||
|   generateNext, | ||||
| } from '../../../../helpers/api-unit.helper'; | ||||
| import ensureDevelpmentMode from '../../../../../website/server/middlewares/ensureDevelpmentMode'; | ||||
| import { NotFound } from '../../../../../website/server/libs/errors'; | ||||
| } from '../../../helpers/api-unit.helper'; | ||||
| import ensureDevelpmentMode from '../../../../website/server/middlewares/ensureDevelpmentMode'; | ||||
| import { NotFound } from '../../../../website/server/libs/errors'; | ||||
| import nconf from 'nconf'; | ||||
| 
 | ||||
| describe('developmentMode middleware', () => { | ||||
| @@ -2,17 +2,17 @@ import { | ||||
|   generateRes, | ||||
|   generateReq, | ||||
|   generateNext, | ||||
| } from '../../../../helpers/api-unit.helper'; | ||||
| } from '../../../helpers/api-unit.helper'; | ||||
| 
 | ||||
| import errorHandler from '../../../../../website/server/middlewares/errorHandler'; | ||||
| import responseMiddleware from '../../../../../website/server/middlewares/response'; | ||||
| import errorHandler from '../../../../website/server/middlewares/errorHandler'; | ||||
| import responseMiddleware from '../../../../website/server/middlewares/response'; | ||||
| import { | ||||
|   getUserLanguage, | ||||
|   attachTranslateFunction, | ||||
| } from '../../../../../website/server/middlewares/language'; | ||||
| } from '../../../../website/server/middlewares/language'; | ||||
| 
 | ||||
| import { BadRequest } from '../../../../../website/server/libs/errors'; | ||||
| import logger from '../../../../../website/server/libs/logger'; | ||||
| import { BadRequest } from '../../../../website/server/libs/errors'; | ||||
| import logger from '../../../../website/server/libs/logger'; | ||||
| 
 | ||||
| describe('errorHandler', () => { | ||||
|   let res, req, next; | ||||
| @@ -2,13 +2,13 @@ import { | ||||
|   generateRes, | ||||
|   generateReq, | ||||
|   generateNext, | ||||
| } from '../../../../helpers/api-unit.helper'; | ||||
| } from '../../../helpers/api-unit.helper'; | ||||
| import { | ||||
|   getUserLanguage, | ||||
|   attachTranslateFunction, | ||||
| } from '../../../../../website/server/middlewares/language'; | ||||
| import common from '../../../../../website/common'; | ||||
| import { model as User } from '../../../../../website/server/models/user'; | ||||
| } from '../../../../website/server/middlewares/language'; | ||||
| import common from '../../../../website/common'; | ||||
| import { model as User } from '../../../../website/server/models/user'; | ||||
| 
 | ||||
| const i18n = common.i18n; | ||||
| 
 | ||||
| @@ -2,13 +2,13 @@ import { | ||||
|   generateRes, | ||||
|   generateReq, | ||||
|   generateNext, | ||||
| } from '../../../../helpers/api-unit.helper'; | ||||
| } from '../../../helpers/api-unit.helper'; | ||||
| import nconf from 'nconf'; | ||||
| import requireAgain from 'require-again'; | ||||
| 
 | ||||
| describe('maintenance mode middleware', () => { | ||||
|   let res, req, next; | ||||
|   let pathToMaintenanceModeMiddleware = '../../../../../website/server/middlewares/maintenanceMode'; | ||||
|   let pathToMaintenanceModeMiddleware = '../../../../website/server/middlewares/maintenanceMode'; | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     res = generateRes(); | ||||
| @@ -2,13 +2,13 @@ import { | ||||
|   generateRes, | ||||
|   generateReq, | ||||
|   generateNext, | ||||
| } from '../../../../helpers/api-unit.helper'; | ||||
| } from '../../../helpers/api-unit.helper'; | ||||
| import nconf from 'nconf'; | ||||
| import requireAgain from 'require-again'; | ||||
| 
 | ||||
| describe('redirects middleware', () => { | ||||
|   let res, req, next; | ||||
|   let pathToRedirectsMiddleware = '../../../../../website/server/middlewares/redirects'; | ||||
|   let pathToRedirectsMiddleware = '../../../../website/server/middlewares/redirects'; | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     res = generateRes(); | ||||
| @@ -73,6 +73,56 @@ describe('redirects middleware', () => { | ||||
| 
 | ||||
|       expect(res.redirect).to.have.not.been.called; | ||||
|     }); | ||||
| 
 | ||||
|     it('does not redirect if passed skip ssl request param is passed with corrrect key', () => { | ||||
|       let nconfStub = sandbox.stub(nconf, 'get'); | ||||
|       nconfStub.withArgs('BASE_URL').returns('https://habitica.com'); | ||||
|       nconfStub.withArgs('IS_PROD').returns(true); | ||||
|       nconfStub.withArgs('SKIP_SSL_CHECK_KEY').returns('test-key'); | ||||
| 
 | ||||
|       req.header = sandbox.stub().withArgs('x-forwarded-proto').returns('http'); | ||||
|       req.originalUrl = '/static/front'; | ||||
|       req.query.skipSSLCheck = 'test-key'; | ||||
| 
 | ||||
|       const attachRedirects = requireAgain(pathToRedirectsMiddleware); | ||||
|       attachRedirects.forceSSL(req, res, next); | ||||
| 
 | ||||
|       expect(res.redirect).to.have.not.been.called; | ||||
|     }); | ||||
| 
 | ||||
|     it('does redirect if skip ssl request param is passed with incorrrect key', () => { | ||||
|       let nconfStub = sandbox.stub(nconf, 'get'); | ||||
|       nconfStub.withArgs('BASE_URL').returns('https://habitica.com'); | ||||
|       nconfStub.withArgs('IS_PROD').returns(true); | ||||
|       nconfStub.withArgs('SKIP_SSL_CHECK_KEY').returns('test-key'); | ||||
| 
 | ||||
|       req.header = sandbox.stub().withArgs('x-forwarded-proto').returns('http'); | ||||
|       req.originalUrl = '/static/front?skipSSLCheck=INVALID'; | ||||
|       req.query.skipSSLCheck = 'INVALID'; | ||||
| 
 | ||||
|       const attachRedirects = requireAgain(pathToRedirectsMiddleware); | ||||
|       attachRedirects.forceSSL(req, res, next); | ||||
| 
 | ||||
|       expect(res.redirect).to.be.calledOnce; | ||||
|       expect(res.redirect).to.be.calledWith('https://habitica.com/static/front?skipSSLCheck=INVALID'); | ||||
|     }); | ||||
| 
 | ||||
|     it('does redirect if skip ssl check key is not set', () => { | ||||
|       let nconfStub = sandbox.stub(nconf, 'get'); | ||||
|       nconfStub.withArgs('BASE_URL').returns('https://habitica.com'); | ||||
|       nconfStub.withArgs('IS_PROD').returns(true); | ||||
|       nconfStub.withArgs('SKIP_SSL_CHECK_KEY').returns(null); | ||||
| 
 | ||||
|       req.header = sandbox.stub().withArgs('x-forwarded-proto').returns('http'); | ||||
|       req.originalUrl = '/static/front'; | ||||
|       req.query.skipSSLCheck = 'INVALID'; | ||||
| 
 | ||||
|       const attachRedirects = requireAgain(pathToRedirectsMiddleware); | ||||
|       attachRedirects.forceSSL(req, res, next); | ||||
| 
 | ||||
|       expect(res.redirect).to.be.calledOnce; | ||||
|       expect(res.redirect).to.be.calledWith('https://habitica.com/static/front'); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   context('forceHabitica', () => { | ||||
| @@ -2,9 +2,9 @@ import { | ||||
|   generateRes, | ||||
|   generateReq, | ||||
|   generateNext, | ||||
| } from '../../../../helpers/api-unit.helper'; | ||||
| import responseMiddleware from '../../../../../website/server/middlewares/response'; | ||||
| import packageInfo from '../../../../../package.json'; | ||||
| } from '../../../helpers/api-unit.helper'; | ||||
| import responseMiddleware from '../../../../website/server/middlewares/response'; | ||||
| import packageInfo from '../../../../package.json'; | ||||
| 
 | ||||
| describe('response middleware', () => { | ||||
|   let res, req, next; | ||||
| @@ -1,8 +1,8 @@ | ||||
| import { model as Challenge } from '../../../../../website/server/models/challenge'; | ||||
| import { model as Group } from '../../../../../website/server/models/group'; | ||||
| import { model as User } from '../../../../../website/server/models/user'; | ||||
| import * as Tasks from '../../../../../website/server/models/task'; | ||||
| import common from '../../../../../website/common/'; | ||||
| import { model as Challenge } from '../../../../website/server/models/challenge'; | ||||
| import { model as Group } from '../../../../website/server/models/group'; | ||||
| import { model as User } from '../../../../website/server/models/user'; | ||||
| import * as Tasks from '../../../../website/server/models/task'; | ||||
| import common from '../../../../website/common/'; | ||||
| import { each, find } from 'lodash'; | ||||
| 
 | ||||
| describe('Challenge Model', () => { | ||||
| @@ -1,26 +1,30 @@ | ||||
| import moment from 'moment'; | ||||
| import { v4 as generateUUID } from 'uuid'; | ||||
| import validator from 'validator'; | ||||
| import { sleep } from '../../../../helpers/api-unit.helper'; | ||||
| import { sleep } from '../../../helpers/api-unit.helper'; | ||||
| import { | ||||
|   SPAM_MESSAGE_LIMIT, | ||||
|   SPAM_MIN_EXEMPT_CONTRIB_LEVEL, | ||||
|   SPAM_WINDOW_LENGTH, | ||||
|   INVITES_LIMIT, | ||||
|   model as Group, | ||||
| } from '../../../../../website/server/models/group'; | ||||
| import { model as User } from '../../../../../website/server/models/user'; | ||||
| import { quests as questScrolls } from '../../../../../website/common/script/content'; | ||||
| import { groupChatReceivedWebhook } from '../../../../../website/server/libs/webhook'; | ||||
| import * as email from '../../../../../website/server/libs/email'; | ||||
| import { TAVERN_ID } from '../../../../../website/common/script/'; | ||||
| import shared from '../../../../../website/common'; | ||||
| } from '../../../../website/server/models/group'; | ||||
| import { model as User } from '../../../../website/server/models/user'; | ||||
| import { quests as questScrolls } from '../../../../website/common/script/content'; | ||||
| import { | ||||
|   groupChatReceivedWebhook, | ||||
|   questActivityWebhook, | ||||
| } from '../../../../website/server/libs/webhook'; | ||||
| import * as email from '../../../../website/server/libs/email'; | ||||
| import { TAVERN_ID } from '../../../../website/common/script/'; | ||||
| import shared from '../../../../website/common'; | ||||
| 
 | ||||
| describe('Group Model', () => { | ||||
|   let party, questLeader, participatingMember, nonParticipatingMember, undecidedMember; | ||||
|   let party, questLeader, participatingMember, sleepingParticipatingMember, nonParticipatingMember, undecidedMember; | ||||
| 
 | ||||
|   beforeEach(async () => { | ||||
|     sandbox.stub(email, 'sendTxn'); | ||||
|     sandbox.stub(questActivityWebhook, 'send'); | ||||
| 
 | ||||
|     party = new Group({ | ||||
|       name: 'test party', | ||||
| @@ -28,8 +32,19 @@ describe('Group Model', () => { | ||||
|       privacy: 'private', | ||||
|     }); | ||||
| 
 | ||||
|     let _progress = { | ||||
|       up: 10, | ||||
|       down: 8, | ||||
|       collectedItems: 5, | ||||
|     }; | ||||
| 
 | ||||
|     questLeader = new User({ | ||||
|       party: { _id: party._id }, | ||||
|       party: { | ||||
|         _id: party._id, | ||||
|         quest: { | ||||
|           progress: _progress, | ||||
|         }, | ||||
|       }, | ||||
|       profile: { name: 'Quest Leader' }, | ||||
|       items: { | ||||
|         quests: { | ||||
| @@ -41,15 +56,40 @@ describe('Group Model', () => { | ||||
|     party.leader = questLeader._id; | ||||
| 
 | ||||
|     participatingMember = new User({ | ||||
|       party: { _id: party._id }, | ||||
|       party: { | ||||
|         _id: party._id, | ||||
|         quest: { | ||||
|           progress: _progress, | ||||
|         }, | ||||
|       }, | ||||
|       profile: { name: 'Participating Member' }, | ||||
|     }); | ||||
|     sleepingParticipatingMember = new User({ | ||||
|       party: { | ||||
|         _id: party._id, | ||||
|         quest: { | ||||
|           progress: _progress, | ||||
|         }, | ||||
|       }, | ||||
|       profile: { name: 'Sleeping Participating Member' }, | ||||
|       preferences: { sleep: true }, | ||||
|     }); | ||||
|     nonParticipatingMember = new User({ | ||||
|       party: { _id: party._id }, | ||||
|       party: { | ||||
|         _id: party._id, | ||||
|         quest: { | ||||
|           progress: _progress, | ||||
|         }, | ||||
|       }, | ||||
|       profile: { name: 'Non-Participating Member' }, | ||||
|     }); | ||||
|     undecidedMember = new User({ | ||||
|       party: { _id: party._id }, | ||||
|       party: { | ||||
|         _id: party._id, | ||||
|         quest: { | ||||
|           progress: _progress, | ||||
|         }, | ||||
|       }, | ||||
|       profile: { name: 'Undecided Member' }, | ||||
|     }); | ||||
| 
 | ||||
| @@ -57,6 +97,7 @@ describe('Group Model', () => { | ||||
|       party.save(), | ||||
|       questLeader.save(), | ||||
|       participatingMember.save(), | ||||
|       sleepingParticipatingMember.save(), | ||||
|       nonParticipatingMember.save(), | ||||
|       undecidedMember.save(), | ||||
|     ]); | ||||
| @@ -76,6 +117,7 @@ describe('Group Model', () => { | ||||
|         party.quest.members = { | ||||
|           [questLeader._id]: true, | ||||
|           [participatingMember._id]: true, | ||||
|           [sleepingParticipatingMember._id]: true, | ||||
|           [nonParticipatingMember._id]: false, | ||||
|           [undecidedMember._id]: null, | ||||
|         }; | ||||
| @@ -171,6 +213,34 @@ describe('Group Model', () => { | ||||
|           expect(party._processBossQuest).to.not.be.called; | ||||
|           expect(Group.prototype._processCollectionQuest).to.be.calledOnce; | ||||
|         }); | ||||
| 
 | ||||
|         it('does not call _processBossQuest when user is resting in the inn', async () => { | ||||
|           party.quest.key = 'whale'; | ||||
| 
 | ||||
|           await party.startQuest(questLeader); | ||||
|           await party.save(); | ||||
| 
 | ||||
|           await Group.processQuestProgress(sleepingParticipatingMember, progress); | ||||
| 
 | ||||
|           party = await Group.findOne({_id: party._id}); | ||||
| 
 | ||||
|           expect(party._processBossQuest).to.not.be.called; | ||||
|           expect(party._processCollectionQuest).to.not.be.called; | ||||
|         }); | ||||
| 
 | ||||
|         it('does not call _processCollectionQuest when user is resting in the inn', async () => { | ||||
|           party.quest.key = 'evilsanta2'; | ||||
| 
 | ||||
|           await party.startQuest(questLeader); | ||||
|           await party.save(); | ||||
| 
 | ||||
|           await Group.processQuestProgress(sleepingParticipatingMember, progress); | ||||
| 
 | ||||
|           party = await Group.findOne({_id: party._id}); | ||||
| 
 | ||||
|           expect(party._processBossQuest).to.not.be.called; | ||||
|           expect(party._processCollectionQuest).to.not.be.called; | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|       context('Boss Quests', () => { | ||||
| @@ -182,7 +252,7 @@ describe('Group Model', () => { | ||||
|           await party.startQuest(questLeader); | ||||
|           await party.save(); | ||||
| 
 | ||||
|           sendChatStub = sandbox.stub(Group.prototype, 'sendChat'); | ||||
|           sendChatStub = sandbox.spy(Group.prototype, 'sendChat'); | ||||
|         }); | ||||
| 
 | ||||
|         afterEach(() => sendChatStub.restore()); | ||||
| @@ -212,17 +282,20 @@ describe('Group Model', () => { | ||||
|           let [ | ||||
|             updatedLeader, | ||||
|             updatedParticipatingMember, | ||||
|             updatedSleepingParticipatingMember, | ||||
|             updatedNonParticipatingMember, | ||||
|             updatedUndecidedMember, | ||||
|           ] = await Promise.all([ | ||||
|             User.findById(questLeader._id), | ||||
|             User.findById(participatingMember._id), | ||||
|             User.findById(sleepingParticipatingMember._id), | ||||
|             User.findById(nonParticipatingMember._id), | ||||
|             User.findById(undecidedMember._id), | ||||
|           ]); | ||||
| 
 | ||||
|           expect(updatedLeader.stats.hp).to.eql(42.5); | ||||
|           expect(updatedParticipatingMember.stats.hp).to.eql(42.5); | ||||
|           expect(updatedSleepingParticipatingMember.stats.hp).to.eql(42.5); | ||||
|           expect(updatedNonParticipatingMember.stats.hp).to.eql(50); | ||||
|           expect(updatedUndecidedMember.stats.hp).to.eql(50); | ||||
|         }); | ||||
| @@ -232,6 +305,7 @@ describe('Group Model', () => { | ||||
|           party.quest.members = { | ||||
|             [questLeader._id]: true, | ||||
|             [participatingMember._id]: true, | ||||
|             [sleepingParticipatingMember._id]: true, | ||||
|             [nonParticipatingMember._id]: false, | ||||
|             [undecidedMember._id]: null, | ||||
|           }; | ||||
| @@ -244,17 +318,20 @@ describe('Group Model', () => { | ||||
|           let [ | ||||
|             updatedLeader, | ||||
|             updatedParticipatingMember, | ||||
|             updatedSleepingParticipatingMember, | ||||
|             updatedNonParticipatingMember, | ||||
|             updatedUndecidedMember, | ||||
|           ] = await Promise.all([ | ||||
|             User.findById(questLeader._id), | ||||
|             User.findById(participatingMember._id), | ||||
|             User.findById(sleepingParticipatingMember._id), | ||||
|             User.findById(nonParticipatingMember._id), | ||||
|             User.findById(undecidedMember._id), | ||||
|           ]); | ||||
| 
 | ||||
|           expect(updatedLeader.stats.hp).to.eql(42.5); | ||||
|           expect(updatedParticipatingMember.stats.hp).to.eql(42.5); | ||||
|           expect(updatedSleepingParticipatingMember.stats.hp).to.eql(42.5); | ||||
|           expect(updatedNonParticipatingMember.stats.hp).to.eql(50); | ||||
|           expect(updatedUndecidedMember.stats.hp).to.eql(50); | ||||
|         }); | ||||
| @@ -378,7 +455,7 @@ describe('Group Model', () => { | ||||
|           await party.startQuest(questLeader); | ||||
|           await party.save(); | ||||
| 
 | ||||
|           sendChatStub = sandbox.stub(Group.prototype, 'sendChat'); | ||||
|           sendChatStub = sandbox.spy(Group.prototype, 'sendChat'); | ||||
|         }); | ||||
| 
 | ||||
|         afterEach(() => sendChatStub.restore()); | ||||
| @@ -431,7 +508,7 @@ describe('Group Model', () => { | ||||
|           party.quest.active = false; | ||||
| 
 | ||||
|           await party.startQuest(questLeader); | ||||
|           Group.prototype.sendChat.reset(); | ||||
|           Group.prototype.sendChat.resetHistory(); | ||||
|           await party.save(); | ||||
| 
 | ||||
|           await Group.processQuestProgress(participatingMember, progress); | ||||
| @@ -450,7 +527,7 @@ describe('Group Model', () => { | ||||
|           party.quest.active = false; | ||||
| 
 | ||||
|           await party.startQuest(questLeader); | ||||
|           Group.prototype.sendChat.reset(); | ||||
|           Group.prototype.sendChat.resetHistory(); | ||||
|           await party.save(); | ||||
| 
 | ||||
|           await Group.processQuestProgress(participatingMember, progress); | ||||
| @@ -493,9 +570,11 @@ describe('Group Model', () => { | ||||
|           let [ | ||||
|             updatedLeader, | ||||
|             updatedParticipatingMember, | ||||
|             updatedSleepingParticipatingMember, | ||||
|           ] = await Promise.all([ | ||||
|             User.findById(questLeader._id), | ||||
|             User.findById(participatingMember._id), | ||||
|             User.findById(sleepingParticipatingMember._id), | ||||
|           ]); | ||||
| 
 | ||||
|           expect(updatedLeader.achievements.quests[party.quest.key]).to.eql(1); | ||||
| @@ -504,6 +583,9 @@ describe('Group Model', () => { | ||||
|           expect(updatedParticipatingMember.achievements.quests[party.quest.key]).to.eql(1); | ||||
|           expect(updatedParticipatingMember.stats.exp).to.be.greaterThan(0); | ||||
|           expect(updatedParticipatingMember.stats.gp).to.be.greaterThan(0); | ||||
|           expect(updatedSleepingParticipatingMember.achievements.quests[party.quest.key]).to.eql(1); | ||||
|           expect(updatedSleepingParticipatingMember.stats.exp).to.be.greaterThan(0); | ||||
|           expect(updatedSleepingParticipatingMember.stats.gp).to.be.greaterThan(0); | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
| @@ -518,7 +600,7 @@ describe('Group Model', () => { | ||||
|       }); | ||||
| 
 | ||||
|       it('throws an error if no uuids or emails are passed in', async () => { | ||||
|         await expect(Group.validateInvitations(null, null, res)).to.eventually.be.rejected.and.eql({ | ||||
|         await expect(Group.validateInvitations({}, res)).to.eventually.be.rejected.and.eql({ | ||||
|           httpCode: 400, | ||||
|           message: 'Bad request.', | ||||
|           name: 'BadRequest', | ||||
| @@ -528,7 +610,7 @@ describe('Group Model', () => { | ||||
|       }); | ||||
| 
 | ||||
|       it('throws an error if only uuids are passed in, but they are not an array', async () => { | ||||
|         await expect(Group.validateInvitations({ uuid: 'user-id'}, null, res)).to.eventually.be.rejected.and.eql({ | ||||
|         await expect(Group.validateInvitations({ uuids: 'user-id'}, res)).to.eventually.be.rejected.and.eql({ | ||||
|           httpCode: 400, | ||||
|           message: 'Bad request.', | ||||
|           name: 'BadRequest', | ||||
| @@ -538,7 +620,7 @@ describe('Group Model', () => { | ||||
|       }); | ||||
| 
 | ||||
|       it('throws an error if only emails are passed in, but they are not an array', async () => { | ||||
|         await expect(Group.validateInvitations(null, { emails: 'user@example.com'}, res)).to.eventually.be.rejected.and.eql({ | ||||
|         await expect(Group.validateInvitations({emails: 'user@example.com'}, res)).to.eventually.be.rejected.and.eql({ | ||||
|           httpCode: 400, | ||||
|           message: 'Bad request.', | ||||
|           name: 'BadRequest', | ||||
| @@ -548,27 +630,27 @@ describe('Group Model', () => { | ||||
|       }); | ||||
| 
 | ||||
|       it('throws an error if emails are not passed in, and uuid array is empty', async () => { | ||||
|         await expect(Group.validateInvitations([], null, res)).to.eventually.be.rejected.and.eql({ | ||||
|         await expect(Group.validateInvitations({uuids: []},  res)).to.eventually.be.rejected.and.eql({ | ||||
|           httpCode: 400, | ||||
|           message: 'Bad request.', | ||||
|           name: 'BadRequest', | ||||
|         }); | ||||
|         expect(res.t).to.be.calledOnce; | ||||
|         expect(res.t).to.be.calledWith('inviteMissingUuid'); | ||||
|         expect(res.t).to.be.calledWith('inviteMustNotBeEmpty'); | ||||
|       }); | ||||
| 
 | ||||
|       it('throws an error if uuids are not passed in, and email array is empty', async () => { | ||||
|         await expect(Group.validateInvitations(null, [], res)).to.eventually.be.rejected.and.eql({ | ||||
|         await expect(Group.validateInvitations({emails: []},  res)).to.eventually.be.rejected.and.eql({ | ||||
|           httpCode: 400, | ||||
|           message: 'Bad request.', | ||||
|           name: 'BadRequest', | ||||
|         }); | ||||
|         expect(res.t).to.be.calledOnce; | ||||
|         expect(res.t).to.be.calledWith('inviteMissingEmail'); | ||||
|         expect(res.t).to.be.calledWith('inviteMustNotBeEmpty'); | ||||
|       }); | ||||
| 
 | ||||
|       it('throws an error if uuids and emails are passed in as empty arrays', async () => { | ||||
|         await expect(Group.validateInvitations([], [], res)).to.eventually.be.rejected.and.eql({ | ||||
|         await expect(Group.validateInvitations({emails: [], uuids: []}, res)).to.eventually.be.rejected.and.eql({ | ||||
|           httpCode: 400, | ||||
|           message: 'Bad request.', | ||||
|           name: 'BadRequest', | ||||
| @@ -588,7 +670,7 @@ describe('Group Model', () => { | ||||
| 
 | ||||
|         uuids.push('one-more-uuid'); // to put it over the limit
 | ||||
| 
 | ||||
|         await expect(Group.validateInvitations(uuids, emails, res)).to.eventually.be.rejected.and.eql({ | ||||
|         await expect(Group.validateInvitations({uuids, emails}, res)).to.eventually.be.rejected.and.eql({ | ||||
|           httpCode: 400, | ||||
|           message: 'Bad request.', | ||||
|           name: 'BadRequest', | ||||
| @@ -606,33 +688,33 @@ describe('Group Model', () => { | ||||
|           emails.push(`user-${i}@example.com`); | ||||
|         } | ||||
| 
 | ||||
|         await Group.validateInvitations(uuids, emails, res); | ||||
|         await Group.validateInvitations({uuids, emails}, res); | ||||
|         expect(res.t).to.not.be.called; | ||||
|       }); | ||||
| 
 | ||||
| 
 | ||||
|       it('does not throw an error if only user ids are passed in', async () => { | ||||
|         await Group.validateInvitations(['user-id', 'user-id2'], null, res); | ||||
|         await Group.validateInvitations({uuids: ['user-id', 'user-id2']}, res); | ||||
|         expect(res.t).to.not.be.called; | ||||
|       }); | ||||
| 
 | ||||
|       it('does not throw an error if only emails are passed in', async () => { | ||||
|         await Group.validateInvitations(null, ['user1@example.com', 'user2@example.com'], res); | ||||
|         await Group.validateInvitations({emails: ['user1@example.com', 'user2@example.com']}, res); | ||||
|         expect(res.t).to.not.be.called; | ||||
|       }); | ||||
| 
 | ||||
|       it('does not throw an error if both uuids and emails are passed in', async () => { | ||||
|         await Group.validateInvitations(['user-id', 'user-id2'], ['user1@example.com', 'user2@example.com'], res); | ||||
|         await Group.validateInvitations({uuids: ['user-id', 'user-id2'], emails: ['user1@example.com', 'user2@example.com']}, res); | ||||
|         expect(res.t).to.not.be.called; | ||||
|       }); | ||||
| 
 | ||||
|       it('does not throw an error if uuids are passed in and emails are an empty array', async () => { | ||||
|         await Group.validateInvitations(['user-id', 'user-id2'], [], res); | ||||
|         await Group.validateInvitations({uuids: ['user-id', 'user-id2'], emails: []}, res); | ||||
|         expect(res.t).to.not.be.called; | ||||
|       }); | ||||
| 
 | ||||
|       it('does not throw an error if emails are passed in and uuids are an empty array', async () => { | ||||
|         await Group.validateInvitations([], ['user1@example.com', 'user2@example.com'], res); | ||||
|         await Group.validateInvitations({uuids: [], emails: ['user1@example.com', 'user2@example.com']}, res); | ||||
|         expect(res.t).to.not.be.called; | ||||
|       }); | ||||
|     }); | ||||
| @@ -643,6 +725,7 @@ describe('Group Model', () => { | ||||
|       it('returns an array of members whose quest status set to true', () => { | ||||
|         party.quest.members = { | ||||
|           [participatingMember._id]: true, | ||||
|           [sleepingParticipatingMember._id]: true, | ||||
|           [questLeader._id]: true, | ||||
|           [nonParticipatingMember._id]: false, | ||||
|           [undecidedMember._id]: null, | ||||
| @@ -650,6 +733,7 @@ describe('Group Model', () => { | ||||
| 
 | ||||
|         expect(party.getParticipatingQuestMembers()).to.eql([ | ||||
|           participatingMember._id, | ||||
|           sleepingParticipatingMember._id, | ||||
|           questLeader._id, | ||||
|         ]); | ||||
|       }); | ||||
| @@ -752,11 +836,12 @@ describe('Group Model', () => { | ||||
|       it('removes user from group quest', async () => { | ||||
|         party.quest.members = { | ||||
|           [participatingMember._id]: true, | ||||
|           [sleepingParticipatingMember._id]: true, | ||||
|           [questLeader._id]: true, | ||||
|           [nonParticipatingMember._id]: false, | ||||
|           [undecidedMember._id]: null, | ||||
|         }; | ||||
|         party.memberCount = 4; | ||||
|         party.memberCount = 5; | ||||
|         await party.save(); | ||||
| 
 | ||||
|         await party.leave(participatingMember); | ||||
| @@ -764,6 +849,7 @@ describe('Group Model', () => { | ||||
|         party = await Group.findOne({_id: party._id}); | ||||
|         expect(party.quest.members).to.eql({ | ||||
|           [questLeader._id]: true, | ||||
|           [sleepingParticipatingMember._id]: true, | ||||
|           [nonParticipatingMember._id]: false, | ||||
|           [undecidedMember._id]: null, | ||||
|         }); | ||||
| @@ -771,6 +857,7 @@ describe('Group Model', () => { | ||||
| 
 | ||||
|       it('deletes a private party when the last member leaves', async () => { | ||||
|         await party.leave(participatingMember); | ||||
|         await party.leave(sleepingParticipatingMember); | ||||
|         await party.leave(questLeader); | ||||
|         await party.leave(nonParticipatingMember); | ||||
|         await party.leave(undecidedMember); | ||||
| @@ -842,6 +929,7 @@ describe('Group Model', () => { | ||||
|         party.privacy = 'public'; | ||||
| 
 | ||||
|         await party.leave(participatingMember); | ||||
|         await party.leave(sleepingParticipatingMember); | ||||
|         await party.leave(questLeader); | ||||
|         await party.leave(nonParticipatingMember); | ||||
|         await party.leave(undecidedMember); | ||||
| @@ -918,21 +1006,8 @@ describe('Group Model', () => { | ||||
|         sandbox.spy(User, 'update'); | ||||
|       }); | ||||
| 
 | ||||
|       it('puts message at top of chat array', () => { | ||||
|         let oldMessage = { | ||||
|           text: 'a message', | ||||
|         }; | ||||
|         party.chat.push(oldMessage, oldMessage, oldMessage); | ||||
| 
 | ||||
|         party.sendChat('a new message', {_id: 'user-id', profile: { name: 'user name' }}); | ||||
| 
 | ||||
|         expect(party.chat).to.have.a.lengthOf(4); | ||||
|         expect(party.chat[0].text).to.eql('a new message'); | ||||
|         expect(party.chat[0].uuid).to.eql('user-id'); | ||||
|       }); | ||||
| 
 | ||||
|       it('formats message', () => { | ||||
|         party.sendChat('a new message', { | ||||
|         const chatMessage = party.sendChat('a new message', { | ||||
|           _id: 'user-id', | ||||
|           profile: { name: 'user name' }, | ||||
|           contributor: { | ||||
| @@ -947,11 +1022,11 @@ describe('Group Model', () => { | ||||
|           }, | ||||
|         }); | ||||
| 
 | ||||
|         let chat = party.chat[0]; | ||||
|         const chat = chatMessage; | ||||
| 
 | ||||
|         expect(chat.text).to.eql('a new message'); | ||||
|         expect(validator.isUUID(chat.id)).to.eql(true); | ||||
|         expect(chat.timestamp).to.be.a('number'); | ||||
|         expect(chat.timestamp).to.be.a('date'); | ||||
|         expect(chat.likes).to.eql({}); | ||||
|         expect(chat.flags).to.eql({}); | ||||
|         expect(chat.flagCount).to.eql(0); | ||||
| @@ -962,13 +1037,11 @@ describe('Group Model', () => { | ||||
|       }); | ||||
| 
 | ||||
|       it('formats message as system if no user is passed in', () => { | ||||
|         party.sendChat('a system message'); | ||||
| 
 | ||||
|         let chat = party.chat[0]; | ||||
|         const chat = party.sendChat('a system message'); | ||||
| 
 | ||||
|         expect(chat.text).to.eql('a system message'); | ||||
|         expect(validator.isUUID(chat.id)).to.eql(true); | ||||
|         expect(chat.timestamp).to.be.a('number'); | ||||
|         expect(chat.timestamp).to.be.a('date'); | ||||
|         expect(chat.likes).to.eql({}); | ||||
|         expect(chat.flags).to.eql({}); | ||||
|         expect(chat.flagCount).to.eql(0); | ||||
| @@ -978,32 +1051,6 @@ describe('Group Model', () => { | ||||
|         expect(chat.user).to.not.exist; | ||||
|       }); | ||||
| 
 | ||||
|       it('cuts down chat to 200 messages', () => { | ||||
|         for (let i = 0; i < 220; i++) { | ||||
|           party.chat.push({ text: 'a message' }); | ||||
|         } | ||||
| 
 | ||||
|         expect(party.chat).to.have.a.lengthOf(220); | ||||
| 
 | ||||
|         party.sendChat('message'); | ||||
| 
 | ||||
|         expect(party.chat).to.have.a.lengthOf(200); | ||||
|       }); | ||||
| 
 | ||||
|       it('cuts down chat to 400 messages when group is subcribed', () => { | ||||
|         party.purchased.plan.customerId = 'test-customer-id'; | ||||
| 
 | ||||
|         for (let i = 0; i < 420; i++) { | ||||
|           party.chat.push({ text: 'a message' }); | ||||
|         } | ||||
| 
 | ||||
|         expect(party.chat).to.have.a.lengthOf(420); | ||||
| 
 | ||||
|         party.sendChat('message'); | ||||
| 
 | ||||
|         expect(party.chat).to.have.a.lengthOf(400); | ||||
|       }); | ||||
| 
 | ||||
|       it('updates users about new messages in party', () => { | ||||
|         party.sendChat('message'); | ||||
| 
 | ||||
| @@ -1085,6 +1132,7 @@ describe('Group Model', () => { | ||||
|           party.quest.members = { | ||||
|             [questLeader._id]: true, | ||||
|             [participatingMember._id]: true, | ||||
|             [sleepingParticipatingMember._id]: true, | ||||
|             [nonParticipatingMember._id]: false, | ||||
|             [undecidedMember._id]: null, | ||||
|           }; | ||||
| @@ -1141,33 +1189,44 @@ describe('Group Model', () => { | ||||
|           let expectedQuestMembers = {}; | ||||
|           expectedQuestMembers[questLeader._id] = true; | ||||
|           expectedQuestMembers[participatingMember._id] = true; | ||||
|           expectedQuestMembers[sleepingParticipatingMember._id] = true; | ||||
| 
 | ||||
|           expect(party.quest.members).to.eql(expectedQuestMembers); | ||||
|         }); | ||||
| 
 | ||||
|         it('applies updates to user object directly if user is participating', async () => { | ||||
|         it('applies updates to user object directly if user is participating (without resetting progress, except progress.down)', async () => { | ||||
|           await party.startQuest(participatingMember); | ||||
| 
 | ||||
|           expect(participatingMember.party.quest.key).to.eql('whale'); | ||||
|           expect(participatingMember.party.quest.progress.up).to.eql(10); | ||||
|           expect(participatingMember.party.quest.progress.down).to.eql(0); | ||||
|           expect(participatingMember.party.quest.progress.collectedItems).to.eql(0); | ||||
|           expect(participatingMember.party.quest.progress.collectedItems).to.eql(5); | ||||
|           expect(participatingMember.party.quest.completed).to.eql(null); | ||||
|         }); | ||||
| 
 | ||||
|         it('applies updates to other participating members', async () => { | ||||
|         it('applies updates to other participating members (without resetting progress, except progress.down)', async () => { | ||||
|           await party.startQuest(nonParticipatingMember); | ||||
| 
 | ||||
|           questLeader = await User.findById(questLeader._id); | ||||
|           participatingMember = await User.findById(participatingMember._id); | ||||
|           sleepingParticipatingMember = await User.findById(sleepingParticipatingMember._id); | ||||
| 
 | ||||
|           expect(participatingMember.party.quest.key).to.eql('whale'); | ||||
|           expect(participatingMember.party.quest.progress.up).to.eql(10); | ||||
|           expect(participatingMember.party.quest.progress.down).to.eql(0); | ||||
|           expect(participatingMember.party.quest.progress.collectedItems).to.eql(0); | ||||
|           expect(participatingMember.party.quest.progress.collectedItems).to.eql(5); | ||||
|           expect(participatingMember.party.quest.completed).to.eql(null); | ||||
| 
 | ||||
|           expect(sleepingParticipatingMember.party.quest.key).to.eql('whale'); | ||||
|           expect(sleepingParticipatingMember.party.quest.progress.up).to.eql(10); | ||||
|           expect(sleepingParticipatingMember.party.quest.progress.down).to.eql(0); | ||||
|           expect(sleepingParticipatingMember.party.quest.progress.collectedItems).to.eql(5); | ||||
|           expect(sleepingParticipatingMember.party.quest.completed).to.eql(null); | ||||
| 
 | ||||
|           expect(questLeader.party.quest.key).to.eql('whale'); | ||||
|           expect(questLeader.party.quest.progress.up).to.eql(10); | ||||
|           expect(questLeader.party.quest.progress.down).to.eql(0); | ||||
|           expect(questLeader.party.quest.progress.collectedItems).to.eql(0); | ||||
|           expect(questLeader.party.quest.progress.collectedItems).to.eql(5); | ||||
|           expect(questLeader.party.quest.completed).to.eql(null); | ||||
|         }); | ||||
| 
 | ||||
| @@ -1178,14 +1237,19 @@ describe('Group Model', () => { | ||||
|           undecidedMember = await User.findById(undecidedMember._id); | ||||
| 
 | ||||
|           expect(nonParticipatingMember.party.quest.key).to.not.eql('whale'); | ||||
|           expect(nonParticipatingMember.party.quest.progress.up).to.eql(10); | ||||
|           expect(nonParticipatingMember.party.quest.progress.down).to.eql(8); | ||||
|           expect(nonParticipatingMember.party.quest.progress.collectedItems).to.eql(5); | ||||
|           expect(undecidedMember.party.quest.key).to.not.eql('whale'); | ||||
|         }); | ||||
| 
 | ||||
|         it('sends email to participating members that quest has started', async () => { | ||||
|           participatingMember.preferences.emailNotifications.questStarted = true; | ||||
|           sleepingParticipatingMember.preferences.emailNotifications.questStarted = true; | ||||
|           questLeader.preferences.emailNotifications.questStarted = true; | ||||
|           await Promise.all([ | ||||
|             participatingMember.save(), | ||||
|             sleepingParticipatingMember.save(), | ||||
|             questLeader.save(), | ||||
|           ]); | ||||
| 
 | ||||
| @@ -1198,17 +1262,70 @@ describe('Group Model', () => { | ||||
|           let memberIds = _.map(email.sendTxn.args[0][0], '_id'); | ||||
|           let typeOfEmail = email.sendTxn.args[0][1]; | ||||
| 
 | ||||
|           expect(memberIds).to.have.a.lengthOf(2); | ||||
|           expect(memberIds).to.have.a.lengthOf(3); | ||||
|           expect(memberIds).to.include(participatingMember._id); | ||||
|           expect(memberIds).to.include(sleepingParticipatingMember._id); | ||||
|           expect(memberIds).to.include(questLeader._id); | ||||
|           expect(typeOfEmail).to.eql('quest-started'); | ||||
|         }); | ||||
| 
 | ||||
|         it('sends webhook to participating members that quest has started', async () => { | ||||
|           // should receive webhook
 | ||||
|           participatingMember.webhooks = [{ | ||||
|             type: 'questActivity', | ||||
|             url: 'http://someurl.com', | ||||
|             options: { | ||||
|               questStarted: true, | ||||
|             }, | ||||
|           }]; | ||||
|           sleepingParticipatingMember.webhooks = [{ | ||||
|             type: 'questActivity', | ||||
|             url: 'http://someurl.com', | ||||
|             options: { | ||||
|               questStarted: true, | ||||
|             }, | ||||
|           }]; | ||||
|           questLeader.webhooks = [{ | ||||
|             type: 'questActivity', | ||||
|             url: 'http://someurl.com', | ||||
|             options: { | ||||
|               questStarted: true, | ||||
|             }, | ||||
|           }]; | ||||
| 
 | ||||
|           await Promise.all([participatingMember.save(), sleepingParticipatingMember.save(), questLeader.save()]); | ||||
| 
 | ||||
|           await party.startQuest(nonParticipatingMember); | ||||
| 
 | ||||
|           await sleep(0.5); | ||||
| 
 | ||||
|           expect(questActivityWebhook.send).to.be.calledThrice; // for 3 participating members
 | ||||
| 
 | ||||
|           let args = questActivityWebhook.send.args[0]; | ||||
|           let webhooks = args[0].webhooks; | ||||
|           let webhookOwner = args[0]._id; | ||||
|           let options = args[1]; | ||||
| 
 | ||||
|           expect(webhooks).to.have.a.lengthOf(1); | ||||
|           if (webhookOwner === questLeader._id) { | ||||
|             expect(webhooks[0].id).to.eql(questLeader.webhooks[0].id); | ||||
|           } else if (webhookOwner === sleepingParticipatingMember._id) { | ||||
|             expect(webhooks[0].id).to.eql(sleepingParticipatingMember.webhooks[0].id); | ||||
|           } else { | ||||
|             expect(webhooks[0].id).to.eql(participatingMember.webhooks[0].id); | ||||
|           } | ||||
|           expect(webhooks[0].type).to.eql('questActivity'); | ||||
|           expect(options.group).to.eql(party); | ||||
|           expect(options.quest.key).to.eql('whale'); | ||||
|         }); | ||||
| 
 | ||||
|         it('sends email only to members who have not opted out', async () => { | ||||
|           participatingMember.preferences.emailNotifications.questStarted = false; | ||||
|           sleepingParticipatingMember.preferences.emailNotifications.questStarted = false; | ||||
|           questLeader.preferences.emailNotifications.questStarted = true; | ||||
|           await Promise.all([ | ||||
|             participatingMember.save(), | ||||
|             sleepingParticipatingMember.save(), | ||||
|             questLeader.save(), | ||||
|           ]); | ||||
| 
 | ||||
| @@ -1222,14 +1339,17 @@ describe('Group Model', () => { | ||||
| 
 | ||||
|           expect(memberIds).to.have.a.lengthOf(1); | ||||
|           expect(memberIds).to.not.include(participatingMember._id); | ||||
|           expect(memberIds).to.not.include(sleepingParticipatingMember._id); | ||||
|           expect(memberIds).to.include(questLeader._id); | ||||
|         }); | ||||
| 
 | ||||
|         it('does not send email to initiating member', async () => { | ||||
|           participatingMember.preferences.emailNotifications.questStarted = true; | ||||
|           sleepingParticipatingMember.preferences.emailNotifications.questStarted = true; | ||||
|           questLeader.preferences.emailNotifications.questStarted = true; | ||||
|           await Promise.all([ | ||||
|             participatingMember.save(), | ||||
|             sleepingParticipatingMember.save(), | ||||
|             questLeader.save(), | ||||
|           ]); | ||||
| 
 | ||||
| @@ -1241,8 +1361,9 @@ describe('Group Model', () => { | ||||
| 
 | ||||
|           let memberIds = _.map(email.sendTxn.args[0][0], '_id'); | ||||
| 
 | ||||
|           expect(memberIds).to.have.a.lengthOf(1); | ||||
|           expect(memberIds).to.have.a.lengthOf(2); | ||||
|           expect(memberIds).to.not.include(participatingMember._id); | ||||
|           expect(memberIds).to.include(sleepingParticipatingMember._id); | ||||
|           expect(memberIds).to.include(questLeader._id); | ||||
|         }); | ||||
| 
 | ||||
| @@ -1251,7 +1372,7 @@ describe('Group Model', () => { | ||||
| 
 | ||||
|           await party.startQuest(nonParticipatingMember); | ||||
| 
 | ||||
|           let members = [questLeader._id, participatingMember._id]; | ||||
|           let members = [questLeader._id, participatingMember._id, sleepingParticipatingMember._id]; | ||||
| 
 | ||||
|           expect(User.update).to.be.calledWith( | ||||
|             { _id: { $in: members } }, | ||||
| @@ -1286,8 +1407,9 @@ describe('Group Model', () => { | ||||
|           let userQuest = participatingMember.party.quest; | ||||
| 
 | ||||
|           expect(userQuest.key).to.eql('whale'); | ||||
|           expect(userQuest.progress.up).to.eql(10); | ||||
|           expect(userQuest.progress.down).to.eql(0); | ||||
|           expect(userQuest.progress.collectedItems).to.eql(0); | ||||
|           expect(userQuest.progress.collectedItems).to.eql(5); | ||||
|           expect(userQuest.completed).to.eql(null); | ||||
|         }); | ||||
| 
 | ||||
| @@ -1316,6 +1438,7 @@ describe('Group Model', () => { | ||||
|         party.quest.members = { | ||||
|           [questLeader._id]: true, | ||||
|           [participatingMember._id]: true, | ||||
|           [sleepingParticipatingMember._id]: true, | ||||
|           [nonParticipatingMember._id]: false, | ||||
|           [undecidedMember._id]: null, | ||||
|         }; | ||||
| @@ -1338,7 +1461,7 @@ describe('Group Model', () => { | ||||
| 
 | ||||
|           await party.finishQuest(quest); | ||||
| 
 | ||||
|           expect(User.update).to.be.calledTwice; | ||||
|           expect(User.update).to.be.calledThrice; | ||||
|         }); | ||||
| 
 | ||||
|         it('stops retrying when a successful update has occurred', async () => { | ||||
| @@ -1348,7 +1471,7 @@ describe('Group Model', () => { | ||||
| 
 | ||||
|           await party.finishQuest(quest); | ||||
| 
 | ||||
|           expect(User.update).to.be.calledThrice; | ||||
|           expect(User.update.callCount).to.equal(4); | ||||
|         }); | ||||
| 
 | ||||
|         it('retries failed updates at most five times per user', async () => { | ||||
| @@ -1356,7 +1479,7 @@ describe('Group Model', () => { | ||||
| 
 | ||||
|           await expect(party.finishQuest(quest)).to.eventually.be.rejected; | ||||
| 
 | ||||
|           expect(User.update.callCount).to.eql(10); | ||||
|           expect(User.update.callCount).to.eql(15); // for 3 users
 | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
| @@ -1366,13 +1489,16 @@ describe('Group Model', () => { | ||||
|         let [ | ||||
|           updatedLeader, | ||||
|           updatedParticipatingMember, | ||||
|           updatedSleepingParticipatingMember, | ||||
|         ] = await Promise.all([ | ||||
|           User.findById(questLeader._id), | ||||
|           User.findById(participatingMember._id), | ||||
|           User.findById(sleepingParticipatingMember._id), | ||||
|         ]); | ||||
| 
 | ||||
|         expect(updatedLeader.achievements.quests[quest.key]).to.eql(1); | ||||
|         expect(updatedParticipatingMember.achievements.quests[quest.key]).to.eql(1); | ||||
|         expect(updatedSleepingParticipatingMember.achievements.quests[quest.key]).to.eql(1); | ||||
|       }); | ||||
| 
 | ||||
|       it('gives out super awesome Masterclasser achievement to the deserving', async () => { | ||||
| @@ -1402,13 +1528,16 @@ describe('Group Model', () => { | ||||
|         let [ | ||||
|           updatedLeader, | ||||
|           updatedParticipatingMember, | ||||
|           updatedSleepingParticipatingMember, | ||||
|         ] = await Promise.all([ | ||||
|           User.findById(questLeader._id).exec(), | ||||
|           User.findById(participatingMember._id).exec(), | ||||
|           User.findById(sleepingParticipatingMember._id).exec(), | ||||
|         ]); | ||||
| 
 | ||||
|         expect(updatedLeader.achievements.lostMasterclasser).to.eql(true); | ||||
|         expect(updatedParticipatingMember.achievements.lostMasterclasser).to.not.eql(true); | ||||
|         expect(updatedSleepingParticipatingMember.achievements.lostMasterclasser).to.not.eql(true); | ||||
|       }); | ||||
| 
 | ||||
|       it('gives out super awesome Masterclasser achievement when quests done out of order', async () => { | ||||
| @@ -1438,13 +1567,16 @@ describe('Group Model', () => { | ||||
|         let [ | ||||
|           updatedLeader, | ||||
|           updatedParticipatingMember, | ||||
|           updatedSleepingParticipatingMember, | ||||
|         ] = await Promise.all([ | ||||
|           User.findById(questLeader._id).exec(), | ||||
|           User.findById(participatingMember._id).exec(), | ||||
|           User.findById(sleepingParticipatingMember._id).exec(), | ||||
|         ]); | ||||
| 
 | ||||
|         expect(updatedLeader.achievements.lostMasterclasser).to.eql(true); | ||||
|         expect(updatedParticipatingMember.achievements.lostMasterclasser).to.not.eql(true); | ||||
|         expect(updatedSleepingParticipatingMember.achievements.lostMasterclasser).to.not.eql(true); | ||||
|       }); | ||||
| 
 | ||||
|       it('gives xp and gold', async () => { | ||||
| @@ -1453,15 +1585,19 @@ describe('Group Model', () => { | ||||
|         let [ | ||||
|           updatedLeader, | ||||
|           updatedParticipatingMember, | ||||
|           updatedSleepingParticipatingMember, | ||||
|         ] = await Promise.all([ | ||||
|           User.findById(questLeader._id), | ||||
|           User.findById(participatingMember._id), | ||||
|           User.findById(sleepingParticipatingMember._id), | ||||
|         ]); | ||||
| 
 | ||||
|         expect(updatedLeader.stats.exp).to.eql(quest.drop.exp); | ||||
|         expect(updatedLeader.stats.gp).to.eql(quest.drop.gp); | ||||
|         expect(updatedParticipatingMember.stats.exp).to.eql(quest.drop.exp); | ||||
|         expect(updatedParticipatingMember.stats.gp).to.eql(quest.drop.gp); | ||||
|         expect(updatedSleepingParticipatingMember.stats.exp).to.eql(quest.drop.exp); | ||||
|         expect(updatedSleepingParticipatingMember.stats.gp).to.eql(quest.drop.gp); | ||||
|       }); | ||||
| 
 | ||||
|       context('drops', () => { | ||||
| @@ -1561,28 +1697,74 @@ describe('Group Model', () => { | ||||
|           sandbox.spy(User, 'update'); | ||||
|           await party.finishQuest(quest); | ||||
| 
 | ||||
|           expect(User.update).to.be.calledTwice; | ||||
|           expect(User.update).to.be.calledThrice; | ||||
|           expect(User.update).to.be.calledWithMatch({ | ||||
|             _id: questLeader._id, | ||||
|           }); | ||||
|           expect(User.update).to.be.calledWithMatch({ | ||||
|             _id: participatingMember._id, | ||||
|           }); | ||||
|           expect(User.update).to.be.calledWithMatch({ | ||||
|             _id: sleepingParticipatingMember._id, | ||||
|           }); | ||||
|         }); | ||||
| 
 | ||||
|         it('sets user quest object to a clean state', async () => { | ||||
|         it('updates participating members quest object to a clean state (except for progress)', async () => { | ||||
|           await party.finishQuest(quest); | ||||
| 
 | ||||
|           let updatedLeader = await User.findById(questLeader._id); | ||||
|           questLeader = await User.findById(questLeader._id); | ||||
|           participatingMember = await User.findById(participatingMember._id); | ||||
| 
 | ||||
|           expect(updatedLeader.party.quest.completed).to.eql('whale'); | ||||
|           expect(updatedLeader.party.quest.progress.up).to.eql(0); | ||||
|           expect(updatedLeader.party.quest.progress.down).to.eql(0); | ||||
|           expect(updatedLeader.party.quest.progress.collectedItems).to.eql(0); | ||||
|           expect(updatedLeader.party.quest.RSVPNeeded).to.eql(false); | ||||
|           expect(questLeader.party.quest.completed).to.eql('whale'); | ||||
|           expect(questLeader.party.quest.progress.up).to.eql(10); | ||||
|           expect(questLeader.party.quest.progress.down).to.eql(8); | ||||
|           expect(questLeader.party.quest.progress.collectedItems).to.eql(5); | ||||
|           expect(questLeader.party.quest.RSVPNeeded).to.eql(false); | ||||
| 
 | ||||
|           expect(participatingMember.party.quest.completed).to.eql('whale'); | ||||
|           expect(participatingMember.party.quest.progress.up).to.eql(10); | ||||
|           expect(participatingMember.party.quest.progress.down).to.eql(8); | ||||
|           expect(participatingMember.party.quest.progress.collectedItems).to.eql(5); | ||||
|           expect(participatingMember.party.quest.RSVPNeeded).to.eql(false); | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|       it('sends webhook to participating members that quest has finished', async () => { | ||||
|         // should receive webhook
 | ||||
|         participatingMember.webhooks = [{ | ||||
|           type: 'questActivity', | ||||
|           url: 'http://someurl.com', | ||||
|           options: { | ||||
|             questFinished: true, | ||||
|           }, | ||||
|         }]; | ||||
|         questLeader.webhooks = [{ | ||||
|           type: 'questActivity', | ||||
|           url: 'http://someurl.com', | ||||
|           options: { | ||||
|             questStarted: true, // will not receive the webhook
 | ||||
|           }, | ||||
|         }]; | ||||
| 
 | ||||
|         await Promise.all([participatingMember.save(), sleepingParticipatingMember.save(), questLeader.save()]); | ||||
| 
 | ||||
|         await party.finishQuest(quest); | ||||
| 
 | ||||
|         await sleep(0.5); | ||||
| 
 | ||||
|         expect(questActivityWebhook.send).to.be.calledOnce; | ||||
| 
 | ||||
|         let args = questActivityWebhook.send.args[0]; | ||||
|         let webhooks = args[0].webhooks; | ||||
|         let options = args[1]; | ||||
| 
 | ||||
|         expect(webhooks).to.have.a.lengthOf(1); | ||||
|         expect(webhooks[0].id).to.eql(participatingMember.webhooks[0].id); | ||||
|         expect(webhooks[0].type).to.eql('questActivity'); | ||||
|         expect(options.group).to.eql(party); | ||||
|         expect(options.quest.key).to.eql(quest.key); | ||||
|       }); | ||||
| 
 | ||||
|       context('World quests in Tavern', () => { | ||||
|         let tavernQuest; | ||||
| 
 | ||||
| @@ -1698,7 +1880,7 @@ describe('Group Model', () => { | ||||
|         expect(groupChatReceivedWebhook.send).to.be.calledOnce; | ||||
| 
 | ||||
|         let args = groupChatReceivedWebhook.send.args[0]; | ||||
|         let webhooks = args[0]; | ||||
|         let webhooks = args[0].webhooks; | ||||
|         let options = args[1]; | ||||
| 
 | ||||
|         expect(webhooks).to.have.a.lengthOf(1); | ||||
| @@ -1707,6 +1889,62 @@ describe('Group Model', () => { | ||||
|         expect(options.chat).to.eql(chat); | ||||
|       }); | ||||
| 
 | ||||
|       it('sends webhooks for users with webhooks triggered by system messages', async () => { | ||||
|         let guild = new Group({ | ||||
|           name: 'some guild', | ||||
|           type: 'guild', | ||||
|         }); | ||||
| 
 | ||||
|         let memberWithWebhook = new User({ | ||||
|           guilds: [guild._id], | ||||
|           webhooks: [{ | ||||
|             type: 'groupChatReceived', | ||||
|             url: 'http://someurl.com', | ||||
|             options: { | ||||
|               groupId: guild._id, | ||||
|             }, | ||||
|           }], | ||||
|         }); | ||||
|         let memberWithoutWebhook = new User({ | ||||
|           guilds: [guild._id], | ||||
|         }); | ||||
|         let nonMemberWithWebhooks = new User({ | ||||
|           webhooks: [{ | ||||
|             type: 'groupChatReceived', | ||||
|             url: 'http://a-different-url.com', | ||||
|             options: { | ||||
|               groupId: generateUUID(), | ||||
|             }, | ||||
|           }], | ||||
|         }); | ||||
| 
 | ||||
|         await Promise.all([ | ||||
|           memberWithWebhook.save(), | ||||
|           memberWithoutWebhook.save(), | ||||
|           nonMemberWithWebhooks.save(), | ||||
|         ]); | ||||
| 
 | ||||
|         guild.leader = memberWithWebhook._id; | ||||
| 
 | ||||
|         await guild.save(); | ||||
| 
 | ||||
|         const groupMessage = guild.sendChat('Test message.'); | ||||
|         await groupMessage.save(); | ||||
| 
 | ||||
|         await sleep(); | ||||
| 
 | ||||
|         expect(groupChatReceivedWebhook.send).to.be.calledOnce; | ||||
| 
 | ||||
|         let args = groupChatReceivedWebhook.send.args[0]; | ||||
|         let webhooks = args[0].webhooks; | ||||
|         let options = args[1]; | ||||
| 
 | ||||
|         expect(webhooks).to.have.a.lengthOf(1); | ||||
|         expect(webhooks[0].id).to.eql(memberWithWebhook.webhooks[0].id); | ||||
|         expect(options.group).to.eql(guild); | ||||
|         expect(options.chat).to.eql(groupMessage); | ||||
|       }); | ||||
| 
 | ||||
|       it('sends webhooks for each user with webhooks in group', async () => { | ||||
|         let guild = new Group({ | ||||
|           name: 'some guild', | ||||
| @@ -1762,9 +2000,9 @@ describe('Group Model', () => { | ||||
|         expect(groupChatReceivedWebhook.send).to.be.calledThrice; | ||||
| 
 | ||||
|         let args = groupChatReceivedWebhook.send.args; | ||||
|         expect(args.find(arg => arg[0][0].id === memberWithWebhook.webhooks[0].id)).to.be.exist; | ||||
|         expect(args.find(arg => arg[0][0].id === memberWithWebhook2.webhooks[0].id)).to.be.exist; | ||||
|         expect(args.find(arg => arg[0][0].id === memberWithWebhook3.webhooks[0].id)).to.be.exist; | ||||
|         expect(args.find(arg => arg[0].webhooks[0].id === memberWithWebhook.webhooks[0].id)).to.be.exist; | ||||
|         expect(args.find(arg => arg[0].webhooks[0].id === memberWithWebhook2.webhooks[0].id)).to.be.exist; | ||||
|         expect(args.find(arg => arg[0].webhooks[0].id === memberWithWebhook3.webhooks[0].id)).to.be.exist; | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
| @@ -1796,28 +2034,54 @@ describe('Group Model', () => { | ||||
| 
 | ||||
|     context('hasNotCancelled', () => { | ||||
|       it('returns false if group does not have customer id', () => { | ||||
|         expect(party.hasNotCancelled()).to.be.undefined; | ||||
|         expect(party.hasNotCancelled()).to.be.false; | ||||
|       }); | ||||
| 
 | ||||
|       it('returns true if party does not have plan.dateTerminated', () => { | ||||
|       it('returns true if group does not have plan.dateTerminated', () => { | ||||
|         party.purchased.plan.customerId = 'test-id'; | ||||
| 
 | ||||
|         expect(party.hasNotCancelled()).to.be.true; | ||||
|       }); | ||||
| 
 | ||||
|       it('returns false if party if plan.dateTerminated is after today', () => { | ||||
|       it('returns false if group if plan.dateTerminated is after today', () => { | ||||
|         party.purchased.plan.customerId = 'test-id'; | ||||
|         party.purchased.plan.dateTerminated = moment().add(1, 'days').toDate(); | ||||
| 
 | ||||
|         expect(party.hasNotCancelled()).to.be.false; | ||||
|       }); | ||||
| 
 | ||||
|       it('returns false if party if plan.dateTerminated is before today', () => { | ||||
|       it('returns false if group if plan.dateTerminated is before today', () => { | ||||
|         party.purchased.plan.customerId = 'test-id'; | ||||
|         party.purchased.plan.dateTerminated = moment().subtract(1, 'days').toDate(); | ||||
| 
 | ||||
|         expect(party.hasNotCancelled()).to.be.false; | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     context('hasCancelled', () => { | ||||
|       it('returns false if group does not have customer id', () => { | ||||
|         expect(party.hasCancelled()).to.be.false; | ||||
|       }); | ||||
| 
 | ||||
|       it('returns false if group does not have plan.dateTerminated', () => { | ||||
|         party.purchased.plan.customerId = 'test-id'; | ||||
| 
 | ||||
|         expect(party.hasCancelled()).to.be.false; | ||||
|       }); | ||||
| 
 | ||||
|       it('returns true if group if plan.dateTerminated is after today', () => { | ||||
|         party.purchased.plan.customerId = 'test-id'; | ||||
|         party.purchased.plan.dateTerminated = moment().add(1, 'days').toDate(); | ||||
| 
 | ||||
|         expect(party.hasCancelled()).to.be.true; | ||||
|       }); | ||||
| 
 | ||||
|       it('returns false if group if plan.dateTerminated is before today', () => { | ||||
|         party.purchased.plan.customerId = 'test-id'; | ||||
|         party.purchased.plan.dateTerminated = moment().subtract(1, 'days').toDate(); | ||||
| 
 | ||||
|         expect(party.hasCancelled()).to.be.false; | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { model as Challenge } from '../../../../../website/server/models/challenge'; | ||||
| import { model as Group } from '../../../../../website/server/models/group'; | ||||
| import { model as User } from '../../../../../website/server/models/user'; | ||||
| import * as Tasks from '../../../../../website/server/models/task'; | ||||
| import { model as Challenge } from '../../../../website/server/models/challenge'; | ||||
| import { model as Group } from '../../../../website/server/models/group'; | ||||
| import { model as User } from '../../../../website/server/models/user'; | ||||
| import * as Tasks from '../../../../website/server/models/task'; | ||||
| import { each, find, findIndex } from 'lodash'; | ||||
| 
 | ||||
| describe('Group Task Methods', () => { | ||||
| @@ -1,10 +1,10 @@ | ||||
| import { model as Challenge } from '../../../../../website/server/models/challenge'; | ||||
| import { model as Group } from '../../../../../website/server/models/group'; | ||||
| import { model as User } from '../../../../../website/server/models/user'; | ||||
| import * as Tasks from '../../../../../website/server/models/task'; | ||||
| import { InternalServerError } from '../../../../../website/server/libs/errors'; | ||||
| import { model as Challenge } from '../../../../website/server/models/challenge'; | ||||
| import { model as Group } from '../../../../website/server/models/group'; | ||||
| import { model as User } from '../../../../website/server/models/user'; | ||||
| import * as Tasks from '../../../../website/server/models/task'; | ||||
| import { InternalServerError } from '../../../../website/server/libs/errors'; | ||||
| import { each } from 'lodash'; | ||||
| import { generateHistory } from '../../../../helpers/api-unit.helper.js'; | ||||
| import { generateHistory } from '../../../helpers/api-unit.helper.js'; | ||||
| 
 | ||||
| describe('Task Model', () => { | ||||
|   let guild, leader, challenge, task; | ||||
| @@ -1,7 +1,7 @@ | ||||
| import moment from 'moment'; | ||||
| import { model as User } from '../../../../../website/server/models/user'; | ||||
| import { model as Group } from '../../../../../website/server/models/group'; | ||||
| import common from '../../../../../website/common'; | ||||
| import { model as User } from '../../../../website/server/models/user'; | ||||
| import { model as Group } from '../../../../website/server/models/group'; | ||||
| import common from '../../../../website/common'; | ||||
| 
 | ||||
| describe('User Model', () => { | ||||
|   it('keeps user._tmp when calling .toJSON', () => { | ||||
| @@ -42,13 +42,48 @@ describe('User Model', () => { | ||||
|     expect(userToJSON.stats.maxHealth).to.not.exist; | ||||
|     expect(userToJSON.stats.toNextLevel).to.not.exist; | ||||
| 
 | ||||
|     user.addComputedStatsToJSONObj(userToJSON.stats); | ||||
|     User.addComputedStatsToJSONObj(userToJSON.stats, userToJSON); | ||||
| 
 | ||||
|     expect(userToJSON.stats.maxMP).to.exist; | ||||
|     expect(userToJSON.stats.maxHealth).to.equal(common.maxHealth); | ||||
|     expect(userToJSON.stats.toNextLevel).to.equal(common.tnl(user.stats.lvl)); | ||||
|   }); | ||||
| 
 | ||||
|   it('can transform user object without mongoose helpers', async () => { | ||||
|     let user = new User(); | ||||
|     await user.save(); | ||||
|     let userToJSON = await User.findById(user._id).lean().exec(); | ||||
| 
 | ||||
|     expect(userToJSON.stats.maxMP).to.not.exist; | ||||
|     expect(userToJSON.stats.maxHealth).to.not.exist; | ||||
|     expect(userToJSON.stats.toNextLevel).to.not.exist; | ||||
|     expect(userToJSON.id).to.not.exist; | ||||
| 
 | ||||
|     User.transformJSONUser(userToJSON); | ||||
| 
 | ||||
|     expect(userToJSON.id).to.equal(userToJSON._id); | ||||
|     expect(userToJSON.stats.maxMP).to.not.exist; | ||||
|     expect(userToJSON.stats.maxHealth).to.not.exist; | ||||
|     expect(userToJSON.stats.toNextLevel).to.not.exist; | ||||
|   }); | ||||
| 
 | ||||
|   it('can transform user object without mongoose helpers (including computed stats)', async () => { | ||||
|     let user = new User(); | ||||
|     await user.save(); | ||||
|     let userToJSON = await User.findById(user._id).lean().exec(); | ||||
| 
 | ||||
|     expect(userToJSON.stats.maxMP).to.not.exist; | ||||
|     expect(userToJSON.stats.maxHealth).to.not.exist; | ||||
|     expect(userToJSON.stats.toNextLevel).to.not.exist; | ||||
| 
 | ||||
|     User.transformJSONUser(userToJSON, true); | ||||
| 
 | ||||
|     expect(userToJSON.id).to.equal(userToJSON._id); | ||||
|     expect(userToJSON.stats.maxMP).to.exist; | ||||
|     expect(userToJSON.stats.maxHealth).to.equal(common.maxHealth); | ||||
|     expect(userToJSON.stats.toNextLevel).to.equal(common.tnl(user.stats.lvl)); | ||||
|   }); | ||||
| 
 | ||||
|   context('notifications', () => { | ||||
|     it('can add notifications without data', () => { | ||||
|       let user = new User(); | ||||
| @@ -280,9 +315,8 @@ describe('User Model', () => { | ||||
|       user = new User(); | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|     it('returns false if user does not have customer id', () => { | ||||
|       expect(user.hasNotCancelled()).to.be.undefined; | ||||
|       expect(user.hasNotCancelled()).to.be.false; | ||||
|     }); | ||||
| 
 | ||||
|     it('returns true if user does not have plan.dateTerminated', () => { | ||||
| @@ -306,6 +340,38 @@ describe('User Model', () => { | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
| 
 | ||||
|   context('hasCancelled', () => { | ||||
|     let user; | ||||
|     beforeEach(() => { | ||||
|       user = new User(); | ||||
|     }); | ||||
| 
 | ||||
|     it('returns false if user does not have customer id', () => { | ||||
|       expect(user.hasCancelled()).to.be.false; | ||||
|     }); | ||||
| 
 | ||||
|     it('returns false if user does not have plan.dateTerminated', () => { | ||||
|       user.purchased.plan.customerId = 'test-id'; | ||||
| 
 | ||||
|       expect(user.hasCancelled()).to.be.false; | ||||
|     }); | ||||
| 
 | ||||
|     it('returns true if user if plan.dateTerminated is after today', () => { | ||||
|       user.purchased.plan.customerId = 'test-id'; | ||||
|       user.purchased.plan.dateTerminated = moment().add(1, 'days').toDate(); | ||||
| 
 | ||||
|       expect(user.hasCancelled()).to.be.true; | ||||
|     }); | ||||
| 
 | ||||
|     it('returns false if user if plan.dateTerminated is before today', () => { | ||||
|       user.purchased.plan.customerId = 'test-id'; | ||||
|       user.purchased.plan.dateTerminated = moment().subtract(1, 'days').toDate(); | ||||
| 
 | ||||
|       expect(user.hasCancelled()).to.be.false; | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   context('pre-save hook', () => { | ||||
|     it('does not try to award achievements when achievements or items not selected in query', async () => { | ||||
|       let user = new User(); | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { model as UserNotification } from '../../../../../website/server/models/userNotification'; | ||||
| import { model as UserNotification } from '../../../../website/server/models/userNotification'; | ||||
| 
 | ||||
| describe('UserNotification Model', () => { | ||||
|   context('convertNotificationsToSafeJson', () => { | ||||
							
								
								
									
										323
									
								
								test/api/unit/models/webhook.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										323
									
								
								test/api/unit/models/webhook.test.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,323 @@ | ||||
| import { model as Webhook } from '../../../../website/server/models/webhook'; | ||||
| import { BadRequest } from '../../../../website/server/libs/errors'; | ||||
| import { v4 as generateUUID } from 'uuid'; | ||||
| import apiError from '../../../../website/server/libs/apiError'; | ||||
|  | ||||
| describe('Webhook Model', () => { | ||||
|   context('Instance Methods', () => { | ||||
|     describe('#formatOptions', () => { | ||||
|       let res; | ||||
|  | ||||
|       beforeEach(() => { | ||||
|         res = { | ||||
|           t: sandbox.spy(), | ||||
|         }; | ||||
|       }); | ||||
|       context('type is taskActivity', () => { | ||||
|         let config; | ||||
|  | ||||
|         beforeEach(() => { | ||||
|           config = { | ||||
|             type: 'taskActivity', | ||||
|             url: 'https//exmaple.com/endpoint', | ||||
|             options: { | ||||
|               created: true, | ||||
|               updated: true, | ||||
|               deleted: true, | ||||
|               scored: true, | ||||
|               checklistScored: true, | ||||
|             }, | ||||
|           }; | ||||
|         }); | ||||
|  | ||||
|         it('it provides default values for options', () => { | ||||
|           delete config.options; | ||||
|  | ||||
|           let wh = new Webhook(config); | ||||
|  | ||||
|           wh.formatOptions(res); | ||||
|  | ||||
|           expect(wh.options).to.eql({ | ||||
|             checklistScored: false, | ||||
|             created: false, | ||||
|             updated: false, | ||||
|             deleted: false, | ||||
|             scored: true, | ||||
|           }); | ||||
|         }); | ||||
|  | ||||
|         it('provides missing task options', () => { | ||||
|           delete config.options.created; | ||||
|  | ||||
|           let wh = new Webhook(config); | ||||
|  | ||||
|           wh.formatOptions(res); | ||||
|  | ||||
|           expect(wh.options).to.eql({ | ||||
|             checklistScored: true, | ||||
|             created: false, | ||||
|             updated: true, | ||||
|             deleted: true, | ||||
|             scored: true, | ||||
|           }); | ||||
|         }); | ||||
|  | ||||
|         it('discards additional options', () => { | ||||
|           config.options.foo = 'another option'; | ||||
|  | ||||
|           let wh = new Webhook(config); | ||||
|  | ||||
|           wh.formatOptions(res); | ||||
|  | ||||
|           expect(wh.options.foo).to.not.exist; | ||||
|           expect(wh.options).to.eql({ | ||||
|             checklistScored: true, | ||||
|             created: true, | ||||
|             updated: true, | ||||
|             deleted: true, | ||||
|             scored: true, | ||||
|           }); | ||||
|         }); | ||||
|  | ||||
|         ['created', 'updated', 'deleted', 'scored', 'checklistScored'].forEach((option) => { | ||||
|           it(`validates that ${option} is a boolean`, (done) => { | ||||
|             config.options[option] = 'not a boolean'; | ||||
|  | ||||
|             try { | ||||
|               let wh = new Webhook(config); | ||||
|  | ||||
|               wh.formatOptions(res); | ||||
|             } catch (err) { | ||||
|               expect(err).to.be.an.instanceOf(BadRequest); | ||||
|               expect(res.t).to.be.calledOnce; | ||||
|               expect(res.t).to.be.calledWith('webhookBooleanOption', { option }); | ||||
|               done(); | ||||
|             } | ||||
|           }); | ||||
|         }); | ||||
|       }); | ||||
|  | ||||
|       context('type is userActivity', () => { | ||||
|         let config; | ||||
|  | ||||
|         beforeEach(() => { | ||||
|           config = { | ||||
|             type: 'userActivity', | ||||
|             url: 'https//exmaple.com/endpoint', | ||||
|             options: { | ||||
|               petHatched: true, | ||||
|               mountRaised: true, | ||||
|               leveledUp: true, | ||||
|             }, | ||||
|           }; | ||||
|         }); | ||||
|  | ||||
|         it('it provides default values for options', () => { | ||||
|           delete config.options; | ||||
|  | ||||
|           let wh = new Webhook(config); | ||||
|  | ||||
|           wh.formatOptions(res); | ||||
|  | ||||
|           expect(wh.options).to.eql({ | ||||
|             petHatched: false, | ||||
|             mountRaised: false, | ||||
|             leveledUp: false, | ||||
|           }); | ||||
|         }); | ||||
|  | ||||
|         it('provides missing user options', () => { | ||||
|           delete config.options.petHatched; | ||||
|  | ||||
|           let wh = new Webhook(config); | ||||
|  | ||||
|           wh.formatOptions(res); | ||||
|  | ||||
|           expect(wh.options).to.eql({ | ||||
|             petHatched: false, | ||||
|             mountRaised: true, | ||||
|             leveledUp: true, | ||||
|           }); | ||||
|         }); | ||||
|  | ||||
|         it('discards additional options', () => { | ||||
|           config.options.foo = 'another option'; | ||||
|  | ||||
|           let wh = new Webhook(config); | ||||
|  | ||||
|           wh.formatOptions(res); | ||||
|  | ||||
|           expect(wh.options.foo).to.not.exist; | ||||
|           expect(wh.options).to.eql({ | ||||
|             petHatched: true, | ||||
|             mountRaised: true, | ||||
|             leveledUp: true, | ||||
|           }); | ||||
|         }); | ||||
|  | ||||
|         ['petHatched', 'petHatched', 'leveledUp'].forEach((option) => { | ||||
|           it(`validates that ${option} is a boolean`, (done) => { | ||||
|             config.options[option] = 'not a boolean'; | ||||
|  | ||||
|             try { | ||||
|               let wh = new Webhook(config); | ||||
|  | ||||
|               wh.formatOptions(res); | ||||
|             } catch (err) { | ||||
|               expect(err).to.be.an.instanceOf(BadRequest); | ||||
|               expect(res.t).to.be.calledOnce; | ||||
|               expect(res.t).to.be.calledWith('webhookBooleanOption', { option }); | ||||
|               done(); | ||||
|             } | ||||
|           }); | ||||
|         }); | ||||
|       }); | ||||
|  | ||||
|       context('type is questActivity', () => { | ||||
|         let config; | ||||
|  | ||||
|         beforeEach(() => { | ||||
|           config = { | ||||
|             type: 'questActivity', | ||||
|             url: 'https//exmaple.com/endpoint', | ||||
|             options: { | ||||
|               questStarted: true, | ||||
|               questFinished: true, | ||||
|             }, | ||||
|           }; | ||||
|         }); | ||||
|  | ||||
|         it('it provides default values for options', () => { | ||||
|           delete config.options; | ||||
|  | ||||
|           let wh = new Webhook(config); | ||||
|  | ||||
|           wh.formatOptions(res); | ||||
|  | ||||
|           expect(wh.options).to.eql({ | ||||
|             questStarted: false, | ||||
|             questFinished: false, | ||||
|           }); | ||||
|         }); | ||||
|  | ||||
|         it('provides missing user options', () => { | ||||
|           delete config.options.questStarted; | ||||
|  | ||||
|           let wh = new Webhook(config); | ||||
|  | ||||
|           wh.formatOptions(res); | ||||
|  | ||||
|           expect(wh.options).to.eql({ | ||||
|             questStarted: false, | ||||
|             questFinished: true, | ||||
|           }); | ||||
|         }); | ||||
|  | ||||
|         it('discards additional options', () => { | ||||
|           config.options.foo = 'another option'; | ||||
|  | ||||
|           let wh = new Webhook(config); | ||||
|  | ||||
|           wh.formatOptions(res); | ||||
|  | ||||
|           expect(wh.options.foo).to.not.exist; | ||||
|           expect(wh.options).to.eql({ | ||||
|             questStarted: true, | ||||
|             questFinished: true, | ||||
|           }); | ||||
|         }); | ||||
|  | ||||
|         ['questStarted', 'questFinished'].forEach((option) => { | ||||
|           it(`validates that ${option} is a boolean`, (done) => { | ||||
|             config.options[option] = 'not a boolean'; | ||||
|  | ||||
|             try { | ||||
|               let wh = new Webhook(config); | ||||
|  | ||||
|               wh.formatOptions(res); | ||||
|             } catch (err) { | ||||
|               expect(err).to.be.an.instanceOf(BadRequest); | ||||
|               expect(res.t).to.be.calledOnce; | ||||
|               expect(res.t).to.be.calledWith('webhookBooleanOption', { option }); | ||||
|               done(); | ||||
|             } | ||||
|           }); | ||||
|         }); | ||||
|       }); | ||||
|  | ||||
|       context('type is groupChatReceived', () => { | ||||
|         let config; | ||||
|  | ||||
|         beforeEach(() => { | ||||
|           config = { | ||||
|             type: 'groupChatReceived', | ||||
|             url: 'https//exmaple.com/endpoint', | ||||
|             options: { | ||||
|               groupId: generateUUID(), | ||||
|             }, | ||||
|           }; | ||||
|         }); | ||||
|  | ||||
|         it('creates options', () => { | ||||
|           let wh = new Webhook(config); | ||||
|  | ||||
|           wh.formatOptions(res); | ||||
|  | ||||
|           expect(wh.options).to.eql(config.options); | ||||
|         }); | ||||
|  | ||||
|         it('discards additional objects', () => { | ||||
|           config.options.foo = 'another thing'; | ||||
|  | ||||
|           let wh = new Webhook(config); | ||||
|  | ||||
|           wh.formatOptions(res); | ||||
|  | ||||
|           expect(wh.options.foo).to.not.exist; | ||||
|           expect(wh.options).to.eql({ | ||||
|             groupId: config.options.groupId, | ||||
|           }); | ||||
|         }); | ||||
|  | ||||
|         it('requires groupId option to be a uuid', (done) => { | ||||
|           config.options.groupId = 'not a uuid'; | ||||
|  | ||||
|           try { | ||||
|             let wh = new Webhook(config); | ||||
|  | ||||
|             wh.formatOptions(res); | ||||
|           } catch (err) { | ||||
|             expect(err).to.be.an.instanceOf(BadRequest); | ||||
|  | ||||
|             expect(err.message).to.eql(apiError('groupIdRequired')); | ||||
|             done(); | ||||
|           } | ||||
|         }); | ||||
|       }); | ||||
|  | ||||
|  | ||||
|       context('type is globalActivity', () => { | ||||
|         let config; | ||||
|  | ||||
|         beforeEach(() => { | ||||
|           config = { | ||||
|             type: 'globalActivity', | ||||
|             url: 'https//exmaple.com/endpoint', | ||||
|             options: { }, | ||||
|           }; | ||||
|         }); | ||||
|  | ||||
|         it('discards additional objects', () => { | ||||
|           config.options.foo = 'another thing'; | ||||
|  | ||||
|           let wh = new Webhook(config); | ||||
|  | ||||
|           wh.formatOptions(res); | ||||
|  | ||||
|           expect(wh.options.foo).to.not.exist; | ||||
|           expect(wh.options).to.eql({}); | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -5,7 +5,7 @@ import { | ||||
|   sleep, | ||||
|   checkExistence, | ||||
|   translate as t, | ||||
| } from '../../../../helpers/api-v3-integration.helper'; | ||||
| } from '../../../../helpers/api-integration/v3'; | ||||
| import { v4 as generateUUID } from 'uuid'; | ||||
|  | ||||
| describe('DELETE /challenges/:challengeId', () => { | ||||
| @@ -41,6 +41,7 @@ describe('DELETE /challenges/:challengeId', () => { | ||||
|       group = populatedGroup.group; | ||||
|  | ||||
|       challenge = await generateChallenge(groupLeader, group); | ||||
|       await groupLeader.post(`/challenges/${challenge._id}/join`); | ||||
|  | ||||
|       await groupLeader.post(`/tasks/challenge/${challenge._id}`, [ | ||||
|         {type: 'habit', text: taskText}, | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import { | ||||
|   createAndPopulateGroup, | ||||
|   generateChallenge, | ||||
|   translate as t, | ||||
| } from '../../../../helpers/api-v3-integration.helper'; | ||||
| } from '../../../../helpers/api-integration/v3'; | ||||
| import { v4 as generateUUID } from 'uuid'; | ||||
|  | ||||
| describe('GET /challenges/:challengeId', () => { | ||||
| @@ -33,9 +33,11 @@ describe('GET /challenges/:challengeId', () => { | ||||
|       group = populatedGroup.group; | ||||
|  | ||||
|       challenge = await generateChallenge(groupLeader, group); | ||||
|       await groupLeader.post(`/challenges/${challenge._id}/join`); | ||||
|     }); | ||||
|  | ||||
|     it('should return challenge data', async () => { | ||||
|       await challenge.sync(); | ||||
|       let chal = await user.get(`/challenges/${challenge._id}`); | ||||
|       expect(chal.memberCount).to.equal(challenge.memberCount); | ||||
|       expect(chal.name).to.equal(challenge.name); | ||||
| @@ -45,6 +47,14 @@ describe('GET /challenges/:challengeId', () => { | ||||
|         _id: groupLeader._id, | ||||
|         id: groupLeader._id, | ||||
|         profile: {name: groupLeader.profile.name}, | ||||
|         auth: { | ||||
|           local: { | ||||
|             username: groupLeader.auth.local.username, | ||||
|           }, | ||||
|         }, | ||||
|         flags: { | ||||
|           verifiedUsername: true, | ||||
|         }, | ||||
|       }); | ||||
|       expect(chal.group).to.eql({ | ||||
|         _id: group._id, | ||||
| @@ -61,44 +71,56 @@ describe('GET /challenges/:challengeId', () => { | ||||
|  | ||||
|   context('private guild', () => { | ||||
|     let groupLeader; | ||||
|     let challengeLeader; | ||||
|     let group; | ||||
|     let challenge; | ||||
|     let members; | ||||
|     let user; | ||||
|     let nonMember; | ||||
|     let otherMember; | ||||
|  | ||||
|     beforeEach(async () => { | ||||
|       user = await generateUser(); | ||||
|       nonMember = await generateUser(); | ||||
|  | ||||
|       let populatedGroup = await createAndPopulateGroup({ | ||||
|         groupDetails: {type: 'guild', privacy: 'private'}, | ||||
|         members: 1, | ||||
|         members: 2, | ||||
|       }); | ||||
|  | ||||
|       groupLeader = populatedGroup.groupLeader; | ||||
|       group = populatedGroup.group; | ||||
|       members = populatedGroup.members; | ||||
|  | ||||
|       challenge = await generateChallenge(groupLeader, group); | ||||
|       await members[0].post(`/challenges/${challenge._id}/join`); | ||||
|       challengeLeader = members[0]; | ||||
|       otherMember = members[1]; | ||||
|  | ||||
|       challenge = await generateChallenge(challengeLeader, group); | ||||
|     }); | ||||
|  | ||||
|     it('fails if user doesn\'t have access to the challenge', async () => { | ||||
|       await expect(user.get(`/challenges/${challenge._id}`)).to.eventually.be.rejected.and.eql({ | ||||
|     it('fails if user isn\'t in the guild and isn\'t challenge leader', async () => { | ||||
|       await expect(nonMember.get(`/challenges/${challenge._id}`)).to.eventually.be.rejected.and.eql({ | ||||
|         code: 404, | ||||
|         error: 'NotFound', | ||||
|         message: t('challengeNotFound'), | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('should return challenge data', async () => { | ||||
|       let chal = await members[0].get(`/challenges/${challenge._id}`); | ||||
|     it('returns challenge data for any user in the guild', async () => { | ||||
|       let chal = await otherMember.get(`/challenges/${challenge._id}`); | ||||
|       expect(chal.name).to.equal(challenge.name); | ||||
|       expect(chal._id).to.equal(challenge._id); | ||||
|  | ||||
|       expect(chal.leader).to.eql({ | ||||
|         _id: groupLeader._id, | ||||
|         id: groupLeader._id, | ||||
|         profile: {name: groupLeader.profile.name}, | ||||
|         _id: challengeLeader._id, | ||||
|         id: challengeLeader._id, | ||||
|         profile: {name: challengeLeader.profile.name}, | ||||
|         auth: { | ||||
|           local: { | ||||
|             username: challengeLeader.auth.local.username, | ||||
|           }, | ||||
|         }, | ||||
|         flags: { | ||||
|           verifiedUsername: true, | ||||
|         }, | ||||
|       }); | ||||
|       expect(chal.group).to.eql({ | ||||
|         _id: group._id, | ||||
| @@ -111,52 +133,88 @@ describe('GET /challenges/:challengeId', () => { | ||||
|         leader: groupLeader.id, | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('returns challenge data if challenge leader isn\'t in the guild or challenge', async () => { | ||||
|       await challengeLeader.post(`/groups/${group._id}/leave`); | ||||
|       await challengeLeader.sync(); | ||||
|       expect(challengeLeader.guilds).to.be.empty; // check that leaving worked | ||||
|  | ||||
|       let chal = await challengeLeader.get(`/challenges/${challenge._id}`); | ||||
|       expect(chal.name).to.equal(challenge.name); | ||||
|       expect(chal._id).to.equal(challenge._id); | ||||
|  | ||||
|       expect(chal.leader).to.eql({ | ||||
|         _id: challengeLeader._id, | ||||
|         id: challengeLeader._id, | ||||
|         profile: {name: challengeLeader.profile.name}, | ||||
|         auth: { | ||||
|           local: { | ||||
|             username: challengeLeader.auth.local.username, | ||||
|           }, | ||||
|         }, | ||||
|         flags: { | ||||
|           verifiedUsername: true, | ||||
|         }, | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   context('party', () => { | ||||
|     let groupLeader; | ||||
|     let challengeLeader; | ||||
|     let group; | ||||
|     let challenge; | ||||
|     let members; | ||||
|     let user; | ||||
|     let nonMember; | ||||
|     let otherMember; | ||||
|  | ||||
|     beforeEach(async () => { | ||||
|       user = await generateUser(); | ||||
|       nonMember = await generateUser(); | ||||
|  | ||||
|       let populatedGroup = await createAndPopulateGroup({ | ||||
|         groupDetails: {type: 'party'}, | ||||
|         members: 1, | ||||
|         groupDetails: {type: 'party', privacy: 'private'}, | ||||
|         members: 2, | ||||
|       }); | ||||
|  | ||||
|       groupLeader = populatedGroup.groupLeader; | ||||
|       group = populatedGroup.group; | ||||
|       members = populatedGroup.members; | ||||
|  | ||||
|       challenge = await generateChallenge(groupLeader, group); | ||||
|       await members[0].post(`/challenges/${challenge._id}/join`); | ||||
|       challengeLeader = members[0]; | ||||
|       otherMember = members[1]; | ||||
|  | ||||
|       challenge = await generateChallenge(challengeLeader, group); | ||||
|     }); | ||||
|  | ||||
|     it('fails if user doesn\'t have access to the challenge', async () => { | ||||
|       await expect(user.get(`/challenges/${challenge._id}`)).to.eventually.be.rejected.and.eql({ | ||||
|     it('fails if user isn\'t in the party and isn\'t challenge leader', async () => { | ||||
|       await expect(nonMember.get(`/challenges/${challenge._id}`)).to.eventually.be.rejected.and.eql({ | ||||
|         code: 404, | ||||
|         error: 'NotFound', | ||||
|         message: t('challengeNotFound'), | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('should return challenge data', async () => { | ||||
|       let chal = await members[0].get(`/challenges/${challenge._id}`); | ||||
|     it('returns challenge data for any user in the party', async () => { | ||||
|       let chal = await otherMember.get(`/challenges/${challenge._id}`); | ||||
|       expect(chal.name).to.equal(challenge.name); | ||||
|       expect(chal._id).to.equal(challenge._id); | ||||
|  | ||||
|       expect(chal.leader).to.eql({ | ||||
|         _id: groupLeader._id, | ||||
|         id: groupLeader.id, | ||||
|         profile: {name: groupLeader.profile.name}, | ||||
|         _id: challengeLeader._id, | ||||
|         id: challengeLeader._id, | ||||
|         profile: {name: challengeLeader.profile.name}, | ||||
|         auth: { | ||||
|           local: { | ||||
|             username: challengeLeader.auth.local.username, | ||||
|           }, | ||||
|         }, | ||||
|         flags: { | ||||
|           verifiedUsername: true, | ||||
|         }, | ||||
|       }); | ||||
|       expect(chal.group).to.eql({ | ||||
|         _id: group._id, | ||||
|         id: group.id, | ||||
|         id: group._id, | ||||
|         categories: [], | ||||
|         name: group.name, | ||||
|         summary: group.name, | ||||
| @@ -165,5 +223,29 @@ describe('GET /challenges/:challengeId', () => { | ||||
|         leader: groupLeader.id, | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('returns challenge data if challenge leader isn\'t in the party or challenge', async () => { | ||||
|       await challengeLeader.post('/groups/party/leave'); | ||||
|       await challengeLeader.sync(); | ||||
|       expect(challengeLeader.party._id).to.be.undefined; // check that leaving worked | ||||
|  | ||||
|       let chal = await challengeLeader.get(`/challenges/${challenge._id}`); | ||||
|       expect(chal.name).to.equal(challenge.name); | ||||
|       expect(chal._id).to.equal(challenge._id); | ||||
|  | ||||
|       expect(chal.leader).to.eql({ | ||||
|         _id: challengeLeader._id, | ||||
|         id: challengeLeader._id, | ||||
|         profile: {name: challengeLeader.profile.name}, | ||||
|         auth: { | ||||
|           local: { | ||||
|             username: challengeLeader.auth.local.username, | ||||
|           }, | ||||
|         }, | ||||
|         flags: { | ||||
|           verifiedUsername: true, | ||||
|         }, | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user