mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-14 21:27:23 +01:00
Compare commits
552 Commits
v5.24.2
...
phillip/co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
192d649ffa | ||
|
|
e7aae55eca | ||
|
|
36b03613e1 | ||
|
|
2de9a16a2c | ||
|
|
895241b7fa | ||
|
|
2535fd7095 | ||
|
|
30f1820a49 | ||
|
|
3bb6c391af | ||
|
|
a0383c785a | ||
|
|
99790c05f4 | ||
|
|
fc5fec9bfe | ||
|
|
9db5d4116d | ||
|
|
6676e94ef6 | ||
|
|
723adceb25 | ||
|
|
440d06da4a | ||
|
|
0ea84668a8 | ||
|
|
5893d8b9bb | ||
|
|
2c799b9c07 | ||
|
|
1550d9b4ee | ||
|
|
ade812b86d | ||
|
|
62e6fbef61 | ||
|
|
67a0f8b65a | ||
|
|
aa432022d3 | ||
|
|
86fb3c1fd1 | ||
|
|
ff2b4add8b | ||
|
|
4ba73dfbec | ||
|
|
e675ea9bd1 | ||
|
|
9c27d86ced | ||
|
|
58ee81adfc | ||
|
|
32c9904a6e | ||
|
|
b86e0a1549 | ||
|
|
154ac9bb38 | ||
|
|
a97060445a | ||
|
|
26b59de1de | ||
|
|
21c8b00ef6 | ||
|
|
c25b7293bb | ||
|
|
15e078cb34 | ||
|
|
f7bb17202b | ||
|
|
213b7696c5 | ||
|
|
fe5c95316b | ||
|
|
54617f8583 | ||
|
|
75c9731ca4 | ||
|
|
31afc45744 | ||
|
|
f6466b161b | ||
|
|
a36114e904 | ||
|
|
529f856ab9 | ||
|
|
9077e66973 | ||
|
|
a47a96b70d | ||
|
|
8a94e88786 | ||
|
|
b3aa236d3d | ||
|
|
4dd58ad89e | ||
|
|
317f7ab598 | ||
|
|
d6c47e7e81 | ||
|
|
1ed61a3d3d | ||
|
|
5c734cfa00 | ||
|
|
07f485a654 | ||
|
|
ae76271469 | ||
|
|
c8a8ecbe1f | ||
|
|
fbf69a4a34 | ||
|
|
7f38ffe676 | ||
|
|
a0e0c392e9 | ||
|
|
573e472077 | ||
|
|
955d22278d | ||
|
|
171ee93108 | ||
|
|
5fb0560f0b | ||
|
|
88b616e206 | ||
|
|
08829425cb | ||
|
|
1dbd2bf0dc | ||
|
|
157f98b331 | ||
|
|
3d689837d6 | ||
|
|
2b76bbe0db | ||
|
|
e75db79b50 | ||
|
|
60919671ea | ||
|
|
bca21c1cf0 | ||
|
|
f1993db0fa | ||
|
|
7351c16578 | ||
|
|
5bc8f5dd64 | ||
|
|
20517cd0b2 | ||
|
|
9a4081c54b | ||
|
|
97e0b31a3d | ||
|
|
af17930314 | ||
|
|
094b19f289 | ||
|
|
8e54cef68b | ||
|
|
1df8d5832f | ||
|
|
0542008b7f | ||
|
|
ffa89202e6 | ||
|
|
1203cbbad8 | ||
|
|
f9fb463128 | ||
|
|
ea398f6294 | ||
|
|
5f41042826 | ||
|
|
486b7d4da1 | ||
|
|
91b47e56ff | ||
|
|
9934e59629 | ||
|
|
50cc66d51c | ||
|
|
936c9dc4f3 | ||
|
|
946ade5da1 | ||
|
|
80068a3674 | ||
|
|
d7c9a7874b | ||
|
|
768e5b3f5b | ||
|
|
f3320d9ae3 | ||
|
|
d4538b0909 | ||
|
|
676ee74f19 | ||
|
|
9059f227fa | ||
|
|
6a14d0f3f3 | ||
|
|
3e5c623125 | ||
|
|
e559fb7e4b | ||
|
|
88a1cfb689 | ||
|
|
f12c4e75e6 | ||
|
|
90f08c58cd | ||
|
|
f6aa96c64c | ||
|
|
2b04a1b50c | ||
|
|
7297fb5241 | ||
|
|
98c5a68a8c | ||
|
|
8e643747f8 | ||
|
|
2483e19bee | ||
|
|
f9d3c6ed48 | ||
|
|
09a0e75351 | ||
|
|
644edc5b76 | ||
|
|
a64b994376 | ||
|
|
fb626ebf7e | ||
|
|
dd334f487e | ||
|
|
cd5c86fb69 | ||
|
|
7878761b6f | ||
|
|
d3b63abdd3 | ||
|
|
23fad37205 | ||
|
|
88558e6b98 | ||
|
|
a84ee8497b | ||
|
|
d560ee2da1 | ||
|
|
fd3fce110e | ||
|
|
1bce2b0e28 | ||
|
|
06a59bfe03 | ||
|
|
83a430afad | ||
|
|
949f638b6e | ||
|
|
2b2193e9ce | ||
|
|
0709bada87 | ||
|
|
506586b74c | ||
|
|
99b2ee273f | ||
|
|
aa6e536851 | ||
|
|
2a2c1af7ba | ||
|
|
48e381d702 | ||
|
|
9aafd76746 | ||
|
|
0069af78a3 | ||
|
|
c25fe7eb3d | ||
|
|
b9a9013685 | ||
|
|
54d075e4fd | ||
|
|
1c40044525 | ||
|
|
5784694dc9 | ||
|
|
7af4a6ff11 | ||
|
|
a601be0666 | ||
|
|
1be169a105 | ||
|
|
6b02af69f2 | ||
|
|
1fe4bd2de7 | ||
|
|
afd00a8ab6 | ||
|
|
63918b3c20 | ||
|
|
6293a4b936 | ||
|
|
44502092ad | ||
|
|
ce0e8284fe | ||
|
|
15f104ddd0 | ||
|
|
7f6ae8ffbf | ||
|
|
b2ecfb5a32 | ||
|
|
fa6ba8b668 | ||
|
|
826dffc794 | ||
|
|
688190ac4a | ||
|
|
4909a3b537 | ||
|
|
64e2150f44 | ||
|
|
3f7abc459c | ||
|
|
618cdafd10 | ||
|
|
e7b37d0378 | ||
|
|
3f3e2525d2 | ||
|
|
765e08f999 | ||
|
|
598bc29647 | ||
|
|
39ccddfb1c | ||
|
|
108214a217 | ||
|
|
271f40e355 | ||
|
|
e801547580 | ||
|
|
fc11941186 | ||
|
|
882fad3113 | ||
|
|
6168492711 | ||
|
|
2aade9aaa6 | ||
|
|
4789946c4e | ||
|
|
a69d8877c9 | ||
|
|
7cc0c3bc57 | ||
|
|
fb78495a1b | ||
|
|
22def5111f | ||
|
|
6e91d51def | ||
|
|
f3b8a4e931 | ||
|
|
69afa52beb | ||
|
|
f09a39d27c | ||
|
|
2e71963fbf | ||
|
|
f740a92fb7 | ||
|
|
21e7ddea16 | ||
|
|
923d90cf22 | ||
|
|
b386a1917d | ||
|
|
4a5427b2b2 | ||
|
|
04554c5309 | ||
|
|
5ef88b5c56 | ||
|
|
1f2397b81a | ||
|
|
892c4934d5 | ||
|
|
60d5aaaaa6 | ||
|
|
f506b840ed | ||
|
|
f357750d88 | ||
|
|
a2dbe68338 | ||
|
|
491d2cfab1 | ||
|
|
fa91abb739 | ||
|
|
6b46d04537 | ||
|
|
b90457c04f | ||
|
|
379d98a91e | ||
|
|
07352480cd | ||
|
|
a6ff8e095a | ||
|
|
1fb44bbe73 | ||
|
|
5323849f90 | ||
|
|
034327f647 | ||
|
|
de9aac0988 | ||
|
|
f55d836398 | ||
|
|
287014518d | ||
|
|
b46e2da61b | ||
|
|
ef47d6cf0b | ||
|
|
1f5d66cd58 | ||
|
|
a88602a21f | ||
|
|
ffd2b4b76f | ||
|
|
c50ed843fb | ||
|
|
760c05df5d | ||
|
|
26d070f2c3 | ||
|
|
850ae5114f | ||
|
|
bc9577439e | ||
|
|
10cd596f0b | ||
|
|
d180062ad2 | ||
|
|
bfacf4b36e | ||
|
|
2912f31dec | ||
|
|
c47b287a89 | ||
|
|
3aa626d2ae | ||
|
|
647ee2a073 | ||
|
|
2080c3f7b8 | ||
|
|
6f65c72921 | ||
|
|
22bbdd6a28 | ||
|
|
1a3d6f6520 | ||
|
|
aa1e78ac94 | ||
|
|
858caa4582 | ||
|
|
a7e1091f3f | ||
|
|
78fca804b7 | ||
|
|
a919ef99fe | ||
|
|
de9ca06607 | ||
|
|
b5c5990e56 | ||
|
|
756af8aafb | ||
|
|
54f84af274 | ||
|
|
c151b6e1bc | ||
|
|
effd729222 | ||
|
|
b7cdbc5c94 | ||
|
|
28193f86fb | ||
|
|
877fe48225 | ||
|
|
e0f6f79c5b | ||
|
|
f62254d68e | ||
|
|
d054e6fc16 | ||
|
|
7231f699c1 | ||
|
|
1dae0793fd | ||
|
|
f18fbe86b6 | ||
|
|
61a61724ca | ||
|
|
93cf30eb18 | ||
|
|
379f41ff04 | ||
|
|
cff08adcd0 | ||
|
|
71936c1f0a | ||
|
|
4da2ed4a1f | ||
|
|
11c5b26c59 | ||
|
|
19c79ce510 | ||
|
|
0ba14c18b1 | ||
|
|
ac85bb2e2d | ||
|
|
74dfb2710f | ||
|
|
d2a0ab684a | ||
|
|
12d38fa813 | ||
|
|
8dbd3c3db1 | ||
|
|
74b3b348ff | ||
|
|
3386d61fde | ||
|
|
db41e00990 | ||
|
|
5d5275ce70 | ||
|
|
e39c63700e | ||
|
|
550ac2db9d | ||
|
|
19da14531c | ||
|
|
254dd80f24 | ||
|
|
0bc3f16b4b | ||
|
|
5184973bd5 | ||
|
|
b6accca5ca | ||
|
|
fec68e6211 | ||
|
|
fc63c906dd | ||
|
|
3333f8f0f5 | ||
|
|
89a3ac3dde | ||
|
|
16551ec83f | ||
|
|
2645bf6023 | ||
|
|
f5f4974a73 | ||
|
|
162e337d14 | ||
|
|
21a7d36b7b | ||
|
|
f2506c3231 | ||
|
|
d47641e25a | ||
|
|
983e01cb3f | ||
|
|
a55ede9175 | ||
|
|
fb8479ad1e | ||
|
|
28491cb01d | ||
|
|
b49dddeb47 | ||
|
|
31e501f65a | ||
|
|
3810cf3ef3 | ||
|
|
050c227e6f | ||
|
|
ddb8725052 | ||
|
|
e11b9ebe26 | ||
|
|
4da53f83c9 | ||
|
|
d05da3722c | ||
|
|
b8a3440ef2 | ||
|
|
44d63032d8 | ||
|
|
9d7da91ec6 | ||
|
|
dde675fdc1 | ||
|
|
d34502bba2 | ||
|
|
309954eb44 | ||
|
|
98f9d2a8f4 | ||
|
|
5719e5e996 | ||
|
|
39add61618 | ||
|
|
1c1543f012 | ||
|
|
10a27354bb | ||
|
|
a00f199d18 | ||
|
|
6c5bff7843 | ||
|
|
388c3d38ed | ||
|
|
485584c144 | ||
|
|
b83f62bd82 | ||
|
|
758b6138c2 | ||
|
|
37f08c4534 | ||
|
|
544d67e7e5 | ||
|
|
1f0a4dad23 | ||
|
|
eb3220c96b | ||
|
|
d4ba96796c | ||
|
|
ebdac0b388 | ||
|
|
6d13a257dd | ||
|
|
c2b370f4d3 | ||
|
|
3313584d60 | ||
|
|
b76585cce3 | ||
|
|
8d9af82521 | ||
|
|
dcf25c0b4a | ||
|
|
31036ad9e4 | ||
|
|
1ba85b403f | ||
|
|
c26f410cc3 | ||
|
|
242e64cedc | ||
|
|
a44418c4fa | ||
|
|
ea66e4e4a1 | ||
|
|
6889b65123 | ||
|
|
fd5bc8f0b9 | ||
|
|
f33aff577c | ||
|
|
9ba986f5e5 | ||
|
|
bf46c798a6 | ||
|
|
54b1afd5b4 | ||
|
|
3cd966bc03 | ||
|
|
28531f3e2a | ||
|
|
7ecaf098cd | ||
|
|
ef1912d571 | ||
|
|
cd685c1b2b | ||
|
|
b501e06f27 | ||
|
|
2062c68877 | ||
|
|
b2dde8a977 | ||
|
|
19521b1894 | ||
|
|
688e7181f0 | ||
|
|
a53a9be4b7 | ||
|
|
66c56225a4 | ||
|
|
cabc08c04b | ||
|
|
9ae6063f78 | ||
|
|
7936677fd8 | ||
|
|
88a0b57335 | ||
|
|
2303d5de32 | ||
|
|
1f5de1ab42 | ||
|
|
435047cace | ||
|
|
c0d6338eba | ||
|
|
36b589e92d | ||
|
|
42cafbeaab | ||
|
|
5b5d5a39a4 | ||
|
|
8d479e358d | ||
|
|
3bb1cceed1 | ||
|
|
0756d36fb3 | ||
|
|
c709adeaec | ||
|
|
c5dcd089a6 | ||
|
|
57deadaa5c | ||
|
|
becdf640b5 | ||
|
|
756e99c089 | ||
|
|
709a14fd51 | ||
|
|
33181c0ac4 | ||
|
|
ee974dfa19 | ||
|
|
b697598d75 | ||
|
|
6e5b13668a | ||
|
|
4c13f3193e | ||
|
|
15cea33c4b | ||
|
|
cac0a84763 | ||
|
|
c56c07a0f8 | ||
|
|
929778bdad | ||
|
|
d6dba9767d | ||
|
|
7e7ce44c77 | ||
|
|
4d38880249 | ||
|
|
46d164ddd1 | ||
|
|
31e4c51c3f | ||
|
|
eebfb81bd2 | ||
|
|
06623991b3 | ||
|
|
fe697898ee | ||
|
|
f3fc14bd53 | ||
|
|
7f0b0a3909 | ||
|
|
0f395bcc3e | ||
|
|
d366b2cde1 | ||
|
|
7d8611bae2 | ||
|
|
17964c0ab7 | ||
|
|
5b6cc23fb7 | ||
|
|
efe8cff1ad | ||
|
|
6591f6780c | ||
|
|
592c320d1d | ||
|
|
df641d0866 | ||
|
|
da4606df5e | ||
|
|
ceb6b93dc1 | ||
|
|
e006f3f7de | ||
|
|
246cc25b6d | ||
|
|
a1bb61793b | ||
|
|
6523ed08cd | ||
|
|
ab34257c03 | ||
|
|
6afdffae92 | ||
|
|
c3b17e3db0 | ||
|
|
b9e128b387 | ||
|
|
cac14ab2cc | ||
|
|
c64a6eb66e | ||
|
|
cd33a539cf | ||
|
|
ec76757f93 | ||
|
|
03adff80f7 | ||
|
|
a73abcca74 | ||
|
|
a8c8fffa7c | ||
|
|
f835cf2761 | ||
|
|
96fed21fbd | ||
|
|
7d62c87de3 | ||
|
|
b28251dc9e | ||
|
|
b713e10c14 | ||
|
|
fce5371fce | ||
|
|
a9cefd284a | ||
|
|
6ed422cd28 | ||
|
|
02914685dc | ||
|
|
856ed24dcb | ||
|
|
4a9ec734c1 | ||
|
|
61d151d2bb | ||
|
|
32cb201b81 | ||
|
|
44a7006295 | ||
|
|
21a0bf7d65 | ||
|
|
0089506165 | ||
|
|
fbce5aae32 | ||
|
|
a8726eee0b | ||
|
|
9efe370d33 | ||
|
|
47df62e716 | ||
|
|
dac792dd27 | ||
|
|
eacf6de19a | ||
|
|
c1ca4e84b8 | ||
|
|
87fc01cb81 | ||
|
|
71f21c643c | ||
|
|
1ced4a18d6 | ||
|
|
9dabe79d5e | ||
|
|
4c2cdfe5b8 | ||
|
|
f05888b116 | ||
|
|
ae8607c0c3 | ||
|
|
d6cabeedb4 | ||
|
|
99a7b90247 | ||
|
|
fbdaa50fcf | ||
|
|
30e81297da | ||
|
|
b373eaff39 | ||
|
|
80a212683d | ||
|
|
b0a4ed30d4 | ||
|
|
c2cb37ffe6 | ||
|
|
558894fafd | ||
|
|
4bbdf27f48 | ||
|
|
daa296f2af | ||
|
|
4c51212315 | ||
|
|
372763c57c | ||
|
|
3bf323032c | ||
|
|
7a50b2d2ff | ||
|
|
8df326bf92 | ||
|
|
2d6555fe0f | ||
|
|
b0d6f7722b | ||
|
|
aedbe7d333 | ||
|
|
6079dd4af6 | ||
|
|
5c448188cf | ||
|
|
d7dc878b1c | ||
|
|
7baec4e48e | ||
|
|
5cd58d4119 | ||
|
|
4cdfefd92b | ||
|
|
28b936e2d1 | ||
|
|
e4ec7e3e1e | ||
|
|
3c7ecef6a8 | ||
|
|
e2a5a1ab39 | ||
|
|
5f64b2fb25 | ||
|
|
f2a2d4cde5 | ||
|
|
edc3c58876 | ||
|
|
4277c08324 | ||
|
|
424f29a82b | ||
|
|
e52c7ff9ce | ||
|
|
fdc709d1c2 | ||
|
|
ea750571a0 | ||
|
|
f8fbea4654 | ||
|
|
e0f4a4ecb8 | ||
|
|
5ce12d97be | ||
|
|
4fdd064cd6 | ||
|
|
37731b236a | ||
|
|
b06c708480 | ||
|
|
f8c452ae3f | ||
|
|
9befbec2b0 | ||
|
|
7b46f3bc23 | ||
|
|
64a500987c | ||
|
|
92eaece5eb | ||
|
|
ee73d5b628 | ||
|
|
a7eda1355b | ||
|
|
8b9b79db8e | ||
|
|
69c0488335 | ||
|
|
c18e06f071 | ||
|
|
a50c0eb1e7 | ||
|
|
fb56f7df20 | ||
|
|
3540a274b3 | ||
|
|
fe2c02679e | ||
|
|
bca3e96e9c | ||
|
|
041edb3042 | ||
|
|
6e96085f99 | ||
|
|
249394b4ad | ||
|
|
2a84561e00 | ||
|
|
962456204e | ||
|
|
593524905e | ||
|
|
1b12e9d8b7 | ||
|
|
127f105934 | ||
|
|
2dfe5585eb | ||
|
|
93011f182f | ||
|
|
d11e95ab26 | ||
|
|
f99ddbe60f | ||
|
|
982069df36 | ||
|
|
db4bec37e3 | ||
|
|
736ef16430 | ||
|
|
129cb7627c | ||
|
|
f223b5dd2a | ||
|
|
b3521be629 | ||
|
|
17db6a1772 | ||
|
|
278d9b74f9 | ||
|
|
ce796fa1d9 | ||
|
|
ec0275e6f6 | ||
|
|
39252c7828 | ||
|
|
75a88ab25a | ||
|
|
70434b17cc | ||
|
|
a921a8bc61 | ||
|
|
0aa9d4d1d5 | ||
|
|
0ead06937b | ||
|
|
037fb6737d | ||
|
|
4e0d8cba51 | ||
|
|
ecc8a65d28 | ||
|
|
28fef8df86 | ||
|
|
33b54a734e | ||
|
|
1f8aa7d778 | ||
|
|
09ff3ee865 | ||
|
|
cbfeb18517 | ||
|
|
63e7ace693 | ||
|
|
0f9cf48b55 | ||
|
|
5a48436eff | ||
|
|
8b3267458b | ||
|
|
bccfaab350 | ||
|
|
753f12979e |
30
.github/workflows/test.yml
vendored
30
.github/workflows/test.yml
vendored
@@ -19,7 +19,8 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: sudo apt-get -y install libkrb5-dev
|
||||
- run: sudo apt update
|
||||
- run: sudo apt -y install libkrb5-dev
|
||||
- run: cp config.json.example config.json
|
||||
- name: npm install
|
||||
run: |
|
||||
@@ -41,7 +42,8 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: sudo apt-get -y install libkrb5-dev
|
||||
- run: sudo apt update
|
||||
- run: sudo apt -y install libkrb5-dev
|
||||
- run: cp config.json.example config.json
|
||||
- name: npm install
|
||||
run: |
|
||||
@@ -63,7 +65,8 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: sudo apt-get -y install libkrb5-dev
|
||||
- run: sudo apt update
|
||||
- run: sudo apt -y install libkrb5-dev
|
||||
- run: cp config.json.example config.json
|
||||
- name: npm install
|
||||
run: |
|
||||
@@ -86,7 +89,8 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: sudo apt-get -y install libkrb5-dev
|
||||
- run: sudo apt update
|
||||
- run: sudo apt -y install libkrb5-dev
|
||||
- run: cp config.json.example config.json
|
||||
- name: npm install
|
||||
run: |
|
||||
@@ -108,7 +112,8 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: sudo apt-get -y install libkrb5-dev
|
||||
- run: sudo apt update
|
||||
- run: sudo apt -y install libkrb5-dev
|
||||
- run: cp config.json.example config.json
|
||||
- name: npm install
|
||||
run: |
|
||||
@@ -137,7 +142,8 @@ jobs:
|
||||
with:
|
||||
mongodb-version: ${{ matrix.mongodb-version }}
|
||||
mongodb-replica-set: rs
|
||||
- run: sudo apt-get -y install libkrb5-dev
|
||||
- run: sudo apt update
|
||||
- run: sudo apt -y install libkrb5-dev
|
||||
- run: cp config.json.example config.json
|
||||
- name: npm install
|
||||
run: |
|
||||
@@ -167,7 +173,8 @@ jobs:
|
||||
with:
|
||||
mongodb-version: ${{ matrix.mongodb-version }}
|
||||
mongodb-replica-set: rs
|
||||
- run: sudo apt-get -y install libkrb5-dev
|
||||
- run: sudo apt update
|
||||
- run: sudo apt -y install libkrb5-dev
|
||||
- run: cp config.json.example config.json
|
||||
- name: npm install
|
||||
run: |
|
||||
@@ -197,7 +204,8 @@ jobs:
|
||||
with:
|
||||
mongodb-version: ${{ matrix.mongodb-version }}
|
||||
mongodb-replica-set: rs
|
||||
- run: sudo apt-get -y install libkrb5-dev
|
||||
- run: sudo apt update
|
||||
- run: sudo apt -y install libkrb5-dev
|
||||
- run: cp config.json.example config.json
|
||||
- name: npm install
|
||||
run: |
|
||||
@@ -222,7 +230,8 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: sudo apt-get -y install libkrb5-dev
|
||||
- run: sudo apt update
|
||||
- run: sudo apt -y install libkrb5-dev
|
||||
- run: cp config.json.example config.json
|
||||
- name: npm install
|
||||
run: |
|
||||
@@ -246,7 +255,8 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: sudo apt-get -y install libkrb5-dev
|
||||
- run: sudo apt update
|
||||
- run: sudo apt -y install libkrb5-dev
|
||||
- run: cp config.json.example config.json
|
||||
- name: npm install
|
||||
run: |
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -8,7 +8,7 @@ i18n_cache
|
||||
apidoc/html
|
||||
*.swp
|
||||
.idea*
|
||||
config.json
|
||||
config*.json
|
||||
npm-debug.log*
|
||||
lib
|
||||
newrelic_agent.log
|
||||
@@ -47,4 +47,5 @@ webpack.webstorm.config
|
||||
|
||||
# mongodb replica set for local dev
|
||||
mongodb-*.tgz
|
||||
/mongodb-data
|
||||
/mongodb-data*
|
||||
/.nyc_output
|
||||
|
||||
25
.heroku/report_deploy.sh
Executable file
25
.heroku/report_deploy.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
DEVELOPER="someone"
|
||||
if git rev-parse --git-dir > /dev/null 2>&1; then
|
||||
DEVELOPERS=$(git log -5 --pretty=format:'%an')
|
||||
IFS=$'\n'
|
||||
DEVELOPER=""
|
||||
for dev in $DEVELOPERS
|
||||
do
|
||||
if [ "$DEVELOPER" == "someone" ]; then
|
||||
if [[ ${dev} != *"[bot]"* ]]; then
|
||||
DEVELOPER=$dev
|
||||
continue
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
PARTS=$(cut -d"." -f1 <<< $BASE_URL)
|
||||
SERVER_NAME=$(cut -d"/" -f3 <<< ${PARTS[0]})
|
||||
|
||||
SERVER_NAME=":$SERVER_EMOJI: $SERVER_NAME"
|
||||
|
||||
wget $SLACK_DEPLOY_URL --post-data="{\"server_name\": \"$SERVER_NAME\", \"developer\": \"$DEVELOPER\", \"base_url\": \"$BASE_URL\"}" -O /dev/null
|
||||
@@ -3,12 +3,13 @@ FROM node:20
|
||||
# Install global packages
|
||||
RUN npm install -g gulp-cli mocha
|
||||
|
||||
# Copy package.json and package-lock.json into image, then install
|
||||
# dependencies.
|
||||
# Copy package.json and package-lock.json into image
|
||||
WORKDIR /usr/src/habitica
|
||||
COPY ["package.json", "package-lock.json", "./"]
|
||||
RUN npm install
|
||||
|
||||
# Copy the remaining source files in.
|
||||
COPY . /usr/src/habitica
|
||||
# Install dependencies
|
||||
RUN npm install
|
||||
RUN npm run postinstall
|
||||
RUN npm run client:build
|
||||
RUN gulp build:prod
|
||||
|
||||
18
README.md
18
README.md
@@ -1,14 +1,20 @@
|
||||
Habitica  [](https://codeclimate.com/github/HabitRPG/habitrpg) [](https://www.bountysource.com/trackers/68393-habitrpg?utm_source=68393&utm_medium=shield&utm_campaign=TRACKER_BADGE)
|
||||
Habitica 
|
||||
===============
|
||||
|
||||
[Habitica](https://habitica.com) is an open-source habit-building program that treats your life like a role-playing game. Level up as you succeed, lose HP as you fail, and earn money to buy weapons and armor.
|
||||
[Habitica](https://habitica.com) is an open-source habit-building program that treats your life like a role-playing game. Level up as you succeed, lose HP as you fail, and earn Gold to buy weapons and armor!
|
||||
|
||||
**We need more programmers!** Your assistance will be greatly appreciated. The wiki pages below and the additional pages they link to will tell you how to get started on contributing code and where you can go to seek further help or ask questions:
|
||||
**Want to contribute code to Habitica?** We're always looking for assistance on any issues in our repo with the "Help Wanted" label. The wiki pages below and the additional linked pages will tell you how to start contributing code and where you can seek further help or ask questions:
|
||||
* [Guidance for Blacksmiths](https://habitica.fandom.com/wiki/Guidance_for_Blacksmiths) - an introduction to the technologies used and how the software is organized.
|
||||
* [Setting up Habitica Locally](https://habitica.fandom.com/wiki/Setting_up_Habitica_Locally) - how to set up a local install of Habitica for development and testing on various platforms.
|
||||
* [Setting up Habitica Locally](https://github.com/HabitRPG/habitica/wiki/Setting-Up-Habitica-for-Local-Development) - how to set up a local install of Habitica for development and testing.
|
||||
|
||||
**Interested in contributing to Habitica’s mobile apps?** Visit the links below for our mobile repositories.
|
||||
* **Android:** https://github.com/HabitRPG/habitica-android
|
||||
* **iOS:** https://github.com/HabitRPG/habitica-ios
|
||||
|
||||
Habitica's code is licensed as described at https://github.com/HabitRPG/habitica/blob/develop/LICENSE
|
||||
|
||||
**Found a bug?** Please report it to [admin email](mailto:admin@habitica.com) rather than creating an issue (an admin will advise you if a new issue is necessary; usually it is not).
|
||||
**Found a bug?** Please report it to [admin email](mailto:admin@habitica.com) rather than create an issue (an admin will advise you if a new issue is necessary; usually it is not).
|
||||
|
||||
**Have any questions about Habitica or its community?** See the links in the [habitica.com](https://habitica.com) website's Help menu or drop in to [Guilds > Tavern Chat](https://habitica.com/groups/tavern) to ask questions or chat socially!
|
||||
**Creating a third-party tool?** Please review our [API Usage Guidelines](https://github.com/HabitRPG/habitica/wiki/API-Usage-Guidelines) to ensure that your tool is compliant and maintains the best experience for Habitica players.
|
||||
|
||||
**Have any questions about Habitica or contributing?** See the links in the [Habitica](https://habitica.com) website's Help menu. There’s FAQ’s, guides, and the option to reach out to us with any further questions!
|
||||
|
||||
@@ -32,10 +32,12 @@
|
||||
"LOGGLY_CLIENT_TOKEN": "token",
|
||||
"LOGGLY_SUBDOMAIN": "example-subdomain",
|
||||
"LOGGLY_TOKEN": "example-token",
|
||||
"LOG_REQUESTS_EXCESSIVE_MODE": "false",
|
||||
"MAINTENANCE_MODE": "false",
|
||||
"NODE_DB_URI": "mongodb://localhost:27017/habitica-dev?replicaSet=rs",
|
||||
"TEST_DB_URI": "mongodb://localhost:27017/habitica-test?replicaSet=rs",
|
||||
"MONGODB_POOL_SIZE": "10",
|
||||
"MONGODB_SOCKET_TIMEOUT": "20000",
|
||||
"NODE_ENV": "development",
|
||||
"PATH": "bin:node_modules/.bin:/usr/local/bin:/usr/bin:/bin",
|
||||
"PAYPAL_BILLING_PLANS_basic_12mo": "basic_12mo",
|
||||
@@ -84,8 +86,12 @@
|
||||
"BLOCKED_IPS": "",
|
||||
"LOG_AMPLITUDE_EVENTS": "false",
|
||||
"RATE_LIMITER_ENABLED": "false",
|
||||
"LIVELINESS_PROBE_KEY": "",
|
||||
"REDIS_HOST": "aaabbbcccdddeeefff",
|
||||
"REDIS_PORT": "1234",
|
||||
"REDIS_PASSWORD": "12345678",
|
||||
"TRUSTED_DOMAINS": "localhost,https://habitica.com"
|
||||
"TRUSTED_DOMAINS": "localhost,https://habitica.com",
|
||||
"TIME_TRAVEL_ENABLED": "false",
|
||||
"DEBUG_ENABLED": "false",
|
||||
"CONTENT_SWITCHOVER_TIME_OFFSET": 8
|
||||
}
|
||||
|
||||
@@ -22,7 +22,8 @@ services:
|
||||
dockerfile: ./Dockerfile-Dev
|
||||
command: ["npm", "start"]
|
||||
depends_on:
|
||||
- mongo
|
||||
mongo:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- NODE_DB_URI=mongodb://mongo/habitrpg
|
||||
networks:
|
||||
@@ -33,7 +34,16 @@ services:
|
||||
- .:/usr/src/habitica
|
||||
- /usr/src/habitica/node_modules
|
||||
mongo:
|
||||
image: mongo:3.6
|
||||
image: mongo:5.0.23
|
||||
restart: unless-stopped
|
||||
command: ["--replSet", "rs", "--bind_ip_all", "--port", "27017"]
|
||||
healthcheck:
|
||||
test: echo "try { rs.status() } catch (err) { rs.initiate() }" | mongosh --port 27017 --quiet
|
||||
interval: 10s
|
||||
timeout: 30s
|
||||
start_period: 0s
|
||||
start_interval: 1s
|
||||
retries: 30
|
||||
networks:
|
||||
- habitica
|
||||
ports:
|
||||
|
||||
@@ -42,10 +42,41 @@ function cssVarMap (sprite) {
|
||||
}
|
||||
}
|
||||
|
||||
function createSpritesStream (name, src) {
|
||||
function filterFile (file) {
|
||||
if (file.relative.indexOf('Mount_Icon_') !== -1) {
|
||||
return false;
|
||||
}
|
||||
if (file.path.indexOf('shop/') !== -1) {
|
||||
return false;
|
||||
}
|
||||
if (file.path.indexOf('stable/eggs') !== -1) {
|
||||
return false;
|
||||
}
|
||||
if (file.path.indexOf('stable/food') !== -1) {
|
||||
return false;
|
||||
}
|
||||
if (file.path.indexOf('stable/potions') !== -1) {
|
||||
return false;
|
||||
}
|
||||
if (file.relative.indexOf('shop_') === 0) {
|
||||
return false;
|
||||
}
|
||||
if (file.relative.indexOf('icon_background') === 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function createSpritesStream (name, src) {
|
||||
const stream = mergeStream();
|
||||
// need to import this way bc of weird dependency things
|
||||
// eslint-disable-next-line global-require
|
||||
const filter = require('gulp-filter');
|
||||
|
||||
const f = filter(filterFile);
|
||||
|
||||
const spriteData = gulp.src(src)
|
||||
.pipe(f)
|
||||
.pipe(spritesmith({
|
||||
imgName: `spritesmith-${name}.png`,
|
||||
cssName: `spritesmith-${name}.css`,
|
||||
@@ -63,7 +94,7 @@ function createSpritesStream (name, src) {
|
||||
return stream;
|
||||
}
|
||||
|
||||
gulp.task('sprites:main', () => {
|
||||
gulp.task('sprites:main', async () => {
|
||||
const mainSrc = sync('habitica-images/**/*.png');
|
||||
return createSpritesStream('main', mainSrc);
|
||||
});
|
||||
|
||||
@@ -44,8 +44,8 @@ function runInChildProcess (command, options = {}, envVariables = '') {
|
||||
return done => pipe(exec(testBin(command, envVariables), options, done));
|
||||
}
|
||||
|
||||
function integrationTestCommand (testDir, coverageDir) {
|
||||
return `istanbul cover --dir coverage/${coverageDir} --report lcovonly node_modules/mocha/bin/_mocha -- ${testDir} --recursive --require ./test/helpers/start-server`;
|
||||
function integrationTestCommand (testDir) {
|
||||
return `nyc --silent --no-clean mocha ${testDir} --recursive --require ./test/helpers/start-server`;
|
||||
}
|
||||
|
||||
/* Test task definitions */
|
||||
@@ -148,7 +148,7 @@ gulp.task('test:content:safe', gulp.series('test:prepare:build', cb => {
|
||||
|
||||
gulp.task(
|
||||
'test:api:unit:run',
|
||||
runInChildProcess(integrationTestCommand('test/api/unit', 'coverage/api-unit')),
|
||||
runInChildProcess(integrationTestCommand('test/api/unit')),
|
||||
);
|
||||
|
||||
gulp.task('test:api:unit:watch', () => gulp.watch(['website/server/libs/*', 'test/api/unit/**/*', 'website/server/controllers/**/*'], gulp.series('test:api:unit:run', done => done())));
|
||||
@@ -156,7 +156,7 @@ gulp.task('test:api:unit:watch', () => gulp.watch(['website/server/libs/*', 'tes
|
||||
gulp.task('test:api-v3:integration', gulp.series(
|
||||
'test:prepare:mongo',
|
||||
runInChildProcess(
|
||||
integrationTestCommand('test/api/v3/integration', 'coverage/api-v3-integration'),
|
||||
integrationTestCommand('test/api/v3/integration'),
|
||||
LIMIT_MAX_BUFFER_OPTIONS,
|
||||
),
|
||||
));
|
||||
@@ -175,7 +175,7 @@ gulp.task('test:api-v3:integration:separate-server', runInChildProcess(
|
||||
gulp.task('test:api-v4:integration', gulp.series(
|
||||
'test:prepare:mongo',
|
||||
runInChildProcess(
|
||||
integrationTestCommand('test/api/v4', 'api-v4-integration'),
|
||||
integrationTestCommand('test/api/v4'),
|
||||
LIMIT_MAX_BUFFER_OPTIONS,
|
||||
),
|
||||
));
|
||||
|
||||
Submodule habitica-images updated: 90bb5934b6...dfb04339a4
149
migrations/archive/2024/20240621_veteran_pets.js
Normal file
149
migrations/archive/2024/20240621_veteran_pets.js
Normal file
@@ -0,0 +1,149 @@
|
||||
/* eslint-disable no-console */
|
||||
const MIGRATION_NAME = '20240621_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 = {};
|
||||
let push = { notifications: { $each: [] }};
|
||||
|
||||
set.migration = MIGRATION_NAME;
|
||||
if (user.items.pets['Dragon-Veteran']) {
|
||||
set['items.pets.Cactus-Veteran'] = 5;
|
||||
push.notifications.$each.push({
|
||||
type: 'ITEM_RECEIVED',
|
||||
data: {
|
||||
icon: 'icon_pet_veteran_cactus',
|
||||
title: 'You’ve received a Veteran Pet!',
|
||||
text: 'To commemorate being here for a new era of Habitica, we’ve awarded you a Veteran Cactus and 24 Gems!',
|
||||
destination: '/inventory/stable',
|
||||
},
|
||||
seen: false,
|
||||
});
|
||||
} else if (user.items.pets['Fox-Veteran']) {
|
||||
set['items.pets.Dragon-Veteran'] = 5;
|
||||
push.notifications.$each.push({
|
||||
type: 'ITEM_RECEIVED',
|
||||
data: {
|
||||
icon: 'icon_pet_veteran_dragon',
|
||||
title: 'You’ve received a Veteran Pet!',
|
||||
text: 'To commemorate being here for a new era of Habitica, we’ve awarded you a Veteran Dragon and 24 Gems!',
|
||||
destination: '/inventory/stable',
|
||||
},
|
||||
seen: false,
|
||||
});
|
||||
} else if (user.items.pets['Bear-Veteran']) {
|
||||
set['items.pets.Fox-Veteran'] = 5;
|
||||
push.notifications.$each.push({
|
||||
type: 'ITEM_RECEIVED',
|
||||
data: {
|
||||
icon: 'icon_pet_veteran_fox',
|
||||
title: 'You’ve received a Veteran Pet!',
|
||||
text: 'To commemorate being here for a new era of Habitica, we’ve awarded you a Veteran Fox and 24 Gems!',
|
||||
destination: '/inventory/stable',
|
||||
},
|
||||
seen: false,
|
||||
});
|
||||
} else if (user.items.pets['Lion-Veteran']) {
|
||||
set['items.pets.Bear-Veteran'] = 5;
|
||||
push.notifications.$each.push({
|
||||
type: 'ITEM_RECEIVED',
|
||||
data: {
|
||||
icon: 'icon_pet_veteran_bear',
|
||||
title: 'You’ve received a Veteran Pet!',
|
||||
text: 'To commemorate being here for a new era of Habitica, we’ve awarded you a Veteran Bear and 24 Gems!',
|
||||
destination: '/inventory/stable',
|
||||
},
|
||||
seen: false,
|
||||
});
|
||||
} else if (user.items.pets['Tiger-Veteran']) {
|
||||
set['items.pets.Lion-Veteran'] = 5;
|
||||
push.notifications.$each.push({
|
||||
type: 'ITEM_RECEIVED',
|
||||
data: {
|
||||
icon: 'icon_pet_veteran_lion',
|
||||
title: 'You’ve received a Veteran Pet!',
|
||||
text: 'To commemorate being here for a new era of Habitica, we’ve awarded you a Veteran Lion and 24 Gems!',
|
||||
destination: '/inventory/stable',
|
||||
},
|
||||
seen: false,
|
||||
});
|
||||
} else if (user.items.pets['Wolf-Veteran']) {
|
||||
set['items.pets.Tiger-Veteran'] = 5;
|
||||
push.notifications.$each.push({
|
||||
type: 'ITEM_RECEIVED',
|
||||
data: {
|
||||
icon: 'icon_pet_veteran_tiger',
|
||||
title: 'You’ve received a Veteran Pet!',
|
||||
text: 'To commemorate being here for a new era of Habitica, we’ve awarded you a Veteran Tiger and 24 Gems!',
|
||||
destination: '/inventory/stable',
|
||||
},
|
||||
seen: false,
|
||||
});
|
||||
} else {
|
||||
set['items.pets.Wolf-Veteran'] = 5;
|
||||
push.notifications.$each.push({
|
||||
type: 'ITEM_RECEIVED',
|
||||
data: {
|
||||
icon: 'icon_pet_veteran_wolf',
|
||||
title: 'You’ve received a Veteran Pet!',
|
||||
text: 'To commemorate being here for a new era of Habitica, we’ve awarded you a Veteran Wolf and 24 Gems!',
|
||||
destination: '/inventory/stable',
|
||||
},
|
||||
seen: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
await user.updateBalance(
|
||||
6,
|
||||
'admin_update_balance',
|
||||
'',
|
||||
'Veteran Ladder award',
|
||||
);
|
||||
|
||||
return await User.updateOne(
|
||||
{ _id: user._id },
|
||||
{ $set: set, $push: push, $inc: { balance: 6 } },
|
||||
).exec();
|
||||
}
|
||||
|
||||
export default async function processUsers () {
|
||||
let query = {
|
||||
migration: {$ne: MIGRATION_NAME},
|
||||
'auth.timestamps.loggedin': { $gt: new Date('2024-05-21') },
|
||||
};
|
||||
|
||||
const fields = {
|
||||
_id: 1,
|
||||
items: 1,
|
||||
migration: 1,
|
||||
contributor: 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)
|
||||
.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
|
||||
}
|
||||
};
|
||||
47
migrations/archive/2024/20240806_purge_invite_accepted.js
Normal file
47
migrations/archive/2024/20240806_purge_invite_accepted.js
Normal file
@@ -0,0 +1,47 @@
|
||||
/* eslint-disable no-console */
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
|
||||
const MIGRATION_NAME = '2024_purge_invite_accepted';
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUsers (userIds) {
|
||||
count += userIds.length;
|
||||
if (count % progressCount === 0) console.warn(`${count} ${userIds[0]}`);
|
||||
|
||||
return await User.updateMany(
|
||||
{ _id: { $in: userIds } },
|
||||
{ $pull: { notifications: { type: 'GROUP_INVITE_ACCEPTED' } } },
|
||||
).exec();
|
||||
}
|
||||
|
||||
export default async function processUsers () {
|
||||
let query = {
|
||||
migration: { $ne: MIGRATION_NAME },
|
||||
'notifications.type': 'GROUP_INVITE_ACCEPTED',
|
||||
'auth.timestamps.loggedin': { $gt: new Date('2024-06-25') },
|
||||
};
|
||||
|
||||
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({ _id: 1 })
|
||||
.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],
|
||||
};
|
||||
}
|
||||
|
||||
const userIds = users.map(user => user._id);
|
||||
|
||||
await updateUsers(userIds); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
};
|
||||
115
migrations/archive/2024/20241119_gem_caps_hourglasses.js
Normal file
115
migrations/archive/2024/20241119_gem_caps_hourglasses.js
Normal file
@@ -0,0 +1,115 @@
|
||||
/* eslint-disable no-console */
|
||||
const MIGRATION_NAME = '20241119_gem_caps_hourglasses';
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count += 1;
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
const { consecutive, customerId, dateTerminated, planId } = user.purchased.plan;
|
||||
const isRecurring = customerId !== 'Gift' && !dateTerminated;
|
||||
const updateOp = {
|
||||
$set: {
|
||||
migration: MIGRATION_NAME,
|
||||
'purchased.plan.consecutive.gemCapExtra': Math.max(2 * Math.ceil((consecutive.gemCapExtra + 1) / 2, 26)),
|
||||
},
|
||||
$inc: {},
|
||||
};
|
||||
|
||||
let hourglassBonus = 0;
|
||||
|
||||
if (isRecurring) {
|
||||
await user.updateBalance(
|
||||
5,
|
||||
'admin_update_balance',
|
||||
'',
|
||||
'Subscription Reward Migration',
|
||||
);
|
||||
updateOp.$inc.balance = 5;
|
||||
switch (planId) {
|
||||
case 'basic':
|
||||
case 'basic_earned':
|
||||
case 'group_plan_auto':
|
||||
hourglassBonus = 2;
|
||||
break;
|
||||
case 'basic_3mo':
|
||||
case 'basic_6mo':
|
||||
case 'google_6mo':
|
||||
hourglassBonus = 4;
|
||||
break;
|
||||
case 'basic_12mo':
|
||||
hourglassBonus = 12;
|
||||
updateOp.$set['purchased.plan.hourglassPromoReceived'] = new Date();
|
||||
break;
|
||||
default:
|
||||
hourglassBonus = 0;
|
||||
}
|
||||
|
||||
if (hourglassBonus) {
|
||||
updateOp.$inc['purchased.plan.consecutive.trinkets'] = hourglassBonus;
|
||||
await user.updateHourglasses(
|
||||
hourglassBonus,
|
||||
'admin_update_balance',
|
||||
'',
|
||||
'Subscription Reward Migration',
|
||||
);
|
||||
}
|
||||
updateOp.$push = {
|
||||
notifications: {
|
||||
type: 'ITEM_RECEIVED',
|
||||
data: {
|
||||
icon: 'notif_subscriber_reward',
|
||||
title: 'Thanks for being a subscriber!',
|
||||
text: 'Enjoy these extra Mystic Hourglasses and Gems to celebrate our new benefits.',
|
||||
},
|
||||
seen: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return await User.updateOne(
|
||||
{ _id: user._id },
|
||||
updateOp,
|
||||
).exec();
|
||||
}
|
||||
|
||||
export default async function processUsers () {
|
||||
let query = {
|
||||
migration: { $ne: MIGRATION_NAME },
|
||||
'purchased.plan.customerId': { $exists: true },
|
||||
$or: [
|
||||
{ 'purchased.plan.dateTerminated': { $exists: false } },
|
||||
{ 'purchased.plan.dateTerminated': null },
|
||||
{ 'purchased.plan.dateTerminated': { $gt: new Date() } },
|
||||
],
|
||||
};
|
||||
|
||||
const fields = {
|
||||
_id: 1,
|
||||
purchased: 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)
|
||||
.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
|
||||
}
|
||||
};
|
||||
@@ -26,7 +26,7 @@ async function updateUser (user) {
|
||||
[{ name: 'BASE_URL', content: BASE_URL }], // Add variables from template
|
||||
);
|
||||
|
||||
return User.update({ _id: user._id }, { $set: { migration: MIGRATION_NAME } }).exec();
|
||||
return User.updateOne({ _id: user._id }, { $set: { migration: MIGRATION_NAME } }).exec();
|
||||
}
|
||||
|
||||
export default async function processUsers () {
|
||||
|
||||
@@ -27,13 +27,13 @@ async function updateUser (user) {
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
return User.update({ _id: user._id }, { $set: set }).exec();
|
||||
return User.updateOne({ _id: user._id }, { $set: set }).exec();
|
||||
}
|
||||
|
||||
export default async function processUsers () {
|
||||
const query = {
|
||||
migration: { $ne: MIGRATION_NAME },
|
||||
'auth.local.lowerCaseUsername': 'olson1',
|
||||
'auth.local.username': 'ExampleHabitican',
|
||||
};
|
||||
|
||||
const fields = {
|
||||
|
||||
@@ -57,7 +57,7 @@ async function updateUser (user) {
|
||||
export default async function processUsers () {
|
||||
const query = {
|
||||
migration: { $ne: MIGRATION_NAME },
|
||||
'auth.local.username': 'SabreTest',
|
||||
'auth.local.username': 'ExampleHabitican',
|
||||
};
|
||||
|
||||
const fields = {
|
||||
|
||||
175
migrations/users/habitoween.js
Normal file
175
migrations/users/habitoween.js
Normal file
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* Award Habitoween ladder items to participants in this month's Habitoween festivities
|
||||
*/
|
||||
/* eslint-disable no-console */
|
||||
import { model as User } from '../../website/server/models/user';
|
||||
|
||||
const MIGRATION_NAME = '20241030_habitoween_ladder'; // Update when running in future years
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count += 1;
|
||||
|
||||
const set = { migration: MIGRATION_NAME };
|
||||
const 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,
|
||||
};
|
||||
const push = { notifications: { $each: [] } };
|
||||
|
||||
if (user && user.items && user.items.mounts && user.items.mounts['JackOLantern-RoyalPurple']) {
|
||||
push.notifications.$each.push({
|
||||
type: 'ITEM_RECEIVED',
|
||||
data: {
|
||||
icon: 'notif_habitoween_candy',
|
||||
title: 'Happy Habitoween!',
|
||||
text: 'For this spooky celebration, you\'ve received an assortment of candy for your Pets!',
|
||||
destination: '/inventory/stable',
|
||||
},
|
||||
seen: false,
|
||||
});
|
||||
} else if (user && user.items && user.items.pets && user.items.pets['JackOLantern-RoyalPurple']) {
|
||||
set['items.mounts.JackOLantern-RoyalPurple'] = true;
|
||||
push.notifications.$each.push({
|
||||
type: 'ITEM_RECEIVED',
|
||||
data: {
|
||||
icon: 'notif_habitoween_purple_mount',
|
||||
title: 'Happy Habitoween!',
|
||||
text: 'For this spooky celebration, you\'ve received a Royal Purple Jack-O-Lantern Mount and an assortment of candy for your Pets!',
|
||||
destination: '/inventory/stable',
|
||||
},
|
||||
seen: false,
|
||||
});
|
||||
} else if (user && user.items && user.items.mounts && user.items.mounts['JackOLantern-Glow']) {
|
||||
set['items.pets.JackOLantern-RoyalPurple'] = 5;
|
||||
push.notifications.$each.push({
|
||||
type: 'ITEM_RECEIVED',
|
||||
data: {
|
||||
icon: 'notif_habitoween_purple_pet',
|
||||
title: 'Happy Habitoween!',
|
||||
text: 'For this spooky celebration, you\'ve received a Royal Purple Jack-O-Lantern Pet and an assortment of candy for your Pets!',
|
||||
destination: '/inventory/stable',
|
||||
},
|
||||
seen: false,
|
||||
});
|
||||
} else if (user && user.items && user.items.pets && user.items.pets['JackOLantern-Glow']) {
|
||||
set['items.mounts.JackOLantern-Glow'] = true;
|
||||
push.notifications.$each.push({
|
||||
type: 'ITEM_RECEIVED',
|
||||
data: {
|
||||
icon: 'notif_habitoween_glow_mount',
|
||||
title: 'Happy Habitoween!',
|
||||
text: 'For this spooky celebration, you\'ve received a Glow-in-the-Dark Jack-O-Lantern Mount and an assortment of candy for your Pets!',
|
||||
destination: '/inventory/stable',
|
||||
},
|
||||
seen: false,
|
||||
});
|
||||
} else if (user && user.items && user.items.mounts && user.items.mounts['JackOLantern-Ghost']) {
|
||||
set['items.pets.JackOLantern-Glow'] = 5;
|
||||
push.notifications.$each.push({
|
||||
type: 'ITEM_RECEIVED',
|
||||
data: {
|
||||
icon: 'notif_habitoween_glow_pet',
|
||||
title: 'Happy Habitoween!',
|
||||
text: 'For this spooky celebration, you\'ve received a Glow-in-the-Dark Jack-O-Lantern Pet and an assortment of candy for your Pets!',
|
||||
destination: '/inventory/stable',
|
||||
},
|
||||
seen: false,
|
||||
});
|
||||
} else if (user && user.items && user.items.pets && user.items.pets['JackOLantern-Ghost']) {
|
||||
set['items.mounts.JackOLantern-Ghost'] = true;
|
||||
push.notifications.$each.push({
|
||||
type: 'ITEM_RECEIVED',
|
||||
data: {
|
||||
icon: 'notif_habitoween_ghost_mount',
|
||||
title: 'Happy Habitoween!',
|
||||
text: 'For this spooky celebration, you\'ve received a Ghost Jack-O-Lantern Mount and an assortment of candy for your Pets!',
|
||||
destination: '/inventory/stable',
|
||||
},
|
||||
seen: false,
|
||||
});
|
||||
} else if (user && user.items && user.items.mounts && user.items.mounts['JackOLantern-Base']) {
|
||||
set['items.pets.JackOLantern-Ghost'] = 5;
|
||||
push.notifications.$each.push({
|
||||
type: 'ITEM_RECEIVED',
|
||||
data: {
|
||||
icon: 'notif_habitoween_ghost_pet',
|
||||
title: 'Happy Habitoween!',
|
||||
text: 'For this spooky celebration, you\'ve received a Ghost Jack-O-Lantern Pet and an assortment of candy for your Pets!',
|
||||
destination: '/inventory/stable',
|
||||
},
|
||||
seen: false,
|
||||
});
|
||||
} else if (user && user.items && user.items.pets && user.items.pets['JackOLantern-Base']) {
|
||||
set['items.mounts.JackOLantern-Base'] = true;
|
||||
push.notifications.$each.push({
|
||||
type: 'ITEM_RECEIVED',
|
||||
data: {
|
||||
icon: 'notif_habitoween_base_mount',
|
||||
title: 'Happy Habitoween!',
|
||||
text: 'For this spooky celebration, you\'ve received a Jack-O-Lantern Mount and an assortment of candy for your Pets!',
|
||||
destination: '/inventory/stable',
|
||||
},
|
||||
seen: false,
|
||||
});
|
||||
} else {
|
||||
set['items.pets.JackOLantern-Base'] = 5;
|
||||
push.notifications.$each.push({
|
||||
type: 'ITEM_RECEIVED',
|
||||
data: {
|
||||
icon: 'notif_habitoween_base_pet',
|
||||
title: 'Happy Habitoween!',
|
||||
text: 'For this spooky celebration, you\'ve received a Jack-O-Lantern Pet and an assortment of candy for your Pets!',
|
||||
destination: '/inventory/stable',
|
||||
},
|
||||
seen: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
return User.updateOne({ _id: user._id }, { $inc: inc, $push: push, $set: set }).exec();
|
||||
}
|
||||
|
||||
export default async function processUsers () {
|
||||
const query = {
|
||||
migration: { $ne: MIGRATION_NAME },
|
||||
'auth.timestamps.loggedin': { $gt: new Date('2024-10-01') },
|
||||
};
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
167
migrations/users/harvest_feast.js
Normal file
167
migrations/users/harvest_feast.js
Normal file
@@ -0,0 +1,167 @@
|
||||
/* eslint-disable no-console */
|
||||
import { model as User } from '../../website/server/models/user';
|
||||
|
||||
const MIGRATION_NAME = '20241120_harvest_feast';
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count += 1;
|
||||
|
||||
const updateOp = {
|
||||
$set: { migration: MIGRATION_NAME },
|
||||
};
|
||||
|
||||
if (typeof user.items.gear.owned.head_special_turkeyHelmGilded !== 'undefined') {
|
||||
updateOp.$inc = {
|
||||
'items.food.Pie_Base': 1,
|
||||
'items.food.Pie_CottonCandyBlue': 1,
|
||||
'items.food.Pie_CottonCandyPink': 1,
|
||||
'items.food.Pie_Desert': 1,
|
||||
'items.food.Pie_Golden': 1,
|
||||
'items.food.Pie_Red': 1,
|
||||
'items.food.Pie_Shade': 1,
|
||||
'items.food.Pie_Skeleton': 1,
|
||||
'items.food.Pie_Zombie': 1,
|
||||
'items.food.Pie_White': 1,
|
||||
};
|
||||
updateOp.$push = {
|
||||
notifications: {
|
||||
type: 'ITEM_RECEIVED',
|
||||
data: {
|
||||
icon: 'notif_harvestfeast_pie',
|
||||
title: 'Happy Harvest Feast!',
|
||||
text: 'Gobble gobble, you\'ve received an assortment of pie for your Pets!',
|
||||
destination: '/inventory/stable',
|
||||
},
|
||||
seen: false,
|
||||
},
|
||||
};
|
||||
} else if (typeof user.items.gear.owned.armor_special_turkeyArmorBase !== 'undefined') {
|
||||
updateOp.$set['items.gear.owned.head_special_turkeyHelmGilded'] = true;
|
||||
updateOp.$set['items.gear.owned.armor_special_turkeyArmorGilded'] = true;
|
||||
updateOp.$set['items.gear.owned.back_special_turkeyTailGilded'] = true;
|
||||
updateOp.$push = {
|
||||
notifications: {
|
||||
type: 'ITEM_RECEIVED',
|
||||
data: {
|
||||
icon: 'notif_harvestfeast_gilded_set',
|
||||
title: 'Happy Harvest Feast!',
|
||||
text: 'Gobble gobble, you\'ve received the Gilded Turkey Armor, Helm, and Tail!',
|
||||
destination: '/inventory/equipment',
|
||||
},
|
||||
seen: false,
|
||||
},
|
||||
};
|
||||
} else if (user.items && user.items.mounts && user.items.mounts['Turkey-Gilded']) {
|
||||
updateOp.$set['items.gear.owned.head_special_turkeyHelmBase'] = true;
|
||||
updateOp.$set['items.gear.owned.armor_special_turkeyArmorBase'] = true;
|
||||
updateOp.$set['items.gear.owned.back_special_turkeyTailBase'] = true;
|
||||
updateOp.$push = {
|
||||
notifications: {
|
||||
type: 'ITEM_RECEIVED',
|
||||
data: {
|
||||
icon: 'notif_harvestfeast_base_set',
|
||||
title: 'Happy Harvest Feast!',
|
||||
text: 'Gobble gobble, you\'ve received the Turkey Armor, Helm, and Tail!',
|
||||
destination: '/inventory/equipment',
|
||||
},
|
||||
seen: false,
|
||||
},
|
||||
};
|
||||
} else if (user.items && user.items.pets && user.items.pets['Turkey-Gilded']) {
|
||||
updateOp.$set['items.mounts.Turkey-Gilded'] = true;
|
||||
updateOp.$push = {
|
||||
notifications: {
|
||||
type: 'ITEM_RECEIVED',
|
||||
data: {
|
||||
icon: 'notif_harvestfeast_gilded_mount',
|
||||
title: 'Happy Harvest Feast!',
|
||||
text: 'Gobble gobble, you\'ve received the Gilded Turkey Mount!',
|
||||
destination: '/inventory/stable',
|
||||
},
|
||||
seen: false,
|
||||
},
|
||||
};
|
||||
} else if (user.items && user.items.mounts && user.items.mounts['Turkey-Base']) {
|
||||
updateOp.$set['items.pets.Turkey-Gilded'] = 5;
|
||||
updateOp.$push = {
|
||||
notifications: {
|
||||
type: 'ITEM_RECEIVED',
|
||||
data: {
|
||||
icon: 'notif_harvestfeast_gilded_pet',
|
||||
title: 'Happy Harvest Feast!',
|
||||
text: 'Gobble gobble, you\'ve received the Gilded Turkey Pet!',
|
||||
destination: '/inventory/stable',
|
||||
},
|
||||
seen: false,
|
||||
},
|
||||
};
|
||||
} else if (user.items && user.items.pets && user.items.pets['Turkey-Base']) {
|
||||
updateOp.$set['items.mounts.Turkey-Base'] = true;
|
||||
updateOp.$push = {
|
||||
notifications: {
|
||||
type: 'ITEM_RECEIVED',
|
||||
data: {
|
||||
icon: 'notif_harvestfeast_base_mount',
|
||||
title: 'Happy Harvest Feast!',
|
||||
text: 'Gobble gobble, you\'ve received the Turkey Mount!',
|
||||
destination: '/inventory/stable',
|
||||
},
|
||||
seen: false,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
updateOp.$set['items.pets.Turkey-Base'] = 5;
|
||||
updateOp.$push = {
|
||||
notifications: {
|
||||
type: 'ITEM_RECEIVED',
|
||||
data: {
|
||||
icon: 'notif_harvestfeast_base_pet',
|
||||
title: 'Happy Harvest Feast!',
|
||||
text: 'Gobble gobble, you\'ve received the Turkey Pet!',
|
||||
destination: '/inventory/stable',
|
||||
},
|
||||
seen: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
return User.updateOne({ _id: user._id }, updateOp).exec();
|
||||
}
|
||||
|
||||
export default async function processUsers () {
|
||||
const query = {
|
||||
migration: { $ne: MIGRATION_NAME },
|
||||
'auth.timestamps.loggedin': { $gt: new Date('2024-10-20') },
|
||||
};
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
/* eslint-disable no-console */
|
||||
const MIGRATION_NAME = '20230731_naming_day';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
import { model as User } from '../../website/server/models/user';
|
||||
|
||||
const MIGRATION_NAME = '20240731_naming_day';
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count++;
|
||||
count += 1;
|
||||
|
||||
let set;
|
||||
let push;
|
||||
@@ -115,16 +113,16 @@ async function updateUser (user) {
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
if (push) {
|
||||
return await user.updateOne({ $set: set, $inc: inc, $push: push }).exec();
|
||||
} else {
|
||||
return await user.updateOne({ $set: set, $inc: inc }).exec();
|
||||
return user.updateOne({ $set: set, $inc: inc, $push: push }).exec();
|
||||
}
|
||||
|
||||
return user.updateOne({ $set: set, $inc: inc }).exec();
|
||||
}
|
||||
|
||||
export default async function processUsers () {
|
||||
let query = {
|
||||
const query = {
|
||||
migration: { $ne: MIGRATION_NAME },
|
||||
'auth.timestamps.loggedin': { $gt: new Date('2023-07-01') },
|
||||
'auth.timestamps.loggedin': { $gt: new Date('2024-07-01') },
|
||||
};
|
||||
|
||||
const fields = {
|
||||
@@ -152,4 +150,4 @@ export default async function processUsers () {
|
||||
|
||||
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
};
|
||||
}
|
||||
125
migrations/users/nye.js
Normal file
125
migrations/users/nye.js
Normal file
@@ -0,0 +1,125 @@
|
||||
/* eslint-disable no-console */
|
||||
import { model as User } from '../../website/server/models/user';
|
||||
|
||||
const MIGRATION_NAME = '20231228_nye';
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count += 1;
|
||||
|
||||
const updateOp = {
|
||||
$set: { migration: MIGRATION_NAME },
|
||||
$push: { },
|
||||
};
|
||||
const data = {
|
||||
title: 'Happy New Year!',
|
||||
destination: '/inventory/equipment',
|
||||
};
|
||||
|
||||
if (typeof user.items.gear.owned.head_special_nye2023 !== 'undefined') {
|
||||
updateOp.$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,
|
||||
};
|
||||
data.icon = 'notif_candy_nye';
|
||||
data.text = 'You’ve received an assortment of candy to celebrate with your Pets!';
|
||||
data.destination = '/inventory/stable';
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2022 !== 'undefined') {
|
||||
updateOp.$set['items.gear.owned.head_special_nye2023'] = true;
|
||||
data.icon = 'notif_2023hat_nye';
|
||||
data.text = 'Take on your resolutions with style in this Ludicrous Party Hat!';
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2021 !== 'undefined') {
|
||||
updateOp.$set['items.gear.owned.head_special_nye2022'] = true;
|
||||
data.icon = 'notif_2022hat_nye';
|
||||
data.text = 'Take on your resolutions with style in this Fabulous Party Hat!';
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2020 !== 'undefined') {
|
||||
updateOp.$set['items.gear.owned.head_special_nye2021'] = true;
|
||||
data.icon = 'notif_2021hat_nye';
|
||||
data.text = 'Take on your resolutions with style in this Preposterous Party Hat!';
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2019 !== 'undefined') {
|
||||
updateOp.$set['items.gear.owned.head_special_nye2020'] = true;
|
||||
data.icon = 'notif_2020hat_nye';
|
||||
data.text = 'Take on your resolutions with style in this Extravagant Party Hat!';
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2018 !== 'undefined') {
|
||||
updateOp.$set['items.gear.owned.head_special_nye2019'] = true;
|
||||
data.icon = 'notif_2019hat_nye';
|
||||
data.text = 'Take on your resolutions with style in this Outrageous Party Hat!';
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2017 !== 'undefined') {
|
||||
updateOp.$set['items.gear.owned.head_special_nye2018'] = true;
|
||||
data.icon = 'notif_2018hat_nye';
|
||||
data.text = 'Take on your resolutions with style in this Outlandish Party Hat!';
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2016 !== 'undefined') {
|
||||
updateOp.$set['items.gear.owned.head_special_nye2017'] = true;
|
||||
data.icon = 'notif_2017hat_nye';
|
||||
data.text = 'Take on your resolutions with style in this Fanciful Party Hat!';
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2015 !== 'undefined') {
|
||||
updateOp.$set['items.gear.owned.head_special_nye2016'] = true;
|
||||
data.icon = 'notif_2016hat_nye';
|
||||
data.text = 'Take on your resolutions with style in this Whimsical Party Hat!';
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2014 !== 'undefined') {
|
||||
updateOp.$set['items.gear.owned.head_special_nye2015'] = true;
|
||||
data.icon = 'notif_2015hat_nye';
|
||||
data.text = 'Take on your resolutions with style in this Ridiculous Party Hat!';
|
||||
} else if (typeof user.items.gear.owned.head_special_nye !== 'undefined') {
|
||||
updateOp.$set['items.gear.owned.head_special_nye2014'] = true;
|
||||
data.icon = 'notif_2014hat_nye';
|
||||
data.text = 'Take on your resolutions with style in this Silly Party Hat!';
|
||||
} else {
|
||||
updateOp.$set['items.gear.owned.head_special_nye'] = true;
|
||||
data.icon = 'notif_2013hat_nye';
|
||||
data.text = 'Take on your resolutions with style in this Absurd Party Hat!';
|
||||
}
|
||||
|
||||
updateOp.$push.notifications = {
|
||||
type: 'ITEM_RECEIVED',
|
||||
data,
|
||||
seen: false,
|
||||
};
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
return User.updateOne({ _id: user._id }, updateOp).exec();
|
||||
}
|
||||
|
||||
export default async function processUsers () {
|
||||
const query = {
|
||||
'auth.timestamps.loggedin': { $gt: new Date('2023-12-01') },
|
||||
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
|
||||
}
|
||||
}
|
||||
3298
package-lock.json
generated
3298
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
34
package.json
34
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "5.24.2",
|
||||
"version": "5.32.5",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
@@ -15,8 +15,9 @@
|
||||
"amplitude": "^6.0.0",
|
||||
"apidoc": "^0.54.0",
|
||||
"apple-auth": "^1.0.9",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"body-parser": "^1.20.2",
|
||||
"body-parser": "^1.20.3",
|
||||
"bootstrap": "^4.6.2",
|
||||
"compression": "^1.7.4",
|
||||
"cookie-session": "^2.0.0",
|
||||
@@ -27,13 +28,15 @@
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-config-habitrpg": "^6.2.3",
|
||||
"eslint-plugin-mocha": "^5.0.0",
|
||||
"express": "^4.19.2",
|
||||
"express": "^4.21.1",
|
||||
"express-basic-auth": "^1.2.1",
|
||||
"express-validator": "^5.2.0",
|
||||
"firebase-admin": "^12.1.1",
|
||||
"glob": "^8.1.0",
|
||||
"got": "^11.8.6",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-babel": "^8.0.0",
|
||||
"gulp-filter": "^7.0.0",
|
||||
"gulp-imagemin": "^7.1.0",
|
||||
"gulp-nodemon": "^2.5.0",
|
||||
"gulp.spritesmith": "^6.13.0",
|
||||
@@ -48,7 +51,7 @@
|
||||
"method-override": "^3.0.0",
|
||||
"moment": "^2.29.4",
|
||||
"moment-recur": "^1.0.7",
|
||||
"mongoose": "^7.6.3",
|
||||
"mongoose": "^7.8.3",
|
||||
"morgan": "^1.10.0",
|
||||
"nconf": "^0.12.1",
|
||||
"node-gcm": "^1.0.5",
|
||||
@@ -66,12 +69,14 @@
|
||||
"remove-markdown": "^0.5.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"short-uuid": "^4.2.2",
|
||||
"sinon": "^15.2.0",
|
||||
"stripe": "^12.18.0",
|
||||
"superagent": "^8.1.2",
|
||||
"universal-analytics": "^0.5.3",
|
||||
"useragent": "^2.1.9",
|
||||
"uuid": "^9.0.0",
|
||||
"validator": "^13.11.0",
|
||||
"webpack-bundle-analyzer": "^4.10.2",
|
||||
"winston": "^3.10.0",
|
||||
"winston-loggly-bulk": "^3.3.0",
|
||||
"xml2js": "^0.6.2"
|
||||
@@ -92,35 +97,36 @@
|
||||
"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",
|
||||
"test:sanity": "nyc --silent --no-clean mocha test/sanity --recursive",
|
||||
"test:common": "nyc --silent --no-clean mocha test/common --recursive",
|
||||
"test:content": "nyc --silent --no-clean mocha test/content --recursive",
|
||||
"test:nodemon": "gulp test:nodemon",
|
||||
"coverage": "COVERAGE=true mocha --require register-handlers.js --reporter html-cov > coverage.html; open coverage.html",
|
||||
"coverage": "nyc report --reporter=html --report-dir coverage/results; open coverage/results/index.html",
|
||||
"sprites": "gulp sprites:compile",
|
||||
"client:dev": "cd website/client && npm run serve",
|
||||
"client:build": "cd website/client && npm run build",
|
||||
"client:unit": "cd website/client && npm run test:unit",
|
||||
"start": "gulp nodemon",
|
||||
"start:simple": "node ./website/server/index.js",
|
||||
"debug": "gulp nodemon --inspect",
|
||||
"mongo:dev": "run-rs -v 5.0.23 -l ubuntu1804 --keep --dbpath mongodb-data --number 1 --quiet",
|
||||
"mongo:test": "run-rs -v 5.0.23 -l ubuntu1804 --keep --dbpath mongodb-data-testing --number 1 --quiet",
|
||||
"postinstall": "git config --global url.\"https://\".insteadOf git:// && gulp build && cd website/client && npm install",
|
||||
"apidoc": "gulp apidoc"
|
||||
"apidoc": "gulp apidoc",
|
||||
"heroku-postbuild": ".heroku/report_deploy.sh"
|
||||
},
|
||||
"devDependencies": {
|
||||
"axios": "^1.4.0",
|
||||
"axios": "^1.7.4",
|
||||
"chai": "^4.3.7",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"chai-moment": "^0.1.0",
|
||||
"chalk": "^5.3.0",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"expect.js": "^0.3.1",
|
||||
"istanbul": "^1.1.0-alpha.1",
|
||||
"cross-spawn": "^7.0.5",
|
||||
"mocha": "^5.1.1",
|
||||
"monk": "^7.3.4",
|
||||
"nyc": "^15.1.0",
|
||||
"require-again": "^2.0.0",
|
||||
"run-rs": "^0.7.7",
|
||||
"sinon": "^15.2.0",
|
||||
"sinon-chai": "^3.7.0",
|
||||
"sinon-stub-promise": "^4.0.0"
|
||||
}
|
||||
|
||||
@@ -44,7 +44,6 @@ describe('bug-report', () => {
|
||||
USER_HOURGLASSES: 0,
|
||||
USER_ID: userId,
|
||||
USER_LEVEL: 1,
|
||||
USER_OFFSET_MONTHS: 0,
|
||||
USER_PAYMENT_PLATFORM: undefined,
|
||||
USER_SUBSCRIPTION: undefined,
|
||||
USER_TIMEZONE_OFFSET: 0,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import fs from 'fs';
|
||||
import * as contentLib from '../../../../website/server/libs/content';
|
||||
import content from '../../../../website/common/script/content';
|
||||
import {
|
||||
generateRes,
|
||||
} from '../../../helpers/api-unit.helper';
|
||||
|
||||
describe('contentLib', () => {
|
||||
describe('CONTENT_CACHE_PATH', () => {
|
||||
@@ -13,5 +17,90 @@ describe('contentLib', () => {
|
||||
contentLib.getLocalizedContentResponse();
|
||||
expect(typeof content.backgrounds.backgrounds062014.beach.text).to.equal('function');
|
||||
});
|
||||
|
||||
it('removes keys from the content data', () => {
|
||||
const response = contentLib.localizeContentData(content, 'en', { backgroundsFlat: true, dropHatchingPotions: true });
|
||||
expect(response.backgroundsFlat).to.not.exist;
|
||||
expect(response.backgrounds).to.exist;
|
||||
expect(response.dropHatchingPotions).to.not.exist;
|
||||
expect(response.hatchingPotions).to.exist;
|
||||
});
|
||||
|
||||
it('removes nested keys from the content data', () => {
|
||||
const response = contentLib.localizeContentData(content, 'en', { gear: { tree: true } });
|
||||
expect(response.gear.tree).to.not.exist;
|
||||
expect(response.gear.flat).to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
it('generates a hash for a filter', () => {
|
||||
const hash = contentLib.hashForFilter('backgroundsFlat,gear.flat');
|
||||
expect(hash).to.equal('-1791877526');
|
||||
});
|
||||
|
||||
it('serves content', () => {
|
||||
const resSpy = generateRes();
|
||||
contentLib.serveContent(resSpy, 'en', '', false);
|
||||
expect(resSpy.send).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('serves filtered content', () => {
|
||||
const resSpy = generateRes();
|
||||
contentLib.serveContent(resSpy, 'en', 'backgroundsFlat,gear.flat', false);
|
||||
expect(resSpy.send).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
describe('caches content', async () => {
|
||||
let resSpy;
|
||||
beforeEach(() => {
|
||||
resSpy = generateRes();
|
||||
if (fs.existsSync(contentLib.CONTENT_CACHE_PATH)) {
|
||||
fs.rmSync(contentLib.CONTENT_CACHE_PATH, { recursive: true });
|
||||
}
|
||||
fs.mkdirSync(contentLib.CONTENT_CACHE_PATH);
|
||||
});
|
||||
|
||||
it('does not cache requests in development mode', async () => {
|
||||
contentLib.serveContent(resSpy, 'en', '', false);
|
||||
expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en.json`)).to.be.false;
|
||||
});
|
||||
|
||||
it('caches unfiltered requests', async () => {
|
||||
expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en.json`)).to.be.false;
|
||||
contentLib.serveContent(resSpy, 'en', '', true);
|
||||
expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en.json`)).to.be.true;
|
||||
});
|
||||
|
||||
it('serves cached requests', async () => {
|
||||
fs.writeFileSync(
|
||||
`${contentLib.CONTENT_CACHE_PATH}en.json`,
|
||||
'{"success": true, "data": {"all": {}}}',
|
||||
'utf8',
|
||||
);
|
||||
contentLib.serveContent(resSpy, 'en', '', true);
|
||||
expect(resSpy.sendFile).to.have.been.calledOnce;
|
||||
expect(resSpy.sendFile).to.have.been.calledWith(`${contentLib.CONTENT_CACHE_PATH}en.json`);
|
||||
});
|
||||
|
||||
it('caches filtered requests', async () => {
|
||||
const filter = 'backgroundsFlat,gear.flat';
|
||||
const hash = contentLib.hashForFilter(filter);
|
||||
expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en${hash}.json`)).to.be.false;
|
||||
contentLib.serveContent(resSpy, 'en', filter, true);
|
||||
expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en${hash}.json`)).to.be.true;
|
||||
});
|
||||
|
||||
it('serves filtered cached requests', async () => {
|
||||
const filter = 'backgroundsFlat,gear.flat';
|
||||
const hash = contentLib.hashForFilter(filter);
|
||||
fs.writeFileSync(
|
||||
`${contentLib.CONTENT_CACHE_PATH}en${hash}.json`,
|
||||
'{"success": true, "data": {}}',
|
||||
'utf8',
|
||||
);
|
||||
contentLib.serveContent(resSpy, 'en', filter, true);
|
||||
expect(resSpy.sendFile).to.have.been.calledOnce;
|
||||
expect(resSpy.sendFile).to.have.been.calledWith(`${contentLib.CONTENT_CACHE_PATH}en${hash}.json`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -154,6 +154,14 @@ describe('cron', async () => {
|
||||
expect(user.purchased.plan.consecutive.count).to.equal(1);
|
||||
});
|
||||
|
||||
it('increments plan.cumulativeCount', async () => {
|
||||
user.purchased.plan.cumulativeCount = 0;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.cumulativeCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('increments plan.consecutive.count by more than 1 if user skipped months between logins', async () => {
|
||||
user.purchased.plan.dateUpdated = moment().subtract(2, 'months').toDate();
|
||||
user.purchased.plan.consecutive.count = 0;
|
||||
@@ -163,12 +171,13 @@ describe('cron', async () => {
|
||||
expect(user.purchased.plan.consecutive.count).to.equal(2);
|
||||
});
|
||||
|
||||
it('decrements plan.consecutive.offset when offset is greater than 0', async () => {
|
||||
user.purchased.plan.consecutive.offset = 2;
|
||||
it('increments plan.cumulativeCount by more than 1 if user skipped months between logins', async () => {
|
||||
user.purchased.plan.dateUpdated = moment().subtract(3, 'months').toDate();
|
||||
user.purchased.plan.cumulativeCount = 0;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.consecutive.offset).to.equal(1);
|
||||
expect(user.purchased.plan.cumulativeCount).to.equal(3);
|
||||
});
|
||||
|
||||
it('does not award unearned plan.consecutive.trinkets if subscription ended during an absence', async () => {
|
||||
@@ -185,12 +194,12 @@ describe('cron', async () => {
|
||||
});
|
||||
|
||||
it('does not increment plan.consecutive.gemCapExtra when user has reached the gemCap limit', async () => {
|
||||
user.purchased.plan.consecutive.gemCapExtra = 25;
|
||||
user.purchased.plan.consecutive.gemCapExtra = 26;
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(25);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(26);
|
||||
});
|
||||
|
||||
it('does not reset plan stats if we are before the last day of the cancelled month', async () => {
|
||||
@@ -205,16 +214,14 @@ describe('cron', async () => {
|
||||
user.purchased.plan.dateTerminated = moment(new Date()).subtract({ days: 1 });
|
||||
user.purchased.plan.consecutive.gemCapExtra = 20;
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
user.purchased.plan.consecutive.offset = 1;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.purchased.plan.customerId).to.not.exist;
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(0);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(20);
|
||||
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', async () => {
|
||||
@@ -236,13 +243,11 @@ describe('cron', async () => {
|
||||
user1.purchased.plan.dateUpdated = moment().toDate();
|
||||
user1.purchased.plan.planId = 'basic';
|
||||
user1.purchased.plan.consecutive.count = 0;
|
||||
user1.purchased.plan.perkMonthCount = 0;
|
||||
user1.purchased.plan.consecutive.offset = 0;
|
||||
user1.purchased.plan.consecutive.trinkets = 0;
|
||||
user1.purchased.plan.consecutive.trinkets = 1;
|
||||
user1.purchased.plan.consecutive.gemCapExtra = 0;
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits after the first month', async () => {
|
||||
it('increments consecutive benefits', async () => {
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
@@ -253,75 +258,8 @@ describe('cron', async () => {
|
||||
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', async () => {
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(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.
|
||||
await 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 second month if they also received a 1 month gift subscription', async () => {
|
||||
user1.purchased.plan.perkMonthCount = 1;
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(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.
|
||||
await cron({
|
||||
user: user1, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user1.purchased.plan.perkMonthCount).to.equal(0);
|
||||
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(1);
|
||||
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(5);
|
||||
});
|
||||
|
||||
it('increments consecutive benefits after the third month', async () => {
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(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.
|
||||
await 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', async () => {
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(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.
|
||||
await 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);
|
||||
expect(user1.purchased.plan.consecutive.trinkets).to.equal(2);
|
||||
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(2);
|
||||
});
|
||||
|
||||
it('increments consecutive benefits correctly if user has been absent with continuous subscription', async () => {
|
||||
@@ -332,33 +270,8 @@ describe('cron', async () => {
|
||||
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);
|
||||
});
|
||||
|
||||
it('initializes plan.perkMonthCount if necessary', async () => {
|
||||
user.purchased.plan.perkMonthCount = undefined;
|
||||
clock = sinon.useFakeTimers(moment(user.purchased.plan.dateUpdated)
|
||||
.utcOffset(0)
|
||||
.startOf('month')
|
||||
.add(1, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.perkMonthCount).to.equal(1);
|
||||
user.purchased.plan.perkMonthCount = undefined;
|
||||
user.purchased.plan.consecutive.count = 8;
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(2, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.perkMonthCount).to.equal(2);
|
||||
expect(user1.purchased.plan.consecutive.trinkets).to.equal(11);
|
||||
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(20);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -379,14 +292,12 @@ describe('cron', async () => {
|
||||
user3.purchased.plan.customerId = 'subscribedId';
|
||||
user3.purchased.plan.dateUpdated = moment().toDate();
|
||||
user3.purchased.plan.planId = 'basic_3mo';
|
||||
user3.purchased.plan.perkMonthCount = 0;
|
||||
user3.purchased.plan.consecutive.count = 0;
|
||||
user3.purchased.plan.consecutive.offset = 3;
|
||||
user3.purchased.plan.consecutive.trinkets = 1;
|
||||
user3.purchased.plan.consecutive.gemCapExtra = 5;
|
||||
user3.purchased.plan.consecutive.gemCapExtra = 0;
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', async () => {
|
||||
it('increments consecutive benefits', async () => {
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
@@ -394,102 +305,8 @@ describe('cron', async () => {
|
||||
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', async () => {
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(2, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await 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', async () => {
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(3, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await 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', async () => {
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(4, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await 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('keeps existing plan.perkMonthCount intact when incrementing consecutive benefits', async () => {
|
||||
user3.purchased.plan.perkMonthCount = 2;
|
||||
user3.purchased.plan.consecutive.trinkets = 1;
|
||||
user3.purchased.plan.consecutive.gemCapExtra = 5;
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(4, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user3, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user3.purchased.plan.perkMonthCount).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', async () => {
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(5, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await 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', async () => {
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(6, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await 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', async () => {
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(7, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await 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);
|
||||
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(2);
|
||||
});
|
||||
|
||||
it('increments consecutive benefits correctly if user has been absent with continuous subscription', async () => {
|
||||
@@ -500,8 +317,7 @@ describe('cron', async () => {
|
||||
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.trinkets).to.equal(11);
|
||||
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(20);
|
||||
});
|
||||
});
|
||||
@@ -523,14 +339,12 @@ describe('cron', async () => {
|
||||
user6.purchased.plan.customerId = 'subscribedId';
|
||||
user6.purchased.plan.dateUpdated = moment().toDate();
|
||||
user6.purchased.plan.planId = 'google_6mo';
|
||||
user6.purchased.plan.perkMonthCount = 0;
|
||||
user6.purchased.plan.consecutive.count = 0;
|
||||
user6.purchased.plan.consecutive.offset = 6;
|
||||
user6.purchased.plan.consecutive.trinkets = 2;
|
||||
user6.purchased.plan.consecutive.gemCapExtra = 10;
|
||||
user6.purchased.plan.consecutive.trinkets = 1;
|
||||
user6.purchased.plan.consecutive.gemCapExtra = 0;
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', async () => {
|
||||
it('increments benefits', async () => {
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
@@ -538,74 +352,8 @@ describe('cron', async () => {
|
||||
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', async () => {
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(6, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await 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', async () => {
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(7, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await 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('keeps existing plan.perkMonthCount intact when incrementing consecutive benefits', async () => {
|
||||
user6.purchased.plan.perkMonthCount = 2;
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(7, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user6, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user6.purchased.plan.perkMonthCount).to.equal(2);
|
||||
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', async () => {
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(13, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await 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', async () => {
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(19, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await 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);
|
||||
expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -626,11 +374,10 @@ describe('cron', async () => {
|
||||
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;
|
||||
user12.purchased.plan.consecutive.trinkets = 1;
|
||||
user12.purchased.plan.consecutive.gemCapExtra = 26;
|
||||
|
||||
it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', async () => {
|
||||
it('increments consecutive benefits the month after the second paid period has started', async () => {
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
@@ -638,61 +385,20 @@ describe('cron', async () => {
|
||||
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', async () => {
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(12, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await 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', async () => {
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(13, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await 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', async () => {
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(25, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await 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);
|
||||
expect(user12.purchased.plan.consecutive.trinkets).to.equal(2);
|
||||
expect(user12.purchased.plan.consecutive.gemCapExtra).to.equal(26);
|
||||
});
|
||||
|
||||
it('increments consecutive benefits correctly if user has been absent with continuous subscription', async () => {
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(37, 'months')
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(10, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user12, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user12.purchased.plan.consecutive.count).to.equal(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);
|
||||
expect(user12.purchased.plan.consecutive.count).to.equal(10);
|
||||
expect(user12.purchased.plan.consecutive.trinkets).to.equal(11);
|
||||
expect(user12.purchased.plan.consecutive.gemCapExtra).to.equal(26);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -715,11 +421,11 @@ describe('cron', async () => {
|
||||
.toDate();
|
||||
user3g.purchased.plan.planId = null;
|
||||
user3g.purchased.plan.consecutive.count = 0;
|
||||
user3g.purchased.plan.consecutive.offset = 3;
|
||||
user3g.purchased.plan.cumulativeCount = 0;
|
||||
user3g.purchased.plan.consecutive.trinkets = 1;
|
||||
user3g.purchased.plan.consecutive.gemCapExtra = 5;
|
||||
user3g.purchased.plan.consecutive.gemCapExtra = 0;
|
||||
|
||||
it('does not increment consecutive benefits in the first month of the gift subscription', async () => {
|
||||
it('increments benefits', async () => {
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
@@ -727,35 +433,9 @@ describe('cron', async () => {
|
||||
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', async () => {
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(2, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await 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', async () => {
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(3, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await 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);
|
||||
expect(user3g.purchased.plan.cumulativeCount).to.equal(1);
|
||||
expect(user3g.purchased.plan.consecutive.trinkets).to.equal(2);
|
||||
expect(user3g.purchased.plan.consecutive.gemCapExtra).to.equal(2);
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the month after the gift subscription has ended', async () => {
|
||||
@@ -767,84 +447,9 @@ describe('cron', async () => {
|
||||
});
|
||||
// subscription has been erased by now
|
||||
expect(user3g.purchased.plan.consecutive.count).to.equal(0);
|
||||
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', async () => {
|
||||
const 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', async () => {
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await 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', async () => {
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(2, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await 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', async () => {
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(3, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await 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', async () => {
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(7, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await 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);
|
||||
expect(user3g.purchased.plan.consecutive.trinkets).to.equal(2);
|
||||
expect(user3g.purchased.plan.consecutive.gemCapExtra).to.equal(2);
|
||||
expect(user3g.purchased.plan.cumulativeCount).to.equal(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -888,12 +493,12 @@ describe('cron', async () => {
|
||||
expect(user.purchased.plan.consecutive.count).to.equal(0);
|
||||
});
|
||||
|
||||
it('does not decrement plan.consecutive.offset when offset is greater than 0', async () => {
|
||||
user.purchased.plan.consecutive.offset = 1;
|
||||
it('does not increment plan.cumulativeCount', async () => {
|
||||
user.purchased.plan.cumulativeCount = 0;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.consecutive.offset).to.equal(1);
|
||||
expect(user.purchased.plan.cumulativeCount).to.equal(0);
|
||||
});
|
||||
|
||||
it('does not increment plan.consecutive.trinkets when user has reached a month that is a multiple of 3', async () => {
|
||||
@@ -913,12 +518,12 @@ describe('cron', async () => {
|
||||
});
|
||||
|
||||
it('does not increment plan.consecutive.gemCapExtra when user has reached the gemCap limit', async () => {
|
||||
user.purchased.plan.consecutive.gemCapExtra = 25;
|
||||
user.purchased.plan.consecutive.gemCapExtra = 26;
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(25);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(26);
|
||||
});
|
||||
|
||||
it('does nothing to plan stats if we are before the last day of the cancelled month', async () => {
|
||||
@@ -928,22 +533,6 @@ describe('cron', async () => {
|
||||
});
|
||||
expect(user.purchased.plan.customerId).to.not.exist;
|
||||
});
|
||||
|
||||
xit('does nothing to plan stats when we are after the last day of the cancelled month', async () => {
|
||||
user.purchased.plan.dateTerminated = moment(new Date()).subtract({ days: 1 });
|
||||
user.purchased.plan.consecutive.gemCapExtra = 20;
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
user.purchased.plan.consecutive.offset = 1;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.purchased.plan.customerId).to.exist;
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.exist;
|
||||
expect(user.purchased.plan.consecutive.count).to.exist;
|
||||
expect(user.purchased.plan.consecutive.offset).to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
describe('todos', async () => {
|
||||
|
||||
@@ -117,7 +117,7 @@ describe('Items Utils', () => {
|
||||
it('converts values for owned gear to true/false', () => {
|
||||
expect(castItemVal('items.gear.owned.shield_warrior_0', 'true')).to.equal(true);
|
||||
expect(castItemVal('items.gear.owned.invalid', 'false')).to.equal(false);
|
||||
expect(castItemVal('items.gear.owned.invalid', 'null')).to.equal(false);
|
||||
expect(castItemVal('items.gear.owned.invalid', 'null')).to.equal(undefined);
|
||||
expect(castItemVal('items.gear.owned.invalid', 'truthy')).to.equal(true);
|
||||
expect(castItemVal('items.gear.owned.invalid', 0)).to.equal(false);
|
||||
});
|
||||
|
||||
@@ -715,7 +715,7 @@ describe('Purchasing a group plan for group', () => {
|
||||
const mysteryItem = { title: 'item' };
|
||||
const mysteryItems = [mysteryItem];
|
||||
const consecutive = {
|
||||
trinkets: 3,
|
||||
trinkets: 4,
|
||||
gemCapExtra: 20,
|
||||
offset: 1,
|
||||
count: 13,
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from '../../../../helpers/api-unit.helper';
|
||||
import * as worldState from '../../../../../website/server/libs/worldState';
|
||||
import { TransactionModel } from '../../../../../website/server/models/transaction';
|
||||
import { REPEATING_EVENTS } from '../../../../../website/common/script/content/constants/events';
|
||||
|
||||
describe('payments/index', () => {
|
||||
let user;
|
||||
@@ -65,7 +66,6 @@ describe('payments/index', () => {
|
||||
mysteryItems: [],
|
||||
consecutive: {
|
||||
trinkets: 0,
|
||||
offset: 0,
|
||||
gemCapExtra: 0,
|
||||
},
|
||||
};
|
||||
@@ -108,14 +108,8 @@ describe('payments/index', () => {
|
||||
});
|
||||
|
||||
it('add a transaction entry to the recipient', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
|
||||
expect(recipient.purchased.plan.extraMonths).to.eql(0);
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.extraMonths).to.eql(3);
|
||||
|
||||
const transactions = await TransactionModel
|
||||
.find({ userId: recipient._id })
|
||||
.sort({ createdAt: -1 })
|
||||
@@ -177,6 +171,45 @@ describe('payments/index', () => {
|
||||
expect(recipient.purchased.plan.dateUpdated).to.exist;
|
||||
});
|
||||
|
||||
it('does not reset gemCapExtra if they already had one', async () => {
|
||||
recipient.purchased.plan.consecutive.gemCapExtra = 10;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(10);
|
||||
});
|
||||
|
||||
it('sets gemCapExtra to 0 if they receive a 3 month sub', async () => {
|
||||
data.gift.subscription.key = 'basic_3mo';
|
||||
data.gift.subscription.months = 3;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
});
|
||||
|
||||
it('sets gemCapExtra to max if they receive a 12 month sub', async () => {
|
||||
recipient.purchased.plan.consecutive.gemCapExtra = 10;
|
||||
|
||||
data.gift.subscription.key = 'basic_12mo';
|
||||
data.gift.subscription.months = 12;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(26);
|
||||
});
|
||||
|
||||
it('gives user 1 hourglass if they have no active subscription', async () => {
|
||||
await api.createSubscription(data);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
});
|
||||
|
||||
it('does not give any hourglasses if they have an active subscription', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
await api.createSubscription(data);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(plan.consecutive.trinkets);
|
||||
});
|
||||
|
||||
it('sets plan.dateUpdated if it did exist but the user has cancelled', async () => {
|
||||
recipient.purchased.plan.dateUpdated = moment().subtract(1, 'days').toDate();
|
||||
recipient.purchased.plan.dateTerminated = moment().subtract(1, 'days').toDate();
|
||||
@@ -235,116 +268,6 @@ describe('payments/index', () => {
|
||||
expect(recipient.purchased.plan.customerId).to.eql('customer-id');
|
||||
});
|
||||
|
||||
it('sets plan.perkMonthCount to 1 if user is not subscribed', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.purchased.plan.perkMonthCount = 1;
|
||||
recipient.purchased.plan.customerId = undefined;
|
||||
data.sub.key = 'basic_earned';
|
||||
data.gift.subscription.key = 'basic_earned';
|
||||
data.gift.subscription.months = 1;
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
|
||||
});
|
||||
|
||||
it('sets plan.perkMonthCount to 1 if field is not initialized', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.purchased.plan.perkMonthCount = -1;
|
||||
recipient.purchased.plan.customerId = undefined;
|
||||
data.sub.key = 'basic_earned';
|
||||
data.gift.subscription.key = 'basic_earned';
|
||||
data.gift.subscription.months = 1;
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(-1);
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
|
||||
});
|
||||
|
||||
it('sets plan.perkMonthCount to 1 if user had previous count but lapsed subscription', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.purchased.plan.perkMonthCount = 2;
|
||||
recipient.purchased.plan.customerId = undefined;
|
||||
data.sub.key = 'basic_earned';
|
||||
data.gift.subscription.key = 'basic_earned';
|
||||
data.gift.subscription.months = 1;
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
|
||||
});
|
||||
|
||||
it('adds to plan.perkMonthCount if user is already subscribed', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.purchased.plan.perkMonthCount = 1;
|
||||
data.sub.key = 'basic_earned';
|
||||
data.gift.subscription.key = 'basic_earned';
|
||||
data.gift.subscription.months = 1;
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
|
||||
});
|
||||
|
||||
it('awards perks if plan.perkMonthCount reaches 3 with existing subscription', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.purchased.plan.perkMonthCount = 2;
|
||||
data.sub.key = 'basic_earned';
|
||||
data.gift.subscription.key = 'basic_earned';
|
||||
data.gift.subscription.months = 1;
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(0);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
||||
});
|
||||
|
||||
it('awards perks if plan.perkMonthCount reaches 3 without existing subscription', async () => {
|
||||
recipient.purchased.plan.perkMonthCount = 0;
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(0);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(0);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
||||
});
|
||||
|
||||
it('awards perks if plan.perkMonthCount reaches 3 without initialized field', async () => {
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(-1);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(0);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
||||
});
|
||||
|
||||
it('awards perks if plan.perkMonthCount goes over 3', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.purchased.plan.perkMonthCount = 2;
|
||||
data.sub.key = 'basic_earned';
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
||||
});
|
||||
|
||||
it('sets plan.customerId to "Gift" if it does not already exist', async () => {
|
||||
expect(recipient.purchased.plan.customerId).to.not.exist;
|
||||
|
||||
@@ -421,8 +344,8 @@ describe('payments/index', () => {
|
||||
context('Active Promotion', () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub(worldState, 'getCurrentEventList').returns([{
|
||||
...common.content.events.winter2021Promo,
|
||||
event: 'winter2021',
|
||||
...REPEATING_EVENTS.giftOneGetOne,
|
||||
event: 'g1g1',
|
||||
}]);
|
||||
});
|
||||
|
||||
@@ -438,22 +361,30 @@ describe('payments/index', () => {
|
||||
expect(user.purchased.plan.dateTerminated).to.exist;
|
||||
expect(user.purchased.plan.dateUpdated).to.exist;
|
||||
expect(user.purchased.plan.dateCreated).to.exist;
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
|
||||
expect(recipient.items.pets['Jackalope-RoyalPurple']).to.eql(5);
|
||||
expect(recipient.purchased.plan.customerId).to.eql('Gift');
|
||||
expect(recipient.purchased.plan.dateTerminated).to.exist;
|
||||
expect(recipient.purchased.plan.dateUpdated).to.exist;
|
||||
expect(recipient.purchased.plan.dateCreated).to.exist;
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
});
|
||||
|
||||
it('adds extraMonths to existing subscription for purchaser and creates a gift subscription for recipient without sub', async () => {
|
||||
user.purchased.plan = plan;
|
||||
|
||||
expect(user.purchased.plan.extraMonths).to.eql(0);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.extraMonths).to.eql(3);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
|
||||
expect(recipient.items.pets['Jackalope-RoyalPurple']).to.eql(5);
|
||||
expect(recipient.purchased.plan.customerId).to.eql('Gift');
|
||||
@@ -466,10 +397,12 @@ describe('payments/index', () => {
|
||||
recipient.purchased.plan = plan;
|
||||
|
||||
expect(recipient.purchased.plan.extraMonths).to.eql(0);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.extraMonths).to.eql(3);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
|
||||
expect(user.items.pets['Jackalope-RoyalPurple']).to.eql(5);
|
||||
expect(user.purchased.plan.customerId).to.eql('Gift');
|
||||
@@ -484,11 +417,15 @@ describe('payments/index', () => {
|
||||
|
||||
expect(user.purchased.plan.extraMonths).to.eql(0);
|
||||
expect(recipient.purchased.plan.extraMonths).to.eql(0);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.extraMonths).to.eql(3);
|
||||
expect(recipient.purchased.plan.extraMonths).to.eql(3);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
});
|
||||
|
||||
it('sends a private message about the promotion', async () => {
|
||||
@@ -511,7 +448,6 @@ describe('payments/index', () => {
|
||||
expect(user.purchased.plan.customerId).to.eql('customer-id');
|
||||
expect(user.purchased.plan.dateUpdated).to.exist;
|
||||
expect(user.purchased.plan.gemsBought).to.eql(0);
|
||||
expect(user.purchased.plan.perkMonthCount).to.eql(0);
|
||||
expect(user.purchased.plan.paymentMethod).to.eql('Payment Method');
|
||||
expect(user.purchased.plan.extraMonths).to.eql(0);
|
||||
expect(user.purchased.plan.dateTerminated).to.eql(null);
|
||||
@@ -549,33 +485,6 @@ describe('payments/index', () => {
|
||||
expect(user.purchased.plan.dateCurrentTypeCreated).to.not.eql(initialDate);
|
||||
});
|
||||
|
||||
it('keeps plan.perkMonthCount when changing subscription type', async () => {
|
||||
await api.createSubscription(data);
|
||||
user.purchased.plan.perkMonthCount = 2;
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.perkMonthCount).to.eql(2);
|
||||
});
|
||||
|
||||
it('sets plan.perkMonthCount to zero when creating new monthly subscription', async () => {
|
||||
user.purchased.plan.perkMonthCount = 2;
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.perkMonthCount).to.eql(0);
|
||||
});
|
||||
|
||||
it('sets plan.perkMonthCount to zero when creating new 3 month subscription', async () => {
|
||||
user.purchased.plan.perkMonthCount = 2;
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.perkMonthCount).to.eql(0);
|
||||
});
|
||||
|
||||
it('updates plan.consecutive.offset when changing subscription type', async () => {
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.consecutive.offset).to.eql(3);
|
||||
data.sub.key = 'basic_6mo';
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.consecutive.offset).to.eql(6);
|
||||
});
|
||||
|
||||
it('awards the Royal Purple Jackalope pet', async () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
@@ -694,6 +603,7 @@ describe('payments/index', () => {
|
||||
expect(user.purchased.plan.dateCreated).to.eql(created);
|
||||
expect(user.purchased.plan.dateUpdated).to.not.eql(updated);
|
||||
expect(user.purchased.plan.customerId).to.eql('customer-id');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -741,55 +651,20 @@ describe('payments/index', () => {
|
||||
});
|
||||
|
||||
context('Block subscription perks', () => {
|
||||
it('adds block months to plan.consecutive.offset', async () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.offset).to.eql(3);
|
||||
});
|
||||
|
||||
it('does not add to plans.consecutive.offset if 1 month subscription', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.offset).to.eql(0);
|
||||
});
|
||||
|
||||
it('resets plans.consecutive.offset if 1 month subscription', async () => {
|
||||
user.purchased.plan.consecutive.offset = 1;
|
||||
await user.save();
|
||||
data.sub.key = 'basic_earned';
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.offset).to.eql(0);
|
||||
});
|
||||
|
||||
it('adds 5 to plan.consecutive.gemCapExtra for 3 month block', async () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
||||
});
|
||||
|
||||
it('adds 10 to plan.consecutive.gemCapExtra for 6 month block', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
|
||||
});
|
||||
|
||||
it('adds 20 to plan.consecutive.gemCapExtra for 12 month block', async () => {
|
||||
it('adds 26 to plan.consecutive.gemCapExtra for 12 month block', async () => {
|
||||
data.sub.key = 'basic_12mo';
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
|
||||
});
|
||||
|
||||
it('does not raise plan.consecutive.gemCapExtra higher than 25', async () => {
|
||||
it('does not raise plan.consecutive.gemCapExtra higher than 26', async () => {
|
||||
data.sub.key = 'basic_12mo';
|
||||
|
||||
await api.createSubscription(data);
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(25);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
|
||||
});
|
||||
|
||||
it('adds a plan.consecutive.trinkets for 3 month block', async () => {
|
||||
@@ -798,20 +673,29 @@ describe('payments/index', () => {
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
});
|
||||
|
||||
it('adds 2 plan.consecutive.trinkets for 6 month block', async () => {
|
||||
it('adds 1 plan.consecutive.trinkets for 6 month block', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
});
|
||||
|
||||
it('adds 4 plan.consecutive.trinkets for 12 month block', async () => {
|
||||
it('adds 1 plan.consecutive.trinkets for 12 month block if they had promo', async () => {
|
||||
user.purchased.plan.hourglassPromoReceived = new Date();
|
||||
data.sub.key = 'basic_12mo';
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
});
|
||||
|
||||
it('adds 12 plan.consecutive.trinkets for 12 month block', async () => {
|
||||
data.sub.key = 'basic_12mo';
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||
});
|
||||
|
||||
context('Upgrades subscription', () => {
|
||||
@@ -819,70 +703,38 @@ describe('payments/index', () => {
|
||||
beforeEach(async () => {
|
||||
data.updatedFrom = { logic: 'payDifference' };
|
||||
});
|
||||
it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
|
||||
data.sub.key = 'basic_6mo';
|
||||
data.updatedFrom.key = 'basic_earned';
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
|
||||
});
|
||||
|
||||
it('Adds 15 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
it('Adds 26 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_3mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_3mo';
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
|
||||
});
|
||||
|
||||
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
|
||||
data.sub.key = 'basic_6mo';
|
||||
data.updatedFrom.key = 'basic_earned';
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
});
|
||||
|
||||
it('Adds 2 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
|
||||
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_6mo';
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||
});
|
||||
|
||||
it('Adds 3 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
@@ -894,7 +746,7 @@ describe('payments/index', () => {
|
||||
data.updatedFrom.key = 'basic_3mo';
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -902,70 +754,39 @@ describe('payments/index', () => {
|
||||
beforeEach(async () => {
|
||||
data.updatedFrom = { logic: 'payFull' };
|
||||
});
|
||||
it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
|
||||
data.sub.key = 'basic_6mo';
|
||||
data.updatedFrom.key = 'basic_earned';
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
|
||||
});
|
||||
|
||||
it('Adds 20 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
it('Adds 26 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_3mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_3mo';
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(25);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
|
||||
});
|
||||
|
||||
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
|
||||
data.sub.key = 'basic_6mo';
|
||||
data.updatedFrom.key = 'basic_earned';
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
});
|
||||
|
||||
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
|
||||
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_6mo';
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(6);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||
});
|
||||
|
||||
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
@@ -977,7 +798,7 @@ describe('payments/index', () => {
|
||||
data.updatedFrom.key = 'basic_3mo';
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(5);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -988,30 +809,13 @@ describe('payments/index', () => {
|
||||
data.updatedFrom = { logic: 'refundAndRepay' };
|
||||
});
|
||||
context('Upgrades within first half of subscription', () => {
|
||||
it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
|
||||
data.sub.key = 'basic_6mo';
|
||||
data.updatedFrom.key = 'basic_earned';
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2022-01-10'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
|
||||
});
|
||||
|
||||
it('Adds 15 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
it('Adds 26 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_3mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_3mo';
|
||||
@@ -1019,28 +823,10 @@ describe('payments/index', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2022-02-05'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
|
||||
});
|
||||
|
||||
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
|
||||
data.sub.key = 'basic_6mo';
|
||||
data.updatedFrom.key = 'basic_earned';
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2022-01-08'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
});
|
||||
|
||||
it('Adds 3 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
@@ -1054,17 +840,17 @@ describe('payments/index', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2022-01-31'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||
});
|
||||
|
||||
it('Adds 2 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
|
||||
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_6mo';
|
||||
@@ -1072,35 +858,17 @@ describe('payments/index', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2022-01-28'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||
});
|
||||
|
||||
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo after initial cycle', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
|
||||
data.sub.key = 'basic_6mo';
|
||||
data.updatedFrom.key = 'basic_earned';
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-08'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
});
|
||||
|
||||
it('Adds 2 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo after initial cycle', async () => {
|
||||
it('2 plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo after initial cycle', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_6mo';
|
||||
@@ -1108,10 +876,10 @@ describe('payments/index', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2022-08-28'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||
});
|
||||
|
||||
it('Adds 3 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo after initial cycle', async () => {
|
||||
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo after initial cycle', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
@@ -1125,11 +893,11 @@ describe('payments/index', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2022-07-31'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||
});
|
||||
});
|
||||
context('Upgrades within second half of subscription', () => {
|
||||
it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
|
||||
it('Adds 0 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
@@ -1144,16 +912,16 @@ describe('payments/index', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2022-01-20'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
});
|
||||
|
||||
it('Adds 20 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
it('Adds 26 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_3mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_3mo';
|
||||
@@ -1161,17 +929,17 @@ describe('payments/index', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2022-02-24'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(25);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
|
||||
});
|
||||
|
||||
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
|
||||
it('Adds 0 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
|
||||
data.sub.key = 'basic_6mo';
|
||||
data.updatedFrom.key = 'basic_earned';
|
||||
@@ -1179,17 +947,17 @@ describe('payments/index', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2022-01-28'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
});
|
||||
|
||||
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
|
||||
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_6mo';
|
||||
@@ -1197,10 +965,10 @@ describe('payments/index', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2022-05-28'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(6);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||
});
|
||||
|
||||
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
@@ -1214,17 +982,17 @@ describe('payments/index', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2022-03-03'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(5);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||
});
|
||||
|
||||
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo after initial cycle', async () => {
|
||||
it('Adds 0 to plan.consecutive.trinkets from basic_earned to basic_6mo after initial cycle', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
|
||||
data.sub.key = 'basic_6mo';
|
||||
data.updatedFrom.key = 'basic_earned';
|
||||
@@ -1232,17 +1000,17 @@ describe('payments/index', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2022-05-28'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
});
|
||||
|
||||
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo after initial cycle', async () => {
|
||||
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo after initial cycle', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_6mo';
|
||||
@@ -1250,10 +1018,10 @@ describe('payments/index', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2023-05-28'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(6);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||
});
|
||||
|
||||
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo after initial cycle', async () => {
|
||||
it('Adds 12 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo after initial cycle', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
@@ -1267,7 +1035,7 @@ describe('payments/index', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2023-09-03'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(5);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||
});
|
||||
});
|
||||
afterEach(async () => {
|
||||
@@ -1277,22 +1045,6 @@ describe('payments/index', () => {
|
||||
});
|
||||
|
||||
context('Downgrades subscription', () => {
|
||||
it('does not remove from plan.consecutive.gemCapExtra from basic_6mo to basic_earned', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
|
||||
|
||||
data.sub.key = 'basic_earned';
|
||||
data.updatedFrom = { key: 'basic_6mo' };
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
|
||||
});
|
||||
|
||||
it('does not remove from plan.consecutive.gemCapExtra from basic_12mo to basic_3mo', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
@@ -1300,28 +1052,12 @@ describe('payments/index', () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
|
||||
|
||||
data.sub.key = 'basic_3mo';
|
||||
data.updatedFrom = { key: 'basic_12mo' };
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
|
||||
});
|
||||
|
||||
it('does not remove from plan.consecutive.trinkets from basic_6mo to basic_earned', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
|
||||
data.sub.key = 'basic_earned';
|
||||
data.updatedFrom = { key: 'basic_6mo' };
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(26);
|
||||
});
|
||||
|
||||
it('does not remove from plan.consecutive.trinkets from basic_12mo to basic_3mo', async () => {
|
||||
@@ -1331,12 +1067,12 @@ describe('payments/index', () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||
|
||||
data.sub.key = 'basic_3mo';
|
||||
data.updatedFrom = { key: 'basic_12mo' };
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(13);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1453,6 +1189,32 @@ describe('payments/index', () => {
|
||||
expect(user.purchased.plan.extraMonths).to.eql(0);
|
||||
});
|
||||
|
||||
it('does not reset gemCapExtra', async () => {
|
||||
user.purchased.plan.consecutive.gemCapExtra = 12;
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(12);
|
||||
});
|
||||
|
||||
it('initializes gemCapExtra', async () => {
|
||||
await api.cancelSubscription(data);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
});
|
||||
|
||||
it('initializes hourglasses', async () => {
|
||||
await api.cancelSubscription(data);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
});
|
||||
|
||||
it('does not reset owned hourglasses', async () => {
|
||||
user.purchased.plan.consecutive.trinkets = 12;
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(12);
|
||||
});
|
||||
|
||||
it('sends an email', async () => {
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ describe('Stripe - Checkout', () => {
|
||||
gift: undefined,
|
||||
sub: undefined,
|
||||
gemsBlock: gemsBlockKey,
|
||||
server_url: BASE_URL,
|
||||
};
|
||||
|
||||
expect(gems.validateGiftMessage).to.not.be.called;
|
||||
@@ -101,6 +102,7 @@ describe('Stripe - Checkout', () => {
|
||||
gift: JSON.stringify(gift),
|
||||
sub: undefined,
|
||||
gemsBlock: undefined,
|
||||
server_url: BASE_URL,
|
||||
};
|
||||
|
||||
expect(gems.validateGiftMessage).to.be.calledOnce;
|
||||
@@ -155,6 +157,7 @@ describe('Stripe - Checkout', () => {
|
||||
gift: JSON.stringify(gift),
|
||||
sub: undefined,
|
||||
gemsBlock: undefined,
|
||||
server_url: BASE_URL,
|
||||
};
|
||||
|
||||
expect(oneTimePayments.getOneTimePaymentInfo).to.be.calledOnce;
|
||||
@@ -192,6 +195,7 @@ describe('Stripe - Checkout', () => {
|
||||
userId: user._id,
|
||||
gift: undefined,
|
||||
sub: JSON.stringify(sub),
|
||||
server_url: BASE_URL,
|
||||
};
|
||||
|
||||
expect(subscriptions.checkSubData).to.be.calledOnce;
|
||||
@@ -258,6 +262,7 @@ describe('Stripe - Checkout', () => {
|
||||
userId: user._id,
|
||||
gift: undefined,
|
||||
sub: JSON.stringify(sub),
|
||||
server_url: BASE_URL,
|
||||
groupId,
|
||||
};
|
||||
|
||||
@@ -328,8 +333,9 @@ describe('Stripe - Checkout', () => {
|
||||
user.purchased.plan.customerId = customerId;
|
||||
|
||||
const metadata = {
|
||||
userId: user._id,
|
||||
type: 'edit-card-user',
|
||||
userId: user._id,
|
||||
server_url: BASE_URL,
|
||||
};
|
||||
|
||||
const res = await createEditCardCheckoutSession({ user }, stripe);
|
||||
@@ -418,6 +424,7 @@ describe('Stripe - Checkout', () => {
|
||||
const metadata = {
|
||||
userId: user._id,
|
||||
type: 'edit-card-group',
|
||||
server_url: BASE_URL,
|
||||
groupId,
|
||||
};
|
||||
|
||||
@@ -455,6 +462,7 @@ describe('Stripe - Checkout', () => {
|
||||
userId: anotherUser._id,
|
||||
type: 'edit-card-group',
|
||||
groupId,
|
||||
server_url: BASE_URL,
|
||||
};
|
||||
|
||||
const res = await createEditCardCheckoutSession({ user: anotherUser, groupId }, stripe);
|
||||
|
||||
@@ -308,6 +308,7 @@ describe('Stripe - One Time Payments', () => {
|
||||
customerId,
|
||||
paymentMethod: 'Gift',
|
||||
gift,
|
||||
autoRenews: false,
|
||||
gemsBlock: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -173,6 +173,7 @@ describe('Stripe - Subscriptions', () => {
|
||||
paymentMethod: 'Stripe',
|
||||
sub: sinon.match({ ...sub }),
|
||||
groupId: null,
|
||||
autoRenews: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -197,6 +198,7 @@ describe('Stripe - Subscriptions', () => {
|
||||
paymentMethod: 'Stripe',
|
||||
sub: sinon.match({ ...sub }),
|
||||
groupId,
|
||||
autoRenews: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -231,6 +233,7 @@ describe('Stripe - Subscriptions', () => {
|
||||
paymentMethod: 'Stripe',
|
||||
sub: sinon.match({ ...sub }),
|
||||
groupId,
|
||||
autoRenews: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ import * as subscriptions from '../../../../../../website/server/libs/payments/s
|
||||
const { i18n } = common;
|
||||
|
||||
describe('Stripe - Webhooks', () => {
|
||||
const BASE_URL = nconf.get('BASE_URL');
|
||||
const stripe = stripeModule('test');
|
||||
const endpointSecret = nconf.get('STRIPE_WEBHOOKS_ENDPOINT_SECRET');
|
||||
const headers = {};
|
||||
@@ -284,7 +285,9 @@ describe('Stripe - Webhooks', () => {
|
||||
const session = {};
|
||||
|
||||
beforeEach(() => {
|
||||
session.metadata = {};
|
||||
session.metadata = {
|
||||
server_url: BASE_URL,
|
||||
};
|
||||
event = { type: eventType, data: { object: session } };
|
||||
constructEventStub = sandbox.stub(stripe.webhooks, 'constructEvent');
|
||||
constructEventStub.returns(event);
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
import apn from '@parse/node-apn/mock';
|
||||
import _ from 'lodash';
|
||||
import nconf from 'nconf';
|
||||
import gcmLib from 'node-gcm'; // works with FCM notifications too
|
||||
import { model as User } from '../../../../website/server/models/user';
|
||||
import {
|
||||
sendNotification as sendPushNotification,
|
||||
MAX_MESSAGE_LENGTH,
|
||||
} from '../../../../website/server/libs/pushNotifications';
|
||||
|
||||
describe('pushNotifications', () => {
|
||||
let user;
|
||||
let fcmSendSpy;
|
||||
let apnSendSpy;
|
||||
|
||||
const identifier = 'identifier';
|
||||
const title = 'title';
|
||||
const message = 'message';
|
||||
|
||||
beforeEach(() => {
|
||||
user = new User();
|
||||
fcmSendSpy = sinon.spy();
|
||||
apnSendSpy = sinon.spy();
|
||||
|
||||
sandbox.stub(nconf, 'get').returns('true-key');
|
||||
|
||||
sandbox.stub(gcmLib.Sender.prototype, 'send').callsFake(fcmSendSpy);
|
||||
|
||||
sandbox.stub(apn.Provider.prototype, 'send').returns({
|
||||
on: () => null,
|
||||
send: apnSendSpy,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('throws if user is not supplied', () => {
|
||||
expect(sendPushNotification).to.throw;
|
||||
expect(fcmSendSpy).to.not.have.been.called;
|
||||
expect(apnSendSpy).to.not.have.been.called;
|
||||
});
|
||||
|
||||
it('throws if user.preferences.pushNotifications.unsubscribeFromAll is true', () => {
|
||||
user.preferences.pushNotifications.unsubscribeFromAll = true;
|
||||
expect(() => sendPushNotification(user)).to.throw;
|
||||
expect(fcmSendSpy).to.not.have.been.called;
|
||||
expect(apnSendSpy).to.not.have.been.called;
|
||||
});
|
||||
|
||||
it('throws if details.identifier is not supplied', () => {
|
||||
expect(() => sendPushNotification(user, {
|
||||
title,
|
||||
message,
|
||||
})).to.throw;
|
||||
expect(fcmSendSpy).to.not.have.been.called;
|
||||
expect(apnSendSpy).to.not.have.been.called;
|
||||
});
|
||||
|
||||
it('throws if details.title is not supplied', () => {
|
||||
expect(() => sendPushNotification(user, {
|
||||
identifier,
|
||||
message,
|
||||
})).to.throw;
|
||||
expect(fcmSendSpy).to.not.have.been.called;
|
||||
expect(apnSendSpy).to.not.have.been.called;
|
||||
});
|
||||
|
||||
it('throws if details.message is not supplied', () => {
|
||||
expect(() => sendPushNotification(user, {
|
||||
identifier,
|
||||
title,
|
||||
})).to.throw;
|
||||
expect(fcmSendSpy).to.not.have.been.called;
|
||||
expect(apnSendSpy).to.not.have.been.called;
|
||||
});
|
||||
|
||||
it('returns if no device is registered', () => {
|
||||
sendPushNotification(user, {
|
||||
identifier,
|
||||
title,
|
||||
message,
|
||||
});
|
||||
expect(fcmSendSpy).to.not.have.been.called;
|
||||
expect(apnSendSpy).to.not.have.been.called;
|
||||
});
|
||||
|
||||
it('cuts the message to 300 chars', () => {
|
||||
const longMessage = `12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345`;
|
||||
|
||||
expect(longMessage.length > MAX_MESSAGE_LENGTH).to.equal(true);
|
||||
|
||||
const details = {
|
||||
identifier,
|
||||
title,
|
||||
message: longMessage,
|
||||
payload: {
|
||||
message: longMessage,
|
||||
},
|
||||
};
|
||||
|
||||
sendPushNotification(user, details);
|
||||
|
||||
expect(details.message).to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
|
||||
expect(details.payload.message)
|
||||
.to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
|
||||
|
||||
expect(details.message.length).to.equal(MAX_MESSAGE_LENGTH);
|
||||
expect(details.payload.message.length).to.equal(MAX_MESSAGE_LENGTH);
|
||||
});
|
||||
|
||||
it('cuts the message to 300 chars (no payload)', () => {
|
||||
const longMessage = `12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345`;
|
||||
|
||||
expect(longMessage.length > MAX_MESSAGE_LENGTH).to.equal(true);
|
||||
|
||||
const details = {
|
||||
identifier,
|
||||
title,
|
||||
message: longMessage,
|
||||
};
|
||||
|
||||
sendPushNotification(user, details);
|
||||
|
||||
expect(details.message).to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
|
||||
expect(details.message.length).to.equal(MAX_MESSAGE_LENGTH);
|
||||
});
|
||||
|
||||
// TODO disabled because APN relies on a Promise
|
||||
xit('uses APN for iOS devices', () => {
|
||||
user.pushDevices.push({
|
||||
type: 'ios',
|
||||
regId: '123',
|
||||
});
|
||||
|
||||
const details = {
|
||||
identifier,
|
||||
title,
|
||||
message,
|
||||
category: 'fun',
|
||||
payload: {
|
||||
a: true,
|
||||
b: true,
|
||||
},
|
||||
};
|
||||
|
||||
const expectedNotification = new apn.Notification({
|
||||
alert: message,
|
||||
sound: 'default',
|
||||
category: 'fun',
|
||||
payload: {
|
||||
identifier,
|
||||
a: true,
|
||||
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;
|
||||
});
|
||||
});
|
||||
354
test/api/unit/libs/pushNotifications.test.js
Normal file
354
test/api/unit/libs/pushNotifications.test.js
Normal file
@@ -0,0 +1,354 @@
|
||||
import apn from '@parse/node-apn';
|
||||
import _ from 'lodash';
|
||||
import nconf from 'nconf';
|
||||
import admin from 'firebase-admin';
|
||||
import { model as User } from '../../../../website/server/models/user';
|
||||
import {
|
||||
MAX_MESSAGE_LENGTH,
|
||||
} from '../../../../website/server/libs/pushNotifications';
|
||||
|
||||
let sendPushNotification;
|
||||
|
||||
describe('pushNotifications', () => {
|
||||
let user;
|
||||
let fcmSendSpy;
|
||||
let apnSendSpy;
|
||||
let updateStub;
|
||||
let classStubbedInstance;
|
||||
|
||||
const identifier = 'identifier';
|
||||
const title = 'title';
|
||||
const message = 'message';
|
||||
|
||||
beforeEach(() => {
|
||||
user = new User();
|
||||
fcmSendSpy = sinon.stub().returns(Promise.resolve('success'));
|
||||
apnSendSpy = sinon.stub().returns(Promise.resolve());
|
||||
|
||||
nconf.set('PUSH_CONFIGS_APN_ENABLED', 'true');
|
||||
|
||||
classStubbedInstance = sandbox.createStubInstance(apn.Provider, {
|
||||
send: apnSendSpy,
|
||||
});
|
||||
sandbox.stub(apn, 'Provider').returns(classStubbedInstance);
|
||||
|
||||
delete require.cache[require.resolve('../../../../website/server/libs/pushNotifications')];
|
||||
// eslint-disable-next-line global-require
|
||||
sendPushNotification = require('../../../../website/server/libs/pushNotifications').sendNotification;
|
||||
|
||||
updateStub = sandbox.stub(User, 'updateOne').resolves();
|
||||
sandbox.stub(admin, 'messaging').get(() => () => ({ send: fcmSendSpy }));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('validates supplied data', () => {
|
||||
it('throws if user is not supplied', () => {
|
||||
expect(sendPushNotification).to.throw;
|
||||
expect(fcmSendSpy).to.not.have.been.called;
|
||||
expect(apnSendSpy).to.not.have.been.called;
|
||||
});
|
||||
|
||||
it('throws if user.preferences.pushNotifications.unsubscribeFromAll is true', () => {
|
||||
user.preferences.pushNotifications.unsubscribeFromAll = true;
|
||||
expect(() => sendPushNotification(user)).to.throw;
|
||||
expect(fcmSendSpy).to.not.have.been.called;
|
||||
expect(apnSendSpy).to.not.have.been.called;
|
||||
});
|
||||
|
||||
it('throws if details.identifier is not supplied', () => {
|
||||
expect(() => sendPushNotification(user, {
|
||||
title,
|
||||
message,
|
||||
})).to.throw;
|
||||
expect(fcmSendSpy).to.not.have.been.called;
|
||||
expect(apnSendSpy).to.not.have.been.called;
|
||||
});
|
||||
|
||||
it('throws if details.title is not supplied', () => {
|
||||
expect(() => sendPushNotification(user, {
|
||||
identifier,
|
||||
message,
|
||||
})).to.throw;
|
||||
expect(fcmSendSpy).to.not.have.been.called;
|
||||
expect(apnSendSpy).to.not.have.been.called;
|
||||
});
|
||||
|
||||
it('throws if details.message is not supplied', () => {
|
||||
expect(() => sendPushNotification(user, {
|
||||
identifier,
|
||||
title,
|
||||
})).to.throw;
|
||||
expect(fcmSendSpy).to.not.have.been.called;
|
||||
expect(apnSendSpy).to.not.have.been.called;
|
||||
});
|
||||
|
||||
it('returns if no device is registered', () => {
|
||||
sendPushNotification(user, {
|
||||
identifier,
|
||||
title,
|
||||
message,
|
||||
});
|
||||
expect(fcmSendSpy).to.not.have.been.called;
|
||||
expect(apnSendSpy).to.not.have.been.called;
|
||||
});
|
||||
});
|
||||
|
||||
it('cuts the message to 300 chars', () => {
|
||||
const longMessage = `12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345`;
|
||||
|
||||
expect(longMessage.length > MAX_MESSAGE_LENGTH).to.equal(true);
|
||||
|
||||
const details = {
|
||||
identifier,
|
||||
title,
|
||||
message: longMessage,
|
||||
payload: {
|
||||
message: longMessage,
|
||||
},
|
||||
};
|
||||
|
||||
sendPushNotification(user, details);
|
||||
|
||||
expect(details.message).to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
|
||||
expect(details.payload.message)
|
||||
.to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
|
||||
|
||||
expect(details.message.length).to.equal(MAX_MESSAGE_LENGTH);
|
||||
expect(details.payload.message.length).to.equal(MAX_MESSAGE_LENGTH);
|
||||
});
|
||||
|
||||
it('cuts the message to 300 chars (no payload)', () => {
|
||||
const longMessage = `12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345`;
|
||||
|
||||
expect(longMessage.length > MAX_MESSAGE_LENGTH).to.equal(true);
|
||||
|
||||
const details = {
|
||||
identifier,
|
||||
title,
|
||||
message: longMessage,
|
||||
};
|
||||
|
||||
sendPushNotification(user, details);
|
||||
|
||||
expect(details.message).to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
|
||||
expect(details.message.length).to.equal(MAX_MESSAGE_LENGTH);
|
||||
});
|
||||
|
||||
describe('sends notifications', () => {
|
||||
let details;
|
||||
|
||||
beforeEach(() => {
|
||||
details = {
|
||||
identifier,
|
||||
title,
|
||||
message,
|
||||
category: 'fun',
|
||||
payload: {
|
||||
a: true,
|
||||
b: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('uses APN for iOS devices', async () => {
|
||||
user.pushDevices.push({
|
||||
type: 'ios',
|
||||
regId: '123',
|
||||
});
|
||||
|
||||
const expectedNotification = new apn.Notification({
|
||||
alert: {
|
||||
title,
|
||||
body: message,
|
||||
},
|
||||
sound: 'default',
|
||||
category: 'fun',
|
||||
payload: {
|
||||
identifier,
|
||||
a: true,
|
||||
b: true,
|
||||
},
|
||||
});
|
||||
|
||||
await sendPushNotification(user, details);
|
||||
expect(apnSendSpy).to.have.been.calledOnce;
|
||||
expect(apnSendSpy).to.have.been.calledWithMatch(expectedNotification, '123');
|
||||
expect(fcmSendSpy).to.not.have.been.called;
|
||||
});
|
||||
|
||||
it('uses FCM for Android devices', async () => {
|
||||
user.pushDevices.push({
|
||||
type: 'android',
|
||||
regId: '123',
|
||||
});
|
||||
|
||||
const expectedMessage = {
|
||||
notification: {
|
||||
title,
|
||||
body: message,
|
||||
},
|
||||
data: {
|
||||
identifier,
|
||||
notificationIdentifier: identifier,
|
||||
},
|
||||
token: '123',
|
||||
};
|
||||
|
||||
await sendPushNotification(user, details);
|
||||
expect(fcmSendSpy).to.have.been.calledOnce;
|
||||
expect(fcmSendSpy).to.have.been.calledWithMatch(expectedMessage);
|
||||
expect(apnSendSpy).to.not.have.been.called;
|
||||
});
|
||||
|
||||
it('handles multiple devices', async () => {
|
||||
user.pushDevices.push({
|
||||
type: 'android',
|
||||
regId: '123',
|
||||
});
|
||||
user.pushDevices.push({
|
||||
type: 'ios',
|
||||
regId: '456',
|
||||
});
|
||||
user.pushDevices.push({
|
||||
type: 'android',
|
||||
regId: '789',
|
||||
});
|
||||
|
||||
await sendPushNotification(user, details);
|
||||
expect(fcmSendSpy).to.have.been.calledTwice;
|
||||
expect(apnSendSpy).to.have.been.calledOnce;
|
||||
});
|
||||
});
|
||||
|
||||
describe('handles sending errors', () => {
|
||||
let clock;
|
||||
|
||||
beforeEach(() => {
|
||||
clock = sinon.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clock.restore();
|
||||
});
|
||||
|
||||
it('removes unregistered fcm devices', async () => {
|
||||
user.pushDevices.push({
|
||||
type: 'android',
|
||||
regId: '123',
|
||||
});
|
||||
|
||||
const error = new Error();
|
||||
error.code = 'messaging/registration-token-not-registered';
|
||||
fcmSendSpy.rejects(error);
|
||||
|
||||
await sendPushNotification(user, {
|
||||
identifier,
|
||||
title,
|
||||
message,
|
||||
});
|
||||
|
||||
expect(fcmSendSpy).to.have.been.calledOnce;
|
||||
expect(apnSendSpy).to.not.have.been.called;
|
||||
await clock.tick(10);
|
||||
expect(updateStub).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('removes invalid fcm devices', async () => {
|
||||
user.pushDevices.push({
|
||||
type: 'android',
|
||||
regId: '123',
|
||||
});
|
||||
|
||||
const error = new Error();
|
||||
error.code = 'messaging/registration-token-not-registered';
|
||||
fcmSendSpy.rejects(error);
|
||||
|
||||
await sendPushNotification(user, {
|
||||
identifier,
|
||||
title,
|
||||
message,
|
||||
});
|
||||
|
||||
expect(fcmSendSpy).to.have.been.calledOnce;
|
||||
expect(apnSendSpy).to.not.have.been.called;
|
||||
expect(updateStub).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('removes unregistered apn devices', async () => {
|
||||
user.pushDevices.push({
|
||||
type: 'ios',
|
||||
regId: '123',
|
||||
});
|
||||
|
||||
const error = {
|
||||
failed: [
|
||||
{
|
||||
device: '123',
|
||||
response: { reason: 'Unregistered' },
|
||||
},
|
||||
],
|
||||
};
|
||||
apnSendSpy.resolves(error);
|
||||
|
||||
await sendPushNotification(user, {
|
||||
identifier,
|
||||
title,
|
||||
message,
|
||||
});
|
||||
|
||||
expect(fcmSendSpy).to.not.have.been.called;
|
||||
expect(apnSendSpy).to.have.been.calledOnce;
|
||||
expect(updateStub).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('removes invalid apn devices', async () => {
|
||||
user.pushDevices.push({
|
||||
type: 'ios',
|
||||
regId: '123',
|
||||
});
|
||||
|
||||
const error = {
|
||||
failed: [
|
||||
{
|
||||
device: '123',
|
||||
response: { reason: 'BadDeviceToken' },
|
||||
},
|
||||
],
|
||||
};
|
||||
apnSendSpy.resolves(error);
|
||||
|
||||
await sendPushNotification(user, {
|
||||
identifier,
|
||||
title,
|
||||
message,
|
||||
});
|
||||
|
||||
expect(fcmSendSpy).to.not.have.been.called;
|
||||
expect(apnSendSpy).to.have.been.calledOnce;
|
||||
expect(updateStub).to.have.been.calledOnce;
|
||||
});
|
||||
});
|
||||
});
|
||||
51
test/api/unit/middlewares/ensureDevelopmentMode.js
Normal file
51
test/api/unit/middlewares/ensureDevelopmentMode.js
Normal file
@@ -0,0 +1,51 @@
|
||||
/* eslint-disable global-require */
|
||||
import nconf from 'nconf';
|
||||
import {
|
||||
generateRes,
|
||||
generateReq,
|
||||
generateNext,
|
||||
} from '../../../helpers/api-unit.helper';
|
||||
import ensureDevelopmentMode from '../../../../website/server/middlewares/ensureDevelopmentMode';
|
||||
import { NotFound } from '../../../../website/server/libs/errors';
|
||||
|
||||
describe('developmentMode middleware', () => {
|
||||
let res; let req; let next;
|
||||
let nconfStub;
|
||||
|
||||
beforeEach(() => {
|
||||
res = generateRes();
|
||||
req = generateReq();
|
||||
next = generateNext();
|
||||
nconfStub = sandbox.stub(nconf, 'get');
|
||||
});
|
||||
|
||||
it('returns not found when on production URL', () => {
|
||||
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
|
||||
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
|
||||
|
||||
ensureDevelopmentMode(req, res, next);
|
||||
|
||||
const calledWith = next.getCall(0).args;
|
||||
expect(calledWith[0] instanceof NotFound).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns not found when intentionally disabled', () => {
|
||||
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
|
||||
nconfStub.withArgs('BASE_URL').returns('http://localhost:3000');
|
||||
|
||||
ensureDevelopmentMode(req, res, next);
|
||||
|
||||
const calledWith = next.getCall(0).args;
|
||||
expect(calledWith[0] instanceof NotFound).to.equal(true);
|
||||
});
|
||||
|
||||
it('passes when enabled and on non-production URL', () => {
|
||||
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
|
||||
nconfStub.withArgs('BASE_URL').returns('http://localhost:3000');
|
||||
|
||||
ensureDevelopmentMode(req, res, next);
|
||||
|
||||
expect(next).to.be.calledOnce;
|
||||
expect(next.args[0]).to.be.empty;
|
||||
});
|
||||
});
|
||||
@@ -1,38 +0,0 @@
|
||||
/* eslint-disable global-require */
|
||||
import nconf from 'nconf';
|
||||
import {
|
||||
generateRes,
|
||||
generateReq,
|
||||
generateNext,
|
||||
} from '../../../helpers/api-unit.helper';
|
||||
import ensureDevelpmentMode from '../../../../website/server/middlewares/ensureDevelpmentMode';
|
||||
import { NotFound } from '../../../../website/server/libs/errors';
|
||||
|
||||
describe('developmentMode middleware', () => {
|
||||
let res; let req; let
|
||||
next;
|
||||
|
||||
beforeEach(() => {
|
||||
res = generateRes();
|
||||
req = generateReq();
|
||||
next = generateNext();
|
||||
});
|
||||
|
||||
it('returns not found when in production mode', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
|
||||
|
||||
ensureDevelpmentMode(req, res, next);
|
||||
|
||||
const calledWith = next.getCall(0).args;
|
||||
expect(calledWith[0] instanceof NotFound).to.equal(true);
|
||||
});
|
||||
|
||||
it('passes when not in production', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(false);
|
||||
|
||||
ensureDevelpmentMode(req, res, next);
|
||||
|
||||
expect(next).to.be.calledOnce;
|
||||
expect(next.args[0]).to.be.empty;
|
||||
});
|
||||
});
|
||||
51
test/api/unit/middlewares/ensureTimeTravelMode.js
Normal file
51
test/api/unit/middlewares/ensureTimeTravelMode.js
Normal file
@@ -0,0 +1,51 @@
|
||||
/* eslint-disable global-require */
|
||||
import nconf from 'nconf';
|
||||
import {
|
||||
generateRes,
|
||||
generateReq,
|
||||
generateNext,
|
||||
} from '../../../helpers/api-unit.helper';
|
||||
import { NotFound } from '../../../../website/server/libs/errors';
|
||||
import ensureTimeTravelMode from '../../../../website/server/middlewares/ensureTimeTravelMode';
|
||||
|
||||
describe('timetravelMode middleware', () => {
|
||||
let res; let req; let next;
|
||||
let nconfStub;
|
||||
|
||||
beforeEach(() => {
|
||||
res = generateRes();
|
||||
req = generateReq();
|
||||
next = generateNext();
|
||||
nconfStub = sandbox.stub(nconf, 'get');
|
||||
});
|
||||
|
||||
it('returns not found when using production URL', () => {
|
||||
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(false);
|
||||
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
|
||||
|
||||
ensureTimeTravelMode(req, res, next);
|
||||
|
||||
const calledWith = next.getCall(0).args;
|
||||
expect(calledWith[0] instanceof NotFound).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns not found when not in time travel mode', () => {
|
||||
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(false);
|
||||
nconfStub.withArgs('BASE_URL').returns('http://localhost:3000');
|
||||
|
||||
ensureTimeTravelMode(req, res, next);
|
||||
|
||||
const calledWith = next.getCall(0).args;
|
||||
expect(calledWith[0] instanceof NotFound).to.equal(true);
|
||||
});
|
||||
|
||||
it('passes when in time travel mode', () => {
|
||||
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(true);
|
||||
nconfStub.withArgs('BASE_URL').returns('http://localhost:3000');
|
||||
|
||||
ensureTimeTravelMode(req, res, next);
|
||||
|
||||
expect(next).to.be.calledOnce;
|
||||
expect(next.args[0]).to.be.empty;
|
||||
});
|
||||
});
|
||||
@@ -54,6 +54,7 @@ describe('rateLimiter middleware', () => {
|
||||
|
||||
it('does not throw when there are available points', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
@@ -71,6 +72,7 @@ describe('rateLimiter middleware', () => {
|
||||
|
||||
it('does not throw when an unknown error is thrown by the rate limiter', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
|
||||
sandbox.stub(logger, 'error');
|
||||
sandbox.stub(RateLimiterMemory.prototype, 'consume')
|
||||
.returns(Promise.reject(new Error('Unknown error.')));
|
||||
@@ -87,8 +89,73 @@ describe('rateLimiter middleware', () => {
|
||||
expect(logger.error).to.have.been.calledWithMatch(Error, 'Rate Limiter Error');
|
||||
});
|
||||
|
||||
it('does not throw when LIVELINESS_PROBE_KEY is correct', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns('abc');
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
|
||||
req.query.liveliness = 'abc';
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
expect(next).to.have.been.calledOnce;
|
||||
const calledWith = next.getCall(0).args;
|
||||
expect(typeof calledWith[0] === 'undefined').to.equal(true);
|
||||
expect(res.set).to.not.have.been.called;
|
||||
});
|
||||
|
||||
it('limits when LIVELINESS_PROBE_KEY is incorrect', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns('abc');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
|
||||
req.query.liveliness = 'das';
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
expect(next).to.have.been.calledOnce;
|
||||
expect(res.set).to.have.been.calledWithMatch({
|
||||
'X-RateLimit-Limit': 30,
|
||||
'X-RateLimit-Remaining': 29,
|
||||
'X-RateLimit-Reset': sinon.match(Date),
|
||||
});
|
||||
});
|
||||
|
||||
it('limits when LIVELINESS_PROBE_KEY is not set', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns(undefined);
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
expect(next).to.have.been.calledOnce;
|
||||
expect(res.set).to.have.been.calledWithMatch({
|
||||
'X-RateLimit-Limit': 30,
|
||||
'X-RateLimit-Remaining': 29,
|
||||
'X-RateLimit-Reset': sinon.match(Date),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws when LIVELINESS_PROBE_KEY is blank', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns('');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
|
||||
req.query.liveliness = '';
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
expect(next).to.have.been.calledOnce;
|
||||
expect(res.set).to.have.been.calledWithMatch({
|
||||
'X-RateLimit-Limit': 30,
|
||||
'X-RateLimit-Remaining': 29,
|
||||
'X-RateLimit-Reset': sinon.match(Date),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws when there are no available points remaining', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
|
||||
// call for 31 times
|
||||
@@ -112,6 +179,7 @@ describe('rateLimiter middleware', () => {
|
||||
|
||||
it('uses the user id if supplied or the ip address', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
|
||||
req.ip = 1;
|
||||
@@ -138,4 +206,51 @@ describe('rateLimiter middleware', () => {
|
||||
'X-RateLimit-Reset': sinon.match(Date),
|
||||
});
|
||||
});
|
||||
|
||||
it('applies increased cost for registration calls with and without user id', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_REGISTRATION_COST').returns(3);
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
req.path = '/api/v4/user/auth/local/register';
|
||||
|
||||
req.ip = 1;
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
req.headers['x-api-user'] = 'user-1';
|
||||
await attachRateLimiter(req, res, next);
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
// user id an ip are counted as separate sources
|
||||
expect(res.set).to.have.been.calledWithMatch({
|
||||
'X-RateLimit-Limit': 30,
|
||||
'X-RateLimit-Remaining': 27, // 2 calls with user id
|
||||
'X-RateLimit-Reset': sinon.match(Date),
|
||||
});
|
||||
|
||||
req.headers['x-api-user'] = undefined;
|
||||
await attachRateLimiter(req, res, next);
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
expect(res.set).to.have.been.calledWithMatch({
|
||||
'X-RateLimit-Limit': 30,
|
||||
'X-RateLimit-Remaining': 24, // 3 calls with only ip
|
||||
'X-RateLimit-Reset': sinon.match(Date),
|
||||
});
|
||||
});
|
||||
|
||||
it('applies increased cost for unauthenticated API calls', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(10);
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
|
||||
req.ip = 1;
|
||||
await attachRateLimiter(req, res, next);
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
expect(res.set).to.have.been.calledWithMatch({
|
||||
'X-RateLimit-Limit': 30,
|
||||
'X-RateLimit-Remaining': 10,
|
||||
'X-RateLimit-Reset': sinon.match(Date),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
37
test/api/unit/middlewares/requestLogHandler.test.js
Normal file
37
test/api/unit/middlewares/requestLogHandler.test.js
Normal file
@@ -0,0 +1,37 @@
|
||||
/* eslint-disable global-require */
|
||||
import requireAgain from 'require-again';
|
||||
import {
|
||||
generateRes,
|
||||
generateReq,
|
||||
generateNext,
|
||||
} from '../../../helpers/api-unit.helper';
|
||||
|
||||
describe('requestLogHandler middleware', () => {
|
||||
let res; let req; let
|
||||
next;
|
||||
const pathToMiddleWare = '../../../../website/server/middlewares/requestLogHandler';
|
||||
|
||||
beforeEach(() => {
|
||||
res = generateRes();
|
||||
req = generateReq();
|
||||
next = generateNext();
|
||||
});
|
||||
|
||||
it('attaches start time and request ID object to req', () => {
|
||||
const middleware = requireAgain(pathToMiddleWare);
|
||||
|
||||
middleware.logRequestData(req, res, next);
|
||||
|
||||
expect(req.requestStartTime).to.exist;
|
||||
expect(req.requestStartTime).to.be.a('number');
|
||||
expect(req.requestIdentifier).to.exist;
|
||||
expect(req.requestIdentifier).to.be.a('string');
|
||||
});
|
||||
|
||||
it('calls next', () => {
|
||||
const middleware = requireAgain(pathToMiddleWare);
|
||||
const spy = sinon.spy();
|
||||
middleware.logRequestData(req, res, spy);
|
||||
expect(spy.calledOnce).to.be.true;
|
||||
});
|
||||
});
|
||||
@@ -1362,8 +1362,8 @@ describe('Group Model', () => {
|
||||
sandbox.spy(User, 'updateMany');
|
||||
});
|
||||
|
||||
it('formats message', () => {
|
||||
const chatMessage = party.sendChat({
|
||||
it('formats message', async () => {
|
||||
const chatMessage = await party.sendChat({
|
||||
message: 'a _new_ message with *markdown*',
|
||||
user: {
|
||||
_id: 'user-id',
|
||||
@@ -1396,8 +1396,8 @@ describe('Group Model', () => {
|
||||
expect(chat.user).to.eql('user name');
|
||||
});
|
||||
|
||||
it('formats message as system if no user is passed in', () => {
|
||||
const chat = party.sendChat({ message: 'a system message' });
|
||||
it('formats message as system if no user is passed in', async () => {
|
||||
const chat = await party.sendChat({ message: 'a system message' });
|
||||
|
||||
expect(chat.text).to.eql('a system message');
|
||||
expect(validator.isUUID(chat.id)).to.eql(true);
|
||||
@@ -1411,8 +1411,8 @@ describe('Group Model', () => {
|
||||
expect(chat.user).to.not.exist;
|
||||
});
|
||||
|
||||
it('updates users about new messages in party', () => {
|
||||
party.sendChat({ message: 'message' });
|
||||
it('updates users about new messages in party', async () => {
|
||||
await party.sendChat({ message: 'message' });
|
||||
|
||||
expect(User.updateMany).to.be.calledOnce;
|
||||
expect(User.updateMany).to.be.calledWithMatch({
|
||||
@@ -1421,12 +1421,12 @@ describe('Group Model', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('updates users about new messages in group', () => {
|
||||
it('updates users about new messages in group', async () => {
|
||||
const group = new Group({
|
||||
type: 'guild',
|
||||
});
|
||||
|
||||
group.sendChat({ message: 'message' });
|
||||
await group.sendChat({ message: 'message' });
|
||||
|
||||
expect(User.updateMany).to.be.calledOnce;
|
||||
expect(User.updateMany).to.be.calledWithMatch({
|
||||
@@ -1435,8 +1435,8 @@ describe('Group Model', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('does not send update to user that sent the message', () => {
|
||||
party.sendChat({ message: 'message', user: { _id: 'user-id', profile: { name: 'user' } } });
|
||||
it('does not send update to user that sent the message', async () => {
|
||||
await party.sendChat({ message: 'message', user: { _id: 'user-id', profile: { name: 'user' } } });
|
||||
|
||||
expect(User.updateMany).to.be.calledOnce;
|
||||
expect(User.updateMany).to.be.calledWithMatch({
|
||||
@@ -1445,18 +1445,18 @@ describe('Group Model', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('skips sending new message notification for guilds with > 5000 members', () => {
|
||||
it('skips sending new message notification for guilds with > 5000 members', async () => {
|
||||
party.memberCount = 5001;
|
||||
|
||||
party.sendChat({ message: 'message' });
|
||||
await party.sendChat({ message: 'message' });
|
||||
|
||||
expect(User.updateMany).to.not.be.called;
|
||||
});
|
||||
|
||||
it('skips sending messages to the tavern', () => {
|
||||
it('skips sending messages to the tavern', async () => {
|
||||
party._id = TAVERN_ID;
|
||||
|
||||
party.sendChat({ message: 'message' });
|
||||
await party.sendChat({ message: 'message' });
|
||||
|
||||
expect(User.updateMany).to.not.be.called;
|
||||
});
|
||||
@@ -2326,7 +2326,7 @@ describe('Group Model', () => {
|
||||
|
||||
await guild.save();
|
||||
|
||||
const groupMessage = guild.sendChat({ message: 'Test message.' });
|
||||
const groupMessage = await guild.sendChat({ message: 'Test message.' });
|
||||
await groupMessage.save();
|
||||
|
||||
await sleep();
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { model as Group } from '../../../../../website/server/models/group';
|
||||
|
||||
describe('GET /groups/:groupId/chat', () => {
|
||||
let user;
|
||||
@@ -37,4 +38,34 @@ describe('GET /groups/:groupId/chat', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('public Guild', () => {
|
||||
let group;
|
||||
before(async () => {
|
||||
({ group } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'private',
|
||||
},
|
||||
members: 1,
|
||||
upgradeToGroupPlan: true,
|
||||
chat: [
|
||||
'Hello',
|
||||
'Welcome to the Guild',
|
||||
],
|
||||
}));
|
||||
|
||||
// Creation API is shut down, we need to simulate an extant public group
|
||||
await Group.updateOne({ _id: group._id }, { $set: { privacy: 'public' }, $unset: { 'purchased.plan': 1 } }).exec();
|
||||
});
|
||||
|
||||
it('returns error if user attempts to fetch a sunset Guild', async () => {
|
||||
await expect(user.get(`/groups/${group._id}/chat`)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('featureRetired'),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -223,4 +223,23 @@ describe('POST /chat/:chatId/flag', () => {
|
||||
|
||||
expect(auMessageToCheck).to.not.exist;
|
||||
});
|
||||
|
||||
it('validates that the message belongs to the passed group', async () => {
|
||||
const { group: anotherGroup, groupLeader: anotherLeader } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Another Guild',
|
||||
type: 'guild',
|
||||
privacy: 'private',
|
||||
},
|
||||
upgradeToGroupPlan: true,
|
||||
});
|
||||
|
||||
const message = await anotherUser.post(`/groups/${group._id}/chat`, { message: TEST_MESSAGE });
|
||||
await expect(anotherLeader.post(`/groups/${anotherGroup._id}/chat/${message.message.id}/flag`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('messageGroupChatNotFound'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { find } from 'lodash';
|
||||
import {
|
||||
generateUser,
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { model as Group } from '../../../../../website/server/models/group';
|
||||
|
||||
describe('POST /chat/:chatId/like', () => {
|
||||
let user;
|
||||
@@ -79,4 +81,49 @@ describe('POST /chat/:chatId/like', () => {
|
||||
const messageToCheck = find(groupWithoutChatLikes.chat, { id: message.message.id });
|
||||
expect(messageToCheck.likes[user._id]).to.equal(false);
|
||||
});
|
||||
|
||||
it('validates that the message belongs to the passed group', async () => {
|
||||
const { group: anotherGroup, groupLeader: anotherLeader } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Another Guild',
|
||||
type: 'guild',
|
||||
privacy: 'private',
|
||||
},
|
||||
upgradeToGroupPlan: true,
|
||||
});
|
||||
|
||||
const message = await anotherUser.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
|
||||
await expect(anotherLeader.post(`/groups/${anotherGroup._id}/chat/${message.message.id}/like`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('messageGroupChatNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('does not like a message if the user is not in the group', async () => {
|
||||
const thirdUser = await generateUser();
|
||||
|
||||
const message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
|
||||
await expect(thirdUser.post(`/groups/${groupWithChat._id}/chat/${message.message.id}/like`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('does not like a message that belongs to a sunset public group', async () => {
|
||||
const message = await anotherUser.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
|
||||
|
||||
// Creation API is shut down, we need to simulate an extant public group
|
||||
await Group.updateOne({ _id: groupWithChat._id }, { $set: { privacy: 'public' }, $unset: { 'purchased.plan': 1 } }).exec();
|
||||
|
||||
await expect(user.post(`/groups/${groupWithChat._id}/chat/${message.message.id}/like`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('featureRetired'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,4 +22,38 @@ describe('GET /content', () => {
|
||||
expect(res).to.have.nested.property('backgrounds.backgrounds062014.beach');
|
||||
expect(res.backgrounds.backgrounds062014.beach.text).to.equal(t('backgroundBeachText'));
|
||||
});
|
||||
|
||||
it('does not filter content for regular requests', async () => {
|
||||
const res = await requester().get('/content');
|
||||
expect(res).to.have.nested.property('backgrounds.backgrounds062014');
|
||||
expect(res).to.have.nested.property('gear.tree');
|
||||
});
|
||||
|
||||
it('filters content automatically for iOS requests', async () => {
|
||||
const res = await requester(null, { 'x-client': 'habitica-ios' }).get('/content');
|
||||
expect(res).to.have.nested.property('appearances.background.beach');
|
||||
expect(res).to.not.have.nested.property('backgrounds.backgrounds062014');
|
||||
expect(res).to.not.have.property('backgroundsFlat');
|
||||
expect(res).to.not.have.nested.property('gear.tree');
|
||||
});
|
||||
|
||||
it('filters content automatically for Android requests', async () => {
|
||||
const res = await requester(null, { 'x-client': 'habitica-android' }).get('/content');
|
||||
expect(res).to.not.have.nested.property('appearances.background.beach');
|
||||
expect(res).to.have.nested.property('backgrounds.backgrounds062014');
|
||||
expect(res).to.not.have.property('backgroundsFlat');
|
||||
expect(res).to.not.have.nested.property('gear.tree');
|
||||
});
|
||||
|
||||
it('filters content if the request specifies a filter', async () => {
|
||||
const res = await requester().get('/content?filter=backgroundsFlat,gear.flat');
|
||||
expect(res).to.not.have.property('backgroundsFlat');
|
||||
expect(res).to.have.nested.property('gear.tree');
|
||||
expect(res).to.not.have.nested.property('gear.flat');
|
||||
});
|
||||
|
||||
it('filters content if the request contains invalid filters', async () => {
|
||||
const res = await requester().get('/content?filter=backgroundsFlat,invalid');
|
||||
expect(res).to.not.have.property('backgroundsFlat');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import nconf from 'nconf';
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('GET /debug/time-travel-time', () => {
|
||||
let user;
|
||||
let nconfStub;
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser({ permissions: { fullAccess: true } });
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
nconfStub = sandbox.stub(nconf, 'get');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nconfStub.restore();
|
||||
});
|
||||
|
||||
it('returns the shifted time', async () => {
|
||||
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(true);
|
||||
const result = await user.get('/debug/time-travel-time');
|
||||
expect(result.time).to.exist;
|
||||
await user.post('/debug/jump-time', { disable: true });
|
||||
});
|
||||
|
||||
it('returns shifted when the user is not an admin', async () => {
|
||||
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(true);
|
||||
const regularUser = await generateUser();
|
||||
const result = await regularUser.get('/debug/time-travel-time');
|
||||
expect(result.time).to.exist;
|
||||
});
|
||||
|
||||
it('returns error when not in time travel mode', async () => {
|
||||
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(false);
|
||||
|
||||
await expect(user.get('/debug/time-travel-time'))
|
||||
.eventually.be.rejected.and.to.deep.equal({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: 'Not found.',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,16 +5,23 @@ import {
|
||||
|
||||
describe('POST /debug/add-hourglass', () => {
|
||||
let userToGetHourGlass;
|
||||
let nconfStub;
|
||||
|
||||
before(async () => {
|
||||
userToGetHourGlass = await generateUser();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
nconf.set('IS_PROD', false);
|
||||
beforeEach(() => {
|
||||
nconfStub = sandbox.stub(nconf, 'get');
|
||||
nconfStub.withArgs('BASE_URL').returns('https://example.com');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nconfStub.restore();
|
||||
});
|
||||
|
||||
it('adds Hourglass to the current user', async () => {
|
||||
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
|
||||
await userToGetHourGlass.post('/debug/add-hourglass');
|
||||
|
||||
const userWithHourGlass = await userToGetHourGlass.get('/user');
|
||||
@@ -23,7 +30,7 @@ describe('POST /debug/add-hourglass', () => {
|
||||
});
|
||||
|
||||
it('returns error when not in production mode', async () => {
|
||||
nconf.set('IS_PROD', true);
|
||||
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
|
||||
|
||||
await expect(userToGetHourGlass.post('/debug/add-hourglass'))
|
||||
.eventually.be.rejected.and.to.deep.equal({
|
||||
|
||||
@@ -5,16 +5,23 @@ import {
|
||||
|
||||
describe('POST /debug/add-ten-gems', () => {
|
||||
let userToGainTenGems;
|
||||
let nconfStub;
|
||||
|
||||
before(async () => {
|
||||
userToGainTenGems = await generateUser();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
nconf.set('IS_PROD', false);
|
||||
beforeEach(() => {
|
||||
nconfStub = sandbox.stub(nconf, 'get');
|
||||
nconfStub.withArgs('BASE_URL').returns('https://example.com');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nconfStub.restore();
|
||||
});
|
||||
|
||||
it('adds ten gems to the current user', async () => {
|
||||
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
|
||||
await userToGainTenGems.post('/debug/add-ten-gems');
|
||||
|
||||
const userWithTenGems = await userToGainTenGems.get('/user');
|
||||
@@ -23,7 +30,7 @@ describe('POST /debug/add-ten-gems', () => {
|
||||
});
|
||||
|
||||
it('returns error when not in production mode', async () => {
|
||||
nconf.set('IS_PROD', true);
|
||||
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
|
||||
|
||||
await expect(userToGainTenGems.post('/debug/add-ten-gems'))
|
||||
.eventually.be.rejected.and.to.deep.equal({
|
||||
|
||||
73
test/api/v3/integration/debug/POST-debug_boss-rage.test.js
Normal file
73
test/api/v3/integration/debug/POST-debug_boss-rage.test.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import nconf from 'nconf';
|
||||
import {
|
||||
generateUser,
|
||||
createAndPopulateGroup,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('POST /debug/boss-rage', () => {
|
||||
let user;
|
||||
let nconfStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
nconfStub = sandbox.stub(nconf, 'get');
|
||||
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
|
||||
nconfStub.withArgs('BASE_URL').returns('https://example.com');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nconfStub.restore();
|
||||
});
|
||||
|
||||
it('errors if user is not in a party', async () => {
|
||||
await expect(user.post('/debug/boss-rage'))
|
||||
.to.eventually.be.rejected.and.deep.equal({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'User not in a party.',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when not in production mode', async () => {
|
||||
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
|
||||
|
||||
await expect(user.post('/debug/boss-rage'))
|
||||
.to.eventually.be.rejected.and.deep.equal({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: 'Not found.',
|
||||
});
|
||||
});
|
||||
|
||||
context('user is in a party', async () => {
|
||||
let party;
|
||||
|
||||
beforeEach(async () => {
|
||||
const { group, groupLeader } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Test Party',
|
||||
type: 'party',
|
||||
},
|
||||
members: 2,
|
||||
});
|
||||
party = group;
|
||||
user = groupLeader;
|
||||
});
|
||||
|
||||
it('increases boss rage to 50', async () => {
|
||||
await user.post('/debug/boss-rage');
|
||||
await party.sync();
|
||||
expect(party.quest.progress.rage).to.eql(50);
|
||||
});
|
||||
|
||||
it('increases boss rage to 100', async () => {
|
||||
await user.post('/debug/boss-rage');
|
||||
await user.post('/debug/boss-rage');
|
||||
await party.sync();
|
||||
expect(party.quest.progress.rage).to.eql(100);
|
||||
});
|
||||
});
|
||||
});
|
||||
86
test/api/v3/integration/debug/POST-debug_jumpTime.test.js
Normal file
86
test/api/v3/integration/debug/POST-debug_jumpTime.test.js
Normal file
@@ -0,0 +1,86 @@
|
||||
import nconf from 'nconf';
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('POST /debug/jump-time', () => {
|
||||
let user;
|
||||
let today;
|
||||
let nconfStub;
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser({ permissions: { fullAccess: true } });
|
||||
today = new Date();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
nconfStub = sandbox.stub(nconf, 'get');
|
||||
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nconfStub.restore();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
nconf.set('TIME_TRAVEL_ENABLED', true);
|
||||
await user.post('/debug/jump-time', { disable: true });
|
||||
nconf.set('TIME_TRAVEL_ENABLED', false);
|
||||
});
|
||||
|
||||
it('Jumps forward', async () => {
|
||||
const resultDate = new Date((await user.post('/debug/jump-time', { reset: true })).time);
|
||||
expect(resultDate.getDate()).to.eql(today.getDate());
|
||||
expect(resultDate.getMonth()).to.eql(today.getMonth());
|
||||
expect(resultDate.getFullYear()).to.eql(today.getFullYear());
|
||||
const newResultDate = new Date((await user.post('/debug/jump-time', { offsetDays: 1 })).time);
|
||||
const tomorrow = new Date(today.valueOf());
|
||||
tomorrow.setDate(today.getDate() + 1);
|
||||
expect(newResultDate.getDate()).to.eql(tomorrow.getDate());
|
||||
expect(newResultDate.getMonth()).to.eql(tomorrow.getMonth());
|
||||
expect(newResultDate.getFullYear()).to.eql(tomorrow.getFullYear());
|
||||
});
|
||||
|
||||
it('jumps back', async () => {
|
||||
const resultDate = new Date((await user.post('/debug/jump-time', { reset: true })).time);
|
||||
expect(resultDate.getDate()).to.eql(today.getDate());
|
||||
expect(resultDate.getMonth()).to.eql(today.getMonth());
|
||||
expect(resultDate.getFullYear()).to.eql(today.getFullYear());
|
||||
const newResultDate = new Date((await user.post('/debug/jump-time', { offsetDays: -1 })).time);
|
||||
const yesterday = new Date(today.valueOf());
|
||||
yesterday.setDate(today.getDate() - 1);
|
||||
expect(newResultDate.getDate()).to.eql(yesterday.getDate());
|
||||
expect(newResultDate.getMonth()).to.eql(yesterday.getMonth());
|
||||
expect(newResultDate.getFullYear()).to.eql(yesterday.getFullYear());
|
||||
});
|
||||
|
||||
it('can jump a lot', async () => {
|
||||
const resultDate = new Date((await user.post('/debug/jump-time', { reset: true })).time);
|
||||
expect(resultDate.getDate()).to.eql(today.getDate());
|
||||
expect(resultDate.getMonth()).to.eql(today.getMonth());
|
||||
expect(resultDate.getFullYear()).to.eql(today.getFullYear());
|
||||
const newResultDate = new Date((await user.post('/debug/jump-time', { offsetDays: 365 })).time);
|
||||
expect(newResultDate.getFullYear()).to.eql(today.getFullYear() + 1);
|
||||
});
|
||||
|
||||
it('returns error when the user is not an admin', async () => {
|
||||
const regularUser = await generateUser();
|
||||
await expect(regularUser.post('/debug/jump-time', { offsetDays: 1 }))
|
||||
.eventually.be.rejected.and.to.deep.equal({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'You do not have permission to time travel.',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when not in time travel mode', async () => {
|
||||
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(false);
|
||||
|
||||
await expect(user.post('/debug/jump-time', { offsetDays: 1 }))
|
||||
.eventually.be.rejected.and.to.deep.equal({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: 'Not found.',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,16 +5,23 @@ import {
|
||||
|
||||
describe('POST /debug/make-admin', () => {
|
||||
let user;
|
||||
let nconfStub;
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
nconfStub = sandbox.stub(nconf, 'get');
|
||||
nconfStub.withArgs('BASE_URL').returns('https://example.com');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nconf.set('IS_PROD', false);
|
||||
nconfStub.restore();
|
||||
});
|
||||
|
||||
it('makes user an admin', async () => {
|
||||
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
|
||||
await user.post('/debug/make-admin');
|
||||
|
||||
await user.sync();
|
||||
@@ -23,7 +30,7 @@ describe('POST /debug/make-admin', () => {
|
||||
});
|
||||
|
||||
it('returns error when not in production mode', async () => {
|
||||
nconf.set('IS_PROD', true);
|
||||
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
|
||||
|
||||
await expect(user.post('/debug/make-admin'))
|
||||
.eventually.be.rejected.and.to.deep.equal({
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
describe('POST /debug/modify-inventory', () => {
|
||||
let user; let
|
||||
originalItems;
|
||||
let nconfStub;
|
||||
|
||||
before(async () => {
|
||||
originalItems = {
|
||||
@@ -39,8 +40,14 @@ describe('POST /debug/modify-inventory', () => {
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
nconfStub = sandbox.stub(nconf, 'get');
|
||||
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
|
||||
nconfStub.withArgs('BASE_URL').returns('https://example.com');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nconf.set('IS_PROD', false);
|
||||
nconfStub.restore();
|
||||
});
|
||||
|
||||
it('sets equipment', async () => {
|
||||
@@ -149,7 +156,7 @@ describe('POST /debug/modify-inventory', () => {
|
||||
});
|
||||
|
||||
it('returns error when not in production mode', async () => {
|
||||
nconf.set('IS_PROD', true);
|
||||
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
|
||||
|
||||
await expect(user.post('/debug/modify-inventory'))
|
||||
.eventually.be.rejected.and.to.deep.equal({
|
||||
|
||||
@@ -5,13 +5,20 @@ import {
|
||||
|
||||
describe('POST /debug/quest-progress', () => {
|
||||
let user;
|
||||
let nconfStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
nconfStub = sandbox.stub(nconf, 'get');
|
||||
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
|
||||
nconfStub.withArgs('BASE_URL').returns('https://example.com');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nconf.set('IS_PROD', false);
|
||||
nconfStub.restore();
|
||||
});
|
||||
|
||||
it('errors if user is not on a quest', async () => {
|
||||
@@ -48,7 +55,7 @@ describe('POST /debug/quest-progress', () => {
|
||||
});
|
||||
|
||||
it('returns error when not in production mode', async () => {
|
||||
nconf.set('IS_PROD', true);
|
||||
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
|
||||
|
||||
await expect(user.post('/debug/quest-progress'))
|
||||
.eventually.be.rejected.and.to.deep.equal({
|
||||
|
||||
@@ -5,13 +5,20 @@ import {
|
||||
|
||||
describe('POST /debug/set-cron', () => {
|
||||
let user;
|
||||
let nconfStub;
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
nconfStub = sandbox.stub(nconf, 'get');
|
||||
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
|
||||
nconfStub.withArgs('BASE_URL').returns('https://example.com');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nconf.set('IS_PROD', false);
|
||||
nconfStub.restore();
|
||||
});
|
||||
|
||||
it('sets last cron', async () => {
|
||||
@@ -27,7 +34,7 @@ describe('POST /debug/set-cron', () => {
|
||||
});
|
||||
|
||||
it('returns error when not in production mode', async () => {
|
||||
nconf.set('IS_PROD', true);
|
||||
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
|
||||
|
||||
await expect(user.post('/debug/set-cron'))
|
||||
.eventually.be.rejected.and.to.deep.equal({
|
||||
|
||||
@@ -145,6 +145,18 @@ describe('POST /group', () => {
|
||||
expect(updatedUser.party._id).to.eql(party._id);
|
||||
});
|
||||
|
||||
it('removes seeking from user', async () => {
|
||||
await user.updateOne({ 'party.seeking': new Date() });
|
||||
await user.post('/groups', {
|
||||
name: partyName,
|
||||
type: partyType,
|
||||
});
|
||||
|
||||
const updatedUser = await user.get('/user');
|
||||
|
||||
expect(updatedUser.party.seeking).to.not.exist;
|
||||
});
|
||||
|
||||
it('does not award Party Up achievement to solo partier', async () => {
|
||||
await user.post('/groups', {
|
||||
name: partyName,
|
||||
|
||||
@@ -85,22 +85,6 @@ describe('POST /group/:groupId/join', () => {
|
||||
await expect(user.get('/user')).to.eventually.have.nested.property('items.quests.basilist', 1);
|
||||
});
|
||||
|
||||
it('notifies inviting user that their invitation was accepted', async () => {
|
||||
await invitedUser.post(`/groups/${guild._id}/join`);
|
||||
|
||||
const inviter = await user.get('/user');
|
||||
const expectedData = {
|
||||
headerText: t('invitationAcceptedHeader'),
|
||||
bodyText: t('invitationAcceptedBody', {
|
||||
username: invitedUser.auth.local.username,
|
||||
groupName: guild.name,
|
||||
}),
|
||||
};
|
||||
|
||||
expect(inviter.notifications[1].type).to.eql('GROUP_INVITE_ACCEPTED');
|
||||
expect(inviter.notifications[1].data).to.eql(expectedData);
|
||||
});
|
||||
|
||||
it('awards Joined Guild achievement', async () => {
|
||||
await invitedUser.post(`/groups/${guild._id}/join`);
|
||||
|
||||
@@ -155,29 +139,21 @@ describe('POST /group/:groupId/join', () => {
|
||||
await expect(invitedUser.get('/user')).to.eventually.have.nested.property('party._id', party._id);
|
||||
});
|
||||
|
||||
it('notifies inviting user that their invitation was accepted', async () => {
|
||||
await invitedUser.post(`/groups/${party._id}/join`);
|
||||
|
||||
const inviter = await user.get('/user');
|
||||
|
||||
const expectedData = {
|
||||
headerText: t('invitationAcceptedHeader'),
|
||||
bodyText: t('invitationAcceptedBody', {
|
||||
username: invitedUser.auth.local.username,
|
||||
groupName: party.name,
|
||||
}),
|
||||
};
|
||||
|
||||
expect(inviter.notifications[0].type).to.eql('GROUP_INVITE_ACCEPTED');
|
||||
expect(inviter.notifications[0].data).to.eql(expectedData);
|
||||
});
|
||||
|
||||
it('clears invitation from user when joining party', async () => {
|
||||
await invitedUser.post(`/groups/${party._id}/join`);
|
||||
|
||||
await expect(invitedUser.get('/user')).to.eventually.not.have.nested.property('invitations.parties[0].id');
|
||||
});
|
||||
|
||||
it('clears party.seeking from user when joining party', async () => {
|
||||
await invitedUser.updateOne({ 'party.seeking': new Date() });
|
||||
await invitedUser.post(`/groups/${party._id}/join`);
|
||||
|
||||
const updatedUser = await invitedUser.get('/user');
|
||||
|
||||
await expect(updatedUser.party.seeking).to.not.exist;
|
||||
});
|
||||
|
||||
it('increments memberCount when joining party', async () => {
|
||||
const oldMemberCount = party.memberCount;
|
||||
|
||||
|
||||
@@ -60,12 +60,12 @@ describe('PUT /heroes/:heroId', () => {
|
||||
expect(heroRes.profile).to.have.all.keys(['name']);
|
||||
|
||||
// test response values
|
||||
expect(heroRes.balance).to.equal(3 + 0.75); // 3+0.75 for first contrib level
|
||||
expect(heroRes.balance).to.equal(3 + 2.5); // 3+2.5 for first contrib level
|
||||
expect(heroRes.contributor.level).to.equal(1);
|
||||
expect(heroRes.purchased.ads).to.equal(true);
|
||||
// test hero values
|
||||
await hero.sync();
|
||||
expect(hero.balance).to.equal(3 + 0.75); // 3+0.75 for first contrib level
|
||||
expect(hero.balance).to.equal(3 + 2.5); // 3+2.5 for first contrib level
|
||||
expect(hero.contributor.level).to.equal(1);
|
||||
expect(hero.purchased.ads).to.equal(true);
|
||||
expect(hero.auth.blocked).to.equal(prevBlockState);
|
||||
@@ -136,12 +136,12 @@ describe('PUT /heroes/:heroId', () => {
|
||||
expect(heroRes.profile).to.have.all.keys(['name']);
|
||||
|
||||
// test response values
|
||||
expect(heroRes.balance).to.equal(1); // 0+1 for sixth contrib level
|
||||
expect(heroRes.balance).to.equal(15); // 0+15 for sixth contrib level
|
||||
expect(heroRes.contributor.level).to.equal(6);
|
||||
expect(heroRes.items.pets['Dragon-Hydra']).to.equal(5);
|
||||
// test hero values
|
||||
await hero.sync();
|
||||
expect(hero.balance).to.equal(1); // 0+1 for sixth contrib level
|
||||
expect(hero.balance).to.equal(15); // 0+15 for sixth contrib level
|
||||
expect(hero.contributor.level).to.equal(6);
|
||||
expect(hero.items.pets['Dragon-Hydra']).to.equal(5);
|
||||
});
|
||||
|
||||
56
test/api/v3/integration/members/GET-members_username.test.js
Normal file
56
test/api/v3/integration/members/GET-members_username.test.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import common from '../../../../../website/common';
|
||||
|
||||
describe('GET /members/username/:username', () => {
|
||||
let user;
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('validates req.params.username', async () => {
|
||||
await expect(user.get('/members/username/')).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a member public data only', async () => {
|
||||
// make sure user has all the fields that can be returned by the getMember call
|
||||
const member = await generateUser({
|
||||
contributor: { level: 1 },
|
||||
backer: { tier: 3 },
|
||||
preferences: {
|
||||
costume: false,
|
||||
background: 'volcano',
|
||||
},
|
||||
secret: {
|
||||
text: 'Clark Kent',
|
||||
},
|
||||
});
|
||||
const memberRes = await user.get(`/members/username/${member.auth.local.username}`);
|
||||
expect(memberRes).to.have.all.keys([ // works as: object has all and only these keys
|
||||
'_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party',
|
||||
'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives', 'flags',
|
||||
]);
|
||||
expect(Object.keys(memberRes.auth)).to.eql(['local', 'timestamps']);
|
||||
expect(Object.keys(memberRes.preferences).sort()).to.eql([
|
||||
'size', 'hair', 'skin', 'shirt',
|
||||
'chair', 'costume', 'sleep', 'background', 'tasks', 'disableClasses',
|
||||
].sort());
|
||||
|
||||
expect(memberRes.stats.maxMP).to.exist;
|
||||
expect(memberRes.stats.maxHealth).to.equal(common.maxHealth);
|
||||
expect(memberRes.stats.toNextLevel).to.equal(common.tnl(memberRes.stats.lvl));
|
||||
expect(memberRes.inbox.optOut).to.exist;
|
||||
expect(memberRes.inbox.canReceive).to.exist;
|
||||
expect(memberRes.inbox.messages).to.not.exist;
|
||||
expect(memberRes.secret).to.not.exist;
|
||||
|
||||
expect(memberRes.blocks).to.not.exist;
|
||||
});
|
||||
});
|
||||
@@ -17,9 +17,5 @@ describe('GET /shops/backgrounds', () => {
|
||||
expect(shop.notes).to.eql(t('backgroundShop'));
|
||||
expect(shop.imageName).to.equal('background_shop');
|
||||
expect(shop.sets).to.be.an('array');
|
||||
|
||||
const sets = shop.sets.map(set => set.identifier);
|
||||
expect(sets).to.include('incentiveBackgrounds');
|
||||
expect(sets).to.include('backgrounds062014');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,9 +5,15 @@ import {
|
||||
|
||||
describe('GET /shops/time-travelers', () => {
|
||||
let user;
|
||||
let clock;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
clock = sinon.useFakeTimers(new Date('2024-06-08'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clock.restore();
|
||||
});
|
||||
|
||||
it('returns a valid shop object', async () => {
|
||||
|
||||
@@ -125,6 +125,90 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
expect(body.finalLvl).to.eql(user.stats.lvl);
|
||||
});
|
||||
});
|
||||
|
||||
context('handles drops', async () => {
|
||||
let randomStub;
|
||||
|
||||
afterEach(() => {
|
||||
randomStub.restore();
|
||||
});
|
||||
it('gives user a drop', async () => {
|
||||
user = await generateUser({
|
||||
'stats.gp': 100,
|
||||
'achievements.completedTask': true,
|
||||
'items.eggs': {
|
||||
Wolf: 1,
|
||||
},
|
||||
});
|
||||
randomStub = sandbox.stub(Math, 'random').returns(0.1);
|
||||
const task = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
|
||||
const res = await user.post(`/tasks/${task.id}/score/up`);
|
||||
expect(res._tmp.drop).to.be.ok;
|
||||
});
|
||||
|
||||
it('does not give a drop when non-sub drop cap is reached', async () => {
|
||||
user = await generateUser({
|
||||
'stats.gp': 100,
|
||||
'achievements.completedTask': true,
|
||||
'items.eggs': {
|
||||
Wolf: 1,
|
||||
},
|
||||
'items.lastDrop.count': 5,
|
||||
});
|
||||
randomStub = sandbox.stub(Math, 'random').returns(0.1);
|
||||
const task = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
|
||||
const res = await user.post(`/tasks/${task.id}/score/up`);
|
||||
expect(res._tmp.drop).to.be.undefined;
|
||||
});
|
||||
|
||||
it('gives a drop when subscriber is over regular cap but under subscriber cap', async () => {
|
||||
user = await generateUser({
|
||||
'stats.gp': 100,
|
||||
'achievements.completedTask': true,
|
||||
'items.eggs': {
|
||||
Wolf: 1,
|
||||
},
|
||||
'items.lastDrop.count': 6,
|
||||
'purchased.plan.customerId': '123',
|
||||
});
|
||||
randomStub = sandbox.stub(Math, 'random').returns(0.1);
|
||||
const task = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
|
||||
const res = await user.post(`/tasks/${task.id}/score/up`);
|
||||
expect(res._tmp.drop).to.be.ok;
|
||||
});
|
||||
|
||||
it('does not give a drop when subscriber is at subscriber drop cap', async () => {
|
||||
user = await generateUser({
|
||||
'stats.gp': 100,
|
||||
'achievements.completedTask': true,
|
||||
'items.eggs': {
|
||||
Wolf: 1,
|
||||
},
|
||||
'items.lastDrop.count': 10,
|
||||
'purchased.plan.customerId': '123',
|
||||
});
|
||||
randomStub = sandbox.stub(Math, 'random').returns(0.1);
|
||||
const task = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
|
||||
const res = await user.post(`/tasks/${task.id}/score/up`);
|
||||
expect(res._tmp.drop).to.be.undefined;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('todos', () => {
|
||||
|
||||
@@ -105,9 +105,9 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
|
||||
|
||||
const groupTask = await user.get(`/tasks/group/${guild._id}`);
|
||||
|
||||
expect(member.notifications.length).to.equal(2);
|
||||
expect(member.notifications[1].type).to.equal('GROUP_TASK_ASSIGNED');
|
||||
expect(member.notifications[1].taskId).to.equal(groupTask._id);
|
||||
const lastNotification = member.notifications[member.notifications.length - 1];
|
||||
expect(lastNotification.type).to.equal('GROUP_TASK_ASSIGNED');
|
||||
expect(lastNotification.taskId).to.equal(groupTask._id);
|
||||
});
|
||||
|
||||
it('assigns a task to multiple users', async () => {
|
||||
|
||||
@@ -89,10 +89,12 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
|
||||
});
|
||||
|
||||
it('removes task assignment notification from unassigned user', async () => {
|
||||
await member.sync();
|
||||
const oldNotificationCount = member.notifications.length;
|
||||
await user.post(`/tasks/${task._id}/unassign/${member._id}`);
|
||||
|
||||
await member.sync();
|
||||
expect(member.notifications.length).to.equal(1); // mystery items
|
||||
expect(member.notifications.length).to.equal(oldNotificationCount - 1);
|
||||
});
|
||||
|
||||
it('unassigns a user and only that user from a task', async () => {
|
||||
|
||||
@@ -33,6 +33,20 @@ describe('POST /user/purchase/:type/:key', () => {
|
||||
expect(user.items[type][key]).to.equal(1);
|
||||
});
|
||||
|
||||
it('purchases animal ears', async () => {
|
||||
await user.post('/user/purchase/gear/headAccessory_special_tigerEars');
|
||||
await user.sync();
|
||||
|
||||
expect(user.items.gear.owned.headAccessory_special_tigerEars).to.equal(true);
|
||||
});
|
||||
|
||||
it('purchases animal tails', async () => {
|
||||
await user.post('/user/purchase/gear/back_special_pandaTail');
|
||||
await user.sync();
|
||||
|
||||
expect(user.items.gear.owned.back_special_pandaTail).to.equal(true);
|
||||
});
|
||||
|
||||
it('can convert gold to gems if subscribed', async () => {
|
||||
const oldBalance = user.balance;
|
||||
await user.updateOne({
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
|
||||
describe('POST /user/unlock', () => {
|
||||
let user;
|
||||
const unlockPath = 'shirt.convict,shirt.cross,shirt.fire,shirt.horizon,shirt.ocean,shirt.purple,shirt.rainbow,shirt.redblue,shirt.thunder,shirt.tropical,shirt.zombie';
|
||||
const unlockPath = 'shirt.convict,shirt.fire,shirt.horizon,shirt.ocean,shirt.purple,shirt.rainbow,shirt.redblue,shirt.thunder,shirt.tropical,shirt.zombie';
|
||||
const unlockGearSetPath = 'items.gear.owned.headAccessory_special_bearEars,items.gear.owned.headAccessory_special_cactusEars,items.gear.owned.headAccessory_special_foxEars,items.gear.owned.headAccessory_special_lionEars,items.gear.owned.headAccessory_special_pandaEars,items.gear.owned.headAccessory_special_pigEars,items.gear.owned.headAccessory_special_tigerEars,items.gear.owned.headAccessory_special_wolfEars';
|
||||
const unlockCost = 1.25;
|
||||
const usersStartingGems = 5;
|
||||
|
||||
@@ -274,6 +274,14 @@ describe('PUT /user', () => {
|
||||
expect(get(updatedUser.preferences, type)).to.eql(item);
|
||||
});
|
||||
});
|
||||
|
||||
it('updates user when background is unequipped', async () => {
|
||||
expect(get(user.preferences, 'background')).to.not.eql('');
|
||||
|
||||
const updatedUser = await user.put('/user', { 'preferences.background': '' });
|
||||
|
||||
expect(get(updatedUser.preferences, 'background')).to.eql('');
|
||||
});
|
||||
});
|
||||
|
||||
context('Improvement Categories', () => {
|
||||
|
||||
@@ -108,6 +108,20 @@ describe('PUT /user/auth/update-email', () => {
|
||||
const isValidPassword = await bcryptCompare(textPassword, user.auth.local.hashed_password);
|
||||
expect(isValidPassword).to.equal(true);
|
||||
});
|
||||
|
||||
it('invalidates any outstanding password reset code', async () => {
|
||||
await user.updateOne({
|
||||
'auth.local.passwordResetCode': 'impossible-reset-code',
|
||||
});
|
||||
|
||||
await user.put(ENDPOINT, {
|
||||
newEmail: 'bogo@example.com',
|
||||
password: oldPassword,
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
expect(user.auth.local.passwordResetCode).to.not.exist;
|
||||
});
|
||||
});
|
||||
|
||||
context('Social Login User', async () => {
|
||||
|
||||
@@ -11,6 +11,7 @@ const { content } = shared;
|
||||
|
||||
describe('POST /user/buy/:key', () => {
|
||||
let user;
|
||||
let clock;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({
|
||||
@@ -18,6 +19,12 @@ describe('POST /user/buy/:key', () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (clock) {
|
||||
clock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
// More tests in common code unit tests
|
||||
|
||||
it('returns an error if the item is not found', async () => {
|
||||
@@ -68,9 +75,9 @@ describe('POST /user/buy/:key', () => {
|
||||
});
|
||||
|
||||
it('buys a special spell', async () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-10-31T00:00:00Z'));
|
||||
const key = 'spookySparkles';
|
||||
const item = content.special[key];
|
||||
const stub = sinon.stub(item, 'canOwn').returns(true);
|
||||
|
||||
await user.updateOne({ 'stats.gp': 250 });
|
||||
const res = await user.post(`/user/buy/${key}`);
|
||||
@@ -83,8 +90,6 @@ describe('POST /user/buy/:key', () => {
|
||||
expect(res.message).to.equal(t('messageBought', {
|
||||
itemText: item.text(),
|
||||
}));
|
||||
|
||||
stub.restore();
|
||||
});
|
||||
|
||||
it('allows for bulk purchases', async () => {
|
||||
|
||||
@@ -31,7 +31,7 @@ describe('POST /user/buy-mystery-set/:key', () => {
|
||||
|
||||
expect(res.data).to.eql({
|
||||
items: JSON.parse(JSON.stringify(user.items)), // otherwise dates can't be compared
|
||||
purchasedPlanConsecutive: user.purchased.plan.consecutive,
|
||||
purchasedPlanConsecutive: JSON.parse(JSON.stringify(user.purchased.plan.consecutive)),
|
||||
});
|
||||
expect(res.message).to.equal(t('hourglassPurchaseSet'));
|
||||
});
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { TAVERN_ID } from '../../../../../website/server/models/group';
|
||||
import { updateDocument } from '../../../../helpers/mongo';
|
||||
import {
|
||||
requester,
|
||||
resetHabiticaDB,
|
||||
@@ -18,7 +16,9 @@ describe('GET /world-state', () => {
|
||||
});
|
||||
|
||||
it('returns Tavern quest data when world boss is active', async () => {
|
||||
await updateDocument('groups', { _id: TAVERN_ID }, { quest: { active: true, key: 'dysheartener', progress: { hp: 50000, rage: 9999 } } });
|
||||
sinon.stub(worldState, 'getWorldBoss').returns({
|
||||
active: true, extra: {}, key: 'dysheartener', progress: { hp: 50000, rage: 9999, collect: {} },
|
||||
});
|
||||
|
||||
const res = await requester().get('/world-state');
|
||||
expect(res).to.have.nested.property('worldBoss');
|
||||
@@ -33,15 +33,29 @@ describe('GET /world-state', () => {
|
||||
rage: 9999,
|
||||
},
|
||||
});
|
||||
worldState.getWorldBoss.restore();
|
||||
});
|
||||
|
||||
it('calls getRepeatingEvents for data', async () => {
|
||||
const getRepeatingEventsOnDate = sinon.stub(common.content, 'getRepeatingEventsOnDate').returns([]);
|
||||
const getCurrentGalaEvent = sinon.stub(common.schedule, 'getCurrentGalaEvent').returns({});
|
||||
|
||||
await requester().get('/world-state');
|
||||
|
||||
expect(getRepeatingEventsOnDate).to.have.been.calledOnce;
|
||||
expect(getCurrentGalaEvent).to.have.been.calledOnce;
|
||||
|
||||
getRepeatingEventsOnDate.restore();
|
||||
getCurrentGalaEvent.restore();
|
||||
});
|
||||
|
||||
context('no current event', () => {
|
||||
beforeEach(async () => {
|
||||
sinon.stub(worldState, 'getCurrentEvent').returns(null);
|
||||
sinon.stub(worldState, 'getCurrentEventList').returns([]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
worldState.getCurrentEvent.restore();
|
||||
worldState.getCurrentEventList.restore();
|
||||
});
|
||||
|
||||
it('returns null for the current event when there is none active', async () => {
|
||||
@@ -51,18 +65,18 @@ describe('GET /world-state', () => {
|
||||
});
|
||||
});
|
||||
|
||||
context('no current event', () => {
|
||||
context('active event', () => {
|
||||
const evt = {
|
||||
...common.content.events.fall2020,
|
||||
event: 'fall2020',
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
sinon.stub(worldState, 'getCurrentEvent').returns(evt);
|
||||
sinon.stub(worldState, 'getCurrentEventList').returns([evt]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
worldState.getCurrentEvent.restore();
|
||||
worldState.getCurrentEventList.restore();
|
||||
});
|
||||
|
||||
it('returns the current event when there is an active one', async () => {
|
||||
@@ -71,4 +85,45 @@ describe('GET /world-state', () => {
|
||||
expect(res.currentEvent).to.eql(evt);
|
||||
});
|
||||
});
|
||||
|
||||
context('active event with NPC image suffix', () => {
|
||||
const evt = {
|
||||
...common.content.events.fall2020,
|
||||
event: 'fall2020',
|
||||
npcImageSuffix: 'fall',
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
sinon.stub(worldState, 'getCurrentEventList').returns([evt]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
worldState.getCurrentEventList.restore();
|
||||
});
|
||||
|
||||
it('returns the NPC image suffix when present', async () => {
|
||||
const res = await requester().get('/world-state');
|
||||
|
||||
expect(res.npcImageSuffix).to.equal('fall');
|
||||
});
|
||||
|
||||
it('returns the NPC image suffix with multiple events present', async () => {
|
||||
const evt2 = {
|
||||
...common.content.events.winter2020,
|
||||
event: 'test',
|
||||
};
|
||||
|
||||
const evt3 = {
|
||||
...common.content.events.winter2020,
|
||||
event: 'winter2020',
|
||||
npcImageSuffix: 'winter',
|
||||
};
|
||||
|
||||
worldState.getCurrentEventList.returns([evt, evt2, evt3]);
|
||||
|
||||
const res = await requester().get('/world-state');
|
||||
|
||||
expect(res.npcImageSuffix).to.equal('fall');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
104
test/api/v4/inbox/POST-inbox_message_like.test.js
Normal file
104
test/api/v4/inbox/POST-inbox_message_like.test.js
Normal file
@@ -0,0 +1,104 @@
|
||||
import find from 'lodash/find';
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../helpers/api-integration/v4';
|
||||
|
||||
/**
|
||||
* Checks the messages array if the uniqueMessageId has the like flag
|
||||
* @param {InboxMessage[]} messages
|
||||
* @param {String} uniqueMessageId
|
||||
* @param {String} userId
|
||||
* @param {Boolean} likeStatus
|
||||
*/
|
||||
function expectMessagesLikeStatus (messages, uniqueMessageId, userId, likeStatus) {
|
||||
const messageToCheck = find(messages, { uniqueMessageId });
|
||||
|
||||
expect(messageToCheck.likes[userId]).to.equal(likeStatus);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line mocha/no-exclusive-tests
|
||||
describe('POST /inbox/like-private-message/:messageId', () => {
|
||||
let userToSendMessage;
|
||||
const getLikeUrl = messageId => `/inbox/like-private-message/${messageId}`;
|
||||
|
||||
before(async () => {
|
||||
userToSendMessage = await generateUser();
|
||||
});
|
||||
|
||||
it('Returns an error when private message is not found', async () => {
|
||||
await expect(userToSendMessage.post(getLikeUrl('some-unknown-id')))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('messageGroupChatNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('Likes a message', async () => {
|
||||
const receiver = await generateUser();
|
||||
|
||||
const sentMessageResult = await userToSendMessage.post('/members/send-private-message', {
|
||||
message: 'some message :)',
|
||||
toUserId: receiver._id,
|
||||
});
|
||||
|
||||
const { uniqueMessageId } = sentMessageResult.message;
|
||||
|
||||
const likeResult = await receiver.post(getLikeUrl(uniqueMessageId));
|
||||
expect(likeResult.likes[receiver._id]).to.equal(true);
|
||||
|
||||
const senderMessages = await userToSendMessage.get('/inbox/messages');
|
||||
|
||||
expectMessagesLikeStatus(senderMessages, uniqueMessageId, receiver._id, true);
|
||||
|
||||
const receiversMessages = await receiver.get('/inbox/messages');
|
||||
|
||||
expectMessagesLikeStatus(receiversMessages, uniqueMessageId, receiver._id, true);
|
||||
});
|
||||
|
||||
it('Allows to likes their own private message', async () => {
|
||||
const receiver = await generateUser();
|
||||
|
||||
const sentMessageResult = await userToSendMessage.post('/members/send-private-message', {
|
||||
message: 'some message :)',
|
||||
toUserId: receiver._id,
|
||||
});
|
||||
|
||||
const { uniqueMessageId } = sentMessageResult.message;
|
||||
|
||||
const likeResult = await userToSendMessage.post(getLikeUrl(uniqueMessageId));
|
||||
expect(likeResult.likes[userToSendMessage._id]).to.equal(true);
|
||||
|
||||
const messages = await userToSendMessage.get('/inbox/messages');
|
||||
expectMessagesLikeStatus(messages, uniqueMessageId, userToSendMessage._id, true);
|
||||
|
||||
const receiversMessages = await receiver.get('/inbox/messages');
|
||||
|
||||
expectMessagesLikeStatus(receiversMessages, uniqueMessageId, userToSendMessage._id, true);
|
||||
});
|
||||
|
||||
it('Unlikes a message', async () => {
|
||||
const receiver = await generateUser();
|
||||
|
||||
const sentMessageResult = await userToSendMessage.post('/members/send-private-message', {
|
||||
message: 'some message :)',
|
||||
toUserId: receiver._id,
|
||||
});
|
||||
|
||||
const { uniqueMessageId } = sentMessageResult.message;
|
||||
|
||||
const likeResult = await receiver.post(getLikeUrl(uniqueMessageId));
|
||||
|
||||
expect(likeResult.likes[receiver._id]).to.equal(true);
|
||||
|
||||
const unlikeResult = await receiver.post(getLikeUrl(uniqueMessageId));
|
||||
|
||||
expect(unlikeResult.likes[receiver._id]).to.equal(false);
|
||||
|
||||
const messages = await userToSendMessage.get('/inbox/messages');
|
||||
|
||||
const messageToCheck = find(messages, { id: sentMessageResult.message.id });
|
||||
expect(messageToCheck.likes[receiver._id]).to.equal(false);
|
||||
});
|
||||
});
|
||||
@@ -40,6 +40,24 @@ describe('GET /user', () => {
|
||||
expect(returnedUser.stats).to.not.exist;
|
||||
});
|
||||
|
||||
it('returns when ALWAYS_LOADED paths are requested', async () => {
|
||||
const returnedUser = await user.get('/user?userFields=_id,notifications,preferences,auth,flags,permissions');
|
||||
|
||||
expect(returnedUser._id).to.equal(user._id);
|
||||
expect(returnedUser.notifications).to.exist;
|
||||
expect(returnedUser.preferences).to.exist;
|
||||
expect(returnedUser.auth).to.exist;
|
||||
expect(returnedUser.flags).to.exist;
|
||||
expect(returnedUser.permissions).to.exist;
|
||||
});
|
||||
|
||||
it('returns when subpaths paths are requested', async () => {
|
||||
const returnedUser = await user.get('/user?userFields=auth.local.username');
|
||||
|
||||
expect(returnedUser._id).to.equal(user._id);
|
||||
expect(returnedUser.auth.local.username).to.exist;
|
||||
});
|
||||
|
||||
it('does not return requested private properties', async () => {
|
||||
const returnedUser = await user.get('/user?userFields=apiToken,secret.text');
|
||||
|
||||
|
||||
67
test/common/fns/datedMemoize.test.js
Normal file
67
test/common/fns/datedMemoize.test.js
Normal file
@@ -0,0 +1,67 @@
|
||||
/* eslint-disable global-require */
|
||||
import { expect } from 'chai';
|
||||
import nconf from 'nconf';
|
||||
|
||||
const SWITCHOVER_TIME = nconf.get('CONTENT_SWITCHOVER_TIME_OFFSET') || 0;
|
||||
|
||||
describe('datedMemoize', () => {
|
||||
it('should return a function that returns a function', () => {
|
||||
const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default;
|
||||
const memoized = datedMemoize(() => {});
|
||||
expect(memoized).to.be.a('function');
|
||||
});
|
||||
|
||||
it('should not call multiple times', () => {
|
||||
const stub = sandbox.stub().returns({});
|
||||
const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default;
|
||||
const memoized = datedMemoize(stub);
|
||||
memoized(1, 2);
|
||||
memoized(1, 3);
|
||||
expect(stub).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('call multiple times for different identifiers', () => {
|
||||
const stub = sandbox.stub().returns({});
|
||||
const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default;
|
||||
const memoized = datedMemoize(stub);
|
||||
memoized({ identifier: 'a', memoizeConfig: true }, 1, 2);
|
||||
memoized({ identifier: 'b', memoizeConfig: true }, 1, 2);
|
||||
expect(stub).to.have.been.calledTwice;
|
||||
});
|
||||
|
||||
it('call once for the same identifier', () => {
|
||||
const stub = sandbox.stub().returns({});
|
||||
const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default;
|
||||
const memoized = datedMemoize(stub);
|
||||
memoized({ identifier: 'a', memoizeConfig: true }, 1, 2);
|
||||
memoized({ identifier: 'a', memoizeConfig: true }, 1, 2);
|
||||
expect(stub).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('call once on the same day', () => {
|
||||
const stub = sandbox.stub().returns({});
|
||||
const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default;
|
||||
const memoized = datedMemoize(stub);
|
||||
memoized({ date: new Date('2024-01-01'), memoizeConfig: true }, 1, 2);
|
||||
memoized({ date: new Date('2024-01-01'), memoizeConfig: true }, 1, 2);
|
||||
expect(stub).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('call multiple times on different days', () => {
|
||||
const stub = sandbox.stub().returns({});
|
||||
const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default;
|
||||
const memoized = datedMemoize(stub);
|
||||
memoized({ date: new Date('2024-01-01'), memoizeConfig: true }, 1, 2);
|
||||
memoized({ date: new Date('2024-01-02'), memoizeConfig: true }, 1, 2);
|
||||
expect(stub).to.have.been.calledTwice;
|
||||
});
|
||||
|
||||
it('respects switchover time', () => {
|
||||
const stub = sandbox.stub().returns({});
|
||||
const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default;
|
||||
const memoized = datedMemoize(stub);
|
||||
memoized({ date: new Date('2024-01-01T00:00:00.000Z'), memoizeConfig: true }, 1, 2);
|
||||
memoized({ date: new Date(`2024-01-01T${String(SWITCHOVER_TIME).padStart(2, '0')}`), memoizeConfig: true }, 1, 2);
|
||||
expect(stub).to.have.been.calledTwice;
|
||||
});
|
||||
});
|
||||
123
test/common/libs/cleanupPinnedItems.test.js
Normal file
123
test/common/libs/cleanupPinnedItems.test.js
Normal file
@@ -0,0 +1,123 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
import cleanupPinnedItems from '../../../website/common/script/libs/cleanupPinnedItems';
|
||||
|
||||
describe('cleanupPinnedItems', () => {
|
||||
let user;
|
||||
let testPinnedItems;
|
||||
let clock;
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
clock = sinon.useFakeTimers(new Date('2024-04-08'));
|
||||
|
||||
testPinnedItems = [
|
||||
{ type: 'armoire', path: 'armoire' },
|
||||
{ type: 'potion', path: 'potion' },
|
||||
{ type: 'background', path: 'backgrounds.backgrounds042020.heather_field' },
|
||||
{ type: 'background', path: 'backgrounds.backgrounds042021.heather_field' },
|
||||
{ type: 'premiumHatchingPotion', path: 'premiumHatchingPotions.Rainbow' },
|
||||
{ type: 'premiumHatchingPotion', path: 'premiumHatchingPotions.StainedGlass' },
|
||||
{ type: 'quests', path: 'quests.rat' },
|
||||
{ type: 'quests', path: 'quests.spider' },
|
||||
{ type: 'quests', path: 'quests.moon1' },
|
||||
{ type: 'quests', path: 'quests.silver' },
|
||||
{ type: 'marketGear', path: 'gear.flat.head_special_nye2021' },
|
||||
{ type: 'gear', path: 'gear.flat.armor_special_spring2019Rogue' },
|
||||
{ type: 'gear', path: 'gear.flat.armor_special_winter2021Rogue' },
|
||||
{ type: 'mystery_set', path: 'mystery.201804' },
|
||||
{ type: 'mystery_set', path: 'mystery.201506' },
|
||||
{ type: 'bundles', path: 'bundles.farmFriends' },
|
||||
{ type: 'bundles', path: 'bundles.birdBuddies' },
|
||||
{ type: 'customization', path: 'skin.birdBuddies' },
|
||||
];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clock.restore();
|
||||
});
|
||||
it('always keeps armoire and potion', () => {
|
||||
user.pinnedItems = testPinnedItems;
|
||||
|
||||
const result = cleanupPinnedItems(user);
|
||||
expect(_.find(result, item => item.path === 'armoire')).to.exist;
|
||||
expect(_.find(result, item => item.path === 'potion')).to.exist;
|
||||
});
|
||||
|
||||
it('removes simple items that are no longer available', () => {
|
||||
user.pinnedItems = testPinnedItems;
|
||||
|
||||
const result = cleanupPinnedItems(user);
|
||||
expect(_.find(result, item => item.path === 'backgrounds.backgrounds042021.heather_field')).to.not.exist;
|
||||
expect(_.find(result, item => item.path === 'premiumHatchingPotions.Rainbow')).to.not.exist;
|
||||
});
|
||||
|
||||
it('keeps simple items that are still available', () => {
|
||||
user.pinnedItems = testPinnedItems;
|
||||
const result = cleanupPinnedItems(user);
|
||||
expect(_.find(result, item => item.path === 'backgrounds.backgrounds042020.heather_field')).to.exist;
|
||||
expect(_.find(result, item => item.path === 'premiumHatchingPotions.StainedGlass')).to.exist;
|
||||
});
|
||||
|
||||
it('removes gear that is no longer available', () => {
|
||||
user.pinnedItems = testPinnedItems;
|
||||
const result = cleanupPinnedItems(user);
|
||||
expect(_.find(result, item => item.path === 'gear.flat.armor_special_winter2021Rogue')).to.not.exist;
|
||||
});
|
||||
|
||||
it('keeps gear that is still available', () => {
|
||||
user.pinnedItems = testPinnedItems;
|
||||
const result = cleanupPinnedItems(user);
|
||||
expect(_.find(result, item => item.path === 'gear.flat.armor_special_spring2019Rogue')).to.exist;
|
||||
});
|
||||
|
||||
it('keeps gear that is not seasonal', () => {
|
||||
user.pinnedItems = testPinnedItems;
|
||||
const result = cleanupPinnedItems(user);
|
||||
expect(_.find(result, item => item.path === 'gear.flat.head_special_nye2021')).to.exist;
|
||||
});
|
||||
|
||||
it('removes time traveler gear that is no longer available', () => {
|
||||
user.pinnedItems = testPinnedItems;
|
||||
const result = cleanupPinnedItems(user);
|
||||
expect(_.find(result, item => item.path === 'mystery.201506')).to.not.exist;
|
||||
});
|
||||
|
||||
it('keeps time traveler gear that is still available', () => {
|
||||
user.pinnedItems = testPinnedItems;
|
||||
const result = cleanupPinnedItems(user);
|
||||
expect(_.find(result, item => item.path === 'mystery.201804')).to.exist;
|
||||
});
|
||||
|
||||
it('removes quests that are no longer available', () => {
|
||||
user.pinnedItems = testPinnedItems;
|
||||
const result = cleanupPinnedItems(user);
|
||||
expect(_.find(result, item => item.path === 'quests.rat')).to.not.exist;
|
||||
expect(_.find(result, item => item.path === 'quests.silver')).to.not.exist;
|
||||
});
|
||||
|
||||
it('keeps quests that are still available', () => {
|
||||
user.pinnedItems = testPinnedItems;
|
||||
const result = cleanupPinnedItems(user);
|
||||
expect(_.find(result, item => item.path === 'quests.spider')).to.exist;
|
||||
});
|
||||
|
||||
it('keeps quests that are not seasonal', () => {
|
||||
user.pinnedItems = testPinnedItems;
|
||||
const result = cleanupPinnedItems(user);
|
||||
expect(_.find(result, item => item.path === 'quests.moon1')).to.exist;
|
||||
});
|
||||
|
||||
it('removes bundles that are no longer available', () => {
|
||||
user.pinnedItems = testPinnedItems;
|
||||
const result = cleanupPinnedItems(user);
|
||||
expect(_.find(result, item => item.path === 'bundles.farmFriends')).to.not.exist;
|
||||
});
|
||||
|
||||
it('keeps bundles that are still available', () => {
|
||||
user.pinnedItems = testPinnedItems;
|
||||
const result = cleanupPinnedItems(user);
|
||||
expect(_.find(result, item => item.path === 'bundles.birdBuddies')).to.exist;
|
||||
});
|
||||
});
|
||||
@@ -183,8 +183,6 @@ describe('cron utility functions', () => {
|
||||
});
|
||||
|
||||
describe('getPlanContext', () => {
|
||||
const now = new Date(2022, 5, 1);
|
||||
|
||||
function baseUserData (count, offset, planId) {
|
||||
return {
|
||||
purchased: {
|
||||
@@ -192,7 +190,7 @@ describe('cron utility functions', () => {
|
||||
consecutive: {
|
||||
count,
|
||||
offset,
|
||||
gemCapExtra: 25,
|
||||
gemCapExtra: 26,
|
||||
trinkets: 19,
|
||||
},
|
||||
quantity: 1,
|
||||
@@ -213,52 +211,19 @@ describe('cron utility functions', () => {
|
||||
};
|
||||
}
|
||||
|
||||
it('monthly plan, next date in 3 months', () => {
|
||||
it('elapsedMonths is 0 if its the same month', () => {
|
||||
const user = baseUserData(60, 0, 'group_plan_auto');
|
||||
user.purchased.plan.perkMonthCount = 0;
|
||||
|
||||
const planContext = getPlanContext(user, now);
|
||||
|
||||
expect(planContext.nextHourglassDate)
|
||||
.to.be.sameMoment('2022-08-10T02:00:00.144Z');
|
||||
const planContext = getPlanContext(user, new Date(2022, 4, 20));
|
||||
expect(planContext.elapsedMonths).to.equal(0);
|
||||
});
|
||||
|
||||
it('monthly plan, next date in 1 month', () => {
|
||||
const user = baseUserData(62, 0, 'group_plan_auto');
|
||||
user.purchased.plan.perkMonthCount = 2;
|
||||
it('elapsedMonths is 1 after one month', () => {
|
||||
const user = baseUserData(60, 0, 'group_plan_auto');
|
||||
|
||||
const planContext = getPlanContext(user, now);
|
||||
const planContext = getPlanContext(user, new Date(2022, 5, 11));
|
||||
|
||||
expect(planContext.nextHourglassDate)
|
||||
.to.be.sameMoment('2022-06-10T02:00:00.144Z');
|
||||
});
|
||||
|
||||
it('multi-month plan, no offset', () => {
|
||||
const user = baseUserData(60, 0, 'basic_3mo');
|
||||
|
||||
const planContext = getPlanContext(user, now);
|
||||
|
||||
expect(planContext.nextHourglassDate)
|
||||
.to.be.sameMoment('2022-06-10T02:00:00.144Z');
|
||||
});
|
||||
|
||||
it('multi-month plan with offset', () => {
|
||||
const user = baseUserData(60, 1, 'basic_3mo');
|
||||
|
||||
const planContext = getPlanContext(user, now);
|
||||
|
||||
expect(planContext.nextHourglassDate)
|
||||
.to.be.sameMoment('2022-07-10T02:00:00.144Z');
|
||||
});
|
||||
|
||||
it('multi-month plan with perk count', () => {
|
||||
const user = baseUserData(60, 1, 'basic_3mo');
|
||||
user.purchased.plan.perkMonthCount = 2;
|
||||
|
||||
const planContext = getPlanContext(user, now);
|
||||
|
||||
expect(planContext.nextHourglassDate)
|
||||
.to.be.sameMoment('2022-07-10T02:00:00.144Z');
|
||||
expect(planContext.elapsedMonths).to.equal(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,219 +0,0 @@
|
||||
import shared from '../../../website/common';
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
|
||||
describe('shops', () => {
|
||||
const user = generateUser();
|
||||
|
||||
describe('market', () => {
|
||||
const shopCategories = shared.shops.getMarketCategories(user);
|
||||
|
||||
it('contains at least the 3 default categories', () => {
|
||||
expect(shopCategories.length).to.be.greaterThan(2);
|
||||
});
|
||||
|
||||
it('does not contain an empty category', () => {
|
||||
_.each(shopCategories, category => {
|
||||
expect(category.items.length).to.be.greaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not duplicate identifiers', () => {
|
||||
const identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
|
||||
|
||||
expect(identifiers.length).to.eql(shopCategories.length);
|
||||
});
|
||||
|
||||
it('items contain required fields', () => {
|
||||
_.each(shopCategories, category => {
|
||||
_.each(category.items, item => {
|
||||
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'class'], key => {
|
||||
expect(_.has(item, key)).to.eql(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('shows relevant non class gear in special category', () => {
|
||||
const contributor = generateUser({
|
||||
contributor: {
|
||||
level: 7,
|
||||
critical: true,
|
||||
},
|
||||
items: {
|
||||
gear: {
|
||||
owned: {
|
||||
weapon_armoire_basicCrossbow: true, // eslint-disable-line camelcase
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const gearCategories = shared.shops.getMarketGearCategories(contributor);
|
||||
const specialCategory = gearCategories.find(o => o.identifier === 'none');
|
||||
expect(specialCategory.items.find(item => item.key === 'weapon_special_1'));
|
||||
expect(specialCategory.items.find(item => item.key === 'armor_special_1'));
|
||||
expect(specialCategory.items.find(item => item.key === 'head_special_1'));
|
||||
expect(specialCategory.items.find(item => item.key === 'shield_special_1'));
|
||||
expect(specialCategory.items.find(item => item.key === 'weapon_special_critical'));
|
||||
expect(specialCategory.items.find(item => item.key === 'weapon_armoire_basicCrossbow'));// eslint-disable-line camelcase
|
||||
});
|
||||
|
||||
it('does not show gear when it is all owned', () => {
|
||||
const userWithItems = generateUser({
|
||||
stats: {
|
||||
class: 'wizard',
|
||||
},
|
||||
items: {
|
||||
gear: {
|
||||
owned: {
|
||||
weapon_wizard_0: true, // eslint-disable-line camelcase
|
||||
weapon_wizard_1: true, // eslint-disable-line camelcase
|
||||
weapon_wizard_2: true, // eslint-disable-line camelcase
|
||||
weapon_wizard_3: true, // eslint-disable-line camelcase
|
||||
weapon_wizard_4: true, // eslint-disable-line camelcase
|
||||
weapon_wizard_5: true, // eslint-disable-line camelcase
|
||||
weapon_wizard_6: true, // eslint-disable-line camelcase
|
||||
armor_wizard_1: true, // eslint-disable-line camelcase
|
||||
armor_wizard_2: true, // eslint-disable-line camelcase
|
||||
armor_wizard_3: true, // eslint-disable-line camelcase
|
||||
armor_wizard_4: true, // eslint-disable-line camelcase
|
||||
armor_wizard_5: true, // eslint-disable-line camelcase
|
||||
head_wizard_1: true, // eslint-disable-line camelcase
|
||||
head_wizard_2: true, // eslint-disable-line camelcase
|
||||
head_wizard_3: true, // eslint-disable-line camelcase
|
||||
head_wizard_4: true, // eslint-disable-line camelcase
|
||||
head_wizard_5: true, // eslint-disable-line camelcase
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const shopWizardItems = shared.shops.getMarketGearCategories(userWithItems).find(x => x.identifier === 'wizard').items.filter(x => x.klass === 'wizard' && (x.owned === false || x.owned === undefined));
|
||||
expect(shopWizardItems.length).to.eql(0);
|
||||
});
|
||||
|
||||
it('shows available gear not yet purchased and previously owned', () => {
|
||||
const userWithItems = generateUser({
|
||||
stats: {
|
||||
class: 'wizard',
|
||||
},
|
||||
items: {
|
||||
gear: {
|
||||
owned: {
|
||||
weapon_wizard_0: true, // eslint-disable-line camelcase
|
||||
weapon_wizard_1: true, // eslint-disable-line camelcase
|
||||
weapon_wizard_2: true, // eslint-disable-line camelcase
|
||||
weapon_wizard_3: true, // eslint-disable-line camelcase
|
||||
weapon_wizard_4: true, // eslint-disable-line camelcase
|
||||
armor_wizard_1: true, // eslint-disable-line camelcase
|
||||
armor_wizard_2: true, // eslint-disable-line camelcase
|
||||
armor_wizard_3: false, // eslint-disable-line camelcase
|
||||
armor_wizard_4: false, // eslint-disable-line camelcase
|
||||
head_wizard_1: true, // eslint-disable-line camelcase
|
||||
head_wizard_2: false, // eslint-disable-line camelcase
|
||||
head_wizard_3: true, // eslint-disable-line camelcase
|
||||
head_wizard_4: false, // eslint-disable-line camelcase
|
||||
head_wizard_5: true, // eslint-disable-line camelcase
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const shopWizardItems = shared.shops.getMarketGearCategories(userWithItems).find(x => x.identifier === 'wizard').items.filter(x => x.klass === 'wizard' && (x.owned === false || x.owned === undefined));
|
||||
expect(shopWizardItems.find(item => item.key === 'weapon_wizard_5').locked).to.eql(false);
|
||||
expect(shopWizardItems.find(item => item.key === 'weapon_wizard_6').locked).to.eql(true);
|
||||
expect(shopWizardItems.find(item => item.key === 'armor_wizard_3').locked).to.eql(false);
|
||||
expect(shopWizardItems.find(item => item.key === 'armor_wizard_4').locked).to.eql(true);
|
||||
expect(shopWizardItems.find(item => item.key === 'head_wizard_2').locked).to.eql(false);
|
||||
expect(shopWizardItems.find(item => item.key === 'head_wizard_4').locked).to.eql(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('questShop', () => {
|
||||
const shopCategories = shared.shops.getQuestShopCategories(user);
|
||||
|
||||
it('does not contain an empty category', () => {
|
||||
_.each(shopCategories, category => {
|
||||
expect(category.items.length).to.be.greaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not duplicate identifiers', () => {
|
||||
const identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
|
||||
|
||||
expect(identifiers.length).to.eql(shopCategories.length);
|
||||
});
|
||||
|
||||
it('items contain required fields', () => {
|
||||
_.each(shopCategories, category => {
|
||||
if (category.identifier === 'bundle') {
|
||||
_.each(category.items, item => {
|
||||
_.each(['key', 'text', 'notes', 'value', 'currency', 'purchaseType', 'class'], key => {
|
||||
expect(_.has(item, key)).to.eql(true);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
_.each(category.items, item => {
|
||||
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'boss', 'class', 'collect', 'drop', 'unlockCondition', 'lvl'], key => {
|
||||
expect(_.has(item, key)).to.eql(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('timeTravelers', () => {
|
||||
const shopCategories = shared.shops.getTimeTravelersCategories(user);
|
||||
|
||||
it('does not contain an empty category', () => {
|
||||
_.each(shopCategories, category => {
|
||||
expect(category.items.length).to.be.greaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not duplicate identifiers', () => {
|
||||
const identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
|
||||
|
||||
expect(identifiers.length).to.eql(shopCategories.length);
|
||||
});
|
||||
|
||||
it('items contain required fields', () => {
|
||||
_.each(shopCategories, category => {
|
||||
_.each(category.items, item => {
|
||||
_.each(['key', 'text', 'value', 'currency', 'locked', 'purchaseType', 'class', 'notes', 'class'], key => {
|
||||
expect(_.has(item, key)).to.eql(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('seasonalShop', () => {
|
||||
const shopCategories = shared.shops.getSeasonalShopCategories(user);
|
||||
|
||||
it('does not contain an empty category', () => {
|
||||
_.each(shopCategories, category => {
|
||||
expect(category.items.length).to.be.greaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not duplicate identifiers', () => {
|
||||
const identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
|
||||
|
||||
expect(identifiers.length).to.eql(shopCategories.length);
|
||||
});
|
||||
|
||||
it('items contain required fields', () => {
|
||||
_.each(shopCategories, category => {
|
||||
_.each(category.items, item => {
|
||||
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'type'], key => {
|
||||
expect(_.has(item, key)).to.eql(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
442
test/common/libs/shops.test.js
Normal file
442
test/common/libs/shops.test.js
Normal file
@@ -0,0 +1,442 @@
|
||||
import shared from '../../../website/common';
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
|
||||
import seasonalConfig from '../../../website/common/script/libs/shops-seasonal.config';
|
||||
|
||||
describe('shops', () => {
|
||||
const user = generateUser();
|
||||
let clock;
|
||||
|
||||
afterEach(() => {
|
||||
if (clock) {
|
||||
clock.restore();
|
||||
}
|
||||
user.achievements.quests = {};
|
||||
});
|
||||
|
||||
describe('market', () => {
|
||||
const shopCategories = shared.shops.getMarketCategories(user);
|
||||
|
||||
it('contains at least the 3 default categories', () => {
|
||||
expect(shopCategories.length).to.be.greaterThan(2);
|
||||
});
|
||||
|
||||
it('does not contain an empty category', () => {
|
||||
_.each(shopCategories, category => {
|
||||
expect(category.items.length).to.be.greaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not duplicate identifiers', () => {
|
||||
const identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
|
||||
|
||||
expect(identifiers.length).to.eql(shopCategories.length);
|
||||
});
|
||||
|
||||
it('items contain required fields', () => {
|
||||
_.each(shopCategories, category => {
|
||||
_.each(category.items, item => {
|
||||
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'class'], key => {
|
||||
expect(_.has(item, key)).to.eql(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('premium hatching potions', () => {
|
||||
it('contains current scheduled premium hatching potions', async () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-04-01T09:00:00.000Z'));
|
||||
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
|
||||
expect(potions.items.length).to.eql(3);
|
||||
});
|
||||
|
||||
it('does not contain past scheduled premium hatching potions', async () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-04-01T09:00:00.000Z'));
|
||||
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
|
||||
expect(potions.items.filter(x => x.key === 'Aquatic' || x.key === 'Celestial').length, 'Aquatic or Celestial found').to.eql(0);
|
||||
});
|
||||
|
||||
it('returns end date for scheduled premium potions', async () => {
|
||||
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
|
||||
potions.items.forEach(potion => {
|
||||
expect(potion.end).to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
it('contains unlocked quest premium hatching potions', async () => {
|
||||
user.achievements.quests = {
|
||||
bronze: 1,
|
||||
blackPearl: 1,
|
||||
};
|
||||
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
|
||||
expect(potions.items.filter(x => x.key === 'Bronze' || x.key === 'BlackPearl').length).to.eql(2);
|
||||
});
|
||||
|
||||
it('does not contain locked quest premium hatching potions', async () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-04-01T09:00:00.000Z'));
|
||||
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
|
||||
expect(potions.items.length).to.eql(3);
|
||||
expect(potions.items.filter(x => x.key === 'Bronze' || x.key === 'BlackPearl').length).to.eql(0);
|
||||
});
|
||||
|
||||
it('does not return end date for quest premium potions', async () => {
|
||||
user.achievements.quests = {
|
||||
bronze: 1,
|
||||
blackPearl: 1,
|
||||
};
|
||||
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
|
||||
potions.items.filter(x => x.key === 'Bronze' || x.key === 'BlackPearl').forEach(potion => {
|
||||
expect(potion.end).to.not.exist;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does not return items with event data', async () => {
|
||||
shopCategories.forEach(category => {
|
||||
category.items.forEach(item => {
|
||||
expect(item.event).to.not.exist;
|
||||
expect(item.season).to.not.exist;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('shows relevant non class gear in special category', () => {
|
||||
const contributor = generateUser({
|
||||
contributor: {
|
||||
level: 7,
|
||||
critical: true,
|
||||
},
|
||||
items: {
|
||||
gear: {
|
||||
owned: {
|
||||
weapon_armoire_basicCrossbow: true, // eslint-disable-line camelcase
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const gearCategories = shared.shops.getMarketGearCategories(contributor);
|
||||
const specialCategory = gearCategories.find(o => o.identifier === 'none');
|
||||
expect(specialCategory.items.find(item => item.key === 'weapon_special_1'), 'weapon_special_1');
|
||||
expect(specialCategory.items.find(item => item.key === 'armor_special_1'), 'armor_special_1');
|
||||
expect(specialCategory.items.find(item => item.key === 'head_special_1'), 'head_special_1');
|
||||
expect(specialCategory.items.find(item => item.key === 'shield_special_1'), 'shield_special_1');
|
||||
expect(specialCategory.items.find(item => item.key === 'weapon_special_critical'), 'weapon_special_critical');
|
||||
expect(specialCategory.items.find(item => item.key === 'weapon_armoire_basicCrossbow'), 'weapon_armoire_basicCrossbow');// eslint-disable-line camelcase
|
||||
});
|
||||
|
||||
describe('handles seasonal gear', () => {
|
||||
beforeEach(() => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-04-01'));
|
||||
});
|
||||
|
||||
it('shows current seasonal gear for warriors', () => {
|
||||
const warriorItems = shared.shops.getMarketGearCategories(user).find(x => x.identifier === 'warrior').items.filter(x => x.key.indexOf('spring2024') !== -1);
|
||||
expect(warriorItems.length, 'Warrior seasonal gear').to.eql(4);
|
||||
});
|
||||
|
||||
it('shows current seasonal gear for mages', () => {
|
||||
const mageItems = shared.shops.getMarketGearCategories(user).find(x => x.identifier === 'wizard').items.filter(x => x.key.indexOf('spring2024') !== -1);
|
||||
expect(mageItems.length, 'Mage seasonal gear').to.eql(3);
|
||||
});
|
||||
|
||||
it('shows current seasonal gear for healers', () => {
|
||||
const healerItems = shared.shops.getMarketGearCategories(user).find(x => x.identifier === 'healer').items.filter(x => x.key.indexOf('spring2024') !== -1);
|
||||
expect(healerItems.length, 'Healer seasonal gear').to.eql(4);
|
||||
});
|
||||
|
||||
it('shows current seasonal gear for rogues', () => {
|
||||
const rogueItems = shared.shops.getMarketGearCategories(user).find(x => x.identifier === 'rogue').items.filter(x => x.key.indexOf('spring2024') !== -1);
|
||||
expect(rogueItems.length, 'Rogue seasonal gear').to.eql(4);
|
||||
});
|
||||
|
||||
it('seasonal gear contains end date', () => {
|
||||
const categories = shared.shops.getMarketGearCategories(user);
|
||||
categories.forEach(category => {
|
||||
category.items.filter(x => x.key.indexOf('spring2024') !== -1).forEach(item => {
|
||||
expect(item.end, item.key).to.exist;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('only shows gear for the current season', () => {
|
||||
const categories = shared.shops.getMarketGearCategories(user);
|
||||
categories.forEach(category => {
|
||||
const otherSeasons = category.items.filter(item => item.key.indexOf('winter') !== -1 || item.key.indexOf('summer') !== -1 || item.key.indexOf('fall') !== -1);
|
||||
expect(otherSeasons.length).to.eql(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not show gear from past seasons', () => {
|
||||
const categories = shared.shops.getMarketGearCategories(user);
|
||||
categories.forEach(category => {
|
||||
const otherYears = category.items.filter(item => item.key.indexOf('spring') !== -1 && item.key.indexOf('2024') === -1);
|
||||
expect(otherYears.length).to.eql(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does not show gear when it is all owned', () => {
|
||||
const userWithItems = generateUser({
|
||||
stats: {
|
||||
class: 'wizard',
|
||||
},
|
||||
items: {
|
||||
gear: {
|
||||
owned: {
|
||||
weapon_wizard_0: true, // eslint-disable-line camelcase
|
||||
weapon_wizard_1: true, // eslint-disable-line camelcase
|
||||
weapon_wizard_2: true, // eslint-disable-line camelcase
|
||||
weapon_wizard_3: true, // eslint-disable-line camelcase
|
||||
weapon_wizard_4: true, // eslint-disable-line camelcase
|
||||
weapon_wizard_5: true, // eslint-disable-line camelcase
|
||||
weapon_wizard_6: true, // eslint-disable-line camelcase
|
||||
armor_wizard_1: true, // eslint-disable-line camelcase
|
||||
armor_wizard_2: true, // eslint-disable-line camelcase
|
||||
armor_wizard_3: true, // eslint-disable-line camelcase
|
||||
armor_wizard_4: true, // eslint-disable-line camelcase
|
||||
armor_wizard_5: true, // eslint-disable-line camelcase
|
||||
head_wizard_1: true, // eslint-disable-line camelcase
|
||||
head_wizard_2: true, // eslint-disable-line camelcase
|
||||
head_wizard_3: true, // eslint-disable-line camelcase
|
||||
head_wizard_4: true, // eslint-disable-line camelcase
|
||||
head_wizard_5: true, // eslint-disable-line camelcase
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const shopWizardItems = shared.shops.getMarketGearCategories(userWithItems).find(x => x.identifier === 'wizard').items.filter(x => x.klass === 'wizard' && (x.owned === false || x.owned === undefined));
|
||||
expect(shopWizardItems.length).to.eql(0);
|
||||
});
|
||||
|
||||
it('shows available gear not yet purchased and previously owned', () => {
|
||||
const userWithItems = generateUser({
|
||||
stats: {
|
||||
class: 'wizard',
|
||||
},
|
||||
items: {
|
||||
gear: {
|
||||
owned: {
|
||||
weapon_wizard_0: true, // eslint-disable-line camelcase
|
||||
weapon_wizard_1: true, // eslint-disable-line camelcase
|
||||
weapon_wizard_2: true, // eslint-disable-line camelcase
|
||||
weapon_wizard_3: true, // eslint-disable-line camelcase
|
||||
weapon_wizard_4: true, // eslint-disable-line camelcase
|
||||
armor_wizard_1: true, // eslint-disable-line camelcase
|
||||
armor_wizard_2: true, // eslint-disable-line camelcase
|
||||
armor_wizard_3: false, // eslint-disable-line camelcase
|
||||
armor_wizard_4: false, // eslint-disable-line camelcase
|
||||
head_wizard_1: true, // eslint-disable-line camelcase
|
||||
head_wizard_2: false, // eslint-disable-line camelcase
|
||||
head_wizard_3: true, // eslint-disable-line camelcase
|
||||
head_wizard_4: false, // eslint-disable-line camelcase
|
||||
head_wizard_5: true, // eslint-disable-line camelcase
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const shopWizardItems = shared.shops.getMarketGearCategories(userWithItems).find(x => x.identifier === 'wizard').items.filter(x => x.klass === 'wizard' && (x.owned === false || x.owned === undefined));
|
||||
expect(shopWizardItems.find(item => item.key === 'weapon_wizard_5').locked).to.eql(false);
|
||||
expect(shopWizardItems.find(item => item.key === 'weapon_wizard_6').locked).to.eql(true);
|
||||
expect(shopWizardItems.find(item => item.key === 'armor_wizard_3').locked).to.eql(false);
|
||||
expect(shopWizardItems.find(item => item.key === 'armor_wizard_4').locked).to.eql(true);
|
||||
expect(shopWizardItems.find(item => item.key === 'head_wizard_2').locked).to.eql(false);
|
||||
expect(shopWizardItems.find(item => item.key === 'head_wizard_4').locked).to.eql(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('questShop', () => {
|
||||
const shopCategories = shared.shops.getQuestShopCategories(user);
|
||||
|
||||
it('does not contain an empty category', () => {
|
||||
_.each(shopCategories, category => {
|
||||
expect(category.items.length, category.identifier).to.be.greaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not duplicate identifiers', () => {
|
||||
const identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
|
||||
|
||||
expect(identifiers.length).to.eql(shopCategories.length);
|
||||
});
|
||||
|
||||
it('items contain required fields', () => {
|
||||
_.each(shopCategories, category => {
|
||||
if (category.identifier === 'bundle') {
|
||||
_.each(category.items, item => {
|
||||
_.each(['key', 'text', 'notes', 'value', 'currency', 'purchaseType', 'class'], key => {
|
||||
expect(_.has(item, key)).to.eql(true);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
_.each(category.items, item => {
|
||||
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'boss', 'class', 'collect', 'drop', 'unlockCondition', 'lvl'], key => {
|
||||
expect(_.has(item, key)).to.eql(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('does not return items with event data', async () => {
|
||||
shopCategories.forEach(category => {
|
||||
category.items.forEach(item => {
|
||||
expect(item.event).to.not.exist;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('timeTravelers', () => {
|
||||
const shopCategories = shared.shops.getTimeTravelersCategories(user);
|
||||
|
||||
it('does not contain an empty category', () => {
|
||||
_.each(shopCategories, category => {
|
||||
expect(category.items.length).to.be.greaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not duplicate identifiers', () => {
|
||||
const identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
|
||||
|
||||
expect(identifiers.length).to.eql(shopCategories.length);
|
||||
});
|
||||
|
||||
it('items contain required fields', () => {
|
||||
_.each(shopCategories, category => {
|
||||
_.each(category.items, item => {
|
||||
_.each(['key', 'text', 'value', 'currency', 'locked', 'purchaseType', 'class', 'notes', 'class'], key => {
|
||||
expect(_.has(item, key)).to.eql(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does not return items with event data', async () => {
|
||||
shopCategories.forEach(category => {
|
||||
category.items.forEach(item => {
|
||||
expect(item.event).to.not.exist;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('returns pets', () => {
|
||||
const pets = shopCategories.find(cat => cat.identifier === 'pets').items;
|
||||
expect(pets.length).to.be.greaterThan(0);
|
||||
});
|
||||
|
||||
it('returns mounts', () => {
|
||||
const mounts = shopCategories.find(cat => cat.identifier === 'mounts').items;
|
||||
expect(mounts.length).to.be.greaterThan(0);
|
||||
});
|
||||
|
||||
it('returns quests', () => {
|
||||
const quests = shopCategories.find(cat => cat.identifier === 'quests').items;
|
||||
expect(quests.length).to.be.greaterThan(0);
|
||||
});
|
||||
|
||||
it('returns backgrounds', () => {
|
||||
const backgrounds = shopCategories.find(cat => cat.identifier === 'backgrounds').items;
|
||||
expect(backgrounds.length).to.be.greaterThan(0);
|
||||
});
|
||||
|
||||
it('does not add an end date to steampunk gear', () => {
|
||||
const categories = shopCategories.filter(cat => cat.identifier.startsWith('30'));
|
||||
categories.forEach(category => {
|
||||
expect(category.end).to.not.exist;
|
||||
category.items.forEach(item => {
|
||||
expect(item.end).to.not.exist;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('customizationShop', () => {
|
||||
const shopCategories = shared.shops.getCustomizationsShopCategories(user, null);
|
||||
|
||||
it('does not return items with event data', async () => {
|
||||
shopCategories.forEach(category => {
|
||||
category.items.forEach(item => {
|
||||
expect(item.event, item.key).to.not.exist;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('backgrounds category contains end date', () => {
|
||||
const backgroundCategory = shopCategories.find(cat => cat.identifier === 'backgrounds');
|
||||
expect(backgroundCategory.end).to.exist;
|
||||
expect(backgroundCategory.end).to.be.greaterThan(new Date());
|
||||
});
|
||||
|
||||
it('hair color category contains end date', () => {
|
||||
const colorCategory = shopCategories.find(cat => cat.identifier === 'color');
|
||||
expect(colorCategory.end).to.exist;
|
||||
expect(colorCategory.end).to.be.greaterThan(new Date());
|
||||
});
|
||||
|
||||
it('skin category contains end date', () => {
|
||||
const colorCategory = shopCategories.find(cat => cat.identifier === 'color');
|
||||
expect(colorCategory.end).to.exist;
|
||||
expect(colorCategory.end).to.be.greaterThan(new Date());
|
||||
});
|
||||
});
|
||||
|
||||
describe('seasonalShop', () => {
|
||||
const shopCategories = shared.shops.getSeasonalShopCategories(user, null, seasonalConfig());
|
||||
const today = new Date();
|
||||
|
||||
it('does not contain an empty category', () => {
|
||||
_.each(shopCategories, category => {
|
||||
expect(category.items.length).to.be.greaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not duplicate identifiers', () => {
|
||||
const identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
|
||||
|
||||
expect(identifiers.length).to.eql(shopCategories.length);
|
||||
});
|
||||
|
||||
it('does not return items with event data', async () => {
|
||||
shopCategories.forEach(category => {
|
||||
category.items.forEach(item => {
|
||||
expect(item.event, item.key).to.not.exist;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('items contain required fields', () => {
|
||||
_.each(shopCategories, category => {
|
||||
_.each(category.items, item => {
|
||||
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'type'], key => {
|
||||
expect(_.has(item, key), item.key).to.eql(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('items have a valid end date', () => {
|
||||
shopCategories.forEach(category => {
|
||||
category.items.forEach(item => {
|
||||
expect(item.end, item.key).to.be.a('date');
|
||||
expect(item.end, item.key).to.be.greaterThan(today);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('items match current season', () => {
|
||||
const currentSeason = seasonalConfig().currentSeason.toLowerCase();
|
||||
shopCategories.forEach(category => {
|
||||
category.items.forEach(item => {
|
||||
if (item.klass === 'special') {
|
||||
expect(item.season, item.key).to.eql(currentSeason);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,11 @@
|
||||
import * as armoireSet from '../../../website/common/script/content/gear/sets/armoire';
|
||||
import armoireSet from '../../../website/common/script/content/gear/sets/armoire';
|
||||
|
||||
describe('armoireSet items', () => {
|
||||
it('checks if canOwn has the same id', () => {
|
||||
Object.keys(armoireSet).forEach(type => {
|
||||
if (type === 'all') return;
|
||||
Object.keys(armoireSet[type]).forEach(itemKey => {
|
||||
const ownedKey = `${type}_armoire_${itemKey}`;
|
||||
|
||||
expect(armoireSet[type][itemKey].canOwn({
|
||||
items: {
|
||||
gear: {
|
||||
|
||||
@@ -49,7 +49,7 @@ describe('shared.ops.buy', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('recovers 15 hp', async () => {
|
||||
it('buys health potion', async () => {
|
||||
user.stats.hp = 30;
|
||||
await buy(user, { params: { key: 'potion' } }, analytics);
|
||||
expect(user.stats.hp).to.eql(45);
|
||||
@@ -17,9 +17,7 @@ function getFullArmoire () {
|
||||
|
||||
_.each(content.gearTypes, type => {
|
||||
_.each(content.gear.tree[type].armoire, gearObject => {
|
||||
if (gearObject.released) {
|
||||
fullArmoire[gearObject.key] = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,6 +22,7 @@ async function buyGear (user, req, analytics) {
|
||||
describe('shared.ops.buyMarketGear', () => {
|
||||
let user;
|
||||
const analytics = { track () {} };
|
||||
let clock;
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser({
|
||||
@@ -54,6 +55,10 @@ describe('shared.ops.buyMarketGear', () => {
|
||||
shared.fns.predictableRandom.restore();
|
||||
shared.onboarding.checkOnboardingStatus.restore();
|
||||
analytics.track.restore();
|
||||
|
||||
if (clock) {
|
||||
clock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
context('Gear', () => {
|
||||
@@ -184,30 +189,28 @@ describe('shared.ops.buyMarketGear', () => {
|
||||
});
|
||||
|
||||
// TODO after user.ops.equip is done
|
||||
xit('removes one-handed weapon and shield if auto-equip is on and a two-hander is bought', async () => {
|
||||
it('removes one-handed weapon and shield if auto-equip is on and a two-hander is bought', async () => {
|
||||
user.stats.gp = 100;
|
||||
user.preferences.autoEquip = true;
|
||||
await buyGear(user, { params: { key: 'shield_warrior_1' } });
|
||||
user.ops.equip({ params: { key: 'shield_warrior_1' } });
|
||||
await buyGear(user, { params: { key: 'weapon_warrior_1' } });
|
||||
user.ops.equip({ params: { key: 'weapon_warrior_1' } });
|
||||
user.items.gear.equipped.weapon = 'weapon_warrior_1';
|
||||
user.items.gear.equipped.shield = 'shield_warrior_1';
|
||||
user.stats.class = 'wizard';
|
||||
|
||||
await buyGear(user, { params: { key: 'weapon_wizard_1' } });
|
||||
await buyGear(user, { params: { key: 'weapon_wizard_0' } });
|
||||
|
||||
expect(user.items.gear.equipped).to.have.property('shield', 'shield_base_0');
|
||||
expect(user.items.gear.equipped).to.have.property('weapon', 'weapon_wizard_1');
|
||||
expect(user.items.gear.equipped).to.have.property('weapon', 'weapon_wizard_0');
|
||||
});
|
||||
|
||||
// TODO after user.ops.equip is done
|
||||
xit('buyGears two-handed equipment but does not automatically remove sword or shield', async () => {
|
||||
it('buyGears two-handed equipment but does not automatically remove sword or shield', async () => {
|
||||
user.stats.gp = 100;
|
||||
user.preferences.autoEquip = false;
|
||||
await buyGear(user, { params: { key: 'shield_warrior_1' } });
|
||||
user.ops.equip({ params: { key: 'shield_warrior_1' } });
|
||||
await buyGear(user, { params: { key: 'weapon_warrior_1' } });
|
||||
user.ops.equip({ params: { key: 'weapon_warrior_1' } });
|
||||
user.items.gear.equipped.weapon = 'weapon_warrior_1';
|
||||
user.items.gear.equipped.shield = 'shield_warrior_1';
|
||||
user.stats.class = 'wizard';
|
||||
|
||||
await buyGear(user, { params: { key: 'weapon_wizard_1' } });
|
||||
await buyGear(user, { params: { key: 'weapon_wizard_0' } });
|
||||
|
||||
expect(user.items.gear.equipped).to.have.property('shield', 'shield_warrior_1');
|
||||
expect(user.items.gear.equipped).to.have.property('weapon', 'weapon_warrior_1');
|
||||
@@ -283,5 +286,40 @@ describe('shared.ops.buyMarketGear', () => {
|
||||
|
||||
expect(user.items.gear.owned).to.have.property('shield_armoire_ramHornShield', true);
|
||||
});
|
||||
|
||||
it('buys current seasonal gear', async () => {
|
||||
user.stats.gp = 200;
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-01'));
|
||||
|
||||
await buyGear(user, { params: { key: 'armor_special_winter2024Warrior' } });
|
||||
|
||||
expect(user.items.gear.owned).to.have.property('armor_special_winter2024Warrior', true);
|
||||
});
|
||||
|
||||
it('errors when buying past seasonal gear', async () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-01'));
|
||||
user.stats.gp = 200;
|
||||
|
||||
try {
|
||||
await buyGear(user, { params: { key: 'armor_special_winter2023Warrior' } });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('notAvailable'));
|
||||
expect(user.items.gear.owned).to.not.have.property('armor_special_winter2023Warrior');
|
||||
}
|
||||
});
|
||||
|
||||
it('errors when buying gear from wrong season', async () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-01'));
|
||||
user.stats.gp = 200;
|
||||
|
||||
try {
|
||||
await buyGear(user, { params: { key: 'weapon_special_spring2024Warrior' } });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('notAvailable'));
|
||||
expect(user.items.gear.owned).to.not.have.property('weapon_special_spring2024Warrior');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,7 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
|
||||
describe('shared.ops.buyMysterySet', () => {
|
||||
let user;
|
||||
const analytics = { track () {} };
|
||||
let clock;
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser({
|
||||
@@ -31,6 +32,9 @@ describe('shared.ops.buyMysterySet', () => {
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
if (clock) {
|
||||
clock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
context('Mystery Sets', () => {
|
||||
@@ -41,7 +45,7 @@ describe('shared.ops.buyMysterySet', () => {
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.eql(i18n.t('notEnoughHourglasses'));
|
||||
expect(user.items.gear.owned).to.have.property('weapon_warrior_0', true);
|
||||
expect(user.items.gear.owned).to.not.have.property('armor_mystery_201501');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -72,6 +76,18 @@ describe('shared.ops.buyMysterySet', () => {
|
||||
expect(err.message).to.equal(errorMessage('missingKeyParam'));
|
||||
}
|
||||
});
|
||||
|
||||
it('returns error if the set is not available', async () => {
|
||||
user.purchased.plan.consecutive.trinkets = 1;
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-16'));
|
||||
try {
|
||||
await buyMysterySet(user, { params: { key: '201501' } });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.eql(i18n.t('notAvailable'));
|
||||
expect(user.items.gear.owned).to.not.have.property('armor_mystery_201501');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
context('successful purchases', () => {
|
||||
@@ -86,6 +102,16 @@ describe('shared.ops.buyMysterySet', () => {
|
||||
expect(user.items.gear.owned).to.have.property('head_mystery_301404', true);
|
||||
expect(user.items.gear.owned).to.have.property('eyewear_mystery_301404', true);
|
||||
});
|
||||
|
||||
it('buys mystery set if it is available', async () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-16'));
|
||||
user.purchased.plan.consecutive.trinkets = 1;
|
||||
await buyMysterySet(user, { params: { key: '201601' } }, analytics);
|
||||
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
expect(user.items.gear.owned).to.have.property('shield_mystery_201601', true);
|
||||
expect(user.items.gear.owned).to.have.property('head_mystery_201601', true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -10,6 +10,7 @@ import { BuyQuestWithGemOperation } from '../../../../website/common/script/ops/
|
||||
|
||||
describe('shared.ops.buyQuestGems', () => {
|
||||
let user;
|
||||
let clock;
|
||||
const goldPoints = 40;
|
||||
const analytics = { track () {} };
|
||||
|
||||
@@ -26,11 +27,13 @@ describe('shared.ops.buyQuestGems', () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub(analytics, 'track');
|
||||
sinon.spy(pinnedGearUtils, 'removeItemByPath');
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-16'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
pinnedGearUtils.removeItemByPath.restore();
|
||||
clock.restore();
|
||||
});
|
||||
|
||||
context('single purchase', () => {
|
||||
@@ -44,7 +47,7 @@ describe('shared.ops.buyQuestGems', () => {
|
||||
user.pinnedItems.push({ type: 'quests', key: 'gryphon' });
|
||||
});
|
||||
|
||||
it('successfully purchases quest', async () => {
|
||||
it('successfully purchases pet quest', async () => {
|
||||
const key = 'gryphon';
|
||||
|
||||
await buyQuest(user, { params: { key } });
|
||||
@@ -52,6 +55,61 @@ describe('shared.ops.buyQuestGems', () => {
|
||||
expect(user.items.quests[key]).to.equal(1);
|
||||
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
|
||||
});
|
||||
|
||||
it('successfully purchases hatching potion quest', async () => {
|
||||
const key = 'silver';
|
||||
|
||||
await buyQuest(user, { params: { key } });
|
||||
|
||||
expect(user.items.quests[key]).to.equal(1);
|
||||
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
|
||||
});
|
||||
|
||||
it('successfully purchases seasonal quest', async () => {
|
||||
const key = 'evilsanta';
|
||||
|
||||
await buyQuest(user, { params: { key } });
|
||||
|
||||
expect(user.items.quests[key]).to.equal(1);
|
||||
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
|
||||
});
|
||||
|
||||
it('errors if the pet quest is not available', async () => {
|
||||
const key = 'sabretooth';
|
||||
|
||||
try {
|
||||
await buyQuest(user, { params: { key } });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('notAvailable'));
|
||||
expect(user.items.quests[key]).to.eql(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
it('errors if the hatching potion quest is not available', async () => {
|
||||
const key = 'ruby';
|
||||
|
||||
try {
|
||||
await buyQuest(user, { params: { key } });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('notAvailable'));
|
||||
expect(user.items.quests[key]).to.eql(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
it('errors if the seasonal quest is not available', async () => {
|
||||
const key = 'egg';
|
||||
|
||||
try {
|
||||
await buyQuest(user, { params: { key } });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('notAvailable'));
|
||||
expect(user.items.quests[key]).to.eql(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
it('if a user\'s count of a quest scroll is negative, it will be reset to 0 before incrementing when they buy a new one.', async () => {
|
||||
const key = 'dustbunnies';
|
||||
user.items.quests[key] = -1;
|
||||
@@ -61,6 +119,7 @@ describe('shared.ops.buyQuestGems', () => {
|
||||
expect(user.items.quests[key]).to.equal(1);
|
||||
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
|
||||
});
|
||||
|
||||
it('errors if the user has not completed prerequisite quests', async () => {
|
||||
const key = 'atom3';
|
||||
user.achievements.quests.atom1 = 1;
|
||||
@@ -73,6 +132,7 @@ describe('shared.ops.buyQuestGems', () => {
|
||||
expect(user.items.quests[key]).to.eql(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
it('successfully purchases quest if user has completed all prerequisite quests', async () => {
|
||||
const key = 'atom3';
|
||||
user.achievements.quests.atom1 = 1;
|
||||
@@ -1,89 +0,0 @@
|
||||
import { BuySpellOperation } from '../../../../website/common/script/ops/buy/buySpell';
|
||||
import {
|
||||
BadRequest,
|
||||
NotFound,
|
||||
NotAuthorized,
|
||||
} from '../../../../website/common/script/libs/errors';
|
||||
import i18n from '../../../../website/common/script/i18n';
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../helpers/common.helper';
|
||||
import content from '../../../../website/common/script/content/index';
|
||||
import { errorMessage } from '../../../../website/common/script/libs/errorMessage';
|
||||
|
||||
describe('shared.ops.buySpecialSpell', () => {
|
||||
let user;
|
||||
const analytics = { track () {} };
|
||||
|
||||
async function buySpecialSpell (_user, _req, _analytics) {
|
||||
const buyOp = new BuySpellOperation(_user, _req, _analytics);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
});
|
||||
|
||||
it('throws an error if params.key is missing', async () => {
|
||||
try {
|
||||
await buySpecialSpell(user);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(errorMessage('missingKeyParam'));
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if the spell doesn\'t exists', async () => {
|
||||
try {
|
||||
await buySpecialSpell(user, {
|
||||
params: {
|
||||
key: 'notExisting',
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotFound);
|
||||
expect(err.message).to.equal(errorMessage('spellNotFound', { spellId: 'notExisting' }));
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if the user doesn\'t have enough gold', async () => {
|
||||
user.stats.gp = 1;
|
||||
try {
|
||||
await buySpecialSpell(user, {
|
||||
params: {
|
||||
key: 'thankyou',
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('messageNotEnoughGold'));
|
||||
}
|
||||
});
|
||||
|
||||
it('buys an item', async () => {
|
||||
user.stats.gp = 11;
|
||||
const item = content.special.thankyou;
|
||||
|
||||
const [data, message] = await buySpecialSpell(user, {
|
||||
params: {
|
||||
key: 'thankyou',
|
||||
},
|
||||
}, analytics);
|
||||
|
||||
expect(user.stats.gp).to.equal(1);
|
||||
expect(user.items.special.thankyou).to.equal(1);
|
||||
expect(data).to.eql({
|
||||
items: user.items,
|
||||
stats: user.stats,
|
||||
});
|
||||
expect(message).to.equal(i18n.t('messageBought', {
|
||||
itemText: item.text(),
|
||||
}));
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
});
|
||||
172
test/common/ops/buy/buySpell.test.js
Normal file
172
test/common/ops/buy/buySpell.test.js
Normal file
@@ -0,0 +1,172 @@
|
||||
import { BuySpellOperation } from '../../../../website/common/script/ops/buy/buySpell';
|
||||
import {
|
||||
BadRequest,
|
||||
NotFound,
|
||||
NotAuthorized,
|
||||
} from '../../../../website/common/script/libs/errors';
|
||||
import i18n from '../../../../website/common/script/i18n';
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../helpers/common.helper';
|
||||
import content from '../../../../website/common/script/content/index';
|
||||
import { errorMessage } from '../../../../website/common/script/libs/errorMessage';
|
||||
|
||||
describe('shared.ops.buySpecialSpell', () => {
|
||||
let user;
|
||||
let clock;
|
||||
const analytics = { track () {} };
|
||||
|
||||
async function buySpecialSpell (_user, _req, _analytics) {
|
||||
const buyOp = new BuySpellOperation(_user, _req, _analytics);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
if (clock) {
|
||||
clock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if params.key is missing', async () => {
|
||||
try {
|
||||
await buySpecialSpell(user);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(errorMessage('missingKeyParam'));
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if the item doesn\'t exists', async () => {
|
||||
try {
|
||||
await buySpecialSpell(user, {
|
||||
params: {
|
||||
key: 'notExisting',
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotFound);
|
||||
expect(err.message).to.equal(errorMessage('spellNotFound', { spellId: 'notExisting' }));
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if the user doesn\'t have enough gold', async () => {
|
||||
user.stats.gp = 1;
|
||||
try {
|
||||
await buySpecialSpell(user, {
|
||||
params: {
|
||||
key: 'thankyou',
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('messageNotEnoughGold'));
|
||||
}
|
||||
});
|
||||
|
||||
describe('buying cards', () => {
|
||||
it('buys a card that is always available', async () => {
|
||||
user.stats.gp = 11;
|
||||
const item = content.special.thankyou;
|
||||
|
||||
const [data, message] = await buySpecialSpell(user, {
|
||||
params: {
|
||||
key: 'thankyou',
|
||||
},
|
||||
}, analytics);
|
||||
|
||||
expect(user.stats.gp).to.equal(1);
|
||||
expect(user.items.special.thankyou).to.equal(1);
|
||||
expect(data).to.eql({
|
||||
items: user.items,
|
||||
stats: user.stats,
|
||||
});
|
||||
expect(message).to.equal(i18n.t('messageBought', {
|
||||
itemText: item.text(),
|
||||
}));
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('buys a limited card when it is available', async () => {
|
||||
user.stats.gp = 11;
|
||||
const item = content.special.nye;
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-01'));
|
||||
|
||||
const [data, message] = await buySpecialSpell(user, {
|
||||
params: {
|
||||
key: 'nye',
|
||||
},
|
||||
}, analytics);
|
||||
|
||||
expect(user.stats.gp).to.equal(1);
|
||||
expect(user.items.special.nye).to.equal(1);
|
||||
expect(data).to.eql({
|
||||
items: user.items,
|
||||
stats: user.stats,
|
||||
});
|
||||
expect(message).to.equal(i18n.t('messageBought', {
|
||||
itemText: item.text(),
|
||||
}));
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('throws an error if the card is not currently available', async () => {
|
||||
user.stats.gp = 11;
|
||||
clock = sinon.useFakeTimers(new Date('2024-06-01'));
|
||||
try {
|
||||
await buySpecialSpell(user, {
|
||||
params: {
|
||||
key: 'nye',
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('cannotBuyItem'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('buying spells', () => {
|
||||
it('buys a spell if it is currently available', async () => {
|
||||
user.stats.gp = 16;
|
||||
clock = sinon.useFakeTimers(new Date('2024-06-22'));
|
||||
const item = content.special.seafoam;
|
||||
const [data, message] = await buySpecialSpell(user, {
|
||||
params: {
|
||||
key: 'seafoam',
|
||||
},
|
||||
}, analytics);
|
||||
|
||||
expect(user.stats.gp).to.equal(1);
|
||||
expect(user.items.special.seafoam).to.equal(1);
|
||||
expect(data).to.eql({
|
||||
items: user.items,
|
||||
stats: user.stats,
|
||||
});
|
||||
expect(message).to.equal(i18n.t('messageBought', {
|
||||
itemText: item.text(),
|
||||
}));
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('throws an error if the spell is not currently available', async () => {
|
||||
user.stats.gp = 50;
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-22'));
|
||||
try {
|
||||
await buySpecialSpell(user, {
|
||||
params: {
|
||||
key: 'seafoam',
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('cannotBuyItem'));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
describe('shared.ops.purchase', () => {
|
||||
const SEASONAL_FOOD = moment().isBefore('2021-11-02T20:00-04:00') ? 'Candy_Base' : 'Meat';
|
||||
let user;
|
||||
let clock;
|
||||
const goldPoints = 40;
|
||||
const analytics = { track () {} };
|
||||
|
||||
@@ -25,11 +26,13 @@ describe('shared.ops.purchase', () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub(analytics, 'track');
|
||||
sinon.spy(pinnedGearUtils, 'removeItemByPath');
|
||||
clock = sandbox.useFakeTimers(new Date('2024-01-10'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
pinnedGearUtils.removeItemByPath.restore();
|
||||
clock.restore();
|
||||
});
|
||||
|
||||
context('failure conditions', () => {
|
||||
@@ -82,13 +85,77 @@ describe('shared.ops.purchase', () => {
|
||||
|
||||
it('returns error when user does not have enough gems to buy an item', async () => {
|
||||
try {
|
||||
await purchase(user, { params: { type: 'gear', key: 'headAccessory_special_wolfEars' } });
|
||||
await purchase(user, { params: { type: 'gear', key: 'shield_special_winter2019Healer' } });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('notEnoughGems'));
|
||||
}
|
||||
});
|
||||
|
||||
it('returns error when gear is not available', async () => {
|
||||
try {
|
||||
await purchase(user, { params: { type: 'gear', key: 'shield_special_spring2019Healer' } });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('messageNotAvailable'));
|
||||
}
|
||||
});
|
||||
|
||||
it('returns an error when purchasing current seasonal gear', async () => {
|
||||
user.balance = 2;
|
||||
try {
|
||||
await purchase(user, { params: { type: 'gear', key: 'shield_special_winter2024Healer' } });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('messageNotAvailable'));
|
||||
}
|
||||
});
|
||||
|
||||
it('returns error when hatching potion is not available', async () => {
|
||||
try {
|
||||
await purchase(user, { params: { type: 'hatchingPotions', key: 'PolkaDot' } });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('messageNotAvailable'));
|
||||
}
|
||||
});
|
||||
|
||||
it('returns error when quest for hatching potion was not yet completed', async () => {
|
||||
try {
|
||||
await purchase(user, { params: { type: 'hatchingPotions', key: 'BlackPearl' } });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('messageNotAvailable'));
|
||||
}
|
||||
});
|
||||
|
||||
it('returns error when quest for egg was not yet completed', async () => {
|
||||
try {
|
||||
await purchase(user, { params: { type: 'eggs', key: 'Octopus' } });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('messageNotAvailable'));
|
||||
}
|
||||
});
|
||||
|
||||
it('returns error when bundle is not available', async () => {
|
||||
try {
|
||||
await purchase(user, { params: { type: 'bundles', key: 'forestFriends' } });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('notAvailable'));
|
||||
}
|
||||
});
|
||||
|
||||
it('returns error when gear is not gem purchasable', async () => {
|
||||
try {
|
||||
await purchase(user, { params: { type: 'gear', key: 'shield_healer_3' } });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('messageNotAvailable'));
|
||||
}
|
||||
});
|
||||
|
||||
it('returns error when item is not found', async () => {
|
||||
const params = { key: 'notExisting', type: 'food' };
|
||||
|
||||
@@ -99,44 +166,6 @@ describe('shared.ops.purchase', () => {
|
||||
expect(err.message).to.equal(i18n.t('contentKeyNotFound', params));
|
||||
}
|
||||
});
|
||||
|
||||
it('returns error when user supplies a non-numeric quantity', async () => {
|
||||
const type = 'eggs';
|
||||
const key = 'Wolf';
|
||||
|
||||
try {
|
||||
await purchase(user, { params: { type, key }, quantity: 'jamboree' }, analytics);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
||||
}
|
||||
});
|
||||
|
||||
it('returns error when user supplies a negative quantity', async () => {
|
||||
const type = 'eggs';
|
||||
const key = 'Wolf';
|
||||
user.balance = 10;
|
||||
|
||||
try {
|
||||
await purchase(user, { params: { type, key }, quantity: -2 }, analytics);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
||||
}
|
||||
});
|
||||
|
||||
it('returns error when user supplies a decimal quantity', async () => {
|
||||
const type = 'eggs';
|
||||
const key = 'Wolf';
|
||||
user.balance = 10;
|
||||
|
||||
try {
|
||||
await purchase(user, { params: { type, key }, quantity: 2.9 }, analytics);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
context('successful purchase', () => {
|
||||
@@ -150,7 +179,7 @@ describe('shared.ops.purchase', () => {
|
||||
user.pinnedItems.push({ type: 'eggs', key: 'Wolf' });
|
||||
user.pinnedItems.push({ type: 'hatchingPotions', key: 'Base' });
|
||||
user.pinnedItems.push({ type: 'food', key: SEASONAL_FOOD });
|
||||
user.pinnedItems.push({ type: 'gear', key: 'headAccessory_special_tigerEars' });
|
||||
user.pinnedItems.push({ type: 'gear', key: 'shield_special_winter2019Healer' });
|
||||
user.pinnedItems.push({ type: 'bundles', key: 'featheredFriends' });
|
||||
});
|
||||
|
||||
@@ -185,9 +214,9 @@ describe('shared.ops.purchase', () => {
|
||||
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
|
||||
});
|
||||
|
||||
it('purchases gear', async () => {
|
||||
it('purchases past seasonal gear', async () => {
|
||||
const type = 'gear';
|
||||
const key = 'headAccessory_special_tigerEars';
|
||||
const key = 'shield_special_winter2019Healer';
|
||||
|
||||
await purchase(user, { params: { type, key } });
|
||||
|
||||
@@ -195,9 +224,50 @@ describe('shared.ops.purchase', () => {
|
||||
expect(pinnedGearUtils.removeItemByPath.calledOnce).to.equal(true);
|
||||
});
|
||||
|
||||
it('purchases hatching potion', async () => {
|
||||
const type = 'hatchingPotions';
|
||||
const key = 'Peppermint';
|
||||
|
||||
await purchase(user, { params: { type, key } });
|
||||
|
||||
expect(user.items.hatchingPotions[key]).to.eql(1);
|
||||
});
|
||||
|
||||
it('purchases event hatching potion', async () => {
|
||||
clock.restore();
|
||||
clock = sandbox.useFakeTimers(moment('2022-04-10').valueOf());
|
||||
const type = 'hatchingPotions';
|
||||
const key = 'Veggie';
|
||||
|
||||
await purchase(user, { params: { type, key } });
|
||||
|
||||
expect(user.items.hatchingPotions[key]).to.eql(1);
|
||||
});
|
||||
|
||||
it('purchases hatching potion if user completed quest', async () => {
|
||||
const type = 'hatchingPotions';
|
||||
const key = 'Bronze';
|
||||
user.achievements.quests.bronze = 1;
|
||||
|
||||
await purchase(user, { params: { type, key } });
|
||||
|
||||
expect(user.items.hatchingPotions[key]).to.eql(1);
|
||||
});
|
||||
|
||||
it('purchases egg if user completed quest', async () => {
|
||||
const type = 'eggs';
|
||||
const key = 'Deer';
|
||||
user.achievements.quests.ghost_stag = 1;
|
||||
|
||||
await purchase(user, { params: { type, key } });
|
||||
|
||||
expect(user.items.eggs[key]).to.eql(1);
|
||||
});
|
||||
|
||||
it('purchases quest bundles', async () => {
|
||||
const startingBalance = user.balance;
|
||||
const clock = sandbox.useFakeTimers(moment('2024-03-20').valueOf());
|
||||
clock.restore();
|
||||
clock = sandbox.useFakeTimers(moment('2022-03-10').valueOf());
|
||||
const type = 'bundles';
|
||||
const key = 'cuddleBuddies';
|
||||
const price = 1.75;
|
||||
@@ -216,7 +286,6 @@ describe('shared.ops.purchase', () => {
|
||||
expect(user.balance).to.equal(startingBalance - price);
|
||||
|
||||
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
|
||||
clock.restore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -257,5 +326,43 @@ describe('shared.ops.purchase', () => {
|
||||
|
||||
expect(user.items[type][key]).to.equal(2);
|
||||
});
|
||||
|
||||
it('returns error when user supplies a non-numeric quantity', async () => {
|
||||
const type = 'eggs';
|
||||
const key = 'Wolf';
|
||||
|
||||
try {
|
||||
await purchase(user, { params: { type, key }, quantity: 'jamboree' }, analytics);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
||||
}
|
||||
});
|
||||
|
||||
it('returns error when user supplies a negative quantity', async () => {
|
||||
const type = 'eggs';
|
||||
const key = 'Wolf';
|
||||
user.balance = 10;
|
||||
|
||||
try {
|
||||
await purchase(user, { params: { type, key }, quantity: -2 }, analytics);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
||||
}
|
||||
});
|
||||
|
||||
it('returns error when user supplies a decimal quantity', async () => {
|
||||
const type = 'eggs';
|
||||
const key = 'Wolf';
|
||||
user.balance = 10;
|
||||
|
||||
try {
|
||||
await purchase(user, { params: { type, key }, quantity: 2.9 }, analytics);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,14 +2,17 @@ import get from 'lodash/get';
|
||||
import unlock from '../../../website/common/script/ops/unlock';
|
||||
import i18n from '../../../website/common/script/i18n';
|
||||
import { generateUser } from '../../helpers/common.helper';
|
||||
import { NotAuthorized, BadRequest } from '../../../website/common/script/libs/errors';
|
||||
import {
|
||||
NotAuthorized,
|
||||
BadRequest,
|
||||
} from '../../../website/common/script/libs/errors';
|
||||
|
||||
describe('shared.ops.unlock', () => {
|
||||
let user;
|
||||
const unlockPath = 'shirt.convict,shirt.cross,shirt.fire,shirt.horizon,shirt.ocean,shirt.purple,shirt.rainbow,shirt.redblue,shirt.thunder,shirt.tropical,shirt.zombie';
|
||||
let clock;
|
||||
const unlockPath = 'shirt.convict,shirt.fire,shirt.horizon,shirt.ocean,shirt.purple,shirt.rainbow,shirt.redblue,shirt.thunder,shirt.tropical,shirt.zombie';
|
||||
const unlockGearSetPath = 'items.gear.owned.headAccessory_special_bearEars,items.gear.owned.headAccessory_special_cactusEars,items.gear.owned.headAccessory_special_foxEars,items.gear.owned.headAccessory_special_lionEars,items.gear.owned.headAccessory_special_pandaEars,items.gear.owned.headAccessory_special_pigEars,items.gear.owned.headAccessory_special_tigerEars,items.gear.owned.headAccessory_special_wolfEars';
|
||||
const backgroundUnlockPath = 'background.giant_florals';
|
||||
const backgroundSetUnlockPath = 'background.archery_range,background.giant_florals,background.rainbows_end';
|
||||
const hairUnlockPath = 'hair.color.rainbow,hair.color.yellow,hair.color.green,hair.color.purple,hair.color.blue,hair.color.TRUred';
|
||||
const facialHairUnlockPath = 'hair.mustache.1,hair.mustache.2,hair.beard.1,hair.beard.2,hair.beard.3';
|
||||
const usersStartingGems = 50 / 4;
|
||||
@@ -17,6 +20,11 @@ describe('shared.ops.unlock', () => {
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
user.balance = usersStartingGems;
|
||||
clock = sandbox.useFakeTimers(new Date('2024-04-10'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clock.restore();
|
||||
});
|
||||
|
||||
it('returns an error when path is not provided', async () => {
|
||||
@@ -31,7 +39,9 @@ describe('shared.ops.unlock', () => {
|
||||
it('does not unlock lost gear', async () => {
|
||||
user.items.gear.owned.headAccessory_special_bearEars = false;
|
||||
|
||||
await unlock(user, { query: { path: 'items.gear.owned.headAccessory_special_bearEars' } });
|
||||
await unlock(user, {
|
||||
query: { path: 'items.gear.owned.headAccessory_special_bearEars' },
|
||||
});
|
||||
|
||||
expect(user.balance).to.equal(usersStartingGems);
|
||||
});
|
||||
@@ -95,7 +105,9 @@ describe('shared.ops.unlock', () => {
|
||||
|
||||
it('returns an error if gear is not from the animal set', async () => {
|
||||
try {
|
||||
await unlock(user, { query: { path: 'items.gear.owned.back_mystery_202004' } });
|
||||
await unlock(user, {
|
||||
query: { path: 'items.gear.owned.back_mystery_202004' },
|
||||
});
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidUnlockSet'));
|
||||
@@ -153,7 +165,6 @@ describe('shared.ops.unlock', () => {
|
||||
await unlock(user, { query: { path: partialUnlockPaths[4] } });
|
||||
await unlock(user, { query: { path: partialUnlockPaths[5] } });
|
||||
await unlock(user, { query: { path: partialUnlockPaths[6] } });
|
||||
await unlock(user, { query: { path: partialUnlockPaths[7] } });
|
||||
|
||||
await unlock(user, { query: { path: unlockPath } });
|
||||
});
|
||||
@@ -163,7 +174,9 @@ describe('shared.ops.unlock', () => {
|
||||
|
||||
await unlock(user, { query: { path: backgroundUnlockPath } });
|
||||
const afterBalance = user.balance;
|
||||
const response = await unlock(user, { query: { path: backgroundUnlockPath } });
|
||||
const response = await unlock(user, {
|
||||
query: { path: backgroundUnlockPath },
|
||||
});
|
||||
expect(user.balance).to.equal(afterBalance); // do not bill twice
|
||||
|
||||
expect(response.message).to.not.exist;
|
||||
@@ -176,7 +189,9 @@ describe('shared.ops.unlock', () => {
|
||||
await unlock(user, { query: { path: backgroundUnlockPath } }); // unlock
|
||||
const afterBalance = user.balance;
|
||||
await unlock(user, { query: { path: backgroundUnlockPath } }); // equip
|
||||
const response = await unlock(user, { query: { path: backgroundUnlockPath } });
|
||||
const response = await unlock(user, {
|
||||
query: { path: backgroundUnlockPath },
|
||||
});
|
||||
expect(user.balance).to.equal(afterBalance); // do not bill twice
|
||||
|
||||
expect(response.message).to.not.exist;
|
||||
@@ -192,8 +207,9 @@ describe('shared.ops.unlock', () => {
|
||||
individualPaths.forEach(path => {
|
||||
expect(get(user.purchased, path)).to.be.true;
|
||||
});
|
||||
expect(Object.keys(user.purchased.shirt).length)
|
||||
.to.equal(initialShirts + individualPaths.length);
|
||||
expect(Object.keys(user.purchased.shirt).length).to.equal(
|
||||
initialShirts + individualPaths.length,
|
||||
);
|
||||
expect(user.balance).to.equal(usersStartingGems - 1.25);
|
||||
});
|
||||
|
||||
@@ -208,8 +224,9 @@ describe('shared.ops.unlock', () => {
|
||||
individualPaths.forEach(path => {
|
||||
expect(get(user.purchased, path)).to.be.true;
|
||||
});
|
||||
expect(Object.keys(user.purchased.hair.color).length)
|
||||
.to.equal(initialHairColors + individualPaths.length);
|
||||
expect(Object.keys(user.purchased.hair.color).length).to.equal(
|
||||
initialHairColors + individualPaths.length,
|
||||
);
|
||||
expect(user.balance).to.equal(usersStartingGems - 1.25);
|
||||
});
|
||||
|
||||
@@ -219,21 +236,28 @@ describe('shared.ops.unlock', () => {
|
||||
|
||||
const initialMustache = Object.keys(user.purchased.hair.mustache).length;
|
||||
const initialBeard = Object.keys(user.purchased.hair.mustache).length;
|
||||
const [, message] = await unlock(user, { query: { path: facialHairUnlockPath } });
|
||||
const [, message] = await unlock(user, {
|
||||
query: { path: facialHairUnlockPath },
|
||||
});
|
||||
|
||||
expect(message).to.equal(i18n.t('unlocked'));
|
||||
const individualPaths = facialHairUnlockPath.split(',');
|
||||
individualPaths.forEach(path => {
|
||||
expect(get(user.purchased, path)).to.be.true;
|
||||
});
|
||||
expect(Object.keys(user.purchased.hair.mustache).length + Object.keys(user.purchased.hair.beard).length) // eslint-disable-line max-len
|
||||
expect(
|
||||
Object.keys(user.purchased.hair.mustache).length
|
||||
+ Object.keys(user.purchased.hair.beard).length,
|
||||
) // eslint-disable-line max-len
|
||||
.to.equal(initialMustache + initialBeard + individualPaths.length);
|
||||
expect(user.balance).to.equal(usersStartingGems - 1.25);
|
||||
});
|
||||
|
||||
it('unlocks a full set of gear', async () => {
|
||||
const initialGear = Object.keys(user.items.gear.owned).length;
|
||||
const [, message] = await unlock(user, { query: { path: unlockGearSetPath } });
|
||||
const [, message] = await unlock(user, {
|
||||
query: { path: unlockGearSetPath },
|
||||
});
|
||||
|
||||
expect(message).to.equal(i18n.t('unlocked'));
|
||||
|
||||
@@ -241,32 +265,21 @@ describe('shared.ops.unlock', () => {
|
||||
individualPaths.forEach(path => {
|
||||
expect(get(user, path)).to.be.true;
|
||||
});
|
||||
expect(Object.keys(user.items.gear.owned).length)
|
||||
.to.equal(initialGear + individualPaths.length);
|
||||
expect(Object.keys(user.items.gear.owned).length).to.equal(
|
||||
initialGear + individualPaths.length,
|
||||
);
|
||||
expect(user.balance).to.equal(usersStartingGems - 1.25);
|
||||
});
|
||||
|
||||
it('unlocks a full set of backgrounds', async () => {
|
||||
const initialBackgrounds = Object.keys(user.purchased.background).length;
|
||||
const [, message] = await unlock(user, { query: { path: backgroundSetUnlockPath } });
|
||||
|
||||
expect(message).to.equal(i18n.t('unlocked'));
|
||||
const individualPaths = backgroundSetUnlockPath.split(',');
|
||||
individualPaths.forEach(path => {
|
||||
expect(get(user.purchased, path)).to.be.true;
|
||||
});
|
||||
expect(Object.keys(user.purchased.background).length)
|
||||
.to.equal(initialBackgrounds + individualPaths.length);
|
||||
expect(user.balance).to.equal(usersStartingGems - 3.75);
|
||||
});
|
||||
|
||||
it('unlocks an item (appearance)', async () => {
|
||||
const path = unlockPath.split(',')[0];
|
||||
const initialShirts = Object.keys(user.purchased.shirt).length;
|
||||
const [, message] = await unlock(user, { query: { path } });
|
||||
|
||||
expect(message).to.equal(i18n.t('unlocked'));
|
||||
expect(Object.keys(user.purchased.shirt).length).to.equal(initialShirts + 1);
|
||||
expect(Object.keys(user.purchased.shirt).length).to.equal(
|
||||
initialShirts + 1,
|
||||
);
|
||||
expect(get(user.purchased, path)).to.be.true;
|
||||
expect(user.balance).to.equal(usersStartingGems - 0.5);
|
||||
});
|
||||
@@ -279,7 +292,9 @@ describe('shared.ops.unlock', () => {
|
||||
const [, message] = await unlock(user, { query: { path } });
|
||||
|
||||
expect(message).to.equal(i18n.t('unlocked'));
|
||||
expect(Object.keys(user.purchased.hair.color).length).to.equal(initialColorHair + 1);
|
||||
expect(Object.keys(user.purchased.hair.color).length).to.equal(
|
||||
initialColorHair + 1,
|
||||
);
|
||||
expect(get(user.purchased, path)).to.be.true;
|
||||
expect(user.balance).to.equal(usersStartingGems - 0.5);
|
||||
});
|
||||
@@ -295,8 +310,12 @@ describe('shared.ops.unlock', () => {
|
||||
|
||||
expect(message).to.equal(i18n.t('unlocked'));
|
||||
|
||||
expect(Object.keys(user.purchased.hair.mustache).length).to.equal(initialMustache + 1);
|
||||
expect(Object.keys(user.purchased.hair.beard).length).to.equal(initialBeard);
|
||||
expect(Object.keys(user.purchased.hair.mustache).length).to.equal(
|
||||
initialMustache + 1,
|
||||
);
|
||||
expect(Object.keys(user.purchased.hair.beard).length).to.equal(
|
||||
initialBeard,
|
||||
);
|
||||
|
||||
expect(get(user.purchased, path)).to.be.true;
|
||||
expect(user.balance).to.equal(usersStartingGems - 0.5);
|
||||
@@ -315,11 +334,24 @@ describe('shared.ops.unlock', () => {
|
||||
|
||||
it('unlocks an item (background)', async () => {
|
||||
const initialBackgrounds = Object.keys(user.purchased.background).length;
|
||||
const [, message] = await unlock(user, { query: { path: backgroundUnlockPath } });
|
||||
const [, message] = await unlock(user, {
|
||||
query: { path: backgroundUnlockPath },
|
||||
});
|
||||
|
||||
expect(message).to.equal(i18n.t('unlocked'));
|
||||
expect(Object.keys(user.purchased.background).length).to.equal(initialBackgrounds + 1);
|
||||
expect(Object.keys(user.purchased.background).length).to.equal(
|
||||
initialBackgrounds + 1,
|
||||
);
|
||||
expect(get(user.purchased, backgroundUnlockPath)).to.be.true;
|
||||
expect(user.balance).to.equal(usersStartingGems - 1.75);
|
||||
});
|
||||
|
||||
it('handles an invalid hair path gracefully', async () => {
|
||||
try {
|
||||
await unlock(user, { query: { path: 'hair.invalid' } });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidUnlockSet'));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,27 +0,0 @@
|
||||
/* eslint-disable prefer-template, no-shadow, func-names, import/no-commonjs */
|
||||
|
||||
const expect = require('expect.js');
|
||||
|
||||
module.exports.addCustomMatchers = function () {
|
||||
const { Assertion } = expect;
|
||||
Assertion.prototype.toHaveGP = function (gp) {
|
||||
const actual = this.obj.stats.gp;
|
||||
return this.assert(actual === gp, () => 'expected user to have ' + gp + ' gp, but got ' + actual, () => 'expected user to not have ' + gp + ' gp');
|
||||
};
|
||||
Assertion.prototype.toHaveHP = function (hp) {
|
||||
const actual = this.obj.stats.hp;
|
||||
return this.assert(actual === hp, () => 'expected user to have ' + hp + ' hp, but got ' + actual, () => 'expected user to not have ' + hp + ' hp');
|
||||
};
|
||||
Assertion.prototype.toHaveExp = function (exp) {
|
||||
const actual = this.obj.stats.exp;
|
||||
return this.assert(actual === exp, () => 'expected user to have ' + exp + ' experience points, but got ' + actual, () => 'expected user to not have ' + exp + ' experience points');
|
||||
};
|
||||
Assertion.prototype.toHaveLevel = function (lvl) {
|
||||
const actual = this.obj.stats.lvl;
|
||||
return this.assert(actual === lvl, () => 'expected user to be level ' + lvl + ', but got ' + actual, () => 'expected user to not be level ' + lvl);
|
||||
};
|
||||
Assertion.prototype.toHaveMaxMP = function (mp) {
|
||||
const actual = this.obj._statsComputed.maxMP;
|
||||
return this.assert(actual === mp, () => 'expected user to have ' + mp + ' max mp, but got ' + actual, () => 'expected user to not have ' + mp + ' max mp');
|
||||
};
|
||||
};
|
||||
72
test/content/armoire.test.js
Normal file
72
test/content/armoire.test.js
Normal file
@@ -0,0 +1,72 @@
|
||||
/* eslint-disable global-require */
|
||||
import forEach from 'lodash/forEach';
|
||||
import {
|
||||
expectValidTranslationString,
|
||||
} from '../helpers/content.helper';
|
||||
import armoire from '../../website/common/script/content/gear/sets/armoire';
|
||||
|
||||
describe('armoire', () => {
|
||||
let clock;
|
||||
afterEach(() => {
|
||||
if (clock) {
|
||||
clock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it('does not return unreleased gear', async () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-02'));
|
||||
const items = armoire.all;
|
||||
expect(items.length).to.equal(377);
|
||||
expect(items.filter(item => item.set === 'pottersSet' || item.set === 'optimistSet' || item.set === 'schoolUniform')).to.be.an('array').that.is.empty;
|
||||
});
|
||||
|
||||
it('released gear has all required properties', async () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-05-08'));
|
||||
const items = armoire.all;
|
||||
expect(items.length).to.equal(396);
|
||||
forEach(items, item => {
|
||||
if (item.set !== undefined) {
|
||||
expect(item.set, item.key).to.be.a('string');
|
||||
expect(item.set, item.key).to.not.be.empty;
|
||||
}
|
||||
expectValidTranslationString(item.text);
|
||||
expect(item.value, item.key).to.be.a('number');
|
||||
});
|
||||
});
|
||||
|
||||
it('releases gear when appropriate', async () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-01T00:00:00.000Z'));
|
||||
const items = armoire.all;
|
||||
expect(items.length).to.equal(377);
|
||||
clock.restore();
|
||||
delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')];
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-08'));
|
||||
const januaryItems = armoire.all;
|
||||
expect(januaryItems.length).to.equal(381);
|
||||
clock.restore();
|
||||
delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')];
|
||||
clock = sinon.useFakeTimers(new Date('2024-02-07'));
|
||||
const januaryItems2 = armoire.all;
|
||||
expect(januaryItems2.length).to.equal(381);
|
||||
clock.restore();
|
||||
delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')];
|
||||
clock = sinon.useFakeTimers(new Date('2024-02-07T09:00:00.000Z'));
|
||||
const febuaryItems = armoire.all;
|
||||
expect(febuaryItems.length).to.equal(384);
|
||||
});
|
||||
|
||||
it('sets have at least 2 items', () => {
|
||||
const setMap = {};
|
||||
forEach(armoire.all, item => {
|
||||
// Gotta have one outlier
|
||||
if (!item.set || item.set.startsWith('armoire-')) return;
|
||||
if (setMap[item.set] === undefined) {
|
||||
setMap[item.set] = 0;
|
||||
}
|
||||
setMap[item.set] += 1;
|
||||
});
|
||||
Object.keys(setMap).forEach(set => {
|
||||
expect(setMap[set], set).to.be.at.least(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,21 +5,26 @@ import {
|
||||
expectValidTranslationString,
|
||||
} from '../helpers/content.helper';
|
||||
|
||||
import * as eggs from '../../website/common/script/content/eggs';
|
||||
import eggs from '../../website/common/script/content/eggs';
|
||||
|
||||
describe('eggs', () => {
|
||||
describe('all', () => {
|
||||
it('is a combination of drop and quest eggs', () => {
|
||||
const dropNumber = Object.keys(eggs.drops).length;
|
||||
const questNumber = Object.keys(eggs.quests).length;
|
||||
const allNumber = Object.keys(eggs.all).length;
|
||||
let clock;
|
||||
|
||||
expect(allNumber).to.be.greaterThan(0);
|
||||
expect(allNumber).to.equal(dropNumber + questNumber);
|
||||
afterEach(() => {
|
||||
if (clock) {
|
||||
clock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
const eggTypes = [
|
||||
'drops',
|
||||
'quests',
|
||||
];
|
||||
|
||||
eggTypes.forEach(eggType => {
|
||||
describe(eggType, () => {
|
||||
it('contains basic information about each egg', () => {
|
||||
each(eggs.all, (egg, key) => {
|
||||
each(eggs[eggType], (egg, key) => {
|
||||
expectValidTranslationString(egg.text);
|
||||
expectValidTranslationString(egg.adjective);
|
||||
expectValidTranslationString(egg.mountText);
|
||||
@@ -31,3 +36,20 @@ describe('eggs', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does not contain unreleased eggs', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-05-20'));
|
||||
const questEggs = eggs.quests;
|
||||
expect(questEggs.Giraffe).to.not.exist;
|
||||
});
|
||||
|
||||
it('Releases eggs when appropriate without needing restarting', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-05-20'));
|
||||
const mayEggs = eggs.quests;
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2024-06-20'));
|
||||
const juneEggs = eggs.quests;
|
||||
expect(juneEggs.Giraffe).to.exist;
|
||||
expect(Object.keys(mayEggs).length).to.equal(Object.keys(juneEggs).length - 1);
|
||||
});
|
||||
});
|
||||
|
||||
40
test/content/events.test.js
Normal file
40
test/content/events.test.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { getRepeatingEvents } from '../../website/common/script/content/constants/events';
|
||||
|
||||
describe('events', () => {
|
||||
let clock;
|
||||
|
||||
afterEach(() => {
|
||||
if (clock) {
|
||||
clock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it('returns empty array when no events are active', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-08'));
|
||||
const events = getRepeatingEvents();
|
||||
expect(events).to.be.empty;
|
||||
});
|
||||
|
||||
it('returns events when active', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-31'));
|
||||
const events = getRepeatingEvents();
|
||||
expect(events).to.have.length(1);
|
||||
expect(events[0].key).to.equal('birthday');
|
||||
expect(events[0].end).to.be.greaterThan(new Date());
|
||||
expect(events[0].start).to.be.lessThan(new Date());
|
||||
});
|
||||
|
||||
it('returns nye event at beginning of the year', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2025-01-01'));
|
||||
const events = getRepeatingEvents();
|
||||
expect(events).to.have.length(2);
|
||||
expect(events[0].key).to.equal('nye');
|
||||
});
|
||||
|
||||
it('returns nye event at end of the year', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-12-30'));
|
||||
const events = getRepeatingEvents();
|
||||
expect(events).to.have.length(2);
|
||||
expect(events[0].key).to.equal('nye');
|
||||
});
|
||||
});
|
||||
94
test/content/food.test.js
Normal file
94
test/content/food.test.js
Normal file
@@ -0,0 +1,94 @@
|
||||
/* eslint-disable global-require */
|
||||
import {
|
||||
each,
|
||||
} from 'lodash';
|
||||
import {
|
||||
expectValidTranslationString,
|
||||
} from '../helpers/content.helper';
|
||||
import content from '../../website/common/script/content';
|
||||
|
||||
describe('food', () => {
|
||||
let clock;
|
||||
|
||||
afterEach(() => {
|
||||
if (clock) {
|
||||
clock.restore();
|
||||
}
|
||||
delete require.cache[require.resolve('../../website/common/script/content')];
|
||||
});
|
||||
|
||||
describe('all', () => {
|
||||
it('contains basic information about each food item', () => {
|
||||
each(content.food, (foodItem, key) => {
|
||||
if (foodItem.key === 'Saddle') {
|
||||
expectValidTranslationString(foodItem.sellWarningNote);
|
||||
} else {
|
||||
expectValidTranslationString(foodItem.textA);
|
||||
expectValidTranslationString(foodItem.textThe);
|
||||
expect(foodItem.target).to.be.a('string');
|
||||
}
|
||||
expectValidTranslationString(foodItem.text);
|
||||
expectValidTranslationString(foodItem.notes);
|
||||
expect(foodItem.canBuy).to.be.a('function');
|
||||
expect(foodItem.value).to.be.a('number');
|
||||
expect(foodItem.key).to.equal(key);
|
||||
});
|
||||
});
|
||||
|
||||
it('sets canDrop for normal food if there is no food season', () => {
|
||||
clock = sinon.useFakeTimers(new Date(2024, 5, 8));
|
||||
const datedContent = require('../../website/common/script/content').default;
|
||||
each(datedContent.food, foodItem => {
|
||||
if (foodItem.key.indexOf('Cake') === -1 && foodItem.key.indexOf('Candy_') === -1 && foodItem.key.indexOf('Pie_') === -1 && foodItem.key !== 'Saddle') {
|
||||
expect(foodItem.canDrop).to.equal(true);
|
||||
} else {
|
||||
expect(foodItem.canDrop).to.equal(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('sets canDrop for candy if it is candy season', () => {
|
||||
clock = sinon.useFakeTimers(new Date(2024, 9, 31));
|
||||
const datedContent = require('../../website/common/script/content').default;
|
||||
each(datedContent.food, foodItem => {
|
||||
if (foodItem.key.indexOf('Candy_') !== -1) {
|
||||
expect(foodItem.canDrop).to.equal(true);
|
||||
} else {
|
||||
expect(foodItem.canDrop).to.equal(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('sets canDrop for cake if it is cake season', () => {
|
||||
clock = sinon.useFakeTimers(new Date(2024, 0, 31));
|
||||
const datedContent = require('../../website/common/script/content').default;
|
||||
each(datedContent.food, foodItem => {
|
||||
if (foodItem.key.indexOf('Cake_') !== -1) {
|
||||
expect(foodItem.canDrop).to.equal(true);
|
||||
} else {
|
||||
expect(foodItem.canDrop).to.equal(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('sets canDrop for pie if it is pie season', () => {
|
||||
clock = sinon.useFakeTimers(new Date(2024, 2, 15));
|
||||
const datedContent = require('../../website/common/script/content').default;
|
||||
each(datedContent.food, foodItem => {
|
||||
if (foodItem.key.indexOf('Pie_') !== -1) {
|
||||
expect(foodItem.canDrop).to.equal(true);
|
||||
} else {
|
||||
expect(foodItem.canDrop).to.equal(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets correct values for saddles', () => {
|
||||
const saddle = content.food.Saddle;
|
||||
expect(saddle.canBuy).to.be.a('function');
|
||||
expect(saddle.value).to.equal(5);
|
||||
expect(saddle.key).to.equal('Saddle');
|
||||
expect(saddle.canDrop).to.equal(false);
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
expectValidTranslationString,
|
||||
} from '../helpers/content.helper';
|
||||
|
||||
import { CLASSES } from '../../website/common/script/content/constants';
|
||||
|
||||
import gearData from '../../website/common/script/content/gear';
|
||||
import * as backerGear from '../../website/common/script/content/gear/sets/special/special-backer';
|
||||
import * as contributorGear from '../../website/common/script/content/gear/sets/special/special-contributor';
|
||||
@@ -17,35 +19,48 @@ describe('Gear', () => {
|
||||
context(`${klass} ${gearType}s`, () => {
|
||||
it('have a value of at least 0 for each stat', () => {
|
||||
each(items, gear => {
|
||||
expect(gear.con).to.be.at.least(0);
|
||||
expect(gear.int).to.be.at.least(0);
|
||||
expect(gear.per).to.be.at.least(0);
|
||||
expect(gear.str).to.be.at.least(0);
|
||||
expect(gear.con, gear.key).to.be.at.least(0);
|
||||
expect(gear.int, gear.key).to.be.at.least(0);
|
||||
expect(gear.per, gear.key).to.be.at.least(0);
|
||||
expect(gear.str, gear.key).to.be.at.least(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('have a purchase value of at least 0', () => {
|
||||
each(items, gear => {
|
||||
expect(gear.value).to.be.at.least(0);
|
||||
expect(gear.value, gear.key).to.be.at.least(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('has a canBuy function', () => {
|
||||
each(items, gear => {
|
||||
expect(gear.canBuy).to.be.a('function');
|
||||
expect(gear.canBuy, gear.key).to.be.a('function');
|
||||
});
|
||||
});
|
||||
|
||||
it('have valid translation strings for text and notes', () => {
|
||||
each(items, gear => {
|
||||
expectValidTranslationString(gear.text);
|
||||
expectValidTranslationString(gear.notes);
|
||||
expectValidTranslationString(gear.text, gear.key);
|
||||
expectValidTranslationString(gear.notes, gear.key);
|
||||
});
|
||||
});
|
||||
|
||||
it('has a set attribue', () => {
|
||||
each(items, gear => {
|
||||
expect(gear.set).to.exist;
|
||||
expect(gear.set, gear.key).to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
it('has a valid value for klass or specialClass', () => {
|
||||
const validClassValues = CLASSES + ['base', 'mystery', 'armoire'];
|
||||
each(items, gear => {
|
||||
const field = gear.klass === 'special' ? gear.specialClass : gear.klass;
|
||||
if (gear.klass === 'special' && field === undefined) {
|
||||
// some special gear doesn't have a klass
|
||||
return;
|
||||
}
|
||||
expect(field, gear.key).to.exist;
|
||||
expect(validClassValues, gear.key).to.include(field);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -53,6 +68,16 @@ describe('Gear', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('only assigns mage weapons twoHanded', () => {
|
||||
each([allGear.armor.special, allGear.head.special, allGear.shield.special], gearType => {
|
||||
each(gearType, gear => {
|
||||
if (gear.specialClass === 'wizard') {
|
||||
expect(gear.twoHanded, gear.key).to.not.eql(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('backer gear', () => {
|
||||
let user;
|
||||
|
||||
@@ -5,20 +5,25 @@ import {
|
||||
expectValidTranslationString,
|
||||
} from '../helpers/content.helper';
|
||||
|
||||
import * as hatchingPotions from '../../website/common/script/content/hatching-potions';
|
||||
import hatchingPotions from '../../website/common/script/content/hatching-potions';
|
||||
|
||||
describe('hatchingPotions', () => {
|
||||
describe('all', () => {
|
||||
it('is a combination of drop, premium, and wacky potions', () => {
|
||||
const dropNumber = Object.keys(hatchingPotions.drops).length;
|
||||
const premiumNumber = Object.keys(hatchingPotions.premium).length;
|
||||
const wackyNumber = Object.keys(hatchingPotions.wacky).length;
|
||||
const allNumber = Object.keys(hatchingPotions.all).length;
|
||||
let clock;
|
||||
|
||||
expect(allNumber).to.be.greaterThan(0);
|
||||
expect(allNumber).to.equal(dropNumber + premiumNumber + wackyNumber);
|
||||
afterEach(() => {
|
||||
if (clock) {
|
||||
clock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
const potionTypes = [
|
||||
'drops',
|
||||
'quests',
|
||||
'premium',
|
||||
'wacky',
|
||||
];
|
||||
potionTypes.forEach(potionType => {
|
||||
describe(potionType, () => {
|
||||
it('contains basic information about each potion', () => {
|
||||
each(hatchingPotions.all, (potion, key) => {
|
||||
expectValidTranslationString(potion.text);
|
||||
@@ -30,3 +35,20 @@ describe('hatchingPotions', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does not contain unreleased potions', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-05-20'));
|
||||
const premiumPotions = hatchingPotions.premium;
|
||||
expect(premiumPotions.Koi).to.not.exist;
|
||||
});
|
||||
|
||||
it('Releases potions when appropriate without needing restarting', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-05-20'));
|
||||
const mayPotions = hatchingPotions.premium;
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2024-06-20'));
|
||||
const junePotions = hatchingPotions.premium;
|
||||
expect(junePotions.Koi).to.exist;
|
||||
expect(Object.keys(mayPotions).length).to.equal(Object.keys(junePotions).length - 1);
|
||||
});
|
||||
});
|
||||
|
||||
154
test/content/index.test.js
Normal file
154
test/content/index.test.js
Normal file
@@ -0,0 +1,154 @@
|
||||
import content from '../../website/common/script/content';
|
||||
|
||||
describe('content index', () => {
|
||||
let clock;
|
||||
|
||||
afterEach(() => {
|
||||
if (clock) {
|
||||
clock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it('Releases eggs when appropriate without needing restarting', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-06-20'));
|
||||
const mayEggs = content.eggs;
|
||||
expect(mayEggs.Chameleon).to.not.exist;
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2024-07-20'));
|
||||
const juneEggs = content.eggs;
|
||||
expect(juneEggs.Chameleon).to.exist;
|
||||
expect(Object.keys(mayEggs).length, '').to.equal(Object.keys(juneEggs).length - 1);
|
||||
});
|
||||
|
||||
it('Releases hatching potions when appropriate without needing restarting', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-05-20'));
|
||||
const mayHatchingPotions = content.hatchingPotions;
|
||||
expect(mayHatchingPotions.Koi).to.not.exist;
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2024-06-20'));
|
||||
const juneHatchingPotions = content.hatchingPotions;
|
||||
expect(juneHatchingPotions.Koi).to.exist;
|
||||
expect(Object.keys(mayHatchingPotions).length, '').to.equal(Object.keys(juneHatchingPotions).length - 1);
|
||||
});
|
||||
|
||||
it('Releases armoire gear when appropriate without needing restarting', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-06-20'));
|
||||
const juneGear = content.gear.flat;
|
||||
expect(juneGear.armor_armoire_corsairsCoatAndCape).to.not.exist;
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2024-07-10'));
|
||||
const julyGear = content.gear.flat;
|
||||
expect(julyGear.armor_armoire_corsairsCoatAndCape).to.exist;
|
||||
expect(Object.keys(juneGear).length, '').to.equal(Object.keys(julyGear).length - 3);
|
||||
});
|
||||
|
||||
it('Releases pets when appropriate without needing restarting', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-06-20'));
|
||||
const junePets = content.petInfo;
|
||||
expect(junePets['Chameleon-Base']).to.not.exist;
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2024-07-18'));
|
||||
const julyPets = content.petInfo;
|
||||
expect(julyPets['Chameleon-Base']).to.exist;
|
||||
expect(Object.keys(junePets).length, '').to.equal(Object.keys(julyPets).length - 10);
|
||||
});
|
||||
|
||||
it('Releases mounts when appropriate without needing restarting', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-06-20'));
|
||||
const juneMounts = content.mountInfo;
|
||||
expect(juneMounts['Chameleon-Base']).to.not.exist;
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2024-07-18'));
|
||||
const julyMounts = content.mountInfo;
|
||||
expect(julyMounts['Chameleon-Base']).to.exist;
|
||||
expect(Object.keys(juneMounts).length, '').to.equal(Object.keys(julyMounts).length - 10);
|
||||
});
|
||||
|
||||
it('marks regular food as buyable and droppable without any events', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-06-20'));
|
||||
const { food } = content;
|
||||
Object.keys(food).forEach(key => {
|
||||
if (key === 'Saddle') {
|
||||
expect(food[key].canBuy(), `${key} canBuy`).to.be.true;
|
||||
expect(food[key].canDrop, `${key} canDrop`).to.be.false;
|
||||
return;
|
||||
}
|
||||
let expected = true;
|
||||
if (key.startsWith('Cake_')) {
|
||||
expected = false;
|
||||
} else if (key.startsWith('Candy_')) {
|
||||
expected = false;
|
||||
} else if (key.startsWith('Pie_')) {
|
||||
expected = false;
|
||||
}
|
||||
expect(food[key].canBuy(), `${key} canBuy`).to.equal(expected);
|
||||
expect(food[key].canDrop, `${key} canDrop`).to.equal(expected);
|
||||
});
|
||||
});
|
||||
|
||||
it('marks candy as buyable and droppable during habitoween', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-10-31'));
|
||||
const { food } = content;
|
||||
Object.keys(food).forEach(key => {
|
||||
if (key === 'Saddle') {
|
||||
expect(food[key].canBuy(), `${key} canBuy`).to.be.true;
|
||||
expect(food[key].canDrop, `${key} canDrop`).to.be.false;
|
||||
return;
|
||||
}
|
||||
let expected = false;
|
||||
if (key.startsWith('Cake_')) {
|
||||
expected = false;
|
||||
} else if (key.startsWith('Candy_')) {
|
||||
expected = true;
|
||||
} else if (key.startsWith('Pie_')) {
|
||||
expected = false;
|
||||
}
|
||||
expect(food[key].canBuy(), `${key} canBuy`).to.equal(expected);
|
||||
expect(food[key].canDrop, `${key} canDrop`).to.equal(expected);
|
||||
});
|
||||
});
|
||||
|
||||
it('marks cake as buyable and droppable during birthday', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-31'));
|
||||
const { food } = content;
|
||||
Object.keys(food).forEach(key => {
|
||||
if (key === 'Saddle') {
|
||||
expect(food[key].canBuy(), `${key} canBuy`).to.be.true;
|
||||
expect(food[key].canDrop, `${key} canDrop`).to.be.false;
|
||||
return;
|
||||
}
|
||||
let expected = false;
|
||||
if (key.startsWith('Cake_')) {
|
||||
expected = true;
|
||||
} else if (key.startsWith('Candy_')) {
|
||||
expected = false;
|
||||
} else if (key.startsWith('Pie_')) {
|
||||
expected = false;
|
||||
}
|
||||
expect(food[key].canBuy(), `${key} canBuy`).to.equal(expected);
|
||||
expect(food[key].canDrop, `${key} canDrop`).to.equal(expected);
|
||||
});
|
||||
});
|
||||
|
||||
it('marks pie as buyable and droppable during pi day', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-03-15'));
|
||||
const { food } = content;
|
||||
Object.keys(food).forEach(key => {
|
||||
if (key === 'Saddle') {
|
||||
expect(food[key].canBuy(), `${key} canBuy`).to.be.true;
|
||||
expect(food[key].canDrop, `${key} canDrop`).to.be.false;
|
||||
return;
|
||||
}
|
||||
let expected = false;
|
||||
if (key.startsWith('Cake_')) {
|
||||
expected = false;
|
||||
} else if (key.startsWith('Candy_')) {
|
||||
expected = false;
|
||||
} else if (key.startsWith('Pie_')) {
|
||||
expected = true;
|
||||
}
|
||||
expect(food[key].canBuy(), `${key} canBuy`).to.equal(expected);
|
||||
expect(food[key].canDrop, `${key} canDrop`).to.equal(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user