Compare commits
576 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f077451d2 | ||
|
|
595a31db99 | ||
|
|
69e40e2114 | ||
|
|
17d918a172 | ||
|
|
bc74e40280 | ||
|
|
4487363171 | ||
|
|
dc72faad6a | ||
|
|
7c0b3612f0 | ||
|
|
9a32eabb47 | ||
|
|
6fe0d5568a | ||
|
|
2c44f766cd | ||
|
|
c79e3bea05 | ||
|
|
b56f0cfeeb | ||
|
|
fbf1849148 | ||
|
|
a493bb69ce | ||
|
|
7b20d02449 | ||
|
|
207d1b7eaa | ||
|
|
f33aed661b | ||
|
|
7f1d6ffef0 | ||
|
|
75f198789b | ||
|
|
6ad20e7abb | ||
|
|
2705539a70 | ||
|
|
8f05fc250a | ||
|
|
a21295c0e1 | ||
|
|
24e13ddd18 | ||
|
|
4d9e03b6d2 | ||
|
|
8a64def893 | ||
|
|
e6f40aee43 | ||
|
|
dc34db98b4 | ||
|
|
bd228d5d69 | ||
|
|
5d054a0acd | ||
|
|
9a1fbc2d2f | ||
|
|
7b0f43b61c | ||
|
|
bc115bb1f6 | ||
|
|
cd790e7228 | ||
|
|
bc1c637023 | ||
|
|
eadbdeb7b8 | ||
|
|
585359e22f | ||
|
|
f0d6204968 | ||
|
|
14fed0eb43 | ||
|
|
427654d8bd | ||
|
|
cdb5ccf76a | ||
|
|
bf28a46803 | ||
|
|
84c10eb92a | ||
|
|
fa197e1b57 | ||
|
|
993c5552e8 | ||
|
|
1eba72dd36 | ||
|
|
4457b081fa | ||
|
|
395e1c25d4 | ||
|
|
fdef94e826 | ||
|
|
b3cfb57933 | ||
|
|
b4551088c1 | ||
|
|
26e6e583ab | ||
|
|
81c7036cdb | ||
|
|
23906d8f94 | ||
|
|
2d091fc667 | ||
|
|
6d34319455 | ||
|
|
7072fbdd06 | ||
|
|
73cd6aec59 | ||
|
|
d05e535c7e | ||
|
|
e8960f1b0b | ||
|
|
943ea1d8f3 | ||
|
|
61172f82a3 | ||
|
|
e7faebbf40 | ||
|
|
c800f36178 | ||
|
|
f2fff946a4 | ||
|
|
e6b0bdb05e | ||
|
|
709fcd5ae6 | ||
|
|
1b0251a492 | ||
|
|
c5cd20de22 | ||
|
|
32d3da9310 | ||
|
|
9ebf435c82 | ||
|
|
f06fefe9c0 | ||
|
|
4bdaa58592 | ||
|
|
bcae464955 | ||
|
|
0d1643eb17 | ||
|
|
ccae075a4d | ||
|
|
5d275aa2a5 | ||
|
|
26940e4054 | ||
|
|
119502f285 | ||
|
|
5c8817c4ef | ||
|
|
7c081c4607 | ||
|
|
f2b53f651e | ||
|
|
0e1011b875 | ||
|
|
cb999d9277 | ||
|
|
dcdb3efdc2 | ||
|
|
f2281efe99 | ||
|
|
ff93dd9159 | ||
|
|
0568e4f2ae | ||
|
|
fe109f0e00 | ||
|
|
53f19c4da3 | ||
|
|
fa0f60d3a6 | ||
|
|
74ebcf919e | ||
|
|
0701fa4286 | ||
|
|
25d9102674 | ||
|
|
5daf96bbf5 | ||
|
|
62498ab646 | ||
|
|
cf435ad007 | ||
|
|
55f4b8ae87 | ||
|
|
dab9a9b6cb | ||
|
|
4db0002d5d | ||
|
|
4a1c532cd3 | ||
|
|
3c1d7fce5f | ||
|
|
e840661da8 | ||
|
|
ec912a6dda | ||
|
|
5d6583936d | ||
|
|
bc181819f4 | ||
|
|
8434d727bd | ||
|
|
c055d43895 | ||
|
|
2018962eb5 | ||
|
|
4aa51db5ec | ||
|
|
1ed2ebb04d | ||
|
|
f0123a1571 | ||
|
|
e4e200c32c | ||
|
|
637048af0b | ||
|
|
d6be92b346 | ||
|
|
2ed15f58e9 | ||
|
|
42de4397a1 | ||
|
|
661c7b23a1 | ||
|
|
f077e40c4c | ||
|
|
3ce182d0dc | ||
|
|
6a658c45b5 | ||
|
|
7057797ed3 | ||
|
|
d3eb9fe230 | ||
|
|
51b50bfa3c | ||
|
|
8c953ea8cb | ||
|
|
c5b644b892 | ||
|
|
e65bd4164d | ||
|
|
c9ef21aa7e | ||
|
|
f41eae3bf3 | ||
|
|
c984c04e46 | ||
|
|
4e9a2de527 | ||
|
|
7745a0e65a | ||
|
|
6b3a6eb59f | ||
|
|
103b4cb8a8 | ||
|
|
de5473c71e | ||
|
|
f31370d7ba | ||
|
|
3b7bf5de75 | ||
|
|
496950e97d | ||
|
|
281d7b4fe2 | ||
|
|
8d2ecaffb0 | ||
|
|
00f66b3824 | ||
|
|
c4f6644c3a | ||
|
|
58b5af0d4c | ||
|
|
9b76f9831e | ||
|
|
8bd2e09bde | ||
|
|
670843c395 | ||
|
|
a81e6932fb | ||
|
|
4219bcbffa | ||
|
|
e499666f64 | ||
|
|
b5a25d74df | ||
|
|
d5408b89b2 | ||
|
|
4c69bf2090 | ||
|
|
54e2874990 | ||
|
|
278ed4d2df | ||
|
|
f7b4d25657 | ||
|
|
f44b331680 | ||
|
|
284cfde935 | ||
|
|
c19c39d72d | ||
|
|
809398f607 | ||
|
|
5791e87132 | ||
|
|
4a93201f50 | ||
|
|
a8ac927030 | ||
|
|
5327827ef7 | ||
|
|
df1e4af7fc | ||
|
|
d0c9c2917f | ||
|
|
50e009efff | ||
|
|
8c98b7127e | ||
|
|
cb96ff84d1 | ||
|
|
705ae93292 | ||
|
|
1c089c33a0 | ||
|
|
4481217b90 | ||
|
|
d3ba0346af | ||
|
|
41de90e578 | ||
|
|
a863e79214 | ||
|
|
6ed1353e31 | ||
|
|
c748477546 | ||
|
|
365f9c0aa7 | ||
|
|
23e717353d | ||
|
|
d5517ffc5f | ||
|
|
7b6f63958a | ||
|
|
b5d8bcc0fe | ||
|
|
181b33101e | ||
|
|
4319bd5ad1 | ||
|
|
f2c6838e95 | ||
|
|
a2cd79f20e | ||
|
|
6a367d3697 | ||
|
|
ef0b19f17e | ||
|
|
27129754cd | ||
|
|
983aae7f87 | ||
|
|
174ac6d7e3 | ||
|
|
2e59260149 | ||
|
|
997cc9f3c5 | ||
|
|
8a7b4db5ee | ||
|
|
7adb33887e | ||
|
|
85d2e21510 | ||
|
|
868a8a4e77 | ||
|
|
b61425078a | ||
|
|
03876b86bb | ||
|
|
8ac48406e9 | ||
|
|
a6e96f7ad9 | ||
|
|
43b8368f42 | ||
|
|
5362058f35 | ||
|
|
9d6fb2ca26 | ||
|
|
9213ee92de | ||
|
|
3a80875505 | ||
|
|
2d6802ae87 | ||
|
|
497c023995 | ||
|
|
b97d514c68 | ||
|
|
142fdfe743 | ||
|
|
ee5a2b0e36 | ||
|
|
9455f996ef | ||
|
|
5cc3f6e8aa | ||
|
|
8bad3d1184 | ||
|
|
3f65353974 | ||
|
|
ab7c4015a2 | ||
|
|
6d509ae1f8 | ||
|
|
6375bc1d59 | ||
|
|
14714f9e1c | ||
|
|
60b3f2b860 | ||
|
|
965df326ed | ||
|
|
3c49db9bfe | ||
|
|
72bd104567 | ||
|
|
2cbd376cf6 | ||
|
|
a7477e137e | ||
|
|
f6cb57c22f | ||
|
|
76fa4dfd7f | ||
|
|
aceaeacbf0 | ||
|
|
b2cb5f83de | ||
|
|
d09dea5185 | ||
|
|
6fa7fbce3f | ||
|
|
b17c9a33a5 | ||
|
|
31d0cb5a91 | ||
|
|
d678eb920f | ||
|
|
163eb55dbb | ||
|
|
8661716c23 | ||
|
|
ea11e302c6 | ||
|
|
afdd5af7a9 | ||
|
|
a09e56048e | ||
|
|
fd37ee90da | ||
|
|
a4cfb97dbf | ||
|
|
ea766251c2 | ||
|
|
f196ff2e24 | ||
|
|
cc901fe085 | ||
|
|
f257488b39 | ||
|
|
48dbe547c0 | ||
|
|
b15462596b | ||
|
|
2a98b5b7bf | ||
|
|
f7667fcf79 | ||
|
|
05aaad8743 | ||
|
|
5e1f2c16f8 | ||
|
|
82b6a14d5b | ||
|
|
f9cfd9fb5e | ||
|
|
60bef56577 | ||
|
|
af87185bfa | ||
|
|
f8c8be4f4c | ||
|
|
5d220544e0 | ||
|
|
c4fd9daa90 | ||
|
|
fc8f9cbaa0 | ||
|
|
e39d3e52e2 | ||
|
|
625b4a4ad7 | ||
|
|
f2bcdd21de | ||
|
|
ad51675ac6 | ||
|
|
869d2df4fa | ||
|
|
734e997345 | ||
|
|
4fc260e552 | ||
|
|
fa22a47b7a | ||
|
|
0366245fab | ||
|
|
f342eff70b | ||
|
|
84eb39fde2 | ||
|
|
d6fe2c76e2 | ||
|
|
f19e9dd57e | ||
|
|
13566e8a39 | ||
|
|
7d3dd9f157 | ||
|
|
92f283f6a2 | ||
|
|
5d24d584d4 | ||
|
|
0320827f7e | ||
|
|
79c1a5d9c1 | ||
|
|
faa49b1412 | ||
|
|
c3decc3951 | ||
|
|
084adf8b0d | ||
|
|
04574dad69 | ||
|
|
6526a6317e | ||
|
|
c778e5e84e | ||
|
|
b5dacdf9ea | ||
|
|
e05d1dae43 | ||
|
|
864db644e3 | ||
|
|
5ddd4ec564 | ||
|
|
2d7d4af2b8 | ||
|
|
bad3f82dfb | ||
|
|
8595641d12 | ||
|
|
9bb3e17995 | ||
|
|
8b955e2c5e | ||
|
|
1d7e02428b | ||
|
|
eee04255f8 | ||
|
|
7f30385c09 | ||
|
|
5b6eeef290 | ||
|
|
9953c9346d | ||
|
|
d62930b9da | ||
|
|
67c607216f | ||
|
|
672fd43ad0 | ||
|
|
69281f80ea | ||
|
|
093bcbb715 | ||
|
|
1f5992ccc5 | ||
|
|
2d598c9933 | ||
|
|
b4be8286e2 | ||
|
|
96b985a191 | ||
|
|
a196a0cae2 | ||
|
|
8f9043e4f1 | ||
|
|
1cace198a1 | ||
|
|
ec0e5024a7 | ||
|
|
0275636f46 | ||
|
|
ea7ae4bb2f | ||
|
|
9f32a879e3 | ||
|
|
665200b49d | ||
|
|
c63e92c8f2 | ||
|
|
ba5b79855f | ||
|
|
97051bb3ae | ||
|
|
29e3cae0a6 | ||
|
|
274c582af8 | ||
|
|
4a7f40ea37 | ||
|
|
97ad0fae26 | ||
|
|
b7b91caef1 | ||
|
|
daa3458760 | ||
|
|
8e6ce39d64 | ||
|
|
617832f02d | ||
|
|
2cfc765e39 | ||
|
|
43efe2d6d6 | ||
|
|
9474a44df3 | ||
|
|
7cd9f800cc | ||
|
|
9a1cadd49f | ||
|
|
b3285db5b0 | ||
|
|
8b1c009990 | ||
|
|
9061a59fc2 | ||
|
|
cbfed9c0d3 | ||
|
|
77447f7096 | ||
|
|
361bb92b3b | ||
|
|
e50d5f514c | ||
|
|
a7ac4633a8 | ||
|
|
6a27feb3f9 | ||
|
|
31b53fd6ed | ||
|
|
e04d4e8bea | ||
|
|
3e31223812 | ||
|
|
2832226539 | ||
|
|
dcbc5da2ba | ||
|
|
1d44bfe0cd | ||
|
|
6553118636 | ||
|
|
a238b264e5 | ||
|
|
4696237b21 | ||
|
|
b7956a82ee | ||
|
|
a5babc493f | ||
|
|
708bd4a292 | ||
|
|
d9e774dd77 | ||
|
|
97ef3b1d4b | ||
|
|
8e3ac280b0 | ||
|
|
7fedbdde03 | ||
|
|
6ad808f717 | ||
|
|
c6541399bb | ||
|
|
81028893b2 | ||
|
|
943862c0ea | ||
|
|
43511aacb9 | ||
|
|
ce68e2f855 | ||
|
|
ec06faef32 | ||
|
|
cfac54d44c | ||
|
|
f4b41bd958 | ||
|
|
7d7d71e95f | ||
|
|
2b21c2a28e | ||
|
|
dfa8725c55 | ||
|
|
1d8880c04d | ||
|
|
07fd3cef4c | ||
|
|
b58443140a | ||
|
|
a0b93d57e4 | ||
|
|
7cac574c31 | ||
|
|
86619a2ac9 | ||
|
|
058c1464d3 | ||
|
|
70e747c131 | ||
|
|
8869d3ebf0 | ||
|
|
d82f4f5c91 | ||
|
|
d9a6120cfe | ||
|
|
5c7ad58ce4 | ||
|
|
284510d0a3 | ||
|
|
0791848fa3 | ||
|
|
466f109edb | ||
|
|
87fe4aa28e | ||
|
|
baa86bab88 | ||
|
|
17cbe16773 | ||
|
|
660f3635aa | ||
|
|
10a28a9161 | ||
|
|
2964eff298 | ||
|
|
7119763c1e | ||
|
|
12bd10a095 | ||
|
|
221c95eb14 | ||
|
|
7815d501fa | ||
|
|
9dec717e10 | ||
|
|
d23368a826 | ||
|
|
33913743e9 | ||
|
|
d92b344e91 | ||
|
|
def065d86a | ||
|
|
5b50761d9f | ||
|
|
30a52e2bb8 | ||
|
|
b361e3fc82 | ||
|
|
7419e8a926 | ||
|
|
0d04509a97 | ||
|
|
4950ceb814 | ||
|
|
218d186969 | ||
|
|
4f6306e748 | ||
|
|
334478f3d4 | ||
|
|
fd4c50083c | ||
|
|
435471a48c | ||
|
|
be96e2c3a4 | ||
|
|
d018401f96 | ||
|
|
63bb9b8e68 | ||
|
|
682f559762 | ||
|
|
aa39af5ab4 | ||
|
|
d9503ef35a | ||
|
|
322a826576 | ||
|
|
362dea6c5c | ||
|
|
f74e908bd5 | ||
|
|
d4c11ff798 | ||
|
|
24bc3822b6 | ||
|
|
4fc796b177 | ||
|
|
c099242cb7 | ||
|
|
f7142b6f55 | ||
|
|
c0be28072b | ||
|
|
a218f9ef25 | ||
|
|
65e881fa5a | ||
|
|
2c34972629 | ||
|
|
9bd07034a5 | ||
|
|
285507d68a | ||
|
|
5c7227fc02 | ||
|
|
4a99fc5a74 | ||
|
|
0a2e50ce76 | ||
|
|
49d8c739c0 | ||
|
|
2c01e6347d | ||
|
|
d050ef4779 | ||
|
|
21d008aaee | ||
|
|
0d4ed102a0 | ||
|
|
83bcfcde06 | ||
|
|
32ac00b417 | ||
|
|
2a00aefdd6 | ||
|
|
679d615224 | ||
|
|
e4e980a6e3 | ||
|
|
73c5764d63 | ||
|
|
205d2758de | ||
|
|
efe7ea52bc | ||
|
|
938152edea | ||
|
|
3e1e9b6d56 | ||
|
|
8b01d1a88c | ||
|
|
9910fb7b99 | ||
|
|
a91d639606 | ||
|
|
eaeb942ad8 | ||
|
|
c509d0edbd | ||
|
|
59566d9d65 | ||
|
|
e457b8ddc0 | ||
|
|
a43f7734dc | ||
|
|
2618825ce2 | ||
|
|
af35cbb743 | ||
|
|
989361bb46 | ||
|
|
b18225561e | ||
|
|
a500119879 | ||
|
|
4d2a738748 | ||
|
|
38047b9d5c | ||
|
|
035a6be8b7 | ||
|
|
e3bc4a5fc7 | ||
|
|
d8d825c6b0 | ||
|
|
f47c243cb0 | ||
|
|
8c31f944fa | ||
|
|
931403ef0b | ||
|
|
17ff5f4640 | ||
|
|
ea590307c9 | ||
|
|
e12597b330 | ||
|
|
dd92b20ef5 | ||
|
|
fd0365eec9 | ||
|
|
cdeefcf4e9 | ||
|
|
5d799546ff | ||
|
|
931bd33a9f | ||
|
|
0b07aae1bb | ||
|
|
4ff769db0c | ||
|
|
5456cc2348 | ||
|
|
97baa69a8d | ||
|
|
66644b7369 | ||
|
|
4dcac53d6d | ||
|
|
ead1c1b851 | ||
|
|
f3a1ac425f | ||
|
|
e450e7d975 | ||
|
|
4984f99de0 | ||
|
|
3ae6bfcb57 | ||
|
|
5c6f727c4b | ||
|
|
4bbb75a6a3 | ||
|
|
a2dee15289 | ||
|
|
db59a4b925 | ||
|
|
96aaa6f0e3 | ||
|
|
9eb7654a3c | ||
|
|
d2902910bd | ||
|
|
3a7c9b1c1f | ||
|
|
6c214df289 | ||
|
|
c2fe33650c | ||
|
|
53748d62f6 | ||
|
|
c0364c0e00 | ||
|
|
2ccaa02e7e | ||
|
|
943bf1442d | ||
|
|
76f13abee1 | ||
|
|
06a0147a97 | ||
|
|
03984df562 | ||
|
|
f95ad1201d | ||
|
|
91e311284a | ||
|
|
ab554320f3 | ||
|
|
e5afb89706 | ||
|
|
0c1d78acdb | ||
|
|
3361ed1f2d | ||
|
|
4d7d8e84c5 | ||
|
|
28d3611c4a | ||
|
|
fbb758fa89 | ||
|
|
40f9651378 | ||
|
|
e90041976b | ||
|
|
6d737330f2 | ||
|
|
910c49df4f | ||
|
|
1a46fac7bf | ||
|
|
6bbdd74927 | ||
|
|
dd4c9c2536 | ||
|
|
0a6b22fdd9 | ||
|
|
d940066ea2 | ||
|
|
7be3143e55 | ||
|
|
b42875e2e1 | ||
|
|
6c6e8f0001 | ||
|
|
8219dfd9e6 | ||
|
|
025b994224 | ||
|
|
b056763f09 | ||
|
|
6e91326648 | ||
|
|
8b9c76a2b7 | ||
|
|
c4fc2d825d | ||
|
|
411ac94986 | ||
|
|
8b952a6caf | ||
|
|
b21bf0d428 | ||
|
|
1b5134c0bc | ||
|
|
54ffe1d3b6 | ||
|
|
ff77455b00 | ||
|
|
22eaefb3b2 | ||
|
|
b9710c4180 | ||
|
|
919b30c237 | ||
|
|
9a51d200f7 | ||
|
|
170df77c3d | ||
|
|
043f34d619 | ||
|
|
0b20f8a980 | ||
|
|
7f208eb141 | ||
|
|
75a196dd3b | ||
|
|
701a6170a3 | ||
|
|
ee70ed6146 | ||
|
|
d45a6f260d | ||
|
|
de09e56f67 | ||
|
|
17b520de12 | ||
|
|
0655e2e0a0 | ||
|
|
bcf489f847 | ||
|
|
adc2207249 | ||
|
|
1c205a96fb | ||
|
|
8e2bbbd570 | ||
|
|
13d1f1b48b | ||
|
|
7c4faf8b7a | ||
|
|
82be621850 | ||
|
|
265fb7c5f6 | ||
|
|
29a278d0bc | ||
|
|
2fb9dfe909 | ||
|
|
8ca369f937 | ||
|
|
caa02141b8 | ||
|
|
a1cea3b584 | ||
|
|
1b260aee52 | ||
|
|
f2182428e5 | ||
|
|
996b8f67c5 | ||
|
|
b297b2a48a | ||
|
|
74eab5bba9 | ||
|
|
87504f4d76 | ||
|
|
cd11d4c179 | ||
|
|
e821fab4a3 | ||
|
|
bf014ad781 | ||
|
|
b1fc88d330 | ||
|
|
1e54a474b1 |
@@ -1,2 +1 @@
|
||||
https://github.com/heroku/heroku-buildpack-nodejs.git
|
||||
https://github.com/stomita/heroku-buildpack-phantomjs.git
|
||||
https://github.com/heroku/heroku-buildpack-nodejs.git
|
||||
18
.github/workflows/test.yml
vendored
@@ -7,7 +7,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x]
|
||||
node-version: [14.x]
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
with:
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x]
|
||||
node-version: [14.x]
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
with:
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x]
|
||||
node-version: [14.x]
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
with:
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x]
|
||||
node-version: [14.x]
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
with:
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x]
|
||||
node-version: [14.x]
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
with:
|
||||
@@ -114,7 +114,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x]
|
||||
node-version: [14.x]
|
||||
mongodb-version: [4.2]
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
@@ -143,7 +143,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x]
|
||||
node-version: [14.x]
|
||||
mongodb-version: [4.2]
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
@@ -172,7 +172,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x]
|
||||
node-version: [14.x]
|
||||
mongodb-version: [4.2]
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
@@ -202,7 +202,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x]
|
||||
node-version: [14.x]
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
with:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:12
|
||||
FROM node:14
|
||||
|
||||
ENV ADMIN_EMAIL admin@habitica.com
|
||||
ENV EMAILS_COMMUNITY_MANAGER_EMAIL admin@habitica.com
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:12
|
||||
FROM node:14
|
||||
|
||||
# Install global packages
|
||||
RUN npm install -g gulp-cli mocha
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
"SLACK_URL": "https://hooks.slack.com/services/some-url",
|
||||
"STRIPE_API_KEY": "aaaabbbbccccddddeeeeffff00001111",
|
||||
"STRIPE_PUB_KEY": "22223333444455556666777788889999",
|
||||
"STRIPE_WEBHOOKS_ENDPOINT_SECRET": "111111",
|
||||
"TRANSIFEX_SLACK_CHANNEL": "transifex",
|
||||
"WEB_CONCURRENCY": 1,
|
||||
"SKIP_SSL_CHECK_KEY": "key",
|
||||
|
||||
@@ -3,9 +3,7 @@ import { exec } from 'child_process';
|
||||
import gulp from 'gulp';
|
||||
import os from 'os';
|
||||
import nconf from 'nconf';
|
||||
import {
|
||||
pipe,
|
||||
} from './taskHelper';
|
||||
import { pipe } from './taskHelper';
|
||||
import {
|
||||
getDevelopmentConnectionUrl,
|
||||
getDefaultConnectionOptions,
|
||||
@@ -21,15 +19,16 @@ const TEST_DB_URI = nconf.get('TEST_DB_URI');
|
||||
const SANITY_TEST_COMMAND = 'npm run test:sanity';
|
||||
const COMMON_TEST_COMMAND = 'npm run test:common';
|
||||
const CONTENT_TEST_COMMAND = 'npm run test:content';
|
||||
const CONTENT_OPTIONS = { maxBuffer: 1024 * 500 };
|
||||
const LIMIT_MAX_BUFFER_OPTIONS = { maxBuffer: 1024 * 500 };
|
||||
|
||||
/* Helper methods for reporting test summary */
|
||||
/* Helper method for reporting test summary */
|
||||
const testResults = [];
|
||||
const testCount = (stdout, regexp) => {
|
||||
const match = stdout.match(regexp);
|
||||
return parseInt(match && (match[1] || 0), 10);
|
||||
};
|
||||
|
||||
/* Helper methods to correctly run child test processes */
|
||||
const testBin = (string, additionalEnvVariables = '') => {
|
||||
if (os.platform() === 'win32') {
|
||||
if (additionalEnvVariables !== '') {
|
||||
@@ -41,6 +40,15 @@ const testBin = (string, additionalEnvVariables = '') => {
|
||||
return `NODE_ENV=test ${additionalEnvVariables} ${string}`;
|
||||
};
|
||||
|
||||
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`;
|
||||
}
|
||||
|
||||
/* Test task definitions */
|
||||
gulp.task('test:nodemon', gulp.series(done => {
|
||||
process.env.PORT = TEST_SERVER_PORT; // eslint-disable-line no-process-env
|
||||
process.env.NODE_DB_URI = TEST_DB_URI; // eslint-disable-line no-process-env
|
||||
@@ -82,31 +90,9 @@ gulp.task('test:prepare', gulp.series(
|
||||
done => done(),
|
||||
));
|
||||
|
||||
gulp.task('test:sanity', cb => {
|
||||
const runner = exec(
|
||||
testBin(SANITY_TEST_COMMAND),
|
||||
err => {
|
||||
if (err) {
|
||||
process.exit(1);
|
||||
}
|
||||
cb();
|
||||
},
|
||||
);
|
||||
pipe(runner);
|
||||
});
|
||||
gulp.task('test:sanity', runInChildProcess(SANITY_TEST_COMMAND));
|
||||
|
||||
gulp.task('test:common', gulp.series('test:prepare:build', cb => {
|
||||
const runner = exec(
|
||||
testBin(COMMON_TEST_COMMAND),
|
||||
err => {
|
||||
if (err) {
|
||||
process.exit(1);
|
||||
}
|
||||
cb();
|
||||
},
|
||||
);
|
||||
pipe(runner);
|
||||
}));
|
||||
gulp.task('test:common', gulp.series('test:prepare:build', runInChildProcess(COMMON_TEST_COMMAND)));
|
||||
|
||||
gulp.task('test:common:clean', cb => {
|
||||
pipe(exec(testBin(COMMON_TEST_COMMAND), () => cb()));
|
||||
@@ -130,22 +116,11 @@ gulp.task('test:common:safe', gulp.series('test:prepare:build', cb => {
|
||||
pipe(runner);
|
||||
}));
|
||||
|
||||
gulp.task('test:content', gulp.series('test:prepare:build', cb => {
|
||||
const runner = exec(
|
||||
testBin(CONTENT_TEST_COMMAND),
|
||||
CONTENT_OPTIONS,
|
||||
err => {
|
||||
if (err) {
|
||||
process.exit(1);
|
||||
}
|
||||
cb();
|
||||
},
|
||||
);
|
||||
pipe(runner);
|
||||
}));
|
||||
gulp.task('test:content', gulp.series('test:prepare:build',
|
||||
runInChildProcess(CONTENT_TEST_COMMAND, LIMIT_MAX_BUFFER_OPTIONS)));
|
||||
|
||||
gulp.task('test:content:clean', cb => {
|
||||
pipe(exec(testBin(CONTENT_TEST_COMMAND), CONTENT_OPTIONS, () => cb()));
|
||||
pipe(exec(testBin(CONTENT_TEST_COMMAND), LIMIT_MAX_BUFFER_OPTIONS, () => cb()));
|
||||
});
|
||||
|
||||
gulp.task('test:content:watch', gulp.series('test:content:clean', () => gulp.watch(['common/script/content/**', 'test/**'], gulp.series('test:content:clean', done => done()))));
|
||||
@@ -153,7 +128,7 @@ gulp.task('test:content:watch', gulp.series('test:content:clean', () => gulp.wat
|
||||
gulp.task('test:content:safe', gulp.series('test:prepare:build', cb => {
|
||||
const runner = exec(
|
||||
testBin(CONTENT_TEST_COMMAND),
|
||||
CONTENT_OPTIONS,
|
||||
LIMIT_MAX_BUFFER_OPTIONS,
|
||||
(err, stdout) => { // eslint-disable-line handle-callback-err
|
||||
testResults.push({
|
||||
suite: 'Content Specs\t',
|
||||
@@ -167,76 +142,39 @@ gulp.task('test:content:safe', gulp.series('test:prepare:build', cb => {
|
||||
pipe(runner);
|
||||
}));
|
||||
|
||||
gulp.task('test:api:unit:run', done => {
|
||||
const runner = exec(
|
||||
testBin('istanbul cover --dir coverage/api-unit node_modules/mocha/bin/_mocha -- test/api/unit --recursive --require ./test/helpers/start-server'),
|
||||
err => {
|
||||
if (err) {
|
||||
process.exit(1);
|
||||
}
|
||||
done();
|
||||
},
|
||||
);
|
||||
|
||||
pipe(runner);
|
||||
});
|
||||
gulp.task('test:api:unit:run',
|
||||
runInChildProcess(integrationTestCommand('test/api/unit', 'coverage/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())));
|
||||
|
||||
gulp.task('test:api-v3:integration', gulp.series('test:prepare:mongo', done => {
|
||||
const runner = exec(
|
||||
testBin('istanbul cover --dir coverage/api-v3-integration --report lcovonly node_modules/mocha/bin/_mocha -- test/api/v3/integration --recursive --require ./test/helpers/start-server'),
|
||||
{ maxBuffer: 500 * 1024 },
|
||||
err => {
|
||||
if (err) {
|
||||
process.exit(1);
|
||||
}
|
||||
done();
|
||||
},
|
||||
);
|
||||
|
||||
pipe(runner);
|
||||
}));
|
||||
gulp.task('test:api-v3:integration', gulp.series('test:prepare:mongo',
|
||||
runInChildProcess(
|
||||
integrationTestCommand('test/api/v3/integration', 'coverage/api-v3-integration'),
|
||||
LIMIT_MAX_BUFFER_OPTIONS,
|
||||
)));
|
||||
|
||||
gulp.task('test:api-v3:integration:watch', () => gulp.watch([
|
||||
'website/server/controllers/api-v3/**/*', 'common/script/ops/*', 'website/server/libs/*.js',
|
||||
'test/api/v3/integration/**/*',
|
||||
], gulp.series('test:api-v3:integration', done => done())));
|
||||
|
||||
gulp.task('test:api-v3:integration:separate-server', done => {
|
||||
const runner = exec(
|
||||
testBin('mocha test/api/v3/integration --recursive --require ./test/helpers/start-server', 'LOAD_SERVER=0'),
|
||||
{ maxBuffer: 500 * 1024 },
|
||||
err => done(err),
|
||||
);
|
||||
gulp.task('test:api-v3:integration:separate-server', runInChildProcess(
|
||||
'mocha test/api/v3/integration --recursive --require ./test/helpers/start-server',
|
||||
LIMIT_MAX_BUFFER_OPTIONS,
|
||||
'LOAD_SERVER=0',
|
||||
));
|
||||
|
||||
pipe(runner);
|
||||
});
|
||||
gulp.task('test:api-v4:integration', gulp.series('test:prepare:mongo',
|
||||
runInChildProcess(
|
||||
integrationTestCommand('test/api/v4', 'api-v4-integration'),
|
||||
LIMIT_MAX_BUFFER_OPTIONS,
|
||||
)));
|
||||
|
||||
gulp.task('test:api-v4:integration', gulp.series('test:prepare:mongo', done => {
|
||||
const runner = exec(
|
||||
testBin('istanbul cover --dir coverage/api-v4-integration --report lcovonly node_modules/mocha/bin/_mocha -- test/api/v4 --recursive --require ./test/helpers/start-server'),
|
||||
{ maxBuffer: 500 * 1024 },
|
||||
err => {
|
||||
if (err) {
|
||||
process.exit(1);
|
||||
}
|
||||
done();
|
||||
},
|
||||
);
|
||||
|
||||
pipe(runner);
|
||||
}));
|
||||
|
||||
gulp.task('test:api-v4:integration:separate-server', done => {
|
||||
const runner = exec(
|
||||
testBin('mocha test/api/v4 --recursive --require ./test/helpers/start-server', 'LOAD_SERVER=0'),
|
||||
{ maxBuffer: 500 * 1024 },
|
||||
err => done(err),
|
||||
);
|
||||
|
||||
pipe(runner);
|
||||
});
|
||||
gulp.task('test:api-v4:integration:separate-server', runInChildProcess(
|
||||
'mocha test/api/v4 --recursive --require ./test/helpers/start-server',
|
||||
LIMIT_MAX_BUFFER_OPTIONS,
|
||||
'LOAD_SERVER=0',
|
||||
));
|
||||
|
||||
gulp.task('test:api:unit', gulp.series(
|
||||
'test:prepare:mongo',
|
||||
|
||||
82
migrations/archive/2020/20201020_pet_color_achievements.js
Normal file
@@ -0,0 +1,82 @@
|
||||
/* eslint-disable no-console */
|
||||
const MIGRATION_NAME = '20201020_pet_color_achievements';
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count++;
|
||||
|
||||
const set = {
|
||||
migration: MIGRATION_NAME,
|
||||
};
|
||||
|
||||
if (user && user.items && user.items.pets) {
|
||||
const pets = user.items.pets;
|
||||
if (pets['Wolf-Golden'] > 0
|
||||
&& pets['TigerCub-Skeleton'] > 0
|
||||
&& pets['PandaCub-Skeleton'] > 0
|
||||
&& pets['LionCub-Skeleton'] > 0
|
||||
&& pets['Fox-Skeleton'] > 0
|
||||
&& pets['FlyingPig-Skeleton'] > 0
|
||||
&& pets['Dragon-Skeleton'] > 0
|
||||
&& pets['Cactus-Skeleton'] > 0
|
||||
&& pets['BearCub-Skeleton'] > 0) {
|
||||
set['achievements.boneCollector'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (user && user.items && user.items.mounts) {
|
||||
const mounts = user.items.mounts;
|
||||
if (mounts['Wolf-Skeleton']
|
||||
&& mounts['TigerCub-Skeleton']
|
||||
&& mounts['PandaCub-Skeleton']
|
||||
&& mounts['LionCub-Skeleton']
|
||||
&& mounts['Fox-Skeleton']
|
||||
&& mounts['FlyingPig-Skeleton']
|
||||
&& mounts['Dragon-Skeleton']
|
||||
&& mounts['Cactus-Skeleton']
|
||||
&& mounts['BearCub-Skeleton'] ) {
|
||||
set['achievements.skeletonCrew'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
return await User.update({ _id: user._id }, { $set: set }).exec();
|
||||
}
|
||||
|
||||
module.exports = async function processUsers () {
|
||||
let query = {
|
||||
migration: { $ne: MIGRATION_NAME },
|
||||
'auth.timestamps.loggedin': { $gt: new Date('2020-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]._id,
|
||||
};
|
||||
}
|
||||
|
||||
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
};
|
||||
84
migrations/archive/2020/20201029_habitoween_ladder.js
Normal file
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Award Habitoween ladder items to participants in this month's Habitoween festivities
|
||||
*/
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const MIGRATION_NAME = '20201029_habitoween_ladder'; // Update when running in future years
|
||||
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count++;
|
||||
|
||||
const set = {};
|
||||
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,
|
||||
};
|
||||
|
||||
set.migration = MIGRATION_NAME;
|
||||
|
||||
if (user && user.items && user.items.mounts && user.items.mounts['JackOLantern-Glow']) {
|
||||
set['items.pets.JackOLantern-RoyalPurple'] = 5;
|
||||
} else if (user && user.items && user.items.pets && user.items.pets['JackOLantern-Glow']) {
|
||||
set['items.mounts.JackOLantern-Glow'] = true;
|
||||
} else if (user && user.items && user.items.mounts && user.items.mounts['JackOLantern-Ghost']) {
|
||||
set['items.pets.JackOLantern-Glow'] = 5;
|
||||
} else if (user && user.items && user.items.pets && user.items.pets['JackOLantern-Ghost']) {
|
||||
set['items.mounts.JackOLantern-Ghost'] = true;
|
||||
} else if (user && user.items && user.items.mounts && user.items.mounts['JackOLantern-Base']) {
|
||||
set['items.pets.JackOLantern-Ghost'] = 5;
|
||||
} else if (user && user.items && user.items.pets && user.items.pets['JackOLantern-Base']) {
|
||||
set['items.mounts.JackOLantern-Base'] = true;
|
||||
} else {
|
||||
set['items.pets.JackOLantern-Base'] = 5;
|
||||
}
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
return await User.update({_id: user._id}, {$inc: inc, $set: set}).exec();
|
||||
}
|
||||
|
||||
module.exports = async function processUsers () {
|
||||
let query = {
|
||||
migration: {$ne: MIGRATION_NAME},
|
||||
'auth.timestamps.loggedin': {$gt: new Date('2020-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
|
||||
}
|
||||
};
|
||||
58
migrations/archive/2020/20201102_fix_habitoween.js
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Fix JackOLantern-Base for users that signed up recently
|
||||
*/
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const MIGRATION_NAME = '20201102_fix_habitoween'; // Update when running in future years
|
||||
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count++;
|
||||
|
||||
const set = {};
|
||||
|
||||
set.migration = MIGRATION_NAME;
|
||||
set['items.pets.JackOLantern-Base'] = 5;
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
return await User.update({_id: user._id}, {$inc: inc, $set: set}).exec();
|
||||
}
|
||||
|
||||
module.exports = async function processUsers () {
|
||||
let query = {
|
||||
migration: {$ne: MIGRATION_NAME},
|
||||
'auth.timestamps.created': {$gt: new Date('2020-10-26')},
|
||||
'items.pets.JackOLantern-Base': true,
|
||||
};
|
||||
|
||||
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
|
||||
}
|
||||
};
|
||||
62
migrations/archive/2020/20201103_drop_cap_ab_tweaks.js
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* All web users should be enrolled in the Drop Cap AB Test
|
||||
*/
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const MIGRATION_NAME = '20201103_drop_cap_ab_tweaks';
|
||||
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count++;
|
||||
|
||||
const set = {};
|
||||
|
||||
set.migration = MIGRATION_NAME;
|
||||
|
||||
const testGroup = Math.random();
|
||||
// Enroll 100% of users, splitting them 50/50
|
||||
const value = testGroup <= 0.50 ? 'drop-cap-notif-enabled' : 'drop-cap-notif-disabled';
|
||||
set['_ABtests.dropCapNotif'] = value;
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
return await User.update({_id: user._id}, {$set: set}).exec();
|
||||
}
|
||||
|
||||
module.exports = async function processUsers () {
|
||||
let query = {
|
||||
migration: {$ne: MIGRATION_NAME},
|
||||
'auth.timestamps.loggedin': {$gt: new Date('2020-10-10')},
|
||||
'_ABtests.dropCapNotif': 'drop-cap-notif-not-enrolled',
|
||||
};
|
||||
|
||||
const fields = {
|
||||
_id: 1,
|
||||
_ABtests: 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
|
||||
}
|
||||
};
|
||||
62
migrations/archive/2020/20201111_api_date.js
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Fix dates in the database that were stored as $type string instead of Date
|
||||
*/
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const MIGRATION_NAME = '20201111_api_date';
|
||||
|
||||
import * as Tasks from '../../../website/server/models/task';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (todo) {
|
||||
count++;
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${todo._id}`);
|
||||
|
||||
const newDate = new Date(todo.date);
|
||||
if (isValidDate(newDate)) return;
|
||||
|
||||
return await Tasks.Task.update({_id: todo._id, type: 'todo'}, {$unset: {date: ''}}).exec();
|
||||
}
|
||||
|
||||
module.exports = async function processUsers () {
|
||||
let query = {
|
||||
type: 'todo',
|
||||
date: {$exists: true},
|
||||
updatedAt: {$gt: new Date('2020-11-23')},
|
||||
};
|
||||
|
||||
const fields = {
|
||||
_id: 1,
|
||||
type: 1,
|
||||
date: 1,
|
||||
};
|
||||
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
const users = await Tasks.Task // eslint-disable-line no-await-in-loop
|
||||
.find(query)
|
||||
.select(fields)
|
||||
.limit(250)
|
||||
.sort({_id: 1})
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (users.length === 0) {
|
||||
console.warn('All appropriate tasks found and modified.');
|
||||
console.warn(`\n${count} tasks processed\n`);
|
||||
break;
|
||||
} else {
|
||||
query._id = {
|
||||
$gt: users[users.length - 1],
|
||||
};
|
||||
}
|
||||
|
||||
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
};
|
||||
|
||||
function isValidDate(d) {
|
||||
return !isNaN(d.getTime());
|
||||
}
|
||||
82
migrations/archive/2020/20201124_pet_color_achievements.js
Normal file
@@ -0,0 +1,82 @@
|
||||
/* eslint-disable no-console */
|
||||
const MIGRATION_NAME = '20201124_pet_color_achievements';
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count++;
|
||||
|
||||
const set = {
|
||||
migration: MIGRATION_NAME,
|
||||
};
|
||||
|
||||
if (user && user.items && user.items.pets) {
|
||||
const pets = user.items.pets;
|
||||
if (pets['Wolf-Red'] > 0
|
||||
&& pets['TigerCub-Red'] > 0
|
||||
&& pets['PandaCub-Red'] > 0
|
||||
&& pets['LionCub-Red'] > 0
|
||||
&& pets['Fox-Red'] > 0
|
||||
&& pets['FlyingPig-Red'] > 0
|
||||
&& pets['Dragon-Red'] > 0
|
||||
&& pets['Cactus-Red'] > 0
|
||||
&& pets['BearCub-Red'] > 0) {
|
||||
set['achievements.seeingRed'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (user && user.items && user.items.mounts) {
|
||||
const mounts = user.items.mounts;
|
||||
if (mounts['Wolf-Red']
|
||||
&& mounts['TigerCub-Red']
|
||||
&& mounts['PandaCub-Red']
|
||||
&& mounts['LionCub-Red']
|
||||
&& mounts['Fox-Red']
|
||||
&& mounts['FlyingPig-Red']
|
||||
&& mounts['Dragon-Red']
|
||||
&& mounts['Cactus-Red']
|
||||
&& mounts['BearCub-Red'] ) {
|
||||
set['achievements.redLetterDay'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
return await User.update({ _id: user._id }, { $set: set }).exec();
|
||||
}
|
||||
|
||||
module.exports = async function processUsers () {
|
||||
let query = {
|
||||
migration: { $ne: MIGRATION_NAME },
|
||||
'auth.timestamps.loggedin': { $gt: new Date('2020-11-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]._id,
|
||||
};
|
||||
}
|
||||
|
||||
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
};
|
||||
126
migrations/archive/2020/20201126_harvest_feast.js
Normal file
@@ -0,0 +1,126 @@
|
||||
/* eslint-disable no-console */
|
||||
const MIGRATION_NAME = '20201126_harvest_feast';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count++;
|
||||
|
||||
const set = {};
|
||||
let inc;
|
||||
let push;
|
||||
|
||||
set.migration = MIGRATION_NAME;
|
||||
|
||||
if (typeof user.items.gear.owned.head_special_turkeyHelmGilded !== 'undefined') {
|
||||
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,
|
||||
}
|
||||
} else if (typeof user.items.gear.owned.armor_special_turkeyArmorBase !== 'undefined') {
|
||||
set['items.gear.owned.head_special_turkeyHelmGilded'] = false;
|
||||
set['items.gear.owned.armor_special_turkeyArmorGilded'] = false;
|
||||
set['items.gear.owned.back_special_turkeyTailGilded'] = false;
|
||||
push = [
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.head_special_turkeyHelmGilded',
|
||||
_id: uuid(),
|
||||
},
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.armor_special_turkeyArmorGilded',
|
||||
_id: uuid(),
|
||||
},
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.back_special_turkeyTailGilded',
|
||||
_id: uuid(),
|
||||
},
|
||||
];
|
||||
} else if (user.items && user.items.mounts && user.items.mounts['Turkey-Gilded']) {
|
||||
set['items.gear.owned.head_special_turkeyHelmBase'] = false;
|
||||
set['items.gear.owned.armor_special_turkeyArmorBase'] = false;
|
||||
set['items.gear.owned.back_special_turkeyTailBase'] = false;
|
||||
push = [
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.head_special_turkeyHelmBase',
|
||||
_id: uuid(),
|
||||
},
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.armor_special_turkeyArmorBase',
|
||||
_id: uuid(),
|
||||
},
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.back_special_turkeyTailBase',
|
||||
_id: uuid(),
|
||||
},
|
||||
];
|
||||
} else if (user.items && user.items.pets && user.items.pets['Turkey-Gilded']) {
|
||||
set['items.mounts.Turkey-Gilded'] = true;
|
||||
} else if (user.items && user.items.mounts && user.items.mounts['Turkey-Base']) {
|
||||
set['items.pets.Turkey-Gilded'] = 5;
|
||||
} else if (user.items && user.items.pets && user.items.pets['Turkey-Base']) {
|
||||
set['items.mounts.Turkey-Base'] = true;
|
||||
} else {
|
||||
set['items.pets.Turkey-Base'] = 5;
|
||||
}
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
if (inc) {
|
||||
return await User.update({_id: user._id}, {$inc: inc, $set: set}).exec();
|
||||
} else if (push) {
|
||||
return await User.update({_id: user._id}, {$set: set, $push: {pinnedItems: {$each: push}}}).exec();
|
||||
} else {
|
||||
return await User.update({_id: user._id}, {$set: set}).exec();
|
||||
}
|
||||
}
|
||||
|
||||
export default async function processUsers () {
|
||||
let query = {
|
||||
migration: {$ne: MIGRATION_NAME},
|
||||
'auth.timestamps.loggedin': {$gt: new Date('2019-11-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
|
||||
}
|
||||
};
|
||||
126
migrations/archive/2020/20201229_nye.js
Normal file
@@ -0,0 +1,126 @@
|
||||
/* eslint-disable no-console */
|
||||
const MIGRATION_NAME = '20201229_nye';
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count++;
|
||||
|
||||
const set = { migration: MIGRATION_NAME };
|
||||
let push;
|
||||
|
||||
if (typeof user.items.gear.owned.head_special_nye2019 !== 'undefined') {
|
||||
set['items.gear.owned.head_special_nye2020'] = false;
|
||||
push = [
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.head_special_nye2020',
|
||||
_id: uuid(),
|
||||
},
|
||||
];
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2018 !== 'undefined') {
|
||||
set['items.gear.owned.head_special_nye2019'] = false;
|
||||
push = [
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.head_special_nye2019',
|
||||
_id: uuid(),
|
||||
},
|
||||
];
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2017 !== 'undefined') {
|
||||
set['items.gear.owned.head_special_nye2018'] = false;
|
||||
push = [
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.head_special_nye2018',
|
||||
_id: uuid(),
|
||||
},
|
||||
];
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2016 !== 'undefined') {
|
||||
set['items.gear.owned.head_special_nye2017'] = false;
|
||||
push = [
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.head_special_nye2017',
|
||||
_id: uuid(),
|
||||
},
|
||||
];
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2015 !== 'undefined') {
|
||||
set['items.gear.owned.head_special_nye2016'] = false;
|
||||
push = [
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.head_special_nye2016',
|
||||
_id: uuid(),
|
||||
},
|
||||
];
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2014 !== 'undefined') {
|
||||
set['items.gear.owned.head_special_nye2015'] = false;
|
||||
push = [
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.head_special_nye2015',
|
||||
_id: uuid(),
|
||||
},
|
||||
];
|
||||
} else if (typeof user.items.gear.owned.head_special_nye !== 'undefined') {
|
||||
set['items.gear.owned.head_special_nye2014'] = false;
|
||||
push = [
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.head_special_nye2014',
|
||||
_id: uuid(),
|
||||
},
|
||||
];
|
||||
} else {
|
||||
set['items.gear.owned.head_special_nye'] = false;
|
||||
push = [
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.head_special_nye',
|
||||
_id: uuid(),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
return await User.update({_id: user._id}, {$set: set, $push: {pinnedItems: {$each: push}}}).exec();
|
||||
}
|
||||
|
||||
export default async function processUsers () {
|
||||
let query = {
|
||||
'auth.timestamps.loggedin': {$gt: new Date('2020-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
|
||||
}
|
||||
};
|
||||
2006
package-lock.json
generated
50
package.json
@@ -1,26 +1,26 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "4.159.1",
|
||||
"version": "4.176.0",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.11.6",
|
||||
"@babel/preset-env": "^7.11.5",
|
||||
"@babel/register": "^7.11.5",
|
||||
"@google-cloud/trace-agent": "^5.1.0",
|
||||
"@slack/client": "^4.12.0",
|
||||
"@babel/core": "^7.12.10",
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/register": "^7.12.10",
|
||||
"@google-cloud/trace-agent": "^5.1.1",
|
||||
"@slack/webhook": "^5.0.3",
|
||||
"@parse/node-apn": "^4.0.0",
|
||||
"accepts": "^1.3.5",
|
||||
"amazon-payments": "^0.2.8",
|
||||
"amplitude": "^3.5.0",
|
||||
"amplitude": "^5.1.4",
|
||||
"apidoc": "^0.25.0",
|
||||
"apn": "^2.2.0",
|
||||
"apple-auth": "^1.0.6",
|
||||
"bcrypt": "^5.0.0",
|
||||
"body-parser": "^1.18.3",
|
||||
"compression": "^1.7.4",
|
||||
"cookie-session": "^1.4.0",
|
||||
"coupon-code": "^0.4.5",
|
||||
"csv-stringify": "^5.5.1",
|
||||
"csv-stringify": "^5.5.3",
|
||||
"cwait": "^1.1.1",
|
||||
"domain-middleware": "~0.1.0",
|
||||
"eslint": "^6.8.0",
|
||||
@@ -30,27 +30,27 @@
|
||||
"express-basic-auth": "^1.1.5",
|
||||
"express-validator": "^5.2.0",
|
||||
"glob": "^7.1.6",
|
||||
"got": "^11.6.2",
|
||||
"got": "^11.8.1",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-babel": "^8.0.0",
|
||||
"gulp-imagemin": "^7.1.0",
|
||||
"gulp-nodemon": "^2.5.0",
|
||||
"gulp.spritesmith": "^6.9.0",
|
||||
"habitica-markdown": "^3.0.0",
|
||||
"helmet": "^3.23.3",
|
||||
"image-size": "^0.9.1",
|
||||
"helmet": "^4.2.0",
|
||||
"image-size": "^0.9.3",
|
||||
"in-app-purchase": "^1.11.3",
|
||||
"js2xmlparser": "^4.0.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"jwks-rsa": "^1.9.0",
|
||||
"jwks-rsa": "^1.12.0",
|
||||
"lodash": "^4.17.20",
|
||||
"merge-stream": "^2.0.0",
|
||||
"method-override": "^3.0.0",
|
||||
"moment": "^2.28.0",
|
||||
"moment": "^2.29.1",
|
||||
"moment-recur": "^1.0.7",
|
||||
"mongoose": "^5.10.3",
|
||||
"mongoose": "^5.11.8",
|
||||
"morgan": "^1.10.0",
|
||||
"nconf": "^0.10.0",
|
||||
"nconf": "^0.11.0",
|
||||
"node-gcm": "^1.0.3",
|
||||
"on-headers": "^1.0.2",
|
||||
"passport": "^0.4.1",
|
||||
@@ -60,18 +60,18 @@
|
||||
"paypal-rest-sdk": "^1.8.1",
|
||||
"pp-ipn": "^1.1.0",
|
||||
"ps-tree": "^1.0.0",
|
||||
"rate-limiter-flexible": "^2.1.10",
|
||||
"rate-limiter-flexible": "^2.1.15",
|
||||
"redis": "^3.0.2",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"remove-markdown": "^0.3.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"short-uuid": "^3.0.0",
|
||||
"stripe": "^7.15.0",
|
||||
"short-uuid": "^4.1.0",
|
||||
"stripe": "^8.129.0",
|
||||
"superagent": "^6.1.0",
|
||||
"universal-analytics": "^0.4.23",
|
||||
"useragent": "^2.1.9",
|
||||
"uuid": "^8.3.0",
|
||||
"validator": "^13.1.1",
|
||||
"uuid": "^8.3.2",
|
||||
"validator": "^13.5.2",
|
||||
"vinyl-buffer": "^1.0.1",
|
||||
"winston": "^3.3.3",
|
||||
"winston-loggly-bulk": "^3.1.1",
|
||||
@@ -79,7 +79,7 @@
|
||||
},
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": "^12",
|
||||
"node": "^14",
|
||||
"npm": "^6"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -109,7 +109,7 @@
|
||||
"apidoc": "gulp apidoc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"axios": "^0.19.2",
|
||||
"axios": "^0.21.0",
|
||||
"chai": "^4.1.2",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"chai-moment": "^0.1.0",
|
||||
@@ -120,8 +120,8 @@
|
||||
"mocha": "^5.1.1",
|
||||
"monk": "^7.3.2",
|
||||
"require-again": "^2.0.0",
|
||||
"run-rs": "^0.6.2",
|
||||
"sinon": "^9.0.3",
|
||||
"run-rs": "^0.7.3",
|
||||
"sinon": "^9.2.2",
|
||||
"sinon-chai": "^3.5.0",
|
||||
"sinon-stub-promise": "^4.0.0"
|
||||
},
|
||||
|
||||
@@ -35,13 +35,12 @@ async function deleteAmplitudeData (userId, email) {
|
||||
}
|
||||
|
||||
async function deleteHabiticaData (user, email) {
|
||||
const truncatedEmail = email.slice(0, email.indexOf('@'));
|
||||
const set = {
|
||||
'auth.blocked': false,
|
||||
'auth.local.hashed_password': '$2a$10$QDnNh1j1yMPnTXDEOV38xOePEWFd4X8DSYwAM8XTmqmacG5X0DKjW',
|
||||
'auth.local.passwordHashMethod': 'bcrypt',
|
||||
};
|
||||
if (!user.auth.local.email) set['auth.local.email'] = `${truncatedEmail}-gdpr@example.com`;
|
||||
if (!user.auth.local.email) set['auth.local.email'] = `${user._id}@example.com`;
|
||||
await User.update(
|
||||
{ _id: user._id },
|
||||
{ $set: set },
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* eslint-disable camelcase */
|
||||
import nconf from 'nconf';
|
||||
import Amplitude from 'amplitude';
|
||||
import { Visitor } from 'universal-analytics';
|
||||
import * as analyticsService from '../../../../website/server/libs/analyticsService';
|
||||
@@ -15,6 +16,22 @@ describe('analyticsService', () => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('#getServiceByEnvironment', () => {
|
||||
it('returns mock methods when not in production', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(false);
|
||||
expect(analyticsService.getAnalyticsServiceByEnvironment())
|
||||
.to.equal(analyticsService.mockAnalyticsService);
|
||||
});
|
||||
|
||||
it('returns real methods when in production', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
|
||||
expect(analyticsService.getAnalyticsServiceByEnvironment().track)
|
||||
.to.equal(analyticsService.track);
|
||||
expect(analyticsService.getAnalyticsServiceByEnvironment().trackPurchase)
|
||||
.to.equal(analyticsService.trackPurchase);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#track', () => {
|
||||
let eventType; let
|
||||
data;
|
||||
|
||||
@@ -171,28 +171,32 @@ describe('highlightMentions', () => {
|
||||
it('github issue 12118, method crashes when square brackets are used', async () => {
|
||||
const text = '[test]';
|
||||
|
||||
let err;
|
||||
const result = await highlightMentions(text);
|
||||
|
||||
try {
|
||||
await highlightMentions(text);
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
expect(err).to.be.undefined;
|
||||
expect(result[0]).to.equal(text);
|
||||
});
|
||||
|
||||
it('github issue 12138, method crashes when regex chars are used in code block', async () => {
|
||||
const text = '`[test]`';
|
||||
|
||||
let err;
|
||||
const result = await highlightMentions(text);
|
||||
|
||||
try {
|
||||
await highlightMentions(text);
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
expect(result[0]).to.equal(text);
|
||||
});
|
||||
|
||||
expect(err).to.be.undefined;
|
||||
it('github issue 12586, method crashes when empty link is used', async () => {
|
||||
const text = '[]()';
|
||||
|
||||
const result = await highlightMentions(text);
|
||||
|
||||
expect(result[0]).to.equal(text);
|
||||
});
|
||||
|
||||
it('github issue 12586, method crashes when link without title is used', async () => {
|
||||
const text = '[](www.google.com)';
|
||||
|
||||
const result = await highlightMentions(text);
|
||||
|
||||
expect(result[0]).to.equal(text);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import amzLib from '../../../../../../website/server/libs/payments/amazon';
|
||||
import payments from '../../../../../../website/server/libs/payments/payments';
|
||||
import common from '../../../../../../website/common';
|
||||
import apiError from '../../../../../../website/server/libs/apiError';
|
||||
import * as gems from '../../../../../../website/server/libs/payments/gems';
|
||||
|
||||
const { i18n } = common;
|
||||
|
||||
@@ -88,6 +89,7 @@ describe('Amazon Payments - Checkout', () => {
|
||||
paymentCreateSubscritionStub.resolves({});
|
||||
|
||||
sinon.stub(common, 'uuid').returns('uuid-generated');
|
||||
sandbox.stub(gems, 'validateGiftMessage');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -111,7 +113,10 @@ describe('Amazon Payments - Checkout', () => {
|
||||
if (gift) {
|
||||
expectedArgs.gift = gift;
|
||||
expectedArgs.gemsBlock = undefined;
|
||||
expect(gems.validateGiftMessage).to.be.calledOnce;
|
||||
expect(gems.validateGiftMessage).to.be.calledWith(gift, user);
|
||||
} else {
|
||||
expect(gems.validateGiftMessage).to.not.be.called;
|
||||
expectedArgs.gemsBlock = gemsBlock;
|
||||
}
|
||||
expect(paymentBuyGemsStub).to.be.calledWith(expectedArgs);
|
||||
|
||||
@@ -5,6 +5,7 @@ import applePayments from '../../../../../website/server/libs/payments/apple';
|
||||
import iap from '../../../../../website/server/libs/inAppPurchases';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import common from '../../../../../website/common';
|
||||
import * as gems from '../../../../../website/server/libs/payments/gems';
|
||||
|
||||
const { i18n } = common;
|
||||
|
||||
@@ -15,7 +16,7 @@ describe('Apple Payments', () => {
|
||||
let sku; let user; let token; let receipt; let
|
||||
headers;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let paymentBuyGemsStub; let
|
||||
iapGetPurchaseDataStub;
|
||||
iapGetPurchaseDataStub; let validateGiftMessageStub;
|
||||
|
||||
beforeEach(() => {
|
||||
token = 'testToken';
|
||||
@@ -36,6 +37,7 @@ describe('Apple Payments', () => {
|
||||
transactionId: token,
|
||||
}]);
|
||||
paymentBuyGemsStub = sinon.stub(payments, 'buyGems').resolves({});
|
||||
validateGiftMessageStub = sinon.stub(gems, 'validateGiftMessage');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -44,6 +46,7 @@ describe('Apple Payments', () => {
|
||||
iap.isValidated.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
payments.buyGems.restore();
|
||||
gems.validateGiftMessage.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if receipt is invalid', async () => {
|
||||
@@ -143,6 +146,7 @@ describe('Apple Payments', () => {
|
||||
expect(iapIsValidatedStub).to.be.calledOnce;
|
||||
expect(iapIsValidatedStub).to.be.calledWith({});
|
||||
expect(iapGetPurchaseDataStub).to.be.calledOnce;
|
||||
expect(validateGiftMessageStub).to.not.be.called;
|
||||
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
@@ -180,6 +184,9 @@ describe('Apple Payments', () => {
|
||||
expect(iapIsValidatedStub).to.be.calledWith({});
|
||||
expect(iapGetPurchaseDataStub).to.be.calledOnce;
|
||||
|
||||
expect(validateGiftMessageStub).to.be.calledOnce;
|
||||
expect(validateGiftMessageStub).to.be.calledWith(gift, user);
|
||||
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import common from '../../../../../website/common';
|
||||
import { getGemsBlock } from '../../../../../website/server/libs/payments/gems';
|
||||
import {
|
||||
getGemsBlock,
|
||||
validateGiftMessage,
|
||||
} from '../../../../../website/server/libs/payments/gems';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
|
||||
const { i18n } = common;
|
||||
|
||||
describe('payments/gems', () => {
|
||||
describe('#getGemsBlock', () => {
|
||||
@@ -11,4 +17,50 @@ describe('payments/gems', () => {
|
||||
expect(getGemsBlock('21gems')).to.equal(common.content.gems['21gems']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#validateGiftMessage', () => {
|
||||
let user;
|
||||
let gift;
|
||||
|
||||
beforeEach(() => {
|
||||
user = new User();
|
||||
|
||||
gift = {
|
||||
message: (` // exactly 201 chars
|
||||
A gift message that is over the 200 chars limit.
|
||||
A gift message that is over the 200 chars limit.
|
||||
A gift message that is over the 200 chars limit.
|
||||
A gift message that is over the 200 chars limit. 1
|
||||
`).trim().substring(0, 201),
|
||||
};
|
||||
|
||||
expect(gift.message.length).to.equal(201);
|
||||
});
|
||||
|
||||
it('throws if the gift message is too long', () => {
|
||||
let expectedErr;
|
||||
|
||||
try {
|
||||
validateGiftMessage(gift, user);
|
||||
} catch (err) {
|
||||
expectedErr = err;
|
||||
}
|
||||
|
||||
expect(expectedErr).to.exist;
|
||||
expect(expectedErr).to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: i18n.t('giftMessageTooLong', { maxGiftMessageLength: 200 }),
|
||||
});
|
||||
});
|
||||
|
||||
it('does not throw if the gift message is not too long', () => {
|
||||
gift.message = gift.message.substring(0, 200);
|
||||
expect(() => validateGiftMessage(gift, user)).to.not.throw;
|
||||
});
|
||||
|
||||
it('does not throw if it is not a gift', () => {
|
||||
expect(() => validateGiftMessage(null, user)).to.not.throw;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import googlePayments from '../../../../../website/server/libs/payments/google';
|
||||
import iap from '../../../../../website/server/libs/inAppPurchases';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import common from '../../../../../website/common';
|
||||
import * as gems from '../../../../../website/server/libs/payments/gems';
|
||||
|
||||
const { i18n } = common;
|
||||
|
||||
@@ -15,7 +16,7 @@ describe('Google Payments', () => {
|
||||
let sku; let user; let token; let receipt; let signature; let
|
||||
headers; const gemsBlock = common.content.gems['21gems'];
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let
|
||||
paymentBuyGemsStub;
|
||||
paymentBuyGemsStub; let validateGiftMessageStub;
|
||||
|
||||
beforeEach(() => {
|
||||
sku = 'com.habitrpg.android.habitica.iap.21gems';
|
||||
@@ -31,6 +32,7 @@ describe('Google Payments', () => {
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||
.returns(true);
|
||||
paymentBuyGemsStub = sinon.stub(payments, 'buyGems').resolves({});
|
||||
validateGiftMessageStub = sinon.stub(gems, 'validateGiftMessage');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -38,6 +40,7 @@ describe('Google Payments', () => {
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
payments.buyGems.restore();
|
||||
gems.validateGiftMessage.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if receipt is invalid', async () => {
|
||||
@@ -89,6 +92,8 @@ describe('Google Payments', () => {
|
||||
user, receipt, signature, headers,
|
||||
});
|
||||
|
||||
expect(validateGiftMessageStub).to.not.be.called;
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledWith(iap.GOOGLE, {
|
||||
@@ -119,6 +124,9 @@ describe('Google Payments', () => {
|
||||
user, gift, receipt, signature, headers,
|
||||
});
|
||||
|
||||
expect(validateGiftMessageStub).to.be.calledOnce;
|
||||
expect(validateGiftMessageStub).to.be.calledWith(gift, user);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledWith(iap.GOOGLE, {
|
||||
|
||||
@@ -22,7 +22,9 @@ describe('Purchasing a group plan for group', () => {
|
||||
|
||||
let plan; let group; let user; let
|
||||
data;
|
||||
const stripe = stripeModule('test');
|
||||
const stripe = stripeModule('test', {
|
||||
apiVersion: '2020-08-27',
|
||||
});
|
||||
const groupLeaderName = 'sender';
|
||||
const groupName = 'test group';
|
||||
|
||||
|
||||
@@ -32,8 +32,8 @@ describe('payments/index', () => {
|
||||
|
||||
sandbox.stub(sender, 'sendTxn');
|
||||
sandbox.stub(user, 'sendMessage');
|
||||
sandbox.stub(analytics, 'trackPurchase');
|
||||
sandbox.stub(analytics, 'track');
|
||||
sandbox.stub(analytics.mockAnalyticsService, 'trackPurchase');
|
||||
sandbox.stub(analytics.mockAnalyticsService, 'track');
|
||||
sandbox.stub(notifications, 'sendNotification');
|
||||
|
||||
data = {
|
||||
@@ -209,17 +209,6 @@ describe('payments/index', () => {
|
||||
expect(user.purchased.txnCount).to.eql(1);
|
||||
});
|
||||
|
||||
it('sends a private message about the gift', async () => {
|
||||
await api.createSubscription(data);
|
||||
const msg = '`Hello recipient, sender has sent you 3 months of subscription!`';
|
||||
|
||||
expect(user.sendMessage).to.be.calledOnce;
|
||||
expect(user.sendMessage).to.be.calledWith(
|
||||
recipient,
|
||||
{ receiverMsg: msg, senderMsg: msg, save: false },
|
||||
);
|
||||
});
|
||||
|
||||
it('sends an email about the gift', async () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
@@ -237,8 +226,8 @@ describe('payments/index', () => {
|
||||
it('tracks subscription purchase as gift', async () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(analytics.trackPurchase).to.be.calledOnce;
|
||||
expect(analytics.trackPurchase).to.be.calledWith({
|
||||
expect(analytics.mockAnalyticsService.trackPurchase).to.be.calledOnce;
|
||||
expect(analytics.mockAnalyticsService.trackPurchase).to.be.calledWith({
|
||||
uuid: user._id,
|
||||
groupId: undefined,
|
||||
itemPurchased: 'Subscription',
|
||||
@@ -248,12 +237,116 @@ describe('payments/index', () => {
|
||||
quantity: 1,
|
||||
gift: true,
|
||||
purchaseValue: 15,
|
||||
firstPurchase: true,
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
context('No Active Promotion', () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub(worldState, 'getCurrentEvent').returns(null);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
worldState.getCurrentEvent.restore();
|
||||
});
|
||||
|
||||
it('sends a private message about the gift', async () => {
|
||||
await api.createSubscription(data);
|
||||
const msg = '`Hello recipient, sender has sent you 3 months of subscription!`';
|
||||
|
||||
expect(user.sendMessage).to.be.calledOnce;
|
||||
expect(user.sendMessage).to.be.calledWith(
|
||||
recipient,
|
||||
{ receiverMsg: msg, senderMsg: msg, save: false },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('Active Promotion', () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub(worldState, 'getCurrentEvent').returns({
|
||||
...common.content.events.winter2021,
|
||||
event: 'winter2021',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
worldState.getCurrentEvent.restore();
|
||||
});
|
||||
|
||||
it('creates a gift subscription for purchaser and recipient if none exist', async () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.items.pets['Jackalope-RoyalPurple']).to.eql(5);
|
||||
expect(user.purchased.plan.customerId).to.eql('Gift');
|
||||
expect(user.purchased.plan.dateTerminated).to.exist;
|
||||
expect(user.purchased.plan.dateUpdated).to.exist;
|
||||
expect(user.purchased.plan.dateCreated).to.exist;
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.extraMonths).to.eql(3);
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
it('adds extraMonths to existing subscription for recipient and creates a gift subscription for purchaser without sub', 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);
|
||||
|
||||
expect(user.items.pets['Jackalope-RoyalPurple']).to.eql(5);
|
||||
expect(user.purchased.plan.customerId).to.eql('Gift');
|
||||
expect(user.purchased.plan.dateTerminated).to.exist;
|
||||
expect(user.purchased.plan.dateUpdated).to.exist;
|
||||
expect(user.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('adds extraMonths to existing subscriptions for purchaser and recipient', async () => {
|
||||
user.purchased.plan = plan;
|
||||
recipient.purchased.plan = plan;
|
||||
|
||||
expect(user.purchased.plan.extraMonths).to.eql(0);
|
||||
expect(recipient.purchased.plan.extraMonths).to.eql(0);
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.extraMonths).to.eql(3);
|
||||
expect(recipient.purchased.plan.extraMonths).to.eql(3);
|
||||
});
|
||||
|
||||
it('sends a private message about the promotion', async () => {
|
||||
await api.createSubscription(data);
|
||||
const msg = '`Hello sender, you received 3 months of subscription as part of our holiday gift-giving promotion!`';
|
||||
|
||||
expect(user.sendMessage).to.be.calledTwice;
|
||||
expect(user.sendMessage).to.be.calledWith(user, { receiverMsg: msg, save: false });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Purchasing a subscription for self', () => {
|
||||
@@ -334,8 +427,8 @@ describe('payments/index', () => {
|
||||
it('tracks subscription purchase', async () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(analytics.trackPurchase).to.be.calledOnce;
|
||||
expect(analytics.trackPurchase).to.be.calledWith({
|
||||
expect(analytics.mockAnalyticsService.trackPurchase).to.be.calledOnce;
|
||||
expect(analytics.mockAnalyticsService.trackPurchase).to.be.calledWith({
|
||||
uuid: user._id,
|
||||
groupId: undefined,
|
||||
itemPurchased: 'Subscription',
|
||||
@@ -345,6 +438,7 @@ describe('payments/index', () => {
|
||||
quantity: 1,
|
||||
gift: false,
|
||||
purchaseValue: 15,
|
||||
firstPurchase: true,
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
@@ -421,10 +515,22 @@ describe('payments/index', () => {
|
||||
});
|
||||
|
||||
context('Mystery Items', () => {
|
||||
it('awards mystery items when within the timeframe for a mystery item', async () => {
|
||||
const mayMysteryItemTimeframe = 1464725113000; // May 31st 2016
|
||||
const fakeClock = sinon.useFakeTimers(mayMysteryItemTimeframe);
|
||||
let clock;
|
||||
const mayMysteryItem = 'armor_mystery_201605';
|
||||
|
||||
beforeEach(() => {
|
||||
const mayMysteryItemTimeframe = new Date(2016, 4, 31); // May 31st 2016
|
||||
clock = sinon.useFakeTimers({
|
||||
now: mayMysteryItemTimeframe,
|
||||
toFake: ['Date'],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (clock) clock.restore();
|
||||
});
|
||||
|
||||
it('awards mystery items when within the timeframe for a mystery item', async () => {
|
||||
data = { paymentMethod: 'PaymentMethod', user, sub: { key: 'basic_3mo' } };
|
||||
|
||||
const oldNotificationsCount = user.notifications.length;
|
||||
@@ -437,14 +543,9 @@ describe('payments/index', () => {
|
||||
expect(user.purchased.plan.mysteryItems).to.include('head_mystery_201605');
|
||||
expect(user.notifications.length).to.equal(oldNotificationsCount + 1);
|
||||
expect(user.notifications[0].type).to.equal('NEW_MYSTERY_ITEMS');
|
||||
|
||||
fakeClock.restore();
|
||||
});
|
||||
|
||||
it('does not award mystery item when user already owns the item', async () => {
|
||||
const mayMysteryItemTimeframe = 1464725113000; // May 31st 2016
|
||||
const fakeClock = sinon.useFakeTimers(mayMysteryItemTimeframe);
|
||||
const mayMysteryItem = 'armor_mystery_201605';
|
||||
user.items.gear.owned[mayMysteryItem] = true;
|
||||
|
||||
data = { paymentMethod: 'PaymentMethod', user, sub: { key: 'basic_3mo' } };
|
||||
@@ -453,14 +554,9 @@ describe('payments/index', () => {
|
||||
|
||||
expect(user.purchased.plan.mysteryItems).to.have.a.lengthOf(1);
|
||||
expect(user.purchased.plan.mysteryItems).to.include('head_mystery_201605');
|
||||
|
||||
fakeClock.restore();
|
||||
});
|
||||
|
||||
it('does not award mystery item when user already has the item in the mystery box', async () => {
|
||||
const mayMysteryItemTimeframe = 1464725113000; // May 31st 2016
|
||||
const fakeClock = sinon.useFakeTimers(mayMysteryItemTimeframe);
|
||||
const mayMysteryItem = 'armor_mystery_201605';
|
||||
user.purchased.plan.mysteryItems = [mayMysteryItem];
|
||||
|
||||
sandbox.spy(user.purchased.plan.mysteryItems, 'push');
|
||||
@@ -470,8 +566,6 @@ describe('payments/index', () => {
|
||||
|
||||
expect(user.purchased.plan.mysteryItems.push).to.be.calledOnce;
|
||||
expect(user.purchased.plan.mysteryItems.push).to.be.calledWith('head_mystery_201605');
|
||||
|
||||
fakeClock.restore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import paypalPayments from '../../../../../../website/server/libs/payments/paypa
|
||||
import { model as User } from '../../../../../../website/server/models/user';
|
||||
import common from '../../../../../../website/common';
|
||||
import apiError from '../../../../../../website/server/libs/apiError';
|
||||
import * as gems from '../../../../../../website/server/libs/payments/gems';
|
||||
|
||||
const BASE_URL = nconf.get('BASE_URL');
|
||||
const { i18n } = common;
|
||||
@@ -48,6 +49,7 @@ describe('paypal - checkout', () => {
|
||||
.resolves({
|
||||
links: [{ rel: 'approval_url', href: approvalHerf }],
|
||||
});
|
||||
sandbox.stub(gems, 'validateGiftMessage');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -57,6 +59,7 @@ describe('paypal - checkout', () => {
|
||||
it('creates a link for gem purchases', async () => {
|
||||
const link = await paypalPayments.checkout({ user: new User(), gemsBlock: gemsBlockKey });
|
||||
|
||||
expect(gems.validateGiftMessage).to.not.be.called;
|
||||
expect(paypalPaymentCreateStub).to.be.calledOnce;
|
||||
expect(paypalPaymentCreateStub).to.be.calledWith(getPaypalCreateOptions('Habitica Gems', 4.99));
|
||||
expect(link).to.eql(approvalHerf);
|
||||
@@ -105,6 +108,7 @@ describe('paypal - checkout', () => {
|
||||
});
|
||||
|
||||
it('creates a link for gifting gems', async () => {
|
||||
const user = new User();
|
||||
const receivingUser = new User();
|
||||
await receivingUser.save();
|
||||
const gift = {
|
||||
@@ -115,14 +119,17 @@ describe('paypal - checkout', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const link = await paypalPayments.checkout({ gift });
|
||||
const link = await paypalPayments.checkout({ user, gift });
|
||||
|
||||
expect(gems.validateGiftMessage).to.be.calledOnce;
|
||||
expect(gems.validateGiftMessage).to.be.calledWith(gift, user);
|
||||
expect(paypalPaymentCreateStub).to.be.calledOnce;
|
||||
expect(paypalPaymentCreateStub).to.be.calledWith(getPaypalCreateOptions('Habitica Gems (Gift)', '4.00'));
|
||||
expect(link).to.eql(approvalHerf);
|
||||
});
|
||||
|
||||
it('creates a link for gifting a subscription', async () => {
|
||||
const user = new User();
|
||||
const receivingUser = new User();
|
||||
receivingUser.save();
|
||||
const gift = {
|
||||
@@ -133,7 +140,10 @@ describe('paypal - checkout', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const link = await paypalPayments.checkout({ gift });
|
||||
const link = await paypalPayments.checkout({ user, gift });
|
||||
|
||||
expect(gems.validateGiftMessage).to.be.calledOnce;
|
||||
expect(gems.validateGiftMessage).to.be.calledWith(gift, user);
|
||||
|
||||
expect(paypalPaymentCreateStub).to.be.calledOnce;
|
||||
expect(paypalPaymentCreateStub).to.be.calledWith(getPaypalCreateOptions('mo. Habitica Subscription (Gift)', '15.00'));
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
import stripeModule from 'stripe';
|
||||
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../helpers/api-unit.helper';
|
||||
import { model as User } from '../../../../../../website/server/models/user';
|
||||
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
|
||||
import payments from '../../../../../../website/server/libs/payments/payments';
|
||||
import common from '../../../../../../website/common';
|
||||
|
||||
const { i18n } = common;
|
||||
|
||||
describe('stripe - cancel subscription', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
const stripe = stripeModule('test');
|
||||
let user; let groupId; let
|
||||
group;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.customerId = 'customer-id';
|
||||
user.purchased.plan.planId = subKey;
|
||||
user.purchased.plan.lastBillingDate = new Date();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = 'customer-id';
|
||||
group.purchased.plan.planId = subKey;
|
||||
await group.save();
|
||||
|
||||
groupId = group._id;
|
||||
});
|
||||
|
||||
it('throws an error if there is no customer id', async () => {
|
||||
user.purchased.plan.customerId = undefined;
|
||||
|
||||
await expect(stripePayments.cancelSubscription({
|
||||
user,
|
||||
groupId: undefined,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('missingSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if the group is not found', async () => {
|
||||
await expect(stripePayments.cancelSubscription({
|
||||
user,
|
||||
groupId: 'fake-group',
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
message: i18n.t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if user is not the group leader', async () => {
|
||||
const nonLeader = new User();
|
||||
nonLeader.guilds.push(groupId);
|
||||
await nonLeader.save();
|
||||
|
||||
await expect(stripePayments.cancelSubscription({
|
||||
user: nonLeader,
|
||||
groupId,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
describe('success', () => {
|
||||
let stripeDeleteCustomerStub; let paymentsCancelSubStub;
|
||||
let stripeRetrieveStub; let subscriptionId; let
|
||||
currentPeriodEndTimeStamp;
|
||||
|
||||
beforeEach(() => {
|
||||
subscriptionId = 'subId';
|
||||
stripeDeleteCustomerStub = sinon.stub(stripe.customers, 'del').resolves({});
|
||||
paymentsCancelSubStub = sinon.stub(payments, 'cancelSubscription').resolves({});
|
||||
|
||||
currentPeriodEndTimeStamp = (new Date()).getTime();
|
||||
stripeRetrieveStub = sinon.stub(stripe.customers, 'retrieve')
|
||||
.resolves({
|
||||
subscriptions: {
|
||||
data: [{
|
||||
id: subscriptionId,
|
||||
current_period_end: currentPeriodEndTimeStamp,
|
||||
}], // eslint-disable-line camelcase
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
stripe.customers.del.restore();
|
||||
stripe.customers.retrieve.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
|
||||
it('cancels a user subscription', async () => {
|
||||
await stripePayments.cancelSubscription({
|
||||
user,
|
||||
groupId: undefined,
|
||||
}, stripe);
|
||||
|
||||
expect(stripeDeleteCustomerStub).to.be.calledOnce;
|
||||
expect(stripeDeleteCustomerStub).to.be.calledWith(user.purchased.plan.customerId);
|
||||
expect(stripeRetrieveStub).to.be.calledOnce;
|
||||
expect(stripeRetrieveStub).to.be.calledWith(user.purchased.plan.customerId);
|
||||
expect(paymentsCancelSubStub).to.be.calledOnce;
|
||||
expect(paymentsCancelSubStub).to.be.calledWith({
|
||||
user,
|
||||
groupId: undefined,
|
||||
nextBill: currentPeriodEndTimeStamp * 1000, // timestamp in seconds
|
||||
paymentMethod: 'Stripe',
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('cancels a group subscription', async () => {
|
||||
await stripePayments.cancelSubscription({
|
||||
user,
|
||||
groupId,
|
||||
}, stripe);
|
||||
|
||||
expect(stripeDeleteCustomerStub).to.be.calledOnce;
|
||||
expect(stripeDeleteCustomerStub).to.be.calledWith(group.purchased.plan.customerId);
|
||||
expect(stripeRetrieveStub).to.be.calledOnce;
|
||||
expect(stripeRetrieveStub).to.be.calledWith(user.purchased.plan.customerId);
|
||||
expect(paymentsCancelSubStub).to.be.calledOnce;
|
||||
expect(paymentsCancelSubStub).to.be.calledWith({
|
||||
user,
|
||||
groupId,
|
||||
nextBill: currentPeriodEndTimeStamp * 1000, // timestamp in seconds
|
||||
paymentMethod: 'Stripe',
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,321 +0,0 @@
|
||||
import stripeModule from 'stripe';
|
||||
import cc from 'coupon-code';
|
||||
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../helpers/api-unit.helper';
|
||||
import { model as User } from '../../../../../../website/server/models/user';
|
||||
import { model as Coupon } from '../../../../../../website/server/models/coupon';
|
||||
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
|
||||
import payments from '../../../../../../website/server/libs/payments/payments';
|
||||
import common from '../../../../../../website/common';
|
||||
|
||||
const { i18n } = common;
|
||||
|
||||
describe('stripe - checkout with subscription', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
const stripe = stripeModule('test');
|
||||
let user; let group; let data; let gift; let sub;
|
||||
let groupId; let email; let headers; let coupon;
|
||||
let customerIdResponse; let subscriptionId; let
|
||||
token;
|
||||
let spy;
|
||||
let stripeCreateCustomerSpy;
|
||||
let stripePaymentsCreateSubSpy;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.customerId = 'customer-id';
|
||||
user.purchased.plan.planId = subKey;
|
||||
user.purchased.plan.lastBillingDate = new Date();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = 'customer-id';
|
||||
group.purchased.plan.planId = subKey;
|
||||
await group.save();
|
||||
|
||||
sub = {
|
||||
key: 'basic_3mo',
|
||||
};
|
||||
|
||||
data = {
|
||||
user,
|
||||
sub,
|
||||
customerId: 'customer-id',
|
||||
paymentMethod: 'Payment Method',
|
||||
};
|
||||
|
||||
email = 'example@example.com';
|
||||
customerIdResponse = 'test-id';
|
||||
subscriptionId = 'test-sub-id';
|
||||
token = 'test-token';
|
||||
|
||||
spy = sinon.stub(stripe.subscriptions, 'update');
|
||||
spy.resolves;
|
||||
|
||||
stripeCreateCustomerSpy = sinon.stub(stripe.customers, 'create');
|
||||
const stripCustomerResponse = {
|
||||
id: customerIdResponse,
|
||||
subscriptions: {
|
||||
data: [{ id: subscriptionId }],
|
||||
},
|
||||
};
|
||||
stripeCreateCustomerSpy.resolves(stripCustomerResponse);
|
||||
|
||||
stripePaymentsCreateSubSpy = sinon.stub(payments, 'createSubscription');
|
||||
stripePaymentsCreateSubSpy.resolves({});
|
||||
|
||||
data.groupId = group._id;
|
||||
data.sub.quantity = 3;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
stripe.subscriptions.update.restore();
|
||||
stripe.customers.create.restore();
|
||||
payments.createSubscription.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if we are missing a token', async () => {
|
||||
await expect(stripePayments.checkout({
|
||||
user,
|
||||
gift,
|
||||
sub,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: 'Missing req.body.id',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error when coupon code is missing', async () => {
|
||||
sub.discount = 40;
|
||||
|
||||
await expect(stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
sub,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: i18n.t('couponCodeRequired'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error when coupon code is invalid', async () => {
|
||||
sub.discount = 40;
|
||||
sub.key = 'google_6mo';
|
||||
coupon = 'example-coupon';
|
||||
|
||||
const couponModel = new Coupon();
|
||||
couponModel.event = 'google_6mo';
|
||||
await couponModel.save();
|
||||
|
||||
sinon.stub(cc, 'validate').returns('invalid');
|
||||
|
||||
await expect(stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
sub,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: i18n.t('invalidCoupon'),
|
||||
});
|
||||
cc.validate.restore();
|
||||
});
|
||||
|
||||
it('subscribes with stripe with a coupon', async () => {
|
||||
sub.discount = 40;
|
||||
sub.key = 'google_6mo';
|
||||
coupon = 'example-coupon';
|
||||
|
||||
const couponModel = new Coupon();
|
||||
couponModel.event = 'google_6mo';
|
||||
const updatedCouponModel = await couponModel.save();
|
||||
|
||||
sinon.stub(cc, 'validate').returns(updatedCouponModel._id);
|
||||
|
||||
await stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
sub,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe);
|
||||
|
||||
expect(stripeCreateCustomerSpy).to.be.calledOnce;
|
||||
expect(stripeCreateCustomerSpy).to.be.calledWith({
|
||||
email,
|
||||
metadata: { uuid: user._id },
|
||||
card: token,
|
||||
plan: sub.key,
|
||||
});
|
||||
|
||||
expect(stripePaymentsCreateSubSpy).to.be.calledOnce;
|
||||
expect(stripePaymentsCreateSubSpy).to.be.calledWith({
|
||||
user,
|
||||
customerId: customerIdResponse,
|
||||
paymentMethod: 'Stripe',
|
||||
sub,
|
||||
headers,
|
||||
groupId: undefined,
|
||||
subscriptionId: undefined,
|
||||
});
|
||||
|
||||
cc.validate.restore();
|
||||
});
|
||||
|
||||
it('subscribes a user', async () => {
|
||||
sub = data.sub;
|
||||
|
||||
await stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
sub,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe);
|
||||
|
||||
expect(stripeCreateCustomerSpy).to.be.calledOnce;
|
||||
expect(stripeCreateCustomerSpy).to.be.calledWith({
|
||||
email,
|
||||
metadata: { uuid: user._id },
|
||||
card: token,
|
||||
plan: sub.key,
|
||||
});
|
||||
|
||||
expect(stripePaymentsCreateSubSpy).to.be.calledOnce;
|
||||
expect(stripePaymentsCreateSubSpy).to.be.calledWith({
|
||||
user,
|
||||
customerId: customerIdResponse,
|
||||
paymentMethod: 'Stripe',
|
||||
sub,
|
||||
headers,
|
||||
groupId: undefined,
|
||||
subscriptionId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('subscribes a group', async () => {
|
||||
token = 'test-token';
|
||||
sub = data.sub;
|
||||
groupId = group._id;
|
||||
email = 'test@test.com';
|
||||
|
||||
// Add user to group
|
||||
user.guilds.push(groupId);
|
||||
await user.save();
|
||||
|
||||
headers = {};
|
||||
|
||||
await stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
sub,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe);
|
||||
|
||||
expect(stripeCreateCustomerSpy).to.be.calledOnce;
|
||||
expect(stripeCreateCustomerSpy).to.be.calledWith({
|
||||
email,
|
||||
metadata: { uuid: user._id },
|
||||
card: token,
|
||||
plan: sub.key,
|
||||
quantity: 3,
|
||||
});
|
||||
|
||||
expect(stripePaymentsCreateSubSpy).to.be.calledOnce;
|
||||
expect(stripePaymentsCreateSubSpy).to.be.calledWith({
|
||||
user,
|
||||
customerId: customerIdResponse,
|
||||
paymentMethod: 'Stripe',
|
||||
sub,
|
||||
headers,
|
||||
groupId,
|
||||
subscriptionId,
|
||||
});
|
||||
});
|
||||
|
||||
it('subscribes a group with the correct number of group members', async () => {
|
||||
token = 'test-token';
|
||||
sub = data.sub;
|
||||
groupId = group._id;
|
||||
email = 'test@test.com';
|
||||
headers = {};
|
||||
|
||||
// Add user to group
|
||||
user.guilds.push(groupId);
|
||||
await user.save();
|
||||
|
||||
user = new User();
|
||||
user.guilds.push(groupId);
|
||||
await user.save();
|
||||
|
||||
group.memberCount = 2;
|
||||
await group.save();
|
||||
|
||||
await stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
sub,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe);
|
||||
|
||||
expect(stripeCreateCustomerSpy).to.be.calledOnce;
|
||||
expect(stripeCreateCustomerSpy).to.be.calledWith({
|
||||
email,
|
||||
metadata: { uuid: user._id },
|
||||
card: token,
|
||||
plan: sub.key,
|
||||
quantity: 4,
|
||||
});
|
||||
|
||||
expect(stripePaymentsCreateSubSpy).to.be.calledOnce;
|
||||
expect(stripePaymentsCreateSubSpy).to.be.calledWith({
|
||||
user,
|
||||
customerId: customerIdResponse,
|
||||
paymentMethod: 'Stripe',
|
||||
sub,
|
||||
headers,
|
||||
groupId,
|
||||
subscriptionId,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,235 +1,484 @@
|
||||
import stripeModule from 'stripe';
|
||||
|
||||
import { model as User } from '../../../../../../website/server/models/user';
|
||||
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
|
||||
import payments from '../../../../../../website/server/libs/payments/payments';
|
||||
import nconf from 'nconf';
|
||||
import common from '../../../../../../website/common';
|
||||
import apiError from '../../../../../../website/server/libs/apiError';
|
||||
import * as subscriptions from '../../../../../../website/server/libs/payments/stripe/subscriptions';
|
||||
import * as oneTimePayments from '../../../../../../website/server/libs/payments/stripe/oneTimePayments';
|
||||
import {
|
||||
createCheckoutSession,
|
||||
createEditCardCheckoutSession,
|
||||
} from '../../../../../../website/server/libs/payments/stripe/checkout';
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../helpers/api-unit.helper';
|
||||
import { model as User } from '../../../../../../website/server/models/user';
|
||||
import { model as Group } from '../../../../../../website/server/models/group';
|
||||
import * as gems from '../../../../../../website/server/libs/payments/gems';
|
||||
|
||||
const { i18n } = common;
|
||||
|
||||
describe('stripe - checkout', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
const stripe = stripeModule('test');
|
||||
let stripeChargeStub; let paymentBuyGemsStub; let
|
||||
paymentCreateSubscritionStub;
|
||||
let user; let gift; let groupId; let email; let headers; let coupon; let customerIdResponse; let
|
||||
token; const gemsBlockKey = '21gems'; const gemsBlock = common.content.gems[gemsBlockKey];
|
||||
|
||||
beforeEach(() => {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.customerId = 'customer-id';
|
||||
user.purchased.plan.planId = subKey;
|
||||
user.purchased.plan.lastBillingDate = new Date();
|
||||
|
||||
token = 'test-token';
|
||||
|
||||
customerIdResponse = 'example-customerIdResponse';
|
||||
const stripCustomerResponse = {
|
||||
id: customerIdResponse,
|
||||
};
|
||||
stripeChargeStub = sinon.stub(stripe.charges, 'create').resolves(stripCustomerResponse);
|
||||
paymentBuyGemsStub = sinon.stub(payments, 'buyGems').resolves({});
|
||||
paymentCreateSubscritionStub = sinon.stub(payments, 'createSubscription').resolves({});
|
||||
describe('Stripe - Checkout', () => {
|
||||
const stripe = stripeModule('test', {
|
||||
apiVersion: '2020-08-27',
|
||||
});
|
||||
const BASE_URL = nconf.get('BASE_URL');
|
||||
const redirectUrls = {
|
||||
success_url: `${BASE_URL}/redirect/stripe-success-checkout`,
|
||||
cancel_url: `${BASE_URL}/redirect/stripe-error-checkout`,
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
stripe.charges.create.restore();
|
||||
payments.buyGems.restore();
|
||||
payments.createSubscription.restore();
|
||||
});
|
||||
describe('createCheckoutSession', () => {
|
||||
let user;
|
||||
const sessionId = 'session-id';
|
||||
|
||||
it('should error if there is no token', async () => {
|
||||
await expect(stripePayments.checkout({
|
||||
user,
|
||||
gift,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
message: 'Missing req.body.id',
|
||||
name: 'BadRequest',
|
||||
beforeEach(() => {
|
||||
user = new User();
|
||||
sandbox.stub(stripe.checkout.sessions, 'create').returns(sessionId);
|
||||
sandbox.stub(gems, 'validateGiftMessage');
|
||||
});
|
||||
|
||||
it('gems', async () => {
|
||||
const amount = 999;
|
||||
const gemsBlockKey = '21gems';
|
||||
sandbox.stub(oneTimePayments, 'getOneTimePaymentInfo').returns({
|
||||
amount,
|
||||
gemsBlock: common.content.gems[gemsBlockKey],
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if gem amount is too low', async () => {
|
||||
const receivingUser = new User();
|
||||
receivingUser.save();
|
||||
gift = {
|
||||
type: 'gems',
|
||||
gems: {
|
||||
amount: 0,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
const res = await createCheckoutSession({ user, gemsBlock: gemsBlockKey }, stripe);
|
||||
expect(res).to.equal(sessionId);
|
||||
|
||||
await expect(stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
message: 'Amount must be at least 1.',
|
||||
name: 'BadRequest',
|
||||
const metadata = {
|
||||
type: 'gems',
|
||||
userId: user._id,
|
||||
gift: undefined,
|
||||
sub: undefined,
|
||||
gemsBlock: gemsBlockKey,
|
||||
};
|
||||
|
||||
expect(gems.validateGiftMessage).to.not.be.called;
|
||||
expect(oneTimePayments.getOneTimePaymentInfo).to.be.calledOnce;
|
||||
expect(oneTimePayments.getOneTimePaymentInfo).to.be.calledWith(gemsBlockKey, undefined, user);
|
||||
expect(stripe.checkout.sessions.create).to.be.calledOnce;
|
||||
expect(stripe.checkout.sessions.create).to.be.calledWith({
|
||||
payment_method_types: ['card'],
|
||||
metadata,
|
||||
line_items: [{
|
||||
price_data: {
|
||||
product_data: {
|
||||
name: common.i18n.t('nGems', { nGems: 21 }),
|
||||
},
|
||||
unit_amount: amount,
|
||||
currency: 'usd',
|
||||
},
|
||||
quantity: 1,
|
||||
}],
|
||||
mode: 'payment',
|
||||
...redirectUrls,
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if user cannot get gems', async () => {
|
||||
gift = undefined;
|
||||
sinon.stub(user, 'canGetGems').resolves(false);
|
||||
|
||||
await expect(stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gemsBlock: gemsBlockKey,
|
||||
gift,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe)).to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
message: i18n.t('groupPolicyCannotGetGems'),
|
||||
name: 'NotAuthorized',
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if the gems block is invalid', async () => {
|
||||
gift = undefined;
|
||||
|
||||
await expect(stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gemsBlock: 'invalid',
|
||||
gift,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe)).to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
message: apiError('invalidGemsBlock'),
|
||||
name: 'BadRequest',
|
||||
});
|
||||
});
|
||||
|
||||
it('should purchase gems', async () => {
|
||||
gift = undefined;
|
||||
sinon.stub(user, 'canGetGems').resolves(true);
|
||||
|
||||
await stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gemsBlock: gemsBlockKey,
|
||||
gift,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe);
|
||||
|
||||
expect(stripeChargeStub).to.be.calledOnce;
|
||||
expect(stripeChargeStub).to.be.calledWith({
|
||||
amount: 499,
|
||||
currency: 'usd',
|
||||
card: token,
|
||||
});
|
||||
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
customerId: customerIdResponse,
|
||||
paymentMethod: 'Stripe',
|
||||
gift,
|
||||
gemsBlock,
|
||||
});
|
||||
expect(user.canGetGems).to.be.calledOnce;
|
||||
user.canGetGems.restore();
|
||||
});
|
||||
it('gems gift', async () => {
|
||||
const receivingUser = new User();
|
||||
await receivingUser.save();
|
||||
|
||||
it('should gift gems', async () => {
|
||||
const receivingUser = new User();
|
||||
await receivingUser.save();
|
||||
gift = {
|
||||
type: 'gems',
|
||||
uuid: receivingUser._id,
|
||||
gems: {
|
||||
amount: 16,
|
||||
},
|
||||
};
|
||||
|
||||
await stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe);
|
||||
|
||||
expect(stripeChargeStub).to.be.calledOnce;
|
||||
expect(stripeChargeStub).to.be.calledWith({
|
||||
amount: '400',
|
||||
currency: 'usd',
|
||||
card: token,
|
||||
});
|
||||
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
customerId: customerIdResponse,
|
||||
paymentMethod: 'Gift',
|
||||
gift,
|
||||
gemsBlock: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should gift a subscription', async () => {
|
||||
const receivingUser = new User();
|
||||
receivingUser.save();
|
||||
gift = {
|
||||
type: 'subscription',
|
||||
subscription: {
|
||||
key: subKey,
|
||||
const gift = {
|
||||
type: 'gems',
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
gems: {
|
||||
amount: 4,
|
||||
},
|
||||
};
|
||||
const amount = 100;
|
||||
sandbox.stub(oneTimePayments, 'getOneTimePaymentInfo').returns({
|
||||
amount,
|
||||
gemsBlock: null,
|
||||
});
|
||||
|
||||
await stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe);
|
||||
const res = await createCheckoutSession({ user, gift }, stripe);
|
||||
expect(res).to.equal(sessionId);
|
||||
|
||||
gift.member = receivingUser;
|
||||
expect(stripeChargeStub).to.be.calledOnce;
|
||||
expect(stripeChargeStub).to.be.calledWith({
|
||||
amount: '1500',
|
||||
currency: 'usd',
|
||||
card: token,
|
||||
const metadata = {
|
||||
type: 'gift-gems',
|
||||
userId: user._id,
|
||||
gift: JSON.stringify(gift),
|
||||
sub: undefined,
|
||||
gemsBlock: undefined,
|
||||
};
|
||||
|
||||
expect(gems.validateGiftMessage).to.be.calledOnce;
|
||||
expect(gems.validateGiftMessage).to.be.calledWith(gift, user);
|
||||
|
||||
expect(oneTimePayments.getOneTimePaymentInfo).to.be.calledOnce;
|
||||
expect(oneTimePayments.getOneTimePaymentInfo).to.be.calledWith(undefined, gift, user);
|
||||
expect(stripe.checkout.sessions.create).to.be.calledOnce;
|
||||
expect(stripe.checkout.sessions.create).to.be.calledWith({
|
||||
payment_method_types: ['card'],
|
||||
metadata,
|
||||
line_items: [{
|
||||
price_data: {
|
||||
product_data: {
|
||||
name: common.i18n.t('nGemsGift', { nGems: 4 }),
|
||||
},
|
||||
unit_amount: amount,
|
||||
currency: 'usd',
|
||||
},
|
||||
quantity: 1,
|
||||
}],
|
||||
mode: 'payment',
|
||||
...redirectUrls,
|
||||
});
|
||||
});
|
||||
|
||||
expect(paymentCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
customerId: customerIdResponse,
|
||||
paymentMethod: 'Gift',
|
||||
gift,
|
||||
gemsBlock: undefined,
|
||||
it('subscription gift', async () => {
|
||||
const receivingUser = new User();
|
||||
await receivingUser.save();
|
||||
const subKey = 'basic_3mo';
|
||||
|
||||
const gift = {
|
||||
type: 'subscription',
|
||||
uuid: receivingUser._id,
|
||||
subscription: {
|
||||
key: subKey,
|
||||
},
|
||||
};
|
||||
const amount = 1500;
|
||||
sandbox.stub(oneTimePayments, 'getOneTimePaymentInfo').returns({
|
||||
amount,
|
||||
gemsBlock: null,
|
||||
subscription: common.content.subscriptionBlocks[subKey],
|
||||
});
|
||||
|
||||
const res = await createCheckoutSession({ user, gift }, stripe);
|
||||
expect(res).to.equal(sessionId);
|
||||
|
||||
const metadata = {
|
||||
type: 'gift-sub',
|
||||
userId: user._id,
|
||||
gift: JSON.stringify(gift),
|
||||
sub: undefined,
|
||||
gemsBlock: undefined,
|
||||
};
|
||||
|
||||
expect(oneTimePayments.getOneTimePaymentInfo).to.be.calledOnce;
|
||||
expect(oneTimePayments.getOneTimePaymentInfo).to.be.calledWith(undefined, gift, user);
|
||||
expect(stripe.checkout.sessions.create).to.be.calledOnce;
|
||||
expect(stripe.checkout.sessions.create).to.be.calledWith({
|
||||
payment_method_types: ['card'],
|
||||
metadata,
|
||||
line_items: [{
|
||||
price_data: {
|
||||
product_data: {
|
||||
name: common.i18n.t('nMonthsSubscriptionGift', { nMonths: 3 }),
|
||||
},
|
||||
unit_amount: amount,
|
||||
currency: 'usd',
|
||||
},
|
||||
quantity: 1,
|
||||
}],
|
||||
mode: 'payment',
|
||||
...redirectUrls,
|
||||
});
|
||||
});
|
||||
|
||||
it('subscription', async () => {
|
||||
const subKey = 'basic_3mo';
|
||||
const coupon = null;
|
||||
sandbox.stub(subscriptions, 'checkSubData').returns(undefined);
|
||||
const sub = common.content.subscriptionBlocks[subKey];
|
||||
|
||||
const res = await createCheckoutSession({ user, sub, coupon }, stripe);
|
||||
expect(res).to.equal(sessionId);
|
||||
|
||||
const metadata = {
|
||||
type: 'subscription',
|
||||
userId: user._id,
|
||||
gift: undefined,
|
||||
sub: JSON.stringify(sub),
|
||||
};
|
||||
|
||||
expect(subscriptions.checkSubData).to.be.calledOnce;
|
||||
expect(subscriptions.checkSubData).to.be.calledWith(sub, false, coupon);
|
||||
expect(stripe.checkout.sessions.create).to.be.calledOnce;
|
||||
expect(stripe.checkout.sessions.create).to.be.calledWith({
|
||||
payment_method_types: ['card'],
|
||||
metadata,
|
||||
line_items: [{
|
||||
price: sub.key,
|
||||
quantity: 1,
|
||||
// @TODO proper copy
|
||||
}],
|
||||
mode: 'subscription',
|
||||
...redirectUrls,
|
||||
});
|
||||
});
|
||||
|
||||
it('throws if group does not exists', async () => {
|
||||
const groupId = 'invalid';
|
||||
sandbox.stub(Group.prototype, 'getMemberCount').resolves(4);
|
||||
|
||||
const subKey = 'group_monthly';
|
||||
const coupon = null;
|
||||
const sub = common.content.subscriptionBlocks[subKey];
|
||||
|
||||
await expect(createCheckoutSession({
|
||||
user, sub, coupon, groupId,
|
||||
}, stripe))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
message: i18n.t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('group plan', async () => {
|
||||
const group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
const groupId = group._id;
|
||||
await group.save();
|
||||
sandbox.stub(Group.prototype, 'getMemberCount').resolves(4);
|
||||
|
||||
// Add user to group
|
||||
user.guilds.push(groupId);
|
||||
await user.save();
|
||||
|
||||
const subKey = 'group_monthly';
|
||||
const coupon = null;
|
||||
sandbox.stub(subscriptions, 'checkSubData').returns(undefined);
|
||||
const sub = common.content.subscriptionBlocks[subKey];
|
||||
|
||||
const res = await createCheckoutSession({
|
||||
user, sub, coupon, groupId,
|
||||
}, stripe);
|
||||
expect(res).to.equal(sessionId);
|
||||
|
||||
const metadata = {
|
||||
type: 'subscription',
|
||||
userId: user._id,
|
||||
gift: undefined,
|
||||
sub: JSON.stringify(sub),
|
||||
groupId,
|
||||
};
|
||||
|
||||
expect(Group.prototype.getMemberCount).to.be.calledOnce;
|
||||
expect(subscriptions.checkSubData).to.be.calledOnce;
|
||||
expect(subscriptions.checkSubData).to.be.calledWith(sub, true, coupon);
|
||||
expect(stripe.checkout.sessions.create).to.be.calledOnce;
|
||||
expect(stripe.checkout.sessions.create).to.be.calledWith({
|
||||
payment_method_types: ['card'],
|
||||
metadata,
|
||||
line_items: [{
|
||||
price: sub.key,
|
||||
quantity: 6,
|
||||
// @TODO proper copy
|
||||
}],
|
||||
mode: 'subscription',
|
||||
...redirectUrls,
|
||||
});
|
||||
});
|
||||
|
||||
// no gift, sub or gem payment
|
||||
it('throws if type is invalid', async () => {
|
||||
await expect(createCheckoutSession({ user }, stripe))
|
||||
.to.eventually.be.rejected;
|
||||
});
|
||||
});
|
||||
|
||||
describe('createEditCardCheckoutSession', () => {
|
||||
let user;
|
||||
const sessionId = 'session-id';
|
||||
const customerId = 'customerId';
|
||||
const subscriptionId = 'subscription-id';
|
||||
let subscriptionsListStub;
|
||||
|
||||
beforeEach(() => {
|
||||
user = new User();
|
||||
sandbox.stub(stripe.checkout.sessions, 'create').returns(sessionId);
|
||||
subscriptionsListStub = sandbox.stub(stripe.subscriptions, 'list');
|
||||
subscriptionsListStub.resolves({ data: [{ id: subscriptionId }] });
|
||||
});
|
||||
|
||||
it('throws if no valid data is supplied', async () => {
|
||||
await expect(createEditCardCheckoutSession({}, stripe))
|
||||
.to.eventually.be.rejected;
|
||||
});
|
||||
|
||||
it('throws if customer does not exists', async () => {
|
||||
await expect(createEditCardCheckoutSession({ user }, stripe))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('missingSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws if subscription does not exists', async () => {
|
||||
user.purchased.plan.customerId = customerId;
|
||||
subscriptionsListStub.resolves({ data: [] });
|
||||
|
||||
await expect(createEditCardCheckoutSession({ user }, stripe))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('missingSubscription'),
|
||||
});
|
||||
});
|
||||
it('change card for user subscription', async () => {
|
||||
user.purchased.plan.customerId = customerId;
|
||||
|
||||
const metadata = {
|
||||
userId: user._id,
|
||||
type: 'edit-card-user',
|
||||
};
|
||||
|
||||
const res = await createEditCardCheckoutSession({ user }, stripe);
|
||||
expect(res).to.equal(sessionId);
|
||||
expect(subscriptionsListStub).to.be.calledOnce;
|
||||
expect(subscriptionsListStub).to.be.calledWith({ customer: customerId });
|
||||
|
||||
expect(stripe.checkout.sessions.create).to.be.calledOnce;
|
||||
expect(stripe.checkout.sessions.create).to.be.calledWith({
|
||||
mode: 'setup',
|
||||
payment_method_types: ['card'],
|
||||
metadata,
|
||||
customer: customerId,
|
||||
setup_intent_data: {
|
||||
metadata: {
|
||||
customer_id: customerId,
|
||||
subscription_id: subscriptionId,
|
||||
},
|
||||
},
|
||||
...redirectUrls,
|
||||
});
|
||||
});
|
||||
|
||||
it('throws if group does not exists', async () => {
|
||||
const groupId = 'invalid';
|
||||
|
||||
await expect(createEditCardCheckoutSession({ user, groupId }, stripe))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
message: i18n.t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
describe('with group', () => {
|
||||
let group; let groupId;
|
||||
beforeEach(async () => {
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
groupId = group._id;
|
||||
await group.save();
|
||||
});
|
||||
|
||||
it('throws if user is not allowed to change group plan', async () => {
|
||||
const anotherUser = new User();
|
||||
anotherUser.guilds.push(groupId);
|
||||
await anotherUser.save();
|
||||
|
||||
await expect(createEditCardCheckoutSession({ user: anotherUser, groupId }, stripe))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws if customer does not exists (group)', async () => {
|
||||
await expect(createEditCardCheckoutSession({ user, groupId }, stripe))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('missingSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws if subscription does not exists (group)', async () => {
|
||||
group.purchased.plan.customerId = customerId;
|
||||
subscriptionsListStub.resolves({ data: [] });
|
||||
|
||||
await expect(createEditCardCheckoutSession({ user, groupId }, stripe))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('missingSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('change card for group plans - leader', async () => {
|
||||
group.purchased.plan.customerId = customerId;
|
||||
await group.save();
|
||||
|
||||
const metadata = {
|
||||
userId: user._id,
|
||||
type: 'edit-card-group',
|
||||
groupId,
|
||||
};
|
||||
|
||||
const res = await createEditCardCheckoutSession({ user, groupId }, stripe);
|
||||
expect(res).to.equal(sessionId);
|
||||
expect(subscriptionsListStub).to.be.calledOnce;
|
||||
expect(subscriptionsListStub).to.be.calledWith({ customer: customerId });
|
||||
|
||||
expect(stripe.checkout.sessions.create).to.be.calledOnce;
|
||||
expect(stripe.checkout.sessions.create).to.be.calledWith({
|
||||
mode: 'setup',
|
||||
payment_method_types: ['card'],
|
||||
metadata,
|
||||
customer: customerId,
|
||||
setup_intent_data: {
|
||||
metadata: {
|
||||
customer_id: customerId,
|
||||
subscription_id: subscriptionId,
|
||||
},
|
||||
},
|
||||
...redirectUrls,
|
||||
});
|
||||
});
|
||||
|
||||
it('change card for group plans - plan owner', async () => {
|
||||
const anotherUser = new User();
|
||||
anotherUser.guilds.push(groupId);
|
||||
await anotherUser.save();
|
||||
|
||||
group.purchased.plan.customerId = customerId;
|
||||
group.purchased.plan.owner = anotherUser._id;
|
||||
await group.save();
|
||||
|
||||
const metadata = {
|
||||
userId: anotherUser._id,
|
||||
type: 'edit-card-group',
|
||||
groupId,
|
||||
};
|
||||
|
||||
const res = await createEditCardCheckoutSession({ user: anotherUser, groupId }, stripe);
|
||||
expect(res).to.equal(sessionId);
|
||||
expect(subscriptionsListStub).to.be.calledOnce;
|
||||
expect(subscriptionsListStub).to.be.calledWith({ customer: customerId });
|
||||
|
||||
expect(stripe.checkout.sessions.create).to.be.calledOnce;
|
||||
expect(stripe.checkout.sessions.create).to.be.calledWith({
|
||||
mode: 'setup',
|
||||
payment_method_types: ['card'],
|
||||
metadata,
|
||||
customer: customerId,
|
||||
setup_intent_data: {
|
||||
metadata: {
|
||||
customer_id: customerId,
|
||||
subscription_id: subscriptionId,
|
||||
},
|
||||
},
|
||||
...redirectUrls,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
import stripeModule from 'stripe';
|
||||
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../helpers/api-unit.helper';
|
||||
import { model as User } from '../../../../../../website/server/models/user';
|
||||
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
|
||||
import common from '../../../../../../website/common';
|
||||
|
||||
const { i18n } = common;
|
||||
|
||||
describe('stripe - edit subscription', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
const stripe = stripeModule('test');
|
||||
let user; let groupId; let group; let
|
||||
token;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.customerId = 'customer-id';
|
||||
user.purchased.plan.planId = subKey;
|
||||
user.purchased.plan.lastBillingDate = new Date();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = 'customer-id';
|
||||
group.purchased.plan.planId = subKey;
|
||||
await group.save();
|
||||
|
||||
groupId = group._id;
|
||||
|
||||
token = 'test-token';
|
||||
});
|
||||
|
||||
it('throws an error if there is no customer id', async () => {
|
||||
user.purchased.plan.customerId = undefined;
|
||||
|
||||
await expect(stripePayments.editSubscription({
|
||||
user,
|
||||
groupId: undefined,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('missingSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if a token is not provided', async () => {
|
||||
await expect(stripePayments.editSubscription({
|
||||
user,
|
||||
groupId: undefined,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: 'Missing req.body.id',
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if the group is not found', async () => {
|
||||
await expect(stripePayments.editSubscription({
|
||||
token,
|
||||
user,
|
||||
groupId: 'fake-group',
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
message: i18n.t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if user is not the group leader', async () => {
|
||||
const nonLeader = new User();
|
||||
nonLeader.guilds.push(groupId);
|
||||
await nonLeader.save();
|
||||
|
||||
await expect(stripePayments.editSubscription({
|
||||
token,
|
||||
user: nonLeader,
|
||||
groupId,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
describe('success', () => {
|
||||
let stripeListSubscriptionStub; let stripeUpdateSubscriptionStub; let
|
||||
subscriptionId;
|
||||
|
||||
beforeEach(() => {
|
||||
subscriptionId = 'subId';
|
||||
stripeListSubscriptionStub = sinon.stub(stripe.subscriptions, 'list')
|
||||
.resolves({
|
||||
data: [{ id: subscriptionId }],
|
||||
});
|
||||
|
||||
stripeUpdateSubscriptionStub = sinon.stub(stripe.subscriptions, 'update').resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
stripe.subscriptions.list.restore();
|
||||
stripe.subscriptions.update.restore();
|
||||
});
|
||||
|
||||
it('edits a user subscription', async () => {
|
||||
await stripePayments.editSubscription({
|
||||
token,
|
||||
user,
|
||||
groupId: undefined,
|
||||
}, stripe);
|
||||
|
||||
expect(stripeListSubscriptionStub).to.be.calledOnce;
|
||||
expect(stripeListSubscriptionStub).to.be.calledWith({
|
||||
customer: user.purchased.plan.customerId,
|
||||
});
|
||||
expect(stripeUpdateSubscriptionStub).to.be.calledOnce;
|
||||
expect(stripeUpdateSubscriptionStub).to.be.calledWith(
|
||||
subscriptionId,
|
||||
{ card: token },
|
||||
);
|
||||
});
|
||||
|
||||
it('edits a group subscription', async () => {
|
||||
await stripePayments.editSubscription({
|
||||
token,
|
||||
user,
|
||||
groupId,
|
||||
}, stripe);
|
||||
|
||||
expect(stripeListSubscriptionStub).to.be.calledOnce;
|
||||
expect(stripeListSubscriptionStub).to.be.calledWith({
|
||||
customer: group.purchased.plan.customerId,
|
||||
});
|
||||
expect(stripeUpdateSubscriptionStub).to.be.calledOnce;
|
||||
expect(stripeUpdateSubscriptionStub).to.be.calledWith(
|
||||
subscriptionId,
|
||||
{ card: token },
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
316
test/api/unit/libs/payments/stripe/oneTimePayments.test.js
Normal file
@@ -0,0 +1,316 @@
|
||||
import apiError from '../../../../../../website/server/libs/apiError';
|
||||
import common from '../../../../../../website/common';
|
||||
import {
|
||||
getOneTimePaymentInfo,
|
||||
applyGemPayment,
|
||||
} from '../../../../../../website/server/libs/payments/stripe/oneTimePayments';
|
||||
import * as subscriptions from '../../../../../../website/server/libs/payments/stripe/subscriptions';
|
||||
import { model as User } from '../../../../../../website/server/models/user';
|
||||
import payments from '../../../../../../website/server/libs/payments/payments';
|
||||
|
||||
const { i18n } = common;
|
||||
|
||||
describe('Stripe - One Time Payments', () => {
|
||||
describe('getOneTimePaymentInfo', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(() => {
|
||||
user = new User();
|
||||
sandbox.stub(subscriptions, 'checkSubData');
|
||||
});
|
||||
|
||||
describe('gemsBlock', () => {
|
||||
it('returns the gemsBlock and amount', async () => {
|
||||
const { gemsBlock, amount, subscription } = await getOneTimePaymentInfo('21gems', null, user);
|
||||
expect(gemsBlock).to.equal(common.content.gems['21gems']);
|
||||
expect(amount).to.equal(gemsBlock.price);
|
||||
expect(amount).to.equal(499);
|
||||
expect(subscription).to.be.null;
|
||||
expect(subscriptions.checkSubData).to.not.be.called;
|
||||
});
|
||||
|
||||
it('throws if the gemsBlock does not exist', async () => {
|
||||
await expect(getOneTimePaymentInfo('not existant', null, user))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: apiError('invalidGemsBlock'),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws if the user cannot receive gems', async () => {
|
||||
sandbox.stub(user, 'canGetGems').resolves(false);
|
||||
await expect(getOneTimePaymentInfo('21gems', null, user))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('groupPolicyCannotGetGems'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('gift', () => {
|
||||
it('throws if the receiver does not exist', async () => {
|
||||
const gift = {
|
||||
type: 'gems',
|
||||
uuid: 'invalid',
|
||||
gems: {
|
||||
amount: 3,
|
||||
},
|
||||
};
|
||||
|
||||
await expect(getOneTimePaymentInfo(null, gift, user))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
message: i18n.t('userWithIDNotFound', { userId: 'invalid' }),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws if the user cannot receive gems', async () => {
|
||||
const receivingUser = new User();
|
||||
await receivingUser.save();
|
||||
sandbox.stub(User.prototype, 'canGetGems').resolves(false);
|
||||
|
||||
const gift = {
|
||||
type: 'gems',
|
||||
uuid: receivingUser._id,
|
||||
gems: {
|
||||
amount: 2,
|
||||
},
|
||||
};
|
||||
|
||||
await expect(getOneTimePaymentInfo(null, gift, user))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('groupPolicyCannotGetGems'),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws if the amount of gems is <= 0', async () => {
|
||||
const receivingUser = new User();
|
||||
await receivingUser.save();
|
||||
const gift = {
|
||||
type: 'gems',
|
||||
uuid: receivingUser._id,
|
||||
gems: {
|
||||
amount: 0,
|
||||
},
|
||||
};
|
||||
|
||||
await expect(getOneTimePaymentInfo(null, gift, user))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: i18n.t('badAmountOfGemsToPurchase'),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws if the subscription block does not exist', async () => {
|
||||
const receivingUser = new User();
|
||||
await receivingUser.save();
|
||||
const gift = {
|
||||
type: 'subscription',
|
||||
uuid: receivingUser._id,
|
||||
subscription: {
|
||||
key: 'invalid',
|
||||
},
|
||||
};
|
||||
|
||||
await expect(getOneTimePaymentInfo(null, gift, user))
|
||||
.to.eventually.throw;
|
||||
});
|
||||
|
||||
it('returns the amount (gems)', async () => {
|
||||
const receivingUser = new User();
|
||||
await receivingUser.save();
|
||||
const gift = {
|
||||
type: 'gems',
|
||||
uuid: receivingUser._id,
|
||||
gems: {
|
||||
amount: 4,
|
||||
},
|
||||
};
|
||||
|
||||
expect(subscriptions.checkSubData).to.not.be.called;
|
||||
|
||||
const { gemsBlock, amount, subscription } = await getOneTimePaymentInfo(null, gift, user);
|
||||
expect(gemsBlock).to.equal(null);
|
||||
expect(amount).to.equal('100');
|
||||
expect(subscription).to.be.null;
|
||||
});
|
||||
|
||||
it('returns the amount (subscription)', async () => {
|
||||
const receivingUser = new User();
|
||||
await receivingUser.save();
|
||||
const gift = {
|
||||
type: 'subscription',
|
||||
uuid: receivingUser._id,
|
||||
subscription: {
|
||||
key: 'basic_3mo',
|
||||
},
|
||||
};
|
||||
const sub = common.content.subscriptionBlocks['basic_3mo']; // eslint-disable-line dot-notation
|
||||
|
||||
const { gemsBlock, amount, subscription } = await getOneTimePaymentInfo(null, gift, user);
|
||||
|
||||
expect(subscriptions.checkSubData).to.be.calledOnce;
|
||||
expect(subscriptions.checkSubData).to.be.calledWith(sub, false, null);
|
||||
|
||||
expect(gemsBlock).to.equal(null);
|
||||
expect(amount).to.equal('1500');
|
||||
expect(Number(amount)).to.equal(sub.price * 100);
|
||||
expect(subscription).to.equal(sub);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyGemPayment', () => {
|
||||
let user;
|
||||
let customerId;
|
||||
let subKey;
|
||||
let userFindByIdStub;
|
||||
let paymentsCreateSubSpy;
|
||||
let paymentBuyGemsStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
subKey = 'basic_3mo';
|
||||
|
||||
user = new User();
|
||||
await user.save();
|
||||
|
||||
customerId = 'test-id';
|
||||
|
||||
paymentsCreateSubSpy = sandbox.stub(payments, 'createSubscription');
|
||||
paymentsCreateSubSpy.resolves({});
|
||||
|
||||
paymentBuyGemsStub = sandbox.stub(payments, 'buyGems');
|
||||
paymentBuyGemsStub.resolves({});
|
||||
});
|
||||
|
||||
it('throws if the user does not exist', async () => {
|
||||
const metadata = { userId: 'invalid' };
|
||||
const session = { metadata, customer: customerId };
|
||||
|
||||
await expect(applyGemPayment(session))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
message: i18n.t('userWithIDNotFound', { userId: metadata.userId }),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws if the receiving user does not exist', async () => {
|
||||
const metadata = { userId: 'invalid' };
|
||||
const session = { metadata, customer: customerId };
|
||||
|
||||
await expect(applyGemPayment(session))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
message: i18n.t('userWithIDNotFound', { userId: metadata.userId }),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws if the gems block does not exist', async () => {
|
||||
const gift = {
|
||||
type: 'gems',
|
||||
uuid: 'invalid',
|
||||
gems: {
|
||||
amount: 16,
|
||||
},
|
||||
};
|
||||
|
||||
const metadata = { userId: user._id, gift: JSON.stringify(gift) };
|
||||
const session = { metadata, customer: customerId };
|
||||
|
||||
await expect(applyGemPayment(session))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
message: i18n.t('userWithIDNotFound', { userId: 'invalid' }),
|
||||
});
|
||||
});
|
||||
|
||||
describe('with existing user', () => {
|
||||
beforeEach(() => {
|
||||
const execStub = sandbox.stub().resolves(user);
|
||||
userFindByIdStub = sandbox.stub(User, 'findById');
|
||||
userFindByIdStub.withArgs(user._id).returns({ exec: execStub });
|
||||
});
|
||||
|
||||
it('buys gems', async () => {
|
||||
const metadata = { userId: user._id, gemsBlock: '21gems' };
|
||||
const session = { metadata, customer: customerId };
|
||||
|
||||
await applyGemPayment(session);
|
||||
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
customerId,
|
||||
paymentMethod: 'Stripe',
|
||||
gift: undefined,
|
||||
gemsBlock: common.content.gems['21gems'],
|
||||
});
|
||||
});
|
||||
|
||||
it('gift gems', async () => {
|
||||
const receivingUser = new User();
|
||||
const execStub = sandbox.stub().resolves(receivingUser);
|
||||
userFindByIdStub.withArgs(receivingUser._id).returns({ exec: execStub });
|
||||
const gift = {
|
||||
type: 'gems',
|
||||
uuid: receivingUser._id,
|
||||
gems: {
|
||||
amount: 16,
|
||||
},
|
||||
};
|
||||
|
||||
sandbox.stub(JSON, 'parse').returns(gift);
|
||||
const metadata = { userId: user._id, gift: JSON.stringify(gift) };
|
||||
const session = { metadata, customer: customerId };
|
||||
|
||||
await applyGemPayment(session);
|
||||
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
customerId,
|
||||
paymentMethod: 'Gift',
|
||||
gift,
|
||||
gemsBlock: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('gift sub', async () => {
|
||||
const receivingUser = new User();
|
||||
const execStub = sandbox.stub().resolves(receivingUser);
|
||||
userFindByIdStub.withArgs(receivingUser._id).returns({ exec: execStub });
|
||||
const gift = {
|
||||
type: 'subscription',
|
||||
uuid: receivingUser._id,
|
||||
subscription: {
|
||||
key: subKey,
|
||||
},
|
||||
};
|
||||
|
||||
sandbox.stub(JSON, 'parse').returns(gift);
|
||||
const metadata = { userId: user._id, gift: JSON.stringify(gift) };
|
||||
const session = { metadata, customer: customerId };
|
||||
|
||||
await applyGemPayment(session);
|
||||
|
||||
expect(paymentsCreateSubSpy).to.be.calledOnce;
|
||||
expect(paymentsCreateSubSpy).to.be.calledWith({
|
||||
user,
|
||||
customerId,
|
||||
paymentMethod: 'Gift',
|
||||
gift,
|
||||
gemsBlock: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
442
test/api/unit/libs/payments/stripe/subscriptions.test.js
Normal file
@@ -0,0 +1,442 @@
|
||||
import cc from 'coupon-code';
|
||||
import stripeModule from 'stripe';
|
||||
|
||||
import { model as Coupon } from '../../../../../../website/server/models/coupon';
|
||||
import common from '../../../../../../website/common';
|
||||
import {
|
||||
checkSubData,
|
||||
applySubscription,
|
||||
chargeForAdditionalGroupMember,
|
||||
handlePaymentMethodChange,
|
||||
} from '../../../../../../website/server/libs/payments/stripe/subscriptions';
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../helpers/api-unit.helper';
|
||||
import { model as User } from '../../../../../../website/server/models/user';
|
||||
import payments from '../../../../../../website/server/libs/payments/payments';
|
||||
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
|
||||
|
||||
const { i18n } = common;
|
||||
|
||||
describe('Stripe - Subscriptions', () => {
|
||||
describe('checkSubData', () => {
|
||||
it('does not throw if the subscription can be used', async () => {
|
||||
const sub = common.content.subscriptionBlocks['basic_3mo']; // eslint-disable-line dot-notation
|
||||
const res = await checkSubData(sub);
|
||||
expect(res).to.equal(undefined);
|
||||
});
|
||||
|
||||
it('throws if the subscription does not exists', async () => {
|
||||
await expect(checkSubData())
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: i18n.t('missingSubscriptionCode'),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws if the subscription can\'t be used', async () => {
|
||||
const sub = common.content.subscriptionBlocks['group_plan_auto']; // eslint-disable-line dot-notation
|
||||
await expect(checkSubData(sub, true))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: i18n.t('missingSubscriptionCode'),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws if the subscription targets a group and an user is making the request', async () => {
|
||||
const sub = common.content.subscriptionBlocks['group_monthly']; // eslint-disable-line dot-notation
|
||||
await expect(checkSubData(sub, false))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: i18n.t('missingSubscriptionCode'),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws if the subscription targets an user and a group is making the request', async () => {
|
||||
const sub = common.content.subscriptionBlocks['basic_3mo']; // eslint-disable-line dot-notation
|
||||
await expect(checkSubData(sub, true))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: i18n.t('missingSubscriptionCode'),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws if the coupon is required but not passed', async () => {
|
||||
const sub = common.content.subscriptionBlocks['google_6mo']; // eslint-disable-line dot-notation
|
||||
await expect(checkSubData(sub, false))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: i18n.t('couponCodeRequired'),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws if the coupon is required but does not exist', async () => {
|
||||
const coupon = 'not-valid';
|
||||
const sub = common.content.subscriptionBlocks['google_6mo']; // eslint-disable-line dot-notation
|
||||
await expect(checkSubData(sub, false, coupon))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: i18n.t('invalidCoupon'),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws if the coupon is required but is invalid', async () => {
|
||||
const couponModel = new Coupon();
|
||||
couponModel.event = 'google_6mo';
|
||||
await couponModel.save();
|
||||
|
||||
sandbox.stub(cc, 'validate').returns('invalid');
|
||||
|
||||
const sub = common.content.subscriptionBlocks['google_6mo']; // eslint-disable-line dot-notation
|
||||
await expect(checkSubData(sub, false, couponModel._id))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: i18n.t('invalidCoupon'),
|
||||
});
|
||||
});
|
||||
|
||||
it('works if the coupon is required and valid', async () => {
|
||||
const couponModel = new Coupon();
|
||||
couponModel.event = 'google_6mo';
|
||||
await couponModel.save();
|
||||
|
||||
sandbox.stub(cc, 'validate').returns(couponModel._id);
|
||||
|
||||
const sub = common.content.subscriptionBlocks['google_6mo']; // eslint-disable-line dot-notation
|
||||
await checkSubData(sub, false, couponModel._id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('applySubscription', () => {
|
||||
let user; let group; let sub;
|
||||
let groupId;
|
||||
let customerId; let subscriptionId;
|
||||
let subKey;
|
||||
let userFindByIdStub;
|
||||
let stripePaymentsCreateSubSpy;
|
||||
|
||||
beforeEach(async () => {
|
||||
subKey = 'basic_3mo';
|
||||
sub = common.content.subscriptionBlocks[subKey];
|
||||
|
||||
user = new User();
|
||||
await user.save();
|
||||
|
||||
const execStub = sandbox.stub().resolves(user);
|
||||
userFindByIdStub = sandbox.stub(User, 'findById');
|
||||
userFindByIdStub.withArgs(user._id).returns({ exec: execStub });
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
groupId = group._id;
|
||||
await group.save();
|
||||
|
||||
// Add user to group
|
||||
user.guilds.push(groupId);
|
||||
await user.save();
|
||||
|
||||
customerId = 'test-id';
|
||||
subscriptionId = 'test-sub-id';
|
||||
|
||||
stripePaymentsCreateSubSpy = sandbox.stub(payments, 'createSubscription');
|
||||
stripePaymentsCreateSubSpy.resolves({});
|
||||
});
|
||||
|
||||
it('subscribes a user', async () => {
|
||||
await applySubscription({
|
||||
customer: customerId,
|
||||
subscription: subscriptionId,
|
||||
metadata: {
|
||||
sub: JSON.stringify(sub),
|
||||
userId: user._id,
|
||||
groupId: null,
|
||||
},
|
||||
user,
|
||||
});
|
||||
|
||||
expect(stripePaymentsCreateSubSpy).to.be.calledOnce;
|
||||
expect(stripePaymentsCreateSubSpy).to.be.calledWith({
|
||||
user,
|
||||
customerId,
|
||||
subscriptionId,
|
||||
paymentMethod: 'Stripe',
|
||||
sub: sinon.match({ ...sub }),
|
||||
groupId: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('subscribes a group', async () => {
|
||||
sub = common.content.subscriptionBlocks['group_monthly']; // eslint-disable-line dot-notation
|
||||
await applySubscription({
|
||||
customer: customerId,
|
||||
subscription: subscriptionId,
|
||||
metadata: {
|
||||
sub: JSON.stringify(sub),
|
||||
userId: user._id,
|
||||
groupId,
|
||||
},
|
||||
user,
|
||||
});
|
||||
|
||||
expect(stripePaymentsCreateSubSpy).to.be.calledOnce;
|
||||
expect(stripePaymentsCreateSubSpy).to.be.calledWith({
|
||||
user,
|
||||
customerId,
|
||||
subscriptionId,
|
||||
paymentMethod: 'Stripe',
|
||||
sub: sinon.match({ ...sub }),
|
||||
groupId,
|
||||
});
|
||||
});
|
||||
|
||||
it('subscribes a group with multiple users', async () => {
|
||||
const user2 = new User();
|
||||
user2.guilds.push(groupId);
|
||||
await user2.save();
|
||||
|
||||
const execStub2 = sandbox.stub().resolves(user);
|
||||
userFindByIdStub.withArgs(user2._id).returns({ exec: execStub2 });
|
||||
|
||||
group.memberCount = 2;
|
||||
await group.save();
|
||||
|
||||
sub = common.content.subscriptionBlocks['group_monthly']; // eslint-disable-line dot-notation
|
||||
await applySubscription({
|
||||
customer: customerId,
|
||||
subscription: subscriptionId,
|
||||
metadata: {
|
||||
sub: JSON.stringify(sub),
|
||||
userId: user._id,
|
||||
groupId,
|
||||
},
|
||||
user,
|
||||
});
|
||||
|
||||
expect(stripePaymentsCreateSubSpy).to.be.calledOnce;
|
||||
expect(stripePaymentsCreateSubSpy).to.be.calledWith({
|
||||
user,
|
||||
customerId,
|
||||
subscriptionId,
|
||||
paymentMethod: 'Stripe',
|
||||
sub: sinon.match({ ...sub }),
|
||||
groupId,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handlePaymentMethodChange', () => {
|
||||
const stripe = stripeModule('test', {
|
||||
apiVersion: '2020-08-27',
|
||||
});
|
||||
|
||||
it('updates the plan quantity based on the number of group members', async () => {
|
||||
const stripeIntentRetrieveStub = sandbox.stub(stripe.setupIntents, 'retrieve').resolves({
|
||||
payment_method: 1,
|
||||
metadata: {
|
||||
subscription_id: 2,
|
||||
},
|
||||
});
|
||||
const stripeSubUpdateStub = sandbox.stub(stripe.subscriptions, 'update');
|
||||
|
||||
await handlePaymentMethodChange({}, stripe);
|
||||
expect(stripeIntentRetrieveStub).to.be.calledOnce;
|
||||
expect(stripeSubUpdateStub).to.be.calledOnce;
|
||||
expect(stripeSubUpdateStub).to.be.calledWith(2, {
|
||||
default_payment_method: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('chargeForAdditionalGroupMember', () => {
|
||||
const stripe = stripeModule('test', {
|
||||
apiVersion: '2020-08-27',
|
||||
});
|
||||
let stripeUpdateSubStub;
|
||||
const plan = common.content.subscriptionBlocks['group_monthly']; // eslint-disable-line dot-notation
|
||||
|
||||
let user; let group;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = 'customer-id';
|
||||
group.purchased.plan.planId = plan.key;
|
||||
group.purchased.plan.subscriptionId = 'sub-id';
|
||||
await group.save();
|
||||
|
||||
stripeUpdateSubStub = sandbox.stub(stripe.subscriptions, 'update').resolves({});
|
||||
});
|
||||
|
||||
it('updates the plan quantity based on the number of group members', async () => {
|
||||
group.memberCount = 4;
|
||||
const newQuantity = group.memberCount + plan.quantity - 1;
|
||||
|
||||
await chargeForAdditionalGroupMember(group, stripe);
|
||||
expect(stripeUpdateSubStub).to.be.calledWithMatch(
|
||||
group.purchased.plan.subscriptionId,
|
||||
sinon.match({
|
||||
plan: group.purchased.plan.planId,
|
||||
quantity: newQuantity,
|
||||
}),
|
||||
);
|
||||
expect(group.purchased.plan.quantity).to.equal(newQuantity);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancelSubscription', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
const stripe = stripeModule('test', {
|
||||
apiVersion: '2020-08-27',
|
||||
});
|
||||
let user; let groupId; let
|
||||
group;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.customerId = 'customer-id';
|
||||
user.purchased.plan.planId = subKey;
|
||||
user.purchased.plan.lastBillingDate = new Date();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = 'customer-id';
|
||||
group.purchased.plan.planId = subKey;
|
||||
await group.save();
|
||||
|
||||
groupId = group._id;
|
||||
});
|
||||
|
||||
it('throws an error if there is no customer id', async () => {
|
||||
user.purchased.plan.customerId = undefined;
|
||||
|
||||
await expect(stripePayments.cancelSubscription({
|
||||
user,
|
||||
groupId: undefined,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('missingSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if the group is not found', async () => {
|
||||
await expect(stripePayments.cancelSubscription({
|
||||
user,
|
||||
groupId: 'fake-group',
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
message: i18n.t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if user is not the group leader', async () => {
|
||||
const nonLeader = new User();
|
||||
nonLeader.guilds.push(groupId);
|
||||
await nonLeader.save();
|
||||
|
||||
await expect(stripePayments.cancelSubscription({
|
||||
user: nonLeader,
|
||||
groupId,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
describe('success', () => {
|
||||
let stripeDeleteCustomerStub; let paymentsCancelSubStub;
|
||||
let stripeRetrieveStub; let subscriptionId; let
|
||||
currentPeriodEndTimeStamp;
|
||||
|
||||
beforeEach(() => {
|
||||
subscriptionId = 'subId';
|
||||
stripeDeleteCustomerStub = sinon.stub(stripe.customers, 'del').resolves({});
|
||||
paymentsCancelSubStub = sinon.stub(payments, 'cancelSubscription').resolves({});
|
||||
|
||||
currentPeriodEndTimeStamp = (new Date()).getTime();
|
||||
stripeRetrieveStub = sinon.stub(stripe.customers, 'retrieve')
|
||||
.resolves({
|
||||
subscriptions: {
|
||||
data: [{
|
||||
id: subscriptionId,
|
||||
current_period_end: currentPeriodEndTimeStamp,
|
||||
}], // eslint-disable-line camelcase
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
stripe.customers.del.restore();
|
||||
stripe.customers.retrieve.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
|
||||
it('cancels a user subscription', async () => {
|
||||
await stripePayments.cancelSubscription({
|
||||
user,
|
||||
groupId: undefined,
|
||||
}, stripe);
|
||||
|
||||
expect(stripeDeleteCustomerStub).to.be.calledOnce;
|
||||
expect(stripeDeleteCustomerStub).to.be.calledWith(user.purchased.plan.customerId);
|
||||
expect(stripeRetrieveStub).to.be.calledOnce;
|
||||
expect(stripeRetrieveStub).to.be.calledWith(user.purchased.plan.customerId);
|
||||
expect(paymentsCancelSubStub).to.be.calledOnce;
|
||||
expect(paymentsCancelSubStub).to.be.calledWith({
|
||||
user,
|
||||
groupId: undefined,
|
||||
nextBill: currentPeriodEndTimeStamp * 1000, // timestamp in seconds
|
||||
paymentMethod: 'Stripe',
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('cancels a group subscription', async () => {
|
||||
await stripePayments.cancelSubscription({
|
||||
user,
|
||||
groupId,
|
||||
}, stripe);
|
||||
|
||||
expect(stripeDeleteCustomerStub).to.be.calledOnce;
|
||||
expect(stripeDeleteCustomerStub).to.be.calledWith(group.purchased.plan.customerId);
|
||||
expect(stripeRetrieveStub).to.be.calledOnce;
|
||||
expect(stripeRetrieveStub).to.be.calledWith(user.purchased.plan.customerId);
|
||||
expect(paymentsCancelSubStub).to.be.calledOnce;
|
||||
expect(paymentsCancelSubStub).to.be.calledWith({
|
||||
user,
|
||||
groupId,
|
||||
nextBill: currentPeriodEndTimeStamp * 1000, // timestamp in seconds
|
||||
paymentMethod: 'Stripe',
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,70 +0,0 @@
|
||||
import stripeModule from 'stripe';
|
||||
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../helpers/api-unit.helper';
|
||||
import { model as User } from '../../../../../../website/server/models/user';
|
||||
import { model as Group } from '../../../../../../website/server/models/group';
|
||||
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
|
||||
import payments from '../../../../../../website/server/libs/payments/payments';
|
||||
|
||||
describe('Stripe - Upgrade Group Plan', () => {
|
||||
const stripe = stripeModule('test');
|
||||
let spy; let data; let user; let
|
||||
group;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
|
||||
data = {
|
||||
user,
|
||||
sub: {
|
||||
key: 'basic_3mo', // @TODO: Validate that this is group
|
||||
},
|
||||
customerId: 'customer-id',
|
||||
paymentMethod: 'Payment Method',
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
};
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'private',
|
||||
leader: user._id,
|
||||
});
|
||||
await group.save();
|
||||
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
|
||||
spy = sinon.stub(stripe.subscriptions, 'update');
|
||||
spy.resolves([]);
|
||||
data.groupId = group._id;
|
||||
data.sub.quantity = 3;
|
||||
stripePayments.setStripeApi(stripe);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
stripe.subscriptions.update.restore();
|
||||
});
|
||||
|
||||
it('updates a group plan quantity', async () => {
|
||||
data.paymentMethod = 'Stripe';
|
||||
await payments.createSubscription(data);
|
||||
|
||||
const updatedGroup = await Group.findById(group._id).exec();
|
||||
expect(updatedGroup.purchased.plan.quantity).to.eql(3);
|
||||
|
||||
updatedGroup.memberCount += 1;
|
||||
await updatedGroup.save();
|
||||
|
||||
await stripePayments.chargeForAdditionalGroupMember(updatedGroup);
|
||||
|
||||
expect(spy.calledOnce).to.be.true;
|
||||
expect(updatedGroup.purchased.plan.quantity).to.eql(4);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import stripeModule from 'stripe';
|
||||
|
||||
import nconf from 'nconf';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
@@ -10,76 +10,104 @@ import stripePayments from '../../../../../../website/server/libs/payments/strip
|
||||
import payments from '../../../../../../website/server/libs/payments/payments';
|
||||
import common from '../../../../../../website/common';
|
||||
import logger from '../../../../../../website/server/libs/logger';
|
||||
import * as oneTimePayments from '../../../../../../website/server/libs/payments/stripe/oneTimePayments';
|
||||
import * as subscriptions from '../../../../../../website/server/libs/payments/stripe/subscriptions';
|
||||
|
||||
const { i18n } = common;
|
||||
|
||||
describe('Stripe - Webhooks', () => {
|
||||
const stripe = stripeModule('test');
|
||||
const stripe = stripeModule('test', {
|
||||
apiVersion: '2020-08-27',
|
||||
});
|
||||
const endpointSecret = nconf.get('STRIPE_WEBHOOKS_ENDPOINT_SECRET');
|
||||
const headers = {};
|
||||
const body = {};
|
||||
|
||||
describe('all events', () => {
|
||||
const eventType = 'account.updated';
|
||||
const event = { id: 123 };
|
||||
const eventRetrieved = { type: eventType };
|
||||
let event;
|
||||
let constructEventStub;
|
||||
|
||||
beforeEach(() => {
|
||||
sinon.stub(stripe.events, 'retrieve').resolves(eventRetrieved);
|
||||
sinon.stub(logger, 'error');
|
||||
event = { type: 'payment_intent.created' };
|
||||
constructEventStub = sandbox.stub(stripe.webhooks, 'constructEvent');
|
||||
constructEventStub.returns(event);
|
||||
sandbox.stub(logger, 'error');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
stripe.events.retrieve.restore();
|
||||
logger.error.restore();
|
||||
it('throws if the event can\'t be validated', async () => {
|
||||
const err = new Error('fail');
|
||||
constructEventStub.throws(err);
|
||||
await expect(stripePayments.handleWebhooks({ body: event, headers }, stripe))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: `Webhook Error: ${err.message}`,
|
||||
});
|
||||
|
||||
expect(logger.error).to.have.been.calledOnce;
|
||||
const calledWith = logger.error.getCall(0).args;
|
||||
expect(calledWith[0].message).to.equal('Error verifying Stripe webhook');
|
||||
expect(calledWith[1]).to.eql({ err });
|
||||
});
|
||||
|
||||
it('logs an error if an unsupported webhook event is passed', async () => {
|
||||
const error = new Error(`Missing handler for Stripe webhook ${eventType}`);
|
||||
await stripePayments.handleWebhooks({ requestBody: event }, stripe);
|
||||
expect(logger.error).to.have.been.calledOnce;
|
||||
event.type = 'account.updated';
|
||||
await expect(stripePayments.handleWebhooks({ body, headers }, stripe))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: `Missing handler for Stripe webhook ${event.type}`,
|
||||
});
|
||||
|
||||
expect(logger.error).to.have.been.calledOnce;
|
||||
const calledWith = logger.error.getCall(0).args;
|
||||
expect(calledWith[0].message).to.equal(error.message);
|
||||
expect(calledWith[1].event).to.equal(eventRetrieved);
|
||||
expect(calledWith[0].message).to.equal('Error handling Stripe webhook');
|
||||
expect(calledWith[1].event).to.eql(event);
|
||||
expect(calledWith[1].err.message).to.eql(`Missing handler for Stripe webhook ${event.type}`);
|
||||
});
|
||||
|
||||
it('retrieves and validates the event from Stripe', async () => {
|
||||
await stripePayments.handleWebhooks({ requestBody: event }, stripe);
|
||||
expect(stripe.events.retrieve).to.have.been.calledOnce;
|
||||
expect(stripe.events.retrieve).to.have.been.calledWith(event.id);
|
||||
await stripePayments.handleWebhooks({ body, headers }, stripe);
|
||||
expect(stripe.webhooks.constructEvent).to.have.been.calledOnce;
|
||||
expect(stripe.webhooks.constructEvent)
|
||||
.to.have.been.calledWith(body, undefined, endpointSecret);
|
||||
});
|
||||
});
|
||||
|
||||
describe('customer.subscription.deleted', () => {
|
||||
const eventType = 'customer.subscription.deleted';
|
||||
let event;
|
||||
let constructEventStub;
|
||||
|
||||
beforeEach(() => {
|
||||
sinon.stub(stripe.customers, 'del').resolves({});
|
||||
sinon.stub(payments, 'cancelSubscription').resolves({});
|
||||
event = { type: eventType };
|
||||
constructEventStub = sandbox.stub(stripe.webhooks, 'constructEvent');
|
||||
constructEventStub.returns(event);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
stripe.customers.del.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
beforeEach(() => {
|
||||
sandbox.stub(stripe.customers, 'del').resolves({});
|
||||
sandbox.stub(payments, 'cancelSubscription').resolves({});
|
||||
});
|
||||
|
||||
it('does not do anything if event.request is null (subscription cancelled manually)', async () => {
|
||||
sinon.stub(stripe.events, 'retrieve').resolves({
|
||||
it('does not do anything if event.request is not null (subscription cancelled manually)', async () => {
|
||||
constructEventStub.returns({
|
||||
id: 123,
|
||||
type: eventType,
|
||||
request: 123,
|
||||
request: { id: 123 },
|
||||
});
|
||||
|
||||
await stripePayments.handleWebhooks({ requestBody: {} }, stripe);
|
||||
await stripePayments.handleWebhooks({ body, headers }, stripe);
|
||||
|
||||
expect(stripe.events.retrieve).to.have.been.calledOnce;
|
||||
expect(stripe.webhooks.constructEvent).to.have.been.calledOnce;
|
||||
expect(stripe.customers.del).to.not.have.been.called;
|
||||
expect(payments.cancelSubscription).to.not.have.been.called;
|
||||
stripe.events.retrieve.restore();
|
||||
});
|
||||
|
||||
describe('user subscription', () => {
|
||||
it('throws an error if the user is not found', async () => {
|
||||
const customerId = 456;
|
||||
sinon.stub(stripe.events, 'retrieve').resolves({
|
||||
constructEventStub.returns({
|
||||
id: 123,
|
||||
type: eventType,
|
||||
data: {
|
||||
@@ -90,10 +118,10 @@ describe('Stripe - Webhooks', () => {
|
||||
customer: customerId,
|
||||
},
|
||||
},
|
||||
request: null,
|
||||
request: { id: null },
|
||||
});
|
||||
|
||||
await expect(stripePayments.handleWebhooks({ requestBody: {} }, stripe))
|
||||
await expect(stripePayments.handleWebhooks({ body, headers }, stripe))
|
||||
.to.eventually.be.rejectedWith({
|
||||
message: i18n.t('userNotFound'),
|
||||
httpCode: 404,
|
||||
@@ -102,8 +130,6 @@ describe('Stripe - Webhooks', () => {
|
||||
|
||||
expect(stripe.customers.del).to.not.have.been.called;
|
||||
expect(payments.cancelSubscription).to.not.have.been.called;
|
||||
|
||||
stripe.events.retrieve.restore();
|
||||
});
|
||||
|
||||
it('deletes the customer on Stripe and calls payments.cancelSubscription', async () => {
|
||||
@@ -114,7 +140,7 @@ describe('Stripe - Webhooks', () => {
|
||||
subscriber.purchased.plan.paymentMethod = 'Stripe';
|
||||
await subscriber.save();
|
||||
|
||||
sinon.stub(stripe.events, 'retrieve').resolves({
|
||||
constructEventStub.returns({
|
||||
id: 123,
|
||||
type: eventType,
|
||||
data: {
|
||||
@@ -125,10 +151,10 @@ describe('Stripe - Webhooks', () => {
|
||||
customer: customerId,
|
||||
},
|
||||
},
|
||||
request: null,
|
||||
request: { id: null },
|
||||
});
|
||||
|
||||
await stripePayments.handleWebhooks({ requestBody: {} }, stripe);
|
||||
await stripePayments.handleWebhooks({ body, headers }, stripe);
|
||||
|
||||
expect(stripe.customers.del).to.have.been.calledOnce;
|
||||
expect(stripe.customers.del).to.have.been.calledWith(customerId);
|
||||
@@ -139,15 +165,13 @@ describe('Stripe - Webhooks', () => {
|
||||
expect(cancelSubscriptionOpts.paymentMethod).to.equal('Stripe');
|
||||
expect(Math.round(moment(cancelSubscriptionOpts.nextBill).diff(new Date(), 'days', true))).to.equal(3);
|
||||
expect(cancelSubscriptionOpts.groupId).to.be.undefined;
|
||||
|
||||
stripe.events.retrieve.restore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('group plan subscription', () => {
|
||||
it('throws an error if the group is not found', async () => {
|
||||
const customerId = 456;
|
||||
sinon.stub(stripe.events, 'retrieve').resolves({
|
||||
constructEventStub.returns({
|
||||
id: 123,
|
||||
type: eventType,
|
||||
data: {
|
||||
@@ -158,10 +182,10 @@ describe('Stripe - Webhooks', () => {
|
||||
customer: customerId,
|
||||
},
|
||||
},
|
||||
request: null,
|
||||
request: { id: null },
|
||||
});
|
||||
|
||||
await expect(stripePayments.handleWebhooks({ requestBody: {} }, stripe))
|
||||
await expect(stripePayments.handleWebhooks({ body, headers }, stripe))
|
||||
.to.eventually.be.rejectedWith({
|
||||
message: i18n.t('groupNotFound'),
|
||||
httpCode: 404,
|
||||
@@ -170,8 +194,6 @@ describe('Stripe - Webhooks', () => {
|
||||
|
||||
expect(stripe.customers.del).to.not.have.been.called;
|
||||
expect(payments.cancelSubscription).to.not.have.been.called;
|
||||
|
||||
stripe.events.retrieve.restore();
|
||||
});
|
||||
|
||||
it('throws an error if the group leader is not found', async () => {
|
||||
@@ -187,7 +209,7 @@ describe('Stripe - Webhooks', () => {
|
||||
subscriber.purchased.plan.paymentMethod = 'Stripe';
|
||||
await subscriber.save();
|
||||
|
||||
sinon.stub(stripe.events, 'retrieve').resolves({
|
||||
constructEventStub.returns({
|
||||
id: 123,
|
||||
type: eventType,
|
||||
data: {
|
||||
@@ -198,10 +220,10 @@ describe('Stripe - Webhooks', () => {
|
||||
customer: customerId,
|
||||
},
|
||||
},
|
||||
request: null,
|
||||
request: { id: null },
|
||||
});
|
||||
|
||||
await expect(stripePayments.handleWebhooks({ requestBody: {} }, stripe))
|
||||
await expect(stripePayments.handleWebhooks({ body, headers }, stripe))
|
||||
.to.eventually.be.rejectedWith({
|
||||
message: i18n.t('userNotFound'),
|
||||
httpCode: 404,
|
||||
@@ -210,8 +232,6 @@ describe('Stripe - Webhooks', () => {
|
||||
|
||||
expect(stripe.customers.del).to.not.have.been.called;
|
||||
expect(payments.cancelSubscription).to.not.have.been.called;
|
||||
|
||||
stripe.events.retrieve.restore();
|
||||
});
|
||||
|
||||
it('deletes the customer on Stripe and calls payments.cancelSubscription', async () => {
|
||||
@@ -230,7 +250,7 @@ describe('Stripe - Webhooks', () => {
|
||||
subscriber.purchased.plan.paymentMethod = 'Stripe';
|
||||
await subscriber.save();
|
||||
|
||||
sinon.stub(stripe.events, 'retrieve').resolves({
|
||||
constructEventStub.returns({
|
||||
id: 123,
|
||||
type: eventType,
|
||||
data: {
|
||||
@@ -241,10 +261,10 @@ describe('Stripe - Webhooks', () => {
|
||||
customer: customerId,
|
||||
},
|
||||
},
|
||||
request: null,
|
||||
request: { id: null },
|
||||
});
|
||||
|
||||
await stripePayments.handleWebhooks({ requestBody: {} }, stripe);
|
||||
await stripePayments.handleWebhooks({ body, headers }, stripe);
|
||||
|
||||
expect(stripe.customers.del).to.have.been.calledOnce;
|
||||
expect(stripe.customers.del).to.have.been.calledWith(customerId);
|
||||
@@ -255,9 +275,65 @@ describe('Stripe - Webhooks', () => {
|
||||
expect(cancelSubscriptionOpts.paymentMethod).to.equal('Stripe');
|
||||
expect(Math.round(moment(cancelSubscriptionOpts.nextBill).diff(new Date(), 'days', true))).to.equal(3);
|
||||
expect(cancelSubscriptionOpts.groupId).to.equal(subscriber._id);
|
||||
|
||||
stripe.events.retrieve.restore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkout.session.completed', () => {
|
||||
const eventType = 'checkout.session.completed';
|
||||
let event;
|
||||
let constructEventStub;
|
||||
const session = {};
|
||||
|
||||
beforeEach(() => {
|
||||
session.metadata = {};
|
||||
event = { type: eventType, data: { object: session } };
|
||||
constructEventStub = sandbox.stub(stripe.webhooks, 'constructEvent');
|
||||
constructEventStub.returns(event);
|
||||
|
||||
sandbox.stub(oneTimePayments, 'applyGemPayment').resolves({});
|
||||
sandbox.stub(subscriptions, 'applySubscription').resolves({});
|
||||
sandbox.stub(subscriptions, 'handlePaymentMethodChange').resolves({});
|
||||
});
|
||||
|
||||
it('handles changing an user sub', async () => {
|
||||
session.metadata.type = 'edit-card-user';
|
||||
|
||||
await stripePayments.handleWebhooks({ body, headers }, stripe);
|
||||
|
||||
expect(stripe.webhooks.constructEvent).to.have.been.calledOnce;
|
||||
expect(subscriptions.handlePaymentMethodChange).to.have.been.calledOnce;
|
||||
expect(subscriptions.handlePaymentMethodChange).to.have.been.calledWith(session);
|
||||
});
|
||||
|
||||
it('handles changing a group sub', async () => {
|
||||
session.metadata.type = 'edit-card-group';
|
||||
|
||||
await stripePayments.handleWebhooks({ body, headers }, stripe);
|
||||
|
||||
expect(stripe.webhooks.constructEvent).to.have.been.calledOnce;
|
||||
expect(subscriptions.handlePaymentMethodChange).to.have.been.calledOnce;
|
||||
expect(subscriptions.handlePaymentMethodChange).to.have.been.calledWith(session);
|
||||
});
|
||||
|
||||
it('applies a subscription', async () => {
|
||||
session.metadata.type = 'subscription';
|
||||
|
||||
await stripePayments.handleWebhooks({ body, headers }, stripe);
|
||||
|
||||
expect(stripe.webhooks.constructEvent).to.have.been.calledOnce;
|
||||
expect(subscriptions.applySubscription).to.have.been.calledOnce;
|
||||
expect(subscriptions.applySubscription).to.have.been.calledWith(session);
|
||||
});
|
||||
|
||||
it('handles a one time payment', async () => {
|
||||
session.metadata.type = 'something else';
|
||||
|
||||
await stripePayments.handleWebhooks({ body, headers }, stripe);
|
||||
|
||||
expect(stripe.webhooks.constructEvent).to.have.been.calledOnce;
|
||||
expect(oneTimePayments.applyGemPayment).to.have.been.calledOnce;
|
||||
expect(oneTimePayments.applyGemPayment).to.have.been.calledWith(session);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import apn from 'apn/mock';
|
||||
import apn from '@parse/node-apn/mock';
|
||||
import _ from 'lodash';
|
||||
import nconf from 'nconf';
|
||||
import gcmLib from 'node-gcm'; // works with FCM notifications too
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable camelcase */
|
||||
import { IncomingWebhook } from '@slack/client';
|
||||
import { IncomingWebhook } from '@slack/webhook';
|
||||
import requireAgain from 'require-again';
|
||||
import nconf from 'nconf';
|
||||
import moment from 'moment';
|
||||
@@ -12,7 +12,7 @@ describe('slack', () => {
|
||||
let data;
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox.stub(IncomingWebhook.prototype, 'send');
|
||||
sandbox.stub(IncomingWebhook.prototype, 'send').returns(Promise.resolve());
|
||||
data = {
|
||||
authorEmail: 'author@example.com',
|
||||
flagger: {
|
||||
@@ -112,6 +112,7 @@ describe('slack', () => {
|
||||
|
||||
it('noops if no flagging url is provided', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('SLACK_FLAGGING_URL').returns('');
|
||||
nconf.get.withArgs('IS_TEST').returns(true);
|
||||
sandbox.stub(logger, 'error');
|
||||
const reRequiredSlack = requireAgain('../../../../website/server/libs/slack');
|
||||
|
||||
|
||||
@@ -8,5 +8,10 @@ describe('stringUtils', () => {
|
||||
const matches = getMatchesByWordArray(message, bannedWords);
|
||||
expect(matches.length).to.equal(bannedWords.length);
|
||||
});
|
||||
it('doesn\'t flag names with accented characters', () => {
|
||||
const name = 'TESTPLACEHOLDERSWEARWORDHEREé';
|
||||
const matches = getMatchesByWordArray(name, bannedWords);
|
||||
expect(matches.length).to.equal(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
44
test/api/unit/libs/xmlMarshaller.test.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import * as xmlMarshaller from '../../../../website/server/libs/xmlMarshaller';
|
||||
|
||||
describe('xml marshaller marshalls user data', () => {
|
||||
const minimumUser = {
|
||||
pinnedItems: [],
|
||||
unpinnedItems: [],
|
||||
inbox: {},
|
||||
};
|
||||
|
||||
function userDataWith (fields) {
|
||||
return { ...minimumUser, ...fields };
|
||||
}
|
||||
|
||||
it('maps the newMessages field to have id as a value in a list.', () => {
|
||||
const userData = userDataWith({
|
||||
newMessages: {
|
||||
'283171a5-422c-4991-bc78-95b1b5b51629': {
|
||||
name: 'The Language Hackers',
|
||||
value: true,
|
||||
},
|
||||
'283171a6-422c-4991-bc78-95b1b5b51629': {
|
||||
name: 'The Bug Hackers',
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const xml = xmlMarshaller.marshallUserData(userData);
|
||||
|
||||
expect(xml).to.equal(`<user>
|
||||
<inbox/>
|
||||
<newMessages>
|
||||
<id>283171a5-422c-4991-bc78-95b1b5b51629</id>
|
||||
<name>The Language Hackers</name>
|
||||
<value>true</value>
|
||||
</newMessages>
|
||||
<newMessages>
|
||||
<id>283171a6-422c-4991-bc78-95b1b5b51629</id>
|
||||
<name>The Bug Hackers</name>
|
||||
<value>false</value>
|
||||
</newMessages>
|
||||
</user>`);
|
||||
});
|
||||
});
|
||||
@@ -293,4 +293,90 @@ describe('cron middleware', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Drop Cap A/B Test', async () => {
|
||||
it('enrolls web users', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
req.headers['x-client'] = 'habitica-web';
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, async err => {
|
||||
if (err) return reject(err);
|
||||
user = await User.findById(user._id).exec();
|
||||
expect(user._ABtests.dropCapNotif).to.be.a.string;
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('enables the new notification for 50% of users', async () => {
|
||||
sandbox.stub(Math, 'random').returns(0.5);
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
req.headers['x-client'] = 'habitica-web';
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, async err => {
|
||||
if (err) return reject(err);
|
||||
user = await User.findById(user._id).exec();
|
||||
expect(user._ABtests.dropCapNotif).to.be.equal('drop-cap-notif-enabled');
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('disables the new notification for 50% of users', async () => {
|
||||
sandbox.stub(Math, 'random').returns(0.51);
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
req.headers['x-client'] = 'habitica-web';
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, async err => {
|
||||
if (err) return reject(err);
|
||||
user = await User.findById(user._id).exec();
|
||||
expect(user._ABtests.dropCapNotif).to.be.equal('drop-cap-notif-disabled');
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does not affect subscribers', async () => {
|
||||
sandbox.stub(Math, 'random').returns(0.2);
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
req.headers['x-client'] = 'habitica-web';
|
||||
sandbox.stub(User.prototype, 'isSubscribed').returns(true);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, async err => {
|
||||
if (err) return reject(err);
|
||||
user = await User.findById(user._id).exec();
|
||||
expect(user._ABtests.dropCapNotif).to.not.exist;
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does not affect mobile users', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
req.headers['x-client'] = 'habitica-ios';
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, async err => {
|
||||
if (err) return reject(err);
|
||||
user = await User.findById(user._id).exec();
|
||||
expect(user._ABtests.dropCapNotif).to.not.exist;
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
generateNext,
|
||||
} from '../../../helpers/api-unit.helper';
|
||||
import i18n from '../../../../website/common/script/i18n';
|
||||
import { ensureAdmin, ensureSudo } from '../../../../website/server/middlewares/ensureAccessRight';
|
||||
import { ensureAdmin, ensureSudo, ensureNewsPoster } from '../../../../website/server/middlewares/ensureAccessRight';
|
||||
import { NotAuthorized } from '../../../../website/server/libs/errors';
|
||||
import apiError from '../../../../website/server/libs/apiError';
|
||||
|
||||
@@ -40,6 +40,27 @@ describe('ensure access middlewares', () => {
|
||||
});
|
||||
});
|
||||
|
||||
context('ensure newsPoster', () => {
|
||||
it('returns not authorized when user is not a newsPoster', () => {
|
||||
res.locals = { user: { contributor: { newsPoster: false } } };
|
||||
|
||||
ensureNewsPoster(req, res, next);
|
||||
|
||||
const calledWith = next.getCall(0).args;
|
||||
expect(calledWith[0].message).to.equal(apiError('noNewsPosterAccess'));
|
||||
expect(calledWith[0] instanceof NotAuthorized).to.equal(true);
|
||||
});
|
||||
|
||||
it('passes when user is a newsPoster', () => {
|
||||
res.locals = { user: { contributor: { newsPoster: true } } };
|
||||
|
||||
ensureNewsPoster(req, res, next);
|
||||
|
||||
expect(next).to.be.calledOnce;
|
||||
expect(next.args[0]).to.be.empty;
|
||||
});
|
||||
});
|
||||
|
||||
context('ensure sudo', () => {
|
||||
it('returns not authorized when user is not a sudo user', () => {
|
||||
res.locals = { user: { contributor: { sudo: false } } };
|
||||
|
||||
138
test/api/unit/models/newsPost.js
Normal file
@@ -0,0 +1,138 @@
|
||||
import { v4 } from 'uuid';
|
||||
import { model as NewsPost, refreshNewsPost } from '../../../../website/server/models/newsPost';
|
||||
import { sleep } from '../../../helpers/api-unit.helper';
|
||||
|
||||
describe('NewsPost Model', () => {
|
||||
const publishDate = Number(new Date());
|
||||
|
||||
// NOTE publishDate is manually increased by +500 for each test
|
||||
// to make sure it's always in the future from the previous one
|
||||
// bevause NewsPost.lastNewsPost() is not reset between tests.
|
||||
// And without a more recent publishDate it wouldn't update
|
||||
|
||||
it('#lastNewsPost', () => {
|
||||
const lastPost = { _id: v4(), publishDate, published: true };
|
||||
NewsPost.updateLastNewsPost(lastPost);
|
||||
expect(NewsPost.lastNewsPost()).to.equal(lastPost);
|
||||
});
|
||||
|
||||
it('#getLastPostFromDatabase', async () => {
|
||||
const expectedId = v4();
|
||||
|
||||
await NewsPost.create([
|
||||
// more recent but not published
|
||||
{
|
||||
_id: v4(),
|
||||
publishDate: new Date(publishDate + 50),
|
||||
author: v4(),
|
||||
published: false,
|
||||
title: 'Title',
|
||||
credits: 'credits',
|
||||
text: 'text',
|
||||
},
|
||||
// expected
|
||||
{
|
||||
_id: expectedId,
|
||||
publishDate,
|
||||
author: v4(),
|
||||
published: true,
|
||||
title: 'Title',
|
||||
credits: 'credits',
|
||||
text: 'text',
|
||||
},
|
||||
// published but less recent
|
||||
{
|
||||
_id: v4(),
|
||||
publishDate: new Date(Number(publishDate) - 50),
|
||||
author: v4(),
|
||||
published: true,
|
||||
title: 'Title',
|
||||
credits: 'credits',
|
||||
text: 'text',
|
||||
},
|
||||
]);
|
||||
|
||||
const fetched = await NewsPost.getLastPostFromDatabase();
|
||||
expect(fetched._id).to.equal(expectedId);
|
||||
});
|
||||
|
||||
context('#updateLastNewsPost', () => {
|
||||
it('updates the post if new one is more recent and published', () => {
|
||||
const previousPost = {
|
||||
_id: v4(),
|
||||
publishDate: new Date(publishDate + 100),
|
||||
published: true,
|
||||
};
|
||||
NewsPost.updateLastNewsPost(previousPost);
|
||||
const newPost = {
|
||||
_id: v4(),
|
||||
publishDate: new Date(publishDate + 150),
|
||||
published: true,
|
||||
};
|
||||
NewsPost.updateLastNewsPost(newPost);
|
||||
expect(NewsPost.lastNewsPost()._id).to.equal(newPost._id);
|
||||
});
|
||||
|
||||
it('does not update the post if new one is from the past', () => {
|
||||
const previousPost = new NewsPost({
|
||||
_id: v4(), publishDate: new Date(publishDate + 200), published: true,
|
||||
});
|
||||
NewsPost.updateLastNewsPost(previousPost);
|
||||
const newPost = new NewsPost({
|
||||
_id: v4(), publishDate: new Date(publishDate + 175), published: true,
|
||||
});
|
||||
NewsPost.updateLastNewsPost(newPost);
|
||||
expect(NewsPost.lastNewsPost()._id).to.equal(previousPost._id);
|
||||
});
|
||||
|
||||
it('does not update the post if new one is not published', () => {
|
||||
const previousPost = new NewsPost({
|
||||
_id: v4(), publishDate: new Date(publishDate + 250), published: true,
|
||||
});
|
||||
NewsPost.updateLastNewsPost(previousPost);
|
||||
const newPost = new NewsPost({
|
||||
_id: v4(), publishDate: new Date(publishDate + 300), published: false,
|
||||
});
|
||||
NewsPost.updateLastNewsPost(newPost);
|
||||
expect(NewsPost.lastNewsPost()._id).to.equal(previousPost._id);
|
||||
});
|
||||
});
|
||||
|
||||
context('refreshes NewsPost', () => {
|
||||
let intervalId;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Delete all existing posts from the database
|
||||
await NewsPost.remove();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (intervalId) clearInterval(intervalId);
|
||||
});
|
||||
|
||||
it('refreshes the last post at a specific interval', async () => {
|
||||
await sleep(0.1); // wait 100ms to make sure all previous posts are in the past
|
||||
const previousPost = {
|
||||
_id: v4(), publishDate: new Date(), published: true,
|
||||
};
|
||||
NewsPost.updateLastNewsPost(previousPost);
|
||||
intervalId = refreshNewsPost(50); // refreshes every 50ms
|
||||
|
||||
await sleep(0.1); // wait 100ms to make sure the new post has a more recent publishDate
|
||||
const newPost = await NewsPost.create({
|
||||
_id: v4(),
|
||||
publishDate: new Date(),
|
||||
author: v4(),
|
||||
published: true,
|
||||
title: 'Title',
|
||||
credits: 'credits',
|
||||
text: 'text',
|
||||
});
|
||||
|
||||
expect(NewsPost.lastNewsPost()._id).to.equal(previousPost._id);
|
||||
await sleep(0.15); // wait 150ms
|
||||
|
||||
expect(NewsPost.lastNewsPost()._id).to.equal(newPost._id);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,87 +1,90 @@
|
||||
import moment from 'moment';
|
||||
import { model as User } from '../../../../website/server/models/user';
|
||||
import { model as NewsPost } from '../../../../website/server/models/newsPost';
|
||||
import { model as Group } from '../../../../website/server/models/group';
|
||||
import common from '../../../../website/common';
|
||||
|
||||
describe('User Model', () => {
|
||||
it('keeps user._tmp when calling .toJSON', () => {
|
||||
const user = new User({
|
||||
auth: {
|
||||
local: {
|
||||
username: 'username',
|
||||
lowerCaseUsername: 'username',
|
||||
email: 'email@email.email',
|
||||
salt: 'salt',
|
||||
hashed_password: 'hashed_password', // eslint-disable-line camelcase
|
||||
describe('.toJSON()', () => {
|
||||
it('keeps user._tmp when calling .toJSON', () => {
|
||||
const user = new User({
|
||||
auth: {
|
||||
local: {
|
||||
username: 'username',
|
||||
lowerCaseUsername: 'username',
|
||||
email: 'email@email.email',
|
||||
salt: 'salt',
|
||||
hashed_password: 'hashed_password', // eslint-disable-line camelcase
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
user._tmp = { ok: true };
|
||||
user._nonTmp = { ok: true };
|
||||
|
||||
expect(user._tmp).to.eql({ ok: true });
|
||||
expect(user._nonTmp).to.eql({ ok: true });
|
||||
|
||||
const toObject = user.toObject();
|
||||
const toJSON = user.toJSON();
|
||||
|
||||
expect(toObject).to.not.have.keys('_tmp');
|
||||
expect(toObject).to.not.have.keys('_nonTmp');
|
||||
|
||||
expect(toJSON).to.have.any.key('_tmp');
|
||||
expect(toJSON._tmp).to.eql({ ok: true });
|
||||
expect(toJSON).to.not.have.keys('_nonTmp');
|
||||
});
|
||||
|
||||
user._tmp = { ok: true };
|
||||
user._nonTmp = { ok: true };
|
||||
it('can add computed stats to a JSONified user object', () => {
|
||||
const user = new User();
|
||||
const userToJSON = user.toJSON();
|
||||
|
||||
expect(user._tmp).to.eql({ ok: true });
|
||||
expect(user._nonTmp).to.eql({ ok: true });
|
||||
expect(userToJSON.stats.maxMP).to.not.exist;
|
||||
expect(userToJSON.stats.maxHealth).to.not.exist;
|
||||
expect(userToJSON.stats.toNextLevel).to.not.exist;
|
||||
|
||||
const toObject = user.toObject();
|
||||
const toJSON = user.toJSON();
|
||||
User.addComputedStatsToJSONObj(userToJSON.stats, userToJSON);
|
||||
|
||||
expect(toObject).to.not.have.keys('_tmp');
|
||||
expect(toObject).to.not.have.keys('_nonTmp');
|
||||
expect(userToJSON.stats.maxMP).to.exist;
|
||||
expect(userToJSON.stats.maxHealth).to.equal(common.maxHealth);
|
||||
expect(userToJSON.stats.toNextLevel).to.equal(common.tnl(user.stats.lvl));
|
||||
});
|
||||
|
||||
expect(toJSON).to.have.any.key('_tmp');
|
||||
expect(toJSON._tmp).to.eql({ ok: true });
|
||||
expect(toJSON).to.not.have.keys('_nonTmp');
|
||||
});
|
||||
it('can transform user object without mongoose helpers', async () => {
|
||||
const user = new User();
|
||||
await user.save();
|
||||
const userToJSON = await User.findById(user._id).lean().exec();
|
||||
|
||||
it('can add computed stats to a JSONified user object', () => {
|
||||
const user = new User();
|
||||
const userToJSON = user.toJSON();
|
||||
expect(userToJSON.stats.maxMP).to.not.exist;
|
||||
expect(userToJSON.stats.maxHealth).to.not.exist;
|
||||
expect(userToJSON.stats.toNextLevel).to.not.exist;
|
||||
expect(userToJSON.id).to.not.exist;
|
||||
|
||||
expect(userToJSON.stats.maxMP).to.not.exist;
|
||||
expect(userToJSON.stats.maxHealth).to.not.exist;
|
||||
expect(userToJSON.stats.toNextLevel).to.not.exist;
|
||||
User.transformJSONUser(userToJSON);
|
||||
|
||||
User.addComputedStatsToJSONObj(userToJSON.stats, userToJSON);
|
||||
expect(userToJSON.id).to.equal(userToJSON._id);
|
||||
expect(userToJSON.stats.maxMP).to.not.exist;
|
||||
expect(userToJSON.stats.maxHealth).to.not.exist;
|
||||
expect(userToJSON.stats.toNextLevel).to.not.exist;
|
||||
});
|
||||
|
||||
expect(userToJSON.stats.maxMP).to.exist;
|
||||
expect(userToJSON.stats.maxHealth).to.equal(common.maxHealth);
|
||||
expect(userToJSON.stats.toNextLevel).to.equal(common.tnl(user.stats.lvl));
|
||||
});
|
||||
it('can transform user object without mongoose helpers (including computed stats)', async () => {
|
||||
const user = new User();
|
||||
await user.save();
|
||||
const userToJSON = await User.findById(user._id).lean().exec();
|
||||
|
||||
it('can transform user object without mongoose helpers', async () => {
|
||||
const user = new User();
|
||||
await user.save();
|
||||
const userToJSON = await User.findById(user._id).lean().exec();
|
||||
expect(userToJSON.stats.maxMP).to.not.exist;
|
||||
expect(userToJSON.stats.maxHealth).to.not.exist;
|
||||
expect(userToJSON.stats.toNextLevel).to.not.exist;
|
||||
|
||||
expect(userToJSON.stats.maxMP).to.not.exist;
|
||||
expect(userToJSON.stats.maxHealth).to.not.exist;
|
||||
expect(userToJSON.stats.toNextLevel).to.not.exist;
|
||||
expect(userToJSON.id).to.not.exist;
|
||||
User.transformJSONUser(userToJSON, true);
|
||||
|
||||
User.transformJSONUser(userToJSON);
|
||||
|
||||
expect(userToJSON.id).to.equal(userToJSON._id);
|
||||
expect(userToJSON.stats.maxMP).to.not.exist;
|
||||
expect(userToJSON.stats.maxHealth).to.not.exist;
|
||||
expect(userToJSON.stats.toNextLevel).to.not.exist;
|
||||
});
|
||||
|
||||
it('can transform user object without mongoose helpers (including computed stats)', async () => {
|
||||
const user = new User();
|
||||
await user.save();
|
||||
const userToJSON = await User.findById(user._id).lean().exec();
|
||||
|
||||
expect(userToJSON.stats.maxMP).to.not.exist;
|
||||
expect(userToJSON.stats.maxHealth).to.not.exist;
|
||||
expect(userToJSON.stats.toNextLevel).to.not.exist;
|
||||
|
||||
User.transformJSONUser(userToJSON, true);
|
||||
|
||||
expect(userToJSON.id).to.equal(userToJSON._id);
|
||||
expect(userToJSON.stats.maxMP).to.exist;
|
||||
expect(userToJSON.stats.maxHealth).to.equal(common.maxHealth);
|
||||
expect(userToJSON.stats.toNextLevel).to.equal(common.tnl(user.stats.lvl));
|
||||
expect(userToJSON.id).to.equal(userToJSON._id);
|
||||
expect(userToJSON.stats.maxMP).to.exist;
|
||||
expect(userToJSON.stats.maxHealth).to.equal(common.maxHealth);
|
||||
expect(userToJSON.stats.toNextLevel).to.equal(common.tnl(user.stats.lvl));
|
||||
});
|
||||
});
|
||||
|
||||
context('achievements', () => {
|
||||
@@ -589,6 +592,50 @@ describe('User Model', () => {
|
||||
});
|
||||
|
||||
context('pre-save hook', () => {
|
||||
it('enrolls users that signup through web in the Drop Cap AB test', async () => {
|
||||
let user = new User();
|
||||
user.registeredThrough = 'habitica-web';
|
||||
user = await user.save();
|
||||
expect(user._ABtests.dropCapNotif).to.exist;
|
||||
});
|
||||
|
||||
it('does not enroll users that signup through modal in the Drop Cap AB test', async () => {
|
||||
let user = new User();
|
||||
user.registeredThrough = 'habitica-ios';
|
||||
user = await user.save();
|
||||
expect(user._ABtests.dropCapNotif).to.not.exist;
|
||||
});
|
||||
|
||||
it('marks the last news post as read for new users', async () => {
|
||||
const lastNewsPost = { _id: '1' };
|
||||
sandbox.stub(NewsPost, 'lastNewsPost').returns(lastNewsPost);
|
||||
|
||||
let user = new User();
|
||||
expect(user.isNew).to.equal(true);
|
||||
user = await user.save();
|
||||
|
||||
expect(user.checkNewStuff()).to.equal(false);
|
||||
expect(user.toJSON().flags.newStuff).to.equal(false);
|
||||
expect(user.flags.lastNewStuffRead).to.equal(lastNewsPost._id);
|
||||
});
|
||||
|
||||
it('does not mark the last news post as read for existing users', async () => {
|
||||
const lastNewsPost = { _id: '1' };
|
||||
const lastNewsPostStub = sandbox.stub(NewsPost, 'lastNewsPost');
|
||||
lastNewsPostStub.returns(lastNewsPost);
|
||||
|
||||
let user = new User();
|
||||
user = await user.save();
|
||||
|
||||
expect(user.isNew).to.equal(false);
|
||||
user.profile.name = 'new name';
|
||||
|
||||
lastNewsPostStub.returns({ _id: '2' });
|
||||
user = await user.save();
|
||||
|
||||
expect(user.flags.lastNewStuffRead).to.equal(lastNewsPost._id); // not _id: 2
|
||||
});
|
||||
|
||||
it('does not try to award achievements when achievements or items not selected in query', async () => {
|
||||
let user = new User();
|
||||
user = await user.save(); // necessary for user.isSelected to work correctly
|
||||
@@ -827,4 +874,46 @@ describe('User Model', () => {
|
||||
expect(daysMissed).to.eql(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('isNewsPoster', async () => {
|
||||
const user = new User();
|
||||
await user.save();
|
||||
|
||||
expect(user.isNewsPoster()).to.equal(false);
|
||||
|
||||
user.contributor.newsPoster = true;
|
||||
expect(user.isNewsPoster()).to.equal(true);
|
||||
});
|
||||
|
||||
describe('checkNewStuff', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(() => {
|
||||
user = new User();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('no last news post', () => {
|
||||
sandbox.stub(NewsPost, 'lastNewsPost').returns(null);
|
||||
expect(user.checkNewStuff()).to.equal(false);
|
||||
expect(user.toJSON().flags.newStuff).to.equal(false);
|
||||
});
|
||||
|
||||
it('last news post read', () => {
|
||||
sandbox.stub(NewsPost, 'lastNewsPost').returns({ _id: '123' });
|
||||
user.flags.lastNewStuffRead = '123';
|
||||
expect(user.checkNewStuff()).to.equal(false);
|
||||
expect(user.toJSON().flags.newStuff).to.equal(false);
|
||||
});
|
||||
|
||||
it('last news post not read', () => {
|
||||
sandbox.stub(NewsPost, 'lastNewsPost').returns({ _id: '123' });
|
||||
user.flags.lastNewStuffRead = '124';
|
||||
expect(user.checkNewStuff()).to.equal(true);
|
||||
expect(user.toJSON().flags.newStuff).to.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import {
|
||||
generateUser,
|
||||
requester,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { mockAnalyticsService as analytics } from '../../../../../website/server/libs/analyticsService';
|
||||
|
||||
describe('POST /analytics/track/:eventName', () => {
|
||||
it('calls res.analytics', async () => {
|
||||
const user = await generateUser();
|
||||
sandbox.spy(analytics, 'track');
|
||||
|
||||
const requestWithHeaders = requester(user, { 'x-client': 'habitica-web' });
|
||||
await requestWithHeaders.post('/analytics/track/eventName', { data: 'example' }, { 'x-client': 'habitica-web' });
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
expect(analytics.track).to.be.calledWith('eventName', sandbox.match({ data: 'example' }));
|
||||
|
||||
sandbox.restore();
|
||||
});
|
||||
});
|
||||
@@ -117,26 +117,7 @@ describe('GET /challenges/:challengeId/members', () => {
|
||||
expect(res[0].profile).to.have.all.keys(['name']);
|
||||
});
|
||||
|
||||
it('returns only first 30 members if req.query.includeAllMembers is not true and req.query.limit is undefined', async () => {
|
||||
const group = await generateGroup(user, { type: 'party', name: generateUUID() });
|
||||
const challenge = await generateChallenge(user, group);
|
||||
await user.post(`/challenges/${challenge._id}/join`);
|
||||
|
||||
const usersToGenerate = [];
|
||||
for (let i = 0; i < 31; i += 1) {
|
||||
usersToGenerate.push(generateUser({ challenges: [challenge._id] }));
|
||||
}
|
||||
await Promise.all(usersToGenerate);
|
||||
|
||||
const res = await user.get(`/challenges/${challenge._id}/members?includeAllMembers=not-true`);
|
||||
expect(res.length).to.equal(30);
|
||||
res.forEach(member => {
|
||||
expect(member).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile']);
|
||||
expect(member.profile).to.have.all.keys(['name']);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns only first 30 members if req.query.includeAllMembers is not defined and req.query.limit is undefined', async () => {
|
||||
it('returns only first 30 members if req.query.limit is undefined', async () => {
|
||||
const group = await generateGroup(user, { type: 'party', name: generateUUID() });
|
||||
const challenge = await generateChallenge(user, group);
|
||||
await user.post(`/challenges/${challenge._id}/join`);
|
||||
@@ -217,25 +198,6 @@ describe('GET /challenges/:challengeId/members', () => {
|
||||
});
|
||||
}).timeout(30000);
|
||||
|
||||
it('returns all members if req.query.includeAllMembers is true', async () => {
|
||||
const group = await generateGroup(user, { type: 'party', name: generateUUID() });
|
||||
const challenge = await generateChallenge(user, group);
|
||||
await user.post(`/challenges/${challenge._id}/join`);
|
||||
|
||||
const usersToGenerate = [];
|
||||
for (let i = 0; i < 31; i += 1) {
|
||||
usersToGenerate.push(generateUser({ challenges: [challenge._id] }));
|
||||
}
|
||||
await Promise.all(usersToGenerate);
|
||||
|
||||
const res = await user.get(`/challenges/${challenge._id}/members?includeAllMembers=true`);
|
||||
expect(res.length).to.equal(32);
|
||||
res.forEach(member => {
|
||||
expect(member).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile']);
|
||||
expect(member.profile).to.have.all.keys(['name']);
|
||||
});
|
||||
});
|
||||
|
||||
it('supports using req.query.lastId to get more members', async function test () {
|
||||
this.timeout(30000); // @TODO: times out after 8 seconds
|
||||
const group = await generateGroup(user, { type: 'party', name: generateUUID() });
|
||||
@@ -259,6 +221,34 @@ describe('GET /challenges/:challengeId/members', () => {
|
||||
expect(resIds).to.eql(expectedIds.sort());
|
||||
});
|
||||
|
||||
it('supports using req.query.includeTasks in order to add challenge-related tasks of all members', async () => {
|
||||
const group = await generateGroup(user, { type: 'party', name: generateUUID() });
|
||||
const challenge = await generateChallenge(user, group);
|
||||
await user.post(`/challenges/${challenge._id}/join`);
|
||||
|
||||
const usersToGenerate = [];
|
||||
for (let i = 0; i < 8; i += 1) {
|
||||
usersToGenerate.push(generateUser({ challenges: [challenge._id] }));
|
||||
}
|
||||
await Promise.all(usersToGenerate);
|
||||
await user.post(`/tasks/challenge/${challenge._id}`, [{ type: 'habit', text: 'Some task' }]);
|
||||
await user.post(`/tasks/challenge/${challenge._id}`, [{ type: 'daily', text: 'Some different task' }]);
|
||||
|
||||
const res = await user.get(`/challenges/${challenge._id}/members?includeTasks=true`);
|
||||
expect(res.length).to.equal(9);
|
||||
res.forEach(member => {
|
||||
expect(member).to.have.property('tasks');
|
||||
expect(member.tasks).to.be.an('array');
|
||||
expect(member.tasks).to.have.lengthOf(2);
|
||||
member.tasks.forEach(task => {
|
||||
expect(task).to.include.all.keys(['type', 'value', 'priority', 'text', '_id', 'userId']);
|
||||
expect(task).to.not.have.any.keys(['tags', 'checklist']);
|
||||
expect(task.challenge.id).to.be.equal(challenge._id);
|
||||
expect(task.userId).to.be.equal(member._id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('supports using req.query.search to get search members', async () => {
|
||||
const group = await generateGroup(user, { type: 'party', name: generateUUID() });
|
||||
const challenge = await generateChallenge(user, group);
|
||||
|
||||
@@ -116,7 +116,6 @@ describe('GET /challenges/:challengeId/members/:memberId', () => {
|
||||
}]);
|
||||
|
||||
const memberProgress = await user.get(`/challenges/${challenge._id}/members/${user._id}`);
|
||||
expect(memberProgress.tasks[0]).not.to.have.key('tags');
|
||||
expect(memberProgress.tasks[0].checklist).to.eql([]);
|
||||
expect(memberProgress.tasks[0]).to.not.have.any.keys(['tags', 'checklist']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -56,7 +56,7 @@ describe('GET challenges/user', () => {
|
||||
});
|
||||
context('all challenges', () => {
|
||||
it('should return challenges user has joined', async () => {
|
||||
const challenges = await nonMember.get('/challenges/user');
|
||||
const challenges = await nonMember.get('/challenges/user?page=0');
|
||||
|
||||
const foundChallenge = _.find(challenges, { _id: challenge._id });
|
||||
expect(foundChallenge).to.exist;
|
||||
@@ -65,14 +65,14 @@ describe('GET challenges/user', () => {
|
||||
});
|
||||
|
||||
it('should not return challenges a non-member has not joined', async () => {
|
||||
const challenges = await nonMember.get('/challenges/user');
|
||||
const challenges = await nonMember.get('/challenges/user?page=0');
|
||||
|
||||
const foundChallenge2 = _.find(challenges, { _id: challenge2._id });
|
||||
expect(foundChallenge2).to.not.exist;
|
||||
});
|
||||
|
||||
it('should return challenges user has created', async () => {
|
||||
const challenges = await user.get('/challenges/user');
|
||||
const challenges = await user.get('/challenges/user?page=0');
|
||||
|
||||
const foundChallenge1 = _.find(challenges, { _id: challenge._id });
|
||||
expect(foundChallenge1).to.exist;
|
||||
@@ -85,7 +85,7 @@ describe('GET challenges/user', () => {
|
||||
});
|
||||
|
||||
it('should return challenges in user\'s group', async () => {
|
||||
const challenges = await member.get('/challenges/user');
|
||||
const challenges = await member.get('/challenges/user?page=0');
|
||||
|
||||
const foundChallenge1 = _.find(challenges, { _id: challenge._id });
|
||||
expect(foundChallenge1).to.exist;
|
||||
@@ -98,7 +98,7 @@ describe('GET challenges/user', () => {
|
||||
});
|
||||
|
||||
it('should return newest challenges first', async () => {
|
||||
let challenges = await user.get('/challenges/user');
|
||||
let challenges = await user.get('/challenges/user?page=0');
|
||||
|
||||
let foundChallengeIndex = _.findIndex(challenges, { _id: challenge2._id });
|
||||
expect(foundChallengeIndex).to.eql(0);
|
||||
@@ -106,7 +106,7 @@ describe('GET challenges/user', () => {
|
||||
const newChallenge = await generateChallenge(user, publicGuild);
|
||||
await user.post(`/challenges/${newChallenge._id}/join`);
|
||||
|
||||
challenges = await user.get('/challenges/user');
|
||||
challenges = await user.get('/challenges/user?page=0');
|
||||
|
||||
foundChallengeIndex = _.findIndex(challenges, { _id: newChallenge._id });
|
||||
expect(foundChallengeIndex).to.eql(0);
|
||||
@@ -125,7 +125,7 @@ describe('GET challenges/user', () => {
|
||||
const privateChallenge = await generateChallenge(groupLeader, group);
|
||||
await groupLeader.post(`/challenges/${privateChallenge._id}/join`);
|
||||
|
||||
const challenges = await nonMember.get('/challenges/user');
|
||||
const challenges = await nonMember.get('/challenges/user?page=0');
|
||||
|
||||
const foundChallenge = _.find(challenges, { _id: privateChallenge._id });
|
||||
expect(foundChallenge).to.not.exist;
|
||||
@@ -149,7 +149,7 @@ describe('GET challenges/user', () => {
|
||||
});
|
||||
await groupLeader.post(`/challenges/${privateChallenge._id}/join`);
|
||||
|
||||
const challenges = await nonMember.get('/challenges/user?categories=academics&owned=not_owned');
|
||||
const challenges = await nonMember.get('/challenges/user?page=0&categories=academics&owned=not_owned');
|
||||
|
||||
const foundChallenge = _.find(challenges, { _id: privateChallenge._id });
|
||||
expect(foundChallenge).to.not.exist;
|
||||
@@ -158,7 +158,7 @@ describe('GET challenges/user', () => {
|
||||
|
||||
context('my challenges', () => {
|
||||
it('should return challenges user has joined', async () => {
|
||||
const challenges = await nonMember.get(`/challenges/user?member=${true}`);
|
||||
const challenges = await nonMember.get(`/challenges/user?page=0&member=${true}`);
|
||||
|
||||
const foundChallenge = _.find(challenges, { _id: challenge._id });
|
||||
expect(foundChallenge).to.exist;
|
||||
@@ -167,7 +167,7 @@ describe('GET challenges/user', () => {
|
||||
});
|
||||
|
||||
it('should return challenges user has created', async () => {
|
||||
const challenges = await user.get(`/challenges/user?member=${true}`);
|
||||
const challenges = await user.get(`/challenges/user?page=0&member=${true}`);
|
||||
|
||||
const foundChallenge1 = _.find(challenges, { _id: challenge._id });
|
||||
expect(foundChallenge1).to.exist;
|
||||
@@ -180,7 +180,7 @@ describe('GET challenges/user', () => {
|
||||
});
|
||||
|
||||
it('should return challenges user has created if filter by owned', async () => {
|
||||
const challenges = await user.get(`/challenges/user?member=${true}&owned=owned`);
|
||||
const challenges = await user.get(`/challenges/user?member=${true}&owned=owned&page=0`);
|
||||
|
||||
const foundChallenge1 = _.find(challenges, { _id: challenge._id });
|
||||
expect(foundChallenge1).to.exist;
|
||||
@@ -193,7 +193,7 @@ describe('GET challenges/user', () => {
|
||||
});
|
||||
|
||||
it('should not return challenges user has created if filter by not owned', async () => {
|
||||
const challenges = await user.get(`/challenges/user?owned=not_owned&member=${true}`);
|
||||
const challenges = await user.get(`/challenges/user?page=0&owned=not_owned&member=${true}`);
|
||||
|
||||
const foundChallenge1 = _.find(challenges, { _id: challenge._id });
|
||||
expect(foundChallenge1).to.not.exist;
|
||||
@@ -202,7 +202,7 @@ describe('GET challenges/user', () => {
|
||||
});
|
||||
|
||||
it('should not return challenges in user groups', async () => {
|
||||
const challenges = await member.get(`/challenges/user?member=${true}`);
|
||||
const challenges = await member.get(`/challenges/user?page=0&member=${true}`);
|
||||
|
||||
const foundChallenge1 = _.find(challenges, { _id: challenge._id });
|
||||
expect(foundChallenge1).to.not.exist;
|
||||
@@ -253,7 +253,7 @@ describe('GET challenges/user', () => {
|
||||
});
|
||||
|
||||
it('should return official challenges first', async () => {
|
||||
const challenges = await user.get('/challenges/user');
|
||||
const challenges = await user.get('/challenges/user?page=0');
|
||||
|
||||
const foundChallengeIndex = _.findIndex(challenges, { _id: officialChallenge._id });
|
||||
expect(foundChallengeIndex).to.eql(0);
|
||||
@@ -274,7 +274,7 @@ describe('GET challenges/user', () => {
|
||||
const newChallenge = await generateChallenge(user, publicGuild);
|
||||
await user.post(`/challenges/${newChallenge._id}/join`);
|
||||
|
||||
challenges = await user.get('/challenges/user');
|
||||
challenges = await user.get('/challenges/user?page=0');
|
||||
|
||||
const foundChallengeIndex = _.findIndex(challenges, { _id: newChallenge._id });
|
||||
expect(foundChallengeIndex).to.eql(1);
|
||||
@@ -314,18 +314,12 @@ describe('GET challenges/user', () => {
|
||||
it('returns public guilds filtered by category', async () => {
|
||||
const categoryChallenge = await generateChallenge(user, guild, { categories });
|
||||
await user.post(`/challenges/${categoryChallenge._id}/join`);
|
||||
const challenges = await user.get(`/challenges/user?categories=${categories[0].slug}`);
|
||||
const challenges = await user.get(`/challenges/user?page=0&categories=${categories[0].slug}`);
|
||||
|
||||
expect(challenges[0]._id).to.eql(categoryChallenge._id);
|
||||
expect(challenges.length).to.eql(1);
|
||||
});
|
||||
|
||||
it('does not page challenges if page parameter is absent', async () => {
|
||||
const challenges = await user.get('/challenges/user');
|
||||
|
||||
expect(challenges.length).to.be.above(11);
|
||||
});
|
||||
|
||||
it('paginates challenges', async () => {
|
||||
const challenges = await user.get('/challenges/user?page=0');
|
||||
const challengesPaged = await user.get('/challenges/user?page=1&owned=owned');
|
||||
@@ -335,7 +329,7 @@ describe('GET challenges/user', () => {
|
||||
});
|
||||
|
||||
it('filters by owned', async () => {
|
||||
const challenges = await member.get('/challenges/user?owned=owned');
|
||||
const challenges = await member.get('/challenges/user?page=0&owned=owned');
|
||||
|
||||
expect(challenges.length).to.eql(0);
|
||||
});
|
||||
|
||||
@@ -103,7 +103,15 @@ describe('POST /challenges/:challengeId/winner/:winnerId', () => {
|
||||
await expect(winningUser.sync()).to.eventually.have.nested.property('achievements.challenges').to.include(challenge.name);
|
||||
// 2 because winningUser just joined the challenge, which now awards an achievement
|
||||
expect(winningUser.notifications.length).to.equal(2);
|
||||
expect(winningUser.notifications[1].type).to.equal('WON_CHALLENGE');
|
||||
|
||||
const notif = winningUser.notifications[1];
|
||||
expect(notif.type).to.equal('WON_CHALLENGE');
|
||||
expect(notif.data).to.eql({
|
||||
id: challenge._id,
|
||||
name: challenge.name,
|
||||
prize: challenge.prize,
|
||||
leader: challenge.leader,
|
||||
});
|
||||
});
|
||||
|
||||
it('gives winner gems as reward', async () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { find } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import nconf from 'nconf';
|
||||
import { IncomingWebhook } from '@slack/client';
|
||||
import { IncomingWebhook } from '@slack/webhook';
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
@@ -20,7 +20,7 @@ describe('POST /chat/:chatId/flag', () => {
|
||||
admin = await generateUser({ balance: 1, 'contributor.admin': true });
|
||||
anotherUser = await generateUser({ 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate() });
|
||||
newUser = await generateUser({ 'auth.timestamps.created': moment().subtract(1, 'days').toDate() });
|
||||
sandbox.stub(IncomingWebhook.prototype, 'send');
|
||||
sandbox.stub(IncomingWebhook.prototype, 'send').returns(Promise.resolve());
|
||||
|
||||
group = await user.post('/groups', {
|
||||
name: 'Test Guild',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IncomingWebhook } from '@slack/client';
|
||||
import { IncomingWebhook } from '@slack/webhook';
|
||||
import nconf from 'nconf';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import {
|
||||
@@ -133,7 +133,7 @@ describe('POST /chat', () => {
|
||||
describe('shadow-mute user', () => {
|
||||
beforeEach(() => {
|
||||
sandbox.spy(email, 'sendTxn');
|
||||
sandbox.stub(IncomingWebhook.prototype, 'send');
|
||||
sandbox.stub(IncomingWebhook.prototype, 'send').returns(Promise.resolve());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -355,7 +355,7 @@ describe('POST /chat', () => {
|
||||
context('banned slur', () => {
|
||||
beforeEach(() => {
|
||||
sandbox.spy(email, 'sendTxn');
|
||||
sandbox.stub(IncomingWebhook.prototype, 'send');
|
||||
sandbox.stub(IncomingWebhook.prototype, 'send').returns(Promise.resolve());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -251,6 +251,29 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
|
||||
expect(party.quest.members[partyMember._id]).to.not.exist;
|
||||
});
|
||||
|
||||
it('prevents user from being removed if they are the quest owner', async () => {
|
||||
const petQuest = 'whale';
|
||||
await partyMember.update({
|
||||
[`items.quests.${petQuest}`]: 1,
|
||||
});
|
||||
|
||||
await partyMember.post(`/groups/${party._id}/quests/invite/${petQuest}`);
|
||||
await partyLeader.post(`/groups/${party._id}/quests/accept`);
|
||||
|
||||
await party.sync();
|
||||
|
||||
expect(party.quest.members[partyLeader._id]).to.be.true;
|
||||
expect(party.quest.members[partyMember._id]).to.be.true;
|
||||
|
||||
await party.sync();
|
||||
|
||||
expect(leader.post(`/groups/${party._id}/removeMember/${partyMember._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
text: t('cannotRemoveQuestOwner'),
|
||||
});
|
||||
});
|
||||
|
||||
it('sends email to user with rescinded invite', async () => {
|
||||
await partyLeader.post(`/groups/${party._id}/removeMember/${partyInvitedUser._id}`);
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
|
||||
describe('GET /news', () => {
|
||||
let api;
|
||||
|
||||
beforeEach(async () => {
|
||||
api = requester();
|
||||
});
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { model as NewsPost } from '../../../../../website/server/models/newsPost';
|
||||
|
||||
describe('POST /news/tell-me-later', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({
|
||||
'flags.newStuff': true,
|
||||
NewsPost.updateLastNewsPost({
|
||||
_id: '1234', publishDate: new Date(), title: 'Title', published: true,
|
||||
});
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('marks new stuff as read and adds notification', async () => {
|
||||
expect(user.flags.newStuff).to.equal(true);
|
||||
const initialNotifications = user.notifications.length;
|
||||
|
||||
await user.post('/news/tell-me-later');
|
||||
await user.sync();
|
||||
|
||||
expect(user.flags.newStuff).to.equal(false);
|
||||
expect(user.flags.lastNewStuffRead).to.equal('1234');
|
||||
// fetching the user because newStuff is a computed property
|
||||
expect((await user.get('/user')).flags.newStuff).to.equal(false);
|
||||
expect(user.notifications.length).to.equal(initialNotifications + 1);
|
||||
|
||||
const notification = user.notifications[user.notifications.length - 1];
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
|
||||
import common from '../../../../../../website/common';
|
||||
|
||||
describe('payments - stripe - #createCheckoutSession', () => {
|
||||
const endpoint = '/stripe/checkout-session';
|
||||
let user; const groupId = 'groupId';
|
||||
const gift = {}; const subKey = 'basic_3mo';
|
||||
const gemsBlock = '21gems'; const coupon = 'coupon';
|
||||
let stripeCreateCheckoutSessionStub; const sessionId = 'sessionId';
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
stripeCreateCheckoutSessionStub = sinon
|
||||
.stub(stripePayments, 'createCheckoutSession')
|
||||
.resolves({ id: sessionId });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
stripePayments.createCheckoutSession.restore();
|
||||
});
|
||||
|
||||
it('works', async () => {
|
||||
const res = await user.post(endpoint, {
|
||||
groupId,
|
||||
gift,
|
||||
sub: subKey,
|
||||
gemsBlock,
|
||||
coupon,
|
||||
});
|
||||
|
||||
expect(res.sessionId).to.equal(sessionId);
|
||||
|
||||
expect(stripeCreateCheckoutSessionStub).to.be.calledOnce;
|
||||
expect(stripeCreateCheckoutSessionStub.args[0][0].user._id).to.eql(user._id);
|
||||
expect(stripeCreateCheckoutSessionStub.args[0][0].groupId).to.eql(groupId);
|
||||
expect(stripeCreateCheckoutSessionStub.args[0][0].gift).to.eql(gift);
|
||||
expect(stripeCreateCheckoutSessionStub.args[0][0].sub)
|
||||
.to.eql(common.content.subscriptionBlocks[subKey]);
|
||||
expect(stripeCreateCheckoutSessionStub.args[0][0].gemsBlock).to.eql(gemsBlock);
|
||||
expect(stripeCreateCheckoutSessionStub.args[0][0].coupon).to.eql(coupon);
|
||||
});
|
||||
});
|
||||
@@ -1,79 +0,0 @@
|
||||
import {
|
||||
generateUser,
|
||||
generateGroup,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
|
||||
|
||||
describe('payments - stripe - #checkout', () => {
|
||||
const endpoint = '/stripe/checkout';
|
||||
let user; let
|
||||
group;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('verifies credentials', async () => {
|
||||
await expect(user.post(
|
||||
`${endpoint}?gemsBlock=4gems`,
|
||||
{ id: 123 },
|
||||
)).to.eventually.be.rejected.and.include({
|
||||
code: 401,
|
||||
error: 'Error',
|
||||
// message: 'Invalid API Key provided: aaaabbbb********************1111',
|
||||
});
|
||||
});
|
||||
|
||||
describe('success', () => {
|
||||
let stripeCheckoutSubscriptionStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
stripeCheckoutSubscriptionStub = sinon.stub(stripePayments, 'checkout').resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
stripePayments.checkout.restore();
|
||||
});
|
||||
|
||||
it('creates a user subscription', async () => {
|
||||
user = await generateUser({
|
||||
'profile.name': 'sender',
|
||||
'purchased.plan.customerId': 'customer-id',
|
||||
'purchased.plan.planId': 'basic_3mo',
|
||||
'purchased.plan.lastBillingDate': new Date(),
|
||||
balance: 2,
|
||||
});
|
||||
|
||||
await user.post(endpoint);
|
||||
|
||||
expect(stripeCheckoutSubscriptionStub).to.be.calledOnce;
|
||||
expect(stripeCheckoutSubscriptionStub.args[0][0].user._id).to.eql(user._id);
|
||||
expect(stripeCheckoutSubscriptionStub.args[0][0].groupId).to.eql(undefined);
|
||||
});
|
||||
|
||||
it('creates a group subscription', async () => {
|
||||
user = await generateUser({
|
||||
'profile.name': 'sender',
|
||||
'purchased.plan.customerId': 'customer-id',
|
||||
'purchased.plan.planId': 'basic_3mo',
|
||||
'purchased.plan.lastBillingDate': new Date(),
|
||||
balance: 2,
|
||||
});
|
||||
|
||||
group = await generateGroup(user, {
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
'purchased.plan.customerId': 'customer-id',
|
||||
'purchased.plan.planId': 'basic_3mo',
|
||||
'purchased.plan.lastBillingDate': new Date(),
|
||||
});
|
||||
|
||||
await user.post(`${endpoint}?groupId=${group._id}`);
|
||||
|
||||
expect(stripeCheckoutSubscriptionStub).to.be.calledOnce;
|
||||
expect(stripeCheckoutSubscriptionStub.args[0][0].user._id).to.eql(user._id);
|
||||
expect(stripeCheckoutSubscriptionStub.args[0][0].groupId).to.eql(group._id);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,79 +1,31 @@
|
||||
import {
|
||||
generateUser,
|
||||
generateGroup,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
|
||||
|
||||
describe('payments - stripe - #subscribeEdit', () => {
|
||||
const endpoint = '/stripe/subscribe/edit';
|
||||
let user; let
|
||||
group;
|
||||
let user; const groupId = 'groupId';
|
||||
let stripeEditSubscriptionStub;
|
||||
const sessionId = 'sessionId';
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
stripeEditSubscriptionStub = sinon
|
||||
.stub(stripePayments, 'createEditCardCheckoutSession')
|
||||
.resolves({ id: sessionId });
|
||||
});
|
||||
|
||||
it('verifies credentials', async () => {
|
||||
await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('missingSubscription'),
|
||||
});
|
||||
afterEach(() => {
|
||||
stripePayments.createEditCardCheckoutSession.restore();
|
||||
});
|
||||
|
||||
describe('success', () => {
|
||||
let stripeEditSubscriptionStub;
|
||||
it('works', async () => {
|
||||
const res = await user.post(endpoint, { groupId });
|
||||
expect(res.sessionId).to.equal(sessionId);
|
||||
|
||||
beforeEach(async () => {
|
||||
stripeEditSubscriptionStub = sinon.stub(stripePayments, 'editSubscription').resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
stripePayments.editSubscription.restore();
|
||||
});
|
||||
|
||||
it('cancels a user subscription', async () => {
|
||||
user = await generateUser({
|
||||
'profile.name': 'sender',
|
||||
'purchased.plan.customerId': 'customer-id',
|
||||
'purchased.plan.planId': 'basic_3mo',
|
||||
'purchased.plan.lastBillingDate': new Date(),
|
||||
balance: 2,
|
||||
});
|
||||
|
||||
await user.post(endpoint);
|
||||
|
||||
expect(stripeEditSubscriptionStub).to.be.calledOnce;
|
||||
expect(stripeEditSubscriptionStub.args[0][0].user._id).to.eql(user._id);
|
||||
expect(stripeEditSubscriptionStub.args[0][0].groupId).to.eql(undefined);
|
||||
});
|
||||
|
||||
it('cancels a group subscription', async () => {
|
||||
user = await generateUser({
|
||||
'profile.name': 'sender',
|
||||
'purchased.plan.customerId': 'customer-id',
|
||||
'purchased.plan.planId': 'basic_3mo',
|
||||
'purchased.plan.lastBillingDate': new Date(),
|
||||
balance: 2,
|
||||
});
|
||||
|
||||
group = await generateGroup(user, {
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
'purchased.plan.customerId': 'customer-id',
|
||||
'purchased.plan.planId': 'basic_3mo',
|
||||
'purchased.plan.lastBillingDate': new Date(),
|
||||
});
|
||||
|
||||
await user.post(endpoint, {
|
||||
groupId: group._id,
|
||||
});
|
||||
|
||||
expect(stripeEditSubscriptionStub).to.be.calledOnce;
|
||||
expect(stripeEditSubscriptionStub.args[0][0].user._id).to.eql(user._id);
|
||||
expect(stripeEditSubscriptionStub.args[0][0].groupId).to.eql(group._id);
|
||||
});
|
||||
expect(stripeEditSubscriptionStub).to.be.calledOnce;
|
||||
expect(stripeEditSubscriptionStub.args[0][0].user._id).to.eql(user._id);
|
||||
expect(stripeEditSubscriptionStub.args[0][0].groupId).to.eql(groupId);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
|
||||
|
||||
describe('payments - stripe - #handleWebhooks', () => {
|
||||
const endpoint = '/stripe/webhooks';
|
||||
let user; const body = '{"key": "val"}';
|
||||
let stripeHandleWebhooksStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
stripeHandleWebhooksStub = sinon
|
||||
.stub(stripePayments, 'handleWebhooks')
|
||||
.resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
stripePayments.handleWebhooks.restore();
|
||||
});
|
||||
|
||||
it('works', async () => {
|
||||
const res = await user.post(endpoint, body);
|
||||
expect(res).to.eql({});
|
||||
|
||||
expect(stripeHandleWebhooksStub).to.be.calledOnce;
|
||||
expect(stripeHandleWebhooksStub.args[0][0].body).to.exist;
|
||||
expect(stripeHandleWebhooksStub.args[0][0].headers).to.exist;
|
||||
});
|
||||
});
|
||||
@@ -83,22 +83,6 @@ describe('POST /groups/:groupId/quests/invite/:questKey', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('does not issue invites if the user is of insufficient Level', async () => {
|
||||
const LEVELED_QUEST = 'atom1';
|
||||
const LEVELED_QUEST_REQ = questScrolls[LEVELED_QUEST].lvl;
|
||||
const leaderUpdate = {};
|
||||
leaderUpdate[`items.quests.${LEVELED_QUEST}`] = 1;
|
||||
leaderUpdate['stats.lvl'] = LEVELED_QUEST_REQ - 1;
|
||||
|
||||
await leader.update(leaderUpdate);
|
||||
|
||||
await expect(leader.post(`/groups/${questingGroup._id}/quests/invite/${LEVELED_QUEST}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('questLevelTooHigh', { level: LEVELED_QUEST_REQ }),
|
||||
});
|
||||
});
|
||||
|
||||
it('does not issue invites if a quest is already underway', async () => {
|
||||
const QUEST_IN_PROGRESS = 'atom1';
|
||||
const leaderUpdate = {};
|
||||
@@ -212,6 +196,18 @@ describe('POST /groups/:groupId/quests/invite/:questKey', () => {
|
||||
expect(returnedGroup.chat[0]._meta).to.be.undefined;
|
||||
});
|
||||
|
||||
it('successfully issues a quest invitation when quest level is higher than user level', async () => {
|
||||
const LEVELED_QUEST = 'atom1';
|
||||
const LEVELED_QUEST_REQ = questScrolls[LEVELED_QUEST].lvl;
|
||||
const leaderUpdate = {};
|
||||
leaderUpdate[`items.quests.${LEVELED_QUEST}`] = 1;
|
||||
leaderUpdate['stats.lvl'] = LEVELED_QUEST_REQ - 1;
|
||||
|
||||
await leader.update(leaderUpdate);
|
||||
|
||||
await leader.post(`/groups/${questingGroup._id}/quests/invite/${LEVELED_QUEST}`);
|
||||
});
|
||||
|
||||
context('sending quest activity webhooks', () => {
|
||||
before(async () => {
|
||||
await server.start();
|
||||
|
||||
@@ -91,7 +91,9 @@ describe('POST /tasks/:taskId/move/to/:position', () => {
|
||||
|
||||
const taskToMove = tasks[1];
|
||||
expect(taskToMove.text).to.equal('habit 2');
|
||||
const newOrder = await user.post(`/tasks/${tasks[1]._id}/move/to/-1`);
|
||||
await user.post(`/tasks/${tasks[1]._id}/move/to/-1`);
|
||||
await user.sync();
|
||||
const newOrder = user.tasksOrder.habits;
|
||||
expect(newOrder[4]).to.equal(taskToMove._id);
|
||||
expect(newOrder.length).to.equal(5);
|
||||
});
|
||||
|
||||
@@ -220,6 +220,18 @@ describe('POST /tasks/user', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('errors if todo due date supplied is an invalid date', async () => {
|
||||
await expect(user.post('/tasks/user', {
|
||||
type: 'todo',
|
||||
text: 'todo text',
|
||||
date: 'invalid date',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'todo validation failed',
|
||||
});
|
||||
});
|
||||
|
||||
context('sending task activity webhooks', () => {
|
||||
before(async () => {
|
||||
await server.start();
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { model as NewsPost } from '../../../../../website/server/models/newsPost';
|
||||
|
||||
describe('PUT /user', () => {
|
||||
let user;
|
||||
@@ -101,6 +102,24 @@ describe('PUT /user', () => {
|
||||
message: t('displaynameIssueNewline'),
|
||||
});
|
||||
});
|
||||
|
||||
it('can set flags.newStuff to false', async () => {
|
||||
NewsPost.updateLastNewsPost({
|
||||
_id: '1234', publishDate: new Date(), title: 'Title', published: true,
|
||||
});
|
||||
|
||||
await user.update({
|
||||
'flags.lastNewStuffRead': '123',
|
||||
});
|
||||
|
||||
await user.put('/user', {
|
||||
'flags.newStuff': false,
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
|
||||
expect(user.flags.lastNewStuffRead).to.eql('1234');
|
||||
});
|
||||
});
|
||||
|
||||
context('Top Level Protected Operations', () => {
|
||||
|
||||
49
test/api/v4/news/DELETE-news.test.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import { v4 } from 'uuid';
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../helpers/api-integration/v4';
|
||||
|
||||
describe('DELETE /news/:newsID', () => {
|
||||
let user;
|
||||
const newsPost = {
|
||||
title: 'New Post',
|
||||
publishDate: new Date(),
|
||||
published: true,
|
||||
credits: 'credits',
|
||||
text: 'news body',
|
||||
};
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({
|
||||
'contributor.newsPoster': true,
|
||||
});
|
||||
});
|
||||
|
||||
it('disallows access to non-newsPosters', async () => {
|
||||
const nonAdminUser = await generateUser({ 'contributor.newsPoster': false });
|
||||
await expect(nonAdminUser.del(`/news/${v4()}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: 'You don\'t have news poster access.',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if the post does not exist', async () => {
|
||||
await expect(user.del(`/news/${v4()}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('newsPostNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('deletes news posts', async () => {
|
||||
const existingPost = await user.post('/news', newsPost);
|
||||
await user.del(`/news/${existingPost._id}`);
|
||||
|
||||
const returnedPosts = await user.get('/news');
|
||||
const deletedPost = returnedPosts.find(returnedPost => returnedPost._id === existingPost._id);
|
||||
|
||||
expect(returnedPosts).is.an('array');
|
||||
expect(deletedPost).to.not.exist;
|
||||
});
|
||||
});
|
||||
50
test/api/v4/news/GET-news.test.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
requester, generateUser,
|
||||
} from '../../../helpers/api-integration/v4';
|
||||
|
||||
describe('GET /news', () => {
|
||||
let api;
|
||||
const newsPost = {
|
||||
title: 'New Post',
|
||||
publishDate: new Date(),
|
||||
published: true,
|
||||
credits: 'credits',
|
||||
text: 'news body',
|
||||
};
|
||||
|
||||
before(async () => {
|
||||
api = requester();
|
||||
const user = await generateUser({
|
||||
'contributor.newsPoster': true,
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
user.post('/news', newsPost),
|
||||
user.post('/news', newsPost),
|
||||
user.post('/news', newsPost),
|
||||
user.post('/news', newsPost),
|
||||
user.post('/news', newsPost),
|
||||
user.post('/news', newsPost),
|
||||
user.post('/news', newsPost),
|
||||
user.post('/news', newsPost),
|
||||
user.post('/news', newsPost),
|
||||
user.post('/news', newsPost),
|
||||
user.post('/news', newsPost),
|
||||
user.post('/news', newsPost),
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns the latest news in json format, does not require authentication, 10 per page', async () => {
|
||||
const res = await api.get('/news');
|
||||
expect(res.length).to.be.equal(10);
|
||||
expect(res[0].title).to.be.not.empty;
|
||||
expect(res[0].text).to.be.not.empty;
|
||||
});
|
||||
|
||||
it('supports pagination', async () => {
|
||||
const res = await api.get('/news?page=1');
|
||||
expect(res.length).to.be.equal(2);
|
||||
expect(res[0].title).to.be.not.empty;
|
||||
expect(res[0].text).to.be.not.empty;
|
||||
});
|
||||
});
|
||||
36
test/api/v4/news/GET-news_id.test.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { v4 } from 'uuid';
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../helpers/api-integration/v4';
|
||||
|
||||
describe('GET /news/:newsID', () => {
|
||||
let user;
|
||||
const newsPost = {
|
||||
title: 'New Post',
|
||||
publishDate: new Date(),
|
||||
published: true,
|
||||
credits: 'credits',
|
||||
text: 'news body',
|
||||
};
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({
|
||||
'contributor.newsPoster': true,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if the post does not exist', async () => {
|
||||
await expect(user.get(`/news/${v4()}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('newsPostNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('fetches an existing post', async () => {
|
||||
const existingPost = await user.post('/news', newsPost);
|
||||
const fetchedPost = await user.get(`/news/${existingPost._id}`);
|
||||
|
||||
expect(fetchedPost._id).to.equal(existingPost._id);
|
||||
});
|
||||
});
|
||||
134
test/api/v4/news/POST-news.test.js
Normal file
@@ -0,0 +1,134 @@
|
||||
import moment from 'moment';
|
||||
import {
|
||||
generateUser,
|
||||
sleep,
|
||||
} from '../../../helpers/api-integration/v4';
|
||||
import { model as NewsPost } from '../../../../website/server/models/newsPost';
|
||||
|
||||
describe('POST /news', () => {
|
||||
let user;
|
||||
const newsPost = {
|
||||
title: 'New Post',
|
||||
publishDate: new Date(),
|
||||
published: true,
|
||||
credits: 'credits',
|
||||
text: 'news body',
|
||||
};
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({
|
||||
'contributor.newsPoster': true,
|
||||
});
|
||||
});
|
||||
|
||||
it('disallows access to non-admins', async () => {
|
||||
const nonAdminUser = await generateUser({ 'contributor.newsPoster': false });
|
||||
await expect(nonAdminUser.post('/news')).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: 'You don\'t have news poster access.',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates news posts', async () => {
|
||||
const response = await user.post('/news', newsPost);
|
||||
|
||||
expect(response.title).to.equal(newsPost.title);
|
||||
expect(response.credits).to.equal(newsPost.credits);
|
||||
expect(response.text).to.equal(newsPost.text);
|
||||
expect(response._id).to.exist;
|
||||
|
||||
const res = await user.get('/news');
|
||||
expect(res[0]._id).to.equal(response._id);
|
||||
expect(res[0].title).to.equal(newsPost.title);
|
||||
expect(res[0].text).to.equal(newsPost.text);
|
||||
});
|
||||
|
||||
context('calls updateLastNewsPost', () => {
|
||||
beforeEach(async () => {
|
||||
await NewsPost.remove({ });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
newsPost.publishDate = new Date();
|
||||
newsPost.published = true;
|
||||
});
|
||||
|
||||
it('new post is published and the most recent one', async () => {
|
||||
newsPost.publishDate = new Date();
|
||||
const newPost = await user.post('/news', newsPost);
|
||||
await sleep(0.05);
|
||||
expect(NewsPost.lastNewsPost()._id).to.equal(newPost._id);
|
||||
});
|
||||
|
||||
it('new post is not published', async () => {
|
||||
newsPost.published = false;
|
||||
const newPost = await user.post('/news', newsPost);
|
||||
await sleep(0.05);
|
||||
expect(NewsPost.lastNewsPost()._id).to.not.equal(newPost._id);
|
||||
});
|
||||
|
||||
it('new post is published but in the future', async () => {
|
||||
newsPost.publishDate = moment().add({ days: 1 }).toDate();
|
||||
const newPost = await user.post('/news', newsPost);
|
||||
await sleep(0.05);
|
||||
expect(NewsPost.lastNewsPost()._id).to.not.equal(newPost._id);
|
||||
});
|
||||
|
||||
it('new post is published but not the most recent one', async () => {
|
||||
const oldPost = await user.post('/news', newsPost);
|
||||
newsPost.publishDate = moment().subtract({ days: 1 }).toDate();
|
||||
await user.post('/news', newsPost);
|
||||
await sleep(0.05);
|
||||
expect(NewsPost.lastNewsPost()._id).to.equal(oldPost._id);
|
||||
});
|
||||
});
|
||||
|
||||
it('sets default fields', async () => {
|
||||
const response = await user.post('/news', {
|
||||
title: 'A post',
|
||||
credits: 'Credits',
|
||||
text: 'Text',
|
||||
});
|
||||
|
||||
expect(response.published).to.equal(false);
|
||||
expect(response.publishDate).to.exist;
|
||||
expect(response.author).to.equal(user._id);
|
||||
expect(response.createdAt).to.exist;
|
||||
expect(response.updatedAt).to.exist;
|
||||
});
|
||||
|
||||
context('required fields', () => {
|
||||
it('title', async () => {
|
||||
await expect(user.post('/news', {
|
||||
text: 'Text',
|
||||
credits: 'Credits',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'NewsPost validation failed',
|
||||
});
|
||||
});
|
||||
|
||||
it('credits', async () => {
|
||||
await expect(user.post('/news', {
|
||||
text: 'Text',
|
||||
title: 'Title',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'NewsPost validation failed',
|
||||
});
|
||||
});
|
||||
|
||||
it('text', async () => {
|
||||
await expect(user.post('/news', {
|
||||
credits: 'credits',
|
||||
title: 'Title',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'NewsPost validation failed',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
22
test/api/v4/news/POST-news_read.test.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../helpers/api-integration/v4';
|
||||
import { model as NewsPost } from '../../../../website/server/models/newsPost';
|
||||
|
||||
describe('POST /news/read', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('marks new stuff as read', async () => {
|
||||
NewsPost.updateLastNewsPost({ _id: '1234', publishDate: new Date(), published: true });
|
||||
await user.post('/news/read');
|
||||
await user.sync();
|
||||
|
||||
expect(user.flags.lastNewStuffRead).to.equal('1234');
|
||||
// fetching the user because newStuff is a computed property
|
||||
expect((await user.get('/user')).flags.newStuff).to.equal(false);
|
||||
});
|
||||
});
|
||||
103
test/api/v4/news/PUT-news_newsId.test.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import { v4 } from 'uuid';
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
sleep,
|
||||
} from '../../../helpers/api-integration/v4';
|
||||
import { model as NewsPost } from '../../../../website/server/models/newsPost';
|
||||
|
||||
describe('PUT /news/:newsID', () => {
|
||||
let user;
|
||||
const newsPost = {
|
||||
title: 'New Post',
|
||||
publishDate: new Date(),
|
||||
published: true,
|
||||
credits: 'credits',
|
||||
text: 'news body',
|
||||
};
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({
|
||||
'contributor.newsPoster': true,
|
||||
});
|
||||
});
|
||||
|
||||
it('disallows access to non-admins', async () => {
|
||||
const nonAdminUser = await generateUser({ 'contributor.newsPoster': false });
|
||||
await expect(nonAdminUser.put('/news/1234')).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: 'You don\'t have news poster access.',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if the post does not exist', async () => {
|
||||
await expect(user.put(`/news/${v4()}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('newsPostNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('updates existing news posts', async () => {
|
||||
const existingPost = await user.post('/news', newsPost);
|
||||
const updatedPost = await user.put(`/news/${existingPost._id}`, {
|
||||
title: 'Changed Title',
|
||||
});
|
||||
|
||||
expect(updatedPost.title).to.equal('Changed Title');
|
||||
expect(updatedPost.credits).to.equal(existingPost.credits);
|
||||
expect(updatedPost.text).to.equal(existingPost.text);
|
||||
expect(updatedPost.published).to.equal(existingPost.published);
|
||||
expect(updatedPost._id).to.equal(existingPost._id);
|
||||
});
|
||||
|
||||
context('calls updateLastNewsPost', () => {
|
||||
beforeEach(async () => {
|
||||
await NewsPost.remove({ });
|
||||
});
|
||||
|
||||
it('updates post data', async () => {
|
||||
const existingPost = await user.post('/news', { ...newsPost, publishDate: new Date() });
|
||||
const updatedPost = await user.put(`/news/${existingPost._id}`, {
|
||||
title: 'Changed Title',
|
||||
});
|
||||
await sleep(0.05);
|
||||
|
||||
expect(NewsPost.lastNewsPost().title).to.equal(updatedPost.title);
|
||||
});
|
||||
|
||||
it('updated post is not published', async () => {
|
||||
const oldPost = await user.post('/news', { ...newsPost, publishDate: new Date() });
|
||||
const newUnpublished = await user.post('/news', { ...newsPost, published: false });
|
||||
await user.put(`/news/${newUnpublished._id}`, {
|
||||
title: 'Changed Title',
|
||||
});
|
||||
await sleep(0.05);
|
||||
|
||||
expect(NewsPost.lastNewsPost()._id).to.equal(oldPost._id);
|
||||
});
|
||||
|
||||
it('updated post is published', async () => {
|
||||
await user.post('/news', { ...newsPost, publishDate: new Date() });
|
||||
const newUnpublished = await user.post('/news', { ...newsPost, published: false, publishDate: new Date() });
|
||||
await user.put(`/news/${newUnpublished._id}`, {
|
||||
publishDate: new Date(),
|
||||
published: true,
|
||||
});
|
||||
await sleep(0.05);
|
||||
|
||||
expect(NewsPost.lastNewsPost()._id).to.equal(newUnpublished._id);
|
||||
});
|
||||
|
||||
it('updated post publishDate is in future', async () => {
|
||||
const oldPost = await user.post('/news', { ...newsPost, publishDate: new Date() });
|
||||
const newUnpublished = await user.post('/news', newsPost);
|
||||
await user.put(`/news/${newUnpublished._id}`, {
|
||||
publishDate: Date.now() + 50000,
|
||||
});
|
||||
await sleep(0.05);
|
||||
|
||||
expect(NewsPost.lastNewsPost()._id).to.equal(oldPost._id);
|
||||
});
|
||||
});
|
||||
});
|
||||
52
test/api/v4/user/POST-user_unequip.test.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../helpers/api-integration/v4';
|
||||
import { UNEQUIP_EQUIPPED } from '../../../../website/common/script/ops/unequip';
|
||||
|
||||
describe('POST /user/unequip', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({
|
||||
preferences: {
|
||||
background: 'violet',
|
||||
},
|
||||
items: {
|
||||
currentMount: 'BearCub-Base',
|
||||
currentPet: 'BearCub-Base',
|
||||
gear: {
|
||||
owned: {
|
||||
weapon_warrior_0: true,
|
||||
weapon_warrior_1: true,
|
||||
weapon_warrior_2: true,
|
||||
weapon_wizard_1: true,
|
||||
weapon_wizard_2: true,
|
||||
shield_base_0: true,
|
||||
shield_warrior_1: true,
|
||||
},
|
||||
equipped: {
|
||||
weapon: 'weapon_warrior_2',
|
||||
shield: 'shield_warrior_1',
|
||||
},
|
||||
costume: {
|
||||
weapon: 'weapon_warrior_2',
|
||||
shield: 'shield_warrior_1',
|
||||
},
|
||||
},
|
||||
},
|
||||
stats: { gp: 200 },
|
||||
});
|
||||
});
|
||||
|
||||
// More tests in common code unit tests
|
||||
|
||||
context('Gear', () => {
|
||||
it('should unequip all battle gear items', async () => {
|
||||
await user.post(`/user/unequip/${UNEQUIP_EQUIPPED}`);
|
||||
await user.sync();
|
||||
|
||||
expect(user.items.gear.equipped.weapon).to.eq('weapon_base_0');
|
||||
expect(user.items.gear.equipped.shield).to.eq('shield_base_0');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import randomDrop from '../../../website/common/script/fns/randomDrop';
|
||||
import i18n from '../../../website/common/script/i18n';
|
||||
import {
|
||||
generateUser,
|
||||
generateTodo,
|
||||
@@ -144,5 +145,148 @@ describe('common.fns.randomDrop', () => {
|
||||
expect(acceptableDrops).to.contain(user._tmp.drop.key); // always Desert
|
||||
});
|
||||
});
|
||||
|
||||
context('drop cap notification', () => {
|
||||
let analytics;
|
||||
const req = {};
|
||||
let isSubscribedStub;
|
||||
|
||||
beforeEach(() => {
|
||||
user.addNotification = () => {};
|
||||
sandbox.stub(user, 'addNotification');
|
||||
user.isSubscribed = () => {};
|
||||
isSubscribedStub = sandbox.stub(user, 'isSubscribed');
|
||||
isSubscribedStub.returns(false);
|
||||
analytics = { track () {} };
|
||||
sandbox.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
it('sends a notification if A/B test is enabled when drop cap is reached', () => {
|
||||
user._ABtests.dropCapNotif = 'drop-cap-notif-enabled';
|
||||
predictableRandom.returns(0.1);
|
||||
|
||||
// Max Drop Count is 5
|
||||
expect(user.items.lastDrop.count).to.equal(0);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
expect(user.items.lastDrop.count).to.equal(5);
|
||||
expect(user.addNotification).to.be.calledOnce;
|
||||
expect(user.addNotification).to.be.calledWith('DROP_CAP_REACHED', {
|
||||
message: i18n.t('dropCapReached'),
|
||||
items: 5,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not send a notification if user is enrolled in disabled A/B test group', () => {
|
||||
user._ABtests.dropCapNotif = 'drop-cap-notif-disabled';
|
||||
predictableRandom.returns(0.1);
|
||||
|
||||
// Max Drop Count is 5
|
||||
expect(user.items.lastDrop.count).to.equal(0);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
expect(user.items.lastDrop.count).to.equal(5);
|
||||
expect(user.addNotification).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not send a notification if user is enrolled in disabled A/B test group', () => {
|
||||
user._ABtests.dropCapNotif = 'drop-cap-notif-not-enrolled';
|
||||
predictableRandom.returns(0.1);
|
||||
|
||||
// Max Drop Count is 5
|
||||
expect(user.items.lastDrop.count).to.equal(0);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
expect(user.items.lastDrop.count).to.equal(5);
|
||||
expect(user.addNotification).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not send a notification if drop cap is not reached', () => {
|
||||
user._ABtests.dropCapNotif = 'drop-cap-notif-enabled';
|
||||
predictableRandom.returns(0.1);
|
||||
|
||||
// Max Drop Count is 5
|
||||
expect(user.items.lastDrop.count).to.equal(0);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
expect(user.items.lastDrop.count).to.equal(4);
|
||||
expect(user.addNotification).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not send a notification if user is subscribed', () => {
|
||||
user._ABtests.dropCapNotif = 'drop-cap-notif-enabled';
|
||||
predictableRandom.returns(0.1);
|
||||
isSubscribedStub.returns(true);
|
||||
|
||||
// Max Drop Count is 5
|
||||
expect(user.items.lastDrop.count).to.equal(0);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
expect(user.items.lastDrop.count).to.equal(5);
|
||||
expect(user.addNotification).to.not.be.called;
|
||||
});
|
||||
|
||||
it('tracks drop cap reached event for enrolled users (notification enabled)', () => {
|
||||
user._ABtests.dropCapNotif = 'drop-cap-notif-enabled';
|
||||
predictableRandom.returns(0.1);
|
||||
isSubscribedStub.returns(true);
|
||||
|
||||
// Max Drop Count is 5
|
||||
expect(user.items.lastDrop.count).to.equal(0);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
expect(user.items.lastDrop.count).to.equal(5);
|
||||
expect(analytics.track).to.be.calledWith('drop cap reached');
|
||||
});
|
||||
|
||||
it('tracks drop cap reached event for enrolled users (notification disabled)', () => {
|
||||
user._ABtests.dropCapNotif = 'drop-cap-notif-disabled';
|
||||
predictableRandom.returns(0.1);
|
||||
isSubscribedStub.returns(true);
|
||||
|
||||
// Max Drop Count is 5
|
||||
expect(user.items.lastDrop.count).to.equal(0);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
expect(user.items.lastDrop.count).to.equal(5);
|
||||
expect(analytics.track).to.be.calledWith('drop cap reached');
|
||||
});
|
||||
|
||||
it('does not track drop cap reached event for users not enrolled in A/B test', () => {
|
||||
user._ABtests.dropCapNotif = 'drop-cap-notif-not-enrolled';
|
||||
predictableRandom.returns(0.1);
|
||||
isSubscribedStub.returns(true);
|
||||
|
||||
// Max Drop Count is 5
|
||||
expect(user.items.lastDrop.count).to.equal(0);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
expect(user.items.lastDrop.count).to.equal(5);
|
||||
expect(analytics.track).to.not.be.calledWith('drop cap reached');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
100
test/common/ops/unequip.js
Normal file
@@ -0,0 +1,100 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
import {
|
||||
UNEQUIP_ALL,
|
||||
UNEQUIP_BACKGROUND,
|
||||
UNEQUIP_COSTUME,
|
||||
UNEQUIP_EQUIPPED,
|
||||
UNEQUIP_PET_MOUNT,
|
||||
unEquipByType,
|
||||
} from '../../../website/common/script/ops/unequip';
|
||||
|
||||
describe('shared.ops.unequip', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser({
|
||||
preferences: {
|
||||
background: 'violet',
|
||||
},
|
||||
items: {
|
||||
currentMount: 'BearCub-Base',
|
||||
currentPet: 'BearCub-Base',
|
||||
gear: {
|
||||
owned: {
|
||||
weapon_warrior_0: true,
|
||||
weapon_warrior_1: true,
|
||||
weapon_warrior_2: true,
|
||||
weapon_wizard_1: true,
|
||||
weapon_wizard_2: true,
|
||||
shield_base_0: true,
|
||||
shield_warrior_1: true,
|
||||
},
|
||||
equipped: {
|
||||
weapon: 'weapon_warrior_2',
|
||||
shield: 'shield_warrior_1',
|
||||
},
|
||||
costume: {
|
||||
weapon: 'weapon_warrior_2',
|
||||
shield: 'shield_warrior_1',
|
||||
},
|
||||
},
|
||||
},
|
||||
stats: { gp: 200 },
|
||||
});
|
||||
});
|
||||
|
||||
context('Gear', () => {
|
||||
it('should unequip all battle gear items', () => {
|
||||
unEquipByType(user, { params: { type: UNEQUIP_EQUIPPED } });
|
||||
|
||||
expect(user.items.gear.equipped.weapon).to.eq('weapon_base_0');
|
||||
expect(user.items.gear.equipped.shield).to.eq('shield_base_0');
|
||||
});
|
||||
});
|
||||
|
||||
context('Costume', () => {
|
||||
it('should unequip all costume items', () => {
|
||||
unEquipByType(user, { params: { type: UNEQUIP_COSTUME } });
|
||||
|
||||
expect(user.items.gear.costume.weapon).to.eq('weapon_base_0');
|
||||
expect(user.items.gear.costume.shield).to.eq('shield_base_0');
|
||||
});
|
||||
});
|
||||
|
||||
context('Pet and Mount', () => {
|
||||
it('should unequip Pet and Mount', () => {
|
||||
unEquipByType(user, { params: { type: UNEQUIP_PET_MOUNT } });
|
||||
|
||||
expect(user.items.currentMount).to.eq('');
|
||||
expect(user.items.currentPet).to.eq('');
|
||||
});
|
||||
});
|
||||
|
||||
context('Background', () => {
|
||||
it('should unequip Background', () => {
|
||||
unEquipByType(user, { params: { type: UNEQUIP_BACKGROUND } });
|
||||
|
||||
expect(user.preferences.background).to.eq('');
|
||||
});
|
||||
});
|
||||
|
||||
context('All Items', () => {
|
||||
it('should unequip all Items', () => {
|
||||
unEquipByType(user, { params: { type: UNEQUIP_ALL } });
|
||||
|
||||
expect(user.items.gear.equipped.weapon).to.eq('weapon_base_0');
|
||||
expect(user.items.gear.equipped.shield).to.eq('shield_base_0');
|
||||
|
||||
expect(user.items.gear.costume.weapon).to.eq('weapon_base_0');
|
||||
expect(user.items.gear.costume.shield).to.eq('shield_base_0');
|
||||
|
||||
expect(user.items.currentMount).to.eq('');
|
||||
expect(user.items.currentPet).to.eq('');
|
||||
expect(user.preferences.background).to.eq('');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -41,6 +41,7 @@ function _requestMaker (user, method, additionalSets = {}) {
|
||||
|| route.indexOf('/amazon') === 0
|
||||
|| route.indexOf('/stripe') === 0
|
||||
|| route.indexOf('/qr-code') === 0
|
||||
|| route.indexOf('/analytics') === 0
|
||||
) {
|
||||
url += `${route}`;
|
||||
} else {
|
||||
|
||||
@@ -50,4 +50,5 @@ function loadStories () {
|
||||
req.keys().forEach(filename => req(filename));
|
||||
}
|
||||
|
||||
|
||||
configure(loadStories, module);
|
||||
|
||||
2442
website/client/package-lock.json
generated
@@ -18,48 +18,48 @@
|
||||
"@storybook/addon-links": "^5.3.19",
|
||||
"@storybook/addon-notes": "^5.3.21",
|
||||
"@storybook/vue": "^5.3.19",
|
||||
"@vue/cli-plugin-babel": "^4.5.6",
|
||||
"@vue/cli-plugin-eslint": "^4.5.6",
|
||||
"@vue/cli-plugin-router": "^4.5.6",
|
||||
"@vue/cli-plugin-unit-mocha": "^4.5.6",
|
||||
"@vue/cli-service": "^4.5.6",
|
||||
"@vue/cli-plugin-babel": "^4.5.9",
|
||||
"@vue/cli-plugin-eslint": "^4.5.9",
|
||||
"@vue/cli-plugin-router": "^4.5.9",
|
||||
"@vue/cli-plugin-unit-mocha": "^4.5.9",
|
||||
"@vue/cli-service": "^4.5.9",
|
||||
"@vue/test-utils": "1.0.0-beta.29",
|
||||
"amplitude-js": "^7.1.1",
|
||||
"axios": "^0.19.2",
|
||||
"amplitude-js": "^7.3.3",
|
||||
"axios": "^0.21.0",
|
||||
"axios-progress-bar": "^1.2.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"bootstrap": "^4.5.2",
|
||||
"bootstrap-vue": "^2.17.0",
|
||||
"bootstrap": "^4.5.3",
|
||||
"bootstrap-vue": "^2.21.1",
|
||||
"chai": "^4.1.2",
|
||||
"core-js": "^3.6.5",
|
||||
"core-js": "^3.8.1",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-habitrpg": "^6.2.0",
|
||||
"eslint-plugin-mocha": "^5.3.0",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"habitica-markdown": "^3.0.0",
|
||||
"hellojs": "^1.18.4",
|
||||
"hellojs": "^1.18.6",
|
||||
"inspectpack": "^4.5.2",
|
||||
"intro.js": "^2.9.3",
|
||||
"jquery": "^3.5.1",
|
||||
"lodash": "^4.17.20",
|
||||
"moment": "^2.28.0",
|
||||
"nconf": "^0.10.0",
|
||||
"sass": "^1.26.10",
|
||||
"moment": "^2.29.1",
|
||||
"nconf": "^0.11.0",
|
||||
"sass": "^1.30.0",
|
||||
"sass-loader": "^8.0.2",
|
||||
"smartbanner.js": "^1.16.0",
|
||||
"svg-inline-loader": "^0.8.2",
|
||||
"svg-url-loader": "^6.0.0",
|
||||
"svgo": "^1.3.2",
|
||||
"svgo-loader": "^2.2.1",
|
||||
"uuid": "^8.3.0",
|
||||
"validator": "^13.1.1",
|
||||
"uuid": "^8.3.2",
|
||||
"validator": "^13.5.2",
|
||||
"vue": "^2.6.12",
|
||||
"vue-cli-plugin-storybook": "^0.6.1",
|
||||
"vue-mugen-scroll": "^0.2.6",
|
||||
"vue-router": "^3.4.3",
|
||||
"vue-router": "^3.4.9",
|
||||
"vue-template-compiler": "^2.6.12",
|
||||
"vuedraggable": "^2.24.1",
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0",
|
||||
"webpack": "^4.44.1"
|
||||
"webpack": "^4.44.2"
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |