mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-10-27 11:12:28 +01:00
Compare commits
432 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1540ec89ee | ||
|
|
f304d4fe52 | ||
|
|
023e433a5c | ||
|
|
ef4aeb29ab | ||
|
|
2b80931202 | ||
|
|
2950713712 | ||
|
|
118f3bd1bb | ||
|
|
69f1343ea8 | ||
|
|
918ee02d64 | ||
|
|
0cac34dd26 | ||
|
|
1c859fc91f | ||
|
|
857aa5827b | ||
|
|
28e8ec2d2c | ||
|
|
856f13d213 | ||
|
|
121fd38bd1 | ||
|
|
36d72f5f7a | ||
|
|
f1b8bd80e7 | ||
|
|
84d2ce6a3f | ||
|
|
76010e6c8f | ||
|
|
c707b6c99b | ||
|
|
e4bd466cc7 | ||
|
|
001b8eb894 | ||
|
|
9abcfe8614 | ||
|
|
bc6102551d | ||
|
|
959a3ff85b | ||
|
|
518b874f64 | ||
|
|
6cc359a935 | ||
|
|
514d35c0be | ||
|
|
13da92ea68 | ||
|
|
03c4d82b7d | ||
|
|
d905ab7f86 | ||
|
|
c6560b6b1b | ||
|
|
c61f660255 | ||
|
|
2f1b683ec9 | ||
|
|
47bb217068 | ||
|
|
f49fd05da1 | ||
|
|
b0341aa06f | ||
|
|
b07ec18e33 | ||
|
|
12930a2bac | ||
|
|
91f5c47d9d | ||
|
|
fe7850d10f | ||
|
|
c5c2da75bf | ||
|
|
969607cd3b | ||
|
|
2a1f52a359 | ||
|
|
47d9594679 | ||
|
|
97e40c81f3 | ||
|
|
c8b61a2f7d | ||
|
|
e9543f0d28 | ||
|
|
77b88490e4 | ||
|
|
7fc2500bfd | ||
|
|
fb229acb58 | ||
|
|
6ce83d1fa4 | ||
|
|
2be4815aea | ||
|
|
1dbc42f48a | ||
|
|
89279c8aed | ||
|
|
faedb13598 | ||
|
|
c0c74659c5 | ||
|
|
bf5ad2db1f | ||
|
|
7d99873960 | ||
|
|
e02ef00397 | ||
|
|
23c5c4211c | ||
|
|
69cc134fff | ||
|
|
ffd9400cb7 | ||
|
|
5be91ef842 | ||
|
|
3123183e46 | ||
|
|
49cca7a601 | ||
|
|
7fbd38d18c | ||
|
|
1f95376d39 | ||
|
|
2da0a1e88c | ||
|
|
afacd3e1cf | ||
|
|
a69b9e6705 | ||
|
|
e4e5d10316 | ||
|
|
27c38bdf45 | ||
|
|
ea24eeb019 | ||
|
|
55a8eef3e1 | ||
|
|
92cbb4a07d | ||
|
|
3f96d05365 | ||
|
|
0b72f6a613 | ||
|
|
5e1e6be518 | ||
|
|
472ec99291 | ||
|
|
0284e9a4e3 | ||
|
|
1a0721c078 | ||
|
|
6b6b548ac5 | ||
|
|
30f3d786bb | ||
|
|
07bbba6789 | ||
|
|
6afb2bd0d4 | ||
|
|
f1a3bd5001 | ||
|
|
3f6a13d209 | ||
|
|
3658e41fec | ||
|
|
c69d5c7ae6 | ||
|
|
747f9e6a99 | ||
|
|
7755ab090b | ||
|
|
9ed17df1e3 | ||
|
|
faeb040a83 | ||
|
|
0a1ae1375e | ||
|
|
9756030fa2 | ||
|
|
c66172b74b | ||
|
|
281f6d1806 | ||
|
|
237095d109 | ||
|
|
fa788f49fc | ||
|
|
0817cf96e1 | ||
|
|
97e1d75dce | ||
|
|
52bf20c27d | ||
|
|
5dbaf39aba | ||
|
|
66d402c985 | ||
|
|
8048146223 | ||
|
|
e2c07e458d | ||
|
|
90a9e8e192 | ||
|
|
f8039f48a6 | ||
|
|
04337f8e83 | ||
|
|
45297f8bf9 | ||
|
|
6f112c29f2 | ||
|
|
4d1edb363c | ||
|
|
4e303cc592 | ||
|
|
798a975185 | ||
|
|
eb2b46fc5d | ||
|
|
29854d3bdb | ||
|
|
f8751b002c | ||
|
|
cd545e08d5 | ||
|
|
f69bb4f023 | ||
|
|
847081d2b2 | ||
|
|
8112d46ea4 | ||
|
|
d13bded647 | ||
|
|
1de4ab3612 | ||
|
|
f9f22f313f | ||
|
|
f57eed85a8 | ||
|
|
10dd3318ab | ||
|
|
cbef83c14a | ||
|
|
59709a8590 | ||
|
|
f85f2a0c6d | ||
|
|
605a5a1d5c | ||
|
|
2d5d786c8e | ||
|
|
5efe5b7b10 | ||
|
|
3e92bb22fa | ||
|
|
1249b9d410 | ||
|
|
197aafe092 | ||
|
|
79829ca128 | ||
|
|
adaa1d9a3e | ||
|
|
3e6691dbbb | ||
|
|
046761b9aa | ||
|
|
0b0466b960 | ||
|
|
f8d4a2bd6b | ||
|
|
1af59a3770 | ||
|
|
bbcb13c91b | ||
|
|
d27dc46c50 | ||
|
|
679459b83b | ||
|
|
5a619773d5 | ||
|
|
ad76ab1315 | ||
|
|
15eb8db925 | ||
|
|
a0e92c5605 | ||
|
|
eac3e36c07 | ||
|
|
0b8def555b | ||
|
|
5f5fa5c2eb | ||
|
|
1eac8bbbbe | ||
|
|
49c7580cd4 | ||
|
|
dca958f2e2 | ||
|
|
eae5f0d605 | ||
|
|
6ab091645c | ||
|
|
d66041c280 | ||
|
|
de070a450a | ||
|
|
eaaab35f31 | ||
|
|
6a63f080ad | ||
|
|
c42f81b629 | ||
|
|
9a78a7b896 | ||
|
|
8b70721137 | ||
|
|
44ffbd716d | ||
|
|
5bfc3a5ff4 | ||
|
|
0ba5df4164 | ||
|
|
52a59c8192 | ||
|
|
c1a860494d | ||
|
|
395dafa127 | ||
|
|
bab41647f5 | ||
|
|
8582a67308 | ||
|
|
0d58fb0fd3 | ||
|
|
1d2482f8bc | ||
|
|
f4cf906127 | ||
|
|
559f9b1825 | ||
|
|
c7039bc9ea | ||
|
|
f929d36e1a | ||
|
|
254d1a3465 | ||
|
|
442aae8a35 | ||
|
|
bcb0ed0a5c | ||
|
|
a48b8f0e34 | ||
|
|
7eeeda2aae | ||
|
|
a5ad9c30f0 | ||
|
|
ac732b2c85 | ||
|
|
a56b2d68fb | ||
|
|
25b0ff38c4 | ||
|
|
dcc06931cc | ||
|
|
bc3ebbd095 | ||
|
|
e5b9581743 | ||
|
|
4b9fe49e3a | ||
|
|
ab4c8b0a46 | ||
|
|
f6c26fe869 | ||
|
|
80e9735b28 | ||
|
|
aa6f188bd9 | ||
|
|
e8b7660376 | ||
|
|
7d76622410 | ||
|
|
928e5f66c4 | ||
|
|
6a343535c0 | ||
|
|
f58f6acb44 | ||
|
|
64754777ed | ||
|
|
3b5e4b6d84 | ||
|
|
9383578cb8 | ||
|
|
474672ec64 | ||
|
|
25c6691793 | ||
|
|
3ea7b72024 | ||
|
|
2d6f05a9a4 | ||
|
|
28637286d6 | ||
|
|
874887b790 | ||
|
|
c977e5ebb5 | ||
|
|
f040e668f3 | ||
|
|
55a15f938c | ||
|
|
8c4f35daf4 | ||
|
|
8f38ce3424 | ||
|
|
b8f57a74d0 | ||
|
|
7ed26c0dbe | ||
|
|
e8f5b26d4d | ||
|
|
0273648b6b | ||
|
|
b6fdac8885 | ||
|
|
00e6389672 | ||
|
|
e02c669b61 | ||
|
|
f0cb7c6bf3 | ||
|
|
571ef0b309 | ||
|
|
74328d1bcc | ||
|
|
d34a9d828c | ||
|
|
2fd35b3a40 | ||
|
|
e27512f626 | ||
|
|
dbf9cb3b4e | ||
|
|
34c1245519 | ||
|
|
f602bfe438 | ||
|
|
9aa4b8aa64 | ||
|
|
5a150ebc5b | ||
|
|
cbe1892b50 | ||
|
|
13df60e0dd | ||
|
|
3ff7692528 | ||
|
|
111bba84dc | ||
|
|
b0d2b72b88 | ||
|
|
696317ea8a | ||
|
|
593178a46a | ||
|
|
f8fe16482d | ||
|
|
5108480ec5 | ||
|
|
95968b1b1c | ||
|
|
566569af98 | ||
|
|
6693e9fca9 | ||
|
|
431bde56d2 | ||
|
|
7cf17c0e63 | ||
|
|
49561bfc8c | ||
|
|
8cbbb58e78 | ||
|
|
905549e379 | ||
|
|
5d45c7209a | ||
|
|
371cddfe17 | ||
|
|
fcfac30caa | ||
|
|
b094fb1e52 | ||
|
|
a2dd82b6db | ||
|
|
e6071610e4 | ||
|
|
bdd0e2bb79 | ||
|
|
054a9a6f2b | ||
|
|
35b9ed6273 | ||
|
|
e65277baa5 | ||
|
|
421bd8624c | ||
|
|
4562c6422a | ||
|
|
a5cd9f2473 | ||
|
|
18bbdfa84b | ||
|
|
d8c37f6e2d | ||
|
|
7f38c61c70 | ||
|
|
1c018cedb1 | ||
|
|
80892bd6a8 | ||
|
|
6801dae75d | ||
|
|
59e1de6771 | ||
|
|
5b240a1950 | ||
|
|
3ec3722038 | ||
|
|
d798ebadfe | ||
|
|
6cbddef627 | ||
|
|
016de411c9 | ||
|
|
2173f53883 | ||
|
|
f2e5bc52e5 | ||
|
|
393a9290e9 | ||
|
|
ad5045bc09 | ||
|
|
9b515ebdd1 | ||
|
|
97bf9ee8e8 | ||
|
|
f5ba636579 | ||
|
|
4dd7e49552 | ||
|
|
d2f673ef1e | ||
|
|
e198dd551a | ||
|
|
0bfc9d9516 | ||
|
|
d4e20ee4aa | ||
|
|
a751a367fc | ||
|
|
d323be19c6 | ||
|
|
be3f61a94b | ||
|
|
f1bb2db73b | ||
|
|
a622344d44 | ||
|
|
e279a3550b | ||
|
|
70aab3059c | ||
|
|
c264e37182 | ||
|
|
b31bc15493 | ||
|
|
ba19c00617 | ||
|
|
93aa92de7c | ||
|
|
d021680945 | ||
|
|
f9595af8a5 | ||
|
|
d2756278c3 | ||
|
|
2e2dc179c4 | ||
|
|
acf7b811ab | ||
|
|
d5170251c0 | ||
|
|
c9ba9054e3 | ||
|
|
d4aac1ee4b | ||
|
|
9615a332a5 | ||
|
|
417455e5ef | ||
|
|
136502a110 | ||
|
|
425887c1e4 | ||
|
|
cfa8a5190f | ||
|
|
df5be81706 | ||
|
|
08b3491047 | ||
|
|
e73c3147c1 | ||
|
|
a43254000e | ||
|
|
4e3c984baf | ||
|
|
c112e923f1 | ||
|
|
540353f024 | ||
|
|
2b9b5e369e | ||
|
|
cb38475765 | ||
|
|
8bb92577b0 | ||
|
|
fb26cbd26d | ||
|
|
a0de5cd8f8 | ||
|
|
9fe10b1818 | ||
|
|
d8dd39422a | ||
|
|
3f9b710773 | ||
|
|
8a8bab4be1 | ||
|
|
2a0747ed72 | ||
|
|
a5196e94f6 | ||
|
|
009ab26711 | ||
|
|
3fabf3391f | ||
|
|
8020990264 | ||
|
|
a2cfeafc02 | ||
|
|
d04a4fb1ed | ||
|
|
aeb86db306 | ||
|
|
49960c0e32 | ||
|
|
932cb5cf6a | ||
|
|
74d6e77504 | ||
|
|
8400f1786b | ||
|
|
d7bd5dd9f8 | ||
|
|
3288b0de33 | ||
|
|
c025ffbd10 | ||
|
|
afb5b473a3 | ||
|
|
aeee29f5fa | ||
|
|
0cca2a07a2 | ||
|
|
55d94c129a | ||
|
|
358e1aed22 | ||
|
|
36241f061f | ||
|
|
b6201a3b75 | ||
|
|
005f74d918 | ||
|
|
926e188017 | ||
|
|
94da808279 | ||
|
|
7568dd52e9 | ||
|
|
c6e2b78982 | ||
|
|
b6104c3ef3 | ||
|
|
56b5c960f0 | ||
|
|
528abf77af | ||
|
|
8db6b7c6cb | ||
|
|
578dee59bd | ||
|
|
d40c923e6e | ||
|
|
3c4c64b023 | ||
|
|
c84d6ba141 | ||
|
|
5f3b147d2a | ||
|
|
ff08e8b586 | ||
|
|
cb2acbfefd | ||
|
|
b16da35585 | ||
|
|
826d7b85d7 | ||
|
|
6bcc6a15e2 | ||
|
|
b600eceb49 | ||
|
|
b83ef872c9 | ||
|
|
4ebc2e2175 | ||
|
|
2f4b8c569a | ||
|
|
85b5b5a62d | ||
|
|
e271e57f63 | ||
|
|
558fb145b5 | ||
|
|
fc30456b53 | ||
|
|
68b2d19b04 | ||
|
|
6d33acccf4 | ||
|
|
acee4bad80 | ||
|
|
30fe5088b8 | ||
|
|
69602f93e9 | ||
|
|
0109aa4250 | ||
|
|
2dc0958678 | ||
|
|
52f4e5f37d | ||
|
|
c014da297c | ||
|
|
285041cdee | ||
|
|
6a82206f81 | ||
|
|
8b6052a3ca | ||
|
|
04fd907a45 | ||
|
|
70343079f1 | ||
|
|
df952eece5 | ||
|
|
e3a619c7ff | ||
|
|
23f531372b | ||
|
|
97b15006fd | ||
|
|
35b92f13a3 | ||
|
|
556a7e5229 | ||
|
|
378625b4af | ||
|
|
ee15e29ba4 | ||
|
|
ed880a665a | ||
|
|
3c7f71d214 | ||
|
|
edac06b0d1 | ||
|
|
24562f8d60 | ||
|
|
97840ed732 | ||
|
|
76499412ed | ||
|
|
9b10f348cc | ||
|
|
17b0329c43 | ||
|
|
cda84a6d68 | ||
|
|
306505ebab | ||
|
|
2476cdd873 | ||
|
|
8465dd69be | ||
|
|
461e7445c2 | ||
|
|
24df8d8f2f | ||
|
|
2bca92b4d5 | ||
|
|
c3843cae80 | ||
|
|
816e4a2f19 | ||
|
|
d0d4927e59 | ||
|
|
023ff5789d | ||
|
|
cc9be6f4a1 | ||
|
|
145bcb6f7c | ||
|
|
d7db599f88 | ||
|
|
ca935670f7 | ||
|
|
c2eb113672 | ||
|
|
257e932bc3 | ||
|
|
50e2731811 | ||
|
|
d67b9e5688 | ||
|
|
bfc7b9d3e8 | ||
|
|
eb0e234afa | ||
|
|
177f78cbb0 | ||
|
|
e3b484b29a | ||
|
|
941000d737 | ||
|
|
63ce7c6034 | ||
|
|
921f9a65a3 |
3
.dockerignore
Normal file
3
.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
.git
|
||||
website
|
||||
@@ -14,7 +14,7 @@ files:
|
||||
owner: root
|
||||
group: users
|
||||
content: |
|
||||
$(ls -td /opt/elasticbeanstalk/node-install/node-* | head -1)/bin/npm install -g npm@3
|
||||
$(ls -td /opt/elasticbeanstalk/node-install/node-* | head -1)/bin/npm install -g npm@4
|
||||
container_commands:
|
||||
01_makeBabel:
|
||||
command: "touch /tmp/.babel.json"
|
||||
|
||||
@@ -10,7 +10,6 @@ dist-client/
|
||||
# Not linted
|
||||
migrations/*
|
||||
website/client-old/
|
||||
debug-scripts/*
|
||||
scripts/*
|
||||
test/server_side/**/*
|
||||
test/client-old/spec/**/*
|
||||
@@ -23,4 +22,6 @@ Gruntfile.js
|
||||
gulpfile.js
|
||||
gulp
|
||||
webpack
|
||||
test/client
|
||||
test/client/e2e
|
||||
test/client/unit/index.js
|
||||
test/client/unit/karma.conf.js
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"node": true,
|
||||
},
|
||||
"extends": [
|
||||
"habitrpg/es6",
|
||||
"habitrpg"
|
||||
"habitrpg",
|
||||
"habitrpg/esnext"
|
||||
],
|
||||
}
|
||||
|
||||
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -9,5 +9,6 @@ Fixes put_issue_url_here
|
||||
|
||||
|
||||
[//]: # (Put User ID in here - found in Settings -> API)
|
||||
|
||||
----
|
||||
UUID:
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,7 +14,6 @@ npm-debug.log*
|
||||
lib
|
||||
website/client-old/bower_components
|
||||
website/client-old/new-stuff.html
|
||||
website/build
|
||||
newrelic_agent.log
|
||||
.bower-tmp
|
||||
.bower-registry
|
||||
|
||||
27
.travis.yml
27
.travis.yml
@@ -1,17 +1,22 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- '4.3.1'
|
||||
- '6'
|
||||
before_install:
|
||||
- "npm install -g npm@3"
|
||||
- "npm install -g gulp"
|
||||
- "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"
|
||||
- npm install -g npm@4
|
||||
- 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
|
||||
before_script:
|
||||
- 'npm install -g grunt-cli mocha'
|
||||
- npm run test:build
|
||||
- cp config.json.example config.json
|
||||
- "until nc -z localhost 27017; do echo Waiting for MongoDB; sleep 1; done"
|
||||
- "export DISPLAY=:99"
|
||||
- if [ $REQUIRES_SERVER ]; then until nc -z localhost 27017; do echo Waiting for MongoDB; sleep 1; done; export DISPLAY=:99; fi
|
||||
after_script:
|
||||
- "./node_modules/.bin/lcov-result-merger 'coverage/**/*.info' | ./node_modules/coveralls/bin/coveralls.js"
|
||||
- ./node_modules/.bin/lcov-result-merger 'coverage/**/*.info' | ./node_modules/coveralls/bin/coveralls.js
|
||||
script: npm run $TEST
|
||||
env:
|
||||
matrix:
|
||||
- TEST="lint"
|
||||
- TEST="test:api-v3" REQUIRES_SERVER=true
|
||||
- TEST="test:sanity"
|
||||
- TEST="test:content"
|
||||
- TEST="test:common"
|
||||
- TEST="test:karma"
|
||||
- TEST="client:unit"
|
||||
|
||||
10
Dockerfile
10
Dockerfile
@@ -17,20 +17,22 @@ RUN apt-get install -y \
|
||||
python
|
||||
|
||||
# Install NodeJS
|
||||
RUN curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash -
|
||||
RUN curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
|
||||
RUN apt-get install -y nodejs
|
||||
|
||||
# Install npm@latest
|
||||
RUN curl -sL https://www.npmjs.org/install.sh | sh
|
||||
|
||||
# Clean up package management
|
||||
RUN apt-get clean
|
||||
RUN rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install global packages
|
||||
RUN npm install -g npm@3
|
||||
RUN npm install -g gulp grunt-cli bower
|
||||
RUN npm install -g gulp grunt-cli bower mocha
|
||||
|
||||
# Clone Habitica repo and install dependencies
|
||||
WORKDIR /habitrpg
|
||||
RUN git clone https://github.com/HabitRPG/habitrpg.git /habitrpg
|
||||
RUN git clone https://github.com/HabitRPG/habitica.git /habitrpg
|
||||
RUN npm install
|
||||
RUN bower install --allow-root
|
||||
|
||||
|
||||
13
Gruntfile.js
13
Gruntfile.js
@@ -57,7 +57,7 @@ module.exports = function(grunt) {
|
||||
files: [
|
||||
{expand: true, cwd: 'website/client-old/', src: 'favicon.ico', dest: 'website/build/'},
|
||||
{expand: true, cwd: 'website/client-old/', src: 'favicon_192x192.png', dest: 'website/build/'},
|
||||
{expand: true, cwd: 'website/assets/sprites/dist/', src: 'spritesmith*.png', dest: 'website/build/'},
|
||||
{expand: true, cwd: 'website/assets/sprites/dist/', src: 'spritesmith*.png', dest: 'website/build/static/sprites'},
|
||||
{expand: true, cwd: 'website/assets/sprites/', src: 'backer-only/*.gif', dest: 'website/build/'},
|
||||
{expand: true, cwd: 'website/assets/sprites/', src: 'npc_ian.gif', dest: 'website/build/'},
|
||||
{expand: true, cwd: 'website/assets/sprites/', src: 'quest_*.gif', dest: 'website/build/'},
|
||||
@@ -78,6 +78,7 @@ module.exports = function(grunt) {
|
||||
'website/build/favicon.ico',
|
||||
'website/build/favicon_192x192.png',
|
||||
'website/build/*.png',
|
||||
'website/build/static/sprites/*.png',
|
||||
'website/build/*.gif',
|
||||
'website/build/bower_components/bootstrap/dist/fonts/*'
|
||||
],
|
||||
@@ -126,15 +127,7 @@ module.exports = function(grunt) {
|
||||
// Register tasks.
|
||||
grunt.registerTask('build:prod', ['loadManifestFiles', 'clean:build', 'uglify', 'stylus', 'cssmin', 'copy:build', 'hashres']);
|
||||
grunt.registerTask('build:dev', ['cssmin', 'stylus']);
|
||||
grunt.registerTask('build:test', ['test:prepare:translations', 'build:dev']);
|
||||
|
||||
grunt.registerTask('test:prepare:translations', function() {
|
||||
var i18n = require('./website/server/libs/i18n'),
|
||||
fs = require('fs');
|
||||
fs.writeFileSync('test/client-old/spec/mocks/translations.js',
|
||||
"if(!window.env) window.env = {};\n" +
|
||||
"window.env.translations = " + JSON.stringify(i18n.translations['en']) + ';');
|
||||
});
|
||||
grunt.registerTask('build:test', ['build:dev']);
|
||||
|
||||
// Load tasks
|
||||
grunt.loadNpmTasks('grunt-contrib-uglify');
|
||||
|
||||
20
README.md
20
README.md
@@ -1,4 +1,4 @@
|
||||
Habitica [](https://travis-ci.org/HabitRPG/habitrpg) [](https://codeclimate.com/github/HabitRPG/habitrpg) [](https://coveralls.io/r/HabitRPG/habitrpg?branch=develop) [](https://www.bountysource.com/trackers/68393-habitrpg?utm_source=68393&utm_medium=shield&utm_campaign=TRACKER_BADGE)
|
||||
Habitica [](https://travis-ci.org/HabitRPG/habitica) [](https://codeclimate.com/github/HabitRPG/habitrpg) [](https://coveralls.io/r/HabitRPG/habitrpg?branch=develop) [](https://www.bountysource.com/trackers/68393-habitrpg?utm_source=68393&utm_medium=shield&utm_campaign=TRACKER_BADGE)
|
||||
===============
|
||||
|
||||
[Habitica](https://habitica.com) is an open source habit building program which treats your life like a Role Playing Game. Level up as you succeed, lose HP as you fail, earn money to buy weapons and armor.
|
||||
@@ -10,21 +10,3 @@ For an introduction to the technologies used and how the software is organized,
|
||||
To set up a local install of Habitica for development and testing, see [Setting up Habitica Locally](http://habitica.wikia.com/wiki/Setting_up_Habitica_Locally), which contains instructions for Windows, *nix / Mac OS, and Vagrant.
|
||||
|
||||
Then read [Guidance for Blacksmiths](http://habitica.wikia.com/wiki/Guidance_for_Blacksmiths) for additional instructions and useful tips.
|
||||
|
||||
## Debug Scripts
|
||||
|
||||
In the `./debug-scripts/` folder, there are a few files. Here's a sample:
|
||||
|
||||
```bash
|
||||
grant-all-equipment.js
|
||||
grant-all-mounts.js
|
||||
grant-all-pets.js
|
||||
```
|
||||
|
||||
You can run them by doing:
|
||||
|
||||
```bash
|
||||
node debug-scripts/name-of-script.js
|
||||
```
|
||||
|
||||
If there are more arguments required to make the script work, it will print out the usage and an explanation of what the script does.
|
||||
|
||||
@@ -36,14 +36,15 @@
|
||||
"jquery-ui": "1.10.3",
|
||||
"jquery.cookie": "1.4.0",
|
||||
"js-emoji": "snicker/js-emoji#f25d8a303f",
|
||||
"ngInfiniteScroll": "1.0.0",
|
||||
"ngInfiniteScroll": "1.1.0",
|
||||
"pnotify": "1.3.1",
|
||||
"sticky": "1.0.3",
|
||||
"swagger-ui": "wordnik/swagger-ui#v2.0.24",
|
||||
"smart-app-banner": "78ef9c0679723b25be1a0ae04f7b4aef7cbced4f",
|
||||
"habitica-markdown": "1.2.2",
|
||||
"pusher-js-auth": "^2.0.0",
|
||||
"pusher-websocket-iso": "pusher#^3.2.0"
|
||||
"pusher-websocket-iso": "pusher#^3.2.0",
|
||||
"taggle": "^1.11.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"angular-mocks": "1.3.9"
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"FACEBOOK_ANALYTICS":"1234567890123456",
|
||||
"FACEBOOK_KEY":"123456789012345",
|
||||
"FACEBOOK_SECRET":"aaaabbbbccccddddeeeeffff00001111",
|
||||
"GOOGLE_CLIENT_ID":"123456789012345",
|
||||
"GOOGLE_CLIENT_SECRET":"aaaabbbbccccddddeeeeffff00001111",
|
||||
"NODE_DB_URI":"mongodb://localhost/habitrpg",
|
||||
"TEST_DB_URI":"mongodb://localhost/habitrpg_test",
|
||||
"NODE_ENV":"development",
|
||||
@@ -77,6 +79,7 @@
|
||||
},
|
||||
"SLACK": {
|
||||
"FLAGGING_URL": "https://hooks.slack.com/services/id/id/id",
|
||||
"FLAGGING_FOOTER_LINK": "https://habitrpg.github.io/flag-o-rama/"
|
||||
"FLAGGING_FOOTER_LINK": "https://habitrpg.github.io/flag-o-rama/",
|
||||
"SUBSCRIPTIONS_URL": "https://hooks.slack.com/services/id/id/id"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { MongoClient as mongo } from 'mongodb';
|
||||
import config from '../config';
|
||||
|
||||
module.exports.updateUser = (_id, path, value) => {
|
||||
mongo.connect(config.NODE_DB_URI, (err, db) => {
|
||||
if (err) throw err;
|
||||
|
||||
let collection = db.collection('users');
|
||||
collection.updateOne(
|
||||
{ _id },
|
||||
{ $set: { [`${path}`]: value } },
|
||||
(updateErr, result) => {
|
||||
if (updateErr) throw updateErr;
|
||||
console.log('done updating', _id);
|
||||
db.close();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
require('babel-register');
|
||||
|
||||
let _ = require('lodash');
|
||||
let updateUser = require('./_helper').updateUser;
|
||||
|
||||
let userId = process.argv[2];
|
||||
|
||||
if (!userId) {
|
||||
console.error('USAGE: node debug-scripts/grant-all-equipment.js <user_id>');
|
||||
console.error('EFFECT: Adds all gear to specified user');
|
||||
return;
|
||||
}
|
||||
|
||||
let gearFlat = require('../common').content.gear.flat;
|
||||
|
||||
let userGear = {};
|
||||
|
||||
_.each(gearFlat, (piece, key) => {
|
||||
userGear[key] = true;
|
||||
});
|
||||
|
||||
updateUser(userId, 'items.gear.owned', userGear);
|
||||
@@ -1,28 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
require('babel-register');
|
||||
|
||||
let _ = require('lodash');
|
||||
let updateUser = require('./_helper').updateUser;
|
||||
let userId = process.argv[2];
|
||||
|
||||
if (!userId) {
|
||||
console.error('USAGE: node debug-scripts/grant-all-mounts.js <user_id>');
|
||||
console.error('EFFECT: Adds all mounts to specified user');
|
||||
return;
|
||||
}
|
||||
|
||||
let dropMounts = require('../common').content.mounts;
|
||||
let questMounts = require('../common').content.questMounts;
|
||||
let specialMounts = require('../common').content.specialMounts;
|
||||
let premiumMounts = require('../common').content.premiumPets; // premium mounts isn't exposed on the content object
|
||||
|
||||
let userMounts = {};
|
||||
|
||||
_.each([ dropMounts, questMounts, specialMounts, premiumMounts ], (set) => {
|
||||
_.each(set, (pet, key) => {
|
||||
userMounts[key] = true;
|
||||
});
|
||||
})
|
||||
|
||||
updateUser(userId, 'items.mounts', userMounts);
|
||||
@@ -1,28 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
require('babel-register');
|
||||
|
||||
let _ = require('lodash');
|
||||
let updateUser = require('./_helper').updateUser;
|
||||
let userId = process.argv[2];
|
||||
|
||||
if (!userId) {
|
||||
console.error('USAGE: node debug-scripts/grant-all-pets.js <user_id>');
|
||||
console.error('EFFECT: Adds all pets to specified user');
|
||||
return;
|
||||
}
|
||||
|
||||
let dropPets = require('../common').content.pets;
|
||||
let questPets = require('../common').content.questPets;
|
||||
let specialPets = require('../common').content.specialPets;
|
||||
let premiumPets = require('../common').content.premiumPets;
|
||||
|
||||
let userPets = {};
|
||||
|
||||
_.each([ dropPets, questPets, specialPets, premiumPets ], (set) => {
|
||||
_.each(set, (pet, key) => {
|
||||
userPets[key] = 95;
|
||||
});
|
||||
})
|
||||
|
||||
updateUser(userId, 'items.pets', userPets);
|
||||
@@ -1,3 +1,3 @@
|
||||
web:
|
||||
volumes:
|
||||
- '.:/habitrpg'
|
||||
- '.:/habitrpg'
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
web:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
- "3000:3000"
|
||||
links:
|
||||
- mongo
|
||||
- mongo
|
||||
environment:
|
||||
- NODE_DB_URI=mongodb://mongo/habitrpg
|
||||
- NODE_DB_URI=mongodb://mongo/habitrpg
|
||||
|
||||
mongo:
|
||||
image: mongo
|
||||
ports:
|
||||
- "27017:27017"
|
||||
- "27017:27017"
|
||||
|
||||
@@ -20,3 +20,7 @@ gulp.task('apidoc', ['apidoc:clean'], (done) => {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
gulp.task('apidoc:watch', ['apidoc'], () => {
|
||||
return gulp.watch(APIDOC_SRC_PATH + '/**/*.js', ['apidoc']);
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import fs from 'fs';
|
||||
// Code taken from https://www.artembutusov.com/webpack-semantic-ui/
|
||||
|
||||
// Relative to node_modules/semantic-ui-less
|
||||
const SEMANTIC_THEME_PATH = '../../website/client/assets/semantic-ui/theme.config';
|
||||
const SEMANTIC_THEME_PATH = '../../website/client/assets/less/semantic-ui/theme.config';
|
||||
|
||||
// fix well known bug with default distribution
|
||||
function fixFontPath (filename) {
|
||||
|
||||
@@ -12,6 +12,9 @@ import {each} from 'lodash';
|
||||
const MAX_SPRITESHEET_SIZE = 1024 * 1024 * 3;
|
||||
const DIST_PATH = 'website/assets/sprites/dist/';
|
||||
|
||||
const IMG_DIST_PATH_NEW_CLIENT = 'website/static/sprites/';
|
||||
const CSS_DIST_PATH_NEW_CLIENT = 'website/client/assets/css/sprites/';
|
||||
|
||||
gulp.task('sprites:compile', ['sprites:clean', 'sprites:main', 'sprites:largeSprites', 'sprites:checkCompiledDimensions']);
|
||||
|
||||
gulp.task('sprites:main', () => {
|
||||
@@ -25,7 +28,7 @@ gulp.task('sprites:largeSprites', () => {
|
||||
});
|
||||
|
||||
gulp.task('sprites:clean', (done) => {
|
||||
clean(`${DIST_PATH}spritesmith*`, done);
|
||||
clean(`{${DIST_PATH}spritesmith*,${IMG_DIST_PATH_NEW_CLIENT}spritesmith*,${CSS_DIST_PATH_NEW_CLIENT}spritesmith*}`, done);
|
||||
});
|
||||
|
||||
gulp.task('sprites:checkCompiledDimensions', ['sprites:main', 'sprites:largeSprites'], () => {
|
||||
@@ -66,14 +69,16 @@ function createSpritesStream (name, src) {
|
||||
algorithm: 'binary-tree',
|
||||
padding: 1,
|
||||
cssTemplate: 'website/assets/sprites/css/css.template.handlebars',
|
||||
cssVarMap: cssVarMap
|
||||
cssVarMap: cssVarMap,
|
||||
}));
|
||||
|
||||
let imgStream = spriteData.img
|
||||
.pipe(imagemin())
|
||||
.pipe(gulp.dest(IMG_DIST_PATH_NEW_CLIENT))
|
||||
.pipe(gulp.dest(DIST_PATH));
|
||||
|
||||
let cssStream = spriteData.css
|
||||
.pipe(gulp.dest(CSS_DIST_PATH_NEW_CLIENT))
|
||||
.pipe(gulp.dest(DIST_PATH));
|
||||
|
||||
stream.add(imgStream);
|
||||
@@ -148,4 +153,9 @@ function cssVarMap (sprite) {
|
||||
}
|
||||
if (~sprite.name.indexOf('shirt'))
|
||||
sprite.custom.px.offset_y = `-${ sprite.y + 30 }px`; // even more for shirts
|
||||
if (~sprite.name.indexOf('hair_base')) {
|
||||
let styleArray = sprite.name.split('_').slice(2,3);
|
||||
if (Number(styleArray[0]) > 14)
|
||||
sprite.custom.px.offset_y = `-${ sprite.y }px`; // don't crop updos
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ import Bluebird from 'bluebird';
|
||||
import runSequence from 'run-sequence';
|
||||
import os from 'os';
|
||||
import nconf from 'nconf';
|
||||
import fs from 'fs';
|
||||
|
||||
const i18n = require('../website/server/libs/i18n');
|
||||
|
||||
// TODO rewrite
|
||||
|
||||
@@ -72,10 +75,17 @@ gulp.task('test:prepare:server', ['test:prepare:mongo'], () => {
|
||||
}
|
||||
});
|
||||
|
||||
gulp.task('test:prepare:build', ['build'], (cb) => {
|
||||
exec(testBin('grunt build:test'), cb);
|
||||
gulp.task('test:prepare:translations', (cb) => {
|
||||
fs.writeFile(
|
||||
'test/client-old/spec/mocks/translations.js',
|
||||
`if(!window.env) window.env = {};
|
||||
window.env.translations = ${JSON.stringify(i18n.translations['en'])};`, cb);
|
||||
|
||||
});
|
||||
|
||||
gulp.task('test:prepare:build', ['build', 'test:prepare:translations']);
|
||||
// exec(testBin('grunt build:test'), cb);
|
||||
|
||||
gulp.task('test:prepare:webdriver', (cb) => {
|
||||
exec('npm run test:prepare:webdriver', cb);
|
||||
});
|
||||
@@ -175,32 +185,6 @@ gulp.task('test:content:safe', ['test:prepare:build'], (cb) => {
|
||||
pipe(runner);
|
||||
});
|
||||
|
||||
gulp.task('test:server_side', ['test:prepare:build'], (cb) => {
|
||||
let runner = exec(
|
||||
testBin(SERVER_SIDE_TEST_COMMAND),
|
||||
(err, stdout, stderr) => {
|
||||
cb(err);
|
||||
}
|
||||
);
|
||||
pipe(runner);
|
||||
});
|
||||
|
||||
gulp.task('test:server_side:safe', ['test:prepare:build'], (cb) => {
|
||||
let runner = exec(
|
||||
testBin(SERVER_SIDE_TEST_COMMAND),
|
||||
(err, stdout, stderr) => {
|
||||
testResults.push({
|
||||
suite: 'Server Side Specs',
|
||||
pass: testCount(stdout, /(\d+) passing/),
|
||||
fail: testCount(stdout, /(\d+) failing/),
|
||||
pend: testCount(stdout, /(\d+) pending/),
|
||||
});
|
||||
cb();
|
||||
}
|
||||
);
|
||||
pipe(runner);
|
||||
});
|
||||
|
||||
gulp.task('test:karma', ['test:prepare:build'], (cb) => {
|
||||
let runner = exec(
|
||||
testBin(KARMA_TEST_COMMAND),
|
||||
@@ -296,7 +280,7 @@ gulp.task('test:e2e:safe', ['test:prepare', 'test:prepare:server'], (cb) => {
|
||||
|
||||
gulp.task('test:api-v3:unit', (done) => {
|
||||
let runner = exec(
|
||||
testBin('mocha test/api/v3/unit --recursive'),
|
||||
testBin('mocha test/api/v3/unit --recursive --require ./test/helpers/start-server'),
|
||||
(err, stdout, stderr) => {
|
||||
if (err) {
|
||||
process.exit(1);
|
||||
@@ -314,7 +298,7 @@ gulp.task('test:api-v3:unit:watch', () => {
|
||||
|
||||
gulp.task('test:api-v3:integration', (done) => {
|
||||
let runner = exec(
|
||||
testBin('mocha test/api/v3/integration --recursive'),
|
||||
testBin('mocha test/api/v3/integration --recursive --require ./test/helpers/start-server'),
|
||||
{maxBuffer: 500 * 1024},
|
||||
(err, stdout, stderr) => {
|
||||
if (err) {
|
||||
@@ -334,7 +318,7 @@ gulp.task('test:api-v3:integration:watch', () => {
|
||||
|
||||
gulp.task('test:api-v3:integration:separate-server', (done) => {
|
||||
let runner = exec(
|
||||
testBin('mocha test/api/v3/integration --recursive', 'LOAD_SERVER=0'),
|
||||
testBin('mocha test/api/v3/integration --recursive --require ./test/helpers/start-server', 'LOAD_SERVER=0'),
|
||||
{maxBuffer: 500 * 1024},
|
||||
(err, stdout, stderr) => done(err)
|
||||
);
|
||||
|
||||
@@ -84,8 +84,8 @@ gulp.task('transifex:malformedStrings', () => {
|
||||
let malformedString = `${lang} - ${file} - ${key} - ${translationString}`;
|
||||
stringsWithMalformedInterpolations.push(malformedString);
|
||||
} else if (englishOccurences.length !== translationOccurences.length && !malformedStringExceptions[key]) {
|
||||
let missingInterploationString = `${lang} - ${file} - ${key} - ${translationString}`;
|
||||
stringsWithIncorrectNumberOfInterpolations.push(missingInterploationString);
|
||||
let missingInterpolationString = `${lang} - ${file} - ${key} - ${translationString}`;
|
||||
stringsWithIncorrectNumberOfInterpolations.push(missingInterpolationString);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
116
migrations/20161002_add_missing_webhook_type.js
Normal file
116
migrations/20161002_add_missing_webhook_type.js
Normal file
@@ -0,0 +1,116 @@
|
||||
'use strict';
|
||||
|
||||
/****************************************
|
||||
* Author: Blade Barringer @crookedneighbor
|
||||
*
|
||||
* Reason: Webhooks have been moved from
|
||||
* being an object on preferences.webhooks
|
||||
* to being an array on webhooks. In addition
|
||||
* they support a type and options and label
|
||||
* ***************************************/
|
||||
|
||||
global.Promise = require('bluebird');
|
||||
const TaskQueue = require('cwait').TaskQueue;
|
||||
const logger = require('./utils/logger');
|
||||
const Timer = require('./utils/timer');
|
||||
const connectToDb = require('./utils/connect').connectToDb;
|
||||
const closeDb = require('./utils/connect').closeDb;
|
||||
const validator = require('validator');
|
||||
|
||||
const timer = new Timer();
|
||||
const MIGRATION_NAME = '20161002_add_missing_webhook_type.js';
|
||||
|
||||
// const DB_URI = 'mongodb://username:password@dsXXXXXX-a0.mlab.com:XXXXX,dsXXXXXX-a1.mlab.com:XXXXX/habitica?replicaSet=rs-dsXXXXXX';
|
||||
const DB_URI = 'mongodb://localhost/prod-copy-1';
|
||||
|
||||
const LOGGEDIN_DATE_RANGE = {
|
||||
$gte: new Date("2016-09-30T00:00:00.000Z"),
|
||||
// $lte: new Date("2016-09-25T00:00:00.000Z"),
|
||||
};
|
||||
|
||||
let Users;
|
||||
|
||||
connectToDb(DB_URI).then((db) => {
|
||||
Users = db.collection('users');
|
||||
})
|
||||
.then(findUsersWithWebhooks)
|
||||
.then(correctWebhooks)
|
||||
.then(() => {
|
||||
timer.stop();
|
||||
closeDb();
|
||||
}).catch(reportError);
|
||||
|
||||
function reportError (err) {
|
||||
logger.error('Uh oh, an error occurred');
|
||||
logger.error(err);
|
||||
closeDb();
|
||||
timer.stop();
|
||||
}
|
||||
|
||||
// Cached ids of users that need updating
|
||||
const USER_IDS = require('../../ids_of_webhooks_to_update.json');
|
||||
|
||||
function findUsersWithWebhooks () {
|
||||
logger.warn('Fetching users with webhooks...');
|
||||
|
||||
return Users.find({'_id': {$in: USER_IDS}}, ['preferences.webhooks']).toArray().then((docs) => {
|
||||
// return Users.find({'preferences.webhooks': {$ne: {} }}, ['preferences.webhooks']).toArray().then((docs) => {
|
||||
// TODO: Run this after the initial migration to catch any webhooks that may have been aded since the prod backup download
|
||||
// return Users.find({'preferences.webhooks': {$ne: {} }, 'auth.timestamps.loggedin': LOGGEDIN_DATE_RANGE}, ['preferences.webhooks']).toArray().then((docs) => {
|
||||
let updates = docs.map((user) => {
|
||||
let oldWebhooks = user.preferences.webhooks;
|
||||
let webhooks = Object.keys(oldWebhooks).map((id) => {
|
||||
let webhook = oldWebhooks[id]
|
||||
|
||||
webhook.type = 'taskActivity';
|
||||
webhook.label = '';
|
||||
webhook.options = {
|
||||
created: false,
|
||||
updated: false,
|
||||
deleted: false,
|
||||
scored: true,
|
||||
};
|
||||
|
||||
return webhook;
|
||||
}).sort((a, b) => {
|
||||
return a.sort - b.sort;
|
||||
});
|
||||
|
||||
return {
|
||||
webhooks,
|
||||
id: user._id,
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.resolve(updates);
|
||||
});
|
||||
}
|
||||
|
||||
function updateUserById (user) {
|
||||
let userId = user.id;
|
||||
let webhooks = user.webhooks;
|
||||
|
||||
return Users.findOneAndUpdate({
|
||||
_id: userId},
|
||||
{$set: {webhooks: webhooks, migration: MIGRATION_NAME}
|
||||
}, {returnOriginal: false})
|
||||
}
|
||||
|
||||
function correctWebhooks (users) {
|
||||
let queue = new TaskQueue(Promise, 300);
|
||||
|
||||
logger.warn('About to update', users.length, 'users...');
|
||||
|
||||
return Promise.map(users, queue.wrap(updateUserById)).then((result) => {
|
||||
let updates = result.filter(res => res.lastErrorObject && res.lastErrorObject.updatedExisting)
|
||||
let failures = result.filter(res => !(res.lastErrorObject && res.lastErrorObject.updatedExisting));
|
||||
|
||||
logger.warn(updates.length, 'users have been fixed');
|
||||
|
||||
if (failures.length > 0) {
|
||||
logger.error(failures.length, 'users could not be found');
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
73
migrations/20161002_takeThis.js
Normal file
73
migrations/20161002_takeThis.js
Normal file
@@ -0,0 +1,73 @@
|
||||
var migrationName = '20161002_takeThis.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 Take This ladder items to participants in this month's challenge
|
||||
*/
|
||||
|
||||
var mongo = require('mongoskin');
|
||||
|
||||
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
|
||||
|
||||
var dbUsers = mongo.db(connectionString).collection('users');
|
||||
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
var query = {
|
||||
'migration':{$ne:migrationName},
|
||||
'challenges':{$in:['4bbf63b5-10bc-49f9-8e95-5bd2ac99cd1c']}
|
||||
};
|
||||
|
||||
// specify fields we are interested in to limit retrieved data (empty if we're not reading data):
|
||||
var fields = {
|
||||
'items.gear.owned': 1
|
||||
};
|
||||
|
||||
console.warn('Updating users...');
|
||||
var progressCount = 1000;
|
||||
var count = 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.');
|
||||
setTimeout(displayData, 300000);
|
||||
return;
|
||||
}
|
||||
count++;
|
||||
|
||||
// specify user data to change:
|
||||
var set = {};
|
||||
|
||||
if (typeof user.items.gear.owned.armor_special_takeThis !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.head_special_takeThis':false};
|
||||
} else if (typeof user.items.gear.owned.weapon_special_takeThis !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.armor_special_takeThis':false};
|
||||
} else if (typeof user.items.gear.owned.shield_special_takeThis !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.weapon_special_takeThis':false};
|
||||
} else {
|
||||
set = {'migration':migrationName, 'items.gear.owned.shield_special_takeThis':false};
|
||||
}
|
||||
|
||||
dbUsers.update({_id:user._id}, {$set:set});
|
||||
|
||||
if (count%progressCount == 0) console.warn(count + ' ' + user._id);
|
||||
if (user._id == authorUuid) console.warn(authorName + ' processed');
|
||||
});
|
||||
|
||||
|
||||
function displayData() {
|
||||
console.warn('\n' + count + ' users processed\n');
|
||||
return exiting(0);
|
||||
}
|
||||
|
||||
|
||||
function exiting(code, msg) {
|
||||
code = code || 0; // 0 = success
|
||||
if (code && !msg) { msg = 'ERROR!'; }
|
||||
if (msg) {
|
||||
if (code) { console.error(msg); }
|
||||
else { console.log( msg); }
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
86
migrations/20161030-jackolanterns.js
Normal file
86
migrations/20161030-jackolanterns.js
Normal file
@@ -0,0 +1,86 @@
|
||||
var migrationName = '20161030-jackolanterns.js';
|
||||
var authorName = 'Sabe'; // in case script author needs to know when their ...
|
||||
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
|
||||
|
||||
/*
|
||||
* set the newStuff flag in all user accounts so they see a Bailey message
|
||||
*/
|
||||
|
||||
var mongo = require('mongoskin');
|
||||
|
||||
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
|
||||
|
||||
var dbUsers = mongo.db(connectionString).collection('users');
|
||||
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
var query = {
|
||||
'auth.timestamps.loggedin':{$gt:new Date('2016-10-01')} // remove when running migration a second time
|
||||
};
|
||||
|
||||
// specify fields we are interested in to limit retrieved data (empty if we're not reading data):
|
||||
var fields = {
|
||||
'migration': 1,
|
||||
'items.pets.JackOLantern-Base': 1,
|
||||
'items.mounts.JackOLantern-Base': 1,
|
||||
};
|
||||
|
||||
console.warn('Updating users...');
|
||||
var progressCount = 1000;
|
||||
var count = 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.');
|
||||
setTimeout(displayData, 300000);
|
||||
return;
|
||||
}
|
||||
count++;
|
||||
|
||||
// specify user data to change:
|
||||
var set = {};
|
||||
var inc = {};
|
||||
if (user.migration !== migrationName) {
|
||||
if (user.items.mounts['JackOLantern-Base']) {
|
||||
set = {'migration':migrationName, 'items.pets.JackOLantern-Ghost':5};
|
||||
} else if (user.items.pets['JackOLantern-Base']) {
|
||||
set = {'migration':migrationName, 'items.mounts.JackOLantern-Base':true};
|
||||
} else {
|
||||
set = {'migration':migrationName, 'items.pets.JackOLantern-Base':5};
|
||||
}
|
||||
inc = {
|
||||
'items.food.Candy_Base': 1,
|
||||
'items.food.Candy_CottonCandyBlue': 1,
|
||||
'items.food.Candy_CottonCandyPink': 1,
|
||||
'items.food.Candy_Desert': 1,
|
||||
'items.food.Candy_Golden': 1,
|
||||
'items.food.Candy_Red': 1,
|
||||
'items.food.Candy_Shade': 1,
|
||||
'items.food.Candy_Skeleton': 1,
|
||||
'items.food.Candy_White': 1,
|
||||
'items.food.Candy_Zombie': 1,
|
||||
}
|
||||
}
|
||||
|
||||
dbUsers.update({_id:user._id}, {$set:set, $inc:inc});
|
||||
|
||||
if (count%progressCount == 0) console.warn(count + ' ' + user._id);
|
||||
if (user._id == authorUuid) console.warn(authorName + ' processed');
|
||||
});
|
||||
|
||||
|
||||
function displayData() {
|
||||
console.warn('\n' + count + ' users processed\n');
|
||||
return exiting(0);
|
||||
}
|
||||
|
||||
|
||||
function exiting(code, msg) {
|
||||
code = code || 0; // 0 = success
|
||||
if (code && !msg) { msg = 'ERROR!'; }
|
||||
if (msg) {
|
||||
if (code) { console.error(msg); }
|
||||
else { console.log( msg); }
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
75
migrations/20161102_takeThis.js
Normal file
75
migrations/20161102_takeThis.js
Normal file
@@ -0,0 +1,75 @@
|
||||
var migrationName = '20161102_takeThis.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 Take This ladder items to participants in this month's challenge
|
||||
*/
|
||||
|
||||
var mongo = require('mongoskin');
|
||||
|
||||
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
|
||||
|
||||
var dbUsers = mongo.db(connectionString).collection('users');
|
||||
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
var query = {
|
||||
'migration':{$ne:migrationName},
|
||||
'challenges':{$in:['d1be0965-e909-4d30-82fa-9a0011f885b2']}
|
||||
};
|
||||
|
||||
// specify fields we are interested in to limit retrieved data (empty if we're not reading data):
|
||||
var fields = {
|
||||
'items.gear.owned': 1
|
||||
};
|
||||
|
||||
console.warn('Updating users...');
|
||||
var progressCount = 1000;
|
||||
var count = 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.');
|
||||
setTimeout(displayData, 300000);
|
||||
return;
|
||||
}
|
||||
count++;
|
||||
|
||||
// specify user data to change:
|
||||
var set = {};
|
||||
|
||||
if (typeof user.items.gear.owned.head_special_takeThis !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.body_special_takeThis':false};
|
||||
} else if (typeof user.items.gear.owned.armor_special_takeThis !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.head_special_takeThis':false};
|
||||
} else if (typeof user.items.gear.owned.weapon_special_takeThis !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.armor_special_takeThis':false};
|
||||
} else if (typeof user.items.gear.owned.shield_special_takeThis !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.weapon_special_takeThis':false};
|
||||
} else {
|
||||
set = {'migration':migrationName, 'items.gear.owned.shield_special_takeThis':false};
|
||||
}
|
||||
|
||||
dbUsers.update({_id:user._id}, {$set:set});
|
||||
|
||||
if (count%progressCount == 0) console.warn(count + ' ' + user._id);
|
||||
if (user._id == authorUuid) console.warn(authorName + ' processed');
|
||||
});
|
||||
|
||||
|
||||
function displayData() {
|
||||
console.warn('\n' + count + ' users processed\n');
|
||||
return exiting(0);
|
||||
}
|
||||
|
||||
|
||||
function exiting(code, msg) {
|
||||
code = code || 0; // 0 = success
|
||||
if (code && !msg) { msg = 'ERROR!'; }
|
||||
if (msg) {
|
||||
if (code) { console.error(msg); }
|
||||
else { console.log( msg); }
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
74
migrations/20161122_turkey_ladder.js
Normal file
74
migrations/20161122_turkey_ladder.js
Normal file
@@ -0,0 +1,74 @@
|
||||
var migrationName = '20161122_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
|
||||
|
||||
/*
|
||||
* Yearly Turkey Day award. Turkey pet, Turkey mount, Gilded Turkey pet, Gilded Turkey mount
|
||||
*/
|
||||
|
||||
var mongo = require('mongoskin');
|
||||
|
||||
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
|
||||
|
||||
var dbUsers = mongo.db(connectionString).collection('users');
|
||||
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
var query = {
|
||||
'migration':{$ne:migrationName},
|
||||
'auth.timestamps.loggedin':{$gt:new Date('2016-10-31')} // Extend timeframe each run of migration
|
||||
};
|
||||
|
||||
// specify fields we are interested in to limit retrieved data (empty if we're not reading data):
|
||||
var fields = {
|
||||
'migration': 1,
|
||||
'items.mounts': 1,
|
||||
'items.pets': 1,
|
||||
};
|
||||
|
||||
console.warn('Updating users...');
|
||||
var progressCount = 1000;
|
||||
var count = 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.');
|
||||
setTimeout(displayData, 300000);
|
||||
return;
|
||||
}
|
||||
count++;
|
||||
|
||||
// specify user data to change:
|
||||
var set = {};
|
||||
|
||||
if (user.items.pets['Turkey-Gilded']) {
|
||||
set = {'migration':migrationName, 'items.mounts.Turkey-Gilded':true};
|
||||
} else if (user.items.mounts['Turkey-Base']) {
|
||||
set = {'migration':migrationName, 'items.pets.Turkey-Gilded':5};
|
||||
} else if (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 (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);
|
||||
}
|
||||
73
migrations/20161230_nye_hats.js
Normal file
73
migrations/20161230_nye_hats.js
Normal file
@@ -0,0 +1,73 @@
|
||||
var migrationName = '20161230_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
|
||||
|
||||
/*
|
||||
* Yearly New Year's party hat award
|
||||
*/
|
||||
|
||||
var mongo = require('mongoskin');
|
||||
|
||||
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
|
||||
|
||||
var dbUsers = mongo.db(connectionString).collection('users');
|
||||
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
var query = {
|
||||
'migration':{$ne:migrationName},
|
||||
'auth.timestamps.loggedin':{$gt:new Date('2016-11-30')} // Remove after first run
|
||||
};
|
||||
|
||||
// specify fields we are interested in to limit retrieved data (empty if we're not reading data):
|
||||
var fields = {
|
||||
'items.gear.owned': 1,
|
||||
};
|
||||
|
||||
console.warn('Updating users...');
|
||||
var progressCount = 1000;
|
||||
var count = 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.');
|
||||
setTimeout(displayData, 300000);
|
||||
return;
|
||||
}
|
||||
count++;
|
||||
|
||||
// specify user data to change:
|
||||
var set = {};
|
||||
|
||||
if (typeof user.items.gear.owned.head_special_nye2015 !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.head_special_nye2016':false};
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2014 !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.head_special_nye2015':false};
|
||||
} else if (typeof user.items.gear.owned.head_special_nye !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.head_special_nye2014':false};
|
||||
} else {
|
||||
set = {'migration':migrationName, 'items.gear.owned.head_special_nye':false};
|
||||
}
|
||||
|
||||
dbUsers.update({_id:user._id}, {$set:set});
|
||||
|
||||
if (count%progressCount == 0) console.warn(count + ' ' + user._id);
|
||||
if (user._id == authorUuid) console.warn(authorName + ' processed');
|
||||
});
|
||||
|
||||
|
||||
function displayData() {
|
||||
console.warn('\n' + count + ' users processed\n');
|
||||
return exiting(0);
|
||||
}
|
||||
|
||||
|
||||
function exiting(code, msg) {
|
||||
code = code || 0; // 0 = success
|
||||
if (code && !msg) { msg = 'ERROR!'; }
|
||||
if (msg) {
|
||||
if (code) { console.error(msg); }
|
||||
else { console.log( msg); }
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ var _id = '';
|
||||
var update = {
|
||||
$addToSet: {
|
||||
'purchased.plan.mysteryItems':{
|
||||
$each:['head_mystery_201609','armor_mystery_201609']
|
||||
$each:['head_mystery_201612','armor_mystery_201612']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,49 +6,70 @@ var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
|
||||
* set the newStuff flag in all user accounts so they see a Bailey message
|
||||
*/
|
||||
|
||||
var mongo = require('mongoskin');
|
||||
|
||||
var monk = require('monk');
|
||||
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
|
||||
var dbUsers = monk(connectionString).get('users', { castIds: false });
|
||||
|
||||
var dbUsers = mongo.db(connectionString).collection('users');
|
||||
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
var query = {
|
||||
'flags.newStuff':{$ne:true}
|
||||
};
|
||||
function processUsers(lastId) {
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
var query = {
|
||||
'flags.newStuff': {$ne:true},
|
||||
};
|
||||
|
||||
// specify fields we are interested in to limit retrieved data (empty if we're not reading data):
|
||||
var fields = {
|
||||
};
|
||||
if (lastId) {
|
||||
query._id = {
|
||||
$gt: lastId
|
||||
}
|
||||
}
|
||||
|
||||
dbUsers.find(query, {
|
||||
sort: {_id: 1},
|
||||
limit: 250,
|
||||
fields: [] // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
|
||||
})
|
||||
.then(updateUsers)
|
||||
.catch(function (err) {
|
||||
console.log(err);
|
||||
return exiting(1, 'ERROR! ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
console.warn('Updating users...');
|
||||
var progressCount = 1000;
|
||||
var count = 0;
|
||||
dbUsers.findEach(query, fields, {batchSize:250}, function(err, user) {
|
||||
if (err) { return exiting(1, 'ERROR! ' + err); }
|
||||
if (!user) {
|
||||
|
||||
function updateUsers (users) {
|
||||
if (!users || users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
setTimeout(displayData, 300000);
|
||||
displayData();
|
||||
return;
|
||||
}
|
||||
|
||||
var userPaymentPromises = users.map(updateUser);
|
||||
var lastUser = users[users.length - 1];
|
||||
|
||||
return Promise.all(userPaymentPromises)
|
||||
.then(function () {
|
||||
processUsers(lastUser._id);
|
||||
});
|
||||
}
|
||||
|
||||
function updateUser (user) {
|
||||
count++;
|
||||
|
||||
// specify user data to change:
|
||||
var set = {'flags.newStuff':true};
|
||||
var set = {'flags.newStuff': true};
|
||||
|
||||
dbUsers.update({_id:user._id}, {$set:set});
|
||||
dbUsers.update({_id: user._id}, {$set:set});
|
||||
|
||||
if (count%progressCount == 0) console.warn(count + ' ' + user._id);
|
||||
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
|
||||
if (user._id == authorUuid) console.warn(authorName + ' processed');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
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!'; }
|
||||
@@ -58,3 +79,5 @@ function exiting(code, msg) {
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
processUsers()
|
||||
|
||||
@@ -6,14 +6,11 @@ var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
|
||||
* Remove flag stating that the Enchanted Armoire is empty, for when new equipment is added
|
||||
*/
|
||||
|
||||
var dbserver = 'localhost:27017'; // FOR TEST DATABASE
|
||||
// var dbserver = 'username:password@ds031379-a0.mongolab.com:31379'; // FOR PRODUCTION DATABASE
|
||||
var dbname = 'habitrpg';
|
||||
|
||||
var mongo = require('mongoskin');
|
||||
var _ = require('lodash');
|
||||
|
||||
var dbUsers = mongo.db(dbserver + '/' + dbname + '?auto_reconnect').collection('users');
|
||||
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
|
||||
|
||||
var dbUsers = mongo.db(connectionString).collection('users');
|
||||
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
var query = {
|
||||
@@ -22,7 +19,6 @@ var query = {
|
||||
|
||||
// specify fields we are interested in to limit retrieved data (empty if we're not reading data):
|
||||
var fields = {
|
||||
'flags.armoireEmpty':1
|
||||
};
|
||||
|
||||
console.warn('Updating users...');
|
||||
@@ -32,7 +28,8 @@ 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();
|
||||
setTimeout(displayData, 300000);
|
||||
return;
|
||||
}
|
||||
count++;
|
||||
|
||||
|
||||
115
migrations/restore-profile-data.js
Normal file
115
migrations/restore-profile-data.js
Normal file
@@ -0,0 +1,115 @@
|
||||
var migrationName = 'restore_profile_data.js';
|
||||
var authorName = 'ThehollidayInn'; // in case script author needs to know when their ...
|
||||
var authorUuid = ''; //... own data is done
|
||||
|
||||
/*
|
||||
* Check if users have empty profile data in new database and update it with old database info
|
||||
*/
|
||||
|
||||
var monk = require('monk');
|
||||
var connectionString = ''; // FOR TEST DATABASE
|
||||
var dbUsers = monk(connectionString).get('users', { castIds: false });
|
||||
|
||||
var monk2 = require('monk');
|
||||
var oldDbConnectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
|
||||
var olDbUsers = monk2(oldDbConnectionString).get('users', { castIds: false });
|
||||
|
||||
function processUsers(lastId)
|
||||
{
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
var query = {
|
||||
// 'profile.name': 'profile name not found',
|
||||
'profile.blurb': null,
|
||||
// 'auth.timestamps.loggedin': {$gt: new Date('11/30/2016')},
|
||||
};
|
||||
|
||||
if (lastId) {
|
||||
query._id = {
|
||||
$gt: lastId
|
||||
}
|
||||
}
|
||||
|
||||
dbUsers.find(query, {
|
||||
sort: {_id: 1},
|
||||
limit: 250,
|
||||
fields: ['_id', 'profile', 'auth.timestamps.loggedin'] // 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.');
|
||||
setTimeout(displayData, 300000);
|
||||
return;
|
||||
}
|
||||
|
||||
var userPaymentPromises = users.map(updateUser);
|
||||
var lastUser = users[users.length - 1];
|
||||
|
||||
return Promise.all(userPaymentPromises)
|
||||
.then(function () {
|
||||
processUsers(lastUser._id);
|
||||
});
|
||||
}
|
||||
|
||||
function updateUser (user) {
|
||||
count++;
|
||||
|
||||
if (!user.profile.name || user.profile.name === 'profile name not found' || !user.profile.imageUrl || !user.profile.blurb) {
|
||||
return olDbUsers.findOne({_id: user._id}, '_id profile')
|
||||
.then((oldUserData) => {
|
||||
if (!oldUserData) return;
|
||||
// specify user data to change:
|
||||
var set = {};
|
||||
|
||||
if (oldUserData.profile.name === 'profile name not found') return;
|
||||
|
||||
var userNeedsProfileName = !user.profile.name || user.profile.name === 'profile name not found';
|
||||
if (userNeedsProfileName && oldUserData.profile.name) {
|
||||
set['profile.name'] = oldUserData.profile.name;
|
||||
}
|
||||
|
||||
if (!user.profile.imageUrl && oldUserData.profile.imageUrl) {
|
||||
set['profile.imageUrl'] = oldUserData.profile.imageUrl;
|
||||
}
|
||||
|
||||
if (!user.profile.blurb && oldUserData.profile.blurb) {
|
||||
set['profile.blurb'] = oldUserData.profile.blurb;
|
||||
}
|
||||
|
||||
if (Object.keys(set).length !== 0 && set.constructor === Object) {
|
||||
console.log(set)
|
||||
return dbUsers.update({_id: user._id}, {$set:set});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
|
||||
if (user._id == authorUuid) console.warn(authorName + ' processed');
|
||||
}
|
||||
|
||||
function displayData() {
|
||||
console.warn('\n' + count + ' users processed\n');
|
||||
return exiting(0);
|
||||
}
|
||||
|
||||
function exiting(code, msg) {
|
||||
code = code || 0; // 0 = success
|
||||
if (code && !msg) { msg = 'ERROR!'; }
|
||||
if (msg) {
|
||||
if (code) { console.error(msg); }
|
||||
else { console.log( msg); }
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
|
||||
processUsers()
|
||||
80
migrations/takeThis.js
Normal file
80
migrations/takeThis.js
Normal file
@@ -0,0 +1,80 @@
|
||||
var migrationName = '20170103_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
|
||||
|
||||
/*
|
||||
* Award Take This ladder items to participants in this month's challenge
|
||||
*/
|
||||
|
||||
var mongo = require('mongoskin');
|
||||
|
||||
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
|
||||
|
||||
var dbUsers = mongo.db(connectionString).collection('users');
|
||||
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
var query = {
|
||||
'migration':{$ne:migrationName},
|
||||
'challenges':{$in:['ff674aba-a114-4a6f-8ebc-1de27ffb646e']}
|
||||
};
|
||||
|
||||
// specify fields we are interested in to limit retrieved data (empty if we're not reading data):
|
||||
var fields = {
|
||||
'items.gear.owned': 1
|
||||
};
|
||||
|
||||
console.warn('Updating users...');
|
||||
var progressCount = 1000;
|
||||
var count = 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.');
|
||||
setTimeout(displayData, 300000);
|
||||
return;
|
||||
}
|
||||
count++;
|
||||
|
||||
// specify user data to change:
|
||||
var set = {};
|
||||
|
||||
if (typeof user.items.gear.owned.back_special_takeThis !== 'undefined') {
|
||||
set = {'migration':migrationName};
|
||||
{ else if (typeof user.items.gear.owned.body_special_takeThis !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.back_special_takeThis':false};
|
||||
} else if (typeof user.items.gear.owned.head_special_takeThis !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.body_special_takeThis':false};
|
||||
} else if (typeof user.items.gear.owned.armor_special_takeThis !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.head_special_takeThis':false};
|
||||
} else if (typeof user.items.gear.owned.weapon_special_takeThis !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.armor_special_takeThis':false};
|
||||
} else if (typeof user.items.gear.owned.shield_special_takeThis !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.weapon_special_takeThis':false};
|
||||
} else {
|
||||
set = {'migration':migrationName, 'items.gear.owned.shield_special_takeThis':false};
|
||||
}
|
||||
|
||||
dbUsers.update({_id:user._id}, {$set:set});
|
||||
|
||||
if (count%progressCount == 0) console.warn(count + ' ' + user._id);
|
||||
if (user._id == authorUuid) console.warn(authorName + ' processed');
|
||||
});
|
||||
|
||||
|
||||
function displayData() {
|
||||
console.warn('\n' + count + ' users processed\n');
|
||||
return exiting(0);
|
||||
}
|
||||
|
||||
|
||||
function exiting(code, msg) {
|
||||
code = code || 0; // 0 = success
|
||||
if (code && !msg) { msg = 'ERROR!'; }
|
||||
if (msg) {
|
||||
if (code) { console.error(msg); }
|
||||
else { console.log( msg); }
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ function connectToDb (dbUri) {
|
||||
function closeDb () {
|
||||
if (db) db.close();
|
||||
|
||||
logger.success('CLosed connection to the database');
|
||||
logger.success('Closed connection to the database');
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
|
||||
6042
npm-shrinkwrap.json
generated
6042
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
41
package.json
41
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "3.43.1",
|
||||
"version": "3.67.0",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@slack/client": "3.6.0",
|
||||
@@ -15,7 +15,10 @@
|
||||
"aws-sdk": "^2.0.25",
|
||||
"babel-core": "^6.0.0",
|
||||
"babel-loader": "^6.0.0",
|
||||
"babel-plugin-syntax-async-functions": "^6.13.0",
|
||||
"babel-plugin-transform-async-to-module-method": "^6.8.0",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.16.0",
|
||||
"babel-plugin-transform-regenerator": "^6.16.1",
|
||||
"babel-polyfill": "^6.6.1",
|
||||
"babel-preset-es2015": "^6.6.0",
|
||||
"babel-register": "^6.6.0",
|
||||
@@ -28,13 +31,13 @@
|
||||
"compression": "^1.6.1",
|
||||
"connect-ratelimit": "0.0.7",
|
||||
"cookie-session": "^1.2.0",
|
||||
"coupon-code": "^0.4.3",
|
||||
"coupon-code": "^0.4.5",
|
||||
"css-loader": "^0.23.1",
|
||||
"csv-stringify": "^1.0.2",
|
||||
"cwait": "^1.0.0",
|
||||
"domain-middleware": "~0.1.0",
|
||||
"estraverse": "^4.1.1",
|
||||
"express": "~4.13.3",
|
||||
"express": "~4.14.0",
|
||||
"express-csv": "~0.6.0",
|
||||
"express-validator": "^2.18.0",
|
||||
"extract-text-webpack-plugin": "^1.0.1",
|
||||
@@ -63,16 +66,18 @@
|
||||
"image-size": "~0.3.2",
|
||||
"in-app-purchase": "^1.1.6",
|
||||
"jade": "~1.11.0",
|
||||
"jquery": "https://registry.npmjs.org/jquery/-/jquery-3.1.1.tgz",
|
||||
"js2xmlparser": "~1.0.0",
|
||||
"json-loader": "^0.5.4",
|
||||
"less": "^2.7.1",
|
||||
"less-loader": "^2.2.3",
|
||||
"lodash": "^3.10.1",
|
||||
"lodash.pickby": "^4.2.0",
|
||||
"lodash.setwith": "^4.2.0",
|
||||
"merge-stream": "^1.0.0",
|
||||
"method-override": "^2.3.5",
|
||||
"moment": "^2.13.0",
|
||||
"mongoose": "^4.4.16",
|
||||
"mongoose": "^4.7.1",
|
||||
"mongoose-id-autoinc": "~2013.7.14-4",
|
||||
"morgan": "^1.7.0",
|
||||
"nconf": "~0.8.2",
|
||||
@@ -85,14 +90,16 @@
|
||||
"pageres": "^4.1.1",
|
||||
"passport": "~0.2.1",
|
||||
"passport-facebook": "2.0.0",
|
||||
"passport-google-oauth20": "1.0.0",
|
||||
"paypal-ipn": "3.0.0",
|
||||
"paypal-rest-sdk": "^1.2.1",
|
||||
"postcss-easy-import": "^1.0.1",
|
||||
"pretty-data": "^0.40.0",
|
||||
"ps-tree": "^1.0.0",
|
||||
"pug": "^2.0.0-beta6",
|
||||
"push-notify": "habitrpg/push-notify#v1.2.0",
|
||||
"pusher": "^1.3.0",
|
||||
"request": "~2.72.0",
|
||||
"request": "~2.74.0",
|
||||
"rimraf": "^2.4.3",
|
||||
"run-sequence": "^1.1.4",
|
||||
"s3-upload-stream": "^1.0.6",
|
||||
@@ -108,13 +115,12 @@
|
||||
"validator": "^4.9.0",
|
||||
"vinyl-buffer": "^1.0.0",
|
||||
"vinyl-source-stream": "^1.1.0",
|
||||
"vue": "^2.0.0-rc.6",
|
||||
"vue": "^2.1.0",
|
||||
"vue-hot-reload-api": "^1.2.0",
|
||||
"vue-loader": "^9.4.0",
|
||||
"vue-loader": "^10.0.0",
|
||||
"vue-resource": "^1.0.2",
|
||||
"vue-router": "^2.0.0-rc.5",
|
||||
"vuex": "^2.0.0-rc.5",
|
||||
"vuex-router-sync": "^3.0.0",
|
||||
"vue-template-compiler": "^2.1.0",
|
||||
"webpack": "^1.12.2",
|
||||
"webpack-merge": "^0.8.3",
|
||||
"winston": "^2.1.0",
|
||||
@@ -122,12 +128,13 @@
|
||||
},
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": "^4.3.1",
|
||||
"npm": "^3.8.9"
|
||||
"node": "^6.9.1",
|
||||
"npm": "^4.0.2"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .js,.vue .",
|
||||
"test": "npm run lint && gulp test && npm run client:unit",
|
||||
"test:build": "gulp test:prepare:build",
|
||||
"test:api-v3": "gulp test:api-v3",
|
||||
"test:api-v3:unit": "gulp test:api-v3:unit",
|
||||
"test:api-v3:integration": "gulp test:api-v3:integration",
|
||||
@@ -146,13 +153,13 @@
|
||||
"client:dev": "node webpack/dev-server.js",
|
||||
"client:build": "node webpack/build.js",
|
||||
"client:unit": "karma start test/client/unit/karma.conf.js --single-run",
|
||||
"client:unit:watch": "karma start test/client/unit/karma.conf.js",
|
||||
"client:e2e": "node test/client/e2e/runner.js",
|
||||
"client:test": "npm run client:unit && npm run client:e2e",
|
||||
"start": "gulp run:dev",
|
||||
"postinstall": "bower --config.interactive=false install -f; gulp build; npm run client:build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^6.0.0",
|
||||
"chai": "^3.4.0",
|
||||
"chai-as-promised": "^5.1.0",
|
||||
"chalk": "^1.1.3",
|
||||
@@ -162,13 +169,12 @@
|
||||
"cross-spawn": "^2.1.5",
|
||||
"csv": "~0.3.6",
|
||||
"deep-diff": "~0.1.4",
|
||||
"eslint": "~2.12.0",
|
||||
"eslint-config-habitrpg": "^1.0.0",
|
||||
"eslint": "^3.0.0",
|
||||
"eslint-config-habitrpg": "^2.0.0",
|
||||
"eslint-friendly-formatter": "^2.0.5",
|
||||
"eslint-loader": "^1.3.0",
|
||||
"eslint-plugin-babel": "^3.0.0",
|
||||
"eslint-plugin-html": "^1.3.0",
|
||||
"eslint-plugin-mocha": "^2.1.0",
|
||||
"eslint-plugin-mocha": "^4.7.0",
|
||||
"event-stream": "^3.2.2",
|
||||
"eventsource-polyfill": "^0.9.6",
|
||||
"expect.js": "~0.2.0",
|
||||
@@ -193,6 +199,7 @@
|
||||
"mocha": "^2.3.3",
|
||||
"mongodb": "^2.0.46",
|
||||
"mongoskin": "~2.1.0",
|
||||
"monk": "^3.1.3",
|
||||
"nightwatch": "^0.8.18",
|
||||
"phantomjs-prebuilt": "^2.1.12",
|
||||
"protractor": "^3.1.1",
|
||||
@@ -201,8 +208,8 @@
|
||||
"selenium-server": "2.53.0",
|
||||
"sinon": "^1.17.2",
|
||||
"sinon-chai": "^2.8.0",
|
||||
"sinon-stub-promise": "^4.0.0",
|
||||
"superagent-defaults": "^0.1.13",
|
||||
"vinyl-source-stream": "^1.0.0",
|
||||
"vinyl-transform": "^1.0.0",
|
||||
"webpack-dev-middleware": "^1.4.0",
|
||||
"webpack-hot-middleware": "^2.6.0"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"extends": [
|
||||
"habitrpg/mocha",
|
||||
"habitrpg/babel"
|
||||
"habitrpg/esnext"
|
||||
],
|
||||
"env": {
|
||||
"node": true,
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
|
||||
describe('GET challenges/group/:groupId', () => {
|
||||
describe('GET challenges/groups/:groupId', () => {
|
||||
context('Public Guild', () => {
|
||||
let publicGuild, user, nonMember, challenge, challenge2;
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import {
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
sleep,
|
||||
server,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('POST /chat', () => {
|
||||
let user, groupWithChat, userWithChatRevoked, member;
|
||||
@@ -32,6 +35,24 @@ describe('POST /chat', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('Returns an error when an empty message is provided', async () => {
|
||||
await expect(user.post(`/groups/${groupWithChat._id}/chat`, { message: ' '}))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('Returns an error when an message containing only newlines is provided', async () => {
|
||||
await expect(user.post(`/groups/${groupWithChat._id}/chat`, { message: '\n\n'}))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('Returns an error when group is not found', async () => {
|
||||
await expect(user.post('/groups/invalidID/chat', { message: testMessage})).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
@@ -40,7 +61,7 @@ describe('POST /chat', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('Returns an error when chat privileges are revoked', async () => {
|
||||
it('returns an error when chat privileges are revoked when sending a message to a public guild', async () => {
|
||||
await expect(userWithChatRevoked.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage})).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
@@ -48,12 +69,86 @@ describe('POST /chat', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('does not error when sending a message to a private guild with a user with revoked chat', async () => {
|
||||
let { group, members } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Private Guild',
|
||||
type: 'guild',
|
||||
privacy: 'private',
|
||||
},
|
||||
members: 1,
|
||||
});
|
||||
|
||||
let privateGuildMemberWithChatsRevoked = members[0];
|
||||
await privateGuildMemberWithChatsRevoked.update({'flags.chatRevoked': true});
|
||||
|
||||
let message = await privateGuildMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage});
|
||||
|
||||
expect(message.message.id).to.exist;
|
||||
});
|
||||
|
||||
it('does not error when sending a message to a party with a user with revoked chat', async () => {
|
||||
let { group, members } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Party',
|
||||
type: 'party',
|
||||
privacy: 'private',
|
||||
},
|
||||
members: 1,
|
||||
});
|
||||
|
||||
let privatePartyMemberWithChatsRevoked = members[0];
|
||||
await privatePartyMemberWithChatsRevoked.update({'flags.chatRevoked': true});
|
||||
|
||||
let message = await privatePartyMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage});
|
||||
|
||||
expect(message.message.id).to.exist;
|
||||
});
|
||||
|
||||
it('creates a chat', async () => {
|
||||
let message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage});
|
||||
|
||||
expect(message.message.id).to.exist;
|
||||
});
|
||||
|
||||
it('sends group chat received webhooks', async () => {
|
||||
let userUuid = generateUUID();
|
||||
let memberUuid = generateUUID();
|
||||
await server.start();
|
||||
|
||||
await user.post('/user/webhook', {
|
||||
url: `http://localhost:${server.port}/webhooks/${userUuid}`,
|
||||
type: 'groupChatReceived',
|
||||
enabled: true,
|
||||
options: {
|
||||
groupId: groupWithChat.id,
|
||||
},
|
||||
});
|
||||
await member.post('/user/webhook', {
|
||||
url: `http://localhost:${server.port}/webhooks/${memberUuid}`,
|
||||
type: 'groupChatReceived',
|
||||
enabled: true,
|
||||
options: {
|
||||
groupId: groupWithChat.id,
|
||||
},
|
||||
});
|
||||
|
||||
let message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
|
||||
|
||||
await sleep();
|
||||
|
||||
await server.close();
|
||||
|
||||
let userBody = server.getWebhookData(userUuid);
|
||||
let memberBody = server.getWebhookData(memberUuid);
|
||||
|
||||
[userBody, memberBody].forEach((body) => {
|
||||
expect(body.group.id).to.eql(groupWithChat._id);
|
||||
expect(body.group.name).to.eql(groupWithChat.name);
|
||||
expect(body.chat).to.eql(message.message);
|
||||
});
|
||||
});
|
||||
|
||||
it('notifies other users of new messages for a guild', async () => {
|
||||
let message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage});
|
||||
let memberWithNotification = await member.get('/user');
|
||||
|
||||
@@ -29,14 +29,6 @@ describe('POST /coupons/generate/:event', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if event is missing', async () => {
|
||||
await expect(user.post('/coupons/generate')).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: 'Not found.',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if event is invalid', async () => {
|
||||
await expect(user.post('/coupons/generate/notValid?count=1')).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
|
||||
@@ -7,15 +7,21 @@ import {
|
||||
import moment from 'moment';
|
||||
|
||||
describe('GET /export/history.csv', () => {
|
||||
it('should return a valid CSV file with tasks history data', async () => {
|
||||
// TODO disabled because it randomly causes the build to fail
|
||||
xit('should return a valid CSV file with tasks history data', async () => {
|
||||
let user = await generateUser();
|
||||
let tasks = await user.post('/tasks/user', [
|
||||
{type: 'habit', text: 'habit 1'},
|
||||
{type: 'daily', text: 'daily 1'},
|
||||
{type: 'habit', text: 'habit 1'},
|
||||
{type: 'habit', text: 'habit 2'},
|
||||
{type: 'todo', text: 'todo 1'},
|
||||
]);
|
||||
|
||||
// to handle occasional inconsistency in task creation order
|
||||
tasks.sort(function (a, b) {
|
||||
return a.text.localeCompare(b.text);
|
||||
});
|
||||
|
||||
// score all the tasks twice
|
||||
await user.post(`/tasks/${tasks[0]._id}/score/up`);
|
||||
await user.post(`/tasks/${tasks[1]._id}/score/up`);
|
||||
@@ -28,7 +34,7 @@ describe('GET /export/history.csv', () => {
|
||||
await user.post(`/tasks/${tasks[3]._id}/score/up`);
|
||||
|
||||
// adding an history entry to daily 1 manually because cron didn't run yet
|
||||
await updateDocument('tasks', tasks[1], {
|
||||
await updateDocument('tasks', tasks[0], {
|
||||
history: [{value: 3.2, date: Number(new Date())}],
|
||||
});
|
||||
|
||||
@@ -41,11 +47,11 @@ describe('GET /export/history.csv', () => {
|
||||
let splitRes = res.split('\n');
|
||||
|
||||
expect(splitRes[0]).to.equal('Task Name,Task ID,Task Type,Date,Value');
|
||||
expect(splitRes[1]).to.equal(`habit 1,${tasks[0]._id},habit,${moment(tasks[0].history[0].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[0].history[0].value}`);
|
||||
expect(splitRes[2]).to.equal(`habit 1,${tasks[0]._id},habit,${moment(tasks[0].history[1].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[0].history[1].value}`);
|
||||
expect(splitRes[3]).to.equal(`habit 2,${tasks[2]._id},habit,${moment(tasks[2].history[0].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[2].history[0].value}`);
|
||||
expect(splitRes[4]).to.equal(`habit 2,${tasks[2]._id},habit,${moment(tasks[2].history[1].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[2].history[1].value}`);
|
||||
expect(splitRes[5]).to.equal(`daily 1,${tasks[1]._id},daily,${moment(tasks[1].history[0].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[1].history[0].value}`);
|
||||
expect(splitRes[1]).to.equal(`daily 1,${tasks[0]._id},daily,${moment(tasks[0].history[0].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[0].history[0].value}`);
|
||||
expect(splitRes[2]).to.equal(`habit 1,${tasks[1]._id},habit,${moment(tasks[1].history[0].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[1].history[0].value}`);
|
||||
expect(splitRes[3]).to.equal(`habit 1,${tasks[1]._id},habit,${moment(tasks[1].history[1].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[1].history[1].value}`);
|
||||
expect(splitRes[4]).to.equal(`habit 2,${tasks[2]._id},habit,${moment(tasks[2].history[0].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[2].history[0].value}`);
|
||||
expect(splitRes[5]).to.equal(`habit 2,${tasks[2]._id},habit,${moment(tasks[2].history[1].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[2].history[1].value}`);
|
||||
expect(splitRes[6]).to.equal('');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,8 @@ import Bluebird from 'bluebird';
|
||||
let parseStringAsync = Bluebird.promisify(xml2js.parseString, {context: xml2js});
|
||||
|
||||
describe('GET /export/userdata.xml', () => {
|
||||
it('should return a valid XML file with user data', async () => {
|
||||
// TODO disabled because it randomly causes the build to fail
|
||||
xit('should return a valid XML file with user data', async () => {
|
||||
let user = await generateUser();
|
||||
let tasks = await user.post('/tasks/user', [
|
||||
{type: 'habit', text: 'habit 1'},
|
||||
|
||||
@@ -82,8 +82,10 @@ describe('GET /groups/:groupId/members', () => {
|
||||
'backer', 'contributor', 'auth', 'items', 'inbox',
|
||||
]);
|
||||
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
|
||||
expect(Object.keys(memberRes.preferences).sort()).to.eql(['size', 'hair', 'skin', 'shirt',
|
||||
'chair', 'costume', 'sleep', 'background'].sort());
|
||||
expect(Object.keys(memberRes.preferences).sort()).to.eql([
|
||||
'size', 'hair', 'skin', 'shirt',
|
||||
'chair', 'costume', 'sleep', 'background',
|
||||
].sort());
|
||||
|
||||
expect(memberRes.stats.maxMP).to.exist;
|
||||
expect(memberRes.stats.maxHealth).to.equal(common.maxHealth);
|
||||
|
||||
@@ -134,6 +134,22 @@ describe('POST /group/:groupId/join', () => {
|
||||
|
||||
await expect(user.get('/user')).to.eventually.have.deep.property('items.quests.basilist', 1);
|
||||
});
|
||||
|
||||
it('notifies inviting user that their invitation was accepted', async () => {
|
||||
await invitedUser.post(`/groups/${guild._id}/join`);
|
||||
|
||||
let inviter = await user.get('/user');
|
||||
let expectedData = {
|
||||
headerText: t('invitationAcceptedHeader'),
|
||||
bodyText: t('invitationAcceptedBody', {
|
||||
username: invitedUser.auth.local.username,
|
||||
groupName: guild.name,
|
||||
}),
|
||||
};
|
||||
|
||||
expect(inviter.notifications[0].type).to.eql('GROUP_INVITE_ACCEPTED');
|
||||
expect(inviter.notifications[0].data).to.eql(expectedData);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -172,6 +188,23 @@ describe('POST /group/:groupId/join', () => {
|
||||
await expect(invitedUser.get('/user')).to.eventually.have.deep.property('party._id', party._id);
|
||||
});
|
||||
|
||||
it('notifies inviting user that their invitation was accepted', async () => {
|
||||
await invitedUser.post(`/groups/${party._id}/join`);
|
||||
|
||||
let inviter = await user.get('/user');
|
||||
|
||||
let expectedData = {
|
||||
headerText: t('invitationAcceptedHeader'),
|
||||
bodyText: t('invitationAcceptedBody', {
|
||||
username: invitedUser.auth.local.username,
|
||||
groupName: party.name,
|
||||
}),
|
||||
};
|
||||
|
||||
expect(inviter.notifications[0].type).to.eql('GROUP_INVITE_ACCEPTED');
|
||||
expect(inviter.notifications[0].data).to.eql(expectedData);
|
||||
});
|
||||
|
||||
it('clears invitation from user when joining party', async () => {
|
||||
await invitedUser.post(`/groups/${party._id}/join`);
|
||||
|
||||
|
||||
@@ -65,6 +65,19 @@ describe('POST /groups/:groupId/leave', () => {
|
||||
expect(groupToLeave.leader).to.equal(member._id);
|
||||
});
|
||||
|
||||
it('removes new messages for that group from user', async () => {
|
||||
await member.post(`/groups/${groupToLeave._id}/chat`, { message: 'Some message' });
|
||||
|
||||
await leader.sync();
|
||||
|
||||
expect(leader.newMessages[groupToLeave._id]).to.not.be.empty;
|
||||
|
||||
await leader.post(`/groups/${groupToLeave._id}/leave`);
|
||||
await leader.sync();
|
||||
|
||||
expect(leader.newMessages[groupToLeave._id]).to.be.empty;
|
||||
});
|
||||
|
||||
context('With challenges', () => {
|
||||
let challenge;
|
||||
|
||||
@@ -122,6 +135,8 @@ describe('POST /groups/:groupId/leave', () => {
|
||||
privateGuild = group;
|
||||
leader = groupLeader;
|
||||
invitedUser = invitees[0];
|
||||
|
||||
await leader.post(`/groups/${group._id}/chat`, { message: 'Some message' });
|
||||
});
|
||||
|
||||
it('removes a group when the last member leaves', async () => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import * as email from '../../../../../website/server/libs/email';
|
||||
|
||||
describe('POST /groups/:groupId/removeMember/:memberId', () => {
|
||||
let leader;
|
||||
@@ -60,6 +61,14 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
|
||||
});
|
||||
|
||||
context('Guilds', () => {
|
||||
beforeEach(() => {
|
||||
sandbox.spy(email, 'sendTxn');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('can remove other members', async () => {
|
||||
await leader.post(`/groups/${guild._id}/removeMember/${member._id}`);
|
||||
let memberRemoved = await member.get('/user');
|
||||
@@ -80,6 +89,22 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
|
||||
|
||||
expect(_.findIndex(invitedUserWithoutInvite.invitations.guilds, {id: guild._id})).eql(-1);
|
||||
});
|
||||
|
||||
it('sends email to user with rescinded invite', async () => {
|
||||
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');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
context('Party', () => {
|
||||
@@ -87,6 +112,7 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
|
||||
let partyLeader;
|
||||
let partyInvitedUser;
|
||||
let partyMember;
|
||||
let removedMember;
|
||||
|
||||
beforeEach(async () => {
|
||||
let { group, groupLeader, invitees, members } = await createAndPopulateGroup({
|
||||
@@ -96,13 +122,19 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
|
||||
privacy: 'private',
|
||||
},
|
||||
invites: 1,
|
||||
members: 1,
|
||||
members: 2,
|
||||
});
|
||||
|
||||
party = group;
|
||||
partyLeader = groupLeader;
|
||||
partyInvitedUser = invitees[0];
|
||||
partyMember = members[0];
|
||||
removedMember = members[1];
|
||||
sandbox.spy(email, 'sendTxn');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('can remove other members', async () => {
|
||||
@@ -129,6 +161,18 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
|
||||
expect(invitedUserWithoutInvite.invitations.party).to.be.empty;
|
||||
});
|
||||
|
||||
it('removes new messages from a member who is removed', async () => {
|
||||
await partyLeader.post(`/groups/${party._id}/chat`, { message: 'Some message' });
|
||||
await removedMember.sync();
|
||||
|
||||
expect(removedMember.newMessages[party._id]).to.not.be.empty;
|
||||
|
||||
await partyLeader.post(`/groups/${party._id}/removeMember/${removedMember._id}`);
|
||||
await removedMember.sync();
|
||||
|
||||
expect(removedMember.newMessages[party._id]).to.be.empty;
|
||||
});
|
||||
|
||||
it('removes user from quest when removing user from party after quest starts', async () => {
|
||||
let petQuest = 'whale';
|
||||
await partyLeader.update({
|
||||
@@ -173,5 +217,21 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
|
||||
expect(party.quest.members[partyLeader._id]).to.be.true;
|
||||
expect(party.quest.members[partyMember._id]).to.not.exist;
|
||||
});
|
||||
|
||||
it('sends email to user with rescinded invite', async () => {
|
||||
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');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -57,11 +57,27 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns empty when uuids is empty', async () => {
|
||||
it('returns an error when uuids and emails are empty', async () => {
|
||||
await expect(inviter.post(`/groups/${group._id}/invite`, {
|
||||
emails: [],
|
||||
uuids: [],
|
||||
}))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('inviteMustNotBeEmpty'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error when uuids is empty and emails is not passed', async () => {
|
||||
await expect(inviter.post(`/groups/${group._id}/invite`, {
|
||||
uuids: [],
|
||||
}))
|
||||
.to.eventually.be.empty;
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('inviteMissingUuid'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error when there are more than INVITES_LIMIT uuids', async () => {
|
||||
@@ -159,11 +175,15 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns empty when emails is an empty array', async () => {
|
||||
it('returns an error when emails is empty and uuids is not passed', async () => {
|
||||
await expect(inviter.post(`/groups/${group._id}/invite`, {
|
||||
emails: [],
|
||||
}))
|
||||
.to.eventually.be.empty;
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('inviteMissingEmail'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error when there are more than INVITES_LIMIT emails', async () => {
|
||||
@@ -280,6 +300,26 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
message: t('userAlreadyInGroup'),
|
||||
});
|
||||
});
|
||||
|
||||
// @TODO: Add this after we are able to mock the group plan route
|
||||
xit('returns an error when a non-leader invites to a group plan', async () => {
|
||||
let userToInvite = await generateUser();
|
||||
|
||||
let nonGroupLeader = await generateUser();
|
||||
await inviter.post(`/groups/${group._id}/invite`, {
|
||||
uuids: [nonGroupLeader._id],
|
||||
});
|
||||
await nonGroupLeader.post(`/groups/${group._id}/join`);
|
||||
|
||||
await expect(nonGroupLeader.post(`/groups/${group._id}/invite`, {
|
||||
uuids: [userToInvite._id],
|
||||
}))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('onlyGroupLeaderCanInviteToGroupPlan'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('party invites', () => {
|
||||
|
||||
44
test/api/v3/integration/members/GET-achievements.test.js
Normal file
44
test/api/v3/integration/members/GET-achievements.test.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('GET /members/:memberId/achievements', () => {
|
||||
let user;
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('validates req.params.memberId', async () => {
|
||||
await expect(user.get('/members/invalidUUID/achievements')).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns achievements based on given user', async () => {
|
||||
let member = await generateUser({
|
||||
contributor: {level: 1},
|
||||
backer: {tier: 3},
|
||||
});
|
||||
let achievementsRes = await user.get(`/members/${member._id}/achievements`);
|
||||
|
||||
expect(achievementsRes.special.achievements.contributor.earned).to.equal(true);
|
||||
expect(achievementsRes.special.achievements.contributor.value).to.equal(1);
|
||||
|
||||
expect(achievementsRes.special.achievements.kickstarter.earned).to.equal(true);
|
||||
expect(achievementsRes.special.achievements.kickstarter.value).to.equal(3);
|
||||
});
|
||||
|
||||
it('handles non-existing members', async () => {
|
||||
let dummyId = generateUUID();
|
||||
await expect(user.get(`/members/${dummyId}/achievements`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('userWithIDNotFound', {userId: dummyId}),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -35,8 +35,10 @@ describe('GET /members/:memberId', () => {
|
||||
'backer', 'contributor', 'auth', 'items', 'inbox',
|
||||
]);
|
||||
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
|
||||
expect(Object.keys(memberRes.preferences).sort()).to.eql(['size', 'hair', 'skin', 'shirt',
|
||||
'chair', 'costume', 'sleep', 'background'].sort());
|
||||
expect(Object.keys(memberRes.preferences).sort()).to.eql([
|
||||
'size', 'hair', 'skin', 'shirt',
|
||||
'chair', 'costume', 'sleep', 'background',
|
||||
].sort());
|
||||
|
||||
expect(memberRes.stats.maxMP).to.exist;
|
||||
expect(memberRes.stats.maxHealth).to.equal(common.maxHealth);
|
||||
|
||||
@@ -4,6 +4,14 @@ import {
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
function findMessage (messages, receiverId) {
|
||||
let message = _.find(messages, (inboxMessage) => {
|
||||
return inboxMessage.uuid === receiverId;
|
||||
});
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
describe('POST /members/transfer-gems', () => {
|
||||
let userToSendMessage;
|
||||
let receiver;
|
||||
@@ -116,19 +124,14 @@ describe('POST /members/transfer-gems', () => {
|
||||
let updatedReceiver = await receiver.get('/user');
|
||||
let updatedSender = await userToSendMessage.get('/user');
|
||||
|
||||
let sendersMessageInReceiversInbox = _.find(updatedReceiver.inbox.messages, (inboxMessage) => {
|
||||
return inboxMessage.uuid === userToSendMessage._id;
|
||||
});
|
||||
let sendersMessageInReceiversInbox = findMessage(updatedReceiver.inbox.messages, userToSendMessage._id);
|
||||
let sendersMessageInSendersInbox = findMessage(updatedSender.inbox.messages, receiver._id);
|
||||
|
||||
let sendersMessageInSendersInbox = _.find(updatedSender.inbox.messages, (inboxMessage) => {
|
||||
return inboxMessage.uuid === receiver._id;
|
||||
});
|
||||
|
||||
let messageSentContent = t('privateMessageGiftIntro', {
|
||||
let messageSentContent = t('privateMessageGiftGemsMessage', {
|
||||
receiverName: receiver.profile.name,
|
||||
senderName: userToSendMessage.profile.name,
|
||||
gemAmount,
|
||||
});
|
||||
messageSentContent += t('privateMessageGiftGemsMessage', {gemAmount});
|
||||
messageSentContent = `\`${messageSentContent}\` `;
|
||||
messageSentContent += message;
|
||||
|
||||
@@ -150,19 +153,14 @@ describe('POST /members/transfer-gems', () => {
|
||||
let updatedReceiver = await receiver.get('/user');
|
||||
let updatedSender = await userToSendMessage.get('/user');
|
||||
|
||||
let sendersMessageInReceiversInbox = _.find(updatedReceiver.inbox.messages, (inboxMessage) => {
|
||||
return inboxMessage.uuid === userToSendMessage._id;
|
||||
});
|
||||
let sendersMessageInReceiversInbox = findMessage(updatedReceiver.inbox.messages, userToSendMessage._id);
|
||||
let sendersMessageInSendersInbox = findMessage(updatedSender.inbox.messages, receiver._id);
|
||||
|
||||
let sendersMessageInSendersInbox = _.find(updatedSender.inbox.messages, (inboxMessage) => {
|
||||
return inboxMessage.uuid === receiver._id;
|
||||
});
|
||||
|
||||
let messageSentContent = t('privateMessageGiftIntro', {
|
||||
let messageSentContent = t('privateMessageGiftGemsMessage', {
|
||||
receiverName: receiver.profile.name,
|
||||
senderName: userToSendMessage.profile.name,
|
||||
gemAmount,
|
||||
});
|
||||
messageSentContent += t('privateMessageGiftGemsMessage', {gemAmount});
|
||||
messageSentContent = `\`${messageSentContent}\` `;
|
||||
|
||||
expect(sendersMessageInReceiversInbox).to.exist;
|
||||
@@ -173,4 +171,40 @@ describe('POST /members/transfer-gems', () => {
|
||||
expect(sendersMessageInSendersInbox.text).to.equal(messageSentContent);
|
||||
expect(updatedSender.balance).to.equal(0);
|
||||
});
|
||||
|
||||
it('sends transfer gems message in each participant\'s language', async () => {
|
||||
await receiver.update({
|
||||
'preferences.language': 'es',
|
||||
});
|
||||
await userToSendMessage.update({
|
||||
'preferences.language': 'cs',
|
||||
});
|
||||
await userToSendMessage.post('/members/transfer-gems', {
|
||||
gemAmount,
|
||||
toUserId: receiver._id,
|
||||
});
|
||||
|
||||
let updatedReceiver = await receiver.get('/user');
|
||||
let updatedSender = await userToSendMessage.get('/user');
|
||||
|
||||
let sendersMessageInReceiversInbox = findMessage(updatedReceiver.inbox.messages, userToSendMessage._id);
|
||||
let sendersMessageInSendersInbox = findMessage(updatedSender.inbox.messages, receiver._id);
|
||||
|
||||
let [receieversMessageContent, sendersMessageContent] = ['es', 'cs'].map((lang) => {
|
||||
let messageContent = t('privateMessageGiftGemsMessage', {
|
||||
receiverName: receiver.profile.name,
|
||||
senderName: userToSendMessage.profile.name,
|
||||
gemAmount,
|
||||
}, lang);
|
||||
|
||||
return `\`${messageContent}\` `;
|
||||
});
|
||||
|
||||
expect(sendersMessageInReceiversInbox).to.exist;
|
||||
expect(sendersMessageInReceiversInbox.text).to.equal(receieversMessageContent);
|
||||
|
||||
expect(sendersMessageInSendersInbox).to.exist;
|
||||
expect(sendersMessageInSendersInbox.text).to.equal(sendersMessageContent);
|
||||
expect(updatedSender.balance).to.equal(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('POST /notifications/:notificationId/read', () => {
|
||||
let user;
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('errors when notification is not found', async () => {
|
||||
let dummyId = generateUUID();
|
||||
await expect(user.post(`/notifications/${dummyId}/read`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('messageNotificationNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
xit('removes a notification', async () => {
|
||||
});
|
||||
});
|
||||
@@ -10,13 +10,11 @@ describe('payments - amazon - #createOrderReferenceId', () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('verifies billingAgreementId', async (done) => {
|
||||
try {
|
||||
await user.post(endpoint);
|
||||
} catch (e) {
|
||||
// Parameter AWSAccessKeyId cannot be empty.
|
||||
expect(e.error).to.eql('BadRequest');
|
||||
done();
|
||||
}
|
||||
it('verifies billingAgreementId', async () => {
|
||||
await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'Missing req.body.billingAgreementId',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import { model as Group } from '../../../../../website/server/models/group';
|
||||
|
||||
describe('POST /groups/:groupId/quests/abort', () => {
|
||||
let questingGroup;
|
||||
@@ -85,6 +86,7 @@ describe('POST /groups/:groupId/quests/abort', () => {
|
||||
});
|
||||
|
||||
it('aborts a quest', async () => {
|
||||
sandbox.stub(Group.prototype, 'sendChat');
|
||||
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
|
||||
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
|
||||
await partyMembers[1].post(`/groups/${questingGroup._id}/quests/accept`);
|
||||
@@ -123,5 +125,8 @@ describe('POST /groups/:groupId/quests/abort', () => {
|
||||
},
|
||||
members: {},
|
||||
});
|
||||
expect(Group.prototype.sendChat).to.be.calledOnce;
|
||||
expect(Group.prototype.sendChat).to.be.calledWithMatch(/aborted the party quest Wail of the Whale.`/);
|
||||
Group.prototype.sendChat.restore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ describe('GET /shops/seasonal', () => {
|
||||
|
||||
expect(shop.identifier).to.equal('seasonalShop');
|
||||
expect(shop.text).to.eql(t('seasonalShop'));
|
||||
expect(shop.notes).to.eql(t('seasonalShopFallText'));
|
||||
expect(shop.notes).to.be.a('string');
|
||||
expect(shop.imageName).to.be.a('string');
|
||||
expect(shop.categories).to.be.an('array');
|
||||
});
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
generateGroup,
|
||||
sleep,
|
||||
generateChallenge,
|
||||
server,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('DELETE /tasks/:id', () => {
|
||||
let user;
|
||||
@@ -42,6 +47,77 @@ describe('DELETE /tasks/:id', () => {
|
||||
});
|
||||
});
|
||||
|
||||
context('sending task activity webhooks', () => {
|
||||
before(async () => {
|
||||
await server.start();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it('sends task activity webhooks if task is user owned', async () => {
|
||||
let uuid = generateUUID();
|
||||
|
||||
let task = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
|
||||
await user.post('/user/webhook', {
|
||||
url: `http://localhost:${server.port}/webhooks/${uuid}`,
|
||||
type: 'taskActivity',
|
||||
enabled: true,
|
||||
options: {
|
||||
created: false,
|
||||
deleted: true,
|
||||
},
|
||||
});
|
||||
|
||||
await user.del(`/tasks/${task.id}`);
|
||||
|
||||
await sleep();
|
||||
|
||||
let body = server.getWebhookData(uuid);
|
||||
|
||||
expect(body.type).to.eql('deleted');
|
||||
expect(body.task).to.eql(task);
|
||||
});
|
||||
|
||||
it('does not send task activity webhooks if task is not user owned', async () => {
|
||||
let uuid = generateUUID();
|
||||
|
||||
await user.update({
|
||||
balance: 10,
|
||||
});
|
||||
let guild = await generateGroup(user);
|
||||
let challenge = await generateChallenge(user, guild);
|
||||
|
||||
await user.post('/user/webhook', {
|
||||
url: `http://localhost:${server.port}/webhooks/${uuid}`,
|
||||
type: 'taskActivity',
|
||||
enabled: true,
|
||||
options: {
|
||||
created: false,
|
||||
deleted: true,
|
||||
},
|
||||
});
|
||||
|
||||
let challengeTask = await user.post(`/tasks/challenge/${challenge._id}`, {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
|
||||
await user.del(`/tasks/${challengeTask.id}`);
|
||||
|
||||
await sleep();
|
||||
|
||||
let body = server.getWebhookData(uuid);
|
||||
|
||||
expect(body).to.not.exist;
|
||||
});
|
||||
});
|
||||
|
||||
context('task cannot be deleted', () => {
|
||||
it('cannot delete a non-existant task', async () => {
|
||||
await expect(user.del('/tasks/550e8400-e29b-41d4-a716-446655440000')).to.eventually.be.rejected.and.eql({
|
||||
|
||||
@@ -115,7 +115,7 @@ describe('GET /tasks/user', () => {
|
||||
for (let i = 0; i < numberOfTodos; i++) {
|
||||
let id = todos[i]._id;
|
||||
|
||||
await user.post(`/tasks/${id}/score/up`); // eslint-disable-line babel/no-await-in-loop
|
||||
await user.post(`/tasks/${id}/score/up`); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
await user.sync();
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('POST /tasks/clearCompletedTodos', () => {
|
||||
it('deletes all completed todos except the ones from a challenge', async () => {
|
||||
it('deletes all completed todos except the ones from a challenge and group', async () => {
|
||||
let user = await generateUser({balance: 1});
|
||||
let guild = await generateGroup(user);
|
||||
let challenge = await generateChallenge(user, guild);
|
||||
@@ -24,12 +24,18 @@ describe('POST /tasks/clearCompletedTodos', () => {
|
||||
type: 'todo',
|
||||
});
|
||||
|
||||
let groupTask = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'todo 7',
|
||||
type: 'todo',
|
||||
});
|
||||
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
|
||||
|
||||
let tasks = await user.get('/tasks/user?type=todos');
|
||||
expect(tasks.length).to.equal(initialTodoCount + 6);
|
||||
expect(tasks.length).to.equal(initialTodoCount + 7);
|
||||
|
||||
for (let task of tasks) {
|
||||
if (['todo 2', 'todo 3', 'todo 6'].indexOf(task.text) !== -1) {
|
||||
await user.post(`/tasks/${task._id}/score/up`); // eslint-disable-line babel/no-await-in-loop
|
||||
await user.post(`/tasks/${task._id}/score/up`); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +44,6 @@ describe('POST /tasks/clearCompletedTodos', () => {
|
||||
let todos = await user.get('/tasks/user?type=todos');
|
||||
let allTodos = todos.concat(completedTodos);
|
||||
expect(allTodos.length).to.equal(initialTodoCount + 4); // + 6 - 3 completed (but one is from challenge)
|
||||
expect(allTodos[allTodos.length - 1].text).to.equal('todo 6');
|
||||
expect(allTodos[allTodos.length - 1].text).to.equal('todo 7');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {
|
||||
generateUser,
|
||||
sleep,
|
||||
translate as t,
|
||||
server,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
@@ -45,6 +47,40 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('sends task scored webhooks', async () => {
|
||||
let uuid = generateUUID();
|
||||
await server.start();
|
||||
|
||||
await user.post('/user/webhook', {
|
||||
url: `http://localhost:${server.port}/webhooks/${uuid}`,
|
||||
type: 'taskActivity',
|
||||
enabled: true,
|
||||
options: {
|
||||
created: false,
|
||||
scored: true,
|
||||
},
|
||||
});
|
||||
|
||||
let task = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
|
||||
await user.post(`/tasks/${task.id}/score/up`);
|
||||
|
||||
await sleep();
|
||||
|
||||
await server.close();
|
||||
|
||||
let body = server.getWebhookData(uuid);
|
||||
|
||||
expect(body.user).to.have.all.keys('_id', '_tmp', 'stats');
|
||||
expect(body.user.stats).to.have.all.keys('hp', 'mp', 'exp', 'gp', 'lvl', 'class', 'points', 'str', 'con', 'int', 'per', 'buffs', 'training', 'maxHealth', 'maxMP', 'toNextLevel');
|
||||
expect(body.task.id).to.eql(task.id);
|
||||
expect(body.direction).to.eql('up');
|
||||
expect(body.delta).to.be.greaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
context('todos', () => {
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import {
|
||||
generateUser,
|
||||
sleep,
|
||||
translate as t,
|
||||
server,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('POST /tasks/user', () => {
|
||||
let user;
|
||||
|
||||
before(async () => {
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
@@ -205,6 +207,71 @@ describe('POST /tasks/user', () => {
|
||||
});
|
||||
});
|
||||
|
||||
context('sending task activity webhooks', () => {
|
||||
before(async () => {
|
||||
await server.start();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it('sends task activity webhooks', async () => {
|
||||
let uuid = generateUUID();
|
||||
|
||||
await user.post('/user/webhook', {
|
||||
url: `http://localhost:${server.port}/webhooks/${uuid}`,
|
||||
type: 'taskActivity',
|
||||
enabled: true,
|
||||
options: {
|
||||
created: true,
|
||||
},
|
||||
});
|
||||
|
||||
let task = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
|
||||
await sleep();
|
||||
|
||||
let body = server.getWebhookData(uuid);
|
||||
|
||||
expect(body.task).to.eql(task);
|
||||
});
|
||||
|
||||
it('sends a task activity webhook for each task', async () => {
|
||||
let uuid = generateUUID();
|
||||
|
||||
await user.post('/user/webhook', {
|
||||
url: `http://localhost:${server.port}/webhooks/${uuid}`,
|
||||
type: 'taskActivity',
|
||||
enabled: true,
|
||||
options: {
|
||||
created: true,
|
||||
},
|
||||
});
|
||||
|
||||
let tasks = await user.post('/tasks/user', [{
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
}, {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
}]);
|
||||
|
||||
await sleep();
|
||||
|
||||
let taskBodies = [
|
||||
server.getWebhookData(uuid),
|
||||
server.getWebhookData(uuid),
|
||||
];
|
||||
|
||||
expect(taskBodies.find(body => body.task.id === tasks[0].id)).to.exist;
|
||||
expect(taskBodies.find(body => body.task.id === tasks[1].id)).to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
context('all types', () => {
|
||||
it('can create reminders', async () => {
|
||||
let id1 = generateUUID();
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
generateGroup,
|
||||
sleep,
|
||||
generateChallenge,
|
||||
server,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
@@ -73,6 +74,7 @@ describe('PUT /tasks/:id', () => {
|
||||
checklist: [
|
||||
{text: 123, completed: false},
|
||||
],
|
||||
collapseChecklist: false,
|
||||
});
|
||||
await sleep(2);
|
||||
|
||||
@@ -110,6 +112,7 @@ describe('PUT /tasks/:id', () => {
|
||||
{text: 123, completed: false},
|
||||
{text: 456, completed: true},
|
||||
],
|
||||
collapseChecklist: true,
|
||||
notes: 'new notes',
|
||||
attribute: 'per',
|
||||
tags: [challengeUserTaskId],
|
||||
@@ -142,6 +145,83 @@ describe('PUT /tasks/:id', () => {
|
||||
expect(savedChallengeUserTask.streak).to.equal(25);
|
||||
expect(savedChallengeUserTask.reminders.length).to.equal(2);
|
||||
expect(savedChallengeUserTask.checklist.length).to.equal(2);
|
||||
expect(savedChallengeUserTask.alias).to.equal('a-short-task-name');
|
||||
expect(savedChallengeUserTask.collapseChecklist).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
context('sending task activity webhooks', () => {
|
||||
before(async () => {
|
||||
await server.start();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it('sends task activity webhooks if task is user owned', async () => {
|
||||
let uuid = generateUUID();
|
||||
|
||||
await user.post('/user/webhook', {
|
||||
url: `http://localhost:${server.port}/webhooks/${uuid}`,
|
||||
type: 'taskActivity',
|
||||
enabled: true,
|
||||
options: {
|
||||
created: false,
|
||||
updated: true,
|
||||
},
|
||||
});
|
||||
|
||||
let task = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
|
||||
let updatedTask = await user.put(`/tasks/${task.id}`, {
|
||||
text: 'updated text',
|
||||
});
|
||||
|
||||
await sleep();
|
||||
|
||||
let body = server.getWebhookData(uuid);
|
||||
|
||||
expect(body.type).to.eql('updated');
|
||||
expect(body.task).to.eql(updatedTask);
|
||||
});
|
||||
|
||||
it('does not send task activity webhooks if task is not user owned', async () => {
|
||||
let uuid = generateUUID();
|
||||
|
||||
await user.update({
|
||||
balance: 10,
|
||||
});
|
||||
let guild = await generateGroup(user);
|
||||
let challenge = await generateChallenge(user, guild);
|
||||
|
||||
await user.post('/user/webhook', {
|
||||
url: `http://localhost:${server.port}/webhooks/${uuid}`,
|
||||
type: 'taskActivity',
|
||||
enabled: true,
|
||||
options: {
|
||||
created: false,
|
||||
updated: true,
|
||||
},
|
||||
});
|
||||
|
||||
let task = await user.post(`/tasks/challenge/${challenge._id}`, {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
|
||||
await user.put(`/tasks/${task.id}`, {
|
||||
text: 'updated text',
|
||||
});
|
||||
|
||||
await sleep();
|
||||
|
||||
let body = server.getWebhookData(uuid);
|
||||
|
||||
expect(body).to.not.exist;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -5,12 +5,17 @@ import {
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import { find } from 'lodash';
|
||||
|
||||
describe('POST /tasks/challenge/:challengeId', () => {
|
||||
let user;
|
||||
let guild;
|
||||
let challenge;
|
||||
|
||||
function findUserChallengeTask (memberTask) {
|
||||
return memberTask.challenge.id === challenge._id;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({balance: 1});
|
||||
guild = await generateGroup(user);
|
||||
@@ -88,6 +93,9 @@ describe('POST /tasks/challenge/:challengeId', () => {
|
||||
});
|
||||
let challengeWithTask = await user.get(`/challenges/${challenge._id}`);
|
||||
|
||||
let memberTasks = await user.get('/tasks/user');
|
||||
let userChallengeTask = find(memberTasks, findUserChallengeTask);
|
||||
|
||||
expect(challengeWithTask.tasksOrder.habits.indexOf(task._id)).to.be.above(-1);
|
||||
expect(task.challenge.id).to.equal(challenge._id);
|
||||
expect(task.text).to.eql('test habit');
|
||||
@@ -95,6 +103,8 @@ describe('POST /tasks/challenge/:challengeId', () => {
|
||||
expect(task.type).to.eql('habit');
|
||||
expect(task.up).to.eql(false);
|
||||
expect(task.down).to.eql(true);
|
||||
|
||||
expect(userChallengeTask.notes).to.eql(task.notes);
|
||||
});
|
||||
|
||||
it('creates a todo', async () => {
|
||||
@@ -105,11 +115,16 @@ describe('POST /tasks/challenge/:challengeId', () => {
|
||||
});
|
||||
let challengeWithTask = await user.get(`/challenges/${challenge._id}`);
|
||||
|
||||
let memberTasks = await user.get('/tasks/user');
|
||||
let userChallengeTask = find(memberTasks, findUserChallengeTask);
|
||||
|
||||
expect(challengeWithTask.tasksOrder.todos.indexOf(task._id)).to.be.above(-1);
|
||||
expect(task.challenge.id).to.equal(challenge._id);
|
||||
expect(task.text).to.eql('test todo');
|
||||
expect(task.notes).to.eql('1976');
|
||||
expect(task.type).to.eql('todo');
|
||||
|
||||
expect(userChallengeTask.notes).to.eql(task.notes);
|
||||
});
|
||||
|
||||
it('creates a daily', async () => {
|
||||
@@ -124,6 +139,9 @@ describe('POST /tasks/challenge/:challengeId', () => {
|
||||
});
|
||||
let challengeWithTask = await user.get(`/challenges/${challenge._id}`);
|
||||
|
||||
let memberTasks = await user.get('/tasks/user');
|
||||
let userChallengeTask = find(memberTasks, findUserChallengeTask);
|
||||
|
||||
expect(challengeWithTask.tasksOrder.dailys.indexOf(task._id)).to.be.above(-1);
|
||||
expect(task.challenge.id).to.equal(challenge._id);
|
||||
expect(task.text).to.eql('test daily');
|
||||
@@ -132,5 +150,7 @@ describe('POST /tasks/challenge/:challengeId', () => {
|
||||
expect(task.frequency).to.eql('daily');
|
||||
expect(task.everyX).to.eql(5);
|
||||
expect(new Date(task.startDate)).to.eql(now);
|
||||
|
||||
expect(userChallengeTask.notes).to.eql(task.notes);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -69,4 +69,48 @@ describe('DELETE /tasks/:id', () => {
|
||||
expect(syncedTask.group.broken).to.equal('TASK_DELETED');
|
||||
expect(member2SyncedTask.group.broken).to.equal('TASK_DELETED');
|
||||
});
|
||||
|
||||
it('prevents a user from deleting a task they are assigned to', async () => {
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
await expect(member.del(`/tasks/${syncedTask._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('cantDeleteAssignedGroupTasks'),
|
||||
});
|
||||
});
|
||||
|
||||
it('allows a user to delete a broken task', async () => {
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
await user.del(`/tasks/${task._id}`);
|
||||
|
||||
await member.del(`/tasks/${syncedTask._id}`);
|
||||
|
||||
await expect(member.get(`/tasks/${syncedTask._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: 'Task not found.',
|
||||
});
|
||||
});
|
||||
|
||||
it('allows a user to delete a task after leaving a group', async () => {
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
await member.post(`/groups/${guild._id}/leave`);
|
||||
|
||||
await member.del(`/tasks/${syncedTask._id}`);
|
||||
|
||||
await expect(member.get(`/tasks/${syncedTask._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: 'Task not found.',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import { find } from 'lodash';
|
||||
|
||||
describe('GET /approvals/group/:groupId', () => {
|
||||
let user, guild, member, task, syncedTask;
|
||||
|
||||
function findAssignedTask (memberTask) {
|
||||
return memberTask.group.id === guild._id;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
let {group, members, groupLeader} = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Test Guild',
|
||||
type: 'guild',
|
||||
},
|
||||
members: 1,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
user = groupLeader;
|
||||
member = members[0];
|
||||
|
||||
task = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
requiresApproval: true,
|
||||
});
|
||||
|
||||
await user.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
try {
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-empty
|
||||
}
|
||||
});
|
||||
|
||||
it('errors when user is not the group leader', async () => {
|
||||
await expect(member.get(`/approvals/group/${guild._id}`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('onlyGroupLeaderCanEditTasks'),
|
||||
});
|
||||
});
|
||||
|
||||
it('gets a list of task that need approval', async () => {
|
||||
let approvals = await user.get(`/approvals/group/${guild._id}`);
|
||||
expect(approvals[0]._id).to.equal(syncedTask._id);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import {
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import { find } from 'lodash';
|
||||
|
||||
describe('POST /tasks/:id/approve/:userId', () => {
|
||||
let user, guild, member, 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: 1,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
user = groupLeader;
|
||||
member = members[0];
|
||||
|
||||
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}/approve/${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}/approve/${member._id}`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('onlyGroupLeaderCanEditTasks'),
|
||||
});
|
||||
});
|
||||
|
||||
it('approves an assigned user', async () => {
|
||||
await user.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
await user.post(`/tasks/${task._id}/approve/${member._id}`);
|
||||
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
await member.sync();
|
||||
|
||||
expect(member.notifications.length).to.equal(2);
|
||||
expect(member.notifications[0].type).to.equal('GROUP_TASK_APPROVED');
|
||||
expect(member.notifications[0].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
|
||||
expect(member.notifications[1].type).to.equal('SCORED_TASK');
|
||||
expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
|
||||
|
||||
expect(syncedTask.group.approval.approved).to.be.true;
|
||||
expect(syncedTask.group.approval.approvingUser).to.equal(user._id);
|
||||
expect(syncedTask.group.approval.dateApproved).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
import {
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import { find } from 'lodash';
|
||||
|
||||
describe('POST /tasks/:id/score/:direction', () => {
|
||||
let user, guild, member, 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: 1,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
user = groupLeader;
|
||||
member = members[0];
|
||||
|
||||
task = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
requiresApproval: true,
|
||||
});
|
||||
|
||||
await user.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
});
|
||||
|
||||
it('prevents user from scoring a task that needs to be approved', async () => {
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
let updatedTask = await member.get(`/tasks/${syncedTask._id}`);
|
||||
|
||||
await user.sync();
|
||||
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
expect(user.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(user.notifications[0].data.message).to.equal(t('userHasRequestedTaskApproval', {
|
||||
user: member.auth.local.username,
|
||||
taskName: updatedTask.text,
|
||||
}));
|
||||
expect(user.notifications[0].data.groupId).to.equal(guild._id);
|
||||
|
||||
expect(updatedTask.group.approval.requested).to.equal(true);
|
||||
expect(updatedTask.group.approval.requestedDate).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
|
||||
});
|
||||
|
||||
it('errors when approval has already been requested', async () => {
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskRequiresApproval'),
|
||||
});
|
||||
});
|
||||
|
||||
it('allows a user to score an apporoved task', async () => {
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
await user.post(`/tasks/${task._id}/approve/${member._id}`);
|
||||
|
||||
await member.post(`/tasks/${syncedTask._id}/score/up`);
|
||||
let updatedTask = await member.get(`/tasks/${syncedTask._id}`);
|
||||
|
||||
expect(updatedTask.completed).to.equal(true);
|
||||
expect(updatedTask.dateCompleted).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
|
||||
});
|
||||
});
|
||||
@@ -74,7 +74,7 @@ describe('POST /tasks/:taskId', () => {
|
||||
});
|
||||
|
||||
it('returns error when non leader tries to create a task', async () => {
|
||||
await expect(member.post(`/tasks/${task._id}/assign/${member._id}`))
|
||||
await expect(member2.post(`/tasks/${task._id}/assign/${member._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
@@ -82,6 +82,26 @@ describe('POST /tasks/:taskId', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('allows user to assign themselves (claim)', async () => {
|
||||
await member.post(`/tasks/${task._id}/assign/${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.contain(member._id);
|
||||
expect(syncedTask).to.exist;
|
||||
});
|
||||
|
||||
it('sends a message to the group when a user claims a task', async () => {
|
||||
await member.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
|
||||
let updateGroup = await user.get(`/groups/${guild._id}`);
|
||||
|
||||
expect(updateGroup.chat[0].text).to.equal(t('userIsClamingTask', {username: member.profile.name, task: task.text}));
|
||||
expect(updateGroup.chat[0].uuid).to.equal('system');
|
||||
});
|
||||
|
||||
it('assigns a task to a user', async () => {
|
||||
await user.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import {
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../../../helpers/api-integration/v3';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('DELETE group /tasks/:taskId/checklist/:itemId', () => {
|
||||
let user, guild, task;
|
||||
|
||||
before(async () => {
|
||||
let {group, groupLeader} = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Test Guild',
|
||||
type: 'guild',
|
||||
},
|
||||
members: 2,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
user = groupLeader;
|
||||
});
|
||||
|
||||
it('deletes a checklist item', async () => {
|
||||
task = await user.post(`/tasks/group/${guild._id}`, {
|
||||
type: 'daily',
|
||||
text: 'Daily with checklist',
|
||||
});
|
||||
|
||||
let savedTask = await user.post(`/tasks/${task._id}/checklist`, {text: 'Checklist Item 1', completed: false});
|
||||
|
||||
await user.del(`/tasks/${task._id}/checklist/${savedTask.checklist[0].id}`);
|
||||
savedTask = await user.get(`/tasks/group/${guild._id}`);
|
||||
|
||||
expect(savedTask[0].checklist.length).to.equal(0);
|
||||
});
|
||||
|
||||
it('does not work with habits', async () => {
|
||||
let habit = await user.post(`/tasks/group/${guild._id}`, {
|
||||
type: 'habit',
|
||||
text: 'habit with checklist',
|
||||
});
|
||||
|
||||
await expect(user.del(`/tasks/${habit._id}/checklist/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('checklistOnlyDailyTodo'),
|
||||
});
|
||||
});
|
||||
|
||||
it('does not work with rewards', async () => {
|
||||
let reward = await user.post(`/tasks/group/${guild._id}`, {
|
||||
type: 'reward',
|
||||
text: 'reward with checklist',
|
||||
});
|
||||
|
||||
await expect(user.del(`/tasks/${reward._id}/checklist/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('checklistOnlyDailyTodo'),
|
||||
});
|
||||
});
|
||||
|
||||
it('fails on task not found', async () => {
|
||||
await expect(user.del(`/tasks/${generateUUID()}/checklist/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('taskNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('fails on checklist item not found', async () => {
|
||||
let createdTask = await user.post(`/tasks/group/${guild._id}`, {
|
||||
type: 'daily',
|
||||
text: 'daily with checklist',
|
||||
});
|
||||
|
||||
await expect(user.del(`/tasks/${createdTask._id}/checklist/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('checklistItemNotFound'),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../../../helpers/api-integration/v3';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('POST group /tasks/:taskId/checklist/', () => {
|
||||
let user, guild, task;
|
||||
|
||||
before(async () => {
|
||||
let {group, groupLeader} = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Test Guild',
|
||||
type: 'guild',
|
||||
},
|
||||
members: 2,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
user = groupLeader;
|
||||
});
|
||||
|
||||
it('adds a checklist item to a task', async () => {
|
||||
task = await user.post(`/tasks/group/${guild._id}`, {
|
||||
type: 'daily',
|
||||
text: 'Daily with checklist',
|
||||
});
|
||||
|
||||
await user.post(`/tasks/${task._id}/checklist`, {
|
||||
text: 'Checklist Item 1',
|
||||
ignored: false,
|
||||
_id: 123,
|
||||
});
|
||||
|
||||
let updatedTasks = await user.get(`/tasks/group/${guild._id}`);
|
||||
let updatedTask = updatedTasks[0];
|
||||
|
||||
expect(updatedTask.checklist.length).to.equal(1);
|
||||
expect(updatedTask.checklist[0].text).to.equal('Checklist Item 1');
|
||||
expect(updatedTask.checklist[0].completed).to.equal(false);
|
||||
expect(updatedTask.checklist[0].id).to.be.a('string');
|
||||
expect(updatedTask.checklist[0].id).to.not.equal('123');
|
||||
expect(updatedTask.checklist[0].ignored).to.be.an('undefined');
|
||||
});
|
||||
|
||||
it('does not add a checklist to habits', async () => {
|
||||
let habit = await user.post(`/tasks/group/${guild._id}`, {
|
||||
type: 'habit',
|
||||
text: 'habit with checklist',
|
||||
});
|
||||
|
||||
await expect(user.post(`/tasks/${habit._id}/checklist`, {
|
||||
text: 'Checklist Item 1',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('checklistOnlyDailyTodo'),
|
||||
});
|
||||
});
|
||||
|
||||
it('does not add a checklist to rewards', async () => {
|
||||
let reward = await user.post(`/tasks/group/${guild._id}`, {
|
||||
type: 'reward',
|
||||
text: 'reward with checklist',
|
||||
});
|
||||
|
||||
await expect(user.post(`/tasks/${reward._id}/checklist`, {
|
||||
text: 'Checklist Item 1',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('checklistOnlyDailyTodo'),
|
||||
});
|
||||
});
|
||||
|
||||
it('fails on task not found', async () => {
|
||||
await expect(user.post(`/tasks/${generateUUID()}/checklist`, {
|
||||
text: 'Checklist Item 1',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('taskNotFound'),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../../../helpers/api-integration/v3';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('PUT group /tasks/:taskId/checklist/:itemId', () => {
|
||||
let user, guild, task;
|
||||
|
||||
before(async () => {
|
||||
let {group, groupLeader} = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Test Guild',
|
||||
type: 'guild',
|
||||
},
|
||||
members: 2,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
user = groupLeader;
|
||||
});
|
||||
|
||||
it('updates a checklist item', async () => {
|
||||
task = await user.post(`/tasks/group/${guild._id}`, {
|
||||
type: 'daily',
|
||||
text: 'Daily with checklist',
|
||||
});
|
||||
|
||||
let savedTask = await user.post(`/tasks/${task._id}/checklist`, {
|
||||
text: 'Checklist Item 1',
|
||||
completed: false,
|
||||
});
|
||||
|
||||
savedTask = await user.put(`/tasks/${task._id}/checklist/${savedTask.checklist[0].id}`, {
|
||||
text: 'updated',
|
||||
completed: true,
|
||||
_id: 123, // ignored
|
||||
});
|
||||
|
||||
expect(savedTask.checklist.length).to.equal(1);
|
||||
expect(savedTask.checklist[0].text).to.equal('updated');
|
||||
expect(savedTask.checklist[0].completed).to.equal(true);
|
||||
expect(savedTask.checklist[0].id).to.not.equal('123');
|
||||
});
|
||||
|
||||
it('fails on habits', async () => {
|
||||
let habit = await user.post(`/tasks/group/${guild._id}`, {
|
||||
type: 'habit',
|
||||
text: 'habit with checklist',
|
||||
});
|
||||
|
||||
await expect(user.put(`/tasks/${habit._id}/checklist/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('checklistOnlyDailyTodo'),
|
||||
});
|
||||
});
|
||||
|
||||
it('fails on rewards', async () => {
|
||||
let reward = await user.post(`/tasks/group/${guild._id}`, {
|
||||
type: 'reward',
|
||||
text: 'reward with checklist',
|
||||
});
|
||||
|
||||
await expect(user.put(`/tasks/${reward._id}/checklist/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('checklistOnlyDailyTodo'),
|
||||
});
|
||||
});
|
||||
|
||||
it('fails on task not found', async () => {
|
||||
await expect(user.put(`/tasks/${generateUUID()}/checklist/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('taskNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('fails on checklist item not found', async () => {
|
||||
let createdTask = await user.post(`/tasks/group/${guild._id}`, {
|
||||
type: 'daily',
|
||||
text: 'daily with checklist',
|
||||
});
|
||||
|
||||
await expect(user.put(`/tasks/${createdTask._id}/checklist/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('checklistItemNotFound'),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../../../helpers/api-integration/v3';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
// Currently we do not support adding tags to group original tasks, but if we do in the future, these tests will check
|
||||
xdescribe('DELETE group /tasks/:taskId/tags/:tagId', () => {
|
||||
let user, guild, task;
|
||||
|
||||
before(async () => {
|
||||
let {group, groupLeader} = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Test Guild',
|
||||
type: 'guild',
|
||||
},
|
||||
members: 2,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
user = groupLeader;
|
||||
});
|
||||
|
||||
it('removes a tag from a task', async () => {
|
||||
task = await user.post(`/tasks/group/${guild._id}`, {
|
||||
type: 'habit',
|
||||
text: 'Task with tag',
|
||||
});
|
||||
|
||||
let tag = await user.post('/tags', {name: 'Tag 1'});
|
||||
|
||||
await user.post(`/tasks/${task._id}/tags/${tag.id}`);
|
||||
await user.del(`/tasks/${task._id}/tags/${tag.id}`);
|
||||
|
||||
let updatedTask = await user.get(`/tasks/group/${guild._id}`);
|
||||
|
||||
expect(updatedTask[0].tags.length).to.equal(0);
|
||||
});
|
||||
|
||||
it('only deletes existing tags', async () => {
|
||||
let createdTask = await user.post(`/tasks/group/${guild._id}`, {
|
||||
type: 'habit',
|
||||
text: 'Task with tag',
|
||||
});
|
||||
|
||||
await expect(user.del(`/tasks/${createdTask._id}/tags/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('tagNotFound'),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../../../helpers/api-integration/v3';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
// Currently we do not support adding tags to group original tasks, but if we do in the future, these tests will check
|
||||
xdescribe('POST group /tasks/:taskId/tags/:tagId', () => {
|
||||
let user, guild, task;
|
||||
|
||||
before(async () => {
|
||||
let {group, groupLeader} = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Test Guild',
|
||||
type: 'guild',
|
||||
},
|
||||
members: 2,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
user = groupLeader;
|
||||
});
|
||||
|
||||
it('adds a tag to a task', async () => {
|
||||
task = await user.post(`/tasks/group/${guild._id}`, {
|
||||
type: 'habit',
|
||||
text: 'Task with tag',
|
||||
});
|
||||
|
||||
let tag = await user.post('/tags', {name: 'Tag 1'});
|
||||
let savedTask = await user.post(`/tasks/${task._id}/tags/${tag.id}`);
|
||||
|
||||
expect(savedTask.tags[0]).to.equal(tag.id);
|
||||
});
|
||||
|
||||
it('does not add a tag to a task twice', async () => {
|
||||
task = await user.post(`/tasks/group/${guild._id}`, {
|
||||
type: 'habit',
|
||||
text: 'Task with tag',
|
||||
});
|
||||
|
||||
let tag = await user.post('/tags', {name: 'Tag 1'});
|
||||
|
||||
await user.post(`/tasks/${task._id}/tags/${tag.id}`);
|
||||
|
||||
await expect(user.post(`/tasks/${task._id}/tags/${tag.id}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('alreadyTagged'),
|
||||
});
|
||||
});
|
||||
|
||||
it('does not add a non existing tag to a task', async () => {
|
||||
task = await user.post(`/tasks/group/${guild._id}`, {
|
||||
type: 'habit',
|
||||
text: 'Task with tag',
|
||||
});
|
||||
|
||||
await expect(user.post(`/tasks/${task._id}/tags/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
let user;
|
||||
let endpoint = '/user/webhook';
|
||||
|
||||
describe('DELETE /user/webhook', () => {
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('succeeds', async () => {
|
||||
let id = 'some-id';
|
||||
user.preferences.webhooks[id] = { url: 'http://some-url.com', enabled: true };
|
||||
await user.sync();
|
||||
expect(user.preferences.webhooks).to.eql({});
|
||||
let response = await user.del(`${endpoint}/${id}`);
|
||||
expect(response).to.eql({});
|
||||
await user.sync();
|
||||
expect(user.preferences.webhooks).to.eql({});
|
||||
});
|
||||
});
|
||||
@@ -13,12 +13,19 @@ describe('GET /user/anonymized', () => {
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser();
|
||||
await user.update({ newMessages: ['some', 'new', 'messages'], 'profile.name': 'profile', 'purchased.plan': 'purchased plan',
|
||||
contributor: 'contributor', invitations: 'invitations', 'items.special.nyeReceived': 'some', 'items.special.valentineReceived': 'some',
|
||||
webhooks: 'some', 'achievements.challenges': 'some',
|
||||
'inbox.messages': [{ text: 'some text' }],
|
||||
tags: [{ name: 'some name', challenge: 'some challenge' }],
|
||||
});
|
||||
await user.update({
|
||||
newMessages: ['some', 'new', 'messages'],
|
||||
'profile.name': 'profile',
|
||||
'purchased.plan': 'purchased plan',
|
||||
contributor: 'contributor',
|
||||
invitations: 'invitations',
|
||||
'items.special.nyeReceived': 'some',
|
||||
'items.special.valentineReceived': 'some',
|
||||
webhooks: [{url: 'https://somurl.com'}],
|
||||
'achievements.challenges': 'some',
|
||||
'inbox.messages': [{ text: 'some text' }],
|
||||
tags: [{ name: 'some name', challenge: 'some challenge' }],
|
||||
});
|
||||
|
||||
await generateHabit({ userId: user._id });
|
||||
await generateHabit({ userId: user._id, text: generateUUID() });
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
let user;
|
||||
let endpoint = '/user/webhook';
|
||||
|
||||
describe('POST /user/webhook', () => {
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('validates', async () => {
|
||||
await expect(user.post(endpoint, { enabled: true })).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidUrl'),
|
||||
});
|
||||
});
|
||||
|
||||
it('successfully adds the webhook', async () => {
|
||||
expect(user.preferences.webhooks).to.eql({});
|
||||
let response = await user.post(endpoint, { enabled: true, url: 'http://some-url.com'});
|
||||
expect(response.id).to.exist;
|
||||
await user.sync();
|
||||
expect(user.preferences.webhooks).to.not.eql({});
|
||||
});
|
||||
});
|
||||
@@ -62,15 +62,6 @@ describe('POST /user/buy/:key', () => {
|
||||
await user.post(`/user/buy/${key}`);
|
||||
await user.sync();
|
||||
|
||||
expect(user.items.gear.owned).to.eql({
|
||||
armor_warrior_1: true,
|
||||
eyewear_special_blackTopFrame: true,
|
||||
eyewear_special_blueTopFrame: true,
|
||||
eyewear_special_greenTopFrame: true,
|
||||
eyewear_special_pinkTopFrame: true,
|
||||
eyewear_special_redTopFrame: true,
|
||||
eyewear_special_whiteTopFrame: true,
|
||||
eyewear_special_yellowTopFrame: true,
|
||||
});
|
||||
expect(user.items.gear.owned.armor_warrior_1).to.eql(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,15 +31,6 @@ describe('POST /user/buy-gear/:key', () => {
|
||||
await user.post(`/user/buy-gear/${key}`);
|
||||
await user.sync();
|
||||
|
||||
expect(user.items.gear.owned).to.eql({
|
||||
armor_warrior_1: true,
|
||||
eyewear_special_blackTopFrame: true,
|
||||
eyewear_special_blueTopFrame: true,
|
||||
eyewear_special_greenTopFrame: true,
|
||||
eyewear_special_pinkTopFrame: true,
|
||||
eyewear_special_redTopFrame: true,
|
||||
eyewear_special_whiteTopFrame: true,
|
||||
eyewear_special_yellowTopFrame: true,
|
||||
});
|
||||
expect(user.items.gear.owned.armor_warrior_1).to.eql(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import { find } from 'lodash';
|
||||
|
||||
describe('POST /user/class/cast/:spellId', () => {
|
||||
let user;
|
||||
@@ -120,6 +121,31 @@ describe('POST /user/class/cast/:spellId', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if a group task was targeted', async () => {
|
||||
let {group, groupLeader} = await createAndPopulateGroup();
|
||||
|
||||
let groupTask = await groupLeader.post(`/tasks/group/${group._id}`, {
|
||||
text: 'todo group',
|
||||
type: 'todo',
|
||||
});
|
||||
await groupLeader.post(`/tasks/${groupTask._id}/assign/${groupLeader._id}`);
|
||||
let memberTasks = await groupLeader.get('/tasks/user');
|
||||
let syncedGroupTask = find(memberTasks, function findAssignedTask (memberTask) {
|
||||
return memberTask.group.id === group._id;
|
||||
});
|
||||
|
||||
await groupLeader.update({'stats.class': 'rogue', 'stats.lvl': 11});
|
||||
await sleep(0.5);
|
||||
await groupLeader.sync();
|
||||
|
||||
await expect(groupLeader.post(`/user/class/cast/pickPocket?targetId=${syncedGroupTask._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('groupTasksNoCast'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if targeted party member doesn\'t exist', async () => {
|
||||
let {groupLeader} = await createAndPopulateGroup({
|
||||
groupDetails: { type: 'party', privacy: 'private' },
|
||||
|
||||
@@ -20,6 +20,6 @@ describe('POST /user/purchase-hourglass/:type/:key', () => {
|
||||
|
||||
expect(response.message).to.eql(t('hourglassPurchase'));
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
expect(user.items.pets).to.eql({'MantisShrimp-Base': 5});
|
||||
expect(user.items.pets['MantisShrimp-Base']).to.eql(5);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
generateChallenge,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { find } from 'lodash';
|
||||
|
||||
describe('POST /user/reset', () => {
|
||||
let user;
|
||||
@@ -86,19 +87,34 @@ describe('POST /user/reset', () => {
|
||||
expect(user.tasksOrder.rewards).to.be.empty;
|
||||
});
|
||||
|
||||
it('does not delete challenge tasks', async () => {
|
||||
it('does not delete challenge or group tasks', async () => {
|
||||
let guild = await generateGroup(user);
|
||||
let challenge = await generateChallenge(user, guild);
|
||||
let task = await user.post(`/tasks/challenge/${challenge._id}`, {
|
||||
await user.post(`/tasks/challenge/${challenge._id}`, {
|
||||
text: 'test challenge habit',
|
||||
type: 'habit',
|
||||
});
|
||||
|
||||
let groupTask = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'todo group',
|
||||
type: 'todo',
|
||||
});
|
||||
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
|
||||
|
||||
await user.post('/user/reset');
|
||||
await user.sync();
|
||||
|
||||
let userChallengeTask = await user.get(`/tasks/${task._id}`);
|
||||
let memberTasks = await user.get('/tasks/user');
|
||||
|
||||
expect(userChallengeTask).to.eql(task);
|
||||
let syncedGroupTask = find(memberTasks, function findAssignedTask (memberTask) {
|
||||
return memberTask.group.id === guild._id;
|
||||
});
|
||||
|
||||
let userChallengeTask = find(memberTasks, function findAssignedTask (memberTask) {
|
||||
return memberTask.challenge.id === challenge._id;
|
||||
});
|
||||
|
||||
expect(userChallengeTask).to.exist;
|
||||
expect(syncedGroupTask).to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,6 +26,32 @@ describe('PUT /user', () => {
|
||||
expect(user.preferences.costume).to.eql(true);
|
||||
expect(user.stats.hp).to.eql(14);
|
||||
});
|
||||
|
||||
it('profile.name cannot be an empty string or null', async () => {
|
||||
await expect(user.put('/user', {
|
||||
'profile.name': ' ', // string should be trimmed
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'User validation failed',
|
||||
});
|
||||
|
||||
await expect(user.put('/user', {
|
||||
'profile.name': '',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'User validation failed',
|
||||
});
|
||||
|
||||
await expect(user.put('/user', {
|
||||
'profile.name': null,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'User validation failed',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Top Level Protected Operations', () => {
|
||||
@@ -37,6 +63,7 @@ describe('PUT /user', () => {
|
||||
subscriptions: {'purchased.plan.extraMonths': 500, 'purchased.plan.consecutive.trinkets': 1000},
|
||||
'customization gem purchases': {'purchased.background.tavern': true, 'purchased.skin.bear': true},
|
||||
notifications: [{type: 123}],
|
||||
webhooks: {webhooks: [{url: 'https://foobar.com'}]},
|
||||
};
|
||||
|
||||
each(protectedOperations, (data, testName) => {
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
let user;
|
||||
let url = 'http://new-url.com';
|
||||
let enabled = true;
|
||||
|
||||
describe('PUT /user/webhook/:id', () => {
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('validation fails', async () => {
|
||||
await expect(user.put('/user/webhook/some-id'), { enabled: true }).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidUrl'),
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', async () => {
|
||||
let response = await user.post('/user/webhook', { enabled: true, url: 'http://some-url.com'});
|
||||
await user.sync();
|
||||
expect(user.preferences.webhooks[response.id].url).to.not.eql(url);
|
||||
let response2 = await user.put(`/user/webhook/${response.id}`, {url, enabled});
|
||||
expect(response2.url).to.eql(url);
|
||||
await user.sync();
|
||||
expect(user.preferences.webhooks[response.id].url).to.eql(url);
|
||||
});
|
||||
});
|
||||
@@ -5,36 +5,94 @@ import {
|
||||
|
||||
describe('DELETE social registration', () => {
|
||||
let user;
|
||||
let endpoint = '/user/auth/social/facebook';
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
await user.update({ 'auth.facebook.id': 'some-fb-id' });
|
||||
expect(user.auth.local.username).to.not.be.empty;
|
||||
expect(user.auth.facebook).to.not.be.empty;
|
||||
});
|
||||
context('of NOT-FACEBOOK', () => {
|
||||
|
||||
context('NOT-SUPPORTED', () => {
|
||||
it('is not supported', async () => {
|
||||
await expect(user.del('/user/auth/social/SOME-OTHER-NETWORK')).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('onlyFbSupported'),
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('unsupportedNetwork'),
|
||||
});
|
||||
});
|
||||
});
|
||||
context('of facebook', () => {
|
||||
it('fails if local registration does not exist for this user', async () => {
|
||||
await user.update({ 'auth.local': { ok: true } });
|
||||
await expect(user.del(endpoint)).to.eventually.be.rejected.and.eql({
|
||||
|
||||
context('Facebook', () => {
|
||||
it('fails if user does not have an alternative registration method', async () => {
|
||||
await user.update({
|
||||
'auth.facebook.id': 'some-fb-id',
|
||||
'auth.local': { ok: true },
|
||||
});
|
||||
await expect(user.del('/user/auth/social/facebook')).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('cantDetachFb'),
|
||||
message: t('cantDetachSocial'),
|
||||
});
|
||||
});
|
||||
it('succeeds', async () => {
|
||||
let response = await user.del(endpoint);
|
||||
|
||||
it('succeeds if user has a local registration', async () => {
|
||||
await user.update({
|
||||
'auth.facebook.id': 'some-fb-id',
|
||||
});
|
||||
|
||||
let response = await user.del('/user/auth/social/facebook');
|
||||
expect(response).to.eql({});
|
||||
await user.sync();
|
||||
expect(user.auth.facebook).to.be.empty;
|
||||
});
|
||||
|
||||
it('succeeds if user has a google registration', async () => {
|
||||
await user.update({
|
||||
'auth.facebook.id': 'some-fb-id',
|
||||
'auth.google.id': 'some-google-id',
|
||||
'auth.local': { ok: true },
|
||||
});
|
||||
|
||||
let response = await user.del('/user/auth/social/facebook');
|
||||
expect(response).to.eql({});
|
||||
await user.sync();
|
||||
expect(user.auth.facebook).to.be.empty;
|
||||
});
|
||||
});
|
||||
|
||||
context('Google', () => {
|
||||
it('fails if user does not have an alternative registration method', async () => {
|
||||
await user.update({
|
||||
'auth.google.id': 'some-google-id',
|
||||
'auth.local': { ok: true },
|
||||
});
|
||||
await expect(user.del('/user/auth/social/google')).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('cantDetachSocial'),
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds if user has a local registration', async () => {
|
||||
await user.update({
|
||||
'auth.google.id': 'some-google-id',
|
||||
});
|
||||
|
||||
let response = await user.del('/user/auth/social/google');
|
||||
expect(response).to.eql({});
|
||||
await user.sync();
|
||||
expect(user.auth.google).to.be.empty;
|
||||
});
|
||||
|
||||
it('succeeds if user has a facebook registration', async () => {
|
||||
await user.update({
|
||||
'auth.google.id': 'some-google-id',
|
||||
'auth.facebook.id': 'some-facebook-id',
|
||||
'auth.local': { ok: true },
|
||||
});
|
||||
|
||||
let response = await user.del('/user/auth/social/google');
|
||||
expect(response).to.eql({});
|
||||
await user.sync();
|
||||
expect(user.auth.google).to.be.empty;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,6 +32,7 @@ describe('POST /user/auth/local/register', () => {
|
||||
expect(user._id).to.exist;
|
||||
expect(user.apiToken).to.exist;
|
||||
expect(user.auth.local.username).to.eql(username);
|
||||
expect(user.profile.name).to.eql(username);
|
||||
});
|
||||
|
||||
it('provides default tags and tasks', async () => {
|
||||
@@ -66,6 +67,7 @@ describe('POST /user/auth/local/register', () => {
|
||||
});
|
||||
|
||||
await expect(getProperty('users', user._id, '_ABtest')).to.eventually.be.a('string');
|
||||
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
|
||||
});
|
||||
|
||||
it('requires password and confirmPassword to match', async () => {
|
||||
|
||||
@@ -12,58 +12,134 @@ describe('POST /user/auth/social', () => {
|
||||
let endpoint = '/user/auth/social';
|
||||
let randomAccessToken = '123456';
|
||||
let facebookId = 'facebookId';
|
||||
let network = 'facebook';
|
||||
let googleId = 'googleId';
|
||||
let network = 'NoNetwork';
|
||||
|
||||
before(async () => {
|
||||
beforeEach(async () => {
|
||||
api = requester();
|
||||
user = await generateUser();
|
||||
|
||||
let expectedResult = {id: facebookId};
|
||||
let passportFacebookProfile = sandbox.stub(passport._strategies.facebook, 'userProfile');
|
||||
passportFacebookProfile.yields(null, expectedResult);
|
||||
});
|
||||
|
||||
it('fails if network is not facebook', async () => {
|
||||
it('fails if network is not supported', async () => {
|
||||
await expect(api.post(endpoint, {
|
||||
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
|
||||
network: 'NotFacebook',
|
||||
network,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('onlyFbSupported'),
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('unsupportedNetwork'),
|
||||
});
|
||||
});
|
||||
|
||||
it('registers a new user', async () => {
|
||||
let response = await api.post(endpoint, {
|
||||
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
|
||||
network,
|
||||
describe('facebook', () => {
|
||||
before(async () => {
|
||||
let expectedResult = {id: facebookId, displayName: 'a facebook user'};
|
||||
sandbox.stub(passport._strategies.facebook, 'userProfile').yields(null, expectedResult);
|
||||
network = 'facebook';
|
||||
});
|
||||
|
||||
expect(response.apiToken).to.exist;
|
||||
expect(response.id).to.exist;
|
||||
expect(response.newUser).to.be.true;
|
||||
it('registers a new user', async () => {
|
||||
let response = await api.post(endpoint, {
|
||||
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
expect(response.apiToken).to.exist;
|
||||
expect(response.id).to.exist;
|
||||
expect(response.newUser).to.be.true;
|
||||
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a facebook user');
|
||||
});
|
||||
|
||||
it('logs an existing user in', async () => {
|
||||
let registerResponse = await api.post(endpoint, {
|
||||
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
let response = await api.post(endpoint, {
|
||||
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
expect(response.apiToken).to.eql(registerResponse.apiToken);
|
||||
expect(response.id).to.eql(registerResponse.id);
|
||||
expect(response.newUser).to.be.false;
|
||||
});
|
||||
|
||||
it('add social auth to an existing user', async () => {
|
||||
let response = await user.post(endpoint, {
|
||||
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
expect(response.apiToken).to.exist;
|
||||
expect(response.id).to.exist;
|
||||
expect(response.newUser).to.be.false;
|
||||
});
|
||||
|
||||
it('enrolls a new user in an A/B test', async () => {
|
||||
await api.post(endpoint, {
|
||||
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
await expect(getProperty('users', user._id, '_ABtest')).to.eventually.be.a('string');
|
||||
});
|
||||
});
|
||||
|
||||
it('enrolls a new user in an A/B test', async () => {
|
||||
await api.post(endpoint, {
|
||||
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
|
||||
network,
|
||||
describe('google', () => {
|
||||
before(async () => {
|
||||
let expectedResult = {id: googleId, displayName: 'a google user'};
|
||||
sandbox.stub(passport._strategies.google, 'userProfile').yields(null, expectedResult);
|
||||
network = 'google';
|
||||
});
|
||||
|
||||
await expect(getProperty('users', user._id, '_ABtest')).to.eventually.be.a('string');
|
||||
});
|
||||
it('registers a new user', async () => {
|
||||
let response = await api.post(endpoint, {
|
||||
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
it('logs an existing user in', async () => {
|
||||
await user.update({ 'auth.facebook.id': facebookId });
|
||||
|
||||
let response = await api.post(endpoint, {
|
||||
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
|
||||
network,
|
||||
expect(response.apiToken).to.exist;
|
||||
expect(response.id).to.exist;
|
||||
expect(response.newUser).to.be.true;
|
||||
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a google user');
|
||||
});
|
||||
|
||||
expect(response.apiToken).to.eql(user.apiToken);
|
||||
expect(response.id).to.eql(user._id);
|
||||
expect(response.newUser).to.be.false;
|
||||
it('logs an existing user in', async () => {
|
||||
let registerResponse = await api.post(endpoint, {
|
||||
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
let response = await api.post(endpoint, {
|
||||
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
expect(response.apiToken).to.eql(registerResponse.apiToken);
|
||||
expect(response.id).to.eql(registerResponse.id);
|
||||
expect(response.newUser).to.be.false;
|
||||
});
|
||||
|
||||
it('add social auth to an existing user', async () => {
|
||||
let response = await user.post(endpoint, {
|
||||
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
expect(response.apiToken).to.exist;
|
||||
expect(response.id).to.exist;
|
||||
expect(response.newUser).to.be.false;
|
||||
});
|
||||
|
||||
it('enrolls a new user in an A/B test', async () => {
|
||||
await api.post(endpoint, {
|
||||
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
await expect(getProperty('users', user._id, '_ABtest')).to.eventually.be.a('string');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
let user, webhookToDelete;
|
||||
let endpoint = '/user/webhook';
|
||||
|
||||
describe('DELETE /user/webhook', () => {
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
|
||||
webhookToDelete = await user.post('/user/webhook', {
|
||||
url: 'http://some-url.com',
|
||||
enabled: true,
|
||||
});
|
||||
await user.post('/user/webhook', {
|
||||
url: 'http://some-other-url.com',
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
});
|
||||
|
||||
it('deletes a webhook', async () => {
|
||||
expect(user.webhooks).to.have.a.lengthOf(2);
|
||||
await user.del(`${endpoint}/${webhookToDelete.id}`);
|
||||
|
||||
await user.sync();
|
||||
|
||||
expect(user.webhooks).to.have.a.lengthOf(1);
|
||||
});
|
||||
|
||||
it('returns the remaining webhooks', async () => {
|
||||
let [remainingWebhook] = await user.del(`${endpoint}/${webhookToDelete.id}`);
|
||||
|
||||
await user.sync();
|
||||
|
||||
let webhook = user.webhooks[0];
|
||||
|
||||
expect(remainingWebhook.id).to.eql(webhook.id);
|
||||
expect(remainingWebhook.url).to.eql(webhook.url);
|
||||
expect(remainingWebhook.type).to.eql(webhook.type);
|
||||
expect(remainingWebhook.options).to.eql(webhook.options);
|
||||
});
|
||||
|
||||
it('returns an error if webhook with id does not exist', async () => {
|
||||
await expect(user.del(`${endpoint}/id-that-does-not-exist`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('noWebhookWithId', {id: 'id-that-does-not-exist'}),
|
||||
});
|
||||
});
|
||||
});
|
||||
221
test/api/v3/integration/webhook/POST-user_add_webhook.test.js
Normal file
221
test/api/v3/integration/webhook/POST-user_add_webhook.test.js
Normal file
@@ -0,0 +1,221 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('POST /user/webhook', () => {
|
||||
let user, body;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
body = {
|
||||
id: generateUUID(),
|
||||
url: 'https://example.com/endpoint',
|
||||
type: 'taskActivity',
|
||||
enabled: false,
|
||||
};
|
||||
});
|
||||
|
||||
it('requires a url', async () => {
|
||||
delete body.url;
|
||||
|
||||
await expect(user.post('/user/webhook', body)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'User validation failed',
|
||||
});
|
||||
});
|
||||
|
||||
it('requires custom id to be a uuid', async () => {
|
||||
body.id = 'not-a-uuid';
|
||||
|
||||
await expect(user.post('/user/webhook', body)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'User validation failed',
|
||||
});
|
||||
});
|
||||
|
||||
it('defaults id to a uuid', async () => {
|
||||
delete body.id;
|
||||
|
||||
let webhook = await user.post('/user/webhook', body);
|
||||
|
||||
expect(webhook.id).to.exist;
|
||||
});
|
||||
|
||||
it('requires type to be of an accetable type', async () => {
|
||||
body.type = 'not a valid type';
|
||||
|
||||
await expect(user.post('/user/webhook', body)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'User validation failed',
|
||||
});
|
||||
});
|
||||
|
||||
it('defaults enabled to true', async () => {
|
||||
delete body.enabled;
|
||||
|
||||
let webhook = await user.post('/user/webhook', body);
|
||||
|
||||
expect(webhook.enabled).to.be.true;
|
||||
});
|
||||
|
||||
it('can pass a label', async () => {
|
||||
body.label = 'Custom Label';
|
||||
|
||||
let webhook = await user.post('/user/webhook', body);
|
||||
|
||||
expect(webhook.label).to.equal('Custom Label');
|
||||
});
|
||||
|
||||
it('defaults type to taskActivity', async () => {
|
||||
delete body.type;
|
||||
|
||||
let webhook = await user.post('/user/webhook', body);
|
||||
|
||||
expect(webhook.type).to.eql('taskActivity');
|
||||
});
|
||||
|
||||
it('successfully adds the webhook', async () => {
|
||||
expect(user.webhooks).to.eql([]);
|
||||
|
||||
let response = await user.post('/user/webhook', body);
|
||||
|
||||
expect(response.id).to.eql(body.id);
|
||||
expect(response.type).to.eql(body.type);
|
||||
expect(response.url).to.eql(body.url);
|
||||
expect(response.enabled).to.eql(body.enabled);
|
||||
|
||||
await user.sync();
|
||||
|
||||
expect(user.webhooks).to.not.eql([]);
|
||||
|
||||
let webhook = user.webhooks[0];
|
||||
|
||||
expect(webhook.enabled).to.be.false;
|
||||
expect(webhook.type).to.eql('taskActivity');
|
||||
expect(webhook.url).to.eql(body.url);
|
||||
});
|
||||
|
||||
it('cannot use an id of a webhook that already exists', async () => {
|
||||
await user.post('/user/webhook', body);
|
||||
|
||||
await expect(user.post('/user/webhook', body)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('webhookIdAlreadyTaken', { id: body.id }),
|
||||
});
|
||||
});
|
||||
|
||||
it('defaults taskActivity options', async () => {
|
||||
body.type = 'taskActivity';
|
||||
|
||||
let webhook = await user.post('/user/webhook', body);
|
||||
|
||||
expect(webhook.options).to.eql({
|
||||
created: false,
|
||||
updated: false,
|
||||
deleted: false,
|
||||
scored: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('can set taskActivity options', async () => {
|
||||
body.type = 'taskActivity';
|
||||
body.options = {
|
||||
created: true,
|
||||
updated: true,
|
||||
deleted: true,
|
||||
scored: false,
|
||||
};
|
||||
|
||||
let webhook = await user.post('/user/webhook', body);
|
||||
|
||||
expect(webhook.options).to.eql({
|
||||
created: true,
|
||||
updated: true,
|
||||
deleted: true,
|
||||
scored: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('discards extra properties in taskActivity options', async () => {
|
||||
body.type = 'taskActivity';
|
||||
body.options = {
|
||||
created: true,
|
||||
updated: true,
|
||||
deleted: true,
|
||||
scored: false,
|
||||
foo: 'bar',
|
||||
};
|
||||
|
||||
let webhook = await user.post('/user/webhook', body);
|
||||
|
||||
expect(webhook.options.foo).to.not.exist;
|
||||
expect(webhook.options).to.eql({
|
||||
created: true,
|
||||
updated: true,
|
||||
deleted: true,
|
||||
scored: false,
|
||||
});
|
||||
});
|
||||
|
||||
['created', 'updated', 'deleted', 'scored'].forEach((option) => {
|
||||
it(`requires taskActivity option ${option} to be a boolean`, async () => {
|
||||
body.type = 'taskActivity';
|
||||
body.options = {
|
||||
[option]: 'not a boolean',
|
||||
};
|
||||
|
||||
await expect(user.post('/user/webhook', body)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('webhookBooleanOption', { option }),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can set groupChatReceived options', async () => {
|
||||
body.type = 'groupChatReceived';
|
||||
body.options = {
|
||||
groupId: generateUUID(),
|
||||
};
|
||||
|
||||
let webhook = await user.post('/user/webhook', body);
|
||||
|
||||
expect(webhook.options).to.eql({
|
||||
groupId: body.options.groupId,
|
||||
});
|
||||
});
|
||||
|
||||
it('groupChatReceived options requires a uuid for the groupId', async () => {
|
||||
body.type = 'groupChatReceived';
|
||||
body.options = {
|
||||
groupId: 'not-a-uuid',
|
||||
};
|
||||
|
||||
await expect(user.post('/user/webhook', body)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('groupIdRequired'),
|
||||
});
|
||||
});
|
||||
|
||||
it('discards extra properties in groupChatReceived options', async () => {
|
||||
body.type = 'groupChatReceived';
|
||||
body.options = {
|
||||
groupId: generateUUID(),
|
||||
foo: 'bar',
|
||||
};
|
||||
|
||||
let webhook = await user.post('/user/webhook', body);
|
||||
|
||||
expect(webhook.options.foo).to.not.exist;
|
||||
expect(webhook.options).to.eql({
|
||||
groupId: body.options.groupId,
|
||||
});
|
||||
});
|
||||
});
|
||||
132
test/api/v3/integration/webhook/PUT-user_update_webhook.test.js
Normal file
132
test/api/v3/integration/webhook/PUT-user_update_webhook.test.js
Normal file
@@ -0,0 +1,132 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { v4 as generateUUID} from 'uuid';
|
||||
|
||||
describe('PUT /user/webhook/:id', () => {
|
||||
let user, webhookToUpdate;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
|
||||
webhookToUpdate = await user.post('/user/webhook', {
|
||||
url: 'http://some-url.com',
|
||||
label: 'Original Label',
|
||||
enabled: true,
|
||||
type: 'taskActivity',
|
||||
options: { created: true, scored: true },
|
||||
});
|
||||
await user.post('/user/webhook', {
|
||||
url: 'http://some-other-url.com',
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
});
|
||||
|
||||
it('returns an error if webhook with id does not exist', async () => {
|
||||
await expect(user.put('/user/webhook/id-that-does-not-exist')).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('noWebhookWithId', {id: 'id-that-does-not-exist'}),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if validation fails', async () => {
|
||||
await expect(user.put(`/user/webhook/${webhookToUpdate.id}`, { url: 'foo', enabled: true })).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'User validation failed',
|
||||
});
|
||||
});
|
||||
|
||||
it('updates a webhook', async () => {
|
||||
let url = 'http://a-new-url.com';
|
||||
let type = 'groupChatReceived';
|
||||
let label = 'New Label';
|
||||
let options = { groupId: generateUUID() };
|
||||
|
||||
await user.put(`/user/webhook/${webhookToUpdate.id}`, {url, type, options, label});
|
||||
|
||||
await user.sync();
|
||||
|
||||
let webhook = user.webhooks.find(hook => webhookToUpdate.id === hook.id);
|
||||
|
||||
expect(webhook.url).to.equal(url);
|
||||
expect(webhook.label).to.equal(label);
|
||||
expect(webhook.type).to.equal(type);
|
||||
expect(webhook.options).to.eql(options);
|
||||
});
|
||||
|
||||
it('returns the updated webhook', async () => {
|
||||
let url = 'http://a-new-url.com';
|
||||
let type = 'groupChatReceived';
|
||||
let options = { groupId: generateUUID() };
|
||||
|
||||
let response = await user.put(`/user/webhook/${webhookToUpdate.id}`, {url, type, options});
|
||||
|
||||
expect(response.url).to.eql(url);
|
||||
expect(response.type).to.eql(type);
|
||||
expect(response.options).to.eql(options);
|
||||
});
|
||||
|
||||
it('cannot update the id', async () => {
|
||||
let id = generateUUID();
|
||||
let url = 'http://a-new-url.com';
|
||||
|
||||
await user.put(`/user/webhook/${webhookToUpdate.id}`, {url, id});
|
||||
|
||||
await user.sync();
|
||||
|
||||
let webhook = user.webhooks.find(hook => webhookToUpdate.id === hook.id);
|
||||
|
||||
expect(webhook.id).to.eql(webhookToUpdate.id);
|
||||
expect(webhook.url).to.eql(url);
|
||||
});
|
||||
|
||||
it('can update taskActivity options', async () => {
|
||||
let type = 'taskActivity';
|
||||
let options = {
|
||||
updated: false,
|
||||
deleted: true,
|
||||
};
|
||||
|
||||
let webhook = await user.put(`/user/webhook/${webhookToUpdate.id}`, {type, options});
|
||||
|
||||
expect(webhook.options).to.eql({
|
||||
created: true, // starting value
|
||||
updated: false,
|
||||
deleted: true,
|
||||
scored: true, // default value
|
||||
});
|
||||
});
|
||||
|
||||
it('errors if taskActivity option is not a boolean', async () => {
|
||||
let type = 'taskActivity';
|
||||
let options = {
|
||||
created: 'not a boolean',
|
||||
updated: false,
|
||||
deleted: true,
|
||||
};
|
||||
|
||||
await expect(user.put(`/user/webhook/${webhookToUpdate.id}`, {type, options})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('webhookBooleanOption', { option: 'created' }),
|
||||
});
|
||||
});
|
||||
|
||||
it('errors if groupChatRecieved groupId option is not a uuid', async () => {
|
||||
let type = 'groupChatReceived';
|
||||
let options = {
|
||||
groupId: 'not-a-uuid',
|
||||
};
|
||||
|
||||
await expect(user.put(`/user/webhook/${webhookToUpdate.id}`, {type, options})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('groupIdRequired'),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -277,6 +277,8 @@ describe('analyticsService', () => {
|
||||
dailys: [{_id: 'daily'}],
|
||||
todos: [{_id: 'todo'}],
|
||||
rewards: [{_id: 'reward'}],
|
||||
balance: 12,
|
||||
loginIncentives: 1,
|
||||
};
|
||||
|
||||
data.user = user;
|
||||
@@ -300,6 +302,9 @@ describe('analyticsService', () => {
|
||||
},
|
||||
contributorLevel: 1,
|
||||
subscription: 'foo-plan',
|
||||
balance: 12,
|
||||
balanceGemAmount: 48,
|
||||
loginIncentives: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -349,7 +354,8 @@ describe('analyticsService', () => {
|
||||
purchaseType: 'checkout',
|
||||
gift: false,
|
||||
quantity: 1,
|
||||
headers: {'x-client': 'habitica-web',
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import { model as User } from '../../../../../website/server/models/user';
|
||||
import * as Tasks from '../../../../../website/server/models/task';
|
||||
import { clone } from 'lodash';
|
||||
import common from '../../../../../website/common';
|
||||
import analytics from '../../../../../website/server/libs/analyticsService';
|
||||
|
||||
// const scoreTask = common.ops.scoreTask;
|
||||
|
||||
@@ -17,9 +18,6 @@ describe('cron', () => {
|
||||
let user;
|
||||
let tasksByType = {habits: [], dailys: [], todos: [], rewards: []};
|
||||
let daysMissed = 0;
|
||||
let analytics = {
|
||||
track: sinon.spy(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
user = new User({
|
||||
@@ -34,11 +32,17 @@ describe('cron', () => {
|
||||
},
|
||||
});
|
||||
|
||||
sinon.spy(analytics, 'track');
|
||||
|
||||
user._statsComputed = {
|
||||
mp: 10,
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
});
|
||||
|
||||
it('updates user.preferences.timezoneOffsetAtLastCron', () => {
|
||||
let timezoneOffsetFromUserPrefs = 1;
|
||||
|
||||
@@ -59,10 +63,15 @@ describe('cron', () => {
|
||||
expect(user.flags.cronCount).to.be.greaterThan(cronCountBefore);
|
||||
});
|
||||
|
||||
it('calls analytics', () => {
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(analytics.track.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
describe('end of the month perks', () => {
|
||||
beforeEach(() => {
|
||||
user.purchased.plan.customerId = 'subscribedId';
|
||||
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
|
||||
user.purchased.plan.dateUpdated = moment().subtract(1, 'months').toDate();
|
||||
});
|
||||
|
||||
it('resets plan.gemsBought on a new month', () => {
|
||||
@@ -71,10 +80,21 @@ describe('cron', () => {
|
||||
expect(user.purchased.plan.gemsBought).to.equal(0);
|
||||
});
|
||||
|
||||
it('resets plan.dateUpdated on a new month', () => {
|
||||
let currentMonth = moment().format('MMYYYY');
|
||||
it('does not reset plan.gemsBought within the month', () => {
|
||||
let clock = sinon.useFakeTimers(moment().startOf('month').add(2, 'days').unix());
|
||||
user.purchased.plan.dateUpdated = moment().startOf('month').toDate();
|
||||
|
||||
user.purchased.plan.gemsBought = 10;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(moment(user.purchased.plan.dateUpdated).format('MMYYYY')).to.equal(currentMonth);
|
||||
expect(user.purchased.plan.gemsBought).to.equal(10);
|
||||
|
||||
clock.restore();
|
||||
});
|
||||
|
||||
it('resets plan.dateUpdated on a new month', () => {
|
||||
let currentMonth = moment().startOf('month');
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(moment(user.purchased.plan.dateUpdated).startOf('month').isSame(currentMonth)).to.eql(true);
|
||||
});
|
||||
|
||||
it('increments plan.consecutive.count', () => {
|
||||
@@ -83,6 +103,13 @@ describe('cron', () => {
|
||||
expect(user.purchased.plan.consecutive.count).to.equal(1);
|
||||
});
|
||||
|
||||
it('increments plan.consecutive.count by more than 1 if user skipped months between logins', () => {
|
||||
user.purchased.plan.dateUpdated = moment().subtract(2, 'months').toDate();
|
||||
user.purchased.plan.consecutive.count = 0;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.purchased.plan.consecutive.count).to.equal(2);
|
||||
});
|
||||
|
||||
it('decrements plan.consecutive.offset when offset is greater than 0', () => {
|
||||
user.purchased.plan.consecutive.offset = 2;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
@@ -97,6 +124,21 @@ describe('cron', () => {
|
||||
expect(user.purchased.plan.consecutive.offset).to.equal(0);
|
||||
});
|
||||
|
||||
it('increments plan.consecutive.trinkets multiple times if user has been absent with continuous subscription', () => {
|
||||
user.purchased.plan.dateUpdated = moment().subtract(6, 'months').toDate();
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.equal(2);
|
||||
});
|
||||
|
||||
it('does not award unearned plan.consecutive.trinkets if subscription ended during an absence', () => {
|
||||
user.purchased.plan.dateUpdated = moment().subtract(6, 'months').toDate();
|
||||
user.purchased.plan.dateTerminated = moment().subtract(3, 'months').toDate();
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.equal(1);
|
||||
});
|
||||
|
||||
it('increments plan.consecutive.gemCapExtra when user has reached a month that is a multiple of 3', () => {
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
user.purchased.plan.consecutive.offset = 1;
|
||||
@@ -105,6 +147,13 @@ describe('cron', () => {
|
||||
expect(user.purchased.plan.consecutive.offset).to.equal(0);
|
||||
});
|
||||
|
||||
it('increments plan.consecutive.gemCapExtra multiple times if user has been absent with continuous subscription', () => {
|
||||
user.purchased.plan.dateUpdated = moment().subtract(6, 'months').toDate();
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(10);
|
||||
});
|
||||
|
||||
it('does not increment plan.consecutive.gemCapExtra when user has reached the gemCap limit', () => {
|
||||
user.purchased.plan.consecutive.gemCapExtra = 25;
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
@@ -118,7 +167,7 @@ describe('cron', () => {
|
||||
expect(user.purchased.plan.customerId).to.exist;
|
||||
});
|
||||
|
||||
it('does reset plan stats until we are after the last day of the cancelled month', () => {
|
||||
it('does reset plan stats if we are after the last day of the cancelled month', () => {
|
||||
user.purchased.plan.dateTerminated = moment(new Date()).subtract({days: 1});
|
||||
user.purchased.plan.consecutive.gemCapExtra = 20;
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
@@ -134,10 +183,25 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
describe('end of the month perks when user is not subscribed', () => {
|
||||
it('does not reset plan.gemsBought on a new month', () => {
|
||||
beforeEach(() => {
|
||||
user.purchased.plan.dateUpdated = moment().subtract(1, 'months').toDate();
|
||||
});
|
||||
|
||||
it('resets plan.gemsBought on a new month', () => {
|
||||
user.purchased.plan.gemsBought = 10;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.purchased.plan.gemsBought).to.equal(0);
|
||||
});
|
||||
|
||||
it('does not reset plan.gemsBought within the month', () => {
|
||||
let clock = sinon.useFakeTimers(moment().startOf('month').add(2, 'days').unix());
|
||||
user.purchased.plan.dateUpdated = moment().startOf('month').toDate();
|
||||
|
||||
user.purchased.plan.gemsBought = 10;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.purchased.plan.gemsBought).to.equal(10);
|
||||
|
||||
clock.restore();
|
||||
});
|
||||
|
||||
it('does not reset plan.dateUpdated on a new month', () => {
|
||||
@@ -202,6 +266,11 @@ describe('cron', () => {
|
||||
user.preferences.sleep = true;
|
||||
});
|
||||
|
||||
it('calls analytics', () => {
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(analytics.track.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('clears user buffs', () => {
|
||||
user.stats.buffs = {
|
||||
str: 1,
|
||||
@@ -231,7 +300,7 @@ describe('cron', () => {
|
||||
startDate: new Date(),
|
||||
};
|
||||
|
||||
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line babel/new-cap
|
||||
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line new-cap
|
||||
tasksByType.dailys.push(task);
|
||||
tasksByType.dailys[0].completed = true;
|
||||
|
||||
@@ -252,7 +321,7 @@ describe('cron', () => {
|
||||
value: 0,
|
||||
};
|
||||
|
||||
let task = new Tasks.todo(Tasks.Task.sanitize(todo)); // eslint-disable-line babel/new-cap
|
||||
let task = new Tasks.todo(Tasks.Task.sanitize(todo)); // eslint-disable-line new-cap
|
||||
tasksByType.todos.push(task);
|
||||
});
|
||||
|
||||
@@ -278,7 +347,7 @@ describe('cron', () => {
|
||||
type: 'daily',
|
||||
};
|
||||
|
||||
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line babel/new-cap
|
||||
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line new-cap
|
||||
tasksByType.dailys = [];
|
||||
tasksByType.dailys.push(task);
|
||||
|
||||
@@ -380,7 +449,7 @@ describe('cron', () => {
|
||||
type: 'habit',
|
||||
};
|
||||
|
||||
let task = new Tasks.habit(Tasks.Task.sanitize(habit)); // eslint-disable-line babel/new-cap
|
||||
let task = new Tasks.habit(Tasks.Task.sanitize(habit)); // eslint-disable-line new-cap
|
||||
tasksByType.habits = [];
|
||||
tasksByType.habits.push(task);
|
||||
});
|
||||
@@ -421,7 +490,7 @@ describe('cron', () => {
|
||||
type: 'daily',
|
||||
};
|
||||
|
||||
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line babel/new-cap
|
||||
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line new-cap
|
||||
tasksByType.dailys = [];
|
||||
tasksByType.dailys.push(task);
|
||||
|
||||
@@ -564,7 +633,7 @@ describe('cron', () => {
|
||||
type: 'daily',
|
||||
};
|
||||
|
||||
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line babel/new-cap
|
||||
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line new-cap
|
||||
tasksByType.dailys = [];
|
||||
tasksByType.dailys.push(task);
|
||||
|
||||
@@ -601,9 +670,9 @@ describe('cron', () => {
|
||||
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
expect(user.notifications[0].type).to.equal('CRON');
|
||||
expect(user.notifications[0].data).to.eql({
|
||||
expect(user.notifications.length).to.be.greaterThan(0);
|
||||
expect(user.notifications[1].type).to.equal('CRON');
|
||||
expect(user.notifications[1].data).to.eql({
|
||||
hp: user.stats.hp - hpBefore,
|
||||
mp: user.stats.mp - mpBefore,
|
||||
});
|
||||
@@ -620,13 +689,14 @@ describe('cron', () => {
|
||||
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
expect(user.notifications[0].type).to.equal('CRON');
|
||||
expect(user.notifications[0].data).to.eql({
|
||||
expect(user.notifications.length).to.be.greaterThan(0);
|
||||
expect(user.notifications[1].type).to.equal('CRON');
|
||||
expect(user.notifications[1].data).to.eql({
|
||||
hp: user.stats.hp - hpBefore1,
|
||||
mp: user.stats.mp - mpBefore1,
|
||||
});
|
||||
|
||||
let notifsBefore2 = user.notifications.length;
|
||||
let hpBefore2 = user.stats.hp;
|
||||
let mpBefore2 = user.stats.mp;
|
||||
|
||||
@@ -634,12 +704,14 @@ describe('cron', () => {
|
||||
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
expect(user.notifications[0].type).to.equal('CRON');
|
||||
expect(user.notifications[0].data).to.eql({
|
||||
expect(user.notifications.length - notifsBefore2).to.equal(0);
|
||||
expect(user.notifications[0].type).to.not.equal('CRON');
|
||||
expect(user.notifications[1].type).to.equal('CRON');
|
||||
expect(user.notifications[1].data).to.eql({
|
||||
hp: user.stats.hp - hpBefore2 - (hpBefore2 - hpBefore1),
|
||||
mp: user.stats.mp - mpBefore2 - (mpBefore2 - mpBefore1),
|
||||
});
|
||||
expect(user.notifications[0].type).to.not.equal('CRON');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -692,6 +764,188 @@ describe('cron', () => {
|
||||
expect(user.inbox.messages[messageId]).to.not.exist;
|
||||
});
|
||||
});
|
||||
|
||||
describe('login incentives', () => {
|
||||
it('increments incentive counter each cron', () => {
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.loginIncentives).to.eql(1);
|
||||
user.lastCron = moment(new Date()).subtract({days: 1});
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.loginIncentives).to.eql(2);
|
||||
});
|
||||
|
||||
it('pushes a notification of the day\'s incentive each cron', () => {
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.notifications.length).to.be.greaterThan(1);
|
||||
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
||||
});
|
||||
|
||||
it('replaces previous notifications', () => {
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
|
||||
let filteredNotifications = user.notifications.filter(n => n.type === 'LOGIN_INCENTIVE');
|
||||
|
||||
expect(filteredNotifications.length).to.equal(1);
|
||||
});
|
||||
|
||||
it('increments loginIncentives by 1 even if days are skipped in between', () => {
|
||||
daysMissed = 3;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.loginIncentives).to.eql(1);
|
||||
});
|
||||
|
||||
it('increments loginIncentives by 1 even if user has Dailies paused', () => {
|
||||
user.preferences.sleep = true;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.loginIncentives).to.eql(1);
|
||||
});
|
||||
|
||||
it('awards user bard robes if login incentive is 1', () => {
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.loginIncentives).to.eql(1);
|
||||
expect(user.items.gear.owned.armor_special_bardRobes).to.eql(true);
|
||||
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
||||
});
|
||||
|
||||
it('awards user incentive backgrounds if login incentive is 2', () => {
|
||||
user.loginIncentives = 1;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.loginIncentives).to.eql(2);
|
||||
expect(user.purchased.background.blue).to.eql(true);
|
||||
expect(user.purchased.background.green).to.eql(true);
|
||||
expect(user.purchased.background.purple).to.eql(true);
|
||||
expect(user.purchased.background.red).to.eql(true);
|
||||
expect(user.purchased.background.yellow).to.eql(true);
|
||||
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
||||
});
|
||||
|
||||
it('awards user Bard Hat if login incentive is 3', () => {
|
||||
user.loginIncentives = 2;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.loginIncentives).to.eql(3);
|
||||
expect(user.items.gear.owned.head_special_bardHat).to.eql(true);
|
||||
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
||||
});
|
||||
|
||||
it('awards user RoyalPurple Hatching Potion if login incentive is 4', () => {
|
||||
user.loginIncentives = 3;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.loginIncentives).to.eql(4);
|
||||
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
|
||||
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
||||
});
|
||||
|
||||
it('awards user a Chocolate, Meat and Pink Contton Candy if login incentive is 5', () => {
|
||||
user.loginIncentives = 4;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.loginIncentives).to.eql(5);
|
||||
|
||||
expect(user.items.food.Chocolate).to.eql(1);
|
||||
expect(user.items.food.Meat).to.eql(1);
|
||||
expect(user.items.food.CottonCandyPink).to.eql(1);
|
||||
|
||||
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
||||
});
|
||||
|
||||
it('awards user moon quest if login incentive is 7', () => {
|
||||
user.loginIncentives = 6;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.loginIncentives).to.eql(7);
|
||||
expect(user.items.quests.moon1).to.eql(1);
|
||||
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
||||
});
|
||||
|
||||
it('awards user RoyalPurple Hatching Potion if login incentive is 10', () => {
|
||||
user.loginIncentives = 9;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.loginIncentives).to.eql(10);
|
||||
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
|
||||
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
||||
});
|
||||
|
||||
it('awards user a Strawberry, Patato and Blue Contton Candy if login incentive is 14', () => {
|
||||
user.loginIncentives = 13;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.loginIncentives).to.eql(14);
|
||||
|
||||
expect(user.items.food.Strawberry).to.eql(1);
|
||||
expect(user.items.food.Potatoe).to.eql(1);
|
||||
expect(user.items.food.CottonCandyBlue).to.eql(1);
|
||||
|
||||
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
||||
});
|
||||
|
||||
it('awards user a bard instrument if login incentive is 18', () => {
|
||||
user.loginIncentives = 17;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.loginIncentives).to.eql(18);
|
||||
expect(user.items.gear.owned.weapon_special_bardInstrument).to.eql(true);
|
||||
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
||||
});
|
||||
|
||||
it('awards user second moon quest if login incentive is 22', () => {
|
||||
user.loginIncentives = 21;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.loginIncentives).to.eql(22);
|
||||
expect(user.items.quests.moon2).to.eql(1);
|
||||
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
||||
});
|
||||
|
||||
it('awards user a RoyalPurple hatching potion if login incentive is 26', () => {
|
||||
user.loginIncentives = 25;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.loginIncentives).to.eql(26);
|
||||
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
|
||||
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
||||
});
|
||||
|
||||
it('awards user Fish, Milk, Rotten Meat and Honey if login incentive is 30', () => {
|
||||
user.loginIncentives = 29;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.loginIncentives).to.eql(30);
|
||||
|
||||
expect(user.items.food.Fish).to.eql(1);
|
||||
expect(user.items.food.Milk).to.eql(1);
|
||||
expect(user.items.food.RottenMeat).to.eql(1);
|
||||
expect(user.items.food.Honey).to.eql(1);
|
||||
|
||||
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
||||
});
|
||||
|
||||
it('awards user a RoyalPurple hatching potion if login incentive is 35', () => {
|
||||
user.loginIncentives = 34;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.loginIncentives).to.eql(35);
|
||||
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
|
||||
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
||||
});
|
||||
|
||||
it('awards user the third moon quest if login incentive is 40', () => {
|
||||
user.loginIncentives = 39;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.loginIncentives).to.eql(40);
|
||||
expect(user.items.quests.moon3).to.eql(1);
|
||||
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
||||
});
|
||||
|
||||
it('awards user a RoyalPurple hatching potion if login incentive is 45', () => {
|
||||
user.loginIncentives = 44;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.loginIncentives).to.eql(45);
|
||||
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
|
||||
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
||||
});
|
||||
|
||||
it('awards user a saddle if login incentive is 50', () => {
|
||||
user.loginIncentives = 49;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.loginIncentives).to.eql(50);
|
||||
expect(user.items.food.Saddle).to.eql(1);
|
||||
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('recoverCron', () => {
|
||||
|
||||
@@ -3,15 +3,33 @@ import * as api from '../../../../../website/server/libs/payments';
|
||||
import analytics from '../../../../../website/server/libs/analyticsService';
|
||||
import notifications from '../../../../../website/server/libs/pushNotifications';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import { model as Group } from '../../../../../website/server/models/group';
|
||||
import stripeModule from 'stripe';
|
||||
import moment from 'moment';
|
||||
import { translate as t } from '../../../../helpers/api-v3-integration.helper';
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../helpers/api-unit.helper.js';
|
||||
import i18n from '../../../../../website/common/script/i18n';
|
||||
import amzLib from '../../../../../website/server/libs/amazonPayments';
|
||||
|
||||
describe('payments/index', () => {
|
||||
let user, data, plan;
|
||||
let user, group, data, plan;
|
||||
|
||||
beforeEach(() => {
|
||||
let stripe = stripeModule('test');
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
await group.save();
|
||||
|
||||
sandbox.stub(sender, 'sendTxn');
|
||||
sandbox.stub(user, 'sendMessage');
|
||||
sandbox.stub(analytics, 'trackPurchase');
|
||||
@@ -80,6 +98,24 @@ describe('payments/index', () => {
|
||||
expect(recipient.purchased.plan.extraMonths).to.eql(3);
|
||||
});
|
||||
|
||||
it('does not set negative extraMonths if plan has past dateTerminated date', async () => {
|
||||
let dateTerminated = moment().subtract(2, 'months').toDate();
|
||||
recipient.purchased.plan.dateTerminated = dateTerminated;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.extraMonths).to.eql(0);
|
||||
});
|
||||
|
||||
it('does not reset Gold-to-Gems cap on an existing subscription', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.purchased.plan.gemsBought = 12;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.gemsBought).to.eql(12);
|
||||
});
|
||||
|
||||
it('adds to date terminated for an existing plan with a future terminated date', async () => {
|
||||
let dateTerminated = moment().add(1, 'months').toDate();
|
||||
recipient.purchased.plan = plan;
|
||||
@@ -115,6 +151,14 @@ describe('payments/index', () => {
|
||||
expect(recipient.purchased.plan.dateUpdated).to.exist;
|
||||
});
|
||||
|
||||
it('sets plan.dateCreated if it did not previously exist', async () => {
|
||||
expect(recipient.purchased.plan.dateCreated).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('does not change plan.customerId if it already exists', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
data.customerId = 'purchaserCustomerId';
|
||||
@@ -143,9 +187,10 @@ describe('payments/index', () => {
|
||||
|
||||
it('sends a private message about the gift', async () => {
|
||||
await api.createSubscription(data);
|
||||
let msg = '\`Hello recipient, sender has sent you 3 months of subscription!\`';
|
||||
|
||||
expect(user.sendMessage).to.be.calledOnce;
|
||||
expect(user.sendMessage).to.be.calledWith(recipient, '\`Hello recipient, sender has sent you 3 months of subscription!\`');
|
||||
expect(user.sendMessage).to.be.calledWith(recipient, { receiverMsg: msg, senderMsg: msg });
|
||||
});
|
||||
|
||||
it('sends an email about the gift', async () => {
|
||||
@@ -168,6 +213,7 @@ describe('payments/index', () => {
|
||||
expect(analytics.trackPurchase).to.be.calledOnce;
|
||||
expect(analytics.trackPurchase).to.be.calledWith({
|
||||
uuid: user._id,
|
||||
groupId: undefined,
|
||||
itemPurchased: 'Subscription',
|
||||
sku: 'payment method-subscription',
|
||||
purchaseType: 'subscribe',
|
||||
@@ -210,6 +256,25 @@ describe('payments/index', () => {
|
||||
expect(user.purchased.plan.extraMonths).to.within(1.9, 2);
|
||||
});
|
||||
|
||||
it('does not set negative extraMonths if plan has past dateTerminated date', async () => {
|
||||
user.purchased.plan = plan;
|
||||
user.purchased.plan.dateTerminated = moment(new Date()).subtract(2, 'months');
|
||||
expect(user.purchased.plan.extraMonths).to.eql(0);
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.extraMonths).to.eql(0);
|
||||
});
|
||||
|
||||
it('does not reset Gold-to-Gems cap on additional subscription', async () => {
|
||||
user.purchased.plan = plan;
|
||||
user.purchased.plan.gemsBought = 10;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.gemsBought).to.eql(10);
|
||||
});
|
||||
|
||||
it('sets lastBillingDate if payment method is "Amazon Payments"', async () => {
|
||||
data.paymentMethod = 'Amazon Payments';
|
||||
|
||||
@@ -218,7 +283,7 @@ describe('payments/index', () => {
|
||||
expect(user.purchased.plan.lastBillingDate).to.exist;
|
||||
});
|
||||
|
||||
it('increases the user\'s transcation count', async () => {
|
||||
it('increases the user\'s transaction count', async () => {
|
||||
expect(user.purchased.txnCount).to.eql(0);
|
||||
|
||||
await api.createSubscription(data);
|
||||
@@ -239,6 +304,7 @@ describe('payments/index', () => {
|
||||
expect(analytics.trackPurchase).to.be.calledOnce;
|
||||
expect(analytics.trackPurchase).to.be.calledWith({
|
||||
uuid: user._id,
|
||||
groupId: undefined,
|
||||
itemPurchased: 'Subscription',
|
||||
sku: 'payment method-subscription',
|
||||
purchaseType: 'subscribe',
|
||||
@@ -254,6 +320,53 @@ describe('payments/index', () => {
|
||||
});
|
||||
});
|
||||
|
||||
context('Purchasing a subscription for group', () => {
|
||||
it('creates a subscription', async () => {
|
||||
expect(group.purchased.plan.planId).to.not.exist;
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedGroup = await Group.findById(group._id).exec();
|
||||
|
||||
expect(updatedGroup.purchased.plan.planId).to.eql('basic_3mo');
|
||||
expect(updatedGroup.purchased.plan.customerId).to.eql('customer-id');
|
||||
expect(updatedGroup.purchased.plan.dateUpdated).to.exist;
|
||||
expect(updatedGroup.purchased.plan.gemsBought).to.eql(0);
|
||||
expect(updatedGroup.purchased.plan.paymentMethod).to.eql('Payment Method');
|
||||
expect(updatedGroup.purchased.plan.extraMonths).to.eql(0);
|
||||
expect(updatedGroup.purchased.plan.dateTerminated).to.eql(null);
|
||||
expect(updatedGroup.purchased.plan.lastBillingDate).to.not.exist;
|
||||
expect(updatedGroup.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('sets extraMonths if plan has dateTerminated date', async () => {
|
||||
group.purchased.plan = plan;
|
||||
group.purchased.plan.dateTerminated = moment(new Date()).add(2, 'months');
|
||||
await group.save();
|
||||
expect(group.purchased.plan.extraMonths).to.eql(0);
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedGroup = await Group.findById(group._id).exec();
|
||||
expect(updatedGroup.purchased.plan.extraMonths).to.within(1.9, 2);
|
||||
});
|
||||
|
||||
it('does not set negative extraMonths if plan has past dateTerminated date', async () => {
|
||||
group.purchased.plan = plan;
|
||||
group.purchased.plan.dateTerminated = moment(new Date()).subtract(2, 'months');
|
||||
await group.save();
|
||||
expect(group.purchased.plan.extraMonths).to.eql(0);
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedGroup = await Group.findById(group._id).exec();
|
||||
expect(updatedGroup.purchased.plan.extraMonths).to.eql(0);
|
||||
});
|
||||
});
|
||||
|
||||
context('Block subscription perks', () => {
|
||||
it('adds block months to plan.consecutive.offset', async () => {
|
||||
await api.createSubscription(data);
|
||||
@@ -389,61 +502,169 @@ describe('payments/index', () => {
|
||||
data = { user };
|
||||
});
|
||||
|
||||
it('adds a month termination date by default', async () => {
|
||||
await api.cancelSubscription(data);
|
||||
context('Canceling a subscription for self', () => {
|
||||
it('adds a month termination date by default', async () => {
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
let now = new Date();
|
||||
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
|
||||
let now = new Date();
|
||||
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
|
||||
|
||||
expect(daysTillTermination).to.be.within(29, 30); // 1 month +/- 1 days
|
||||
expect(daysTillTermination).to.be.within(29, 30); // 1 month +/- 1 days
|
||||
});
|
||||
|
||||
it('adds extraMonths to dateTerminated value', async () => {
|
||||
user.purchased.plan.extraMonths = 2;
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
let now = new Date();
|
||||
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
|
||||
|
||||
expect(daysTillTermination).to.be.within(89, 90); // 3 months +/- 1 days
|
||||
});
|
||||
|
||||
it('handles extra month fractions', async () => {
|
||||
user.purchased.plan.extraMonths = 0.3;
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
let now = new Date();
|
||||
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
|
||||
|
||||
expect(daysTillTermination).to.be.within(38, 39); // should be about 1 month + 1/3 month
|
||||
});
|
||||
|
||||
it('terminates at next billing date if it exists', async () => {
|
||||
data.nextBill = moment().add({ days: 15 });
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
let now = new Date();
|
||||
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
|
||||
|
||||
expect(daysTillTermination).to.be.within(13, 15);
|
||||
});
|
||||
|
||||
it('resets plan.extraMonths', async () => {
|
||||
user.purchased.plan.extraMonths = 5;
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.extraMonths).to.eql(0);
|
||||
});
|
||||
|
||||
it('sends an email', async () => {
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
expect(sender.sendTxn).to.be.calledOnce;
|
||||
expect(sender.sendTxn).to.be.calledWith(user, 'cancel-subscription');
|
||||
});
|
||||
});
|
||||
|
||||
it('adds extraMonths to dateTerminated value', async () => {
|
||||
user.purchased.plan.extraMonths = 2;
|
||||
context('Canceling a subscription for group', () => {
|
||||
it('adds a month termination date by default', async () => {
|
||||
data.groupId = group._id;
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
let now = new Date();
|
||||
let updatedGroup = await Group.findById(group._id).exec();
|
||||
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
|
||||
|
||||
let now = new Date();
|
||||
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
|
||||
expect(daysTillTermination).to.be.within(29, 30); // 1 month +/- 1 days
|
||||
});
|
||||
|
||||
expect(daysTillTermination).to.be.within(89, 90); // 3 months +/- 1 days
|
||||
});
|
||||
it('adds extraMonths to dateTerminated value', async () => {
|
||||
group.purchased.plan.extraMonths = 2;
|
||||
await group.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
it('handles extra month fractions', async () => {
|
||||
user.purchased.plan.extraMonths = 0.3;
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
let now = new Date();
|
||||
let updatedGroup = await Group.findById(group._id).exec();
|
||||
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
|
||||
|
||||
let now = new Date();
|
||||
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
|
||||
expect(daysTillTermination).to.be.within(89, 90); // 3 months +/- 1 days
|
||||
});
|
||||
|
||||
expect(daysTillTermination).to.be.within(38, 39); // should be about 1 month + 1/3 month
|
||||
});
|
||||
it('handles extra month fractions', async () => {
|
||||
group.purchased.plan.extraMonths = 0.3;
|
||||
await group.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
it('terminates at next billing date if it exists', async () => {
|
||||
data.nextBill = moment().add({ days: 15 });
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
let now = new Date();
|
||||
let updatedGroup = await Group.findById(group._id).exec();
|
||||
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
|
||||
|
||||
let now = new Date();
|
||||
let daysTillTermination = moment(user.purchased.plan.dateTerminated).diff(now, 'days');
|
||||
expect(daysTillTermination).to.be.within(38, 39); // should be about 1 month + 1/3 month
|
||||
});
|
||||
|
||||
expect(daysTillTermination).to.be.within(13, 15);
|
||||
});
|
||||
it('terminates at next billing date if it exists', async () => {
|
||||
data.nextBill = moment().add({ days: 15 });
|
||||
data.groupId = group._id;
|
||||
|
||||
it('resets plan.extraMonths', async () => {
|
||||
user.purchased.plan.extraMonths = 5;
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
let now = new Date();
|
||||
let updatedGroup = await Group.findById(group._id).exec();
|
||||
let daysTillTermination = moment(updatedGroup.purchased.plan.dateTerminated).diff(now, 'days');
|
||||
|
||||
expect(user.purchased.plan.extraMonths).to.eql(0);
|
||||
});
|
||||
expect(daysTillTermination).to.be.within(13, 15);
|
||||
});
|
||||
|
||||
it('sends an email', async () => {
|
||||
await api.cancelSubscription(data);
|
||||
it('resets plan.extraMonths', async () => {
|
||||
group.purchased.plan.extraMonths = 5;
|
||||
await group.save();
|
||||
data.groupId = group._id;
|
||||
|
||||
expect(sender.sendTxn).to.be.calledOnce;
|
||||
expect(sender.sendTxn).to.be.calledWith(user, 'cancel-subscription');
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
let updatedGroup = await Group.findById(group._id).exec();
|
||||
expect(updatedGroup.purchased.plan.extraMonths).to.eql(0);
|
||||
});
|
||||
|
||||
it('sends an email', async () => {
|
||||
data.groupId = group._id;
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
expect(sender.sendTxn).to.be.calledOnce;
|
||||
expect(sender.sendTxn).to.be.calledWith(user, 'group-cancel-subscription');
|
||||
});
|
||||
|
||||
it('prevents non group leader from manging subscription', async () => {
|
||||
let groupMember = new User();
|
||||
data.user = groupMember;
|
||||
data.groupId = group._id;
|
||||
|
||||
await expect(api.cancelSubscription(data))
|
||||
.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
|
||||
name: 'NotAuthorized',
|
||||
});
|
||||
});
|
||||
|
||||
it('allows old group leader to cancel if they created the subscription', async () => {
|
||||
data.groupId = group._id;
|
||||
data.sub = {
|
||||
key: 'group_monthly',
|
||||
};
|
||||
data.paymentMethod = 'Payment Method';
|
||||
await api.createSubscription(data);
|
||||
|
||||
let updatedGroup = await Group.findById(group._id).exec();
|
||||
let newLeader = new User();
|
||||
updatedGroup.leader = newLeader._id;
|
||||
await updatedGroup.save();
|
||||
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
updatedGroup = await Group.findById(group._id).exec();
|
||||
|
||||
expect(updatedGroup.purchased.plan.dateTerminated).to.exist;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -516,14 +737,190 @@ describe('payments/index', () => {
|
||||
|
||||
it('sends a message from purchaser to recipient', async () => {
|
||||
await api.buyGems(data);
|
||||
let msg = '\`Hello recipient, sender has sent you 4 gems!\`';
|
||||
|
||||
expect(user.sendMessage).to.be.calledWith(recipient, '\`Hello recipient, sender has sent you 4 gems!\`');
|
||||
expect(user.sendMessage).to.be.calledWith(recipient, { receiverMsg: msg, senderMsg: msg });
|
||||
});
|
||||
|
||||
it('sends a push notification if user did not gift to self', async () => {
|
||||
await api.buyGems(data);
|
||||
expect(notifications.sendNotification).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('sends gem donation message in each participant\'s language', async () => {
|
||||
// TODO using english for both users because other languages are not loaded
|
||||
// for api.buyGems
|
||||
await recipient.update({
|
||||
'preferences.language': 'en',
|
||||
});
|
||||
await user.update({
|
||||
'preferences.language': 'en',
|
||||
});
|
||||
await api.buyGems(data);
|
||||
|
||||
let [recipientsMessageContent, sendersMessageContent] = ['en', 'en'].map((lang) => {
|
||||
let messageContent = t('giftedGemsFull', {
|
||||
username: recipient.profile.name,
|
||||
sender: user.profile.name,
|
||||
gemAmount: data.gift.gems.amount,
|
||||
}, lang);
|
||||
|
||||
return `\`${messageContent}\``;
|
||||
});
|
||||
|
||||
expect(user.sendMessage).to.be.calledWith(recipient, { receiverMsg: recipientsMessageContent, senderMsg: sendersMessageContent });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#upgradeGroupPlan', () => {
|
||||
let spy;
|
||||
|
||||
beforeEach(function () {
|
||||
spy = sinon.stub(stripe.subscriptions, 'update');
|
||||
spy.returnsPromise().resolves([]);
|
||||
data.groupId = group._id;
|
||||
data.sub.quantity = 3;
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore(stripe.subscriptions.update);
|
||||
});
|
||||
|
||||
it('updates a group plan quantity', async () => {
|
||||
data.paymentMethod = 'Stripe';
|
||||
await api.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 api.updateStripeGroupPlan(updatedGroup, stripe);
|
||||
|
||||
expect(spy.calledOnce).to.be.true;
|
||||
expect(updatedGroup.purchased.plan.quantity).to.eql(4);
|
||||
});
|
||||
|
||||
it('does not update a group plan quantity that has a payment method other than stripe', async () => {
|
||||
await api.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 api.updateStripeGroupPlan(updatedGroup, stripe);
|
||||
|
||||
expect(spy.calledOnce).to.be.false;
|
||||
expect(updatedGroup.purchased.plan.quantity).to.eql(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('payWithStripe', () => {
|
||||
let spy;
|
||||
let stripeCreateCustomerSpy;
|
||||
let createSubSpy;
|
||||
|
||||
beforeEach(function () {
|
||||
spy = sinon.stub(stripe.subscriptions, 'update');
|
||||
spy.returnsPromise().resolves;
|
||||
|
||||
stripeCreateCustomerSpy = sinon.stub(stripe.customers, 'create');
|
||||
let stripCustomerResponse = {
|
||||
subscriptions: {
|
||||
data: [{id: 'test-id'}],
|
||||
},
|
||||
};
|
||||
stripeCreateCustomerSpy.returnsPromise().resolves(stripCustomerResponse);
|
||||
|
||||
createSubSpy = sinon.stub(api, 'createSubscription');
|
||||
createSubSpy.returnsPromise().resolves({});
|
||||
|
||||
data.groupId = group._id;
|
||||
data.sub.quantity = 3;
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore(stripe.subscriptions.update);
|
||||
stripe.customers.create.restore();
|
||||
api.createSubscription.restore();
|
||||
});
|
||||
|
||||
it('subscribes with stripe', async () => {
|
||||
let token = 'test-token';
|
||||
let gift;
|
||||
let sub = data.sub;
|
||||
let groupId = group._id;
|
||||
let email = 'test@test.com';
|
||||
let headers = {};
|
||||
let coupon;
|
||||
|
||||
await api.payWithStripe({
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
sub,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe);
|
||||
|
||||
expect(stripeCreateCustomerSpy.calledOnce).to.be.true;
|
||||
expect(createSubSpy.calledOnce).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscribeWithAmazon', () => {
|
||||
let amazonSetBillingAgreementDetailsSpy;
|
||||
let amazonConfirmBillingAgreementSpy;
|
||||
let amazongAuthorizeOnBillingAgreementSpy;
|
||||
let createSubSpy;
|
||||
|
||||
beforeEach(function () {
|
||||
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(api, 'createSubscription');
|
||||
createSubSpy.returnsPromise().resolves({});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
amzLib.setBillingAgreementDetails.restore();
|
||||
amzLib.confirmBillingAgreement.restore();
|
||||
amzLib.authorizeOnBillingAgreement.restore();
|
||||
api.createSubscription.restore();
|
||||
});
|
||||
|
||||
it('subscribes with amazon', async () => {
|
||||
let billingAgreementId = 'billingAgreementId';
|
||||
let sub = data.sub;
|
||||
let coupon;
|
||||
let groupId = group._id;
|
||||
let headers = {};
|
||||
|
||||
await api.subscribeWithAmazon({
|
||||
billingAgreementId,
|
||||
sub,
|
||||
coupon,
|
||||
user,
|
||||
groupId,
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(amazonSetBillingAgreementDetailsSpy.calledOnce).to.be.true;
|
||||
expect(amazonConfirmBillingAgreementSpy.calledOnce).to.be.true;
|
||||
expect(amazongAuthorizeOnBillingAgreementSpy.calledOnce).to.be.true;
|
||||
expect(createSubSpy.calledOnce).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,27 +8,33 @@ import nconf from 'nconf';
|
||||
|
||||
describe('slack', () => {
|
||||
describe('sendFlagNotification', () => {
|
||||
let flagger, group, message;
|
||||
let data;
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox.stub(IncomingWebhook.prototype, 'send');
|
||||
flagger = {
|
||||
id: 'flagger-id',
|
||||
profile: {
|
||||
name: 'flagger',
|
||||
data = {
|
||||
authorEmail: 'author@example.com',
|
||||
flagger: {
|
||||
id: 'flagger-id',
|
||||
profile: {
|
||||
name: 'flagger',
|
||||
},
|
||||
preferences: {
|
||||
language: 'flagger-lang',
|
||||
},
|
||||
},
|
||||
group: {
|
||||
id: 'group-id',
|
||||
privacy: 'private',
|
||||
name: 'Some group',
|
||||
type: 'guild',
|
||||
},
|
||||
message: {
|
||||
id: 'chat-id',
|
||||
user: 'Author',
|
||||
uuid: 'author-id',
|
||||
text: 'some text',
|
||||
},
|
||||
};
|
||||
group = {
|
||||
id: 'group-id',
|
||||
privacy: 'private',
|
||||
name: 'Some group',
|
||||
type: 'guild',
|
||||
};
|
||||
message = {
|
||||
id: 'chat-id',
|
||||
user: 'Author',
|
||||
uuid: 'author-id',
|
||||
text: 'some text',
|
||||
};
|
||||
});
|
||||
|
||||
@@ -37,19 +43,15 @@ describe('slack', () => {
|
||||
});
|
||||
|
||||
it('sends a slack webhook', () => {
|
||||
slack.sendFlagNotification({
|
||||
flagger,
|
||||
group,
|
||||
message,
|
||||
});
|
||||
slack.sendFlagNotification(data);
|
||||
|
||||
expect(IncomingWebhook.prototype.send).to.be.calledOnce;
|
||||
expect(IncomingWebhook.prototype.send).to.be.calledWith({
|
||||
text: 'flagger (flagger-id) flagged a message',
|
||||
text: 'flagger (flagger-id) flagged a message (language: flagger-lang)',
|
||||
attachments: [{
|
||||
fallback: 'Flag Message',
|
||||
color: 'danger',
|
||||
author_name: 'Author - author-id',
|
||||
author_name: 'Author - author@example.com - author-id',
|
||||
title: 'Flag in Some group - (private guild)',
|
||||
title_link: undefined,
|
||||
text: 'some text',
|
||||
@@ -62,13 +64,9 @@ describe('slack', () => {
|
||||
});
|
||||
|
||||
it('includes a title link if guild is public', () => {
|
||||
group.privacy = 'public';
|
||||
data.group.privacy = 'public';
|
||||
|
||||
slack.sendFlagNotification({
|
||||
flagger,
|
||||
group,
|
||||
message,
|
||||
});
|
||||
slack.sendFlagNotification(data);
|
||||
|
||||
expect(IncomingWebhook.prototype.send).to.be.calledWithMatch({
|
||||
attachments: [sandbox.match({
|
||||
@@ -79,15 +77,11 @@ describe('slack', () => {
|
||||
});
|
||||
|
||||
it('links to tavern', () => {
|
||||
group.privacy = 'public';
|
||||
group.name = 'Tavern';
|
||||
group.id = TAVERN_ID;
|
||||
data.group.privacy = 'public';
|
||||
data.group.name = 'Tavern';
|
||||
data.group.id = TAVERN_ID;
|
||||
|
||||
slack.sendFlagNotification({
|
||||
flagger,
|
||||
group,
|
||||
message,
|
||||
});
|
||||
slack.sendFlagNotification(data);
|
||||
|
||||
expect(IncomingWebhook.prototype.send).to.be.calledWithMatch({
|
||||
attachments: [sandbox.match({
|
||||
@@ -98,14 +92,10 @@ describe('slack', () => {
|
||||
});
|
||||
|
||||
it('provides name for system message', () => {
|
||||
message.uuid = 'system';
|
||||
delete message.user;
|
||||
data.message.uuid = 'system';
|
||||
delete data.message.user;
|
||||
|
||||
slack.sendFlagNotification({
|
||||
flagger,
|
||||
group,
|
||||
message,
|
||||
});
|
||||
slack.sendFlagNotification(data);
|
||||
|
||||
expect(IncomingWebhook.prototype.send).to.be.calledWithMatch({
|
||||
attachments: [sandbox.match({
|
||||
@@ -121,11 +111,7 @@ describe('slack', () => {
|
||||
|
||||
expect(logger.error).to.be.calledOnce;
|
||||
|
||||
reRequiredSlack.sendFlagNotification({
|
||||
flagger,
|
||||
group,
|
||||
message,
|
||||
});
|
||||
reRequiredSlack.sendFlagNotification(data);
|
||||
|
||||
expect(IncomingWebhook.prototype.send).to.not.be.called;
|
||||
});
|
||||
|
||||
@@ -1,135 +1,376 @@
|
||||
import request from 'request';
|
||||
import { sendTaskWebhook } from '../../../../../website/server/libs/webhook';
|
||||
import {
|
||||
WebhookSender,
|
||||
taskScoredWebhook,
|
||||
groupChatReceivedWebhook,
|
||||
taskActivityWebhook,
|
||||
} from '../../../../../website/server/libs/webhook';
|
||||
|
||||
describe('webhooks', () => {
|
||||
let webhooks;
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox.stub(request, 'post');
|
||||
|
||||
webhooks = [{
|
||||
id: 'taskActivity',
|
||||
url: 'http://task-scored.com',
|
||||
enabled: true,
|
||||
type: 'taskActivity',
|
||||
options: {
|
||||
created: true,
|
||||
updated: true,
|
||||
deleted: true,
|
||||
scored: true,
|
||||
},
|
||||
}, {
|
||||
id: 'groupChatReceived',
|
||||
url: 'http://group-chat-received.com',
|
||||
enabled: true,
|
||||
type: 'groupChatReceived',
|
||||
options: {
|
||||
groupId: 'group-id',
|
||||
},
|
||||
}];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('sendTaskWebhook', () => {
|
||||
let task = {
|
||||
details: { _id: 'task-id' },
|
||||
delta: 1.4,
|
||||
direction: 'up',
|
||||
};
|
||||
describe('WebhookSender', () => {
|
||||
it('creates a new WebhookSender object', () => {
|
||||
let sendWebhook = new WebhookSender({
|
||||
type: 'custom',
|
||||
});
|
||||
|
||||
let data = {
|
||||
task,
|
||||
user: { _id: 'user-id' },
|
||||
};
|
||||
expect(sendWebhook.type).to.equal('custom');
|
||||
expect(sendWebhook).to.respondTo('send');
|
||||
});
|
||||
|
||||
it('does not send if no webhook endpoints exist', () => {
|
||||
let webhooks = { };
|
||||
it('provides default function for data transformation', () => {
|
||||
sandbox.spy(WebhookSender, 'defaultTransformData');
|
||||
let sendWebhook = new WebhookSender({
|
||||
type: 'custom',
|
||||
});
|
||||
|
||||
sendTaskWebhook(webhooks, data);
|
||||
let body = { foo: 'bar' };
|
||||
|
||||
sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}], body);
|
||||
|
||||
expect(WebhookSender.defaultTransformData).to.be.calledOnce;
|
||||
expect(request.post).to.be.calledOnce;
|
||||
expect(request.post).to.be.calledWithMatch({
|
||||
body,
|
||||
});
|
||||
});
|
||||
|
||||
it('can pass in a data transformation function', () => {
|
||||
sandbox.spy(WebhookSender, 'defaultTransformData');
|
||||
let sendWebhook = new WebhookSender({
|
||||
type: 'custom',
|
||||
transformData (data) {
|
||||
let dataToSend = Object.assign({baz: 'biz'}, data);
|
||||
|
||||
return dataToSend;
|
||||
},
|
||||
});
|
||||
|
||||
let body = { foo: 'bar' };
|
||||
|
||||
sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}], body);
|
||||
|
||||
expect(WebhookSender.defaultTransformData).to.not.be.called;
|
||||
expect(request.post).to.be.calledOnce;
|
||||
expect(request.post).to.be.calledWithMatch({
|
||||
body: {
|
||||
foo: 'bar',
|
||||
baz: 'biz',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('provieds a default filter function', () => {
|
||||
sandbox.spy(WebhookSender, 'defaultWebhookFilter');
|
||||
let sendWebhook = new WebhookSender({
|
||||
type: 'custom',
|
||||
});
|
||||
|
||||
let body = { foo: 'bar' };
|
||||
|
||||
sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}], body);
|
||||
|
||||
expect(WebhookSender.defaultWebhookFilter).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('can pass in a webhook filter function', () => {
|
||||
sandbox.spy(WebhookSender, 'defaultWebhookFilter');
|
||||
let sendWebhook = new WebhookSender({
|
||||
type: 'custom',
|
||||
webhookFilter (hook) {
|
||||
return hook.url !== 'http://custom-url.com';
|
||||
},
|
||||
});
|
||||
|
||||
let body = { foo: 'bar' };
|
||||
|
||||
sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}], body);
|
||||
|
||||
expect(WebhookSender.defaultWebhookFilter).to.not.be.called;
|
||||
expect(request.post).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not send if no webhooks are enabled', () => {
|
||||
let webhooks = {
|
||||
'some-id': {
|
||||
sort: 0,
|
||||
id: 'some-id',
|
||||
enabled: false,
|
||||
url: 'http://example.org/endpoint',
|
||||
it('can pass in a webhook filter function that filters on data', () => {
|
||||
sandbox.spy(WebhookSender, 'defaultWebhookFilter');
|
||||
let sendWebhook = new WebhookSender({
|
||||
type: 'custom',
|
||||
webhookFilter (hook, data) {
|
||||
return hook.options.foo === data.foo;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
sendTaskWebhook(webhooks, data);
|
||||
let body = { foo: 'bar' };
|
||||
|
||||
expect(request.post).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not send if webhook url is not valid', () => {
|
||||
let webhooks = {
|
||||
'some-id': {
|
||||
sort: 0,
|
||||
id: 'some-id',
|
||||
enabled: true,
|
||||
url: 'http://malformedurl/endpoint',
|
||||
},
|
||||
};
|
||||
|
||||
sendTaskWebhook(webhooks, data);
|
||||
|
||||
expect(request.post).to.not.be.called;
|
||||
});
|
||||
|
||||
it('sends task direction, task, task delta, and abridged user data', () => {
|
||||
let webhooks = {
|
||||
'some-id': {
|
||||
sort: 0,
|
||||
id: 'some-id',
|
||||
enabled: true,
|
||||
url: 'http://example.org/endpoint',
|
||||
},
|
||||
};
|
||||
|
||||
sendTaskWebhook(webhooks, data);
|
||||
sendWebhook.send([
|
||||
{ id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom', options: { foo: 'bar' }},
|
||||
{ id: 'other-custom-webhook', url: 'http://other-custom-url.com', enabled: true, type: 'custom', options: { foo: 'foo' }},
|
||||
], body);
|
||||
|
||||
expect(request.post).to.be.calledOnce;
|
||||
expect(request.post).to.be.calledWith({
|
||||
url: 'http://example.org/endpoint',
|
||||
body: {
|
||||
direction: 'up',
|
||||
task: { _id: 'task-id' },
|
||||
delta: 1.4,
|
||||
user: {
|
||||
_id: 'user-id',
|
||||
},
|
||||
},
|
||||
expect(request.post).to.be.calledWithMatch({
|
||||
url: 'http://custom-url.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores disabled webhooks', () => {
|
||||
let sendWebhook = new WebhookSender({
|
||||
type: 'custom',
|
||||
});
|
||||
|
||||
let body = { foo: 'bar' };
|
||||
|
||||
sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: false, type: 'custom'}], body);
|
||||
|
||||
expect(request.post).to.not.be.called;
|
||||
});
|
||||
|
||||
it('ignores webhooks with invalid urls', () => {
|
||||
let sendWebhook = new WebhookSender({
|
||||
type: 'custom',
|
||||
});
|
||||
|
||||
let body = { foo: 'bar' };
|
||||
|
||||
sendWebhook.send([{id: 'custom-webhook', url: 'httxp://custom-url!!', enabled: true, type: 'custom'}], body);
|
||||
|
||||
expect(request.post).to.not.be.called;
|
||||
});
|
||||
|
||||
it('ignores webhooks of other types', () => {
|
||||
let sendWebhook = new WebhookSender({
|
||||
type: 'custom',
|
||||
});
|
||||
|
||||
let body = { foo: 'bar' };
|
||||
|
||||
sendWebhook.send([
|
||||
{ id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'},
|
||||
{ id: 'other-webhook', url: 'http://other-url.com', enabled: true, type: 'other'},
|
||||
], body);
|
||||
|
||||
expect(request.post).to.be.calledOnce;
|
||||
expect(request.post).to.be.calledWithMatch({
|
||||
url: 'http://custom-url.com',
|
||||
body,
|
||||
json: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('sends a post request for each webhook endpoint', () => {
|
||||
let webhooks = {
|
||||
'some-id': {
|
||||
sort: 0,
|
||||
id: 'some-id',
|
||||
enabled: true,
|
||||
url: 'http://example.org/endpoint',
|
||||
},
|
||||
'second-webhook': {
|
||||
sort: 1,
|
||||
id: 'second-webhook',
|
||||
enabled: true,
|
||||
url: 'http://example.com/2/endpoint',
|
||||
},
|
||||
};
|
||||
it('sends multiple webhooks of the same type', () => {
|
||||
let sendWebhook = new WebhookSender({
|
||||
type: 'custom',
|
||||
});
|
||||
|
||||
sendTaskWebhook(webhooks, data);
|
||||
let body = { foo: 'bar' };
|
||||
|
||||
sendWebhook.send([
|
||||
{ id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'},
|
||||
{ id: 'other-custom-webhook', url: 'http://other-url.com', enabled: true, type: 'custom'},
|
||||
], body);
|
||||
|
||||
expect(request.post).to.be.calledTwice;
|
||||
expect(request.post).to.be.calledWith({
|
||||
url: 'http://example.org/endpoint',
|
||||
body: {
|
||||
direction: 'up',
|
||||
task: { _id: 'task-id' },
|
||||
delta: 1.4,
|
||||
user: {
|
||||
_id: 'user-id',
|
||||
},
|
||||
},
|
||||
expect(request.post).to.be.calledWithMatch({
|
||||
url: 'http://custom-url.com',
|
||||
body,
|
||||
json: true,
|
||||
});
|
||||
expect(request.post).to.be.calledWith({
|
||||
url: 'http://example.com/2/endpoint',
|
||||
body: {
|
||||
direction: 'up',
|
||||
task: { _id: 'task-id' },
|
||||
delta: 1.4,
|
||||
user: {
|
||||
_id: 'user-id',
|
||||
},
|
||||
},
|
||||
expect(request.post).to.be.calledWithMatch({
|
||||
url: 'http://other-url.com',
|
||||
body,
|
||||
json: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('taskScoredWebhook', () => {
|
||||
let data;
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
user: {
|
||||
_id: 'user-id',
|
||||
_tmp: {foo: 'bar'},
|
||||
stats: {
|
||||
lvl: 5,
|
||||
int: 10,
|
||||
str: 5,
|
||||
exp: 423,
|
||||
toJSON () {
|
||||
return this;
|
||||
},
|
||||
},
|
||||
addComputedStatsToJSONObj () {
|
||||
let mockStats = Object.assign({
|
||||
maxHealth: 50,
|
||||
maxMP: 103,
|
||||
toNextLevel: 40,
|
||||
}, this.stats);
|
||||
|
||||
delete mockStats.toJSON;
|
||||
|
||||
return mockStats;
|
||||
},
|
||||
},
|
||||
task: {
|
||||
text: 'text',
|
||||
},
|
||||
direction: 'up',
|
||||
delta: 176,
|
||||
};
|
||||
});
|
||||
|
||||
it('sends task and stats data', () => {
|
||||
taskScoredWebhook.send(webhooks, data);
|
||||
|
||||
expect(request.post).to.be.calledOnce;
|
||||
expect(request.post).to.be.calledWithMatch({
|
||||
body: {
|
||||
type: 'scored',
|
||||
user: {
|
||||
_id: 'user-id',
|
||||
_tmp: {foo: 'bar'},
|
||||
stats: {
|
||||
lvl: 5,
|
||||
int: 10,
|
||||
str: 5,
|
||||
exp: 423,
|
||||
toNextLevel: 40,
|
||||
maxHealth: 50,
|
||||
maxMP: 103,
|
||||
},
|
||||
},
|
||||
task: {
|
||||
text: 'text',
|
||||
},
|
||||
direction: 'up',
|
||||
delta: 176,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('does not send task scored data if scored option is not true', () => {
|
||||
webhooks[0].options.scored = false;
|
||||
|
||||
taskScoredWebhook.send(webhooks, data);
|
||||
|
||||
expect(request.post).to.not.be.called;
|
||||
});
|
||||
});
|
||||
|
||||
describe('taskActivityWebhook', () => {
|
||||
let data;
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
task: {
|
||||
text: 'text',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
['created', 'updated', 'deleted'].forEach((type) => {
|
||||
it(`sends ${type} tasks`, () => {
|
||||
data.type = type;
|
||||
|
||||
taskActivityWebhook.send(webhooks, data);
|
||||
|
||||
expect(request.post).to.be.calledOnce;
|
||||
expect(request.post).to.be.calledWithMatch({
|
||||
body: {
|
||||
type,
|
||||
task: data.task,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it(`does not send task ${type} data if ${type} option is not true`, () => {
|
||||
data.type = type;
|
||||
webhooks[0].options[type] = false;
|
||||
|
||||
taskActivityWebhook.send(webhooks, data);
|
||||
|
||||
expect(request.post).to.not.be.called;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('groupChatReceivedWebhook', () => {
|
||||
it('sends chat data', () => {
|
||||
let data = {
|
||||
group: {
|
||||
id: 'group-id',
|
||||
name: 'some group',
|
||||
otherData: 'foo',
|
||||
},
|
||||
chat: {
|
||||
id: 'some-id',
|
||||
text: 'message',
|
||||
},
|
||||
};
|
||||
|
||||
groupChatReceivedWebhook.send(webhooks, data);
|
||||
|
||||
expect(request.post).to.be.calledOnce;
|
||||
expect(request.post).to.be.calledWithMatch({
|
||||
body: {
|
||||
group: {
|
||||
id: 'group-id',
|
||||
name: 'some group',
|
||||
},
|
||||
chat: {
|
||||
id: 'some-id',
|
||||
text: 'message',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('does not send chat data for group if not selected', () => {
|
||||
let data = {
|
||||
group: {
|
||||
id: 'not-group-id',
|
||||
name: 'some group',
|
||||
otherData: 'foo',
|
||||
},
|
||||
chat: {
|
||||
id: 'some-id',
|
||||
text: 'message',
|
||||
},
|
||||
};
|
||||
|
||||
groupChatReceivedWebhook.send(webhooks, data);
|
||||
|
||||
expect(request.post).to.not.be.called;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('cors middleware', () => {
|
||||
expect(res.set).to.have.been.calledWith({
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'OPTIONS,GET,POST,PUT,HEAD,DELETE',
|
||||
'Access-Control-Allow-Headers': 'Content-Type,Accept,Content-Encoding,X-Requested-With,x-api-user,x-api-key',
|
||||
'Access-Control-Allow-Headers': 'Content-Type,Accept,Content-Encoding,X-Requested-With,x-api-user,x-api-key,x-client',
|
||||
});
|
||||
expect(res.sendStatus).to.not.have.been.called;
|
||||
expect(next).to.have.been.called.once;
|
||||
@@ -32,7 +32,7 @@ describe('cors middleware', () => {
|
||||
expect(res.set).to.have.been.calledWith({
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'OPTIONS,GET,POST,PUT,HEAD,DELETE',
|
||||
'Access-Control-Allow-Headers': 'Content-Type,Accept,Content-Encoding,X-Requested-With,x-api-user,x-api-key',
|
||||
'Access-Control-Allow-Headers': 'Content-Type,Accept,Content-Encoding,X-Requested-With,x-api-user,x-api-key,x-client',
|
||||
});
|
||||
expect(res.sendStatus).to.have.been.calledWith(200);
|
||||
expect(next).to.not.have.been.called;
|
||||
|
||||
@@ -98,7 +98,6 @@ describe('response middleware', () => {
|
||||
{
|
||||
type: notification.type,
|
||||
id: notification.id,
|
||||
createdAt: notification.createdAt,
|
||||
data: {},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -6,32 +6,36 @@ import common from '../../../../../website/common/';
|
||||
import { each, find } from 'lodash';
|
||||
|
||||
describe('Challenge Model', () => {
|
||||
let guild, leader, challenge, task, tasksToTest;
|
||||
let guild, leader, challenge, task;
|
||||
let tasksToTest = {
|
||||
habit: {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
up: false,
|
||||
down: true,
|
||||
notes: 'test notes',
|
||||
},
|
||||
todo: {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
notes: 'test notes',
|
||||
},
|
||||
daily: {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
frequency: 'daily',
|
||||
everyX: 5,
|
||||
startDate: new Date(),
|
||||
notes: 'test notes',
|
||||
},
|
||||
reward: {
|
||||
text: 'test reward',
|
||||
type: 'reward',
|
||||
notes: 'test notes',
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
tasksToTest = {
|
||||
habit: {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
up: false,
|
||||
down: true,
|
||||
},
|
||||
todo: {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
},
|
||||
daily: {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
frequency: 'daily',
|
||||
everyX: 5,
|
||||
startDate: new Date(),
|
||||
},
|
||||
reward: {
|
||||
text: 'test reward',
|
||||
type: 'reward',
|
||||
},
|
||||
};
|
||||
guild = new Group({
|
||||
name: 'test party',
|
||||
type: 'guild',
|
||||
@@ -77,6 +81,7 @@ describe('Challenge Model', () => {
|
||||
});
|
||||
|
||||
expect(syncedTask).to.exist;
|
||||
expect(syncedTask.notes).to.eql(task.notes);
|
||||
});
|
||||
|
||||
it('syncs a challenge to a user', async () => {
|
||||
@@ -96,8 +101,8 @@ describe('Challenge Model', () => {
|
||||
});
|
||||
|
||||
expect(updatedNewMember.challenges).to.contain(challenge._id);
|
||||
expect(updatedNewMember.tags[3].id).to.equal(challenge._id);
|
||||
expect(updatedNewMember.tags[3].name).to.equal(challenge.shortName);
|
||||
expect(updatedNewMember.tags[7].id).to.equal(challenge._id);
|
||||
expect(updatedNewMember.tags[7].name).to.equal(challenge.shortName);
|
||||
expect(syncedTask).to.exist;
|
||||
});
|
||||
|
||||
@@ -161,7 +166,7 @@ describe('Challenge Model', () => {
|
||||
|
||||
context('type specific updates', () => {
|
||||
it('updates habit specific field to challenge and challenge members', async () => {
|
||||
task = new Tasks.habit(Tasks.Task.sanitize(tasksToTest.habit)); // eslint-disable-line babel/new-cap
|
||||
task = new Tasks.habit(Tasks.Task.sanitize(tasksToTest.habit)); // eslint-disable-line new-cap
|
||||
task.challenge.id = challenge._id;
|
||||
await task.save();
|
||||
|
||||
@@ -180,7 +185,7 @@ describe('Challenge Model', () => {
|
||||
});
|
||||
|
||||
it('updates todo specific field to challenge and challenge members', async () => {
|
||||
task = new Tasks.todo(Tasks.Task.sanitize(tasksToTest.todo)); // eslint-disable-line babel/new-cap
|
||||
task = new Tasks.todo(Tasks.Task.sanitize(tasksToTest.todo)); // eslint-disable-line new-cap
|
||||
task.challenge.id = challenge._id;
|
||||
await task.save();
|
||||
|
||||
@@ -196,7 +201,7 @@ describe('Challenge Model', () => {
|
||||
});
|
||||
|
||||
it('does not update checklists on the user task', async () => {
|
||||
task = new Tasks.todo(Tasks.Task.sanitize(tasksToTest.todo)); // eslint-disable-line babel/new-cap
|
||||
task = new Tasks.todo(Tasks.Task.sanitize(tasksToTest.todo)); // eslint-disable-line new-cap
|
||||
task.challenge.id = challenge._id;
|
||||
await task.save();
|
||||
|
||||
@@ -214,7 +219,7 @@ describe('Challenge Model', () => {
|
||||
});
|
||||
|
||||
it('updates daily specific field to challenge and challenge members', async () => {
|
||||
task = new Tasks.daily(Tasks.Task.sanitize(tasksToTest.daily)); // eslint-disable-line babel/new-cap
|
||||
task = new Tasks.daily(Tasks.Task.sanitize(tasksToTest.daily)); // eslint-disable-line new-cap
|
||||
task.challenge.id = challenge._id;
|
||||
await task.save();
|
||||
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { sleep } from '../../../../helpers/api-unit.helper';
|
||||
import { model as Group } from '../../../../../website/server/models/group';
|
||||
import { model as Group, INVITES_LIMIT } from '../../../../../website/server/models/group';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import {
|
||||
BadRequest,
|
||||
} from '../../../../../website/server/libs/errors';
|
||||
import { quests as questScrolls } from '../../../../../website/common/script/content';
|
||||
import { groupChatReceivedWebhook } from '../../../../../website/server/libs/webhook';
|
||||
import * as email from '../../../../../website/server/libs/email';
|
||||
import validator from 'validator';
|
||||
import { TAVERN_ID } from '../../../../../website/common/script/';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import shared from '../../../../../website/common';
|
||||
|
||||
describe('Group Model', () => {
|
||||
let party, questLeader, participatingMember, nonParticipatingMember, undecidedMember;
|
||||
@@ -433,6 +439,158 @@ describe('Group Model', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateInvitations', () => {
|
||||
let res;
|
||||
|
||||
beforeEach(() => {
|
||||
res = {
|
||||
t: sandbox.spy(),
|
||||
};
|
||||
});
|
||||
|
||||
it('throws an error if no uuids or emails are passed in', (done) => {
|
||||
try {
|
||||
Group.validateInvitations(null, null, res);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(res.t).to.be.calledOnce;
|
||||
expect(res.t).to.be.calledWith('canOnlyInviteEmailUuid');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if only uuids are passed in, but they are not an array', (done) => {
|
||||
try {
|
||||
Group.validateInvitations({ uuid: 'user-id'}, null, res);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(res.t).to.be.calledOnce;
|
||||
expect(res.t).to.be.calledWith('uuidsMustBeAnArray');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if only emails are passed in, but they are not an array', (done) => {
|
||||
try {
|
||||
Group.validateInvitations(null, { emails: 'user@example.com'}, res);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(res.t).to.be.calledOnce;
|
||||
expect(res.t).to.be.calledWith('emailsMustBeAnArray');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if emails are not passed in, and uuid array is empty', (done) => {
|
||||
try {
|
||||
Group.validateInvitations([], null, res);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(res.t).to.be.calledOnce;
|
||||
expect(res.t).to.be.calledWith('inviteMissingUuid');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if uuids are not passed in, and email array is empty', (done) => {
|
||||
try {
|
||||
Group.validateInvitations(null, [], res);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(res.t).to.be.calledOnce;
|
||||
expect(res.t).to.be.calledWith('inviteMissingEmail');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if uuids and emails are passed in as empty arrays', (done) => {
|
||||
try {
|
||||
Group.validateInvitations([], [], res);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(res.t).to.be.calledOnce;
|
||||
expect(res.t).to.be.calledWith('inviteMustNotBeEmpty');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if total invites exceed max invite constant', (done) => {
|
||||
let uuids = [];
|
||||
let emails = [];
|
||||
|
||||
for (let i = 0; i < INVITES_LIMIT / 2; i++) {
|
||||
uuids.push(`user-id-${i}`);
|
||||
emails.push(`user-${i}@example.com`);
|
||||
}
|
||||
|
||||
uuids.push('one-more-uuid'); // to put it over the limit
|
||||
|
||||
try {
|
||||
Group.validateInvitations(uuids, emails, res);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(res.t).to.be.calledOnce;
|
||||
expect(res.t).to.be.calledWith('canOnlyInviteMaxInvites', {maxInvites: INVITES_LIMIT });
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('does not throw error if number of invites matches max invite limit', () => {
|
||||
let uuids = [];
|
||||
let emails = [];
|
||||
|
||||
for (let i = 0; i < INVITES_LIMIT / 2; i++) {
|
||||
uuids.push(`user-id-${i}`);
|
||||
emails.push(`user-${i}@example.com`);
|
||||
}
|
||||
|
||||
expect(function () {
|
||||
Group.validateInvitations(uuids, emails, res);
|
||||
}).to.not.throw();
|
||||
});
|
||||
|
||||
|
||||
it('does not throw an error if only user ids are passed in', () => {
|
||||
expect(function () {
|
||||
Group.validateInvitations(['user-id', 'user-id2'], null, res);
|
||||
}).to.not.throw();
|
||||
|
||||
expect(res.t).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not throw an error if only emails are passed in', () => {
|
||||
expect(function () {
|
||||
Group.validateInvitations(null, ['user1@example.com', 'user2@example.com'], res);
|
||||
}).to.not.throw();
|
||||
|
||||
expect(res.t).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not throw an error if both uuids and emails are passed in', () => {
|
||||
expect(function () {
|
||||
Group.validateInvitations(['user-id', 'user-id2'], ['user1@example.com', 'user2@example.com'], res);
|
||||
}).to.not.throw();
|
||||
|
||||
expect(res.t).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not throw an error if uuids are passed in and emails are an empty array', () => {
|
||||
expect(function () {
|
||||
Group.validateInvitations(['user-id', 'user-id2'], [], res);
|
||||
}).to.not.throw();
|
||||
|
||||
expect(res.t).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not throw an error if emails are passed in and uuids are an empty array', () => {
|
||||
expect(function () {
|
||||
Group.validateInvitations([], ['user1@example.com', 'user2@example.com'], res);
|
||||
}).to.not.throw();
|
||||
|
||||
expect(res.t).to.not.be.called;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Instance Methods', () => {
|
||||
@@ -473,24 +631,105 @@ describe('Group Model', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('deletes a private group when the last member leaves', async () => {
|
||||
party.memberCount = 1;
|
||||
|
||||
it('deletes a private party when the last member leaves', async () => {
|
||||
await party.leave(participatingMember);
|
||||
await party.leave(questLeader);
|
||||
await party.leave(nonParticipatingMember);
|
||||
await party.leave(undecidedMember);
|
||||
|
||||
party = await Group.findOne({_id: party._id});
|
||||
expect(party).to.not.exist;
|
||||
});
|
||||
|
||||
it('does not delete a public group when the last member leaves', async () => {
|
||||
it('does not delete a private group when the last member leaves and a subscription is active', async () => {
|
||||
party.memberCount = 1;
|
||||
party.purchased.plan.customerId = '110002222333';
|
||||
|
||||
await expect(party.leave(participatingMember))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
name: 'NotAuthorized',
|
||||
httpCode: 401,
|
||||
message: shared.i18n.t('cannotDeleteActiveGroup'),
|
||||
});
|
||||
|
||||
party = await Group.findOne({_id: party._id});
|
||||
expect(party).to.exist;
|
||||
expect(party.memberCount).to.eql(1);
|
||||
});
|
||||
|
||||
it('does not delete a public group when the last member leaves', async () => {
|
||||
party.privacy = 'public';
|
||||
|
||||
await party.leave(participatingMember);
|
||||
await party.leave(questLeader);
|
||||
await party.leave(nonParticipatingMember);
|
||||
await party.leave(undecidedMember);
|
||||
|
||||
party = await Group.findOne({_id: party._id});
|
||||
expect(party).to.exist;
|
||||
});
|
||||
|
||||
it('does not delete a private party when the member count reaches zero if there are still members', async () => {
|
||||
party.memberCount = 1;
|
||||
|
||||
await party.leave(participatingMember);
|
||||
|
||||
party = await Group.findOne({_id: party._id});
|
||||
expect(party).to.exist;
|
||||
});
|
||||
|
||||
it('deletes a private guild when the last member leaves', async () => {
|
||||
let guild = new Group({
|
||||
name: 'test guild',
|
||||
type: 'guild',
|
||||
memberCount: 1,
|
||||
});
|
||||
|
||||
let leader = new User({
|
||||
guilds: [guild._id],
|
||||
});
|
||||
|
||||
guild.leader = leader._id;
|
||||
|
||||
await Promise.all([
|
||||
guild.save(),
|
||||
leader.save(),
|
||||
]);
|
||||
|
||||
await guild.leave(leader);
|
||||
|
||||
guild = await Group.findOne({_id: guild._id});
|
||||
expect(guild).to.not.exist;
|
||||
});
|
||||
|
||||
it('does not delete a private guild when the member count reaches zero if there are still members', async () => {
|
||||
let guild = new Group({
|
||||
name: 'test guild',
|
||||
type: 'guild',
|
||||
memberCount: 1,
|
||||
});
|
||||
|
||||
let leader = new User({
|
||||
guilds: [guild._id],
|
||||
});
|
||||
|
||||
let member = new User({
|
||||
guilds: [guild._id],
|
||||
});
|
||||
|
||||
guild.leader = leader._id;
|
||||
|
||||
await Promise.all([
|
||||
guild.save(),
|
||||
leader.save(),
|
||||
member.save(),
|
||||
]);
|
||||
|
||||
await guild.leave(member);
|
||||
|
||||
guild = await Group.findOne({_id: guild._id});
|
||||
expect(guild).to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
describe('#sendChat', () => {
|
||||
@@ -906,8 +1145,45 @@ describe('Group Model', () => {
|
||||
[nonParticipatingMember._id]: false,
|
||||
[undecidedMember._id]: null,
|
||||
};
|
||||
});
|
||||
|
||||
sandbox.spy(User, 'update');
|
||||
describe('user update retry failures', () => {
|
||||
let successfulMock = {
|
||||
exec: () => {
|
||||
return Promise.resolve({raw: 'sucess'});
|
||||
},
|
||||
};
|
||||
let failedMock = {
|
||||
exec: () => {
|
||||
return Promise.reject(new Error('error'));
|
||||
},
|
||||
};
|
||||
|
||||
it('doesn\'t retry successful operations', async () => {
|
||||
sandbox.stub(User, 'update').returns(successfulMock);
|
||||
|
||||
await party.finishQuest(quest);
|
||||
|
||||
expect(User.update).to.be.calledTwice;
|
||||
});
|
||||
|
||||
it('stops retrying when a successful update has occurred', async () => {
|
||||
let updateStub = sandbox.stub(User, 'update');
|
||||
updateStub.onCall(0).returns(failedMock);
|
||||
updateStub.returns(successfulMock);
|
||||
|
||||
await party.finishQuest(quest);
|
||||
|
||||
expect(User.update).to.be.calledThrice;
|
||||
});
|
||||
|
||||
it('retries failed updates at most five times per user', async () => {
|
||||
sandbox.stub(User, 'update').returns(failedMock);
|
||||
|
||||
await expect(party.finishQuest(quest)).to.eventually.be.rejected;
|
||||
|
||||
expect(User.update.callCount).to.eql(10);
|
||||
});
|
||||
});
|
||||
|
||||
it('gives out achievements', async () => {
|
||||
@@ -983,14 +1259,34 @@ describe('Group Model', () => {
|
||||
expect(updatedParticipatingMember.items.hatchingPotions.Shade).to.eql(2);
|
||||
});
|
||||
|
||||
it('awards quests', async () => {
|
||||
it('awards quest scrolls to owner', async () => {
|
||||
let questAwardQuest = questScrolls.vice2;
|
||||
|
||||
await party.finishQuest(questAwardQuest);
|
||||
|
||||
let updatedLeader = await User.findById(questLeader._id);
|
||||
|
||||
expect(updatedLeader.items.quests.vice3).to.eql(1);
|
||||
});
|
||||
|
||||
it('awards non quest leader rewards to quest leader', async () => {
|
||||
let gearQuest = questScrolls.vice3;
|
||||
|
||||
await party.finishQuest(gearQuest);
|
||||
|
||||
let updatedLeader = await User.findById(questLeader._id);
|
||||
|
||||
expect(updatedLeader.items.gear.owned.weapon_special_2).to.eql(true);
|
||||
});
|
||||
|
||||
it('doesn\'t award quest owner rewards to all participants', async () => {
|
||||
let questAwardQuest = questScrolls.vice2;
|
||||
|
||||
await party.finishQuest(questAwardQuest);
|
||||
|
||||
let updatedParticipatingMember = await User.findById(participatingMember._id);
|
||||
|
||||
expect(updatedParticipatingMember.items.quests.vice3).to.eql(1);
|
||||
expect(updatedParticipatingMember.items.quests.vice3).to.not.exist;
|
||||
});
|
||||
|
||||
it('awards pets', async () => {
|
||||
@@ -1016,13 +1312,15 @@ describe('Group Model', () => {
|
||||
|
||||
context('Party quests', () => {
|
||||
it('updates participating members with rewards', async () => {
|
||||
sandbox.spy(User, 'update');
|
||||
await party.finishQuest(quest);
|
||||
|
||||
expect(User.update).to.be.calledOnce;
|
||||
expect(User.update).to.be.calledTwice;
|
||||
expect(User.update).to.be.calledWithMatch({
|
||||
_id: {
|
||||
$in: [questLeader._id, participatingMember._id],
|
||||
},
|
||||
_id: questLeader._id,
|
||||
});
|
||||
expect(User.update).to.be.calledWithMatch({
|
||||
_id: participatingMember._id,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1049,6 +1347,7 @@ describe('Group Model', () => {
|
||||
});
|
||||
|
||||
it('updates all users with rewards', async () => {
|
||||
sandbox.spy(User, 'update');
|
||||
await party.finishQuest(tavernQuest);
|
||||
|
||||
expect(User.update).to.be.calledOnce;
|
||||
@@ -1064,5 +1363,163 @@ describe('Group Model', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendGroupChatReceivedWebhooks', () => {
|
||||
beforeEach(() => {
|
||||
sandbox.stub(groupChatReceivedWebhook, 'send');
|
||||
});
|
||||
|
||||
it('looks for users in specified guild with webhooks', () => {
|
||||
sandbox.spy(User, 'find');
|
||||
|
||||
let guild = new Group({
|
||||
type: 'guild',
|
||||
});
|
||||
|
||||
guild.sendGroupChatReceivedWebhooks({});
|
||||
|
||||
expect(User.find).to.be.calledWith({
|
||||
webhooks: {
|
||||
$elemMatch: {
|
||||
type: 'groupChatReceived',
|
||||
'options.groupId': guild._id,
|
||||
},
|
||||
},
|
||||
guilds: guild._id,
|
||||
});
|
||||
});
|
||||
|
||||
it('looks for users in specified party with webhooks', () => {
|
||||
sandbox.spy(User, 'find');
|
||||
|
||||
party.sendGroupChatReceivedWebhooks({});
|
||||
|
||||
expect(User.find).to.be.calledWith({
|
||||
webhooks: {
|
||||
$elemMatch: {
|
||||
type: 'groupChatReceived',
|
||||
'options.groupId': party._id,
|
||||
},
|
||||
},
|
||||
'party._id': party._id,
|
||||
});
|
||||
});
|
||||
|
||||
it('sends webhooks for users with webhooks', async () => {
|
||||
let guild = new Group({
|
||||
name: 'some guild',
|
||||
type: 'guild',
|
||||
});
|
||||
|
||||
let chat = {message: 'text'};
|
||||
let memberWithWebhook = new User({
|
||||
guilds: [guild._id],
|
||||
webhooks: [{
|
||||
type: 'groupChatReceived',
|
||||
url: 'http://someurl.com',
|
||||
options: {
|
||||
groupId: guild._id,
|
||||
},
|
||||
}],
|
||||
});
|
||||
let memberWithoutWebhook = new User({
|
||||
guilds: [guild._id],
|
||||
});
|
||||
let nonMemberWithWebhooks = new User({
|
||||
webhooks: [{
|
||||
type: 'groupChatReceived',
|
||||
url: 'http://a-different-url.com',
|
||||
options: {
|
||||
groupId: generateUUID(),
|
||||
},
|
||||
}],
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
memberWithWebhook.save(),
|
||||
memberWithoutWebhook.save(),
|
||||
nonMemberWithWebhooks.save(),
|
||||
]);
|
||||
|
||||
guild.leader = memberWithWebhook._id;
|
||||
|
||||
await guild.save();
|
||||
|
||||
guild.sendGroupChatReceivedWebhooks(chat);
|
||||
|
||||
await sleep();
|
||||
|
||||
expect(groupChatReceivedWebhook.send).to.be.calledOnce;
|
||||
|
||||
let args = groupChatReceivedWebhook.send.args[0];
|
||||
let webhooks = args[0];
|
||||
let options = args[1];
|
||||
|
||||
expect(webhooks).to.have.a.lengthOf(1);
|
||||
expect(webhooks[0].id).to.eql(memberWithWebhook.webhooks[0].id);
|
||||
expect(options.group).to.eql(guild);
|
||||
expect(options.chat).to.eql(chat);
|
||||
});
|
||||
|
||||
it('sends webhooks for each user with webhooks in group', async () => {
|
||||
let guild = new Group({
|
||||
name: 'some guild',
|
||||
type: 'guild',
|
||||
});
|
||||
|
||||
let chat = {message: 'text'};
|
||||
let memberWithWebhook = new User({
|
||||
guilds: [guild._id],
|
||||
webhooks: [{
|
||||
type: 'groupChatReceived',
|
||||
url: 'http://someurl.com',
|
||||
options: {
|
||||
groupId: guild._id,
|
||||
},
|
||||
}],
|
||||
});
|
||||
let memberWithWebhook2 = new User({
|
||||
guilds: [guild._id],
|
||||
webhooks: [{
|
||||
type: 'groupChatReceived',
|
||||
url: 'http://another-member.com',
|
||||
options: {
|
||||
groupId: guild._id,
|
||||
},
|
||||
}],
|
||||
});
|
||||
let memberWithWebhook3 = new User({
|
||||
guilds: [guild._id],
|
||||
webhooks: [{
|
||||
type: 'groupChatReceived',
|
||||
url: 'http://a-third-member.com',
|
||||
options: {
|
||||
groupId: guild._id,
|
||||
},
|
||||
}],
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
memberWithWebhook.save(),
|
||||
memberWithWebhook2.save(),
|
||||
memberWithWebhook3.save(),
|
||||
]);
|
||||
|
||||
guild.leader = memberWithWebhook._id;
|
||||
|
||||
await guild.save();
|
||||
|
||||
guild.sendGroupChatReceivedWebhooks(chat);
|
||||
|
||||
await sleep();
|
||||
|
||||
expect(groupChatReceivedWebhook.send).to.be.calledThrice;
|
||||
|
||||
let args = groupChatReceivedWebhook.send.args;
|
||||
expect(args.find(arg => arg[0][0].id === memberWithWebhook.webhooks[0].id)).to.be.exist;
|
||||
expect(args.find(arg => arg[0][0].id === memberWithWebhook2.webhooks[0].id)).to.be.exist;
|
||||
expect(args.find(arg => arg[0][0].id === memberWithWebhook3.webhooks[0].id)).to.be.exist;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { model as Challenge } from '../../../../../website/server/models/challen
|
||||
import { model as Group } from '../../../../../website/server/models/group';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import * as Tasks from '../../../../../website/server/models/task';
|
||||
import { each, find } from 'lodash';
|
||||
import { each, find, findIndex } from 'lodash';
|
||||
|
||||
describe('Group Task Methods', () => {
|
||||
let guild, leader, challenge, task;
|
||||
@@ -68,11 +68,29 @@ describe('Group Task Methods', () => {
|
||||
task = new Tasks[`${taskType}`](Tasks.Task.sanitize(taskValue));
|
||||
task.group.id = guild._id;
|
||||
await task.save();
|
||||
if (task.checklist) {
|
||||
task.checklist.push({
|
||||
text: 'Checklist Item 1',
|
||||
completed: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('syncs an assigned task to a user', async () => {
|
||||
await guild.syncTask(task, leader);
|
||||
|
||||
let updatedLeader = await User.findOne({_id: leader._id});
|
||||
let tagIndex = findIndex(updatedLeader.tags, {id: guild._id});
|
||||
let newTag = updatedLeader.tags[tagIndex];
|
||||
|
||||
expect(newTag.id).to.equal(guild._id);
|
||||
expect(newTag.name).to.equal(guild.name);
|
||||
expect(newTag.group).to.equal(guild._id);
|
||||
});
|
||||
|
||||
it('create tags for a user when task is synced', async () => {
|
||||
await guild.syncTask(task, leader);
|
||||
|
||||
let updatedLeader = await User.findOne({_id: leader._id});
|
||||
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
|
||||
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
|
||||
@@ -96,35 +114,124 @@ describe('Group Task Methods', () => {
|
||||
expect(syncedTask.text).to.equal(task.text);
|
||||
});
|
||||
|
||||
it('syncs updated info for assigned task to all users', async () => {
|
||||
let newMember = new User({
|
||||
guilds: [guild._id],
|
||||
});
|
||||
await newMember.save();
|
||||
|
||||
it('syncs checklist items to an assigned user', async () => {
|
||||
await guild.syncTask(task, leader);
|
||||
await guild.syncTask(task, newMember);
|
||||
|
||||
let updatedTaskName = 'Update Task name';
|
||||
task.text = updatedTaskName;
|
||||
|
||||
await guild.updateTask(task);
|
||||
|
||||
let updatedLeader = await User.findOne({_id: leader._id});
|
||||
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
|
||||
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
|
||||
|
||||
let updatedMember = await User.findOne({_id: newMember._id});
|
||||
let updatedMemberTasks = await Tasks.Task.find({_id: { $in: updatedMember.tasksOrder[`${taskType}s`]}});
|
||||
let syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
|
||||
if (task.type !== 'daily' && task.type !== 'todo') return;
|
||||
|
||||
expect(task.group.assignedUsers).to.contain(leader._id);
|
||||
expect(syncedTask).to.exist;
|
||||
expect(syncedTask.text).to.equal(task.text);
|
||||
expect(syncedTask.checklist.length).to.equal(task.checklist.length);
|
||||
expect(syncedTask.checklist[0].text).to.equal(task.checklist[0].text);
|
||||
});
|
||||
|
||||
expect(task.group.assignedUsers).to.contain(newMember._id);
|
||||
expect(syncedMemberTask).to.exist;
|
||||
expect(syncedMemberTask.text).to.equal(task.text);
|
||||
describe('syncs updated info', async() => {
|
||||
let newMember;
|
||||
|
||||
beforeEach(async () => {
|
||||
newMember = new User({
|
||||
guilds: [guild._id],
|
||||
});
|
||||
await newMember.save();
|
||||
|
||||
await guild.syncTask(task, leader);
|
||||
await guild.syncTask(task, newMember);
|
||||
});
|
||||
|
||||
it('syncs updated info for assigned task to all users', async () => {
|
||||
let updatedTaskName = 'Update Task name';
|
||||
task.text = updatedTaskName;
|
||||
task.group.approval.required = true;
|
||||
|
||||
await guild.updateTask(task);
|
||||
|
||||
let updatedLeader = await User.findOne({_id: leader._id});
|
||||
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
|
||||
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
|
||||
|
||||
let updatedMember = await User.findOne({_id: newMember._id});
|
||||
let updatedMemberTasks = await Tasks.Task.find({_id: { $in: updatedMember.tasksOrder[`${taskType}s`]}});
|
||||
let syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
|
||||
|
||||
expect(task.group.assignedUsers).to.contain(leader._id);
|
||||
expect(syncedTask).to.exist;
|
||||
expect(syncedTask.text).to.equal(task.text);
|
||||
expect(syncedTask.group.approval.required).to.equal(true);
|
||||
|
||||
expect(task.group.assignedUsers).to.contain(newMember._id);
|
||||
expect(syncedMemberTask).to.exist;
|
||||
expect(syncedMemberTask.text).to.equal(task.text);
|
||||
expect(syncedMemberTask.group.approval.required).to.equal(true);
|
||||
});
|
||||
|
||||
it('syncs a new checklist item to all assigned users', async () => {
|
||||
if (task.type !== 'daily' && task.type !== 'todo') return;
|
||||
|
||||
let newCheckListItem = {
|
||||
text: 'Checklist Item 1',
|
||||
completed: false,
|
||||
};
|
||||
|
||||
task.checklist.push(newCheckListItem);
|
||||
|
||||
await guild.updateTask(task, {newCheckListItem});
|
||||
|
||||
let updatedLeader = await User.findOne({_id: leader._id});
|
||||
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
|
||||
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
|
||||
|
||||
let updatedMember = await User.findOne({_id: newMember._id});
|
||||
let updatedMemberTasks = await Tasks.Task.find({_id: { $in: updatedMember.tasksOrder[`${taskType}s`]}});
|
||||
let syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
|
||||
|
||||
expect(syncedTask.checklist.length).to.equal(task.checklist.length);
|
||||
expect(syncedTask.checklist[1].text).to.equal(task.checklist[1].text);
|
||||
expect(syncedMemberTask.checklist.length).to.equal(task.checklist.length);
|
||||
expect(syncedMemberTask.checklist[1].text).to.equal(task.checklist[1].text);
|
||||
});
|
||||
|
||||
it('syncs updated info for checklist in assigned task to all users when flag is passed', async () => {
|
||||
if (task.type !== 'daily' && task.type !== 'todo') return;
|
||||
|
||||
let updateCheckListText = 'Updated checklist item';
|
||||
if (task.checklist) {
|
||||
task.checklist[0].text = updateCheckListText;
|
||||
}
|
||||
|
||||
await guild.updateTask(task, {updateCheckListItems: [task.checklist[0]]});
|
||||
|
||||
let updatedLeader = await User.findOne({_id: leader._id});
|
||||
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
|
||||
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
|
||||
|
||||
let updatedMember = await User.findOne({_id: newMember._id});
|
||||
let updatedMemberTasks = await Tasks.Task.find({_id: { $in: updatedMember.tasksOrder[`${taskType}s`]}});
|
||||
let syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
|
||||
|
||||
expect(syncedTask.checklist.length).to.equal(task.checklist.length);
|
||||
expect(syncedTask.checklist[0].text).to.equal(updateCheckListText);
|
||||
expect(syncedMemberTask.checklist.length).to.equal(task.checklist.length);
|
||||
expect(syncedMemberTask.checklist[0].text).to.equal(updateCheckListText);
|
||||
});
|
||||
|
||||
it('removes a checklist item in assigned task to all users when flag is passed with checklist id', async () => {
|
||||
if (task.type !== 'daily' && task.type !== 'todo') return;
|
||||
|
||||
await guild.updateTask(task, {removedCheckListItemId: task.checklist[0].id});
|
||||
|
||||
let updatedLeader = await User.findOne({_id: leader._id});
|
||||
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
|
||||
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
|
||||
|
||||
let updatedMember = await User.findOne({_id: newMember._id});
|
||||
let updatedMemberTasks = await Tasks.Task.find({_id: { $in: updatedMember.tasksOrder[`${taskType}s`]}});
|
||||
let syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
|
||||
|
||||
expect(syncedTask.checklist.length).to.equal(0);
|
||||
expect(syncedMemberTask.checklist.length).to.equal(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('removes an assigned task and unlinks assignees', async () => {
|
||||
|
||||
@@ -81,7 +81,7 @@ describe('Task Model', () => {
|
||||
user = new User();
|
||||
await user.save();
|
||||
|
||||
taskWithAlias = new Tasks.todo({ // eslint-disable-line babel/new-cap
|
||||
taskWithAlias = new Tasks.todo({ // eslint-disable-line new-cap
|
||||
text: 'some text',
|
||||
alias: 'short-name',
|
||||
userId: user.id,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user