mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-10-26 10:42:52 +01:00
Compare commits
470 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd5ab32c43 | ||
|
|
e0933dc0a1 | ||
|
|
d69e7e66ee | ||
|
|
fa06628361 | ||
|
|
7f1067e1ab | ||
|
|
db6644a572 | ||
|
|
b7b8faedd6 | ||
|
|
7da0b641dd | ||
|
|
82ff241e39 | ||
|
|
cf4aaf1618 | ||
|
|
31eeb13598 | ||
|
|
818c201afe | ||
|
|
f78bcc20c9 | ||
|
|
9017eea0c4 | ||
|
|
92abb19f9c | ||
|
|
3bee0446b8 | ||
|
|
e1e5a6cc34 | ||
|
|
9eb70ee9ba | ||
|
|
7485300ee9 | ||
|
|
762c202154 | ||
|
|
7613e50917 | ||
|
|
4c77434bf0 | ||
|
|
1ab26a200e | ||
|
|
b738824f76 | ||
|
|
694c440b55 | ||
|
|
667bb28fe3 | ||
|
|
8993337ad7 | ||
|
|
a251a5d089 | ||
|
|
dcdeec6256 | ||
|
|
9fd0df9f2f | ||
|
|
1061fb0c31 | ||
|
|
c14fdd3fed | ||
|
|
e219ad6bdf | ||
|
|
42d7fd0861 | ||
|
|
7ade91a8b8 | ||
|
|
7c6dd6a6bd | ||
|
|
9a00779698 | ||
|
|
a61d911c48 | ||
|
|
fbacb56700 | ||
|
|
185717e6c3 | ||
|
|
505dd4969d | ||
|
|
944e4fe399 | ||
|
|
804dd087f8 | ||
|
|
cb1136aadc | ||
|
|
0f4b8f5f30 | ||
|
|
2d612b655d | ||
|
|
46877fb20c | ||
|
|
38cded2083 | ||
|
|
65074df668 | ||
|
|
b0ab09c352 | ||
|
|
3a60e8de66 | ||
|
|
886a96dac9 | ||
|
|
20556e2af4 | ||
|
|
3e849ec9a8 | ||
|
|
03946e6a87 | ||
|
|
fdcc69fe4a | ||
|
|
322b6a5e44 | ||
|
|
2243a2f3dc | ||
|
|
59c848add5 | ||
|
|
2d6a1fe709 | ||
|
|
fac0338437 | ||
|
|
e46f30894c | ||
|
|
553dc116c5 | ||
|
|
2029cd884c | ||
|
|
ab89941ed5 | ||
|
|
5dc0f5bb9b | ||
|
|
d46a7ba985 | ||
|
|
86b3228a59 | ||
|
|
4efbbd7bac | ||
|
|
d39e8a3587 | ||
|
|
8bd3ef6f24 | ||
|
|
6d0917964b | ||
|
|
d463e2373e | ||
|
|
2af99d7c65 | ||
|
|
4e01b14874 | ||
|
|
944781c2f8 | ||
|
|
027eed1b25 | ||
|
|
803f63d991 | ||
|
|
78ad1cd8b0 | ||
|
|
a3ddd0746c | ||
|
|
94845ec629 | ||
|
|
7e63856e64 | ||
|
|
3727d69d51 | ||
|
|
1fbdb7dbd0 | ||
|
|
b430b6ccb6 | ||
|
|
7859f20a40 | ||
|
|
389d6f18b4 | ||
|
|
fd1d1da509 | ||
|
|
014b367f56 | ||
|
|
4580b33e43 | ||
|
|
48bcc1e968 | ||
|
|
35368eb6ad | ||
|
|
9b337983ed | ||
|
|
cd4951d204 | ||
|
|
bd83d6f5aa | ||
|
|
9c241a6159 | ||
|
|
7a6baeadbd | ||
|
|
5495acea96 | ||
|
|
a3aa2cc175 | ||
|
|
8b0d02a16b | ||
|
|
c3c0eb974a | ||
|
|
56539100e3 | ||
|
|
b706db43e4 | ||
|
|
3e0a7c70ed | ||
|
|
6a5bd1b0a5 | ||
|
|
7f8a9be766 | ||
|
|
dbdf679e4a | ||
|
|
eb28dfadf9 | ||
|
|
ede28ac33a | ||
|
|
33b249d078 | ||
|
|
a85282763f | ||
|
|
ba9d7b3b5e | ||
|
|
3b794c017a | ||
|
|
47dbe4561f | ||
|
|
97135a1ac3 | ||
|
|
a636e15d11 | ||
|
|
3cc15e869e | ||
|
|
88c8b92a68 | ||
|
|
cee4d7e87b | ||
|
|
9488ec2eb0 | ||
|
|
4fe6c8db64 | ||
|
|
ccf8e0b320 | ||
|
|
1dc558ddba | ||
|
|
ae27ae0090 | ||
|
|
47c2a3a21a | ||
|
|
5d4e1362bb | ||
|
|
25cecf298f | ||
|
|
2de3b63e87 | ||
|
|
7abb8a81a7 | ||
|
|
3eb3891899 | ||
|
|
4b0ad422f1 | ||
|
|
3c603e3bb1 | ||
|
|
4ee788f541 | ||
|
|
99ab9726b4 | ||
|
|
23dd402e79 | ||
|
|
6bd90807f3 | ||
|
|
563a5845f0 | ||
|
|
2e580baf27 | ||
|
|
44ded25f6d | ||
|
|
70da5940a7 | ||
|
|
12aa8a78c1 | ||
|
|
94619737e8 | ||
|
|
ccc9e6611c | ||
|
|
1f1459b0d8 | ||
|
|
6489e74b6b | ||
|
|
c1e264955f | ||
|
|
f302d15bc4 | ||
|
|
8c70c8839b | ||
|
|
3fcd04fd8a | ||
|
|
d85f18751c | ||
|
|
1390c4eae5 | ||
|
|
18ade8ca65 | ||
|
|
7b026fa32c | ||
|
|
33698c219f | ||
|
|
b76d731cee | ||
|
|
4d1ac51543 | ||
|
|
3818fbdd3e | ||
|
|
af245b63d9 | ||
|
|
028da1d6a9 | ||
|
|
49397244c4 | ||
|
|
2b04ed3246 | ||
|
|
51aebb540c | ||
|
|
f5d7777b2c | ||
|
|
be1ffbd671 | ||
|
|
5640139ef1 | ||
|
|
0959499450 | ||
|
|
90ffe587dd | ||
|
|
38aafb6c7b | ||
|
|
ecfcf09184 | ||
|
|
7083dc7e05 | ||
|
|
d4e0417c48 | ||
|
|
ec7c25de9f | ||
|
|
6f9db87843 | ||
|
|
46c9038f54 | ||
|
|
1ce09aeb34 | ||
|
|
2ba327ef14 | ||
|
|
de93b47493 | ||
|
|
b0a21e116a | ||
|
|
53d1a5f9dc | ||
|
|
274f942b1e | ||
|
|
4aad52242c | ||
|
|
166a48e139 | ||
|
|
13de97dde6 | ||
|
|
6d8407ff94 | ||
|
|
663b794435 | ||
|
|
c0276e3663 | ||
|
|
6d57ce3050 | ||
|
|
2159df785f | ||
|
|
9762258975 | ||
|
|
deea64e839 | ||
|
|
9e615ba862 | ||
|
|
d34beca3cc | ||
|
|
07ed989862 | ||
|
|
049844ea7d | ||
|
|
ff4c76165a | ||
|
|
c3220e7c03 | ||
|
|
cb4c6b3ca6 | ||
|
|
ba36ba0157 | ||
|
|
dd95acf436 | ||
|
|
a73b03452a | ||
|
|
935fa1baae | ||
|
|
745f930749 | ||
|
|
d87db40c52 | ||
|
|
0ea91016f8 | ||
|
|
d4f634c3d8 | ||
|
|
286566fc0c | ||
|
|
2ed4df0b7c | ||
|
|
9bb7c6ece0 | ||
|
|
db0a6f6bb8 | ||
|
|
b6305826be | ||
|
|
f00ab86eff | ||
|
|
a44f29dad8 | ||
|
|
67b396bf16 | ||
|
|
ce14a9dadb | ||
|
|
183c90ac3a | ||
|
|
9e1a262f96 | ||
|
|
06dd9fe859 | ||
|
|
2a2c525c2d | ||
|
|
b2c1c9d9dc | ||
|
|
c33eba6736 | ||
|
|
56434cce71 | ||
|
|
c41123c36c | ||
|
|
043a6cd4ba | ||
|
|
0ca2f9034f | ||
|
|
4c7157807b | ||
|
|
0afe797bae | ||
|
|
1c8797e473 | ||
|
|
e0bf6d2e55 | ||
|
|
e96d0659cb | ||
|
|
72d70236ea | ||
|
|
ee2fc8c763 | ||
|
|
b53c03bca8 | ||
|
|
9545f692ef | ||
|
|
0112bd9b5a | ||
|
|
d235576e18 | ||
|
|
3d5d5da933 | ||
|
|
9b19477e2f | ||
|
|
5a9c95f07e | ||
|
|
3000e2b72c | ||
|
|
c1f6f0398e | ||
|
|
cb6488fa05 | ||
|
|
126d90f471 | ||
|
|
98d4fb0f34 | ||
|
|
d3ee3ca53d | ||
|
|
7eac5cebf5 | ||
|
|
6a109adbc5 | ||
|
|
587847f5e9 | ||
|
|
7842cd8a41 | ||
|
|
2f9cf02932 | ||
|
|
daa796454c | ||
|
|
c531239618 | ||
|
|
f6ac7b890a | ||
|
|
229e39facf | ||
|
|
75b00ce2df | ||
|
|
4576353f26 | ||
|
|
acf4b4da63 | ||
|
|
8b5933177a | ||
|
|
a6ddd6d233 | ||
|
|
5ca5adc774 | ||
|
|
005ffe850a | ||
|
|
71cb4e8510 | ||
|
|
40244ab81b | ||
|
|
15b65b342a | ||
|
|
7df3aba71b | ||
|
|
6bb535c129 | ||
|
|
e3bf3d29f7 | ||
|
|
df9c42c1b5 | ||
|
|
7e241bb76f | ||
|
|
17fb681671 | ||
|
|
0069aee5b0 | ||
|
|
240dd1b965 | ||
|
|
88e6b2da7c | ||
|
|
6e7f4a231d | ||
|
|
822a0e56af | ||
|
|
da73c5c418 | ||
|
|
2cf8439bd1 | ||
|
|
0e404ad6ba | ||
|
|
b9f709ab30 | ||
|
|
d57c525fab | ||
|
|
9a3a104ba4 | ||
|
|
63bba13b5f | ||
|
|
d90d781740 | ||
|
|
a3bf329c44 | ||
|
|
22a12e37fa | ||
|
|
446e0422c7 | ||
|
|
5220cc1bf3 | ||
|
|
e8976b40f4 | ||
|
|
3b4b459e68 | ||
|
|
bbbdd89ade | ||
|
|
a20c1ba751 | ||
|
|
d725b5be19 | ||
|
|
545b052c10 | ||
|
|
028b9d569d | ||
|
|
85b861c4a9 | ||
|
|
762e87a82a | ||
|
|
b68e69e1a1 | ||
|
|
4764f115b1 | ||
|
|
95c99295c1 | ||
|
|
a7617fa947 | ||
|
|
2da2a47f32 | ||
|
|
8f744565e2 | ||
|
|
714512b0a3 | ||
|
|
9538c86d02 | ||
|
|
afc1ffd90b | ||
|
|
6988875e8a | ||
|
|
6e0b6171c6 | ||
|
|
53bbd93d80 | ||
|
|
75092336c4 | ||
|
|
310bdf8cb5 | ||
|
|
9435a3089a | ||
|
|
bb6dac2e84 | ||
|
|
acf34e2344 | ||
|
|
1aac4c713d | ||
|
|
bb527caa06 | ||
|
|
98bb6fd7ce | ||
|
|
b8c716ff82 | ||
|
|
9830fce760 | ||
|
|
7fccf59f50 | ||
|
|
dd79f2be60 | ||
|
|
fbdcd4b0a3 | ||
|
|
e229bc5042 | ||
|
|
44c7e8c9dc | ||
|
|
c4ffe39ec9 | ||
|
|
dc3d694d0e | ||
|
|
4f0ce77205 | ||
|
|
c28ec24c33 | ||
|
|
54db84fddc | ||
|
|
e7fd2b4c79 | ||
|
|
05640f513e | ||
|
|
b0ebdfeb65 | ||
|
|
6c01db8d81 | ||
|
|
5a3751cbac | ||
|
|
7802e30e80 | ||
|
|
899452279b | ||
|
|
566716e2fe | ||
|
|
2a42bc9450 | ||
|
|
1ef62d1b66 | ||
|
|
355773ecf3 | ||
|
|
2bb5751f33 | ||
|
|
2570c59130 | ||
|
|
2dfcda068b | ||
|
|
507133c76e | ||
|
|
a7c115877f | ||
|
|
1750a0c2e6 | ||
|
|
759ce61492 | ||
|
|
57193bd5f3 | ||
|
|
e1a1b4eab6 | ||
|
|
350894f985 | ||
|
|
0184d774c2 | ||
|
|
d136162d48 | ||
|
|
2be8ddb60d | ||
|
|
3c67f91525 | ||
|
|
c02aadfac4 | ||
|
|
2f956252ab | ||
|
|
341f16cc82 | ||
|
|
ec179182e7 | ||
|
|
b886d7bb33 | ||
|
|
a8f8f4f544 | ||
|
|
4047bf6943 | ||
|
|
a5a985fd00 | ||
|
|
444d6889de | ||
|
|
c56c69d464 | ||
|
|
4b610ba3f1 | ||
|
|
65e3b599e6 | ||
|
|
7caf211bec | ||
|
|
d4bc7c77a9 | ||
|
|
bfaa7c0fea | ||
|
|
8367de34bf | ||
|
|
ea6b78b7ca | ||
|
|
401067bfed | ||
|
|
b457daa616 | ||
|
|
54c2441934 | ||
|
|
9e8807c40d | ||
|
|
9bfbeaf93e | ||
|
|
860efefdb2 | ||
|
|
6310482b9d | ||
|
|
2d4928cd2b | ||
|
|
f3c2c0f901 | ||
|
|
1fc84c2357 | ||
|
|
13cdcedcba | ||
|
|
72f0b8ed7c | ||
|
|
95f9479d7a | ||
|
|
af095d8450 | ||
|
|
470495387c | ||
|
|
bdef1ca23c | ||
|
|
1835804e86 | ||
|
|
cb58994bdf | ||
|
|
44f3b73183 | ||
|
|
7e23fdc22a | ||
|
|
e138d2b67b | ||
|
|
3e3248fecb | ||
|
|
78ee60611a | ||
|
|
3c7aaa605b | ||
|
|
00343da266 | ||
|
|
56d09411d9 | ||
|
|
ae0df2242a | ||
|
|
5b06b28c97 | ||
|
|
c6a3bfb291 | ||
|
|
7797794cd5 | ||
|
|
d9e09a5f3d | ||
|
|
4e73c8513e | ||
|
|
9421fd7ced | ||
|
|
699de64328 | ||
|
|
6f9cbf9ca1 | ||
|
|
a097819b72 | ||
|
|
77f71b5415 | ||
|
|
ced3621dea | ||
|
|
e321d85b3c | ||
|
|
d72b40d5b0 | ||
|
|
54443a2980 | ||
|
|
00dc990974 | ||
|
|
3737aa045d | ||
|
|
b03ddf6f7d | ||
|
|
4ab89fd3e0 | ||
|
|
f1e200c0f5 | ||
|
|
218664dfcc | ||
|
|
a0f29e970d | ||
|
|
200cd66d66 | ||
|
|
dd05a8d608 | ||
|
|
299e88233c | ||
|
|
26bde1f766 | ||
|
|
d95836b881 | ||
|
|
fac81bb9ee | ||
|
|
b323abd225 | ||
|
|
b3870e5f34 | ||
|
|
29dc56c12f | ||
|
|
b62f08d500 | ||
|
|
f62177fb1a | ||
|
|
885f2998ae | ||
|
|
2afd96e11c | ||
|
|
cd92f44365 | ||
|
|
863177902a | ||
|
|
96974461e5 | ||
|
|
8895b70ffa | ||
|
|
03480ebfc7 | ||
|
|
9b8676f02e | ||
|
|
3e7738b5b1 | ||
|
|
33a235b46c | ||
|
|
137d6c1f9d | ||
|
|
1a5e820d88 | ||
|
|
0c7f9ca6bb | ||
|
|
3e6b3ce3ff | ||
|
|
ea5ba965e7 | ||
|
|
7215a550b5 | ||
|
|
3235dfa236 | ||
|
|
9baf7a7c67 | ||
|
|
cd629ef7fa | ||
|
|
9ef7c45241 | ||
|
|
fef3d09f2d | ||
|
|
53c83c585a | ||
|
|
e628c5dc3b | ||
|
|
9eaa531f66 | ||
|
|
3ffea4332e | ||
|
|
4618fd8954 | ||
|
|
791c19b5f1 | ||
|
|
7193cc6bae | ||
|
|
1845bd1e35 | ||
|
|
5f468d16b7 | ||
|
|
20a99e526d | ||
|
|
1e69f42d0f | ||
|
|
9c2f5213cb | ||
|
|
c06d5107ac | ||
|
|
1eb0f5baa5 | ||
|
|
b28189fff5 | ||
|
|
82497e4041 | ||
|
|
2a5e9c0780 | ||
|
|
c8ca67aa64 | ||
|
|
89e4cbcffe | ||
|
|
67564317fb | ||
|
|
dc2269a307 |
21
.travis.yml
21
.travis.yml
@@ -1,36 +1,27 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- '6'
|
||||
sudo: required
|
||||
dist: precise
|
||||
services:
|
||||
- mongodb
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- g++-4.8
|
||||
cache:
|
||||
directories:
|
||||
- 'node_modules'
|
||||
before_install:
|
||||
- $CXX --version
|
||||
- npm install -g npm@5
|
||||
- if [ $REQUIRES_SERVER ]; then sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10; echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list; sudo apt-get update; sudo apt-get install mongodb-org-server; fi
|
||||
install:
|
||||
- npm install &> npm.install.log || (cat npm.install.log; false)
|
||||
before_script:
|
||||
- npm run test:build
|
||||
- cp config.json.example config.json
|
||||
- sleep 15
|
||||
- sleep 5
|
||||
script:
|
||||
- npm run $TEST
|
||||
- if [ $COVERAGE ]; then ./node_modules/.bin/lcov-result-merger 'coverage/**/*.info' | ./node_modules/coveralls/bin/coveralls.js; fi
|
||||
env:
|
||||
global:
|
||||
- CXX=g++-4.8
|
||||
- DISABLE_REQUEST_LOGGING=true
|
||||
matrix:
|
||||
- TEST="lint"
|
||||
- TEST="test:api-v3" REQUIRES_SERVER=true COVERAGE=true
|
||||
- TEST="test:api-v3:unit" REQUIRES_SERVER=true COVERAGE=true
|
||||
- TEST="test:api-v3:integration" REQUIRES_SERVER=true COVERAGE=true
|
||||
- TEST="test:sanity"
|
||||
- TEST="test:content" COVERAGE=true
|
||||
- TEST="test:common" COVERAGE=true
|
||||
|
||||
@@ -15,12 +15,12 @@ ENV STRIPE_PUB_KEY pk_85fQ0yMECHNfHTSsZoxZXlPSwSNfA
|
||||
# The used solution is suggested here https://github.com/npm/npm/issues/16807#issuecomment-313591975
|
||||
RUN yarn global add npm@5
|
||||
# Install global packages
|
||||
RUN npm install -g gulp mocha
|
||||
RUN npm install -g gulp-cli mocha
|
||||
|
||||
# Clone Habitica repo and install dependencies
|
||||
RUN mkdir -p /usr/src/habitrpg
|
||||
WORKDIR /usr/src/habitrpg
|
||||
RUN git clone --branch v4.11.0 https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
|
||||
RUN git clone --branch v4.26.3 https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
|
||||
RUN npm install
|
||||
RUN gulp build:prod --force
|
||||
|
||||
|
||||
@@ -16,5 +16,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
||||
config.vm.hostname = "habitrpg"
|
||||
config.vm.network "forwarded_port", guest: 3000, host: 3000, auto_correct: true
|
||||
config.vm.usable_port_range = (3000..3050)
|
||||
config.vm.network "forwarded_port", guest: 8080, host: 8080, auto_correct: true
|
||||
config.vm.usable_port_range = (8080..8130)
|
||||
config.vm.provision :shell, :path => "vagrant_scripts/vagrant.sh"
|
||||
end
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
"CRON_SEMI_SAFE_MODE":"false",
|
||||
"MAINTENANCE_MODE": "false",
|
||||
"SESSION_SECRET":"YOUR SECRET HERE",
|
||||
"SESSION_SECRET_KEY": "1234567891234567891234567891234567891234567891234567891234567891",
|
||||
"SESSION_SECRET_IV": "12345678912345678912345678912345",
|
||||
"ADMIN_EMAIL": "you@example.com",
|
||||
"SMTP_USER":"user@example.com",
|
||||
"SMTP_PASS":"password",
|
||||
@@ -71,6 +73,7 @@
|
||||
},
|
||||
"IAP_GOOGLE_KEYDIR": "/path/to/google/public/key/dir/",
|
||||
"LOGGLY_TOKEN": "token",
|
||||
"LOGGLY_CLIENT_TOKEN": "token",
|
||||
"LOGGLY_ACCOUNT": "account",
|
||||
"PUSH_CONFIGS": {
|
||||
"GCM_SERVER_API_KEY": "",
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
web:
|
||||
volumes:
|
||||
- '.:/usr/src/habitrpg'
|
||||
version: "3"
|
||||
services:
|
||||
|
||||
client:
|
||||
volumes:
|
||||
- '.:/usr/src/habitrpg'
|
||||
|
||||
server:
|
||||
volumes:
|
||||
- '.:/usr/src/habitrpg'
|
||||
|
||||
@@ -1,13 +1,36 @@
|
||||
web:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
links:
|
||||
- mongo
|
||||
environment:
|
||||
- NODE_DB_URI=mongodb://mongo/habitrpg
|
||||
version: "3"
|
||||
services:
|
||||
|
||||
mongo:
|
||||
image: mongo
|
||||
ports:
|
||||
- "27017:27017"
|
||||
client:
|
||||
build: .
|
||||
networks:
|
||||
- habitica
|
||||
environment:
|
||||
- BASE_URL=http://server:3000
|
||||
ports:
|
||||
- "8080:8080"
|
||||
command: ["npm", "run", "client:dev"]
|
||||
depends_on:
|
||||
- server
|
||||
|
||||
server:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
networks:
|
||||
- habitica
|
||||
environment:
|
||||
- NODE_DB_URI=mongodb://mongo/habitrpg
|
||||
depends_on:
|
||||
- mongo
|
||||
|
||||
mongo:
|
||||
image: mongo
|
||||
ports:
|
||||
- "27017:27017"
|
||||
networks:
|
||||
- habitica
|
||||
|
||||
networks:
|
||||
habitica:
|
||||
driver: bridge
|
||||
|
||||
@@ -8,7 +8,7 @@ gulp.task('apidoc:clean', (done) => {
|
||||
clean(APIDOC_DEST_PATH, done);
|
||||
});
|
||||
|
||||
gulp.task('apidoc', ['apidoc:clean'], (done) => {
|
||||
gulp.task('apidoc', gulp.series('apidoc:clean', (done) => {
|
||||
let result = apidoc.createDoc({
|
||||
src: APIDOC_SRC_PATH,
|
||||
dest: APIDOC_DEST_PATH,
|
||||
@@ -19,8 +19,8 @@ gulp.task('apidoc', ['apidoc:clean'], (done) => {
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
gulp.task('apidoc:watch', ['apidoc'], () => {
|
||||
return gulp.watch(`${APIDOC_SRC_PATH}/**/*.js`, ['apidoc']);
|
||||
});
|
||||
gulp.task('apidoc:watch', gulp.series('apidoc', (done) => {
|
||||
return gulp.watch(`${APIDOC_SRC_PATH}/**/*.js`, gulp.series('apidoc', done));
|
||||
}));
|
||||
|
||||
@@ -2,12 +2,6 @@ import gulp from 'gulp';
|
||||
import babel from 'gulp-babel';
|
||||
import webpackProductionBuild from '../webpack/build';
|
||||
|
||||
gulp.task('build', () => {
|
||||
if (process.env.NODE_ENV === 'production') { // eslint-disable-line no-process-env
|
||||
gulp.start('build:prod');
|
||||
}
|
||||
});
|
||||
|
||||
gulp.task('build:src', () => {
|
||||
return gulp.src('website/server/**/*.js')
|
||||
.pipe(babel())
|
||||
@@ -20,18 +14,30 @@ gulp.task('build:common', () => {
|
||||
.pipe(gulp.dest('website/common/transpiled-babel/'));
|
||||
});
|
||||
|
||||
gulp.task('build:server', ['build:src', 'build:common']);
|
||||
gulp.task('build:server', gulp.series('build:src', 'build:common', done => done()));
|
||||
|
||||
// Client Production Build
|
||||
gulp.task('build:client', (done) => {
|
||||
webpackProductionBuild((err, output) => {
|
||||
if (err) return done(err);
|
||||
console.log(output); // eslint-disable-line no-console
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task('build:prod', [
|
||||
gulp.task('build:prod', gulp.series(
|
||||
'build:server',
|
||||
'build:client',
|
||||
'apidoc',
|
||||
]);
|
||||
done => done()
|
||||
));
|
||||
|
||||
let buildArgs = [];
|
||||
|
||||
if (process.env.NODE_ENV === 'production') { // eslint-disable-line no-process-env
|
||||
buildArgs.push('build:prod');
|
||||
}
|
||||
|
||||
gulp.task('build', gulp.series(buildArgs, (done) => {
|
||||
done();
|
||||
}));
|
||||
@@ -1,5 +1,4 @@
|
||||
import mongoose from 'mongoose';
|
||||
import autoinc from 'mongoose-id-autoinc';
|
||||
import logger from '../website/server/libs/logger';
|
||||
import nconf from 'nconf';
|
||||
import repl from 'repl';
|
||||
@@ -25,23 +24,23 @@ let improveRepl = (context) => {
|
||||
|
||||
const isProd = nconf.get('NODE_ENV') === 'production';
|
||||
const mongooseOptions = !isProd ? {} : {
|
||||
replset: { socketOptions: { keepAlive: 1, connectTimeoutMS: 30000 } },
|
||||
server: { socketOptions: { keepAlive: 1, connectTimeoutMS: 30000 } },
|
||||
keepAlive: 1,
|
||||
connectTimeoutMS: 30000,
|
||||
useMongoClient: true,
|
||||
};
|
||||
autoinc.init(
|
||||
mongoose.connect(
|
||||
nconf.get('NODE_DB_URI'),
|
||||
mongooseOptions,
|
||||
(err) => {
|
||||
if (err) throw err;
|
||||
logger.info('Connected with Mongoose');
|
||||
}
|
||||
)
|
||||
mongoose.connect(
|
||||
nconf.get('NODE_DB_URI'),
|
||||
mongooseOptions,
|
||||
(err) => {
|
||||
if (err) throw err;
|
||||
logger.info('Connected with Mongoose');
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
gulp.task('console', () => {
|
||||
gulp.task('console', (done) => {
|
||||
improveRepl(repl.start({
|
||||
prompt: 'Habitica > ',
|
||||
}).context);
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import mergeStream from 'merge-stream';
|
||||
import {basename} from 'path';
|
||||
import {sync} from 'glob';
|
||||
import {each} from 'lodash';
|
||||
import vinylBuffer from 'vinyl-buffer';
|
||||
|
||||
// https://github.com/Ensighten/grunt-spritesmith/issues/67#issuecomment-34786248
|
||||
const MAX_SPRITESHEET_SIZE = 1024 * 1024 * 3;
|
||||
@@ -104,6 +105,7 @@ function createSpritesStream (name, src) {
|
||||
}));
|
||||
|
||||
let imgStream = spriteData.img
|
||||
.pipe(vinylBuffer())
|
||||
.pipe(imagemin())
|
||||
.pipe(gulp.dest(IMG_DIST_PATH));
|
||||
|
||||
@@ -117,8 +119,6 @@ function createSpritesStream (name, src) {
|
||||
return stream;
|
||||
}
|
||||
|
||||
gulp.task('sprites:compile', ['sprites:clean', 'sprites:main', 'sprites:largeSprites', 'sprites:checkCompiledDimensions']);
|
||||
|
||||
gulp.task('sprites:main', () => {
|
||||
let mainSrc = sync('website/raw_sprites/spritesmith/**/*.png');
|
||||
return createSpritesStream('main', mainSrc);
|
||||
@@ -133,7 +133,7 @@ gulp.task('sprites:clean', (done) => {
|
||||
clean(`${IMG_DIST_PATH}spritesmith*,${CSS_DIST_PATH}spritesmith*}`, done);
|
||||
});
|
||||
|
||||
gulp.task('sprites:checkCompiledDimensions', ['sprites:main', 'sprites:largeSprites'], () => {
|
||||
gulp.task('sprites:checkCompiledDimensions', gulp.series('sprites:main', 'sprites:largeSprites', (done) => {
|
||||
console.log('Verifiying that images do not exceed max dimensions'); // eslint-disable-line no-console
|
||||
|
||||
let numberOfSheetsThatAreTooBig = 0;
|
||||
@@ -159,4 +159,7 @@ gulp.task('sprites:checkCompiledDimensions', ['sprites:main', 'sprites:largeSpri
|
||||
} else {
|
||||
console.log('All images are within the correct dimensions'); // eslint-disable-line no-console
|
||||
}
|
||||
});
|
||||
done();
|
||||
}));
|
||||
|
||||
gulp.task('sprites:compile', gulp.series('sprites:clean', 'sprites:main', 'sprites:largeSprites', 'sprites:checkCompiledDimensions', done => done()));
|
||||
|
||||
@@ -3,7 +3,7 @@ import nodemon from 'gulp-nodemon';
|
||||
|
||||
let pkg = require('../package.json');
|
||||
|
||||
gulp.task('nodemon', () => {
|
||||
gulp.task('nodemon', (done) => {
|
||||
nodemon({
|
||||
script: pkg.main,
|
||||
ignore: [
|
||||
@@ -12,4 +12,5 @@ gulp.task('nodemon', () => {
|
||||
'common/dist/script/content/*',
|
||||
],
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
import mongoose from 'mongoose';
|
||||
import { exec } from 'child_process';
|
||||
import gulp from 'gulp';
|
||||
import runSequence from 'run-sequence';
|
||||
import os from 'os';
|
||||
import nconf from 'nconf';
|
||||
|
||||
@@ -39,23 +38,23 @@ let testBin = (string, additionalEnvVariables = '') => {
|
||||
}
|
||||
};
|
||||
|
||||
gulp.task('test:nodemon', () => {
|
||||
gulp.task('test:nodemon', gulp.series(function setupNodemon (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
|
||||
|
||||
runSequence('nodemon');
|
||||
});
|
||||
done();
|
||||
}, 'nodemon'));
|
||||
|
||||
gulp.task('test:prepare:mongo', (cb) => {
|
||||
mongoose.connect(TEST_DB_URI, (err) => {
|
||||
if (err) return cb(`Unable to connect to mongo database. Are you sure it's running? \n\n${err}`);
|
||||
mongoose.connection.db.dropDatabase();
|
||||
mongoose.connection.close();
|
||||
cb();
|
||||
mongoose.connection.dropDatabase((err2) => {
|
||||
if (err2) return cb(err2);
|
||||
mongoose.connection.close(cb);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task('test:prepare:server', ['test:prepare:mongo'], () => {
|
||||
gulp.task('test:prepare:server', gulp.series('test:prepare:mongo', (done) => {
|
||||
if (!server) {
|
||||
server = exec(testBin('node ./website/server/index.js', `NODE_DB_URI=${TEST_DB_URI} PORT=${TEST_SERVER_PORT}`), (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
@@ -64,16 +63,18 @@ gulp.task('test:prepare:server', ['test:prepare:mongo'], () => {
|
||||
if (stderr) {
|
||||
console.error(stderr); // eslint-disable-line no-console
|
||||
}
|
||||
done();
|
||||
});
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
gulp.task('test:prepare:build', ['build']);
|
||||
gulp.task('test:prepare:build', gulp.series('build', done => done()));
|
||||
|
||||
gulp.task('test:prepare', [
|
||||
gulp.task('test:prepare', gulp.series(
|
||||
'test:prepare:build',
|
||||
'test:prepare:mongo',
|
||||
]);
|
||||
done => done()
|
||||
));
|
||||
|
||||
gulp.task('test:sanity', (cb) => {
|
||||
let runner = exec(
|
||||
@@ -88,7 +89,7 @@ gulp.task('test:sanity', (cb) => {
|
||||
pipe(runner);
|
||||
});
|
||||
|
||||
gulp.task('test:common', ['test:prepare:build'], (cb) => {
|
||||
gulp.task('test:common', gulp.series('test:prepare:build', (cb) => {
|
||||
let runner = exec(
|
||||
testBin(COMMON_TEST_COMMAND),
|
||||
(err) => {
|
||||
@@ -99,17 +100,17 @@ gulp.task('test:common', ['test:prepare:build'], (cb) => {
|
||||
}
|
||||
);
|
||||
pipe(runner);
|
||||
});
|
||||
}));
|
||||
|
||||
gulp.task('test:common:clean', (cb) => {
|
||||
pipe(exec(testBin(COMMON_TEST_COMMAND), () => cb()));
|
||||
});
|
||||
|
||||
gulp.task('test:common:watch', ['test:common:clean'], () => {
|
||||
gulp.watch(['common/script/**/*', 'test/common/**/*'], ['test:common:clean']);
|
||||
});
|
||||
gulp.task('test:common:watch', gulp.series('test:common:clean', () => {
|
||||
return gulp.watch(['common/script/**/*', 'test/common/**/*'], gulp.series('test:common:clean', done => done()));
|
||||
}));
|
||||
|
||||
gulp.task('test:common:safe', ['test:prepare:build'], (cb) => {
|
||||
gulp.task('test:common:safe', gulp.series('test:prepare:build', (cb) => {
|
||||
let runner = exec(
|
||||
testBin(COMMON_TEST_COMMAND),
|
||||
(err, stdout) => { // eslint-disable-line handle-callback-err
|
||||
@@ -123,9 +124,9 @@ gulp.task('test:common:safe', ['test:prepare:build'], (cb) => {
|
||||
}
|
||||
);
|
||||
pipe(runner);
|
||||
});
|
||||
}));
|
||||
|
||||
gulp.task('test:content', ['test:prepare:build'], (cb) => {
|
||||
gulp.task('test:content', gulp.series('test:prepare:build', (cb) => {
|
||||
let runner = exec(
|
||||
testBin(CONTENT_TEST_COMMAND),
|
||||
CONTENT_OPTIONS,
|
||||
@@ -137,17 +138,17 @@ gulp.task('test:content', ['test:prepare:build'], (cb) => {
|
||||
}
|
||||
);
|
||||
pipe(runner);
|
||||
});
|
||||
}));
|
||||
|
||||
gulp.task('test:content:clean', (cb) => {
|
||||
pipe(exec(testBin(CONTENT_TEST_COMMAND), CONTENT_OPTIONS, () => cb()));
|
||||
});
|
||||
|
||||
gulp.task('test:content:watch', ['test:content:clean'], () => {
|
||||
gulp.watch(['common/script/content/**', 'test/**'], ['test:content:clean']);
|
||||
});
|
||||
gulp.task('test:content:watch', gulp.series('test:content:clean', () => {
|
||||
return gulp.watch(['common/script/content/**', 'test/**'], gulp.series('test:content:clean', done => done()));
|
||||
}));
|
||||
|
||||
gulp.task('test:content:safe', ['test:prepare:build'], (cb) => {
|
||||
gulp.task('test:content:safe', gulp.series('test:prepare:build', (cb) => {
|
||||
let runner = exec(
|
||||
testBin(CONTENT_TEST_COMMAND),
|
||||
CONTENT_OPTIONS,
|
||||
@@ -162,7 +163,7 @@ gulp.task('test:content:safe', ['test:prepare:build'], (cb) => {
|
||||
}
|
||||
);
|
||||
pipe(runner);
|
||||
});
|
||||
}));
|
||||
|
||||
gulp.task('test:api-v3:unit', (done) => {
|
||||
let runner = exec(
|
||||
@@ -179,7 +180,7 @@ gulp.task('test:api-v3:unit', (done) => {
|
||||
});
|
||||
|
||||
gulp.task('test:api-v3:unit:watch', () => {
|
||||
gulp.watch(['website/server/libs/*', 'test/api/v3/unit/**/*', 'website/server/controllers/**/*'], ['test:api-v3:unit']);
|
||||
return gulp.watch(['website/server/libs/*', 'test/api/v3/unit/**/*', 'website/server/controllers/**/*'], gulp.series('test:api-v3:unit', done => done()));
|
||||
});
|
||||
|
||||
gulp.task('test:api-v3:integration', (done) => {
|
||||
@@ -198,8 +199,10 @@ gulp.task('test:api-v3:integration', (done) => {
|
||||
});
|
||||
|
||||
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/**/*'], ['test:api-v3:integration']);
|
||||
return 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) => {
|
||||
@@ -212,21 +215,17 @@ gulp.task('test:api-v3:integration:separate-server', (done) => {
|
||||
pipe(runner);
|
||||
});
|
||||
|
||||
gulp.task('test', (done) => {
|
||||
runSequence(
|
||||
'test:sanity',
|
||||
'test:content',
|
||||
'test:common',
|
||||
'test:api-v3:unit',
|
||||
'test:api-v3:integration',
|
||||
done
|
||||
);
|
||||
});
|
||||
gulp.task('test', gulp.series(
|
||||
'test:sanity',
|
||||
'test:content',
|
||||
'test:common',
|
||||
'test:api-v3:unit',
|
||||
'test:api-v3:integration',
|
||||
done => done()
|
||||
));
|
||||
|
||||
gulp.task('test:api-v3', (done) => {
|
||||
runSequence(
|
||||
'test:api-v3:unit',
|
||||
'test:api-v3:integration',
|
||||
done
|
||||
);
|
||||
});
|
||||
gulp.task('test:api-v3', gulp.series(
|
||||
'test:api-v3:unit',
|
||||
'test:api-v3:integration',
|
||||
done => done()
|
||||
));
|
||||
|
||||
@@ -93,9 +93,7 @@ const malformedStringExceptions = {
|
||||
feedPet: true,
|
||||
};
|
||||
|
||||
gulp.task('transifex', ['transifex:missingFiles', 'transifex:missingStrings', 'transifex:malformedStrings']);
|
||||
|
||||
gulp.task('transifex:missingFiles', () => {
|
||||
gulp.task('transifex:missingFiles', (done) => {
|
||||
let missingStrings = [];
|
||||
|
||||
eachTranslationFile(ALL_LANGUAGES, (error) => {
|
||||
@@ -109,9 +107,10 @@ gulp.task('transifex:missingFiles', () => {
|
||||
let formattedMessage = formatMessageForPosting(message, missingStrings);
|
||||
postToSlack(formattedMessage, SLACK_CONFIG);
|
||||
}
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task('transifex:missingStrings', () => {
|
||||
gulp.task('transifex:missingStrings', (done) => {
|
||||
let missingStrings = [];
|
||||
|
||||
eachTranslationString(ALL_LANGUAGES, (language, filename, key, englishString, translationString) => {
|
||||
@@ -126,9 +125,10 @@ gulp.task('transifex:missingStrings', () => {
|
||||
let formattedMessage = formatMessageForPosting(message, missingStrings);
|
||||
postToSlack(formattedMessage, SLACK_CONFIG);
|
||||
}
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task('transifex:malformedStrings', () => {
|
||||
gulp.task('transifex:malformedStrings', (done) => {
|
||||
let jsonFiles = stripOutNonJsonFiles(fs.readdirSync(ENGLISH_LOCALE));
|
||||
let interpolationRegex = /<%= [a-zA-Z]* %>/g;
|
||||
let stringsToLookFor = getStringsWith(jsonFiles, interpolationRegex);
|
||||
@@ -170,4 +170,11 @@ gulp.task('transifex:malformedStrings', () => {
|
||||
let formattedMessage = formatMessageForPosting(message, stringsWithIncorrectNumberOfInterpolations);
|
||||
postToSlack(formattedMessage, SLACK_CONFIG);
|
||||
}
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task(
|
||||
'transifex',
|
||||
gulp.series('transifex:missingFiles', 'transifex:missingStrings', 'transifex:malformedStrings'),
|
||||
(done) => done()
|
||||
);
|
||||
@@ -8,10 +8,12 @@
|
||||
|
||||
require('babel-register');
|
||||
|
||||
const gulp = require('gulp');
|
||||
|
||||
if (process.env.NODE_ENV === 'production') { // eslint-disable-line no-process-env
|
||||
require('./gulp/gulp-apidoc'); // eslint-disable-line global-require
|
||||
require('./gulp/gulp-build'); // eslint-disable-line global-require
|
||||
} else {
|
||||
require('glob').sync('./gulp/gulp-*').forEach(require); // eslint-disable-line global-require
|
||||
require('gulp').task('default', ['test']); // eslint-disable-line global-require
|
||||
require('gulp').task('default', gulp.series('test')); // eslint-disable-line global-require
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ var migrationName = '20140831_increase_gems_for_previous_contributions';
|
||||
* https://github.com/HabitRPG/habitrpg/issues/3933
|
||||
* Increase Number of Gems for Contributors
|
||||
* author: Alys (d904bd62-da08-416b-a816-ba797c9ee265)
|
||||
*
|
||||
*
|
||||
* Increase everyone's gems per their contribution level.
|
||||
* Originally they were given 2 gems per tier.
|
||||
* Now they are given 3 gems per tier for tiers 1,2,3
|
||||
@@ -70,7 +70,7 @@ dbUsers.findEach(query, fields, function(err, user) {
|
||||
var extraGems = tier; // tiers 1,2,3
|
||||
if (tier > 3) { extraGems = 3 + (tier - 3) * 2; }
|
||||
if (tier == 8) { extraGems = 11; }
|
||||
extraBalance = extraGems / 4;
|
||||
var extraBalance = extraGems / 4;
|
||||
set['balance'] = user.balance + extraBalance;
|
||||
|
||||
// Capture current state of user:
|
||||
|
||||
@@ -39,7 +39,7 @@ function findUsers(gt){
|
||||
console.log('User: ', countUsers, user._id);
|
||||
|
||||
var update = {
|
||||
$set: {};
|
||||
$set: {}
|
||||
};
|
||||
|
||||
if(user.auth && user.auth.local) {
|
||||
@@ -60,4 +60,4 @@ function findUsers(gt){
|
||||
});
|
||||
};
|
||||
|
||||
findUsers();
|
||||
findUsers();
|
||||
|
||||
128
migrations/20171117_turkey_ladder.js
Normal file
128
migrations/20171117_turkey_ladder.js
Normal file
@@ -0,0 +1,128 @@
|
||||
var migrationName = '20171117_turkey_ladder.js';
|
||||
var authorName = 'Sabe'; // in case script author needs to know when their ...
|
||||
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
|
||||
|
||||
/*
|
||||
* Award the Turkey Day ladder:
|
||||
* Grant Turkey Costume to those who have the Gilded Turkey mount
|
||||
* Grant Gilded Turkey mount to those who have the Gilded Turkey pet
|
||||
* Grant Gilded Turkey pet to those who have the Base Turkey mount
|
||||
* Grant Base Turkey mount to those who have the Base Turkey pet
|
||||
* Grant Base Turkey pet to those who have none of the above yet
|
||||
*/
|
||||
|
||||
var monk = require('monk');
|
||||
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
|
||||
var dbUsers = monk(connectionString).get('users', { castIds: false });
|
||||
|
||||
function processUsers(lastId) {
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
var query = {
|
||||
'migration':{$ne:migrationName},
|
||||
'auth.timestamps.loggedin':{$gt:new Date('2017-11-01')},
|
||||
};
|
||||
|
||||
if (lastId) {
|
||||
query._id = {
|
||||
$gt: lastId
|
||||
}
|
||||
}
|
||||
|
||||
dbUsers.find(query, {
|
||||
sort: {_id: 1},
|
||||
limit: 250,
|
||||
fields: [
|
||||
'items.pets',
|
||||
'items.mounts',
|
||||
] // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
|
||||
})
|
||||
.then(updateUsers)
|
||||
.catch(function (err) {
|
||||
console.log(err);
|
||||
return exiting(1, 'ERROR! ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
var progressCount = 1000;
|
||||
var count = 0;
|
||||
|
||||
function updateUsers (users) {
|
||||
if (!users || users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
displayData();
|
||||
return;
|
||||
}
|
||||
|
||||
var userPromises = users.map(updateUser);
|
||||
var lastUser = users[users.length - 1];
|
||||
|
||||
return Promise.all(userPromises)
|
||||
.then(function () {
|
||||
processUsers(lastUser._id);
|
||||
});
|
||||
}
|
||||
|
||||
function updateUser (user) {
|
||||
count++;
|
||||
|
||||
var set = {};
|
||||
|
||||
if (user && user.items && user.items.mounts && user.items.mounts['Turkey-Gilded']) {
|
||||
set = {
|
||||
migration: migrationName,
|
||||
'items.gear.owned.head_special_turkeyHelmBase': false,
|
||||
'items.gear.owned.armor_special_turkeyArmorBase': false,
|
||||
'items.gear.owned.back_special_turkeyTailBase': false,
|
||||
};
|
||||
var push = [
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.head_special_turkeyHelmBase',
|
||||
_id: monk.id(),
|
||||
},
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.armor_special_turkeyArmorBase',
|
||||
_id: monk.id(),
|
||||
},
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.back_special_turkeyTailBase',
|
||||
_id: monk.id(),
|
||||
},
|
||||
];
|
||||
} else if (user && user.items && user.items.pets && user.items.pets['Turkey-Gilded']) {
|
||||
set = {'migration':migrationName, 'items.mounts.Turkey-Gilded':true};
|
||||
} else if (user && user.items && user.items.mounts && user.items.mounts['Turkey-Base']) {
|
||||
set = {'migration':migrationName, 'items.pets.Turkey-Gilded':5};
|
||||
} else if (user && user.items && user.items.pets && user.items.pets['Turkey-Base']) {
|
||||
set = {'migration':migrationName, 'items.mounts.Turkey-Base':true};
|
||||
} else {
|
||||
set = {'migration':migrationName, 'items.pets.Turkey-Base':5};
|
||||
}
|
||||
|
||||
dbUsers.update({_id: user._id}, {$set: set});
|
||||
if (push) {
|
||||
dbUsers.update({_id: user._id}, {$push: {pinnedItems: {$each: push}}});
|
||||
}
|
||||
|
||||
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
|
||||
if (user._id == authorUuid) console.warn(authorName + ' processed');
|
||||
}
|
||||
|
||||
function displayData() {
|
||||
console.warn('\n' + count + ' users processed\n');
|
||||
return exiting(0);
|
||||
}
|
||||
|
||||
function exiting(code, msg) {
|
||||
code = code || 0; // 0 = success
|
||||
if (code && !msg) { msg = 'ERROR!'; }
|
||||
if (msg) {
|
||||
if (code) { console.error(msg); }
|
||||
else { console.log( msg); }
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
module.exports = processUsers;
|
||||
103
migrations/20171230_nye_hats.js
Normal file
103
migrations/20171230_nye_hats.js
Normal file
@@ -0,0 +1,103 @@
|
||||
var migrationName = '20171230_nye_hats.js';
|
||||
var authorName = 'Sabe'; // in case script author needs to know when their ...
|
||||
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
|
||||
|
||||
/*
|
||||
* Award New Year's Eve party hats to users in sequence
|
||||
*/
|
||||
|
||||
var monk = require('monk');
|
||||
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
|
||||
var dbUsers = monk(connectionString).get('users', { castIds: false });
|
||||
|
||||
function processUsers(lastId) {
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
var query = {
|
||||
'migration': {$ne:migrationName},
|
||||
'auth.timestamps.loggedin': {$gt:new Date('2017-11-30')},
|
||||
};
|
||||
|
||||
if (lastId) {
|
||||
query._id = {
|
||||
$gt: lastId
|
||||
}
|
||||
}
|
||||
|
||||
dbUsers.find(query, {
|
||||
sort: {_id: 1},
|
||||
limit: 250,
|
||||
fields: [
|
||||
'items.gear.owned',
|
||||
] // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
|
||||
})
|
||||
.then(updateUsers)
|
||||
.catch(function (err) {
|
||||
console.log(err);
|
||||
return exiting(1, 'ERROR! ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
var progressCount = 1000;
|
||||
var count = 0;
|
||||
|
||||
function updateUsers (users) {
|
||||
if (!users || users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
displayData();
|
||||
return;
|
||||
}
|
||||
|
||||
var userPromises = users.map(updateUser);
|
||||
var lastUser = users[users.length - 1];
|
||||
|
||||
return Promise.all(userPromises)
|
||||
.then(function () {
|
||||
processUsers(lastUser._id);
|
||||
});
|
||||
}
|
||||
|
||||
function updateUser (user) {
|
||||
count++;
|
||||
|
||||
var set = {};
|
||||
var push = {};
|
||||
|
||||
if (typeof user.items.gear.owned.head_special_nye2016 !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.head_special_nye2017':false};
|
||||
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.head_special_nye2017', '_id': monk.id()}};
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2015 !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.head_special_nye2016':false};
|
||||
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.head_special_nye2016', '_id': monk.id()}};
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2014 !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.head_special_nye2015':false};
|
||||
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.head_special_nye2015', '_id': monk.id()}};
|
||||
} else if (typeof user.items.gear.owned.head_special_nye !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.head_special_nye2014':false};
|
||||
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.head_special_nye2014', '_id': monk.id()}};
|
||||
} else {
|
||||
set = {'migration':migrationName, 'items.gear.owned.head_special_nye':false};
|
||||
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.head_special_nye', '_id': monk.id()}};
|
||||
}
|
||||
|
||||
dbUsers.update({_id: user._id}, {$set: set, $push: push});
|
||||
|
||||
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
|
||||
if (user._id == authorUuid) console.warn(authorName + ' processed');
|
||||
}
|
||||
|
||||
function displayData() {
|
||||
console.warn('\n' + count + ' users processed\n');
|
||||
return exiting(0);
|
||||
}
|
||||
|
||||
function exiting(code, msg) {
|
||||
code = code || 0; // 0 = success
|
||||
if (code && !msg) { msg = 'ERROR!'; }
|
||||
if (msg) {
|
||||
if (code) { console.error(msg); }
|
||||
else { console.log( msg); }
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
module.exports = processUsers;
|
||||
79
migrations/20180110_nextPaymentProcessing.js
Normal file
79
migrations/20180110_nextPaymentProcessing.js
Normal file
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Convert purchased.plan.nextPaymentProcessing from a double to a date field for Apple subscribers
|
||||
*/
|
||||
|
||||
var monk = require('monk');
|
||||
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
|
||||
var dbUsers = monk(connectionString).get('users', { castIds: false });
|
||||
|
||||
function processUsers(lastId) {
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
var query = {
|
||||
'purchased.plan.paymentMethod': "Apple",
|
||||
'purchased.plan.nextPaymentProcessing': {$type: 'double'},
|
||||
};
|
||||
|
||||
if (lastId) {
|
||||
query._id = {
|
||||
$gt: lastId
|
||||
}
|
||||
}
|
||||
|
||||
dbUsers.find(query, {
|
||||
sort: {_id: 1},
|
||||
limit: 250,
|
||||
})
|
||||
.then(updateUsers)
|
||||
.catch(function (err) {
|
||||
console.log(err);
|
||||
return exiting(1, 'ERROR! ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
var progressCount = 100;
|
||||
var count = 0;
|
||||
|
||||
function updateUsers (users) {
|
||||
if (!users || users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
displayData();
|
||||
return;
|
||||
}
|
||||
|
||||
var userPromises = users.map(updateUser);
|
||||
var lastUser = users[users.length - 1];
|
||||
|
||||
return Promise.all(userPromises)
|
||||
.then(function () {
|
||||
processUsers(lastUser._id);
|
||||
});
|
||||
}
|
||||
|
||||
function updateUser (user) {
|
||||
count++;
|
||||
|
||||
var set = {
|
||||
'purchased.plan.nextPaymentProcessing': new Date(user.purchased.plan.nextPaymentProcessing),
|
||||
};
|
||||
|
||||
dbUsers.update({_id: user._id}, {$set: set});
|
||||
|
||||
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
|
||||
}
|
||||
|
||||
function displayData() {
|
||||
console.warn('\n' + count + ' users processed\n');
|
||||
return exiting(0);
|
||||
}
|
||||
|
||||
function exiting(code, msg) {
|
||||
code = code || 0; // 0 = success
|
||||
if (code && !msg) { msg = 'ERROR!'; }
|
||||
if (msg) {
|
||||
if (code) { console.error(msg); }
|
||||
else { console.log( msg); }
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
module.exports = processUsers;
|
||||
93
migrations/20180125_clean_new_notifications.js
Normal file
93
migrations/20180125_clean_new_notifications.js
Normal file
@@ -0,0 +1,93 @@
|
||||
const UserNotification = require('../website/server/models/userNotification').model;
|
||||
const content = require('../website/common/script/content/index');
|
||||
|
||||
const migrationName = '20180125_clean_new_migrations';
|
||||
const authorName = 'paglias'; // in case script author needs to know when their ...
|
||||
const authorUuid = 'ed4c688c-6652-4a92-9d03-a5a79844174a'; // ... own data is done
|
||||
|
||||
/*
|
||||
* Clean new migration types for processed users
|
||||
*/
|
||||
|
||||
const monk = require('monk');
|
||||
const connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
|
||||
const dbUsers = monk(connectionString).get('users', { castIds: false });
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
function updateUser (user) {
|
||||
count++;
|
||||
|
||||
const types = ['NEW_MYSTERY_ITEMS', 'CARD_RECEIVED', 'NEW_CHAT_MESSAGE'];
|
||||
|
||||
dbUsers.update({_id: user._id}, {
|
||||
$pull: {notifications: { type: {$in: types } } },
|
||||
$set: {migration: migrationName},
|
||||
});
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count } ${ user._id}`);
|
||||
if (user._id === authorUuid) console.warn(`${authorName } processed`);
|
||||
}
|
||||
|
||||
function exiting (code, msg) {
|
||||
code = code || 0; // 0 = success
|
||||
if (code && !msg) {
|
||||
msg = 'ERROR!';
|
||||
}
|
||||
if (msg) {
|
||||
if (code) {
|
||||
console.error(msg);
|
||||
} else {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
function displayData () {
|
||||
console.warn(`\n${ count } users processed\n`);
|
||||
return exiting(0);
|
||||
}
|
||||
|
||||
function updateUsers (users) {
|
||||
if (!users || users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
displayData();
|
||||
return;
|
||||
}
|
||||
|
||||
const userPromises = users.map(updateUser);
|
||||
const lastUser = users[users.length - 1];
|
||||
|
||||
return Promise.all(userPromises)
|
||||
.then(() => {
|
||||
processUsers(lastUser._id);
|
||||
});
|
||||
}
|
||||
|
||||
function processUsers (lastId) {
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
const query = {
|
||||
migration: {$ne: migrationName},
|
||||
'auth.timestamps.loggedin': {$gt: new Date('2010-01-24')},
|
||||
};
|
||||
|
||||
if (lastId) {
|
||||
query._id = {
|
||||
$gt: lastId,
|
||||
};
|
||||
}
|
||||
|
||||
dbUsers.find(query, {
|
||||
sort: {_id: 1},
|
||||
limit: 250,
|
||||
})
|
||||
.then(updateUsers)
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
return exiting(1, `ERROR! ${ err}`);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = processUsers;
|
||||
149
migrations/20180125_notifications.js
Normal file
149
migrations/20180125_notifications.js
Normal file
@@ -0,0 +1,149 @@
|
||||
const UserNotification = require('../website/server/models/userNotification').model;
|
||||
const content = require('../website/common/script/content/index');
|
||||
|
||||
const migrationName = '20180125_migrations-v2';
|
||||
const authorName = 'paglias'; // in case script author needs to know when their ...
|
||||
const authorUuid = 'ed4c688c-6652-4a92-9d03-a5a79844174a'; // ... own data is done
|
||||
|
||||
/*
|
||||
* Migrate to new notifications system
|
||||
*/
|
||||
|
||||
const monk = require('monk');
|
||||
const connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
|
||||
const dbUsers = monk(connectionString).get('users', { castIds: false });
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
function updateUser (user) {
|
||||
count++;
|
||||
|
||||
const notifications = [];
|
||||
|
||||
// UNALLOCATED_STATS_POINTS skipped because added on each save
|
||||
// NEW_STUFF skipped because it's a new type
|
||||
// GROUP_TASK_NEEDS_WORK because it's a new type
|
||||
// NEW_INBOX_MESSAGE not implemented yet
|
||||
|
||||
|
||||
// NEW_MYSTERY_ITEMS
|
||||
const mysteryItems = user.purchased && user.purchased.plan && user.purchased.plan.mysteryItems;
|
||||
if (Array.isArray(mysteryItems) && mysteryItems.length > 0) {
|
||||
const newMysteryNotif = new UserNotification({
|
||||
type: 'NEW_MYSTERY_ITEMS',
|
||||
data: {
|
||||
items: mysteryItems,
|
||||
},
|
||||
}).toJSON();
|
||||
notifications.push(newMysteryNotif);
|
||||
}
|
||||
|
||||
// CARD_RECEIVED
|
||||
Object.keys(content.cardTypes).forEach(cardType => {
|
||||
const existingCards = user.items.special[`${cardType}Received`] || [];
|
||||
existingCards.forEach(sender => {
|
||||
const newNotif = new UserNotification({
|
||||
type: 'CARD_RECEIVED',
|
||||
data: {
|
||||
card: cardType,
|
||||
from: {
|
||||
// id is missing in old notifications
|
||||
name: sender,
|
||||
},
|
||||
},
|
||||
}).toJSON();
|
||||
|
||||
notifications.push(newNotif);
|
||||
});
|
||||
});
|
||||
|
||||
// NEW_CHAT_MESSAGE
|
||||
Object.keys(user.newMessages).forEach(groupId => {
|
||||
const existingNotif = user.newMessages[groupId];
|
||||
|
||||
if (existingNotif) {
|
||||
const newNotif = new UserNotification({
|
||||
type: 'NEW_CHAT_MESSAGE',
|
||||
data: {
|
||||
group: {
|
||||
id: groupId,
|
||||
name: existingNotif.name,
|
||||
},
|
||||
},
|
||||
}).toJSON();
|
||||
|
||||
notifications.push(newNotif);
|
||||
}
|
||||
});
|
||||
|
||||
dbUsers.update({_id: user._id}, {
|
||||
$push: {notifications: { $each: notifications } },
|
||||
$set: {migration: migrationName},
|
||||
});
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count } ${ user._id}`);
|
||||
if (user._id === authorUuid) console.warn(`${authorName } processed`);
|
||||
}
|
||||
|
||||
function exiting (code, msg) {
|
||||
code = code || 0; // 0 = success
|
||||
if (code && !msg) {
|
||||
msg = 'ERROR!';
|
||||
}
|
||||
if (msg) {
|
||||
if (code) {
|
||||
console.error(msg);
|
||||
} else {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
function displayData () {
|
||||
console.warn(`\n${ count } users processed\n`);
|
||||
return exiting(0);
|
||||
}
|
||||
|
||||
function updateUsers (users) {
|
||||
if (!users || users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
displayData();
|
||||
return;
|
||||
}
|
||||
|
||||
const userPromises = users.map(updateUser);
|
||||
const lastUser = users[users.length - 1];
|
||||
|
||||
return Promise.all(userPromises)
|
||||
.then(() => {
|
||||
processUsers(lastUser._id);
|
||||
});
|
||||
}
|
||||
|
||||
function processUsers (lastId) {
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
const query = {
|
||||
migration: {$ne: migrationName},
|
||||
'auth.timestamps.loggedin': {$gt: new Date('2010-01-24')},
|
||||
};
|
||||
|
||||
if (lastId) {
|
||||
query._id = {
|
||||
$gt: lastId,
|
||||
};
|
||||
}
|
||||
|
||||
dbUsers.find(query, {
|
||||
sort: {_id: 1},
|
||||
limit: 250,
|
||||
})
|
||||
.then(updateUsers)
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
return exiting(1, `ERROR! ${ err}`);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = processUsers;
|
||||
118
migrations/20180130_habit_birthday.js
Normal file
118
migrations/20180130_habit_birthday.js
Normal file
@@ -0,0 +1,118 @@
|
||||
var migrationName = '20180130_habit_birthday.js';
|
||||
var authorName = 'Sabe'; // in case script author needs to know when their ...
|
||||
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
|
||||
|
||||
/*
|
||||
* Award party robes: most recent user doesn't have of 2014-2018. Also cake!
|
||||
*/
|
||||
|
||||
var monk = require('monk');
|
||||
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
|
||||
var dbUsers = monk(connectionString).get('users', { castIds: false });
|
||||
|
||||
function processUsers(lastId) {
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
var query = {
|
||||
'migration':{$ne:migrationName},
|
||||
'auth.timestamps.loggedin':{$gt:new Date('2018-01-01')}, // remove after first run to cover remaining users
|
||||
};
|
||||
|
||||
if (lastId) {
|
||||
query._id = {
|
||||
$gt: lastId
|
||||
}
|
||||
}
|
||||
|
||||
dbUsers.find(query, {
|
||||
sort: {_id: 1},
|
||||
limit: 250,
|
||||
fields: [ // specify fields we are interested in to limit retrieved data (empty if we're not reading data)
|
||||
'items.gear.owned'
|
||||
],
|
||||
})
|
||||
.then(updateUsers)
|
||||
.catch(function (err) {
|
||||
console.log(err);
|
||||
return exiting(1, 'ERROR! ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
var progressCount = 1000;
|
||||
var count = 0;
|
||||
|
||||
function updateUsers (users) {
|
||||
if (!users || users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
displayData();
|
||||
return;
|
||||
}
|
||||
|
||||
var userPromises = users.map(updateUser);
|
||||
var lastUser = users[users.length - 1];
|
||||
|
||||
return Promise.all(userPromises)
|
||||
.then(function () {
|
||||
processUsers(lastUser._id);
|
||||
});
|
||||
}
|
||||
|
||||
function updateUser (user) {
|
||||
count++;
|
||||
|
||||
var push;
|
||||
var set = {'migration':migrationName};
|
||||
|
||||
if (user.items && user.items.gear && user.items.gear.owned && user.items.gear.owned.hasOwnProperty('armor_special_birthday2017')) {
|
||||
set['items.gear.owned.armor_special_birthday2018'] = false;
|
||||
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.armor_special_birthday2018', '_id': monk.id()}};
|
||||
} else if (user.items && user.items.gear && user.items.gear.owned && user.items.gear.owned.hasOwnProperty('armor_special_birthday2016')) {
|
||||
set['items.gear.owned.armor_special_birthday2017'] = false;
|
||||
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.armor_special_birthday2017', '_id': monk.id()}};
|
||||
} else if (user.items && user.items.gear && user.items.gear.owned && user.items.gear.owned.hasOwnProperty('armor_special_birthday2015')) {
|
||||
set['items.gear.owned.armor_special_birthday2016'] = false;
|
||||
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.armor_special_birthday2016', '_id': monk.id()}};
|
||||
} else if (user.items && user.items.gear && user.items.gear.owned && user.items.gear.owned.hasOwnProperty('armor_special_birthday')) {
|
||||
set['items.gear.owned.armor_special_birthday2015'] = false;
|
||||
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.armor_special_birthday2015', '_id': monk.id()}};
|
||||
} else {
|
||||
set['items.gear.owned.armor_special_birthday'] = false;
|
||||
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.armor_special_birthday', '_id': monk.id()}};
|
||||
}
|
||||
|
||||
var inc = {
|
||||
'items.food.Cake_Skeleton':1,
|
||||
'items.food.Cake_Base':1,
|
||||
'items.food.Cake_CottonCandyBlue':1,
|
||||
'items.food.Cake_CottonCandyPink':1,
|
||||
'items.food.Cake_Shade':1,
|
||||
'items.food.Cake_White':1,
|
||||
'items.food.Cake_Golden':1,
|
||||
'items.food.Cake_Zombie':1,
|
||||
'items.food.Cake_Desert':1,
|
||||
'items.food.Cake_Red':1,
|
||||
'achievements.habitBirthdays':1
|
||||
};
|
||||
|
||||
dbUsers.update({_id: user._id}, {$set: set, $inc: inc, $push: push});
|
||||
|
||||
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
|
||||
if (user._id == authorUuid) console.warn(authorName + ' processed');
|
||||
}
|
||||
|
||||
function displayData() {
|
||||
console.warn('\n' + count + ' users processed\n');
|
||||
return exiting(0);
|
||||
}
|
||||
|
||||
function exiting(code, msg) {
|
||||
code = code || 0; // 0 = success
|
||||
if (code && !msg) { msg = 'ERROR!'; }
|
||||
if (msg) {
|
||||
if (code) { console.error(msg); }
|
||||
else { console.log( msg); }
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
module.exports = processUsers;
|
||||
|
||||
59
migrations/docs/mongo-indexes.md
Normal file
59
migrations/docs/mongo-indexes.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Indexes
|
||||
|
||||
This file contains a list of indexes that are on Habitica's production Mongo server.
|
||||
If we ever have an issue, use this list to reindex.
|
||||
|
||||
## Challenges
|
||||
- `{ "group": 1, "official": -1, "timestamp": -1 }`
|
||||
- `{ "leader": 1, "official": -1, "timestamp": -1 }`
|
||||
- `{ "official": -1, "timestamp": -1 }`
|
||||
|
||||
## Groups
|
||||
- `{ "privacy": 1, "type": 1, "memberCount": -1 }`
|
||||
- `{ "privacy": 1 }`
|
||||
- `{ "purchased.plan.customerId": 1 }`
|
||||
- `{ "purchased.plan.paymentMethod": 1 }`
|
||||
- `{ "purchased.plan.planId": 1, "purchased.plan.dateTerminated": 1 }`
|
||||
- `{ "type": 1, "memberCount": -1, "_id": 1 }`
|
||||
- `{ "type": 1 }`
|
||||
|
||||
## Tasks
|
||||
- `{ "challenge.id": 1 }`
|
||||
- `{ "challenge.taskId": 1 }`
|
||||
- `{ "group.id": 1 }`
|
||||
- `{ "group.taskId": 1 }`
|
||||
- `{ "type": 1, "everyX": 1, "frequency": 1 }`
|
||||
- `{ "userId": 1 }`
|
||||
- `{ "yesterDaily": 1, "type": 1 }`
|
||||
|
||||
## Users
|
||||
- `{ "_id": 1, "apiToken": 1 }`
|
||||
- `{ "auth.facebook.emails.value": 1 }`
|
||||
- `{ "auth.facebook.id": 1 }`
|
||||
- `{ "auth.google.emails.value": 1 }`
|
||||
- `{ "auth.google.id": 1 }`
|
||||
- `{ "auth.local.email": 1 }`
|
||||
- `{ "auth.local.lowerCaseUsername": 1 }`
|
||||
- `{ "auth.local.username": 1 }`
|
||||
- `{ "auth.timestamps.created": 1 }`
|
||||
- `{ "auth.timestamps.loggedin": 1, "_lastPushNotification": 1, "preferences.timezoneOffset": 1 }`
|
||||
- `{ "auth.timestamps.loggedin": 1 }`
|
||||
- `{ "backer.tier": -1 }`
|
||||
- `{ "challenges": 1, "_id": 1 }`
|
||||
- `{ "contributor.admin": 1, "contributor.level": -1, "backer.npc": -1, "profile.name": 1 }`
|
||||
- `{ "contributor.level": 1 }`
|
||||
- `{ "flags.newStuff": 1 }`
|
||||
- `{ "flags.armoireEmpty": 1 }`
|
||||
- `{ "guilds": 1, "_id": 1 }`
|
||||
- `{ "invitations.guilds.id": 1, "_id": 1 }`
|
||||
- `{ "invitations.party.id": 1 }`
|
||||
- `{ "loginIncentives": 1 }`
|
||||
- `{ "migration": 1 }`
|
||||
- `{ "party._id": 1, "_id": 1 }`
|
||||
- `{ "preferences.sleep": 1, "_id": 1, "flags.lastWeeklyRecap": 1, "preferences.emailNotifications.unsubscribeFromAll": 1, "preferences.emailNotifications.weeklyRecaps": 1 }`
|
||||
- `{ "preferences.sleep": 1, "_id": 1, "lastCron": 1, "preferences.emailNotifications.importantAnnouncements": 1, "preferences.emailNotifications.unsubscribeFromAll": 1, "flags.recaptureEmailsPhase": 1 }`
|
||||
- `{ "profile.name": 1 }`
|
||||
- `{ "purchased.plan.customerId": 1 }`
|
||||
- `{ "purchased.plan.paymentMethod": 1 }`
|
||||
- `{ "stats.score.overall": 1 }`
|
||||
- `{ "webhooks.type": 1 }`
|
||||
@@ -17,8 +17,5 @@ function setUpServer () {
|
||||
setUpServer();
|
||||
|
||||
// Replace this with your migration
|
||||
var processUsers = require('./groups/update-groups-with-group-plans');
|
||||
processUsers()
|
||||
.catch(function (err) {
|
||||
console.log(err)
|
||||
})
|
||||
const processUsers = require('./20180125_clean_new_notifications.js');
|
||||
processUsers();
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
var UserNotification = require('../website/server/models/userNotification').model
|
||||
|
||||
var _id = '';
|
||||
|
||||
var items = ['back_mystery_201801','headAccessory_mystery_201801']
|
||||
|
||||
var update = {
|
||||
$addToSet: {
|
||||
'purchased.plan.mysteryItems':{
|
||||
$each:['armor_mystery_201710','head_mystery_201710']
|
||||
$each: items,
|
||||
}
|
||||
}
|
||||
},
|
||||
$push: {
|
||||
notifications: (new UserNotification({
|
||||
type: 'NEW_MYSTERY_ITEMS',
|
||||
data: {
|
||||
items: items,
|
||||
},
|
||||
})).toJSON(),
|
||||
},
|
||||
};
|
||||
|
||||
/*var update = {
|
||||
|
||||
@@ -18,71 +18,94 @@ var authorUuid = '3e595299-3d8a-4a10-bfe0-88f555e4aa0c'; //... own data is done
|
||||
*
|
||||
*/
|
||||
|
||||
var dbserver = 'localhost:27017'; // FOR TEST DATABASE
|
||||
var dbserver = 'username:password@ds031379-a0.mongolab.com:31379'; // FOR PRODUCTION DATABASE
|
||||
var dbname = 'habitrpg';
|
||||
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
|
||||
|
||||
var mongo = require('mongoskin');
|
||||
var _ = require('lodash');
|
||||
var monk = require('monk');
|
||||
var dbUsers = monk(connectionString).get('users', { castIds: false });
|
||||
|
||||
var dbUsers = mongo.db(dbserver + '/' + dbname + '?auto_reconnect').collection('users');
|
||||
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
var query = {
|
||||
'auth.timestamps.loggedin':{$gt:new Date('2016-01-04')}
|
||||
// '_id': authorUuid // FOR TESTING
|
||||
};
|
||||
function processUsers(lastId) {
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
var query = {
|
||||
'auth.timestamps.loggedin':{$gt:new Date('2016-01-04')}
|
||||
// '_id': authorUuid // FOR TESTING
|
||||
};
|
||||
|
||||
// specify fields we are interested in to limit retrieved data (empty if we're not reading data):
|
||||
var fields = {
|
||||
'flags.armoireEmpty':1,
|
||||
'items.gear.owned':1
|
||||
};
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
var fields = {
|
||||
'flags.armoireEmpty':1,
|
||||
'items.gear.owned':1
|
||||
};
|
||||
|
||||
// specify user data to change:
|
||||
var set = {'migration':migrationName, 'flags.armoireEmpty':false};
|
||||
|
||||
console.warn('Updating users...');
|
||||
var progressCount = 1000;
|
||||
var countSearched = 0;
|
||||
var countModified = 0;
|
||||
dbUsers.findEach(query, fields, {batchSize:250}, function(err, user) {
|
||||
if (err) { return exiting(1, 'ERROR! ' + err); }
|
||||
if (!user) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
return displayData();
|
||||
if (lastId) {
|
||||
query._id = {
|
||||
$gt: lastId
|
||||
}
|
||||
}
|
||||
countSearched++;
|
||||
|
||||
dbUsers.find(query, {
|
||||
sort: {_id: 1},
|
||||
limit: 250,
|
||||
fields: {
|
||||
'flags.armoireEmpty':1,
|
||||
'items.gear.owned':1
|
||||
} // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
|
||||
})
|
||||
.then(updateUsers)
|
||||
.catch(function (err) {
|
||||
console.log(err);
|
||||
return exiting(1, 'ERROR! ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
var progressCount = 1000;
|
||||
var count = 0;
|
||||
|
||||
function updateUsers (users) {
|
||||
if (!users || users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
displayData();
|
||||
return;
|
||||
}
|
||||
|
||||
var userPromises = users.map(updateUser);
|
||||
var lastUser = users[users.length - 1];
|
||||
|
||||
return Promise.all(userPromises)
|
||||
.then(function () {
|
||||
processUsers(lastUser._id);
|
||||
});
|
||||
}
|
||||
|
||||
function updateUser (user) {
|
||||
count++;
|
||||
|
||||
var set = {'migration':migrationName, 'flags.armoireEmpty':false};
|
||||
|
||||
|
||||
if (user.flags.armoireEmpty) {
|
||||
// this user believes their armoire has no more items in it
|
||||
if (user.items.gear.owned.weapon_armoire_barristerGavel && user.items.gear.owned.armor_armoire_barristerRobes && user.items.gear.owned.head_armoire_jesterCap && user.items.gear.owned.armor_armoire_jesterCostume && user.items.gear.owned.head_armoire_barristerWig && user.items.gear.owned.weapon_armoire_jesterBaton && user.items.gear.owned.weapon_armoire_lunarSceptre && user.items.gear.owned.armor_armoire_gladiatorArmor && user.items.gear.owned.weapon_armoire_basicCrossbow && user.items.gear.owned.head_armoire_gladiatorHelm && user.items.gear.owned.armor_armoire_lunarArmor && user.items.gear.owned.head_armoire_redHairbow && user.items.gear.owned.head_armoire_violetFloppyHat && user.items.gear.owned.head_armoire_rancherHat && user.items.gear.owned.shield_armoire_gladiatorShield && user.items.gear.owned.head_armoire_blueHairbow && user.items.gear.owned.weapon_armoire_mythmakerSword && user.items.gear.owned.head_armoire_royalCrown && user.items.gear.owned.head_armoire_hornedIronHelm && user.items.gear.owned.weapon_armoire_rancherLasso && user.items.gear.owned.armor_armoire_rancherRobes && user.items.gear.owned.armor_armoire_hornedIronArmor && user.items.gear.owned.armor_armoire_goldenToga && user.items.gear.owned.weapon_armoire_ironCrook && user.items.gear.owned.head_armoire_goldenLaurels && user.items.gear.owned.head_armoire_redFloppyHat && user.items.gear.owned.armor_armoire_plagueDoctorOvercoat && user.items.gear.owned.head_armoire_plagueDoctorHat && user.items.gear.owned.weapon_armoire_goldWingStaff && user.items.gear.owned.head_armoire_yellowHairbow && user.items.gear.owned.eyewear_armoire_plagueDoctorMask && user.items.gear.owned.head_armoire_blackCat && user.items.gear.owned.weapon_armoire_batWand && user.items.gear.owned.head_armoire_orangeCat && user.items.gear.owned.shield_armoire_midnightShield && user.items.gear.owned.armor_armoire_royalRobes && user.items.gear.owned.head_armoire_blueFloppyHat && user.items.gear.owned.shield_armoire_royalCane && user.items.gear.owned.weapon_armoire_shepherdsCrook && user.items.gear.owned.armor_armoire_shepherdRobes && user.items.gear.owned.head_armoire_shepherdHeaddress && user.items.gear.owned.weapon_armoire_blueLongbow && user.items.gear.owned.weapon_armoire_crystalCrescentStaff && user.items.gear.owned.head_armoire_crystalCrescentHat && user.items.gear.owned.armor_armoire_dragonTamerArmor && user.items.gear.owned.head_armoire_dragonTamerHelm && user.items.gear.owned.armor_armoire_crystalCrescentRobes && user.items.gear.owned.shield_armoire_dragonTamerShield && user.items.gear.owned.weapon_armoire_glowingSpear) {
|
||||
// this user does have all the armoire items so we don't change the flag
|
||||
// console.log("don't change: " + user._id); // FOR TESTING
|
||||
// console.log("don't change: " + user._id); // FOR TESTING
|
||||
} else {
|
||||
// console.log("change: " + user._id); // FOR TESTING
|
||||
dbUsers.update({_id: user._id}, {$set: set});
|
||||
}
|
||||
else {
|
||||
countModified++;
|
||||
// console.log("change: " + user._id); // FOR TESTING
|
||||
dbUsers.update({_id:user._id}, {$set:set});
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// this user already has armoire marked as containing items to be bought
|
||||
// so don't change the flag
|
||||
// console.log("DON'T CHANGE: " + user._id); // FOR TESTING
|
||||
// console.log("DON'T CHANGE: " + user._id); // FOR TESTING
|
||||
}
|
||||
|
||||
if (countSearched%progressCount == 0) console.warn(countSearched + ' ' + user._id);
|
||||
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
|
||||
if (user._id == authorUuid) console.warn(authorName + ' processed');
|
||||
});
|
||||
|
||||
|
||||
function displayData() {
|
||||
console.warn('\n' + countSearched + ' users searched\n');
|
||||
console.warn('\n' + countModified + ' users modified\n');
|
||||
return exiting(0);
|
||||
}
|
||||
|
||||
function displayData() {
|
||||
console.warn('\n' + count + ' users processed\n');
|
||||
return exiting(0);
|
||||
}
|
||||
|
||||
function exiting(code, msg) {
|
||||
code = code || 0; // 0 = success
|
||||
@@ -93,3 +116,5 @@ function exiting(code, msg) {
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
module.exports = processUsers;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
var migrationName = '20170502_takeThis.js'; // Update per month
|
||||
var migrationName = '20180102_takeThis.js'; // Update per month
|
||||
var authorName = 'Sabe'; // in case script author needs to know when their ...
|
||||
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
|
||||
|
||||
@@ -7,14 +7,14 @@ var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
|
||||
*/
|
||||
|
||||
var monk = require('monk');
|
||||
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
|
||||
var connectionString = 'mongodb://sabrecat:z8e8jyRA8CTofMQ@ds013393-a0.mlab.com:13393/habitica?auto_reconnect=true';
|
||||
var dbUsers = monk(connectionString).get('users', { castIds: false });
|
||||
|
||||
function processUsers(lastId) {
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
var query = {
|
||||
'migration':{$ne:migrationName},
|
||||
'challenges':{$in:['69999331-d4ea-45a0-8c3f-f725d22b56c8']} // Update per month
|
||||
'challenges':{$in:['5f70ce5b-2d82-4114-8e44-ca65615aae62']} // Update per month
|
||||
};
|
||||
|
||||
if (lastId) {
|
||||
|
||||
88
migrations/tasks/tasks-set-everyX.js
Normal file
88
migrations/tasks/tasks-set-everyX.js
Normal file
@@ -0,0 +1,88 @@
|
||||
var migrationName = 'tasks-set-everyX';
|
||||
var authorName = 'Sabe'; // in case script author needs to know when their ...
|
||||
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
|
||||
|
||||
/*
|
||||
* Iterates over all tasks and sets invalid everyX values (less than 0 or more than 9999 or not an int) field to 0
|
||||
*/
|
||||
|
||||
var monk = require('monk');
|
||||
var connectionString = 'mongodb://sabrecat:z8e8jyRA8CTofMQ@ds013393-a0.mlab.com:13393/habitica?auto_reconnect=true';
|
||||
var dbTasks = monk(connectionString).get('tasks', { castIds: false });
|
||||
|
||||
function processTasks(lastId) {
|
||||
// specify a query to limit the affected tasks (empty for all tasks):
|
||||
var query = {
|
||||
type: "daily",
|
||||
everyX: {
|
||||
$not: {
|
||||
$gte: 0,
|
||||
$lte: 9999,
|
||||
$type: "int",
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if (lastId) {
|
||||
query._id = {
|
||||
$gt: lastId
|
||||
}
|
||||
}
|
||||
|
||||
dbTasks.find(query, {
|
||||
sort: {_id: 1},
|
||||
limit: 250,
|
||||
fields: [],
|
||||
})
|
||||
.then(updateTasks)
|
||||
.catch(function (err) {
|
||||
console.log(err);
|
||||
return exiting(1, 'ERROR! ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
var progressCount = 1000;
|
||||
var count = 0;
|
||||
|
||||
function updateTasks (tasks) {
|
||||
if (!tasks || tasks.length === 0) {
|
||||
console.warn('All appropriate tasks found and modified.');
|
||||
displayData();
|
||||
return;
|
||||
}
|
||||
|
||||
var taskPromises = tasks.map(updatetask);
|
||||
var lasttask = tasks[tasks.length - 1];
|
||||
|
||||
return Promise.all(taskPromises)
|
||||
.then(function () {
|
||||
processTasks(lasttask._id);
|
||||
});
|
||||
}
|
||||
|
||||
function updatetask (task) {
|
||||
count++;
|
||||
var set = {'everyX': 0};
|
||||
|
||||
dbTasks.update({_id: task._id}, {$set:set});
|
||||
|
||||
if (count % progressCount == 0) console.warn(count + ' ' + task._id);
|
||||
if (task._id == authorUuid) console.warn(authorName + ' processed');
|
||||
}
|
||||
|
||||
function displayData() {
|
||||
console.warn('\n' + count + ' tasks processed\n');
|
||||
return exiting(0);
|
||||
}
|
||||
|
||||
function exiting(code, msg) {
|
||||
code = code || 0; // 0 = success
|
||||
if (code && !msg) { msg = 'ERROR!'; }
|
||||
if (msg) {
|
||||
if (code) { console.error(msg); }
|
||||
else { console.log( msg); }
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
module.exports = processTasks;
|
||||
38
migrations/users/account-transfer.js
Normal file
38
migrations/users/account-transfer.js
Normal file
@@ -0,0 +1,38 @@
|
||||
var migrationName = 'AccountTransfer';
|
||||
var authorName = 'TheHollidayInn'; // in case script author needs to know when their ...
|
||||
var authorUuid = ''; //... own data is done
|
||||
|
||||
/*
|
||||
* This migraition will copy user data from prod to test
|
||||
*/
|
||||
|
||||
const monk = require('monk');
|
||||
const connectionString = '';
|
||||
const Users = monk(connectionString).get('users', { castIds: false });
|
||||
|
||||
import uniq from 'lodash/uniq';
|
||||
import Bluebird from 'bluebird';
|
||||
|
||||
|
||||
module.exports = async function accountTransfer () {
|
||||
const fromAccountId = '';
|
||||
const toAccountId = '';
|
||||
|
||||
const fromAccount = await Users.findOne({_id: fromAccountId});
|
||||
const toAccount = await Users.findOne({_id: toAccountId});
|
||||
|
||||
const newMounts = Object.assign({}, fromAccount.items.mounts, toAccount.items.mounts);
|
||||
const newPets = Object.assign({}, fromAccount.items.pets, toAccount.items.pets);
|
||||
const newBackgrounds = Object.assign({}, fromAccount.purchased.background, toAccount.purchased.background);
|
||||
|
||||
await Users.update({_id: toAccountId}, {
|
||||
$set: {
|
||||
'items.pets': newPets,
|
||||
'items.mounts': newMounts,
|
||||
'purchased.background': newBackgrounds,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
console.log(result);
|
||||
});
|
||||
};
|
||||
93
migrations/users/achievement-restore.js
Normal file
93
migrations/users/achievement-restore.js
Normal file
@@ -0,0 +1,93 @@
|
||||
const migrationName = 'AchievementRestore';
|
||||
const authorName = 'TheHollidayInn'; // in case script author needs to know when their ...
|
||||
const authorUuid = ''; //... own data is done
|
||||
|
||||
/*
|
||||
* This migraition will copy user data from prod to test
|
||||
*/
|
||||
import Bluebird from 'bluebird';
|
||||
|
||||
const monk = require('monk');
|
||||
const connectionString = 'mongodb://localhost/new-habit';
|
||||
const Users = monk(connectionString).get('users', { castIds: false });
|
||||
|
||||
const monkOld = require('monk');
|
||||
const oldConnectionSting = 'mongodb://localhost/old-habit';
|
||||
const UsersOld = monk(oldConnectionSting).get('users', { castIds: false });
|
||||
|
||||
function getAchievementUpdate (newUser, oldUser) {
|
||||
const oldAchievements = oldUser.achievements;
|
||||
const newAchievements = newUser.achievements;
|
||||
|
||||
let achievementsUpdate = Object.assign({}, newAchievements);
|
||||
|
||||
// ultimateGearSets
|
||||
if (!achievementsUpdate.ultimateGearSets && oldAchievements.ultimateGearSets) {
|
||||
achievementsUpdate.ultimateGearSets = oldAchievements.ultimateGearSets;
|
||||
} else if (oldAchievements.ultimateGearSets) {
|
||||
for (let index in oldAchievements.ultimateGearSets) {
|
||||
if (oldAchievements.ultimateGearSets[index]) achievementsUpdate.ultimateGearSets[index] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// challenges
|
||||
if (!newAchievements.challenges) newAchievements.challenges = [];
|
||||
if (!oldAchievements.challenges) oldAchievements.challenges = [];
|
||||
achievementsUpdate.challenges = newAchievements.challenges.concat(oldAchievements.challenges);
|
||||
|
||||
// Quests
|
||||
if (!achievementsUpdate.quests) achievementsUpdate.quests = {};
|
||||
for (let index in oldAchievements.quests) {
|
||||
if (!achievementsUpdate.quests[index]) {
|
||||
achievementsUpdate.quests[index] = oldAchievements.quests[index];
|
||||
} else {
|
||||
achievementsUpdate.quests[index] += oldAchievements.quests[index];
|
||||
}
|
||||
}
|
||||
|
||||
// Rebirth level
|
||||
if (achievementsUpdate.rebirthLevel) {
|
||||
achievementsUpdate.rebirthLevel = Math.max(achievementsUpdate.rebirthLevel, oldAchievements.rebirthLevel);
|
||||
} else if (oldAchievements.rebirthLevel) {
|
||||
achievementsUpdate.rebirthLevel = oldAchievements.rebirthLevel;
|
||||
}
|
||||
|
||||
//All others
|
||||
const indexsToIgnore = ['ultimateGearSets', 'challenges', 'quests', 'rebirthLevel'];
|
||||
for (let index in oldAchievements) {
|
||||
if (indexsToIgnore.indexOf(index) !== -1) continue;
|
||||
|
||||
if (!achievementsUpdate[index]) {
|
||||
achievementsUpdate[index] = oldAchievements[index];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Number.isInteger(oldAchievements[index])) {
|
||||
achievementsUpdate[index] += oldAchievements[index];
|
||||
} else {
|
||||
if (oldAchievements[index] === true) achievementsUpdate[index] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return achievementsUpdate;
|
||||
}
|
||||
|
||||
module.exports = async function achievementRestore () {
|
||||
const userIds = [
|
||||
];
|
||||
|
||||
for (let index in userIds) {
|
||||
const userId = userIds[index];
|
||||
const oldUser = await UsersOld.findOne({_id: userId}, 'achievements');
|
||||
const newUser = await Users.findOne({_id: userId}, 'achievements');
|
||||
const achievementUpdate = getAchievementUpdate(newUser, oldUser);
|
||||
await Users.update(
|
||||
{_id: userId},
|
||||
{
|
||||
$set: {
|
||||
'achievements': achievementUpdate,
|
||||
},
|
||||
});
|
||||
console.log(`Updated ${userId}`);
|
||||
}
|
||||
};
|
||||
12138
package-lock.json
generated
12138
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
105
package.json
105
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "4.11.1",
|
||||
"version": "4.27.0",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@slack/client": "^3.8.1",
|
||||
@@ -10,7 +10,6 @@
|
||||
"amplitude": "^2.0.3",
|
||||
"apidoc": "^0.17.5",
|
||||
"apn": "^1.7.6",
|
||||
"async": "^1.5.0",
|
||||
"autoprefixer": "^6.4.0",
|
||||
"aws-sdk": "^2.0.25",
|
||||
"axios": "^0.16.0",
|
||||
@@ -27,106 +26,89 @@
|
||||
"babel-preset-es2015": "^6.6.0",
|
||||
"babel-register": "^6.6.0",
|
||||
"babel-runtime": "^6.11.6",
|
||||
"babelify": "^7.2.0",
|
||||
"bcrypt": "^1.0.2",
|
||||
"bluebird": "^3.3.5",
|
||||
"body-parser": "^1.15.0",
|
||||
"bootstrap": "4.0.0-beta.2",
|
||||
"bootstrap-vue": "^1.0.2",
|
||||
"browserify": "~12.0.1",
|
||||
"bootstrap": "^4.0.0",
|
||||
"bootstrap-vue": "^1.5.0",
|
||||
"compression": "^1.6.1",
|
||||
"connect-ratelimit": "0.0.7",
|
||||
"cookie-session": "^1.2.0",
|
||||
"coupon-code": "^0.4.5",
|
||||
"cross-env": "^4.0.0",
|
||||
"cross-env": "^5.1.3",
|
||||
"css-loader": "^0.28.0",
|
||||
"csv-stringify": "^1.0.2",
|
||||
"cwait": "~1.0.1",
|
||||
"domain-middleware": "~0.1.0",
|
||||
"estraverse": "^4.1.1",
|
||||
"express": "~4.14.0",
|
||||
"express-basic-auth": "^1.0.1",
|
||||
"express-csv": "~0.6.0",
|
||||
"express-validator": "^2.18.0",
|
||||
"extract-text-webpack-plugin": "^2.0.0-rc.3",
|
||||
"file-loader": "^0.10.0",
|
||||
"glob": "^4.3.5",
|
||||
"glob": "^7.1.2",
|
||||
"got": "^6.1.1",
|
||||
"gulp": "^3.9.0",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-babel": "^6.1.2",
|
||||
"gulp-imagemin": "^2.4.0",
|
||||
"gulp-nodemon": "^2.0.4",
|
||||
"gulp-sourcemaps": "^1.6.0",
|
||||
"gulp-uglify": "^1.4.2",
|
||||
"gulp.spritesmith": "^4.1.0",
|
||||
"gulp-imagemin": "^4.1.0",
|
||||
"gulp-nodemon": "^2.2.1",
|
||||
"gulp.spritesmith": "^6.9.0",
|
||||
"habitica-markdown": "^1.3.0",
|
||||
"hellojs": "^1.15.1",
|
||||
"html-webpack-plugin": "^2.8.1",
|
||||
"image-size": "~0.3.2",
|
||||
"image-size": "^0.6.2",
|
||||
"in-app-purchase": "^1.1.6",
|
||||
"intro.js": "^2.6.0",
|
||||
"jade": "~1.11.0",
|
||||
"jquery": ">=3.0.0",
|
||||
"js2xmlparser": "~1.0.0",
|
||||
"lodash": "^4.17.4",
|
||||
"merge-stream": "^1.0.0",
|
||||
"method-override": "^2.3.5",
|
||||
"moment": "^2.13.0",
|
||||
"moment-recur": "git://github.com/habitrpg/moment-recur#f147ef27bbc26ca67638385f3db4a44084c76626",
|
||||
"mongoose": "~4.8.6",
|
||||
"mongoose-id-autoinc": "~2013.7.14-4",
|
||||
"moment-recur": "git://github.com/habitrpg/moment-recur.git#f147ef27bbc26ca67638385f3db4a44084c76626",
|
||||
"mongoose": "^4.13.10",
|
||||
"morgan": "^1.7.0",
|
||||
"nconf": "~0.8.2",
|
||||
"nib": "^1.1.0",
|
||||
"node-gcm": "^0.14.4",
|
||||
"node-sass": "^4.5.0",
|
||||
"nodemailer": "^2.3.2",
|
||||
"object-path": "^0.9.2",
|
||||
"ora": "^1.1.0",
|
||||
"pageres": "^4.1.1",
|
||||
"passport": "^0.3.2",
|
||||
"passport-facebook": "^2.0.0",
|
||||
"passport-google-oauth20": "1.0.0",
|
||||
"paypal-ipn": "3.0.0",
|
||||
"paypal-rest-sdk": "^1.2.1",
|
||||
"popper.js": "^1.11.0",
|
||||
"paypal-rest-sdk": "^1.8.1",
|
||||
"popper.js": "^1.13.0",
|
||||
"postcss-easy-import": "^2.0.0",
|
||||
"pretty-data": "^0.40.0",
|
||||
"ps-tree": "^1.0.0",
|
||||
"pug": "^2.0.0-beta.12",
|
||||
"push-notify": "git://github.com/habitrpg/push-notify#6bc2b5fdb1bdc9649b9ec1964d79ca50187fc8a9",
|
||||
"pug": "^2.0.0-rc.4",
|
||||
"push-notify": "git://github.com/habitrpg/push-notify.git#6bc2b5fdb1bdc9649b9ec1964d79ca50187fc8a9",
|
||||
"pusher": "^1.3.0",
|
||||
"request": "~2.74.0",
|
||||
"request": "^2.83.0",
|
||||
"rimraf": "^2.4.3",
|
||||
"run-sequence": "^1.1.4",
|
||||
"s3-upload-stream": "^1.0.6",
|
||||
"sass-loader": "^6.0.2",
|
||||
"serve-favicon": "^2.3.0",
|
||||
"shelljs": "^0.7.6",
|
||||
"sortablejs": "^1.6.1",
|
||||
"shelljs": "^0.8.1",
|
||||
"stripe": "^4.2.0",
|
||||
"superagent": "^3.4.3",
|
||||
"svg-inline-loader": "^0.7.1",
|
||||
"svg-url-loader": "^2.0.2",
|
||||
"svgo-loader": "^1.2.1",
|
||||
"universal-analytics": "~0.3.2",
|
||||
"universal-analytics": "^0.4.16",
|
||||
"url-loader": "^0.5.7",
|
||||
"useragent": "^2.1.9",
|
||||
"uuid": "^3.0.1",
|
||||
"validator": "^4.9.0",
|
||||
"vinyl-buffer": "^1.0.0",
|
||||
"vinyl-source-stream": "^1.1.0",
|
||||
"vinyl-buffer": "^1.0.1",
|
||||
"vue": "^2.5.2",
|
||||
"vue-loader": "^13.3.0",
|
||||
"vue-mugen-scroll": "^0.2.1",
|
||||
"vue-router": "^3.0.0",
|
||||
"vue-style-loader": "^3.0.0",
|
||||
"vue-template-compiler": "^2.5.2",
|
||||
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker#825a866b6a9c52dd8c588a3e8b900880875ce914",
|
||||
"vuedraggable": "^2.15.0",
|
||||
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#5d237615463a84a23dd6f3f77c6ab577d68593ec",
|
||||
"webpack": "^2.2.1",
|
||||
"webpack-merge": "^4.0.0",
|
||||
"winston": "^2.1.0",
|
||||
"winston-loggly-bulk": "^1.4.2",
|
||||
"winston-loggly-bulk": "^2.0.2",
|
||||
"xml2js": "^0.4.4"
|
||||
},
|
||||
"private": true,
|
||||
@@ -162,55 +144,44 @@
|
||||
"babel-plugin-istanbul": "^4.0.0",
|
||||
"chai": "^3.4.0",
|
||||
"chai-as-promised": "^5.1.0",
|
||||
"chalk": "^1.1.3",
|
||||
"chalk": "^2.3.0",
|
||||
"chromedriver": "^2.27.2",
|
||||
"connect-history-api-fallback": "^1.1.0",
|
||||
"coveralls": "^2.11.2",
|
||||
"cross-spawn": "^5.0.1",
|
||||
"coveralls": "^3.0.0",
|
||||
"cross-spawn": "^6.0.4",
|
||||
"csv": "~0.3.6",
|
||||
"deep-diff": "~0.1.4",
|
||||
"eslint": "^3.0.0",
|
||||
"eslint-config-habitrpg": "^3.0.0",
|
||||
"eslint-friendly-formatter": "^2.0.5",
|
||||
"eslint-loader": "^1.3.0",
|
||||
"eslint-plugin-html": "^2.0.0",
|
||||
"eslint-plugin-mocha": "^4.7.0",
|
||||
"event-stream": "^3.2.2",
|
||||
"eventsource-polyfill": "^0.9.6",
|
||||
"expect.js": "~0.2.0",
|
||||
"expect.js": "^0.3.1",
|
||||
"http-proxy-middleware": "^0.17.0",
|
||||
"inject-loader": "^3.0.0-beta4",
|
||||
"istanbul": "^1.1.0-alpha.1",
|
||||
"karma": "^1.3.0",
|
||||
"karma-babel-preprocessor": "^6.0.1",
|
||||
"karma-chai-plugins": "~0.6.0",
|
||||
"karma-coverage": "^0.5.3",
|
||||
"karma-mocha": "^0.2.0",
|
||||
"karma-mocha-reporter": "^1.1.1",
|
||||
"karma": "^2.0.0",
|
||||
"karma-babel-preprocessor": "^7.0.0",
|
||||
"karma-chai-plugins": "^0.9.0",
|
||||
"karma-coverage": "^1.1.1",
|
||||
"karma-mocha": "^1.3.0",
|
||||
"karma-mocha-reporter": "^2.2.5",
|
||||
"karma-phantomjs-launcher": "^1.0.0",
|
||||
"karma-sinon-chai": "~1.2.0",
|
||||
"karma-sinon-chai": "^1.3.3",
|
||||
"karma-sinon-stub-promise": "^1.0.0",
|
||||
"karma-sourcemap-loader": "^0.3.7",
|
||||
"karma-spec-reporter": "0.0.24",
|
||||
"karma-spec-reporter": "0.0.32",
|
||||
"karma-webpack": "^2.0.2",
|
||||
"lcov-result-merger": "^1.0.2",
|
||||
"lolex": "^1.4.0",
|
||||
"mocha": "^3.2.0",
|
||||
"mongodb": "^2.0.46",
|
||||
"mongoskin": "~2.1.0",
|
||||
"lcov-result-merger": "^2.0.0",
|
||||
"mocha": "^5.0.0",
|
||||
"monk": "^4.0.0",
|
||||
"nightwatch": "^0.9.12",
|
||||
"phantomjs-prebuilt": "^2.1.12",
|
||||
"protractor": "^3.1.1",
|
||||
"raw-loader": "^0.5.1",
|
||||
"require-again": "^2.0.0",
|
||||
"rewire": "^2.3.3",
|
||||
"selenium-server": "^3.0.1",
|
||||
"sinon": "^1.17.2",
|
||||
"sinon": "^4.2.2",
|
||||
"sinon-chai": "^2.8.0",
|
||||
"sinon-stub-promise": "^4.0.0",
|
||||
"superagent-defaults": "^0.1.13",
|
||||
"vinyl-transform": "^1.0.0",
|
||||
"webpack-bundle-analyzer": "^2.2.1",
|
||||
"webpack-dev-middleware": "^1.10.0",
|
||||
"webpack-hot-middleware": "^2.6.1"
|
||||
|
||||
@@ -64,11 +64,11 @@ describe('GET /challenges/:challengeId/export/csv', () => {
|
||||
let sortedMembers = _.sortBy([members[0], members[1], members[2], groupLeader], '_id');
|
||||
let splitRes = res.split('\n');
|
||||
|
||||
expect(splitRes[0]).to.equal('UUID,name,Task,Value,Notes,Task,Value,Notes');
|
||||
expect(splitRes[1]).to.equal(`${sortedMembers[0]._id},${sortedMembers[0].profile.name},habit:Task 1,0,,todo:Task 2,0,`);
|
||||
expect(splitRes[2]).to.equal(`${sortedMembers[1]._id},${sortedMembers[1].profile.name},habit:Task 1,0,,todo:Task 2,0,`);
|
||||
expect(splitRes[3]).to.equal(`${sortedMembers[2]._id},${sortedMembers[2].profile.name},habit:Task 1,0,,todo:Task 2,0,`);
|
||||
expect(splitRes[4]).to.equal(`${sortedMembers[3]._id},${sortedMembers[3].profile.name},habit:Task 1,0,,todo:Task 2,0,`);
|
||||
expect(splitRes[0]).to.equal('UUID,name,Task,Value,Notes,Streak,Task,Value,Notes,Streak');
|
||||
expect(splitRes[1]).to.equal(`${sortedMembers[0]._id},${sortedMembers[0].profile.name},habit:Task 1,0,,0,todo:Task 2,0,,0`);
|
||||
expect(splitRes[2]).to.equal(`${sortedMembers[1]._id},${sortedMembers[1].profile.name},habit:Task 1,0,,0,todo:Task 2,0,,0`);
|
||||
expect(splitRes[3]).to.equal(`${sortedMembers[2]._id},${sortedMembers[2].profile.name},habit:Task 1,0,,0,todo:Task 2,0,,0`);
|
||||
expect(splitRes[4]).to.equal(`${sortedMembers[3]._id},${sortedMembers[3].profile.name},habit:Task 1,0,,0,todo:Task 2,0,,0`);
|
||||
expect(splitRes[5]).to.equal('');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -149,7 +149,10 @@ describe('GET /challenges/:challengeId/members', () => {
|
||||
|
||||
let usersToGenerate = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
usersToGenerate.push(generateUser({challenges: [challenge._id]}));
|
||||
usersToGenerate.push(generateUser({
|
||||
challenges: [challenge._id],
|
||||
'profile.name': `${i}profilename`,
|
||||
}));
|
||||
}
|
||||
let generatedUsers = await Promise.all(usersToGenerate);
|
||||
let profileNames = generatedUsers.map(generatedUser => generatedUser.profile.name);
|
||||
|
||||
@@ -95,13 +95,23 @@ describe('GET /challenges/:challengeId/members/:memberId', () => {
|
||||
expect(memberProgress.tasks[0].challenge.taskId).to.equal(chalTasks[0]._id);
|
||||
});
|
||||
|
||||
it('returns the tasks without the tags', async () => {
|
||||
it('returns the tasks without the tags and checklist', async () => {
|
||||
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
|
||||
let challenge = await generateChallenge(user, group);
|
||||
let taskText = 'Test Text';
|
||||
await user.post(`/tasks/challenge/${challenge._id}`, [{type: 'habit', text: taskText}]);
|
||||
await user.post(`/tasks/challenge/${challenge._id}`, [{
|
||||
type: 'todo',
|
||||
text: taskText,
|
||||
checklist: [
|
||||
{
|
||||
_id: 123,
|
||||
text: 'test',
|
||||
},
|
||||
],
|
||||
}]);
|
||||
|
||||
let 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([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -314,5 +314,33 @@ describe('POST /challenges', () => {
|
||||
groupLeader = await groupLeader.sync();
|
||||
expect(groupLeader.achievements.joinedChallenge).to.be.true;
|
||||
});
|
||||
|
||||
it('sets summary to challenges name when not supplied', async () => {
|
||||
const name = 'Test Challenge';
|
||||
const challenge = await groupLeader.post('/challenges', {
|
||||
group: group._id,
|
||||
name,
|
||||
shortName: 'TC Label',
|
||||
});
|
||||
|
||||
const updatedChallenge = await groupLeader.get(`/challenges/${challenge._id}`);
|
||||
|
||||
expect(updatedChallenge.summary).to.eql(name);
|
||||
});
|
||||
|
||||
it('sets summary to challenges', async () => {
|
||||
const name = 'Test Challenge';
|
||||
const summary = 'Test Summary Challenge';
|
||||
const challenge = await groupLeader.post('/challenges', {
|
||||
group: group._id,
|
||||
name,
|
||||
shortName: 'TC Label',
|
||||
summary,
|
||||
});
|
||||
|
||||
const updatedChallenge = await groupLeader.get(`/challenges/${challenge._id}`);
|
||||
|
||||
expect(updatedChallenge.summary).to.eql(summary);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -101,19 +101,21 @@ describe('POST /challenges/:challengeId/join', () => {
|
||||
});
|
||||
|
||||
it('syncs challenge tasks to joining user', async () => {
|
||||
let taskText = 'A challenge task text';
|
||||
|
||||
const taskText = 'A challenge task text';
|
||||
await groupLeader.post(`/tasks/challenge/${challenge._id}`, [
|
||||
{type: 'habit', text: taskText},
|
||||
{type: 'daily', text: taskText},
|
||||
]);
|
||||
|
||||
await authorizedUser.post(`/challenges/${challenge._id}/join`);
|
||||
let tasks = await authorizedUser.get('/tasks/user');
|
||||
let tasksTexts = tasks.map((task) => {
|
||||
return task.text;
|
||||
|
||||
const tasks = await authorizedUser.get('/tasks/user');
|
||||
const syncedTask = tasks.find((task) => {
|
||||
return task.text === taskText;
|
||||
});
|
||||
|
||||
expect(tasksTexts).to.include(taskText);
|
||||
expect(syncedTask.text).to.eql(taskText);
|
||||
expect(syncedTask.isDue).to.exist;
|
||||
expect(syncedTask.nextDue).to.exist;
|
||||
});
|
||||
|
||||
it('adds challenge tag to user tags', async () => {
|
||||
|
||||
@@ -149,13 +149,19 @@ describe('POST /challenges/:challengeId/winner/:winnerId', () => {
|
||||
|
||||
await sleep(0.5);
|
||||
|
||||
let tasks = await winningUser.get('/tasks/user');
|
||||
let testTask = _.find(tasks, (task) => {
|
||||
const tasks = await winningUser.get('/tasks/user');
|
||||
const testTask = _.find(tasks, (task) => {
|
||||
return task.text === taskText;
|
||||
});
|
||||
|
||||
const updatedUser = await winningUser.sync();
|
||||
const challengeTag = updatedUser.tags.find(tags => {
|
||||
return tags.id === challenge._id;
|
||||
});
|
||||
|
||||
expect(testTask.challenge.broken).to.eql('CHALLENGE_CLOSED');
|
||||
expect(testTask.challenge.winner).to.eql(winningUser.profile.name);
|
||||
expect(challengeTag.challenge).to.eql('false');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
generateUser,
|
||||
generateGroup,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
|
||||
describe('POST /challenges/:challengeId/clone', () => {
|
||||
it('clones a challenge', async () => {
|
||||
const user = await generateUser({balance: 10});
|
||||
const group = await generateGroup(user);
|
||||
|
||||
const name = 'Test Challenge';
|
||||
const shortName = 'TC Label';
|
||||
const description = 'Test Description';
|
||||
const prize = 1;
|
||||
|
||||
const challenge = await user.post('/challenges', {
|
||||
group: group._id,
|
||||
name,
|
||||
shortName,
|
||||
description,
|
||||
prize,
|
||||
});
|
||||
const challengeTask = await user.post(`/tasks/challenge/${challenge._id}`, {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
up: false,
|
||||
down: true,
|
||||
notes: 1976,
|
||||
});
|
||||
|
||||
const cloneChallengeResponse = await user.post(`/challenges/${challenge._id}/clone`, {
|
||||
group: group._id,
|
||||
name: `${name} cloned`,
|
||||
shortName,
|
||||
description,
|
||||
prize,
|
||||
});
|
||||
|
||||
expect(cloneChallengeResponse.clonedTasks[0].text).to.eql(challengeTask.text);
|
||||
expect(cloneChallengeResponse.clonedTasks[0]._id).to.not.eql(challengeTask._id);
|
||||
expect(cloneChallengeResponse.clonedTasks[0].challenge.id).to.eql(cloneChallengeResponse.clonedChallenge._id);
|
||||
});
|
||||
});
|
||||
@@ -71,11 +71,9 @@ describe('DELETE /groups/:groupId/chat/:chatId', () => {
|
||||
});
|
||||
|
||||
it('returns the update chat when previous message parameter is passed and the chat is updated', async () => {
|
||||
await expect(user.del(`/groups/${groupWithChat._id}/chat/${nextMessage.id}?previousMsg=${message.id}`))
|
||||
.eventually
|
||||
.is.an('array')
|
||||
.to.include(message)
|
||||
.to.be.lengthOf(1);
|
||||
let deleteResult = await user.del(`/groups/${groupWithChat._id}/chat/${nextMessage.id}?previousMsg=${message.id}`);
|
||||
|
||||
expect(deleteResult[0].id).to.eql(message.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
createAndPopulateGroup,
|
||||
generateUser,
|
||||
translate as t,
|
||||
sleep,
|
||||
server,
|
||||
@@ -233,7 +234,7 @@ describe('POST /chat', () => {
|
||||
// Email sent to mods
|
||||
await sleep(0.5);
|
||||
expect(email.sendTxn).to.be.calledOnce;
|
||||
expect(email.sendTxn.args[0][1]).to.be.eql('slur-report-to-mods');
|
||||
expect(email.sendTxn.args[0][1]).to.eql('slur-report-to-mods');
|
||||
|
||||
// Slack message to mods
|
||||
expect(IncomingWebhook.prototype.send).to.be.calledOnce;
|
||||
@@ -286,7 +287,7 @@ describe('POST /chat', () => {
|
||||
// Email sent to mods
|
||||
await sleep(0.5);
|
||||
expect(email.sendTxn).to.be.calledThrice;
|
||||
expect(email.sendTxn.args[2][1]).to.be.eql('slur-report-to-mods');
|
||||
expect(email.sendTxn.args[2][1]).to.eql('slur-report-to-mods');
|
||||
|
||||
// Slack message to mods
|
||||
expect(IncomingWebhook.prototype.send).to.be.calledOnce;
|
||||
@@ -363,6 +364,48 @@ describe('POST /chat', () => {
|
||||
expect(message.message.id).to.exist;
|
||||
});
|
||||
|
||||
it('creates a chat with user styles', async () => {
|
||||
const mount = 'test-mount';
|
||||
const pet = 'test-pet';
|
||||
const style = 'test-style';
|
||||
const userWithStyle = await generateUser({
|
||||
'items.currentMount': mount,
|
||||
'items.currentPet': pet,
|
||||
'preferences.style': style,
|
||||
});
|
||||
await userWithStyle.sync();
|
||||
|
||||
const message = await userWithStyle.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage});
|
||||
|
||||
expect(message.message.id).to.exist;
|
||||
expect(message.message.userStyles.items.currentMount).to.eql(userWithStyle.items.currentMount);
|
||||
expect(message.message.userStyles.items.currentPet).to.eql(userWithStyle.items.currentPet);
|
||||
expect(message.message.userStyles.preferences.style).to.eql(userWithStyle.preferences.style);
|
||||
expect(message.message.userStyles.preferences.hair).to.eql(userWithStyle.preferences.hair);
|
||||
expect(message.message.userStyles.preferences.skin).to.eql(userWithStyle.preferences.skin);
|
||||
expect(message.message.userStyles.preferences.shirt).to.eql(userWithStyle.preferences.shirt);
|
||||
expect(message.message.userStyles.preferences.chair).to.eql(userWithStyle.preferences.chair);
|
||||
expect(message.message.userStyles.preferences.background).to.eql(userWithStyle.preferences.background);
|
||||
});
|
||||
|
||||
it('adds backer info to chat', async () => {
|
||||
const backerInfo = {
|
||||
npc: 'Town Crier',
|
||||
tier: 800,
|
||||
tokensApplied: true,
|
||||
};
|
||||
const backer = await generateUser({
|
||||
backer: backerInfo,
|
||||
});
|
||||
|
||||
const message = await backer.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage});
|
||||
const messageBackerInfo = message.message.backer;
|
||||
|
||||
expect(messageBackerInfo.npc).to.equal(backerInfo.npc);
|
||||
expect(messageBackerInfo.tier).to.equal(backerInfo.tier);
|
||||
expect(messageBackerInfo.tokensApplied).to.equal(backerInfo.tokensApplied);
|
||||
});
|
||||
|
||||
it('sends group chat received webhooks', async () => {
|
||||
let userUuid = generateUUID();
|
||||
let memberUuid = generateUUID();
|
||||
@@ -407,6 +450,9 @@ describe('POST /chat', () => {
|
||||
|
||||
expect(message.message.id).to.exist;
|
||||
expect(memberWithNotification.newMessages[`${groupWithChat._id}`]).to.exist;
|
||||
expect(memberWithNotification.notifications.find(n => {
|
||||
return n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupWithChat._id;
|
||||
})).to.exist;
|
||||
});
|
||||
|
||||
it('notifies other users of new messages for a party', async () => {
|
||||
@@ -424,6 +470,9 @@ describe('POST /chat', () => {
|
||||
|
||||
expect(message.message.id).to.exist;
|
||||
expect(memberWithNotification.newMessages[`${group._id}`]).to.exist;
|
||||
expect(memberWithNotification.notifications.find(n => {
|
||||
return n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === group._id;
|
||||
})).to.exist;
|
||||
});
|
||||
|
||||
context('Spam prevention', () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
createAndPopulateGroup,
|
||||
sleep,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
|
||||
describe('POST /groups/:id/chat/seen', () => {
|
||||
@@ -24,10 +25,15 @@ describe('POST /groups/:id/chat/seen', () => {
|
||||
});
|
||||
|
||||
it('clears new messages for a guild', async () => {
|
||||
await guildMember.sync();
|
||||
const initialNotifications = guildMember.notifications.length;
|
||||
await guildMember.post(`/groups/${guild._id}/chat/seen`);
|
||||
|
||||
await sleep(0.5);
|
||||
|
||||
let guildThatHasSeenChat = await guildMember.get('/user');
|
||||
|
||||
expect(guildThatHasSeenChat.notifications.length).to.equal(initialNotifications - 1);
|
||||
expect(guildThatHasSeenChat.newMessages).to.be.empty;
|
||||
});
|
||||
});
|
||||
@@ -53,10 +59,13 @@ describe('POST /groups/:id/chat/seen', () => {
|
||||
});
|
||||
|
||||
it('clears new messages for a party', async () => {
|
||||
await partyMember.sync();
|
||||
const initialNotifications = partyMember.notifications.length;
|
||||
await partyMember.post(`/groups/${party._id}/chat/seen`);
|
||||
|
||||
let partyMemberThatHasSeenChat = await partyMember.get('/user');
|
||||
|
||||
expect(partyMemberThatHasSeenChat.notifications.length).to.equal(initialNotifications - 1);
|
||||
expect(partyMemberThatHasSeenChat.newMessages).to.be.empty;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import config from '../../../../../config.json';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('POST /groups/:id/chat/:id/clearflags', () => {
|
||||
@@ -74,7 +75,7 @@ describe('POST /groups/:id/chat/:id/clearflags', () => {
|
||||
expect(messages[0].flagCount).to.eql(0);
|
||||
});
|
||||
|
||||
it('can unflag a system message', async () => {
|
||||
it('can\'t flag a system message', async () => {
|
||||
let { group, members } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
type: 'party',
|
||||
@@ -95,13 +96,15 @@ describe('POST /groups/:id/chat/:id/clearflags', () => {
|
||||
await member.post('/user/class/cast/mpheal');
|
||||
|
||||
let [skillMsg] = await member.get(`/groups/${group.id}/chat`);
|
||||
|
||||
await member.post(`/groups/${group._id}/chat/${skillMsg.id}/flag`);
|
||||
await admin.post(`/groups/${group._id}/chat/${skillMsg.id}/clearflags`);
|
||||
|
||||
let messages = await members[0].get(`/groups/${group._id}/chat`);
|
||||
expect(messages[0].id).to.eql(skillMsg.id);
|
||||
expect(messages[0].flagCount).to.eql(0);
|
||||
await expect(member.post(`/groups/${group._id}/chat/${skillMsg.id}/flag`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('messageCannotFlagSystemMessages', {communityManagerEmail: config.EMAILS.COMMUNITY_MANAGER_EMAIL}),
|
||||
});
|
||||
// let messages = await members[0].get(`/groups/${group._id}/chat`);
|
||||
// expect(messages[0].id).to.eql(skillMsg.id);
|
||||
// expect(messages[0].flagCount).to.eql(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
resetHabiticaDB,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import apiMessages from '../../../../../website/server/libs/apiMessages';
|
||||
|
||||
describe('GET /coupons/', () => {
|
||||
let user;
|
||||
@@ -19,7 +19,7 @@ describe('GET /coupons/', () => {
|
||||
await expect(user.get('/coupons')).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('noSudoAccess'),
|
||||
message: apiMessages('noSudoAccess'),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
resetHabiticaDB,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import couponCode from 'coupon-code';
|
||||
import apiMessages from '../../../../../website/server/libs/apiMessages';
|
||||
|
||||
describe('POST /coupons/generate/:event', () => {
|
||||
let user;
|
||||
@@ -25,7 +26,7 @@ describe('POST /coupons/generate/:event', () => {
|
||||
await expect(user.post('/coupons/generate/aaa')).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('noSudoAccess'),
|
||||
message: apiMessages('noSudoAccess'),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ describe('GET /groups/:groupId/members', () => {
|
||||
|
||||
expect(memberRes).to.have.all.keys([ // works as: object has all and only these keys
|
||||
'_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party',
|
||||
'backer', 'contributor', 'auth', 'items', 'inbox',
|
||||
'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives',
|
||||
]);
|
||||
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
|
||||
expect(Object.keys(memberRes.preferences).sort()).to.eql([
|
||||
@@ -93,7 +93,7 @@ describe('GET /groups/:groupId/members', () => {
|
||||
|
||||
expect(memberRes).to.have.all.keys([ // works as: object has all and only these keys
|
||||
'_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party',
|
||||
'backer', 'contributor', 'auth', 'items', 'inbox',
|
||||
'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives',
|
||||
]);
|
||||
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
|
||||
expect(Object.keys(memberRes.preferences).sort()).to.eql([
|
||||
@@ -161,4 +161,19 @@ describe('GET /groups/:groupId/members', () => {
|
||||
let resIds = res.concat(res2).map(member => member._id);
|
||||
expect(resIds).to.eql(expectedIds.sort());
|
||||
});
|
||||
|
||||
it('searches members', async () => {
|
||||
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
|
||||
|
||||
let usersToGenerate = [];
|
||||
for (let i = 0; i < 2; i++) {
|
||||
usersToGenerate.push(generateUser({party: {_id: group._id}}));
|
||||
}
|
||||
const usersCreated = await Promise.all(usersToGenerate);
|
||||
const userToSearch = usersCreated[0].profile.name;
|
||||
|
||||
let res = await user.get(`/groups/party/members?search=${userToSearch}`);
|
||||
expect(res.length).to.equal(1);
|
||||
expect(res[0].profile.name).to.equal(userToSearch);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,6 +44,32 @@ describe('POST /group', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('sets summary to groups name when not supplied', async () => {
|
||||
const name = 'Test Group';
|
||||
const group = await user.post('/groups', {
|
||||
name,
|
||||
type: 'guild',
|
||||
});
|
||||
|
||||
const updatedGroup = await user.get(`/groups/${group._id}`);
|
||||
|
||||
expect(updatedGroup.summary).to.eql(name);
|
||||
});
|
||||
|
||||
it('sets summary to groups', async () => {
|
||||
const name = 'Test Group';
|
||||
const summary = 'Test Summary';
|
||||
const group = await user.post('/groups', {
|
||||
name,
|
||||
type: 'guild',
|
||||
summary,
|
||||
});
|
||||
|
||||
const updatedGroup = await user.get(`/groups/${group._id}`);
|
||||
|
||||
expect(updatedGroup.summary).to.eql(summary);
|
||||
});
|
||||
});
|
||||
|
||||
context('Guilds', () => {
|
||||
|
||||
@@ -10,6 +10,8 @@ import { v4 as generateUUID } from 'uuid';
|
||||
import {
|
||||
each,
|
||||
} from 'lodash';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import * as payments from '../../../../../website/server/libs/payments';
|
||||
|
||||
describe('POST /groups/:groupId/leave', () => {
|
||||
let typesOfGroups = {
|
||||
@@ -68,13 +70,21 @@ describe('POST /groups/:groupId/leave', () => {
|
||||
it('removes new messages for that group from user', async () => {
|
||||
await member.post(`/groups/${groupToLeave._id}/chat`, { message: 'Some message' });
|
||||
|
||||
await sleep(0.5);
|
||||
|
||||
await leader.sync();
|
||||
|
||||
expect(leader.notifications.find(n => {
|
||||
return n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupToLeave._id;
|
||||
})).to.exist;
|
||||
expect(leader.newMessages[groupToLeave._id]).to.not.be.empty;
|
||||
|
||||
await leader.post(`/groups/${groupToLeave._id}/leave`);
|
||||
await leader.sync();
|
||||
|
||||
expect(leader.notifications.find(n => {
|
||||
return n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupToLeave._id;
|
||||
})).to.not.exist;
|
||||
expect(leader.newMessages[groupToLeave._id]).to.be.empty;
|
||||
});
|
||||
|
||||
@@ -254,7 +264,7 @@ describe('POST /groups/:groupId/leave', () => {
|
||||
it('deletes non existant party from user when user tries to leave', async () => {
|
||||
let nonExistentPartyId = generateUUID();
|
||||
let userWithNonExistentParty = await generateUser({'party._id': nonExistentPartyId});
|
||||
expect(userWithNonExistentParty.party._id).to.be.eql(nonExistentPartyId);
|
||||
expect(userWithNonExistentParty.party._id).to.eql(nonExistentPartyId);
|
||||
|
||||
await expect(userWithNonExistentParty.post(`/groups/${nonExistentPartyId}/leave`))
|
||||
.to.eventually.be.rejected;
|
||||
@@ -264,4 +274,45 @@ describe('POST /groups/:groupId/leave', () => {
|
||||
expect(userWithNonExistentParty.party).to.eql({});
|
||||
});
|
||||
});
|
||||
|
||||
context('Leaving a group plan', () => {
|
||||
it('cancels the free subscription', async () => {
|
||||
// Create group
|
||||
let { group, groupLeader, members } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Test Private Guild',
|
||||
type: 'guild',
|
||||
},
|
||||
members: 1,
|
||||
});
|
||||
|
||||
let leader = groupLeader;
|
||||
let member = members[0];
|
||||
let userWithFreePlan = await User.findById(leader._id).exec();
|
||||
|
||||
// Create subscription
|
||||
let paymentData = {
|
||||
user: userWithFreePlan,
|
||||
groupId: group._id,
|
||||
sub: {
|
||||
key: 'basic_3mo',
|
||||
},
|
||||
customerId: 'customer-id',
|
||||
paymentMethod: 'Payment Method',
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
};
|
||||
await payments.createSubscription(paymentData);
|
||||
await member.sync();
|
||||
expect(member.purchased.plan.planId).to.equal('group_plan_auto');
|
||||
expect(member.purchased.plan.dateTerminated).to.not.exist;
|
||||
|
||||
// Leave
|
||||
await member.post(`/groups/${group._id}/leave`);
|
||||
await member.sync();
|
||||
expect(member.purchased.plan.dateTerminated).to.exist;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
generateUser,
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
sleep,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import * as email from '../../../../../website/server/libs/email';
|
||||
|
||||
@@ -119,16 +120,16 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
|
||||
await leader.post(`/groups/${guild._id}/removeMember/${invitedUser._id}`);
|
||||
|
||||
expect(email.sendTxn).to.be.calledOnce;
|
||||
expect(email.sendTxn.args[0][0]._id).to.be.eql(invitedUser._id);
|
||||
expect(email.sendTxn.args[0][1]).to.be.eql('guild-invite-rescinded');
|
||||
expect(email.sendTxn.args[0][0]._id).to.eql(invitedUser._id);
|
||||
expect(email.sendTxn.args[0][1]).to.eql('guild-invite-rescinded');
|
||||
});
|
||||
|
||||
it('sends email to removed user', async () => {
|
||||
await leader.post(`/groups/${guild._id}/removeMember/${member._id}`);
|
||||
|
||||
expect(email.sendTxn).to.be.calledOnce;
|
||||
expect(email.sendTxn.args[0][0]._id).to.be.eql(member._id);
|
||||
expect(email.sendTxn.args[0][1]).to.be.eql('kicked-from-guild');
|
||||
expect(email.sendTxn.args[0][0]._id).to.eql(member._id);
|
||||
expect(email.sendTxn.args[0][1]).to.eql('kicked-from-guild');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -188,13 +189,20 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
|
||||
|
||||
it('removes new messages from a member who is removed', async () => {
|
||||
await partyLeader.post(`/groups/${party._id}/chat`, { message: 'Some message' });
|
||||
await sleep(0.5);
|
||||
await removedMember.sync();
|
||||
|
||||
expect(removedMember.notifications.find(n => {
|
||||
return n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === party._id;
|
||||
})).to.exist;
|
||||
expect(removedMember.newMessages[party._id]).to.not.be.empty;
|
||||
|
||||
await partyLeader.post(`/groups/${party._id}/removeMember/${removedMember._id}`);
|
||||
await removedMember.sync();
|
||||
|
||||
expect(removedMember.notifications.find(n => {
|
||||
return n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === party._id;
|
||||
})).to.not.exist;
|
||||
expect(removedMember.newMessages[party._id]).to.be.empty;
|
||||
});
|
||||
|
||||
@@ -247,16 +255,16 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
|
||||
await partyLeader.post(`/groups/${party._id}/removeMember/${partyInvitedUser._id}`);
|
||||
|
||||
expect(email.sendTxn).to.be.calledOnce;
|
||||
expect(email.sendTxn.args[0][0]._id).to.be.eql(partyInvitedUser._id);
|
||||
expect(email.sendTxn.args[0][1]).to.be.eql('party-invite-rescinded');
|
||||
expect(email.sendTxn.args[0][0]._id).to.eql(partyInvitedUser._id);
|
||||
expect(email.sendTxn.args[0][1]).to.eql('party-invite-rescinded');
|
||||
});
|
||||
|
||||
it('sends email to removed user', async () => {
|
||||
await partyLeader.post(`/groups/${party._id}/removeMember/${partyMember._id}`);
|
||||
|
||||
expect(email.sendTxn).to.be.calledOnce;
|
||||
expect(email.sendTxn.args[0][0]._id).to.be.eql(partyMember._id);
|
||||
expect(email.sendTxn.args[0][1]).to.be.eql('kicked-from-party');
|
||||
expect(email.sendTxn.args[0][0]._id).to.eql(partyMember._id);
|
||||
expect(email.sendTxn.args[0][1]).to.eql('kicked-from-party');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -110,6 +110,7 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
id: group._id,
|
||||
name: groupName,
|
||||
inviter: inviter._id,
|
||||
publicGuild: false,
|
||||
}]);
|
||||
|
||||
await expect(userToInvite.get('/user'))
|
||||
@@ -127,11 +128,13 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
id: group._id,
|
||||
name: groupName,
|
||||
inviter: inviter._id,
|
||||
publicGuild: false,
|
||||
},
|
||||
{
|
||||
id: group._id,
|
||||
name: groupName,
|
||||
inviter: inviter._id,
|
||||
publicGuild: false,
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ describe('GET /members/:memberId', () => {
|
||||
let memberRes = await user.get(`/members/${member._id}`);
|
||||
expect(memberRes).to.have.all.keys([ // works as: object has all and only these keys
|
||||
'_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party',
|
||||
'backer', 'contributor', 'auth', 'items', 'inbox',
|
||||
'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives',
|
||||
]);
|
||||
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
|
||||
expect(Object.keys(memberRes.preferences).sort()).to.eql([
|
||||
|
||||
@@ -98,6 +98,7 @@ describe('POST /members/send-private-message', () => {
|
||||
|
||||
it('sends a private message to a user', async () => {
|
||||
let receiver = await generateUser();
|
||||
// const initialNotifications = receiver.notifications.length;
|
||||
|
||||
await userToSendMessage.post('/members/send-private-message', {
|
||||
message: messageToSend,
|
||||
@@ -115,6 +116,92 @@ describe('POST /members/send-private-message', () => {
|
||||
return message.uuid === receiver._id && message.text === messageToSend;
|
||||
});
|
||||
|
||||
// @TODO waiting for mobile support
|
||||
// expect(updatedReceiver.notifications.length).to.equal(initialNotifications + 1);
|
||||
// const notification = updatedReceiver.notifications[updatedReceiver.notifications.length - 1];
|
||||
|
||||
// expect(notification.type).to.equal('NEW_INBOX_MESSAGE');
|
||||
// expect(notification.data.messageId).to.equal(sendersMessageInReceiversInbox.id);
|
||||
// expect(notification.data.excerpt).to.equal(messageToSend);
|
||||
// expect(notification.data.sender.id).to.equal(updatedSender._id);
|
||||
// expect(notification.data.sender.name).to.equal(updatedSender.profile.name);
|
||||
|
||||
expect(sendersMessageInReceiversInbox).to.exist;
|
||||
expect(sendersMessageInSendersInbox).to.exist;
|
||||
});
|
||||
|
||||
// @TODO waiting for mobile support
|
||||
xit('creates a notification with an excerpt if the message is too long', async () => {
|
||||
let receiver = await generateUser();
|
||||
let longerMessageToSend = 'A very long message, that for sure exceeds the limit of 100 chars for the excerpt that we set to 100 chars';
|
||||
let messageExcerpt = `${longerMessageToSend.substring(0, 100)}...`;
|
||||
|
||||
await userToSendMessage.post('/members/send-private-message', {
|
||||
message: longerMessageToSend,
|
||||
toUserId: receiver._id,
|
||||
});
|
||||
|
||||
let updatedReceiver = await receiver.get('/user');
|
||||
|
||||
let sendersMessageInReceiversInbox = _.find(updatedReceiver.inbox.messages, (message) => {
|
||||
return message.uuid === userToSendMessage._id && message.text === longerMessageToSend;
|
||||
});
|
||||
|
||||
const notification = updatedReceiver.notifications[updatedReceiver.notifications.length - 1];
|
||||
|
||||
expect(notification.type).to.equal('NEW_INBOX_MESSAGE');
|
||||
expect(notification.data.messageId).to.equal(sendersMessageInReceiversInbox.id);
|
||||
expect(notification.data.excerpt).to.equal(messageExcerpt);
|
||||
});
|
||||
|
||||
it('allows admin to send when sender has blocked the admin', async () => {
|
||||
userToSendMessage = await generateUser({
|
||||
'contributor.admin': 1,
|
||||
});
|
||||
const receiver = await generateUser({'inbox.blocks': [userToSendMessage._id]});
|
||||
|
||||
await userToSendMessage.post('/members/send-private-message', {
|
||||
message: messageToSend,
|
||||
toUserId: receiver._id,
|
||||
});
|
||||
|
||||
const updatedReceiver = await receiver.get('/user');
|
||||
const updatedSender = await userToSendMessage.get('/user');
|
||||
|
||||
const sendersMessageInReceiversInbox = _.find(updatedReceiver.inbox.messages, (message) => {
|
||||
return message.uuid === userToSendMessage._id && message.text === messageToSend;
|
||||
});
|
||||
|
||||
const sendersMessageInSendersInbox = _.find(updatedSender.inbox.messages, (message) => {
|
||||
return message.uuid === receiver._id && message.text === messageToSend;
|
||||
});
|
||||
|
||||
expect(sendersMessageInReceiversInbox).to.exist;
|
||||
expect(sendersMessageInSendersInbox).to.exist;
|
||||
});
|
||||
|
||||
it('allows admin to send when to user has opted out of messaging', async () => {
|
||||
userToSendMessage = await generateUser({
|
||||
'contributor.admin': 1,
|
||||
});
|
||||
const receiver = await generateUser({'inbox.optOut': true});
|
||||
|
||||
await userToSendMessage.post('/members/send-private-message', {
|
||||
message: messageToSend,
|
||||
toUserId: receiver._id,
|
||||
});
|
||||
|
||||
const updatedReceiver = await receiver.get('/user');
|
||||
const updatedSender = await userToSendMessage.get('/user');
|
||||
|
||||
const sendersMessageInReceiversInbox = _.find(updatedReceiver.inbox.messages, (message) => {
|
||||
return message.uuid === userToSendMessage._id && message.text === messageToSend;
|
||||
});
|
||||
|
||||
const sendersMessageInSendersInbox = _.find(updatedSender.inbox.messages, (message) => {
|
||||
return message.uuid === receiver._id && message.text === messageToSend;
|
||||
});
|
||||
|
||||
expect(sendersMessageInReceiversInbox).to.exist;
|
||||
expect(sendersMessageInSendersInbox).to.exist;
|
||||
});
|
||||
|
||||
16
test/api/v3/integration/news/GET-news.test.js
Normal file
16
test/api/v3/integration/news/GET-news.test.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import {
|
||||
requester,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
|
||||
describe('GET /news', () => {
|
||||
let api;
|
||||
|
||||
beforeEach(async () => {
|
||||
api = requester();
|
||||
});
|
||||
|
||||
it('returns the latest news in html format, does not require authentication', async () => {
|
||||
const res = await api.get('/news');
|
||||
expect(res).to.be.a.string;
|
||||
});
|
||||
});
|
||||
42
test/api/v3/integration/news/POST-news_tell_me_later.test.js
Normal file
42
test/api/v3/integration/news/POST-news_tell_me_later.test.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
|
||||
describe('POST /news/tell-me-later', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({
|
||||
'flags.newStuff': true,
|
||||
});
|
||||
});
|
||||
|
||||
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.notifications.length).to.equal(initialNotifications + 1);
|
||||
|
||||
const notification = user.notifications[user.notifications.length - 1];
|
||||
|
||||
expect(notification.type).to.equal('NEW_STUFF');
|
||||
// should be marked as seen by default so it's not counted in the number of notifications
|
||||
expect(notification.seen).to.equal(true);
|
||||
expect(notification.data.title).to.be.a.string;
|
||||
});
|
||||
|
||||
it('never adds two notifications', async () => {
|
||||
const initialNotifications = user.notifications.length;
|
||||
|
||||
await user.post('/news/tell-me-later');
|
||||
await user.post('/news/tell-me-later');
|
||||
|
||||
await user.sync();
|
||||
|
||||
expect(user.notifications.length).to.equal(initialNotifications + 1);
|
||||
});
|
||||
});
|
||||
@@ -47,6 +47,7 @@ describe('POST /notifications/:notificationId/read', () => {
|
||||
id: id2,
|
||||
type: 'LOGIN_INCENTIVE',
|
||||
data: {},
|
||||
seen: false,
|
||||
}]);
|
||||
|
||||
await user.sync();
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('POST /notifications/:notificationId/see', () => {
|
||||
let user;
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('errors when notification is not found', async () => {
|
||||
let dummyId = generateUUID();
|
||||
|
||||
await expect(user.post(`/notifications/${dummyId}/see`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('messageNotificationNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('mark a notification as seen', async () => {
|
||||
expect(user.notifications.length).to.equal(0);
|
||||
|
||||
const id = generateUUID();
|
||||
const id2 = generateUUID();
|
||||
|
||||
await user.update({
|
||||
notifications: [{
|
||||
id,
|
||||
type: 'DROPS_ENABLED',
|
||||
data: {},
|
||||
}, {
|
||||
id: id2,
|
||||
type: 'LOGIN_INCENTIVE',
|
||||
data: {},
|
||||
}],
|
||||
});
|
||||
|
||||
const userObj = await user.get('/user'); // so we can check that defaults have been applied
|
||||
expect(userObj.notifications.length).to.equal(2);
|
||||
expect(userObj.notifications[0].seen).to.equal(false);
|
||||
|
||||
const res = await user.post(`/notifications/${id}/see`);
|
||||
expect(res).to.deep.equal({
|
||||
id,
|
||||
type: 'DROPS_ENABLED',
|
||||
data: {},
|
||||
seen: true,
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
expect(user.notifications.length).to.equal(2);
|
||||
expect(user.notifications[0].id).to.equal(id);
|
||||
expect(user.notifications[0].seen).to.equal(true);
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('POST /notifications/:notificationId/read', () => {
|
||||
describe('POST /notifications/read', () => {
|
||||
let user;
|
||||
|
||||
before(async () => {
|
||||
@@ -57,6 +57,7 @@ describe('POST /notifications/:notificationId/read', () => {
|
||||
id: id2,
|
||||
type: 'LOGIN_INCENTIVE',
|
||||
data: {},
|
||||
seen: false,
|
||||
}]);
|
||||
|
||||
await user.sync();
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('POST /notifications/see', () => {
|
||||
let user;
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('errors when notification is not found', async () => {
|
||||
let dummyId = generateUUID();
|
||||
|
||||
await expect(user.post('/notifications/see', {
|
||||
notificationIds: [dummyId],
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('messageNotificationNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('mark multiple notifications as seen', async () => {
|
||||
expect(user.notifications.length).to.equal(0);
|
||||
|
||||
const id = generateUUID();
|
||||
const id2 = generateUUID();
|
||||
const id3 = generateUUID();
|
||||
|
||||
await user.update({
|
||||
notifications: [{
|
||||
id,
|
||||
type: 'DROPS_ENABLED',
|
||||
data: {},
|
||||
seen: false,
|
||||
}, {
|
||||
id: id2,
|
||||
type: 'LOGIN_INCENTIVE',
|
||||
data: {},
|
||||
seen: false,
|
||||
}, {
|
||||
id: id3,
|
||||
type: 'CRON',
|
||||
data: {},
|
||||
seen: false,
|
||||
}],
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
expect(user.notifications.length).to.equal(3);
|
||||
|
||||
const res = await user.post('/notifications/see', {
|
||||
notificationIds: [id, id3],
|
||||
});
|
||||
|
||||
expect(res).to.deep.equal([
|
||||
{
|
||||
id,
|
||||
type: 'DROPS_ENABLED',
|
||||
data: {},
|
||||
seen: true,
|
||||
}, {
|
||||
id: id2,
|
||||
type: 'LOGIN_INCENTIVE',
|
||||
data: {},
|
||||
seen: false,
|
||||
}, {
|
||||
id: id3,
|
||||
type: 'CRON',
|
||||
data: {},
|
||||
seen: true,
|
||||
}]);
|
||||
|
||||
await user.sync();
|
||||
expect(user.notifications.length).to.equal(3);
|
||||
expect(user.notifications[0].id).to.equal(id);
|
||||
expect(user.notifications[0].seen).to.equal(true);
|
||||
|
||||
expect(user.notifications[1].id).to.equal(id2);
|
||||
expect(user.notifications[1].seen).to.equal(false);
|
||||
|
||||
expect(user.notifications[2].id).to.equal(id3);
|
||||
expect(user.notifications[2].seen).to.equal(true);
|
||||
});
|
||||
});
|
||||
@@ -130,6 +130,7 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
});
|
||||
|
||||
it('uncompletes todo when direction is down', async () => {
|
||||
await user.post(`/tasks/${todo._id}/score/up`);
|
||||
await user.post(`/tasks/${todo._id}/score/down`);
|
||||
let updatedTask = await user.get(`/tasks/${todo._id}`);
|
||||
|
||||
@@ -137,9 +138,23 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
expect(updatedTask.dateCompleted).to.be.a('undefined');
|
||||
});
|
||||
|
||||
it('scores up todo even if it is already completed'); // Yes?
|
||||
it('doesn\'t let a todo be completed twice', async () => {
|
||||
await user.post(`/tasks/${todo._id}/score/up`);
|
||||
await expect(user.post(`/tasks/${todo._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('sessionOutdated'),
|
||||
});
|
||||
});
|
||||
|
||||
it('scores down todo even if it is already uncompleted'); // Yes?
|
||||
it('doesn\'t let a todo be uncompleted twice', async () => {
|
||||
await expect(user.post(`/tasks/${todo._id}/score/down`)).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('sessionOutdated'),
|
||||
});
|
||||
});
|
||||
|
||||
context('user stats when direction is up', () => {
|
||||
let updatedUser;
|
||||
@@ -163,23 +178,25 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
});
|
||||
|
||||
context('user stats when direction is down', () => {
|
||||
let updatedUser;
|
||||
let updatedUser, initialUser;
|
||||
|
||||
beforeEach(async () => {
|
||||
await user.post(`/tasks/${todo._id}/score/up`);
|
||||
initialUser = await user.get('/user');
|
||||
await user.post(`/tasks/${todo._id}/score/down`);
|
||||
updatedUser = await user.get('/user');
|
||||
});
|
||||
|
||||
it('decreases user\'s mp', () => {
|
||||
expect(updatedUser.stats.mp).to.be.lessThan(user.stats.mp);
|
||||
expect(updatedUser.stats.mp).to.be.lessThan(initialUser.stats.mp);
|
||||
});
|
||||
|
||||
it('decreases user\'s exp', () => {
|
||||
expect(updatedUser.stats.exp).to.be.lessThan(user.stats.exp);
|
||||
expect(updatedUser.stats.exp).to.be.lessThan(initialUser.stats.exp);
|
||||
});
|
||||
|
||||
it('decreases user\'s gold', () => {
|
||||
expect(updatedUser.stats.gp).to.be.lessThan(user.stats.gp);
|
||||
expect(updatedUser.stats.gp).to.be.lessThan(initialUser.stats.gp);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -202,6 +219,7 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
});
|
||||
|
||||
it('uncompletes daily when direction is down', async () => {
|
||||
await user.post(`/tasks/${daily._id}/score/up`);
|
||||
await user.post(`/tasks/${daily._id}/score/down`);
|
||||
let task = await user.get(`/tasks/${daily._id}`);
|
||||
|
||||
@@ -222,9 +240,22 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
expect(task.nextDue.length).to.eql(6);
|
||||
});
|
||||
|
||||
it('scores up daily even if it is already completed'); // Yes?
|
||||
it('doesn\'t let a daily be completed twice', async () => {
|
||||
await user.post(`/tasks/${daily._id}/score/up`);
|
||||
await expect(user.post(`/tasks/${daily._id}/score/up`)).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('sessionOutdated'),
|
||||
});
|
||||
});
|
||||
|
||||
it('scores down daily even if it is already uncompleted'); // Yes?
|
||||
it('doesn\'t let a daily be uncompleted twice', async () => {
|
||||
await expect(user.post(`/tasks/${daily._id}/score/down`)).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('sessionOutdated'),
|
||||
});
|
||||
});
|
||||
|
||||
context('user stats when direction is up', () => {
|
||||
let updatedUser;
|
||||
@@ -248,23 +279,25 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
});
|
||||
|
||||
context('user stats when direction is down', () => {
|
||||
let updatedUser;
|
||||
let updatedUser, initialUser;
|
||||
|
||||
beforeEach(async () => {
|
||||
await user.post(`/tasks/${daily._id}/score/up`);
|
||||
initialUser = await user.get('/user');
|
||||
await user.post(`/tasks/${daily._id}/score/down`);
|
||||
updatedUser = await user.get('/user');
|
||||
});
|
||||
|
||||
it('decreases user\'s mp', () => {
|
||||
expect(updatedUser.stats.mp).to.be.lessThan(user.stats.mp);
|
||||
expect(updatedUser.stats.mp).to.be.lessThan(initialUser.stats.mp);
|
||||
});
|
||||
|
||||
it('decreases user\'s exp', () => {
|
||||
expect(updatedUser.stats.exp).to.be.lessThan(user.stats.exp);
|
||||
expect(updatedUser.stats.exp).to.be.lessThan(initialUser.stats.exp);
|
||||
});
|
||||
|
||||
it('decreases user\'s gold', () => {
|
||||
expect(updatedUser.stats.gp).to.be.lessThan(user.stats.gp);
|
||||
expect(updatedUser.stats.gp).to.be.lessThan(initialUser.stats.gp);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -302,6 +302,17 @@ describe('POST /tasks/user', () => {
|
||||
|
||||
expect(task.alias).to.eql('a_alias012');
|
||||
});
|
||||
|
||||
// This is a special case for iOS requests
|
||||
it('will round a priority (difficulty)', async () => {
|
||||
let task = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
priority: 0.10000000000005,
|
||||
});
|
||||
|
||||
expect(task.priority).to.eql(0.1);
|
||||
});
|
||||
});
|
||||
|
||||
context('habits', () => {
|
||||
@@ -628,6 +639,43 @@ describe('POST /tasks/user', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if everyX is a non int', async () => {
|
||||
await expect(user.post('/tasks/user', {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
everyX: 2.5,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'daily validation failed',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if everyX is negative', async () => {
|
||||
await expect(user.post('/tasks/user', {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
everyX: -1,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'daily validation failed',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if everyX is above 9999', async () => {
|
||||
await expect(user.post('/tasks/user', {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
everyX: 10000,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'daily validation failed',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('can create checklists', async () => {
|
||||
let task = await user.post('/tasks/user', {
|
||||
text: 'test daily',
|
||||
|
||||
@@ -82,6 +82,13 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
});
|
||||
|
||||
it('should update the history', async () => {
|
||||
let newCron = new Date(2015, 11, 20);
|
||||
|
||||
await user.post('/debug/set-cron', {
|
||||
lastCron: newCron,
|
||||
});
|
||||
|
||||
await user.post('/cron');
|
||||
await user.post(`/tasks/${usersChallengeTaskId}/score/up`);
|
||||
|
||||
let tasks = await user.get(`/tasks/challenge/${challenge._id}`);
|
||||
|
||||
@@ -139,6 +139,23 @@ describe('PUT /tasks/:id', () => {
|
||||
expect(savedHabit.up).to.eql(false);
|
||||
expect(savedHabit.down).to.eql(false);
|
||||
});
|
||||
|
||||
it('allows user to update their copy', async () => {
|
||||
const userTasks = await user.get('/tasks/user');
|
||||
const userChallengeTasks = userTasks.filter(task => task.challenge.id === challenge._id);
|
||||
const userCopyOfChallengeTask = userChallengeTasks[0];
|
||||
|
||||
await user.put(`/tasks/${userCopyOfChallengeTask._id}`, {
|
||||
notes: 'some new notes',
|
||||
counterDown: 1,
|
||||
counterUp: 2,
|
||||
});
|
||||
const savedHabit = await user.get(`/tasks/${userCopyOfChallengeTask._id}`);
|
||||
|
||||
expect(savedHabit.notes).to.eql('some new notes');
|
||||
expect(savedHabit.counterDown).to.eql(1);
|
||||
expect(savedHabit.counterUp).to.eql(2);
|
||||
});
|
||||
});
|
||||
|
||||
context('todos', () => {
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
import {
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import { find } from 'lodash';
|
||||
|
||||
describe('POST /tasks/:id/needs-work/:userId', () => {
|
||||
let user, guild, member, member2, task;
|
||||
|
||||
function findAssignedTask (memberTask) {
|
||||
return memberTask.group.id === guild._id;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
let {group, members, groupLeader} = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Test Guild',
|
||||
type: 'guild',
|
||||
},
|
||||
members: 2,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
user = groupLeader;
|
||||
member = members[0];
|
||||
member2 = members[1];
|
||||
|
||||
task = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
requiresApproval: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('errors when user is not assigned', async () => {
|
||||
await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('taskNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('errors when user is not the group leader', async () => {
|
||||
await user.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
await expect(member.post(`/tasks/${task._id}/needs-work/${member._id}`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('onlyGroupLeaderCanEditTasks'),
|
||||
});
|
||||
});
|
||||
|
||||
it('marks as task as needing more work', async () => {
|
||||
const initialNotifications = member.notifications.length;
|
||||
|
||||
await user.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
// score task to require approval
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
|
||||
await user.post(`/tasks/${task._id}/needs-work/${member._id}`);
|
||||
|
||||
[memberTasks] = await Promise.all([member.get('/tasks/user'), member.sync()]);
|
||||
syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
// Check that the notification approval request has been removed
|
||||
expect(syncedTask.group.approval.requested).to.equal(false);
|
||||
expect(syncedTask.group.approval.requestedDate).to.equal(undefined);
|
||||
|
||||
// Check that the notification is correct
|
||||
expect(member.notifications.length).to.equal(initialNotifications + 1);
|
||||
const notification = member.notifications[member.notifications.length - 1];
|
||||
expect(notification.type).to.equal('GROUP_TASK_NEEDS_WORK');
|
||||
|
||||
const taskText = syncedTask.text;
|
||||
const managerName = user.profile.name;
|
||||
|
||||
expect(notification.data.message).to.equal(t('taskNeedsWork', {taskText, managerName}));
|
||||
|
||||
expect(notification.data.task.id).to.equal(syncedTask._id);
|
||||
expect(notification.data.task.text).to.equal(taskText);
|
||||
|
||||
expect(notification.data.group.id).to.equal(syncedTask.group.id);
|
||||
expect(notification.data.group.name).to.equal(guild.name);
|
||||
|
||||
expect(notification.data.manager.id).to.equal(user._id);
|
||||
expect(notification.data.manager.name).to.equal(managerName);
|
||||
|
||||
// Check that the managers' GROUP_TASK_APPROVAL notifications have been removed
|
||||
await user.sync();
|
||||
|
||||
expect(user.notifications.find(n => {
|
||||
n.data.taskId === syncedTask._id && n.type === 'GROUP_TASK_APPROVAL';
|
||||
})).to.equal(undefined);
|
||||
});
|
||||
|
||||
it('allows a manager to mark a task as needing work', async () => {
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: member2._id,
|
||||
});
|
||||
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
// score task to require approval
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
|
||||
const initialNotifications = member.notifications.length;
|
||||
|
||||
await member2.post(`/tasks/${task._id}/needs-work/${member._id}`);
|
||||
|
||||
[memberTasks] = await Promise.all([member.get('/tasks/user'), member.sync()]);
|
||||
syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
// Check that the notification approval request has been removed
|
||||
expect(syncedTask.group.approval.requested).to.equal(false);
|
||||
expect(syncedTask.group.approval.requestedDate).to.equal(undefined);
|
||||
|
||||
expect(member.notifications.length).to.equal(initialNotifications + 1);
|
||||
const notification = member.notifications[member.notifications.length - 1];
|
||||
expect(notification.type).to.equal('GROUP_TASK_NEEDS_WORK');
|
||||
|
||||
const taskText = syncedTask.text;
|
||||
const managerName = member2.profile.name;
|
||||
|
||||
expect(notification.data.message).to.equal(t('taskNeedsWork', {taskText, managerName}));
|
||||
|
||||
expect(notification.data.task.id).to.equal(syncedTask._id);
|
||||
expect(notification.data.task.text).to.equal(taskText);
|
||||
|
||||
expect(notification.data.group.id).to.equal(syncedTask.group.id);
|
||||
expect(notification.data.group.name).to.equal(guild.name);
|
||||
|
||||
expect(notification.data.manager.id).to.equal(member2._id);
|
||||
expect(notification.data.manager.name).to.equal(managerName);
|
||||
|
||||
// Check that the managers' GROUP_TASK_APPROVAL notifications have been removed
|
||||
await Promise.all([user.sync(), member2.sync()]);
|
||||
|
||||
expect(user.notifications.find(n => {
|
||||
n.data.taskId === syncedTask._id && n.type === 'GROUP_TASK_APPROVAL';
|
||||
})).to.equal(undefined);
|
||||
|
||||
expect(member2.notifications.find(n => {
|
||||
n.data.taskId === syncedTask._id && n.type === 'GROUP_TASK_APPROVAL';
|
||||
})).to.equal(undefined);
|
||||
});
|
||||
|
||||
it('prevents marking a task as needing work if it was already approved', async () => {
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: member2._id,
|
||||
});
|
||||
|
||||
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
|
||||
await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('canOnlyApproveTaskOnce'),
|
||||
});
|
||||
});
|
||||
|
||||
it('prevents marking a task as needing work if it is not waiting for approval', async () => {
|
||||
await user.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
|
||||
await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalWasNotRequested'),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -41,8 +41,9 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
const direction = 'up';
|
||||
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/${direction}`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
@@ -58,6 +59,7 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
user: member.auth.local.username,
|
||||
taskName: updatedTask.text,
|
||||
taskId: updatedTask._id,
|
||||
direction,
|
||||
}, 'cs')); // This test only works if we have the notification translated
|
||||
expect(user.notifications[1].data.groupId).to.equal(guild._id);
|
||||
|
||||
@@ -71,8 +73,9 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
});
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
const direction = 'up';
|
||||
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/${direction}`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
@@ -88,6 +91,7 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
user: member.auth.local.username,
|
||||
taskName: updatedTask.text,
|
||||
taskId: updatedTask._id,
|
||||
direction,
|
||||
}));
|
||||
expect(user.notifications[1].data.groupId).to.equal(guild._id);
|
||||
|
||||
@@ -97,6 +101,7 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
user: member.auth.local.username,
|
||||
taskName: updatedTask.text,
|
||||
taskId: updatedTask._id,
|
||||
direction,
|
||||
}));
|
||||
expect(member2.notifications[0].data.groupId).to.equal(guild._id);
|
||||
});
|
||||
|
||||
@@ -75,15 +75,6 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when non leader tries to create a task', async () => {
|
||||
await expect(member.post(`/tasks/${task._id}/unassign/${member._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('onlyGroupLeaderCanEditTasks'),
|
||||
});
|
||||
});
|
||||
|
||||
it('unassigns a user from a task', async () => {
|
||||
await user.post(`/tasks/${task._id}/unassign/${member._id}`);
|
||||
|
||||
@@ -129,4 +120,26 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
|
||||
expect(groupTask[0].group.assignedUsers).to.not.contain(member._id);
|
||||
expect(syncedTask).to.not.exist;
|
||||
});
|
||||
|
||||
it('allows a user to unassign themselves', async () => {
|
||||
await member.post(`/tasks/${task._id}/unassign/${member._id}`);
|
||||
|
||||
let groupTask = await user.get(`/tasks/group/${guild._id}`);
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
expect(groupTask[0].group.assignedUsers).to.not.contain(member._id);
|
||||
expect(syncedTask).to.not.exist;
|
||||
});
|
||||
|
||||
// @TODO: Which do we want? The user to unassign themselves or not. This test was in
|
||||
// here, but then we had a request to allow to unaissgn.
|
||||
xit('returns error when non leader tries to unassign their a task', async () => {
|
||||
await expect(member.post(`/tasks/${task._id}/unassign/${member._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('onlyGroupLeaderCanEditTasks'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -308,7 +308,7 @@ describe('DELETE /user', () => {
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('incorrectDeletePhrase'),
|
||||
message: t('incorrectDeletePhrase', {magicWord: 'DELETE'}),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -27,4 +27,13 @@ describe('GET /user', () => {
|
||||
expect(returnedUser.auth.local.salt).to.not.exist;
|
||||
expect(returnedUser.apiToken).to.not.exist;
|
||||
});
|
||||
|
||||
it('returns only user properties requested', async () => {
|
||||
let returnedUser = await user.get('/user?userFields=achievements,items.mounts');
|
||||
|
||||
expect(returnedUser._id).to.equal(user._id);
|
||||
expect(returnedUser.achievements).to.exist;
|
||||
expect(returnedUser.items.mounts).to.exist;
|
||||
expect(returnedUser.stats).to.not.exist;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,6 +25,7 @@ describe('GET /user/anonymized', () => {
|
||||
'achievements.challenges': 'some',
|
||||
'inbox.messages': [{ text: 'some text' }],
|
||||
tags: [{ name: 'some name', challenge: 'some challenge' }],
|
||||
notifications: [],
|
||||
});
|
||||
|
||||
await generateHabit({ userId: user._id });
|
||||
@@ -65,6 +66,7 @@ describe('GET /user/anonymized', () => {
|
||||
expect(returnedUser.stats.toNextLevel).to.eql(common.tnl(user.stats.lvl));
|
||||
expect(returnedUser.stats.maxMP).to.eql(30); // TODO why 30?
|
||||
expect(returnedUser.newMessages).to.not.exist;
|
||||
expect(returnedUser.notifications).to.not.exist;
|
||||
expect(returnedUser.profile).to.not.exist;
|
||||
expect(returnedUser.purchased.plan).to.not.exist;
|
||||
expect(returnedUser.contributor).to.not.exist;
|
||||
|
||||
@@ -30,7 +30,7 @@ describe('POST /user/feed/:pet/:food', () => {
|
||||
data: user.items.pets['Wolf-Base'],
|
||||
message: t('messageDontEnjoyFood', {
|
||||
egg: pet.text(),
|
||||
foodText: food.text(),
|
||||
foodText: food.textThe(),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -13,15 +13,20 @@ describe('POST /user/open-mystery-item', () => {
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({
|
||||
'purchased.plan.mysteryItems': [mysteryItemKey],
|
||||
notifications: [
|
||||
{type: 'NEW_MYSTERY_ITEMS', data: { items: [mysteryItemKey] }},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
// More tests in common code unit tests
|
||||
|
||||
it('opens a mystery item', async () => {
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
let response = await user.post('/user/open-mystery-item');
|
||||
await user.sync();
|
||||
|
||||
expect(user.notifications.length).to.equal(0);
|
||||
expect(user.items.gear.owned[mysteryItemKey]).to.be.true;
|
||||
expect(response.message).to.equal(t('mysteryItemOpened'));
|
||||
expect(response.data.key).to.eql(mysteryItemKey);
|
||||
|
||||
@@ -26,13 +26,21 @@ describe('POST /user/read-card/:cardType', () => {
|
||||
await user.update({
|
||||
'items.special.greetingReceived': [true],
|
||||
'flags.cardReceived': true,
|
||||
notifications: [{
|
||||
type: 'CARD_RECEIVED',
|
||||
data: {card: cardType},
|
||||
}],
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
|
||||
let response = await user.post(`/user/read-card/${cardType}`);
|
||||
await user.sync();
|
||||
|
||||
expect(response.message).to.equal(t('readCard', {cardType}));
|
||||
expect(user.items.special[`${cardType}Received`]).to.be.empty;
|
||||
expect(user.flags.cardReceived).to.be.false;
|
||||
expect(user.notifications.length).to.equal(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { mockAnalyticsService as analytics } from '../../../../../website/server/libs/analyticsService';
|
||||
|
||||
describe('POST /user/sleep', () => {
|
||||
let user;
|
||||
@@ -22,4 +23,15 @@ describe('POST /user/sleep', () => {
|
||||
await user.sync();
|
||||
expect(user.preferences.sleep).to.be.false;
|
||||
});
|
||||
|
||||
it('sends sleep status to analytics service', async () => {
|
||||
sandbox.spy(analytics, 'track');
|
||||
|
||||
await user.post('/user/sleep');
|
||||
await user.sync();
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
expect(analytics.track).to.be.calledWith('sleep', sandbox.match.has('status', user.preferences.sleep));
|
||||
|
||||
sandbox.restore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,10 +6,14 @@ import {
|
||||
getProperty,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import { ApiUser } from '../../../../../helpers/api-integration/api-classes';
|
||||
import { v4 as generateRandomUserName } from 'uuid';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { each } from 'lodash';
|
||||
import { encrypt } from '../../../../../../website/server/libs/encryption';
|
||||
|
||||
function generateRandomUserName () {
|
||||
return (Date.now() + uuid()).substring(0, 20);
|
||||
}
|
||||
|
||||
describe('POST /user/auth/local/register', () => {
|
||||
context('username and email are free', () => {
|
||||
let api;
|
||||
@@ -37,6 +41,71 @@ describe('POST /user/auth/local/register', () => {
|
||||
expect(user.newUser).to.eql(true);
|
||||
});
|
||||
|
||||
xit('remove spaces from username', async () => {
|
||||
// TODO can probably delete this test now
|
||||
let username = ' usernamewithspaces ';
|
||||
let email = 'test@example.com';
|
||||
let password = 'password';
|
||||
|
||||
let user = await api.post('/user/auth/local/register', {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
confirmPassword: password,
|
||||
});
|
||||
|
||||
expect(user.auth.local.username).to.eql(username.trim());
|
||||
expect(user.profile.name).to.eql(username.trim());
|
||||
});
|
||||
|
||||
context('validates username', () => {
|
||||
const email = 'test@example.com';
|
||||
const password = 'password';
|
||||
|
||||
it('requires to username to be less than 20', async () => {
|
||||
const username = (Date.now() + uuid()).substring(0, 21);
|
||||
|
||||
await expect(api.post('/user/auth/local/register', {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
confirmPassword: password,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'Invalid request parameters.',
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects chracters not in [-_a-zA-Z0-9]', async () => {
|
||||
const username = 'a-zA_Z09*';
|
||||
|
||||
await expect(api.post('/user/auth/local/register', {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
confirmPassword: password,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'Invalid request parameters.',
|
||||
});
|
||||
});
|
||||
|
||||
it('allows only [-_a-zA-Z0-9] characters', async () => {
|
||||
const username = 'a-zA_Z09';
|
||||
|
||||
const user = await api.post('/user/auth/local/register', {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
confirmPassword: password,
|
||||
});
|
||||
|
||||
expect(user.auth.local.username).to.eql(username);
|
||||
});
|
||||
});
|
||||
|
||||
context('provides default tags and tasks', async () => {
|
||||
it('for a generic API consumer', async () => {
|
||||
let username = generateRandomUserName();
|
||||
|
||||
@@ -25,12 +25,32 @@ describe('POST /user/buy-gear/:key', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('buys a piece of gear', async () => {
|
||||
it('buys the first level weapon gear', async () => {
|
||||
let key = 'weapon_warrior_0';
|
||||
|
||||
await user.post(`/user/buy-gear/${key}`);
|
||||
await user.sync();
|
||||
|
||||
expect(user.items.gear.owned[key]).to.eql(true);
|
||||
});
|
||||
|
||||
it('buys the first level armor gear', async () => {
|
||||
let key = 'armor_warrior_1';
|
||||
|
||||
await user.post(`/user/buy-gear/${key}`);
|
||||
await user.sync();
|
||||
|
||||
expect(user.items.gear.owned.armor_warrior_1).to.eql(true);
|
||||
expect(user.items.gear.owned[key]).to.eql(true);
|
||||
});
|
||||
|
||||
it('tries to buy subsequent, level gear', async () => {
|
||||
let key = 'armor_warrior_2';
|
||||
|
||||
return expect(user.post(`/user/buy-gear/${key}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: 'You need to purchase a lower level gear before this one.',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
35
test/api/v3/integration/world-state/GET-world-state.test.js
Normal file
35
test/api/v3/integration/world-state/GET-world-state.test.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { TAVERN_ID } from '../../../../../website/server/models/group';
|
||||
import { updateDocument } from '../../../../helpers/mongo';
|
||||
import {
|
||||
requester,
|
||||
resetHabiticaDB,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
|
||||
describe('GET /world-state', () => {
|
||||
before(async () => {
|
||||
await resetHabiticaDB();
|
||||
});
|
||||
|
||||
it('returns empty worldBoss object when world boss is not active (and does not require authentication)', async () => {
|
||||
const res = await requester().get('/world-state');
|
||||
expect(res.worldBoss).to.eql({});
|
||||
});
|
||||
|
||||
it('returns Tavern quest data when world boss is active', async () => {
|
||||
await updateDocument('groups', {_id: TAVERN_ID}, {quest: {active: true, key: 'dysheartener', progress: {hp: 50000, rage: 9999}}});
|
||||
|
||||
const res = await requester().get('/world-state');
|
||||
expect(res).to.have.deep.property('worldBoss');
|
||||
|
||||
expect(res.worldBoss).to.eql({
|
||||
active: true,
|
||||
extra: {},
|
||||
key: 'dysheartener',
|
||||
progress: {
|
||||
collect: {},
|
||||
hp: 50000,
|
||||
rage: 9999,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,739 +0,0 @@
|
||||
import moment from 'moment';
|
||||
import cc from 'coupon-code';
|
||||
import uuid from 'uuid';
|
||||
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import { model as Group } from '../../../../../website/server/models/group';
|
||||
import { model as Coupon } from '../../../../../website/server/models/coupon';
|
||||
import amzLib from '../../../../../website/server/libs/amazonPayments';
|
||||
import payments from '../../../../../website/server/libs/payments';
|
||||
import common from '../../../../../website/common';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('Amazon Payments', () => {
|
||||
let subKey = 'basic_3mo';
|
||||
|
||||
describe('checkout', () => {
|
||||
let user, orderReferenceId, headers;
|
||||
let setOrderReferenceDetailsSpy;
|
||||
let confirmOrderReferenceSpy;
|
||||
let authorizeSpy;
|
||||
let closeOrderReferenceSpy;
|
||||
|
||||
let paymentBuyGemsStub;
|
||||
let paymentCreateSubscritionStub;
|
||||
let amount = 5;
|
||||
|
||||
function expectAmazonStubs () {
|
||||
expect(setOrderReferenceDetailsSpy).to.be.calledOnce;
|
||||
expect(setOrderReferenceDetailsSpy).to.be.calledWith({
|
||||
AmazonOrderReferenceId: orderReferenceId,
|
||||
OrderReferenceAttributes: {
|
||||
OrderTotal: {
|
||||
CurrencyCode: amzLib.constants.CURRENCY_CODE,
|
||||
Amount: amount,
|
||||
},
|
||||
SellerNote: amzLib.constants.SELLER_NOTE,
|
||||
SellerOrderAttributes: {
|
||||
SellerOrderId: common.uuid(),
|
||||
StoreName: amzLib.constants.STORE_NAME,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(confirmOrderReferenceSpy).to.be.calledOnce;
|
||||
expect(confirmOrderReferenceSpy).to.be.calledWith({ AmazonOrderReferenceId: orderReferenceId });
|
||||
|
||||
expect(authorizeSpy).to.be.calledOnce;
|
||||
expect(authorizeSpy).to.be.calledWith({
|
||||
AmazonOrderReferenceId: orderReferenceId,
|
||||
AuthorizationReferenceId: common.uuid().substring(0, 32),
|
||||
AuthorizationAmount: {
|
||||
CurrencyCode: amzLib.constants.CURRENCY_CODE,
|
||||
Amount: amount,
|
||||
},
|
||||
SellerAuthorizationNote: amzLib.constants.SELLER_NOTE,
|
||||
TransactionTimeout: 0,
|
||||
CaptureNow: true,
|
||||
});
|
||||
|
||||
expect(closeOrderReferenceSpy).to.be.calledOnce;
|
||||
expect(closeOrderReferenceSpy).to.be.calledWith({ AmazonOrderReferenceId: orderReferenceId });
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
user = new User();
|
||||
headers = {};
|
||||
orderReferenceId = 'orderReferenceId';
|
||||
|
||||
setOrderReferenceDetailsSpy = sinon.stub(amzLib, 'setOrderReferenceDetails');
|
||||
setOrderReferenceDetailsSpy.returnsPromise().resolves({});
|
||||
|
||||
confirmOrderReferenceSpy = sinon.stub(amzLib, 'confirmOrderReference');
|
||||
confirmOrderReferenceSpy.returnsPromise().resolves({});
|
||||
|
||||
authorizeSpy = sinon.stub(amzLib, 'authorize');
|
||||
authorizeSpy.returnsPromise().resolves({});
|
||||
|
||||
closeOrderReferenceSpy = sinon.stub(amzLib, 'closeOrderReference');
|
||||
closeOrderReferenceSpy.returnsPromise().resolves({});
|
||||
|
||||
paymentBuyGemsStub = sinon.stub(payments, 'buyGems');
|
||||
paymentBuyGemsStub.returnsPromise().resolves({});
|
||||
|
||||
paymentCreateSubscritionStub = sinon.stub(payments, 'createSubscription');
|
||||
paymentCreateSubscritionStub.returnsPromise().resolves({});
|
||||
|
||||
sinon.stub(common, 'uuid').returns('uuid-generated');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
amzLib.setOrderReferenceDetails.restore();
|
||||
amzLib.confirmOrderReference.restore();
|
||||
amzLib.authorize.restore();
|
||||
amzLib.closeOrderReference.restore();
|
||||
payments.buyGems.restore();
|
||||
payments.createSubscription.restore();
|
||||
common.uuid.restore();
|
||||
});
|
||||
|
||||
it('should purchase gems', async () => {
|
||||
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
|
||||
await amzLib.checkout({user, orderReferenceId, headers});
|
||||
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
headers,
|
||||
});
|
||||
expectAmazonStubs();
|
||||
expect(user.canGetGems).to.be.calledOnce;
|
||||
user.canGetGems.restore();
|
||||
});
|
||||
|
||||
it('should error if gem amount is too low', async () => {
|
||||
let receivingUser = new User();
|
||||
receivingUser.save();
|
||||
let gift = {
|
||||
type: 'gems',
|
||||
gems: {
|
||||
amount: 0,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
|
||||
await expect(amzLib.checkout({gift, user, orderReferenceId, headers}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
message: 'Amount must be at least 1.',
|
||||
name: 'BadRequest',
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if user cannot get gems gems', async () => {
|
||||
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
|
||||
await expect(amzLib.checkout({user, orderReferenceId, headers})).to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
message: i18n.t('groupPolicyCannotGetGems'),
|
||||
name: 'NotAuthorized',
|
||||
});
|
||||
user.canGetGems.restore();
|
||||
});
|
||||
|
||||
it('should gift gems', async () => {
|
||||
let receivingUser = new User();
|
||||
await receivingUser.save();
|
||||
let gift = {
|
||||
type: 'gems',
|
||||
uuid: receivingUser._id,
|
||||
gems: {
|
||||
amount: 16,
|
||||
},
|
||||
};
|
||||
amount = 16 / 4;
|
||||
await amzLib.checkout({gift, user, orderReferenceId, headers});
|
||||
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD_GIFT,
|
||||
headers,
|
||||
gift,
|
||||
});
|
||||
expectAmazonStubs();
|
||||
});
|
||||
|
||||
it('should gift a subscription', async () => {
|
||||
let receivingUser = new User();
|
||||
receivingUser.save();
|
||||
let gift = {
|
||||
type: 'subscription',
|
||||
subscription: {
|
||||
key: subKey,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
amount = common.content.subscriptionBlocks[subKey].price;
|
||||
|
||||
await amzLib.checkout({user, orderReferenceId, headers, gift});
|
||||
|
||||
gift.member = receivingUser;
|
||||
expect(paymentCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD_GIFT,
|
||||
headers,
|
||||
gift,
|
||||
});
|
||||
expectAmazonStubs();
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscribe', () => {
|
||||
let user, group, amount, billingAgreementId, sub, coupon, groupId, headers;
|
||||
let amazonSetBillingAgreementDetailsSpy;
|
||||
let amazonConfirmBillingAgreementSpy;
|
||||
let amazongAuthorizeOnBillingAgreementSpy;
|
||||
let createSubSpy;
|
||||
|
||||
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();
|
||||
|
||||
amount = common.content.subscriptionBlocks[subKey].price;
|
||||
billingAgreementId = 'billingAgreementId';
|
||||
sub = {
|
||||
key: subKey,
|
||||
price: amount,
|
||||
};
|
||||
groupId = group._id;
|
||||
headers = {};
|
||||
|
||||
amazonSetBillingAgreementDetailsSpy = sinon.stub(amzLib, 'setBillingAgreementDetails');
|
||||
amazonSetBillingAgreementDetailsSpy.returnsPromise().resolves({});
|
||||
|
||||
amazonConfirmBillingAgreementSpy = sinon.stub(amzLib, 'confirmBillingAgreement');
|
||||
amazonConfirmBillingAgreementSpy.returnsPromise().resolves({});
|
||||
|
||||
amazongAuthorizeOnBillingAgreementSpy = sinon.stub(amzLib, 'authorizeOnBillingAgreement');
|
||||
amazongAuthorizeOnBillingAgreementSpy.returnsPromise().resolves({});
|
||||
|
||||
createSubSpy = sinon.stub(payments, 'createSubscription');
|
||||
createSubSpy.returnsPromise().resolves({});
|
||||
|
||||
sinon.stub(common, 'uuid').returns('uuid-generated');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
amzLib.setBillingAgreementDetails.restore();
|
||||
amzLib.confirmBillingAgreement.restore();
|
||||
amzLib.authorizeOnBillingAgreement.restore();
|
||||
payments.createSubscription.restore();
|
||||
common.uuid.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if we are missing a subscription', async () => {
|
||||
await expect(amzLib.subscribe({
|
||||
billingAgreementId,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: i18n.t('missingSubscriptionCode'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if we are missing a billingAgreementId', async () => {
|
||||
await expect(amzLib.subscribe({
|
||||
sub,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: 'Missing req.body.billingAgreementId',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error when coupon code is missing', async () => {
|
||||
sub.discount = 40;
|
||||
|
||||
await expect(amzLib.subscribe({
|
||||
billingAgreementId,
|
||||
sub,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
}))
|
||||
.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';
|
||||
|
||||
let couponModel = new Coupon();
|
||||
couponModel.event = 'google_6mo';
|
||||
await couponModel.save();
|
||||
|
||||
sinon.stub(cc, 'validate').returns('invalid');
|
||||
|
||||
await expect(amzLib.subscribe({
|
||||
billingAgreementId,
|
||||
sub,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('invalidCoupon'),
|
||||
});
|
||||
cc.validate.restore();
|
||||
});
|
||||
|
||||
it('subscribes with amazon with a coupon', async () => {
|
||||
sub.discount = 40;
|
||||
sub.key = 'google_6mo';
|
||||
coupon = 'example-coupon';
|
||||
|
||||
let couponModel = new Coupon();
|
||||
couponModel.event = 'google_6mo';
|
||||
let updatedCouponModel = await couponModel.save();
|
||||
|
||||
sinon.stub(cc, 'validate').returns(updatedCouponModel._id);
|
||||
|
||||
await amzLib.subscribe({
|
||||
billingAgreementId,
|
||||
sub,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(createSubSpy).to.be.calledOnce;
|
||||
expect(createSubSpy).to.be.calledWith({
|
||||
user,
|
||||
customerId: billingAgreementId,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
sub,
|
||||
headers,
|
||||
groupId,
|
||||
});
|
||||
|
||||
cc.validate.restore();
|
||||
});
|
||||
|
||||
it('subscribes with amazon', async () => {
|
||||
await amzLib.subscribe({
|
||||
billingAgreementId,
|
||||
sub,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(amazonSetBillingAgreementDetailsSpy).to.be.calledOnce;
|
||||
expect(amazonSetBillingAgreementDetailsSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
BillingAgreementAttributes: {
|
||||
SellerNote: amzLib.constants.SELLER_NOTE_SUBSCRIPTION,
|
||||
SellerBillingAgreementAttributes: {
|
||||
SellerBillingAgreementId: common.uuid(),
|
||||
StoreName: amzLib.constants.STORE_NAME,
|
||||
CustomInformation: amzLib.constants.SELLER_NOTE_SUBSCRIPTION,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(amazonConfirmBillingAgreementSpy).to.be.calledOnce;
|
||||
expect(amazonConfirmBillingAgreementSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
});
|
||||
|
||||
expect(amazongAuthorizeOnBillingAgreementSpy).to.be.calledOnce;
|
||||
expect(amazongAuthorizeOnBillingAgreementSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
AuthorizationReferenceId: common.uuid().substring(0, 32),
|
||||
AuthorizationAmount: {
|
||||
CurrencyCode: amzLib.constants.CURRENCY_CODE,
|
||||
Amount: amount,
|
||||
},
|
||||
SellerAuthorizationNote: amzLib.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION,
|
||||
TransactionTimeout: 0,
|
||||
CaptureNow: true,
|
||||
SellerNote: amzLib.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION,
|
||||
SellerOrderAttributes: {
|
||||
SellerOrderId: common.uuid(),
|
||||
StoreName: amzLib.constants.STORE_NAME,
|
||||
},
|
||||
});
|
||||
|
||||
expect(createSubSpy).to.be.calledOnce;
|
||||
expect(createSubSpy).to.be.calledWith({
|
||||
user,
|
||||
customerId: billingAgreementId,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
sub,
|
||||
headers,
|
||||
groupId,
|
||||
});
|
||||
});
|
||||
|
||||
it('subscribes with amazon with price to existing users', async () => {
|
||||
user = new User();
|
||||
user.guilds.push(groupId);
|
||||
await user.save();
|
||||
group.memberCount = 2;
|
||||
await group.save();
|
||||
sub.key = 'group_monthly';
|
||||
sub.price = 9;
|
||||
amount = 12;
|
||||
|
||||
await amzLib.subscribe({
|
||||
billingAgreementId,
|
||||
sub,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(amazonSetBillingAgreementDetailsSpy).to.be.calledOnce;
|
||||
expect(amazonSetBillingAgreementDetailsSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
BillingAgreementAttributes: {
|
||||
SellerNote: amzLib.constants.SELLER_NOTE_SUBSCRIPTION,
|
||||
SellerBillingAgreementAttributes: {
|
||||
SellerBillingAgreementId: common.uuid(),
|
||||
StoreName: amzLib.constants.STORE_NAME,
|
||||
CustomInformation: amzLib.constants.SELLER_NOTE_SUBSCRIPTION,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(amazonConfirmBillingAgreementSpy).to.be.calledOnce;
|
||||
expect(amazonConfirmBillingAgreementSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
});
|
||||
|
||||
expect(amazongAuthorizeOnBillingAgreementSpy).to.be.calledOnce;
|
||||
expect(amazongAuthorizeOnBillingAgreementSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
AuthorizationReferenceId: common.uuid().substring(0, 32),
|
||||
AuthorizationAmount: {
|
||||
CurrencyCode: amzLib.constants.CURRENCY_CODE,
|
||||
Amount: amount,
|
||||
},
|
||||
SellerAuthorizationNote: amzLib.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION,
|
||||
TransactionTimeout: 0,
|
||||
CaptureNow: true,
|
||||
SellerNote: amzLib.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION,
|
||||
SellerOrderAttributes: {
|
||||
SellerOrderId: common.uuid(),
|
||||
StoreName: amzLib.constants.STORE_NAME,
|
||||
},
|
||||
});
|
||||
|
||||
expect(createSubSpy).to.be.calledOnce;
|
||||
expect(createSubSpy).to.be.calledWith({
|
||||
user,
|
||||
customerId: billingAgreementId,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
sub,
|
||||
headers,
|
||||
groupId,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancelSubscription', () => {
|
||||
let user, group, headers, billingAgreementId, subscriptionBlock, subscriptionLength;
|
||||
let getBillingAgreementDetailsSpy;
|
||||
let paymentCancelSubscriptionSpy;
|
||||
|
||||
function expectAmazonStubs () {
|
||||
expect(getBillingAgreementDetailsSpy).to.be.calledOnce;
|
||||
expect(getBillingAgreementDetailsSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
group.purchased.plan.lastBillingDate = new Date();
|
||||
await group.save();
|
||||
|
||||
subscriptionBlock = common.content.subscriptionBlocks[subKey];
|
||||
subscriptionLength = subscriptionBlock.months * 30;
|
||||
|
||||
headers = {};
|
||||
|
||||
getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails');
|
||||
getBillingAgreementDetailsSpy.returnsPromise().resolves({
|
||||
BillingAgreementDetails: {
|
||||
BillingAgreementStatus: {State: 'Closed'},
|
||||
},
|
||||
});
|
||||
|
||||
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription');
|
||||
paymentCancelSubscriptionSpy.returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
amzLib.getBillingAgreementDetails.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if we are missing a subscription', async () => {
|
||||
user.purchased.plan.customerId = undefined;
|
||||
|
||||
await expect(amzLib.cancelSubscription({user}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('missingSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel a user subscription', async () => {
|
||||
billingAgreementId = user.purchased.plan.customerId;
|
||||
|
||||
await amzLib.cancelSubscription({user, headers});
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
|
||||
user,
|
||||
groupId: undefined,
|
||||
nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
headers,
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
expectAmazonStubs();
|
||||
});
|
||||
|
||||
it('should close a user subscription if amazon not closed', async () => {
|
||||
amzLib.getBillingAgreementDetails.restore();
|
||||
getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails')
|
||||
.returnsPromise()
|
||||
.resolves({
|
||||
BillingAgreementDetails: {
|
||||
BillingAgreementStatus: {State: 'Open'},
|
||||
},
|
||||
});
|
||||
let closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').returnsPromise().resolves({});
|
||||
billingAgreementId = user.purchased.plan.customerId;
|
||||
|
||||
await amzLib.cancelSubscription({user, headers});
|
||||
|
||||
expectAmazonStubs();
|
||||
expect(closeBillingAgreementSpy).to.be.calledOnce;
|
||||
expect(closeBillingAgreementSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
});
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
|
||||
user,
|
||||
groupId: undefined,
|
||||
nextBill: moment(user.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
headers,
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
amzLib.closeBillingAgreement.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if group is not found', async () => {
|
||||
await expect(amzLib.cancelSubscription({user, groupId: 'fake-id'}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
message: i18n.t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if user is not group leader', async () => {
|
||||
let nonLeader = new User();
|
||||
nonLeader.guilds.push(group._id);
|
||||
await nonLeader.save();
|
||||
|
||||
await expect(amzLib.cancelSubscription({user: nonLeader, groupId: group._id}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel a group subscription', async () => {
|
||||
billingAgreementId = group.purchased.plan.customerId;
|
||||
|
||||
await amzLib.cancelSubscription({user, groupId: group._id, headers});
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
|
||||
user,
|
||||
groupId: group._id,
|
||||
nextBill: moment(group.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
headers,
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
expectAmazonStubs();
|
||||
});
|
||||
|
||||
it('should close a group subscription if amazon not closed', async () => {
|
||||
amzLib.getBillingAgreementDetails.restore();
|
||||
getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails')
|
||||
.returnsPromise()
|
||||
.resolves({
|
||||
BillingAgreementDetails: {
|
||||
BillingAgreementStatus: {State: 'Open'},
|
||||
},
|
||||
});
|
||||
let closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').returnsPromise().resolves({});
|
||||
billingAgreementId = group.purchased.plan.customerId;
|
||||
|
||||
await amzLib.cancelSubscription({user, groupId: group._id, headers});
|
||||
|
||||
expectAmazonStubs();
|
||||
expect(closeBillingAgreementSpy).to.be.calledOnce;
|
||||
expect(closeBillingAgreementSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
});
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
|
||||
user,
|
||||
groupId: group._id,
|
||||
nextBill: moment(group.purchased.plan.lastBillingDate).add({ days: subscriptionLength }),
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
headers,
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
amzLib.closeBillingAgreement.restore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#upgradeGroupPlan', () => {
|
||||
let spy, data, user, group, uuidString;
|
||||
|
||||
beforeEach(async function () {
|
||||
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: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
await group.save();
|
||||
|
||||
spy = sinon.stub(amzLib, 'authorizeOnBillingAgreement');
|
||||
spy.returnsPromise().resolves([]);
|
||||
|
||||
uuidString = 'uuid-v4';
|
||||
sinon.stub(uuid, 'v4').returns(uuidString);
|
||||
|
||||
data.groupId = group._id;
|
||||
data.sub.quantity = 3;
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore(amzLib.authorizeOnBillingAgreement);
|
||||
uuid.v4.restore();
|
||||
});
|
||||
|
||||
it('charges for a new member', async () => {
|
||||
data.paymentMethod = amzLib.constants.PAYMENT_METHOD;
|
||||
await payments.createSubscription(data);
|
||||
|
||||
let updatedGroup = await Group.findById(group._id).exec();
|
||||
|
||||
updatedGroup.memberCount += 1;
|
||||
await updatedGroup.save();
|
||||
|
||||
await amzLib.chargeForAdditionalGroupMember(updatedGroup);
|
||||
|
||||
expect(spy.calledOnce).to.be.true;
|
||||
expect(spy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: updatedGroup.purchased.plan.customerId,
|
||||
AuthorizationReferenceId: uuidString.substring(0, 32),
|
||||
AuthorizationAmount: {
|
||||
CurrencyCode: amzLib.constants.CURRENCY_CODE,
|
||||
Amount: 3,
|
||||
},
|
||||
SellerAuthorizationNote: amzLib.constants.SELLER_NOTE_GROUP_NEW_MEMBER,
|
||||
TransactionTimeout: 0,
|
||||
CaptureNow: true,
|
||||
SellerNote: amzLib.constants.SELLER_NOTE_GROUP_NEW_MEMBER,
|
||||
SellerOrderAttributes: {
|
||||
SellerOrderId: uuidString,
|
||||
StoreName: amzLib.constants.STORE_NAME,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -153,6 +153,24 @@ describe('payments/index', () => {
|
||||
expect(recipient.purchased.plan.dateUpdated).to.exist;
|
||||
});
|
||||
|
||||
it('sets plan.dateUpdated if it did exist but the user has cancelled', async () => {
|
||||
recipient.purchased.plan.dateUpdated = moment().subtract(1, 'days').toDate();
|
||||
recipient.purchased.plan.dateTerminated = moment().subtract(1, 'days').toDate();
|
||||
recipient.purchased.plan.customerId = 'testing';
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(moment(recipient.purchased.plan.dateUpdated).date()).to.eql(moment().date());
|
||||
});
|
||||
|
||||
it('sets plan.dateUpdated if it did exist but the user has a corrupt plan', async () => {
|
||||
recipient.purchased.plan.dateUpdated = moment().subtract(1, 'days').toDate();
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(moment(recipient.purchased.plan.dateUpdated).date()).to.eql(moment().date());
|
||||
});
|
||||
|
||||
it('sets plan.dateCreated if it did not previously exist', async () => {
|
||||
expect(recipient.purchased.plan.dateCreated).to.not.exist;
|
||||
|
||||
@@ -399,13 +417,19 @@ describe('payments/index', () => {
|
||||
it('awards mystery items when within the timeframe for a mystery item', async () => {
|
||||
let mayMysteryItemTimeframe = 1464725113000; // May 31st 2016
|
||||
let fakeClock = sinon.useFakeTimers(mayMysteryItemTimeframe);
|
||||
|
||||
data = { paymentMethod: 'PaymentMethod', user, sub: { key: 'basic_3mo' } };
|
||||
|
||||
const oldNotificationsCount = user.notifications.length;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.notifications.find(n => n.type === 'NEW_MYSTERY_ITEMS')).to.not.be.undefined;
|
||||
expect(user.purchased.plan.mysteryItems).to.have.a.lengthOf(2);
|
||||
expect(user.purchased.plan.mysteryItems).to.include('armor_mystery_201605');
|
||||
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();
|
||||
});
|
||||
|
||||
180
test/api/v3/unit/libs/payments/amazon/cancel.test.js
Normal file
180
test/api/v3/unit/libs/payments/amazon/cancel.test.js
Normal file
@@ -0,0 +1,180 @@
|
||||
import moment from 'moment';
|
||||
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../../../website/server/models/user';
|
||||
import amzLib from '../../../../../../../website/server/libs/amazonPayments';
|
||||
import payments from '../../../../../../../website/server/libs/payments';
|
||||
import common from '../../../../../../../website/common';
|
||||
import { createNonLeaderGroupMember } from '../paymentHelpers';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('Amazon Payments - Cancel Subscription', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
|
||||
let user, group, headers, billingAgreementId, subscriptionBlock, subscriptionLength;
|
||||
let getBillingAgreementDetailsSpy;
|
||||
let paymentCancelSubscriptionSpy;
|
||||
|
||||
function expectAmazonStubs () {
|
||||
expect(getBillingAgreementDetailsSpy).to.be.calledOnce;
|
||||
expect(getBillingAgreementDetailsSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
});
|
||||
}
|
||||
|
||||
function expectAmazonCancelSubscriptionSpy (groupId, lastBillingDate) {
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
|
||||
user,
|
||||
groupId,
|
||||
nextBill: moment(lastBillingDate).add({ days: subscriptionLength }),
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
headers,
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
function expectAmazonCancelUserSubscriptionSpy () {
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expectAmazonCancelSubscriptionSpy(undefined, user.purchased.plan.lastBillingDate);
|
||||
}
|
||||
|
||||
function expectAmazonCancelGroupSubscriptionSpy (groupId) {
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expectAmazonCancelSubscriptionSpy(groupId, group.purchased.plan.lastBillingDate);
|
||||
}
|
||||
|
||||
function expectBillingAggreementDetailSpy () {
|
||||
getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails')
|
||||
.returnsPromise()
|
||||
.resolves({
|
||||
BillingAgreementDetails: {
|
||||
BillingAgreementStatus: {State: 'Open'},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
group.purchased.plan.lastBillingDate = new Date();
|
||||
await group.save();
|
||||
|
||||
subscriptionBlock = common.content.subscriptionBlocks[subKey];
|
||||
subscriptionLength = subscriptionBlock.months * 30;
|
||||
|
||||
headers = {};
|
||||
|
||||
getBillingAgreementDetailsSpy = sinon.stub(amzLib, 'getBillingAgreementDetails');
|
||||
getBillingAgreementDetailsSpy.returnsPromise().resolves({
|
||||
BillingAgreementDetails: {
|
||||
BillingAgreementStatus: {State: 'Closed'},
|
||||
},
|
||||
});
|
||||
|
||||
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription');
|
||||
paymentCancelSubscriptionSpy.returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
amzLib.getBillingAgreementDetails.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if we are missing a subscription', async () => {
|
||||
user.purchased.plan.customerId = undefined;
|
||||
|
||||
await expect(amzLib.cancelSubscription({user}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('missingSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel a user subscription', async () => {
|
||||
billingAgreementId = user.purchased.plan.customerId;
|
||||
|
||||
await amzLib.cancelSubscription({user, headers});
|
||||
|
||||
expectAmazonCancelUserSubscriptionSpy();
|
||||
expectAmazonStubs();
|
||||
});
|
||||
|
||||
it('should close a user subscription if amazon not closed', async () => {
|
||||
amzLib.getBillingAgreementDetails.restore();
|
||||
expectBillingAggreementDetailSpy();
|
||||
let closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').returnsPromise().resolves({});
|
||||
billingAgreementId = user.purchased.plan.customerId;
|
||||
|
||||
await amzLib.cancelSubscription({user, headers});
|
||||
|
||||
expectAmazonStubs();
|
||||
expect(closeBillingAgreementSpy).to.be.calledOnce;
|
||||
expect(closeBillingAgreementSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
});
|
||||
expectAmazonCancelUserSubscriptionSpy();
|
||||
amzLib.closeBillingAgreement.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if group is not found', async () => {
|
||||
await expect(amzLib.cancelSubscription({user, groupId: 'fake-id'}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
message: i18n.t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if user is not group leader', async () => {
|
||||
let nonLeader = await createNonLeaderGroupMember(group);
|
||||
|
||||
await expect(amzLib.cancelSubscription({user: nonLeader, groupId: group._id}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel a group subscription', async () => {
|
||||
billingAgreementId = group.purchased.plan.customerId;
|
||||
|
||||
await amzLib.cancelSubscription({user, groupId: group._id, headers});
|
||||
|
||||
expectAmazonCancelGroupSubscriptionSpy(group._id);
|
||||
expectAmazonStubs();
|
||||
});
|
||||
|
||||
it('should close a group subscription if amazon not closed', async () => {
|
||||
amzLib.getBillingAgreementDetails.restore();
|
||||
expectBillingAggreementDetailSpy();
|
||||
let closeBillingAgreementSpy = sinon.stub(amzLib, 'closeBillingAgreement').returnsPromise().resolves({});
|
||||
billingAgreementId = group.purchased.plan.customerId;
|
||||
|
||||
await amzLib.cancelSubscription({user, groupId: group._id, headers});
|
||||
|
||||
expectAmazonStubs();
|
||||
expect(closeBillingAgreementSpy).to.be.calledOnce;
|
||||
expect(closeBillingAgreementSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
});
|
||||
expectAmazonCancelGroupSubscriptionSpy(group._id);
|
||||
amzLib.closeBillingAgreement.restore();
|
||||
});
|
||||
});
|
||||
193
test/api/v3/unit/libs/payments/amazon/checkout.test.js
Normal file
193
test/api/v3/unit/libs/payments/amazon/checkout.test.js
Normal file
@@ -0,0 +1,193 @@
|
||||
import { model as User } from '../../../../../../../website/server/models/user';
|
||||
import amzLib from '../../../../../../../website/server/libs/amazonPayments';
|
||||
import payments from '../../../../../../../website/server/libs/payments';
|
||||
import common from '../../../../../../../website/common';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('Amazon Payments - Checkout', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
let user, orderReferenceId, headers;
|
||||
let setOrderReferenceDetailsSpy;
|
||||
let confirmOrderReferenceSpy;
|
||||
let authorizeSpy;
|
||||
let closeOrderReferenceSpy;
|
||||
|
||||
let paymentBuyGemsStub;
|
||||
let paymentCreateSubscritionStub;
|
||||
let amount = 5;
|
||||
|
||||
function expectOrderReferenceSpy () {
|
||||
expect(setOrderReferenceDetailsSpy).to.be.calledOnce;
|
||||
expect(setOrderReferenceDetailsSpy).to.be.calledWith({
|
||||
AmazonOrderReferenceId: orderReferenceId,
|
||||
OrderReferenceAttributes: {
|
||||
OrderTotal: {
|
||||
CurrencyCode: amzLib.constants.CURRENCY_CODE,
|
||||
Amount: amount,
|
||||
},
|
||||
SellerNote: amzLib.constants.SELLER_NOTE,
|
||||
SellerOrderAttributes: {
|
||||
SellerOrderId: common.uuid(),
|
||||
StoreName: amzLib.constants.STORE_NAME,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function expectAuthorizeSpy () {
|
||||
expect(authorizeSpy).to.be.calledOnce;
|
||||
expect(authorizeSpy).to.be.calledWith({
|
||||
AmazonOrderReferenceId: orderReferenceId,
|
||||
AuthorizationReferenceId: common.uuid().substring(0, 32),
|
||||
AuthorizationAmount: {
|
||||
CurrencyCode: amzLib.constants.CURRENCY_CODE,
|
||||
Amount: amount,
|
||||
},
|
||||
SellerAuthorizationNote: amzLib.constants.SELLER_NOTE,
|
||||
TransactionTimeout: 0,
|
||||
CaptureNow: true,
|
||||
});
|
||||
}
|
||||
|
||||
function expectAmazonStubs () {
|
||||
expectOrderReferenceSpy();
|
||||
|
||||
expect(confirmOrderReferenceSpy).to.be.calledOnce;
|
||||
expect(confirmOrderReferenceSpy).to.be.calledWith({ AmazonOrderReferenceId: orderReferenceId });
|
||||
|
||||
expectAuthorizeSpy();
|
||||
|
||||
expect(closeOrderReferenceSpy).to.be.calledOnce;
|
||||
expect(closeOrderReferenceSpy).to.be.calledWith({ AmazonOrderReferenceId: orderReferenceId });
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
user = new User();
|
||||
headers = {};
|
||||
orderReferenceId = 'orderReferenceId';
|
||||
|
||||
setOrderReferenceDetailsSpy = sinon.stub(amzLib, 'setOrderReferenceDetails');
|
||||
setOrderReferenceDetailsSpy.returnsPromise().resolves({});
|
||||
|
||||
confirmOrderReferenceSpy = sinon.stub(amzLib, 'confirmOrderReference');
|
||||
confirmOrderReferenceSpy.returnsPromise().resolves({});
|
||||
|
||||
authorizeSpy = sinon.stub(amzLib, 'authorize');
|
||||
authorizeSpy.returnsPromise().resolves({});
|
||||
|
||||
closeOrderReferenceSpy = sinon.stub(amzLib, 'closeOrderReference');
|
||||
closeOrderReferenceSpy.returnsPromise().resolves({});
|
||||
|
||||
paymentBuyGemsStub = sinon.stub(payments, 'buyGems');
|
||||
paymentBuyGemsStub.returnsPromise().resolves({});
|
||||
|
||||
paymentCreateSubscritionStub = sinon.stub(payments, 'createSubscription');
|
||||
paymentCreateSubscritionStub.returnsPromise().resolves({});
|
||||
|
||||
sinon.stub(common, 'uuid').returns('uuid-generated');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
amzLib.setOrderReferenceDetails.restore();
|
||||
amzLib.confirmOrderReference.restore();
|
||||
amzLib.authorize.restore();
|
||||
amzLib.closeOrderReference.restore();
|
||||
payments.buyGems.restore();
|
||||
payments.createSubscription.restore();
|
||||
common.uuid.restore();
|
||||
});
|
||||
|
||||
function expectBuyGemsStub (paymentMethod, gift) {
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
|
||||
let expectedArgs = {
|
||||
user,
|
||||
paymentMethod,
|
||||
headers,
|
||||
};
|
||||
if (gift) expectedArgs.gift = gift;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith(expectedArgs);
|
||||
}
|
||||
|
||||
it('should purchase gems', async () => {
|
||||
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
|
||||
await amzLib.checkout({user, orderReferenceId, headers});
|
||||
|
||||
expectBuyGemsStub(amzLib.constants.PAYMENT_METHOD);
|
||||
expectAmazonStubs();
|
||||
expect(user.canGetGems).to.be.calledOnce;
|
||||
user.canGetGems.restore();
|
||||
});
|
||||
|
||||
it('should error if gem amount is too low', async () => {
|
||||
let receivingUser = new User();
|
||||
receivingUser.save();
|
||||
let gift = {
|
||||
type: 'gems',
|
||||
gems: {
|
||||
amount: 0,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
|
||||
await expect(amzLib.checkout({gift, user, orderReferenceId, headers}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
message: 'Amount must be at least 1.',
|
||||
name: 'BadRequest',
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if user cannot get gems gems', async () => {
|
||||
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
|
||||
await expect(amzLib.checkout({user, orderReferenceId, headers})).to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
message: i18n.t('groupPolicyCannotGetGems'),
|
||||
name: 'NotAuthorized',
|
||||
});
|
||||
user.canGetGems.restore();
|
||||
});
|
||||
|
||||
it('should gift gems', async () => {
|
||||
let receivingUser = new User();
|
||||
await receivingUser.save();
|
||||
let gift = {
|
||||
type: 'gems',
|
||||
uuid: receivingUser._id,
|
||||
gems: {
|
||||
amount: 16,
|
||||
},
|
||||
};
|
||||
amount = 16 / 4;
|
||||
await amzLib.checkout({gift, user, orderReferenceId, headers});
|
||||
|
||||
expectBuyGemsStub(amzLib.constants.PAYMENT_METHOD_GIFT, gift);
|
||||
expectAmazonStubs();
|
||||
});
|
||||
|
||||
it('should gift a subscription', async () => {
|
||||
let receivingUser = new User();
|
||||
receivingUser.save();
|
||||
let gift = {
|
||||
type: 'subscription',
|
||||
subscription: {
|
||||
key: subKey,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
amount = common.content.subscriptionBlocks[subKey].price;
|
||||
|
||||
await amzLib.checkout({user, orderReferenceId, headers, gift});
|
||||
|
||||
gift.member = receivingUser;
|
||||
expect(paymentCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD_GIFT,
|
||||
headers,
|
||||
gift,
|
||||
});
|
||||
expectAmazonStubs();
|
||||
});
|
||||
});
|
||||
267
test/api/v3/unit/libs/payments/amazon/subscribe.test.js
Normal file
267
test/api/v3/unit/libs/payments/amazon/subscribe.test.js
Normal file
@@ -0,0 +1,267 @@
|
||||
import cc from 'coupon-code';
|
||||
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../../../website/server/models/user';
|
||||
import { model as Coupon } from '../../../../../../../website/server/models/coupon';
|
||||
import amzLib from '../../../../../../../website/server/libs/amazonPayments';
|
||||
import payments from '../../../../../../../website/server/libs/payments';
|
||||
import common from '../../../../../../../website/common';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('Amazon Payments - Subscribe', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
let user, group, amount, billingAgreementId, sub, coupon, groupId, headers;
|
||||
let amazonSetBillingAgreementDetailsSpy;
|
||||
let amazonConfirmBillingAgreementSpy;
|
||||
let amazonAuthorizeOnBillingAgreementSpy;
|
||||
let createSubSpy;
|
||||
|
||||
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();
|
||||
|
||||
amount = common.content.subscriptionBlocks[subKey].price;
|
||||
billingAgreementId = 'billingAgreementId';
|
||||
sub = {
|
||||
key: subKey,
|
||||
price: amount,
|
||||
};
|
||||
groupId = group._id;
|
||||
headers = {};
|
||||
|
||||
amazonSetBillingAgreementDetailsSpy = sinon.stub(amzLib, 'setBillingAgreementDetails');
|
||||
amazonSetBillingAgreementDetailsSpy.returnsPromise().resolves({});
|
||||
|
||||
amazonConfirmBillingAgreementSpy = sinon.stub(amzLib, 'confirmBillingAgreement');
|
||||
amazonConfirmBillingAgreementSpy.returnsPromise().resolves({});
|
||||
|
||||
amazonAuthorizeOnBillingAgreementSpy = sinon.stub(amzLib, 'authorizeOnBillingAgreement');
|
||||
amazonAuthorizeOnBillingAgreementSpy.returnsPromise().resolves({});
|
||||
|
||||
createSubSpy = sinon.stub(payments, 'createSubscription');
|
||||
createSubSpy.returnsPromise().resolves({});
|
||||
|
||||
sinon.stub(common, 'uuid').returns('uuid-generated');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
amzLib.setBillingAgreementDetails.restore();
|
||||
amzLib.confirmBillingAgreement.restore();
|
||||
amzLib.authorizeOnBillingAgreement.restore();
|
||||
payments.createSubscription.restore();
|
||||
common.uuid.restore();
|
||||
});
|
||||
|
||||
function expectAmazonAuthorizeBillingAgreementSpy () {
|
||||
expect(amazonAuthorizeOnBillingAgreementSpy).to.be.calledOnce;
|
||||
expect(amazonAuthorizeOnBillingAgreementSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
AuthorizationReferenceId: common.uuid().substring(0, 32),
|
||||
AuthorizationAmount: {
|
||||
CurrencyCode: amzLib.constants.CURRENCY_CODE,
|
||||
Amount: amount,
|
||||
},
|
||||
SellerAuthorizationNote: amzLib.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION,
|
||||
TransactionTimeout: 0,
|
||||
CaptureNow: true,
|
||||
SellerNote: amzLib.constants.SELLER_NOTE_ATHORIZATION_SUBSCRIPTION,
|
||||
SellerOrderAttributes: {
|
||||
SellerOrderId: common.uuid(),
|
||||
StoreName: amzLib.constants.STORE_NAME,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function expectAmazonSetBillingAgreementDetailsSpy () {
|
||||
expect(amazonSetBillingAgreementDetailsSpy).to.be.calledOnce;
|
||||
expect(amazonSetBillingAgreementDetailsSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
BillingAgreementAttributes: {
|
||||
SellerNote: amzLib.constants.SELLER_NOTE_SUBSCRIPTION,
|
||||
SellerBillingAgreementAttributes: {
|
||||
SellerBillingAgreementId: common.uuid(),
|
||||
StoreName: amzLib.constants.STORE_NAME,
|
||||
CustomInformation: amzLib.constants.SELLER_NOTE_SUBSCRIPTION,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function expectCreateSpy () {
|
||||
expect(createSubSpy).to.be.calledOnce;
|
||||
expect(createSubSpy).to.be.calledWith({
|
||||
user,
|
||||
customerId: billingAgreementId,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD,
|
||||
sub,
|
||||
headers,
|
||||
groupId,
|
||||
});
|
||||
}
|
||||
|
||||
it('should throw an error if we are missing a subscription', async () => {
|
||||
await expect(amzLib.subscribe({
|
||||
billingAgreementId,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: i18n.t('missingSubscriptionCode'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if we are missing a billingAgreementId', async () => {
|
||||
await expect(amzLib.subscribe({
|
||||
sub,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: 'Missing req.body.billingAgreementId',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error when coupon code is missing', async () => {
|
||||
sub.discount = 40;
|
||||
|
||||
await expect(amzLib.subscribe({
|
||||
billingAgreementId,
|
||||
sub,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
}))
|
||||
.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';
|
||||
|
||||
let couponModel = new Coupon();
|
||||
couponModel.event = 'google_6mo';
|
||||
await couponModel.save();
|
||||
|
||||
sinon.stub(cc, 'validate').returns('invalid');
|
||||
|
||||
await expect(amzLib.subscribe({
|
||||
billingAgreementId,
|
||||
sub,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('invalidCoupon'),
|
||||
});
|
||||
cc.validate.restore();
|
||||
});
|
||||
|
||||
it('subscribes with amazon with a coupon', async () => {
|
||||
sub.discount = 40;
|
||||
sub.key = 'google_6mo';
|
||||
coupon = 'example-coupon';
|
||||
|
||||
let couponModel = new Coupon();
|
||||
couponModel.event = 'google_6mo';
|
||||
let updatedCouponModel = await couponModel.save();
|
||||
|
||||
sinon.stub(cc, 'validate').returns(updatedCouponModel._id);
|
||||
|
||||
await amzLib.subscribe({
|
||||
billingAgreementId,
|
||||
sub,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
});
|
||||
|
||||
expectCreateSpy();
|
||||
|
||||
cc.validate.restore();
|
||||
});
|
||||
|
||||
it('subscribes with amazon', async () => {
|
||||
await amzLib.subscribe({
|
||||
billingAgreementId,
|
||||
sub,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
});
|
||||
|
||||
expectAmazonSetBillingAgreementDetailsSpy();
|
||||
|
||||
expect(amazonConfirmBillingAgreementSpy).to.be.calledOnce;
|
||||
expect(amazonConfirmBillingAgreementSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
});
|
||||
|
||||
expectAmazonAuthorizeBillingAgreementSpy();
|
||||
|
||||
expectCreateSpy();
|
||||
});
|
||||
|
||||
it('subscribes with amazon with price to existing users', async () => {
|
||||
user = new User();
|
||||
user.guilds.push(groupId);
|
||||
await user.save();
|
||||
group.memberCount = 2;
|
||||
await group.save();
|
||||
sub.key = 'group_monthly';
|
||||
sub.price = 9;
|
||||
amount = 12;
|
||||
|
||||
await amzLib.subscribe({
|
||||
billingAgreementId,
|
||||
sub,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
});
|
||||
|
||||
expectAmazonSetBillingAgreementDetailsSpy();
|
||||
expect(amazonConfirmBillingAgreementSpy).to.be.calledOnce;
|
||||
expect(amazonConfirmBillingAgreementSpy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
});
|
||||
expectAmazonAuthorizeBillingAgreementSpy();
|
||||
expectCreateSpy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
import uuid from 'uuid';
|
||||
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../../../website/server/models/user';
|
||||
import { model as Group } from '../../../../../../../website/server/models/group';
|
||||
import amzLib from '../../../../../../../website/server/libs/amazonPayments';
|
||||
import payments from '../../../../../../../website/server/libs/payments';
|
||||
|
||||
describe('#upgradeGroupPlan', () => {
|
||||
let spy, data, user, group, uuidString;
|
||||
|
||||
beforeEach(async function () {
|
||||
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: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
await group.save();
|
||||
|
||||
spy = sinon.stub(amzLib, 'authorizeOnBillingAgreement');
|
||||
spy.returnsPromise().resolves([]);
|
||||
|
||||
uuidString = 'uuid-v4';
|
||||
sinon.stub(uuid, 'v4').returns(uuidString);
|
||||
|
||||
data.groupId = group._id;
|
||||
data.sub.quantity = 3;
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
amzLib.authorizeOnBillingAgreement.restore();
|
||||
uuid.v4.restore();
|
||||
});
|
||||
|
||||
it('charges for a new member', async () => {
|
||||
data.paymentMethod = amzLib.constants.PAYMENT_METHOD;
|
||||
await payments.createSubscription(data);
|
||||
|
||||
let updatedGroup = await Group.findById(group._id).exec();
|
||||
|
||||
updatedGroup.memberCount += 1;
|
||||
await updatedGroup.save();
|
||||
|
||||
await amzLib.chargeForAdditionalGroupMember(updatedGroup);
|
||||
|
||||
expect(spy.calledOnce).to.be.true;
|
||||
expect(spy).to.be.calledWith({
|
||||
AmazonBillingAgreementId: updatedGroup.purchased.plan.customerId,
|
||||
AuthorizationReferenceId: uuidString.substring(0, 32),
|
||||
AuthorizationAmount: {
|
||||
CurrencyCode: amzLib.constants.CURRENCY_CODE,
|
||||
Amount: 3,
|
||||
},
|
||||
SellerAuthorizationNote: amzLib.constants.SELLER_NOTE_GROUP_NEW_MEMBER,
|
||||
TransactionTimeout: 0,
|
||||
CaptureNow: true,
|
||||
SellerNote: amzLib.constants.SELLER_NOTE_GROUP_NEW_MEMBER,
|
||||
SellerOrderAttributes: {
|
||||
SellerOrderId: uuidString,
|
||||
StoreName: amzLib.constants.STORE_NAME,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
7
test/api/v3/unit/libs/payments/paymentHelpers.js
Normal file
7
test/api/v3/unit/libs/payments/paymentHelpers.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { model as User } from '../../../../../../website/server/models/user';
|
||||
|
||||
export async function createNonLeaderGroupMember (group) {
|
||||
let nonLeader = new User();
|
||||
nonLeader.guilds.push(group._id);
|
||||
return await nonLeader.save();
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/* eslint-disable camelcase */
|
||||
import payments from '../../../../../../../website/server/libs/payments';
|
||||
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
|
||||
import { model as User } from '../../../../../../../website/server/models/user';
|
||||
|
||||
describe('checkout success', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
let user, gift, customerId, paymentId;
|
||||
let paypalPaymentExecuteStub, paymentBuyGemsStub, paymentsCreateSubscritionStub;
|
||||
|
||||
beforeEach(() => {
|
||||
user = new User();
|
||||
customerId = 'customerId-test';
|
||||
paymentId = 'paymentId-test';
|
||||
|
||||
paypalPaymentExecuteStub = sinon.stub(paypalPayments, 'paypalPaymentExecute').returnsPromise().resolves({});
|
||||
paymentBuyGemsStub = sinon.stub(payments, 'buyGems').returnsPromise().resolves({});
|
||||
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
paypalPayments.paypalPaymentExecute.restore();
|
||||
payments.buyGems.restore();
|
||||
payments.createSubscription.restore();
|
||||
});
|
||||
|
||||
it('purchases gems', async () => {
|
||||
await paypalPayments.checkoutSuccess({user, gift, paymentId, customerId});
|
||||
|
||||
expect(paypalPaymentExecuteStub).to.be.calledOnce;
|
||||
expect(paypalPaymentExecuteStub).to.be.calledWith(paymentId, { payer_id: customerId });
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
customerId,
|
||||
paymentMethod: 'Paypal',
|
||||
});
|
||||
});
|
||||
|
||||
it('gifts gems', async () => {
|
||||
let receivingUser = new User();
|
||||
await receivingUser.save();
|
||||
gift = {
|
||||
type: 'gems',
|
||||
gems: {
|
||||
amount: 16,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
|
||||
await paypalPayments.checkoutSuccess({user, gift, paymentId, customerId});
|
||||
|
||||
expect(paypalPaymentExecuteStub).to.be.calledOnce;
|
||||
expect(paypalPaymentExecuteStub).to.be.calledWith(paymentId, { payer_id: customerId });
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
customerId,
|
||||
paymentMethod: 'PayPal (Gift)',
|
||||
gift,
|
||||
});
|
||||
});
|
||||
|
||||
it('gifts subscription', async () => {
|
||||
let receivingUser = new User();
|
||||
await receivingUser.save();
|
||||
gift = {
|
||||
type: 'subscription',
|
||||
subscription: {
|
||||
key: subKey,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
|
||||
await paypalPayments.checkoutSuccess({user, gift, paymentId, customerId});
|
||||
|
||||
expect(paypalPaymentExecuteStub).to.be.calledOnce;
|
||||
expect(paypalPaymentExecuteStub).to.be.calledWith(paymentId, { payer_id: customerId });
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
customerId,
|
||||
paymentMethod: 'PayPal (Gift)',
|
||||
gift,
|
||||
});
|
||||
});
|
||||
});
|
||||
127
test/api/v3/unit/libs/payments/paypal/checkout.test.js
Normal file
127
test/api/v3/unit/libs/payments/paypal/checkout.test.js
Normal file
@@ -0,0 +1,127 @@
|
||||
/* eslint-disable camelcase */
|
||||
import nconf from 'nconf';
|
||||
|
||||
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
|
||||
import { model as User } from '../../../../../../../website/server/models/user';
|
||||
import common from '../../../../../../../website/common';
|
||||
|
||||
const BASE_URL = nconf.get('BASE_URL');
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('checkout', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
let paypalPaymentCreateStub;
|
||||
let approvalHerf;
|
||||
|
||||
function getPaypalCreateOptions (description, amount) {
|
||||
return {
|
||||
intent: 'sale',
|
||||
payer: { payment_method: 'Paypal' },
|
||||
redirect_urls: {
|
||||
return_url: `${BASE_URL}/paypal/checkout/success`,
|
||||
cancel_url: `${BASE_URL}`,
|
||||
},
|
||||
transactions: [{
|
||||
item_list: {
|
||||
items: [{
|
||||
name: description,
|
||||
price: amount,
|
||||
currency: 'USD',
|
||||
quantity: 1,
|
||||
}],
|
||||
},
|
||||
amount: {
|
||||
currency: 'USD',
|
||||
total: amount,
|
||||
},
|
||||
description,
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
approvalHerf = 'approval_href';
|
||||
paypalPaymentCreateStub = sinon.stub(paypalPayments, 'paypalPaymentCreate')
|
||||
.returnsPromise().resolves({
|
||||
links: [{ rel: 'approval_url', href: approvalHerf }],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
paypalPayments.paypalPaymentCreate.restore();
|
||||
});
|
||||
|
||||
it('creates a link for gem purchases', async () => {
|
||||
let link = await paypalPayments.checkout({user: new User()});
|
||||
|
||||
expect(paypalPaymentCreateStub).to.be.calledOnce;
|
||||
expect(paypalPaymentCreateStub).to.be.calledWith(getPaypalCreateOptions('Habitica Gems', 5.00));
|
||||
expect(link).to.eql(approvalHerf);
|
||||
});
|
||||
|
||||
it('should error if gem amount is too low', async () => {
|
||||
let receivingUser = new User();
|
||||
receivingUser.save();
|
||||
let gift = {
|
||||
type: 'gems',
|
||||
gems: {
|
||||
amount: 0,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
|
||||
await expect(paypalPayments.checkout({gift}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
message: 'Amount must be at least 1.',
|
||||
name: 'BadRequest',
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if the user cannot get gems', async () => {
|
||||
let user = new User();
|
||||
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
|
||||
|
||||
await expect(paypalPayments.checkout({user})).to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
message: i18n.t('groupPolicyCannotGetGems'),
|
||||
name: 'NotAuthorized',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a link for gifting gems', async () => {
|
||||
let receivingUser = new User();
|
||||
await receivingUser.save();
|
||||
let gift = {
|
||||
type: 'gems',
|
||||
uuid: receivingUser._id,
|
||||
gems: {
|
||||
amount: 16,
|
||||
},
|
||||
};
|
||||
|
||||
let link = await paypalPayments.checkout({gift});
|
||||
|
||||
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 () => {
|
||||
let receivingUser = new User();
|
||||
receivingUser.save();
|
||||
let gift = {
|
||||
type: 'subscription',
|
||||
subscription: {
|
||||
key: subKey,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
|
||||
let link = await paypalPayments.checkout({gift});
|
||||
|
||||
expect(paypalPaymentCreateStub).to.be.calledOnce;
|
||||
expect(paypalPaymentCreateStub).to.be.calledWith(getPaypalCreateOptions('mo. Habitica Subscription (Gift)', '15.00'));
|
||||
expect(link).to.eql(approvalHerf);
|
||||
});
|
||||
});
|
||||
66
test/api/v3/unit/libs/payments/paypal/ipn.test.js
Normal file
66
test/api/v3/unit/libs/payments/paypal/ipn.test.js
Normal file
@@ -0,0 +1,66 @@
|
||||
/* eslint-disable camelcase */
|
||||
import payments from '../../../../../../../website/server/libs/payments';
|
||||
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../../../website/server/models/user';
|
||||
|
||||
describe('ipn', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
let user, group, txn_type, userPaymentId, groupPaymentId;
|
||||
let ipnVerifyAsyncStub, paymentCancelSubscriptionSpy;
|
||||
|
||||
beforeEach(async () => {
|
||||
txn_type = 'recurring_payment_profile_cancel';
|
||||
userPaymentId = 'userPaymentId-test';
|
||||
groupPaymentId = 'groupPaymentId-test';
|
||||
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.customerId = userPaymentId;
|
||||
user.purchased.plan.planId = subKey;
|
||||
user.purchased.plan.lastBillingDate = new Date();
|
||||
await user.save();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = groupPaymentId;
|
||||
group.purchased.plan.planId = subKey;
|
||||
group.purchased.plan.lastBillingDate = new Date();
|
||||
await group.save();
|
||||
|
||||
ipnVerifyAsyncStub = sinon.stub(paypalPayments, 'ipnVerifyAsync').returnsPromise().resolves({});
|
||||
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
paypalPayments.ipnVerifyAsync.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
|
||||
it('should cancel a user subscription', async () => {
|
||||
await paypalPayments.ipn({txn_type, recurring_payment_id: userPaymentId});
|
||||
|
||||
expect(ipnVerifyAsyncStub).to.be.calledOnce;
|
||||
expect(ipnVerifyAsyncStub).to.be.calledWith({txn_type, recurring_payment_id: userPaymentId});
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy.args[0][0].user._id).to.eql(user._id);
|
||||
expect(paymentCancelSubscriptionSpy.args[0][0].paymentMethod).to.eql('Paypal');
|
||||
});
|
||||
|
||||
it('should cancel a group subscription', async () => {
|
||||
await paypalPayments.ipn({txn_type, recurring_payment_id: groupPaymentId});
|
||||
|
||||
expect(ipnVerifyAsyncStub).to.be.calledOnce;
|
||||
expect(ipnVerifyAsyncStub).to.be.calledWith({txn_type, recurring_payment_id: groupPaymentId});
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({ groupId: group._id, paymentMethod: 'Paypal' });
|
||||
});
|
||||
});
|
||||
124
test/api/v3/unit/libs/payments/paypal/subscribe-cancel.test.js
Normal file
124
test/api/v3/unit/libs/payments/paypal/subscribe-cancel.test.js
Normal file
@@ -0,0 +1,124 @@
|
||||
/* eslint-disable camelcase */
|
||||
import payments from '../../../../../../../website/server/libs/payments';
|
||||
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../../../website/server/models/user';
|
||||
import common from '../../../../../../../website/common';
|
||||
import { createNonLeaderGroupMember } from '../paymentHelpers';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('subscribeCancel', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
let user, group, groupId, customerId, groupCustomerId, nextBillingDate;
|
||||
let paymentCancelSubscriptionSpy, paypalBillingAgreementCancelStub, paypalBillingAgreementGetStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
customerId = 'customer-id';
|
||||
groupCustomerId = 'groupCustomerId-test';
|
||||
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.customerId = customerId;
|
||||
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 = groupCustomerId;
|
||||
group.purchased.plan.planId = subKey;
|
||||
group.purchased.plan.lastBillingDate = new Date();
|
||||
await group.save();
|
||||
|
||||
nextBillingDate = new Date();
|
||||
|
||||
paypalBillingAgreementCancelStub = sinon.stub(paypalPayments, 'paypalBillingAgreementCancel').returnsPromise().resolves({});
|
||||
paypalBillingAgreementGetStub = sinon.stub(paypalPayments, 'paypalBillingAgreementGet')
|
||||
.returnsPromise().resolves({
|
||||
agreement_details: {
|
||||
next_billing_date: nextBillingDate,
|
||||
cycles_completed: 1,
|
||||
},
|
||||
});
|
||||
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
paypalPayments.paypalBillingAgreementGet.restore();
|
||||
paypalPayments.paypalBillingAgreementCancel.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if we are missing a subscription', async () => {
|
||||
user.purchased.plan.customerId = undefined;
|
||||
|
||||
await expect(paypalPayments.subscribeCancel({user}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('missingSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if group is not found', async () => {
|
||||
await expect(paypalPayments.subscribeCancel({user, groupId: 'fake-id'}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
message: i18n.t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if user is not group leader', async () => {
|
||||
let nonLeader = await createNonLeaderGroupMember(group);
|
||||
|
||||
await expect(paypalPayments.subscribeCancel({user: nonLeader, groupId: group._id}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel a user subscription', async () => {
|
||||
await paypalPayments.subscribeCancel({user});
|
||||
|
||||
expect(paypalBillingAgreementGetStub).to.be.calledOnce;
|
||||
expect(paypalBillingAgreementGetStub).to.be.calledWith(customerId);
|
||||
expect(paypalBillingAgreementCancelStub).to.be.calledOnce;
|
||||
expect(paypalBillingAgreementCancelStub).to.be.calledWith(customerId, { note: i18n.t('cancelingSubscription') });
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
|
||||
user,
|
||||
groupId,
|
||||
paymentMethod: 'Paypal',
|
||||
nextBill: nextBillingDate,
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel a group subscription', async () => {
|
||||
await paypalPayments.subscribeCancel({user, groupId: group._id});
|
||||
|
||||
expect(paypalBillingAgreementGetStub).to.be.calledOnce;
|
||||
expect(paypalBillingAgreementGetStub).to.be.calledWith(groupCustomerId);
|
||||
expect(paypalBillingAgreementCancelStub).to.be.calledOnce;
|
||||
expect(paypalBillingAgreementCancelStub).to.be.calledWith(groupCustomerId, { note: i18n.t('cancelingSubscription') });
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
|
||||
user,
|
||||
groupId: group._id,
|
||||
paymentMethod: 'Paypal',
|
||||
nextBill: nextBillingDate,
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
/* eslint-disable camelcase */
|
||||
import payments from '../../../../../../../website/server/libs/payments';
|
||||
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../../../website/server/models/user';
|
||||
import common from '../../../../../../../website/common';
|
||||
|
||||
describe('subscribeSuccess', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
let user, group, block, groupId, token, headers, customerId;
|
||||
let paypalBillingAgreementExecuteStub, paymentsCreateSubscritionStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
|
||||
token = 'test-token';
|
||||
headers = {};
|
||||
block = common.content.subscriptionBlocks[subKey];
|
||||
customerId = 'test-customerId';
|
||||
|
||||
paypalBillingAgreementExecuteStub = sinon.stub(paypalPayments, 'paypalBillingAgreementExecute')
|
||||
.returnsPromise({}).resolves({
|
||||
id: customerId,
|
||||
});
|
||||
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
paypalPayments.paypalBillingAgreementExecute.restore();
|
||||
payments.createSubscription.restore();
|
||||
});
|
||||
|
||||
it('creates a user subscription', async () => {
|
||||
await paypalPayments.subscribeSuccess({user, block, groupId, token, headers});
|
||||
|
||||
expect(paypalBillingAgreementExecuteStub).to.be.calledOnce;
|
||||
expect(paypalBillingAgreementExecuteStub).to.be.calledWith(token, {});
|
||||
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
groupId,
|
||||
customerId,
|
||||
paymentMethod: 'Paypal',
|
||||
sub: block,
|
||||
headers,
|
||||
});
|
||||
});
|
||||
|
||||
it('create a group subscription', async () => {
|
||||
groupId = group._id;
|
||||
|
||||
await paypalPayments.subscribeSuccess({user, block, groupId, token, headers});
|
||||
|
||||
expect(paypalBillingAgreementExecuteStub).to.be.calledOnce;
|
||||
expect(paypalBillingAgreementExecuteStub).to.be.calledWith(token, {});
|
||||
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
groupId,
|
||||
customerId,
|
||||
paymentMethod: 'Paypal',
|
||||
sub: block,
|
||||
headers,
|
||||
});
|
||||
});
|
||||
});
|
||||
112
test/api/v3/unit/libs/payments/paypal/subscribe.test.js
Normal file
112
test/api/v3/unit/libs/payments/paypal/subscribe.test.js
Normal file
@@ -0,0 +1,112 @@
|
||||
/* eslint-disable camelcase */
|
||||
import moment from 'moment';
|
||||
import cc from 'coupon-code';
|
||||
|
||||
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
|
||||
import { model as Coupon } from '../../../../../../../website/server/models/coupon';
|
||||
import common from '../../../../../../../website/common';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('subscribe', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
let coupon, sub, approvalHerf;
|
||||
let paypalBillingAgreementCreateStub;
|
||||
|
||||
beforeEach(() => {
|
||||
approvalHerf = 'approvalHerf-test';
|
||||
sub = Object.assign({}, common.content.subscriptionBlocks[subKey]);
|
||||
|
||||
paypalBillingAgreementCreateStub = sinon.stub(paypalPayments, 'paypalBillingAgreementCreate')
|
||||
.returnsPromise().resolves({
|
||||
links: [{ rel: 'approval_url', href: approvalHerf }],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
paypalPayments.paypalBillingAgreementCreate.restore();
|
||||
});
|
||||
|
||||
it('should throw an error when coupon code is missing', async () => {
|
||||
sub.discount = 40;
|
||||
|
||||
await expect(paypalPayments.subscribe({sub, 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';
|
||||
|
||||
let couponModel = new Coupon();
|
||||
couponModel.event = 'google_6mo';
|
||||
await couponModel.save();
|
||||
|
||||
sinon.stub(cc, 'validate').returns('invalid');
|
||||
|
||||
await expect(paypalPayments.subscribe({sub, coupon}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('invalidCoupon'),
|
||||
});
|
||||
cc.validate.restore();
|
||||
});
|
||||
|
||||
it('subscribes with paypal with a coupon', async () => {
|
||||
sub.discount = 40;
|
||||
sub.key = 'google_6mo';
|
||||
coupon = 'example-coupon';
|
||||
|
||||
let couponModel = new Coupon();
|
||||
couponModel.event = 'google_6mo';
|
||||
let updatedCouponModel = await couponModel.save();
|
||||
|
||||
sinon.stub(cc, 'validate').returns(updatedCouponModel._id);
|
||||
|
||||
let link = await paypalPayments.subscribe({sub, coupon});
|
||||
|
||||
expect(link).to.eql(approvalHerf);
|
||||
expect(paypalBillingAgreementCreateStub).to.be.calledOnce;
|
||||
let billingPlanTitle = `Habitica Subscription ($${sub.price} every ${sub.months} months, recurring)`;
|
||||
expect(paypalBillingAgreementCreateStub).to.be.calledWith({
|
||||
name: billingPlanTitle,
|
||||
description: billingPlanTitle,
|
||||
start_date: moment().add({ minutes: 5 }).format(),
|
||||
plan: {
|
||||
id: sub.paypalKey,
|
||||
},
|
||||
payer: {
|
||||
payment_method: 'Paypal',
|
||||
},
|
||||
});
|
||||
|
||||
cc.validate.restore();
|
||||
});
|
||||
|
||||
it('creates a link for a subscription', async () => {
|
||||
delete sub.discount;
|
||||
|
||||
let link = await paypalPayments.subscribe({sub, coupon});
|
||||
|
||||
expect(link).to.eql(approvalHerf);
|
||||
expect(paypalBillingAgreementCreateStub).to.be.calledOnce;
|
||||
let billingPlanTitle = `Habitica Subscription ($${sub.price} every ${sub.months} months, recurring)`;
|
||||
expect(paypalBillingAgreementCreateStub).to.be.calledWith({
|
||||
name: billingPlanTitle,
|
||||
description: billingPlanTitle,
|
||||
start_date: moment().add({ minutes: 5 }).format(),
|
||||
plan: {
|
||||
id: sub.paypalKey,
|
||||
},
|
||||
payer: {
|
||||
payment_method: 'Paypal',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,143 @@
|
||||
import stripeModule from 'stripe';
|
||||
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../../../website/server/models/user';
|
||||
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
|
||||
import payments from '../../../../../../../website/server/libs/payments';
|
||||
import common from '../../../../../../../website/common';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('cancel subscription', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
const stripe = stripeModule('test');
|
||||
let user, groupId, 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 () => {
|
||||
let 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, paymentsCancelSubStub, stripeRetrieveStub, subscriptionId, currentPeriodEndTimeStamp;
|
||||
|
||||
beforeEach(() => {
|
||||
subscriptionId = 'subId';
|
||||
stripeDeleteCustomerStub = sinon.stub(stripe.customers, 'del').returnsPromise().resolves({});
|
||||
paymentsCancelSubStub = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
|
||||
|
||||
currentPeriodEndTimeStamp = (new Date()).getTime();
|
||||
stripeRetrieveStub = sinon.stub(stripe.customers, 'retrieve')
|
||||
.returnsPromise().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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,307 @@
|
||||
import stripeModule from 'stripe';
|
||||
import cc from 'coupon-code';
|
||||
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../../../website/server/models/user';
|
||||
import { model as Coupon } from '../../../../../../../website/server/models/coupon';
|
||||
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
|
||||
import payments from '../../../../../../../website/server/libs/payments';
|
||||
import common from '../../../../../../../website/common';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('checkout with subscription', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
const stripe = stripeModule('test');
|
||||
let user, group, data, gift, sub, groupId, email, headers, coupon, customerIdResponse, subscriptionId, 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.returnsPromise().resolves;
|
||||
|
||||
stripeCreateCustomerSpy = sinon.stub(stripe.customers, 'create');
|
||||
let stripCustomerResponse = {
|
||||
id: customerIdResponse,
|
||||
subscriptions: {
|
||||
data: [{id: subscriptionId}],
|
||||
},
|
||||
};
|
||||
stripeCreateCustomerSpy.returnsPromise().resolves(stripCustomerResponse);
|
||||
|
||||
stripePaymentsCreateSubSpy = sinon.stub(payments, 'createSubscription');
|
||||
stripePaymentsCreateSubSpy.returnsPromise().resolves({});
|
||||
|
||||
data.groupId = group._id;
|
||||
data.sub.quantity = 3;
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
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';
|
||||
|
||||
let 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';
|
||||
|
||||
let couponModel = new Coupon();
|
||||
couponModel.event = 'google_6mo';
|
||||
let 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';
|
||||
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 = {};
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
193
test/api/v3/unit/libs/payments/stripe/checkout.test.js
Normal file
193
test/api/v3/unit/libs/payments/stripe/checkout.test.js
Normal file
@@ -0,0 +1,193 @@
|
||||
import stripeModule from 'stripe';
|
||||
|
||||
import { model as User } from '../../../../../../../website/server/models/user';
|
||||
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
|
||||
import payments from '../../../../../../../website/server/libs/payments';
|
||||
import common from '../../../../../../../website/common';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('checkout', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
const stripe = stripeModule('test');
|
||||
let stripeChargeStub, paymentBuyGemsStub, paymentCreateSubscritionStub;
|
||||
let user, gift, groupId, email, headers, coupon, customerIdResponse, token;
|
||||
|
||||
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';
|
||||
let stripCustomerResponse = {
|
||||
id: customerIdResponse,
|
||||
};
|
||||
stripeChargeStub = sinon.stub(stripe.charges, 'create').returnsPromise().resolves(stripCustomerResponse);
|
||||
paymentBuyGemsStub = sinon.stub(payments, 'buyGems').returnsPromise().resolves({});
|
||||
paymentCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
stripe.charges.create.restore();
|
||||
payments.buyGems.restore();
|
||||
payments.createSubscription.restore();
|
||||
});
|
||||
|
||||
it('should error if gem amount is too low', async () => {
|
||||
let receivingUser = new User();
|
||||
receivingUser.save();
|
||||
gift = {
|
||||
type: 'gems',
|
||||
gems: {
|
||||
amount: 0,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
|
||||
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',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should error if user cannot get gems', async () => {
|
||||
gift = undefined;
|
||||
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
|
||||
|
||||
await expect(stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe)).to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
message: i18n.t('groupPolicyCannotGetGems'),
|
||||
name: 'NotAuthorized',
|
||||
});
|
||||
});
|
||||
|
||||
it('should purchase gems', async () => {
|
||||
gift = undefined;
|
||||
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
|
||||
|
||||
await stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe);
|
||||
|
||||
expect(stripeChargeStub).to.be.calledOnce;
|
||||
expect(stripeChargeStub).to.be.calledWith({
|
||||
amount: 500,
|
||||
currency: 'usd',
|
||||
card: token,
|
||||
});
|
||||
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
customerId: customerIdResponse,
|
||||
paymentMethod: 'Stripe',
|
||||
gift,
|
||||
});
|
||||
expect(user.canGetGems).to.be.calledOnce;
|
||||
user.canGetGems.restore();
|
||||
});
|
||||
|
||||
it('should gift gems', async () => {
|
||||
let 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,
|
||||
});
|
||||
});
|
||||
|
||||
it('should gift a subscription', async () => {
|
||||
let receivingUser = new User();
|
||||
receivingUser.save();
|
||||
gift = {
|
||||
type: 'subscription',
|
||||
subscription: {
|
||||
key: subKey,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
|
||||
await stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe);
|
||||
|
||||
gift.member = receivingUser;
|
||||
expect(stripeChargeStub).to.be.calledOnce;
|
||||
expect(stripeChargeStub).to.be.calledWith({
|
||||
amount: '1500',
|
||||
currency: 'usd',
|
||||
card: token,
|
||||
});
|
||||
|
||||
expect(paymentCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
customerId: customerIdResponse,
|
||||
paymentMethod: 'Gift',
|
||||
gift,
|
||||
});
|
||||
});
|
||||
});
|
||||
147
test/api/v3/unit/libs/payments/stripe/edit-subscription.test.js
Normal file
147
test/api/v3/unit/libs/payments/stripe/edit-subscription.test.js
Normal file
@@ -0,0 +1,147 @@
|
||||
import stripeModule from 'stripe';
|
||||
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../../../website/server/models/user';
|
||||
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
|
||||
import common from '../../../../../../../website/common';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('edit subscription', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
const stripe = stripeModule('test');
|
||||
let user, groupId, group, 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 () => {
|
||||
let 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, stripeUpdateSubscriptionStub, subscriptionId;
|
||||
|
||||
beforeEach(() => {
|
||||
subscriptionId = 'subId';
|
||||
stripeListSubscriptionStub = sinon.stub(stripe.customers, 'listSubscriptions')
|
||||
.returnsPromise().resolves({
|
||||
data: [{id: subscriptionId}],
|
||||
});
|
||||
|
||||
stripeUpdateSubscriptionStub = sinon.stub(stripe.customers, 'updateSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
stripe.customers.listSubscriptions.restore();
|
||||
stripe.customers.updateSubscription.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(user.purchased.plan.customerId);
|
||||
expect(stripeUpdateSubscriptionStub).to.be.calledOnce;
|
||||
expect(stripeUpdateSubscriptionStub).to.be.calledWith(
|
||||
user.purchased.plan.customerId,
|
||||
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(group.purchased.plan.customerId);
|
||||
expect(stripeUpdateSubscriptionStub).to.be.calledOnce;
|
||||
expect(stripeUpdateSubscriptionStub).to.be.calledWith(
|
||||
group.purchased.plan.customerId,
|
||||
subscriptionId,
|
||||
{ card: token }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
260
test/api/v3/unit/libs/payments/stripe/handle-webhook.test.js
Normal file
260
test/api/v3/unit/libs/payments/stripe/handle-webhook.test.js
Normal file
@@ -0,0 +1,260 @@
|
||||
import stripeModule from 'stripe';
|
||||
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../../../website/server/models/user';
|
||||
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
|
||||
import payments from '../../../../../../../website/server/libs/payments';
|
||||
import common from '../../../../../../../website/common';
|
||||
import logger from '../../../../../../../website/server/libs/logger';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import moment from 'moment';
|
||||
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('Stripe - Webhooks', () => {
|
||||
const stripe = stripeModule('test');
|
||||
|
||||
describe('all events', () => {
|
||||
const eventType = 'account.updated';
|
||||
const event = {id: 123};
|
||||
const eventRetrieved = {type: eventType};
|
||||
|
||||
beforeEach(() => {
|
||||
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves(eventRetrieved);
|
||||
sinon.stub(logger, 'error');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
stripe.events.retrieve.restore();
|
||||
logger.error.restore();
|
||||
});
|
||||
|
||||
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.called.once;
|
||||
|
||||
const calledWith = logger.error.getCall(0).args;
|
||||
expect(calledWith[0].message).to.equal(error.message);
|
||||
expect(calledWith[1].event).to.equal(eventRetrieved);
|
||||
});
|
||||
|
||||
it('retrieves and validates the event from Stripe', async () => {
|
||||
await stripePayments.handleWebhooks({requestBody: event}, stripe);
|
||||
expect(stripe.events.retrieve).to.have.been.called.once;
|
||||
expect(stripe.events.retrieve).to.have.been.calledWith(event.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('customer.subscription.deleted', () => {
|
||||
const eventType = 'customer.subscription.deleted';
|
||||
|
||||
beforeEach(() => {
|
||||
sinon.stub(stripe.customers, 'del').returnsPromise().resolves({});
|
||||
sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
stripe.customers.del.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
|
||||
it('does not do anything if event.request is null (subscription cancelled manually)', async () => {
|
||||
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
|
||||
id: 123,
|
||||
type: eventType,
|
||||
request: 123,
|
||||
});
|
||||
|
||||
await stripePayments.handleWebhooks({requestBody: {}}, stripe);
|
||||
|
||||
expect(stripe.events.retrieve).to.have.been.called.once;
|
||||
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').returnsPromise().resolves({
|
||||
id: 123,
|
||||
type: eventType,
|
||||
data: {
|
||||
object: {
|
||||
plan: {
|
||||
id: 'basic_earned',
|
||||
},
|
||||
customer: customerId,
|
||||
},
|
||||
},
|
||||
request: null,
|
||||
});
|
||||
|
||||
await expect(stripePayments.handleWebhooks({requestBody: {}}, stripe)).to.eventually.be.rejectedWith({
|
||||
message: i18n.t('userNotFound'),
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const customerId = '456';
|
||||
|
||||
let subscriber = new User();
|
||||
subscriber.purchased.plan.customerId = customerId;
|
||||
subscriber.purchased.plan.paymentMethod = 'Stripe';
|
||||
await subscriber.save();
|
||||
|
||||
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
|
||||
id: 123,
|
||||
type: eventType,
|
||||
data: {
|
||||
object: {
|
||||
plan: {
|
||||
id: 'basic_earned',
|
||||
},
|
||||
customer: customerId,
|
||||
},
|
||||
},
|
||||
request: null,
|
||||
});
|
||||
|
||||
await stripePayments.handleWebhooks({requestBody: {}}, stripe);
|
||||
|
||||
expect(stripe.customers.del).to.have.been.calledOnce;
|
||||
expect(stripe.customers.del).to.have.been.calledWith(customerId);
|
||||
expect(payments.cancelSubscription).to.have.been.calledOnce;
|
||||
|
||||
let cancelSubscriptionOpts = payments.cancelSubscription.lastCall.args[0];
|
||||
expect(cancelSubscriptionOpts.user._id).to.equal(subscriber._id);
|
||||
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').returnsPromise().resolves({
|
||||
id: 123,
|
||||
type: eventType,
|
||||
data: {
|
||||
object: {
|
||||
plan: {
|
||||
id: 'group_monthly',
|
||||
},
|
||||
customer: customerId,
|
||||
},
|
||||
},
|
||||
request: null,
|
||||
});
|
||||
|
||||
await expect(stripePayments.handleWebhooks({requestBody: {}}, stripe)).to.eventually.be.rejectedWith({
|
||||
message: i18n.t('groupNotFound'),
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const customerId = 456;
|
||||
|
||||
let subscriber = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: uuid(),
|
||||
});
|
||||
subscriber.purchased.plan.customerId = customerId;
|
||||
subscriber.purchased.plan.paymentMethod = 'Stripe';
|
||||
await subscriber.save();
|
||||
|
||||
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
|
||||
id: 123,
|
||||
type: eventType,
|
||||
data: {
|
||||
object: {
|
||||
plan: {
|
||||
id: 'group_monthly',
|
||||
},
|
||||
customer: customerId,
|
||||
},
|
||||
},
|
||||
request: null,
|
||||
});
|
||||
|
||||
await expect(stripePayments.handleWebhooks({requestBody: {}}, stripe)).to.eventually.be.rejectedWith({
|
||||
message: i18n.t('userNotFound'),
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const customerId = '456';
|
||||
|
||||
let leader = new User();
|
||||
await leader.save();
|
||||
|
||||
let subscriber = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: leader._id,
|
||||
});
|
||||
subscriber.purchased.plan.customerId = customerId;
|
||||
subscriber.purchased.plan.paymentMethod = 'Stripe';
|
||||
await subscriber.save();
|
||||
|
||||
sinon.stub(stripe.events, 'retrieve').returnsPromise().resolves({
|
||||
id: 123,
|
||||
type: eventType,
|
||||
data: {
|
||||
object: {
|
||||
plan: {
|
||||
id: 'group_monthly',
|
||||
},
|
||||
customer: customerId,
|
||||
},
|
||||
},
|
||||
request: null,
|
||||
});
|
||||
|
||||
await stripePayments.handleWebhooks({requestBody: {}}, stripe);
|
||||
|
||||
expect(stripe.customers.del).to.have.been.calledOnce;
|
||||
expect(stripe.customers.del).to.have.been.calledWith(customerId);
|
||||
expect(payments.cancelSubscription).to.have.been.calledOnce;
|
||||
|
||||
let cancelSubscriptionOpts = payments.cancelSubscription.lastCall.args[0];
|
||||
expect(cancelSubscriptionOpts.user._id).to.equal(leader._id);
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
import stripeModule from 'stripe';
|
||||
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../../../website/server/models/user';
|
||||
import { model as Group } from '../../../../../../../website/server/models/group';
|
||||
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
|
||||
import payments from '../../../../../../../website/server/libs/payments';
|
||||
|
||||
describe('Stripe - Upgrade Group Plan', () => {
|
||||
const stripe = stripeModule('test');
|
||||
let spy, data, user, group;
|
||||
|
||||
beforeEach(async function () {
|
||||
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: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
await group.save();
|
||||
|
||||
spy = sinon.stub(stripe.subscriptions, 'update');
|
||||
spy.returnsPromise().resolves([]);
|
||||
data.groupId = group._id;
|
||||
data.sub.quantity = 3;
|
||||
stripePayments.setStripeApi(stripe);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
stripe.subscriptions.update.restore();
|
||||
});
|
||||
|
||||
it('updates a group plan quantity', async () => {
|
||||
data.paymentMethod = 'Stripe';
|
||||
await payments.createSubscription(data);
|
||||
|
||||
let 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,561 +0,0 @@
|
||||
/* eslint-disable camelcase */
|
||||
import nconf from 'nconf';
|
||||
import moment from 'moment';
|
||||
import cc from 'coupon-code';
|
||||
|
||||
import payments from '../../../../../website/server/libs/payments';
|
||||
import paypalPayments from '../../../../../website/server/libs/paypalPayments';
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../helpers/api-unit.helper.js';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import { model as Coupon } from '../../../../../website/server/models/coupon';
|
||||
import common from '../../../../../website/common';
|
||||
|
||||
const BASE_URL = nconf.get('BASE_URL');
|
||||
const i18n = common.i18n;
|
||||
|
||||
describe('Paypal Payments', () => {
|
||||
let subKey = 'basic_3mo';
|
||||
|
||||
describe('checkout', () => {
|
||||
let paypalPaymentCreateStub;
|
||||
let approvalHerf;
|
||||
|
||||
function getPaypalCreateOptions (description, amount) {
|
||||
return {
|
||||
intent: 'sale',
|
||||
payer: { payment_method: 'Paypal' },
|
||||
redirect_urls: {
|
||||
return_url: `${BASE_URL}/paypal/checkout/success`,
|
||||
cancel_url: `${BASE_URL}`,
|
||||
},
|
||||
transactions: [{
|
||||
item_list: {
|
||||
items: [{
|
||||
name: description,
|
||||
price: amount,
|
||||
currency: 'USD',
|
||||
quantity: 1,
|
||||
}],
|
||||
},
|
||||
amount: {
|
||||
currency: 'USD',
|
||||
total: amount,
|
||||
},
|
||||
description,
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
approvalHerf = 'approval_href';
|
||||
paypalPaymentCreateStub = sinon.stub(paypalPayments, 'paypalPaymentCreate')
|
||||
.returnsPromise().resolves({
|
||||
links: [{ rel: 'approval_url', href: approvalHerf }],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
paypalPayments.paypalPaymentCreate.restore();
|
||||
});
|
||||
|
||||
it('creates a link for gem purchases', async () => {
|
||||
let link = await paypalPayments.checkout({user: new User()});
|
||||
|
||||
expect(paypalPaymentCreateStub).to.be.calledOnce;
|
||||
expect(paypalPaymentCreateStub).to.be.calledWith(getPaypalCreateOptions('Habitica Gems', 5.00));
|
||||
expect(link).to.eql(approvalHerf);
|
||||
});
|
||||
|
||||
it('should error if gem amount is too low', async () => {
|
||||
let receivingUser = new User();
|
||||
receivingUser.save();
|
||||
let gift = {
|
||||
type: 'gems',
|
||||
gems: {
|
||||
amount: 0,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
|
||||
await expect(paypalPayments.checkout({gift}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
message: 'Amount must be at least 1.',
|
||||
name: 'BadRequest',
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if the user cannot get gems', async () => {
|
||||
let user = new User();
|
||||
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
|
||||
|
||||
await expect(paypalPayments.checkout({user})).to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
message: i18n.t('groupPolicyCannotGetGems'),
|
||||
name: 'NotAuthorized',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a link for gifting gems', async () => {
|
||||
let receivingUser = new User();
|
||||
await receivingUser.save();
|
||||
let gift = {
|
||||
type: 'gems',
|
||||
uuid: receivingUser._id,
|
||||
gems: {
|
||||
amount: 16,
|
||||
},
|
||||
};
|
||||
|
||||
let link = await paypalPayments.checkout({gift});
|
||||
|
||||
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 () => {
|
||||
let receivingUser = new User();
|
||||
receivingUser.save();
|
||||
let gift = {
|
||||
type: 'subscription',
|
||||
subscription: {
|
||||
key: subKey,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
|
||||
let link = await paypalPayments.checkout({gift});
|
||||
|
||||
expect(paypalPaymentCreateStub).to.be.calledOnce;
|
||||
expect(paypalPaymentCreateStub).to.be.calledWith(getPaypalCreateOptions('mo. Habitica Subscription (Gift)', '15.00'));
|
||||
expect(link).to.eql(approvalHerf);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkout success', () => {
|
||||
let user, gift, customerId, paymentId;
|
||||
let paypalPaymentExecuteStub, paymentBuyGemsStub, paymentsCreateSubscritionStub;
|
||||
|
||||
beforeEach(() => {
|
||||
user = new User();
|
||||
customerId = 'customerId-test';
|
||||
paymentId = 'paymentId-test';
|
||||
|
||||
paypalPaymentExecuteStub = sinon.stub(paypalPayments, 'paypalPaymentExecute').returnsPromise().resolves({});
|
||||
paymentBuyGemsStub = sinon.stub(payments, 'buyGems').returnsPromise().resolves({});
|
||||
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
paypalPayments.paypalPaymentExecute.restore();
|
||||
payments.buyGems.restore();
|
||||
payments.createSubscription.restore();
|
||||
});
|
||||
|
||||
it('purchases gems', async () => {
|
||||
await paypalPayments.checkoutSuccess({user, gift, paymentId, customerId});
|
||||
|
||||
expect(paypalPaymentExecuteStub).to.be.calledOnce;
|
||||
expect(paypalPaymentExecuteStub).to.be.calledWith(paymentId, { payer_id: customerId });
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
customerId,
|
||||
paymentMethod: 'Paypal',
|
||||
});
|
||||
});
|
||||
|
||||
it('gifts gems', async () => {
|
||||
let receivingUser = new User();
|
||||
await receivingUser.save();
|
||||
gift = {
|
||||
type: 'gems',
|
||||
gems: {
|
||||
amount: 16,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
|
||||
await paypalPayments.checkoutSuccess({user, gift, paymentId, customerId});
|
||||
|
||||
expect(paypalPaymentExecuteStub).to.be.calledOnce;
|
||||
expect(paypalPaymentExecuteStub).to.be.calledWith(paymentId, { payer_id: customerId });
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
customerId,
|
||||
paymentMethod: 'PayPal (Gift)',
|
||||
gift,
|
||||
});
|
||||
});
|
||||
|
||||
it('gifts subscription', async () => {
|
||||
let receivingUser = new User();
|
||||
await receivingUser.save();
|
||||
gift = {
|
||||
type: 'subscription',
|
||||
subscription: {
|
||||
key: subKey,
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
};
|
||||
|
||||
await paypalPayments.checkoutSuccess({user, gift, paymentId, customerId});
|
||||
|
||||
expect(paypalPaymentExecuteStub).to.be.calledOnce;
|
||||
expect(paypalPaymentExecuteStub).to.be.calledWith(paymentId, { payer_id: customerId });
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
customerId,
|
||||
paymentMethod: 'PayPal (Gift)',
|
||||
gift,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscribe', () => {
|
||||
let coupon, sub, approvalHerf;
|
||||
let paypalBillingAgreementCreateStub;
|
||||
|
||||
beforeEach(() => {
|
||||
approvalHerf = 'approvalHerf-test';
|
||||
sub = common.content.subscriptionBlocks[subKey];
|
||||
|
||||
paypalBillingAgreementCreateStub = sinon.stub(paypalPayments, 'paypalBillingAgreementCreate')
|
||||
.returnsPromise().resolves({
|
||||
links: [{ rel: 'approval_url', href: approvalHerf }],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
paypalPayments.paypalBillingAgreementCreate.restore();
|
||||
});
|
||||
|
||||
it('should throw an error when coupon code is missing', async () => {
|
||||
sub.discount = 40;
|
||||
|
||||
await expect(paypalPayments.subscribe({sub, 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';
|
||||
|
||||
let couponModel = new Coupon();
|
||||
couponModel.event = 'google_6mo';
|
||||
await couponModel.save();
|
||||
|
||||
sinon.stub(cc, 'validate').returns('invalid');
|
||||
|
||||
await expect(paypalPayments.subscribe({sub, coupon}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('invalidCoupon'),
|
||||
});
|
||||
cc.validate.restore();
|
||||
});
|
||||
|
||||
it('subscribes with amazon with a coupon', async () => {
|
||||
sub.discount = 40;
|
||||
sub.key = 'google_6mo';
|
||||
coupon = 'example-coupon';
|
||||
|
||||
let couponModel = new Coupon();
|
||||
couponModel.event = 'google_6mo';
|
||||
let updatedCouponModel = await couponModel.save();
|
||||
|
||||
sinon.stub(cc, 'validate').returns(updatedCouponModel._id);
|
||||
|
||||
let link = await paypalPayments.subscribe({sub, coupon});
|
||||
|
||||
expect(link).to.eql(approvalHerf);
|
||||
expect(paypalBillingAgreementCreateStub).to.be.calledOnce;
|
||||
let billingPlanTitle = `Habitica Subscription ($${sub.price} every ${sub.months} months, recurring)`;
|
||||
expect(paypalBillingAgreementCreateStub).to.be.calledWith({
|
||||
name: billingPlanTitle,
|
||||
description: billingPlanTitle,
|
||||
start_date: moment().add({ minutes: 5 }).format(),
|
||||
plan: {
|
||||
id: sub.paypalKey,
|
||||
},
|
||||
payer: {
|
||||
payment_method: 'Paypal',
|
||||
},
|
||||
});
|
||||
|
||||
cc.validate.restore();
|
||||
});
|
||||
|
||||
it('creates a link for a subscription', async () => {
|
||||
delete sub.discount;
|
||||
|
||||
let link = await paypalPayments.subscribe({sub, coupon});
|
||||
|
||||
expect(link).to.eql(approvalHerf);
|
||||
expect(paypalBillingAgreementCreateStub).to.be.calledOnce;
|
||||
let billingPlanTitle = `Habitica Subscription ($${sub.price} every ${sub.months} months, recurring)`;
|
||||
expect(paypalBillingAgreementCreateStub).to.be.calledWith({
|
||||
name: billingPlanTitle,
|
||||
description: billingPlanTitle,
|
||||
start_date: moment().add({ minutes: 5 }).format(),
|
||||
plan: {
|
||||
id: sub.paypalKey,
|
||||
},
|
||||
payer: {
|
||||
payment_method: 'Paypal',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscribeSuccess', () => {
|
||||
let user, group, block, groupId, token, headers, customerId;
|
||||
let paypalBillingAgreementExecuteStub, paymentsCreateSubscritionStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
|
||||
token = 'test-token';
|
||||
headers = {};
|
||||
block = common.content.subscriptionBlocks[subKey];
|
||||
customerId = 'test-customerId';
|
||||
|
||||
paypalBillingAgreementExecuteStub = sinon.stub(paypalPayments, 'paypalBillingAgreementExecute')
|
||||
.returnsPromise({}).resolves({
|
||||
id: customerId,
|
||||
});
|
||||
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
paypalPayments.paypalBillingAgreementExecute.restore();
|
||||
payments.createSubscription.restore();
|
||||
});
|
||||
|
||||
it('creates a user subscription', async () => {
|
||||
await paypalPayments.subscribeSuccess({user, block, groupId, token, headers});
|
||||
|
||||
expect(paypalBillingAgreementExecuteStub).to.be.calledOnce;
|
||||
expect(paypalBillingAgreementExecuteStub).to.be.calledWith(token, {});
|
||||
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
groupId,
|
||||
customerId,
|
||||
paymentMethod: 'Paypal',
|
||||
sub: block,
|
||||
headers,
|
||||
});
|
||||
});
|
||||
|
||||
it('create a group subscription', async () => {
|
||||
groupId = group._id;
|
||||
|
||||
await paypalPayments.subscribeSuccess({user, block, groupId, token, headers});
|
||||
|
||||
expect(paypalBillingAgreementExecuteStub).to.be.calledOnce;
|
||||
expect(paypalBillingAgreementExecuteStub).to.be.calledWith(token, {});
|
||||
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
groupId,
|
||||
customerId,
|
||||
paymentMethod: 'Paypal',
|
||||
sub: block,
|
||||
headers,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscribeCancel', () => {
|
||||
let user, group, groupId, customerId, groupCustomerId, nextBillingDate;
|
||||
let paymentCancelSubscriptionSpy, paypalBillingAgreementCancelStub, paypalBillingAgreementGetStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
customerId = 'customer-id';
|
||||
groupCustomerId = 'groupCustomerId-test';
|
||||
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.customerId = customerId;
|
||||
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 = groupCustomerId;
|
||||
group.purchased.plan.planId = subKey;
|
||||
group.purchased.plan.lastBillingDate = new Date();
|
||||
await group.save();
|
||||
|
||||
nextBillingDate = new Date();
|
||||
|
||||
paypalBillingAgreementCancelStub = sinon.stub(paypalPayments, 'paypalBillingAgreementCancel').returnsPromise().resolves({});
|
||||
paypalBillingAgreementGetStub = sinon.stub(paypalPayments, 'paypalBillingAgreementGet')
|
||||
.returnsPromise().resolves({
|
||||
agreement_details: {
|
||||
next_billing_date: nextBillingDate,
|
||||
cycles_completed: 1,
|
||||
},
|
||||
});
|
||||
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
paypalPayments.paypalBillingAgreementGet.restore();
|
||||
paypalPayments.paypalBillingAgreementCancel.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if we are missing a subscription', async () => {
|
||||
user.purchased.plan.customerId = undefined;
|
||||
|
||||
await expect(paypalPayments.subscribeCancel({user}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('missingSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if group is not found', async () => {
|
||||
await expect(paypalPayments.subscribeCancel({user, groupId: 'fake-id'}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
message: i18n.t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if user is not group leader', async () => {
|
||||
let nonLeader = new User();
|
||||
nonLeader.guilds.push(group._id);
|
||||
await nonLeader.save();
|
||||
|
||||
await expect(paypalPayments.subscribeCancel({user: nonLeader, groupId: group._id}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel a user subscription', async () => {
|
||||
await paypalPayments.subscribeCancel({user});
|
||||
|
||||
expect(paypalBillingAgreementGetStub).to.be.calledOnce;
|
||||
expect(paypalBillingAgreementGetStub).to.be.calledWith(customerId);
|
||||
expect(paypalBillingAgreementCancelStub).to.be.calledOnce;
|
||||
expect(paypalBillingAgreementCancelStub).to.be.calledWith(customerId, { note: i18n.t('cancelingSubscription') });
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
|
||||
user,
|
||||
groupId,
|
||||
paymentMethod: 'Paypal',
|
||||
nextBill: nextBillingDate,
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel a group subscription', async () => {
|
||||
await paypalPayments.subscribeCancel({user, groupId: group._id});
|
||||
|
||||
expect(paypalBillingAgreementGetStub).to.be.calledOnce;
|
||||
expect(paypalBillingAgreementGetStub).to.be.calledWith(groupCustomerId);
|
||||
expect(paypalBillingAgreementCancelStub).to.be.calledOnce;
|
||||
expect(paypalBillingAgreementCancelStub).to.be.calledWith(groupCustomerId, { note: i18n.t('cancelingSubscription') });
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
|
||||
user,
|
||||
groupId: group._id,
|
||||
paymentMethod: 'Paypal',
|
||||
nextBill: nextBillingDate,
|
||||
cancellationReason: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ipn', () => {
|
||||
let user, group, txn_type, userPaymentId, groupPaymentId;
|
||||
let ipnVerifyAsyncStub, paymentCancelSubscriptionSpy;
|
||||
|
||||
beforeEach(async () => {
|
||||
txn_type = 'recurring_payment_profile_cancel';
|
||||
userPaymentId = 'userPaymentId-test';
|
||||
groupPaymentId = 'groupPaymentId-test';
|
||||
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.customerId = userPaymentId;
|
||||
user.purchased.plan.planId = subKey;
|
||||
user.purchased.plan.lastBillingDate = new Date();
|
||||
await user.save();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = groupPaymentId;
|
||||
group.purchased.plan.planId = subKey;
|
||||
group.purchased.plan.lastBillingDate = new Date();
|
||||
await group.save();
|
||||
|
||||
ipnVerifyAsyncStub = sinon.stub(paypalPayments, 'ipnVerifyAsync').returnsPromise().resolves({});
|
||||
paymentCancelSubscriptionSpy = sinon.stub(payments, 'cancelSubscription').returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
paypalPayments.ipnVerifyAsync.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
|
||||
it('should cancel a user subscription', async () => {
|
||||
await paypalPayments.ipn({txn_type, recurring_payment_id: userPaymentId});
|
||||
|
||||
expect(ipnVerifyAsyncStub).to.be.calledOnce;
|
||||
expect(ipnVerifyAsyncStub).to.be.calledWith({txn_type, recurring_payment_id: userPaymentId});
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy.args[0][0].user._id).to.eql(user._id);
|
||||
expect(paymentCancelSubscriptionSpy.args[0][0].paymentMethod).to.eql('Paypal');
|
||||
});
|
||||
|
||||
it('should cancel a group subscription', async () => {
|
||||
await paypalPayments.ipn({txn_type, recurring_payment_id: groupPaymentId});
|
||||
|
||||
expect(ipnVerifyAsyncStub).to.be.calledOnce;
|
||||
expect(ipnVerifyAsyncStub).to.be.calledWith({txn_type, recurring_payment_id: groupPaymentId});
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({ groupId: group._id, paymentMethod: 'Paypal' });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -8,7 +8,10 @@ describe('preenHistory', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
// Replace system clocks so we can get predictable results
|
||||
clock = sinon.useFakeTimers(Number(moment('2013-10-20').zone(0).startOf('day').toDate()), 'Date');
|
||||
clock = sinon.useFakeTimers({
|
||||
now: Number(moment('2013-10-20').zone(0).startOf('day').toDate()),
|
||||
toFake: ['Date'],
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
return clock.restore();
|
||||
|
||||
@@ -22,7 +22,7 @@ describe('pushNotifications', () => {
|
||||
|
||||
sandbox.stub(nconf, 'get').returns('true-key');
|
||||
|
||||
sandbox.stub(gcmLib.Sender.prototype, 'send', fcmSendSpy);
|
||||
sandbox.stub(gcmLib.Sender.prototype, 'send').callsFake(fcmSendSpy);
|
||||
|
||||
sandbox.stub(pushNotify, 'apn').returns({
|
||||
on: () => null,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@ import {
|
||||
import i18n from '../../../../../website/common/script/i18n';
|
||||
import { ensureAdmin, ensureSudo } from '../../../../../website/server/middlewares/ensureAccessRight';
|
||||
import { NotAuthorized } from '../../../../../website/server/libs/errors';
|
||||
import apiMessages from '../../../../../website/server/libs/apiMessages';
|
||||
|
||||
describe('ensure access middlewares', () => {
|
||||
let res, req, next;
|
||||
@@ -23,7 +24,9 @@ describe('ensure access middlewares', () => {
|
||||
|
||||
ensureAdmin(req, res, next);
|
||||
|
||||
expect(next).to.be.calledWith(new NotAuthorized(i18n.t('noAdminAccess')));
|
||||
const calledWith = next.getCall(0).args;
|
||||
expect(calledWith[0].message).to.equal(i18n.t('noAdminAccess'));
|
||||
expect(calledWith[0] instanceof NotAuthorized).to.equal(true);
|
||||
});
|
||||
|
||||
it('passes when user is an admin', () => {
|
||||
@@ -42,7 +45,9 @@ describe('ensure access middlewares', () => {
|
||||
|
||||
ensureSudo(req, res, next);
|
||||
|
||||
expect(next).to.be.calledWith(new NotAuthorized(i18n.t('noSudoAccess')));
|
||||
const calledWith = next.getCall(0).args;
|
||||
expect(calledWith[0].message).to.equal(apiMessages('noSudoAccess'));
|
||||
expect(calledWith[0] instanceof NotAuthorized).to.equal(true);
|
||||
});
|
||||
|
||||
it('passes when user is a sudo user', () => {
|
||||
|
||||
@@ -22,7 +22,8 @@ describe('developmentMode middleware', () => {
|
||||
|
||||
ensureDevelpmentMode(req, res, next);
|
||||
|
||||
expect(next).to.be.calledWith(new NotFound());
|
||||
const calledWith = next.getCall(0).args;
|
||||
expect(calledWith[0] instanceof NotFound).to.equal(true);
|
||||
});
|
||||
|
||||
it('passes when not in production', () => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user